trying to integrate chainlink into opencode
at main 462 lines 9.7 kB view raw view rendered
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/)