trying to integrate chainlink into opencode
1# OpenCode Plugin Architecture
2
3Documentation for agents implementing OpenCode plugins. Based on analysis of [kdco/opencode-workspace](https://github.com/kdcokenny/opencode-workspace).
4
5## Quick Reference
6
7```typescript
8import { type Plugin, type Event, tool } from "@opencode-ai/plugin";
9
10export const MyPlugin: Plugin = async ({ $, client, directory }) => {
11 return {
12 tool: {
13 myTool: tool({
14 description: "What this tool does",
15 args: {
16 param: tool.schema.string().describe("Parameter description"),
17 },
18 async execute(args) {
19 // Tools return string output
20 return "result";
21 },
22 }),
23 },
24 "session.created": async ({ sessionID }: { sessionID: string }) => { ... },
25 event: async ({ event }: { event: Event }) => { ... },
26 };
27};
28```
29
30## Plugin Structure
31
32### Plugin Function Signature
33
34```typescript
35export const MyPlugin: Plugin = async (ctx) => {
36 const { $, client, directory, worktree } = ctx;
37 // ctx includes all plugin context
38};
39```
40
41**Context properties:**
42
43- `$`: Bun shell API for executing commands
44- `client`: OpenCode SDK client for API interactions
45- `directory`: Current working directory
46- `worktree`: Git worktree path
47
48### Tool Definition
49
50```typescript
51tool({
52 description: "Human-readable description of the tool",
53 args: {
54 paramName: tool.schema
55 .string() // string, number, boolean
56 .describe("What this parameter means"),
57 optionalParam: tool.schema
58 .string()
59 .optional()
60 .describe("Optional parameter"),
61 enumParam: tool.schema
62 .enum(["option1", "option2"])
63 .describe("Enumerated value"),
64 recordParam: tool.schema
65 .record(tool.schema.string(), tool.schema.unknown())
66 .optional()
67 .describe("Key-value pairs"),
68 },
69 async execute(args) {
70 const { paramName, optionalParam } = args;
71 return "tool output as string";
72 },
73});
74```
75
76**Schema types:**
77
78- `tool.schema.string()`
79- `tool.schema.number()`
80- `tool.schema.boolean()`
81- `tool.schema.enum(["a", "b"])`
82- `tool.schema.record(keyType, valueType)`
83- `.optional()`
84- `.describe()`
85- `.default(value)`
86
87### Returning Values from Tools
88
89Tools must return a **string**. For structured data, return JSON:
90
91```typescript
92async execute(args) {
93 const result = { status: "success", data: {...} };
94 return JSON.stringify(result);
95}
96```
97
98## Lifecycle Hooks
99
100### session.created
101
102Called when a new session starts. Use to inject context.
103
104```typescript
105"session.created": async ({ sessionID }: { sessionID: string }) => {
106 await client.session.prompt({
107 path: { id: sessionID },
108 body: {
109 noReply: true,
110 parts: [{ type: "text", text: "Injected context" }],
111 },
112 });
113}
114```
115
116### event
117
118All other events are dispatched through the generic `event` hook. Filter by `event.type`.
119
120```typescript
121event: async ({ event }: { event: Event }) => {
122 if (event.type !== "session.idle") return;
123 if (!event.sessionID) return;
124
125 // Handle session.idle
126};
127```
128
129**Common event types:**
130
131- `session.created`
132- `session.idle`
133- `session.compacted`
134- `session.deleted`
135- `session.diff`
136- `session.error`
137- `session.status`
138- `session.updated`
139- `tool.execute.before`
140- `tool.execute.after`
141- `chat.message`
142- `command.execute.before`
143
144## Client API
145
146### Logging
147
148```typescript
149// Structured logging
150await client.app
151 .log({
152 body: {
153 service: "my-plugin",
154 level: "debug" | "info" | "warn" | "error",
155 message: "log message",
156 },
157 })
158 .catch(() => {}); // Always handle errors
159```
160
161### Session Operations
162
163```typescript
164// Inject context without triggering AI response
165await client.session.prompt({
166 path: { id: sessionID },
167 body: {
168 noReply: true,
169 parts: [{ type: "text", text: "context" }],
170 },
171});
172
173// Fork a session
174const forked = await client.session.fork({
175 path: { id: sessionID },
176 body: {},
177});
178
179// Get session info
180const session = await client.session.get({
181 path: { id: sessionID },
182});
183
184// Delete a session
185await client.session.delete({ path: { id: sessionID } });
186```
187
188### Tool Context in Hooks
189
190Tool hooks receive a `toolCtx` parameter:
191
192```typescript
193async execute(args, toolCtx) {
194 // toolCtx includes:
195 // - sessionID: string | undefined
196 // - directory: string
197 // - worktree: string
198 const sessionID = toolCtx?.sessionID;
199}
200```
201
202## Shell Execution
203
204Use Bun's `$` template literal syntax:
205
206```typescript
207// Simple command
208const output = await $`echo hello`.text();
209
210// With arguments (properly escaped)
211const output = await $`command "${arg.replace(/"/g, '\\"')}"`.text();
212
213// Capture output
214const result = await $`ls -la`.text();
215
216// Error handling
217try {
218 await $`command`.text();
219} catch (error) {
220 // Command failed
221}
222```
223
224## TypeScript Patterns
225
226### Result Types
227
228```typescript
229type Result<T> =
230 | { ok: true; value: T }
231 | { ok: false; error: string };
232
233// Usage
234const result: Result<string> = ...;
235if (result.ok) {
236 console.log(result.value);
237} else {
238 console.error(result.error);
239}
240```
241
242### Zod Schema Validation
243
244Example from workspace-plugin:
245
246```typescript
247import { z } from "zod";
248
249const PlanSchema = z.object({
250 frontmatter: z.object({
251 status: z.enum(["not-started", "in-progress", "complete", "blocked"]),
252 phase: z.number().int().positive(),
253 }),
254 goal: z.string().min(10),
255});
256
257// Validate at boundary
258const result = PlanSchema.safeParse(candidate);
259if (!result.success) {
260 return `Error: ${result.error.message}`;
261}
262```
263
264### Error Handling
265
266```typescript
267// Type guard for Node.js errors
268function isNodeError(error: unknown): error is NodeJS.ErrnoException {
269 return error instanceof Error && "code" in error;
270}
271
272// Usage
273try {
274 await fs.readFile(path);
275} catch (error) {
276 if (isNodeError(error) && error.code === "ENOENT") {
277 return "File not found";
278 }
279 throw error; // Re-throw unexpected errors
280}
281```
282
283## Common Operations
284
285### Reading Files
286
287```typescript
288import * as fs from "node:fs/promises";
289
290async function readFile(filePath: string): Promise<string> {
291 try {
292 return await fs.readFile(filePath, "utf8");
293 } catch (error) {
294 if (isNodeError(error) && error.code === "ENOENT") {
295 return ""; // File doesn't exist
296 }
297 throw error;
298 }
299}
300```
301
302### Writing Files
303
304```typescript
305import * as fs from "node:fs/promises";
306
307async function writeFile(filePath: string, content: string): Promise<void> {
308 await fs.mkdir(path.dirname(filePath), { recursive: true });
309 await fs.writeFile(filePath, content, "utf8");
310}
311```
312
313### Path Handling
314
315```typescript
316import * as path from "node:path";
317
318// Join paths
319const fullPath = path.join(directory, "subdir", "file.txt");
320
321// Get directory name
322const dir = path.dirname(filePath);
323
324// Get base name
325const base = path.basename(filePath);
326
327// Get extension
328const ext = path.extname(filePath);
329```
330
331### Git Operations
332
333```typescript
334async function git(
335 args: string[],
336 cwd: string,
337): Promise<Result<string, string>> {
338 try {
339 const proc = Bun.spawn(["git", ...args], {
340 cwd,
341 stdout: "pipe",
342 stderr: "pipe",
343 });
344 const [stdout, stderr, exitCode] = await Promise.all([
345 new Response(proc.stdout).text(),
346 new Response(proc.stderr).text(),
347 proc.exited,
348 ]);
349 if (exitCode !== 0) {
350 return { ok: false, error: stderr.trim() };
351 }
352 return { ok: true, value: stdout.trim() };
353 } catch (error) {
354 return { ok: false, error: String(error) };
355 }
356}
357```
358
359## Best Practices
360
361### 1. Early Exit Pattern
362
363```typescript
364async function process(args: Args): Promise<Result> {
365 // Validate first
366 if (!args.required) {
367 return { ok: false, error: "Required parameter missing" };
368 }
369
370 // Guard against null/undefined
371 if (!value) return defaultValue;
372
373 // Happy path
374 return { ok: true, value: computed };
375}
376```
377
378### 2. Parse Don't Validate
379
380Extract data first, validate once:
381
382```typescript
383function extractMarkdownParts(content: string): RawParts {
384 // Extraction only, no validation
385 const match = content.match(/pattern/);
386 return { data: match?.[1] || null };
387}
388
389function parsePlan(content: string): ValidPlan {
390 const parts = extractMarkdownParts(content);
391 const result = schema.safeParse(parts);
392 if (!result.success) {
393 throw new Error("Invalid format");
394 }
395 return result.data;
396}
397```
398
399### 3. Fail Loud
400
401Provide actionable error messages:
402
403```typescript
404async function execute(args) {
405 const result = await riskyOperation();
406 if (!result.ok) {
407 return `❌ Operation failed: ${result.error}\n\nHint: Check that X is configured correctly.`;
408 }
409 return "✅ Success!";
410}
411```
412
413### 4. Log with Context
414
415```typescript
416async function log(level: "info" | "error", message: string) {
417 await client.app
418 .log({
419 body: {
420 service: "my-plugin",
421 level,
422 message: `${message} (session: ${sessionID})`,
423 },
424 })
425 .catch(() => {});
426}
427```
428
429### 5. Handle Promise Rejection
430
431Logging and async operations can reject:
432
433```typescript
434await client.app.log({...}).catch(() => {});
435
436// Or handle gracefully
437try {
438 await client.session.prompt({...});
439} catch (error) {
440 await log("error", `Failed: ${error}`);
441}
442```
443
444## File Structure
445
446```
447my-plugin/
448├── src/
449│ └── index.ts # Main plugin file
450├── package.json # Dependencies
451├── tsconfig.json # TypeScript config
452└── docs/
453 └── opencode-plugin-architecture.md # This file
454```
455
456## References
457
458- [OpenCode Plugins Documentation](https://opencode.ai/docs/plugins)
459- [OpenCode SDK](https://opencode.ai/docs/sdk)
460- [kdco/opencode-workspace](https://github.com/kdcokenny/opencode-workspace)
461- [Bun Shell API](https://bun.com/docs/runtime/shell)
462- [Zod Documentation](https://zod.dev/)