@miller-tech/uap 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +139 -2
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/hooks.d.ts +1 -1
- package/dist/cli/hooks.d.ts.map +1 -1
- package/dist/cli/hooks.js +152 -49
- package/dist/cli/hooks.js.map +1 -1
- package/dist/cli/setup-wizard.d.ts.map +1 -1
- package/dist/cli/setup-wizard.js +4 -0
- package/dist/cli/setup-wizard.js.map +1 -1
- package/dist/models/router.d.ts.map +1 -1
- package/dist/models/router.js +7 -3
- package/dist/models/router.js.map +1 -1
- package/dist/models/types.js +4 -4
- package/dist/models/types.js.map +1 -1
- package/dist/policies/enforced-tool-router.d.ts +2 -2
- package/dist/policies/enforced-tool-router.d.ts.map +1 -1
- package/dist/policies/enforced-tool-router.js +4 -4
- package/dist/policies/enforced-tool-router.js.map +1 -1
- package/docs/getting-started/INTEGRATION.md +193 -14
- package/docs/opencode-integration-guide.md +740 -0
- package/docs/opencode-integration-quickref.md +180 -0
- package/package.json +1 -1
- package/templates/hooks/session-start.sh +1 -8
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
# OpenCode Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to add new integrations to OpenCode based on analysis of the Universal Agent Protocol (UAP) codebase.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [OpenCode Plugin Architecture](#opencode-plugin-architecture)
|
|
8
|
+
2. [Plugin Structure and Registration](#plugin-structure-and-registration)
|
|
9
|
+
3. [Defining Custom Tools](#defining-custom-tools)
|
|
10
|
+
4. [Hook System](#hook-system)
|
|
11
|
+
5. [Integration Patterns](#integration-patterns)
|
|
12
|
+
6. [Example: Creating a New Integration](#example-creating-a-new-integration)
|
|
13
|
+
7. [Best Practices](#best-practices)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## OpenCode Plugin Architecture
|
|
18
|
+
|
|
19
|
+
OpenCode uses a **TypeScript plugin system** via the `@opencode-ai/plugin` package (v1.2.x). Plugins are TypeScript modules that extend agent capabilities through:
|
|
20
|
+
|
|
21
|
+
- **Custom Tools**: Define new tools that the LLM can call
|
|
22
|
+
- **Event Hooks**: Intercept and modify agent behavior at specific points
|
|
23
|
+
- **Middleware**: Transform messages and context before/after processing
|
|
24
|
+
|
|
25
|
+
### Plugin Location
|
|
26
|
+
|
|
27
|
+
Plugins are stored in:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
.opencode/plugin/
|
|
31
|
+
├── uap-commands.ts # UAP CLI commands as tools
|
|
32
|
+
├── uap-skills.ts # Skill loading system
|
|
33
|
+
├── uap-droids.ts # Specialized agent droids
|
|
34
|
+
├── uap-pattern-rag.ts # Pattern retrieval via RAG
|
|
35
|
+
├── uap-task-completion.ts # Task completion tracking
|
|
36
|
+
├── uap-session-hooks.ts # Session lifecycle hooks
|
|
37
|
+
└── uap-enforce.ts # Loop detection and enforcement
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Dependencies
|
|
41
|
+
|
|
42
|
+
The plugin system requires:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@opencode-ai/plugin": "1.2.16"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Plugin Structure and Registration
|
|
55
|
+
|
|
56
|
+
### Basic Plugin Template
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import type { Plugin } from '@opencode-ai/plugin';
|
|
60
|
+
import { tool } from '@opencode-ai/plugin';
|
|
61
|
+
|
|
62
|
+
export const MyPlugin: Plugin = async ({ $, directory, client }) => {
|
|
63
|
+
return {
|
|
64
|
+
// Tool definitions
|
|
65
|
+
tool: {
|
|
66
|
+
my_custom_tool: tool({
|
|
67
|
+
description: 'What this tool does',
|
|
68
|
+
args: {
|
|
69
|
+
param1: tool.schema.string().describe('First parameter'),
|
|
70
|
+
},
|
|
71
|
+
async execute({ param1 }) {
|
|
72
|
+
// Implementation using shell commands or other tools
|
|
73
|
+
const result = await $`command ${param1}`.quiet();
|
|
74
|
+
return result.stdout.toString().trim();
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Event hooks
|
|
80
|
+
event: async ({ event }) => {
|
|
81
|
+
if (event.type === 'session.created') {
|
|
82
|
+
console.log('Session started');
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Middleware for message transformation
|
|
87
|
+
middleware: async (input, next) => {
|
|
88
|
+
// Modify input before processing
|
|
89
|
+
const result = await next(input);
|
|
90
|
+
// Optionally modify output
|
|
91
|
+
return result;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Available Plugin Context
|
|
98
|
+
|
|
99
|
+
| Parameter | Type | Description |
|
|
100
|
+
| ----------- | ------------------- | ----------------------------------------------- |
|
|
101
|
+
| `$` | Template string tag | Shell command execution (similar to $ in shell) |
|
|
102
|
+
| `directory` | string | Project directory path |
|
|
103
|
+
| `client` | OpenCode client | Direct access to OpenCode client API |
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Defining Custom Tools
|
|
108
|
+
|
|
109
|
+
### Tool Schema Definition
|
|
110
|
+
|
|
111
|
+
Tools are defined using the `tool()` function with a schema:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { tool } from '@opencode-ai/plugin';
|
|
115
|
+
|
|
116
|
+
const myTool = tool({
|
|
117
|
+
description: 'Description visible to the LLM',
|
|
118
|
+
args: {
|
|
119
|
+
// Required string parameter
|
|
120
|
+
name: tool.schema.string().describe('User name'),
|
|
121
|
+
|
|
122
|
+
// Optional number with constraints
|
|
123
|
+
age: tool.schema.number().min(0).max(150).default(18).describe('User age'),
|
|
124
|
+
|
|
125
|
+
// Enum parameter
|
|
126
|
+
mode: tool.schema.enum(['read', 'write', 'execute']).default('read').describe('Operation mode'),
|
|
127
|
+
|
|
128
|
+
// Array parameter
|
|
129
|
+
items: tool.schema.array().of(tool.schema.string()).describe('List of items'),
|
|
130
|
+
},
|
|
131
|
+
async execute(args) {
|
|
132
|
+
// Tool implementation
|
|
133
|
+
const { name, age = 18, mode = 'read', items = [] } = args;
|
|
134
|
+
|
|
135
|
+
// Use shell commands via $ template tag
|
|
136
|
+
const result = await $`echo "Processing ${name} in ${mode} mode"`;
|
|
137
|
+
|
|
138
|
+
return result.stdout.toString().trim();
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Tool Registration
|
|
144
|
+
|
|
145
|
+
Tools are registered in the `tool` property of the plugin return value:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
149
|
+
return {
|
|
150
|
+
tool: {
|
|
151
|
+
// Single tool
|
|
152
|
+
my_tool: tool({...}),
|
|
153
|
+
|
|
154
|
+
// Multiple tools
|
|
155
|
+
another_tool: tool({...}),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Tool Naming Convention
|
|
162
|
+
|
|
163
|
+
- Use **snake_case** for tool names (e.g., `my_custom_tool`)
|
|
164
|
+
- Prefix with domain when relevant (e.g., `uap_memory_query`, `git_worktree_create`)
|
|
165
|
+
- Tools become accessible to the LLM as `/tool_name` commands
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Hook System
|
|
170
|
+
|
|
171
|
+
OpenCode provides several hook points for customizing agent behavior:
|
|
172
|
+
|
|
173
|
+
### Event Hooks
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
177
|
+
return {
|
|
178
|
+
event: async ({ event }) => {
|
|
179
|
+
if (event.type === 'session.created') {
|
|
180
|
+
// Session initialization
|
|
181
|
+
console.log('New session started');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (event.type === 'session.compacting') {
|
|
185
|
+
// Before context compression
|
|
186
|
+
await $`echo "Saving critical state before compaction"`;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Available Events
|
|
194
|
+
|
|
195
|
+
| Event Type | When Fired | Use Case |
|
|
196
|
+
| -------------------- | -------------------------- | --------------------------------- |
|
|
197
|
+
| `session.created` | New session starts | Initialize state, load context |
|
|
198
|
+
| `session.compacting` | Before context compression | Preserve important information |
|
|
199
|
+
| `message.created` | User message received | Pre-process input, inject context |
|
|
200
|
+
|
|
201
|
+
### Tool Execution Hooks
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
205
|
+
return {
|
|
206
|
+
'tool.execute.before': async (input, output) => {
|
|
207
|
+
// Before tool execution - can modify args or block
|
|
208
|
+
if (input.tool === 'bash') {
|
|
209
|
+
console.log(`Executing: ${output.args.command}`);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
'tool.execute.after': async (input, _output) => {
|
|
214
|
+
// After tool execution - can log, record, or modify output
|
|
215
|
+
const result = _output.output?.toString();
|
|
216
|
+
await $`echo "Tool ${input.tool} completed" >> /tmp/tool_log.txt`;
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Tool Definition Hooks
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
226
|
+
return {
|
|
227
|
+
'tool.definition': async (_input, output) => {
|
|
228
|
+
// Modify tool descriptions before they reach the LLM
|
|
229
|
+
if (output.description) {
|
|
230
|
+
output.description += '\n\n[Note: This tool requires admin privileges]';
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### System Transform Hooks
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
241
|
+
return {
|
|
242
|
+
'experimental.chat.system.transform': async (_input, output) => {
|
|
243
|
+
// Inject system context into the conversation
|
|
244
|
+
const context = await getRelevantContext();
|
|
245
|
+
output.system.push(context);
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Middleware
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
255
|
+
return {
|
|
256
|
+
middleware: async (input, next) => {
|
|
257
|
+
// Transform input messages before processing
|
|
258
|
+
const lastMessage = input.messages?.[input.messages.length - 1];
|
|
259
|
+
|
|
260
|
+
if (lastMessage?.role === 'user') {
|
|
261
|
+
// Add pre-processing context
|
|
262
|
+
const taskContext = await extractTaskContext(lastMessage.content);
|
|
263
|
+
input.messages.splice(input.messages.length - 1, 0, {
|
|
264
|
+
role: 'system',
|
|
265
|
+
content: `<task-context>${taskContext}</task-context>`,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Call next middleware
|
|
270
|
+
const result = await next(input);
|
|
271
|
+
|
|
272
|
+
// Post-process output if needed
|
|
273
|
+
return result;
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Integration Patterns
|
|
282
|
+
|
|
283
|
+
### Pattern 1: CLI Command Wrapper
|
|
284
|
+
|
|
285
|
+
Wrap existing CLI tools as agent tools:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
289
|
+
return {
|
|
290
|
+
tool: {
|
|
291
|
+
my_cli_wrapper: tool({
|
|
292
|
+
description: 'Execute my-cli command with automatic error handling',
|
|
293
|
+
args: {
|
|
294
|
+
command: tool.schema.string().describe('CLI subcommand'),
|
|
295
|
+
args: tool.schema.array().of(tool.schema.string()).optional(),
|
|
296
|
+
},
|
|
297
|
+
async execute({ command, args = [] }) {
|
|
298
|
+
const result = await $`my-cli ${command} ${args.join(' ')}`.nothrow();
|
|
299
|
+
|
|
300
|
+
if (result.exitCode !== 0) {
|
|
301
|
+
return `Error: ${result.stderr.toString()}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return result.stdout.toString().trim();
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Pattern 2: File System Operations
|
|
313
|
+
|
|
314
|
+
Create file-based tools with validation:
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
import { readFile, writeFile, readdir } from 'fs/promises';
|
|
318
|
+
|
|
319
|
+
export const MyPlugin: Plugin = async ({ directory }) => {
|
|
320
|
+
const projectDir = directory || '.';
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
tool: {
|
|
324
|
+
project_file_read: tool({
|
|
325
|
+
description: 'Read a file from the project with path validation',
|
|
326
|
+
args: {
|
|
327
|
+
path: tool.schema.string().describe('Relative file path'),
|
|
328
|
+
},
|
|
329
|
+
async execute({ path }) {
|
|
330
|
+
const fullPath = path.startsWith('/') ? path : join(projectDir, path);
|
|
331
|
+
|
|
332
|
+
// Security: prevent directory traversal
|
|
333
|
+
if (path.includes('..') || !fullPath.startsWith(projectDir)) {
|
|
334
|
+
return 'Error: Access denied';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
339
|
+
return content;
|
|
340
|
+
} catch (err) {
|
|
341
|
+
return `File not found: ${path}`;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
}),
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Pattern 3: Memory Integration
|
|
351
|
+
|
|
352
|
+
Integrate with persistent memory systems:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import { exec } from 'child_process';
|
|
356
|
+
import { promisify } from 'util';
|
|
357
|
+
|
|
358
|
+
const execAsync = promisify(exec);
|
|
359
|
+
|
|
360
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
361
|
+
return {
|
|
362
|
+
tool: {
|
|
363
|
+
memory_query: tool({
|
|
364
|
+
description: 'Query persistent memory for relevant context',
|
|
365
|
+
args: {
|
|
366
|
+
query: tool.schema.string().describe('Search query'),
|
|
367
|
+
limit: tool.schema.number().default(5).describe('Max results'),
|
|
368
|
+
},
|
|
369
|
+
async execute({ query, limit }) {
|
|
370
|
+
const result =
|
|
371
|
+
await $`python3 ./agents/scripts/query_memory.py "${query}" --limit ${limit}`.quiet();
|
|
372
|
+
return result.stdout.toString().trim() || 'No memories found.';
|
|
373
|
+
},
|
|
374
|
+
}),
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Pattern 4: External API Integration
|
|
381
|
+
|
|
382
|
+
Connect to external services:
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
export const MyPlugin: Plugin = async ({ client }) => {
|
|
386
|
+
return {
|
|
387
|
+
tool: {
|
|
388
|
+
github_issue_create: tool({
|
|
389
|
+
description: 'Create a GitHub issue via the API',
|
|
390
|
+
args: {
|
|
391
|
+
title: tool.schema.string().describe('Issue title'),
|
|
392
|
+
body: tool.schema.string().describe('Issue description'),
|
|
393
|
+
labels: tool.schema.array().of(tool.schema.string()).optional(),
|
|
394
|
+
},
|
|
395
|
+
async execute({ title, body, labels = [] }) {
|
|
396
|
+
const response = await client.fetch(
|
|
397
|
+
'https://api.github.com/repos/{owner}/{repo}/issues',
|
|
398
|
+
{
|
|
399
|
+
method: 'POST',
|
|
400
|
+
headers: { Authorization: `token ${process.env.GITHUB_TOKEN}` },
|
|
401
|
+
body: JSON.stringify({ title, body, labels }),
|
|
402
|
+
}
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
const data = await response.json();
|
|
406
|
+
return `Issue created: ${data.html_url}`;
|
|
407
|
+
},
|
|
408
|
+
}),
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Pattern 5: Context Injection (RAG)
|
|
415
|
+
|
|
416
|
+
Inject relevant context on-demand:
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
420
|
+
return {
|
|
421
|
+
middleware: async (input, next) => {
|
|
422
|
+
const lastMessage = input.messages?.[input.messages.length - 1];
|
|
423
|
+
|
|
424
|
+
if (lastMessage?.role === 'user') {
|
|
425
|
+
const taskText =
|
|
426
|
+
typeof lastMessage.content === 'string'
|
|
427
|
+
? lastMessage.content
|
|
428
|
+
: JSON.stringify(lastMessage.content);
|
|
429
|
+
|
|
430
|
+
// Query for relevant patterns/docs
|
|
431
|
+
if (taskText.length > 50) {
|
|
432
|
+
const result =
|
|
433
|
+
await $`python3 ./scripts/query_patterns.py "${taskText.slice(0, 200)}" --top 3`.quiet();
|
|
434
|
+
const context = result.stdout.toString().trim();
|
|
435
|
+
|
|
436
|
+
if (context) {
|
|
437
|
+
input.messages.splice(input.messages.length - 1, 0, {
|
|
438
|
+
role: 'system',
|
|
439
|
+
content: `<relevant-context>\n${context}\n</relevant-context>`,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return next(input);
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Example: Creating a New Integration
|
|
454
|
+
|
|
455
|
+
Let's create a complete integration example: a **Database Migration Tool** plugin.
|
|
456
|
+
|
|
457
|
+
### Step 1: Create the Plugin File
|
|
458
|
+
|
|
459
|
+
Create `.opencode/plugin/db-migrations.ts`:
|
|
460
|
+
|
|
461
|
+
```typescript
|
|
462
|
+
import type { Plugin } from '@opencode-ai/plugin';
|
|
463
|
+
import { tool } from '@opencode-ai/plugin';
|
|
464
|
+
import { readFile, writeFile, readdir } from 'fs/promises';
|
|
465
|
+
import { join } from 'path';
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Database Migration Plugin
|
|
469
|
+
*
|
|
470
|
+
* Provides tools for managing database migrations:
|
|
471
|
+
* - db_migration_create: Create new migration files
|
|
472
|
+
* - db_migration_status: Check migration status
|
|
473
|
+
* - db_migration_apply: Apply pending migrations
|
|
474
|
+
* - db_migration_history: View migration history
|
|
475
|
+
*/
|
|
476
|
+
|
|
477
|
+
export const DBMigrationsPlugin: Plugin = async ({ $, directory }) => {
|
|
478
|
+
const projectDir = directory || '.';
|
|
479
|
+
const migrationsDir = join(projectDir, 'migrations');
|
|
480
|
+
|
|
481
|
+
// Track applied migrations
|
|
482
|
+
let migrationCache: string[] = [];
|
|
483
|
+
|
|
484
|
+
async function loadMigrationStatus() {
|
|
485
|
+
if (migrationCache.length === 0) {
|
|
486
|
+
try {
|
|
487
|
+
const result =
|
|
488
|
+
await $`sqlite3 ${projectDir}/db.sqlite3 "SELECT name FROM django_migrations ORDER BY id;"`.quiet();
|
|
489
|
+
migrationCache = result.stdout.toString().trim().split('\n').filter(Boolean);
|
|
490
|
+
} catch {
|
|
491
|
+
migrationCache = [];
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return migrationCache;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function getMigrationFiles() {
|
|
498
|
+
try {
|
|
499
|
+
const files = await readdir(migrationsDir);
|
|
500
|
+
return files.filter((f) => f.endsWith('.sql') || f.endsWith('.py'));
|
|
501
|
+
} catch {
|
|
502
|
+
return [];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
tool: {
|
|
508
|
+
db_migration_create: tool({
|
|
509
|
+
description: 'Create a new database migration file with timestamp prefix',
|
|
510
|
+
args: {
|
|
511
|
+
name: tool.schema.string().describe('Migration name (will be slugified)'),
|
|
512
|
+
},
|
|
513
|
+
async execute({ name }) {
|
|
514
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
515
|
+
const slug = name
|
|
516
|
+
.toLowerCase()
|
|
517
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
518
|
+
.replace(/(^-|-$)/g, '');
|
|
519
|
+
const filename = `${timestamp}_${slug}.sql`;
|
|
520
|
+
|
|
521
|
+
const migrationContent = `-- Migration: ${name}\n-- Created: ${new Date().toISOString()}\n\n-- Add your SQL here\n`;
|
|
522
|
+
|
|
523
|
+
await writeFile(join(migrationsDir, filename), migrationContent);
|
|
524
|
+
|
|
525
|
+
return `Created migration: ${filename}`;
|
|
526
|
+
},
|
|
527
|
+
}),
|
|
528
|
+
|
|
529
|
+
db_migration_status: tool({
|
|
530
|
+
description: 'Check which migrations have been applied and which are pending',
|
|
531
|
+
args: {},
|
|
532
|
+
async execute() {
|
|
533
|
+
const [applied, pending] = await Promise.all([
|
|
534
|
+
loadMigrationStatus(),
|
|
535
|
+
getMigrationFiles(),
|
|
536
|
+
]);
|
|
537
|
+
|
|
538
|
+
const pendingMigrations = pending
|
|
539
|
+
.filter((f) => !f.replace('.sql', '').split('_')[0].includes('initial'))
|
|
540
|
+
.filter((f) => {
|
|
541
|
+
const timestamp = f.split('_')[0];
|
|
542
|
+
return !applied.some((m) => m.includes(timestamp));
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
let output = '### Applied Migrations\n';
|
|
546
|
+
applied.forEach((m) => (output += `- ${m}\n`));
|
|
547
|
+
|
|
548
|
+
if (pendingMigrations.length > 0) {
|
|
549
|
+
output += '\n### Pending Migrations\n';
|
|
550
|
+
pendingMigrations.forEach((f) => (output += `- ${f}\n`));
|
|
551
|
+
} else {
|
|
552
|
+
output += '\n✅ All migrations applied!';
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return output;
|
|
556
|
+
},
|
|
557
|
+
}),
|
|
558
|
+
|
|
559
|
+
db_migration_apply: tool({
|
|
560
|
+
description: 'Apply all pending database migrations',
|
|
561
|
+
args: {
|
|
562
|
+
migration: tool.schema.string().optional().describe('Specific migration to apply'),
|
|
563
|
+
},
|
|
564
|
+
async execute({ migration }) {
|
|
565
|
+
if (migration) {
|
|
566
|
+
// Apply specific migration
|
|
567
|
+
const result = await $`python3 manage.py migrate app_name ${migration}`.nothrow();
|
|
568
|
+
return result.stdout.toString() + result.stderr.toString();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Apply all pending
|
|
572
|
+
const result = await $`python3 manage.py migrate`.nothrow();
|
|
573
|
+
return result.stdout.toString() + result.stderr.toString();
|
|
574
|
+
},
|
|
575
|
+
}),
|
|
576
|
+
|
|
577
|
+
db_migration_history: tool({
|
|
578
|
+
description: 'View migration history with timestamps',
|
|
579
|
+
args: {
|
|
580
|
+
limit: tool.schema.number().default(10).describe('Number of recent migrations'),
|
|
581
|
+
},
|
|
582
|
+
async execute({ limit }) {
|
|
583
|
+
const result =
|
|
584
|
+
await $`sqlite3 ${projectDir}/db.sqlite3 "SELECT name, datetime(first_applied) FROM django_migrations ORDER BY id DESC LIMIT ${limit};"`.quiet();
|
|
585
|
+
return result.stdout.toString().trim() || 'No migration history found.';
|
|
586
|
+
},
|
|
587
|
+
}),
|
|
588
|
+
},
|
|
589
|
+
};
|
|
590
|
+
};
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### Step 2: Install Dependencies
|
|
594
|
+
|
|
595
|
+
Ensure `@opencode-ai/plugin` is in your dependencies:
|
|
596
|
+
|
|
597
|
+
```bash
|
|
598
|
+
npm install @opencode-ai/plugin
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Step 3: Use the Plugin
|
|
602
|
+
|
|
603
|
+
The plugin will be automatically loaded by OpenCode when placed in `.opencode/plugin/`. The LLM can now use:
|
|
604
|
+
|
|
605
|
+
```bash
|
|
606
|
+
/db_migration_create --name add_user_email_index
|
|
607
|
+
/db_migration_status
|
|
608
|
+
/db_migration_apply
|
|
609
|
+
/db_migration_history --limit 20
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Best Practices
|
|
615
|
+
|
|
616
|
+
### 1. Error Handling
|
|
617
|
+
|
|
618
|
+
Always handle errors gracefully and provide helpful messages:
|
|
619
|
+
|
|
620
|
+
```typescript
|
|
621
|
+
async execute({ param }) {
|
|
622
|
+
try {
|
|
623
|
+
const result = await $`command ${param}`.nothrow();
|
|
624
|
+
if (result.exitCode !== 0) {
|
|
625
|
+
return `Error: ${result.stderr.toString() || 'Command failed'}`;
|
|
626
|
+
}
|
|
627
|
+
return result.stdout.toString().trim();
|
|
628
|
+
} catch (error) {
|
|
629
|
+
return `Failed to execute command: ${error.message}`;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### 2. Security Considerations
|
|
635
|
+
|
|
636
|
+
- Validate all inputs to prevent command injection
|
|
637
|
+
- Use parameterized commands when possible
|
|
638
|
+
- Implement path traversal protection for file operations
|
|
639
|
+
- Sanitize output before returning to the LLM
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
// Secure file path handling
|
|
643
|
+
const safePath = path.normalize(userPath).replace(/^\.\./, '');
|
|
644
|
+
if (!safePath.startsWith(projectDir)) {
|
|
645
|
+
return 'Error: Access denied';
|
|
646
|
+
}
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
### 3. Performance Optimization
|
|
650
|
+
|
|
651
|
+
- Cache expensive operations when possible
|
|
652
|
+
- Use `--quiet` flag to reduce shell output
|
|
653
|
+
- Implement lazy loading for large datasets
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
let cache: string | null = null;
|
|
657
|
+
|
|
658
|
+
async getCachedData() {
|
|
659
|
+
if (!cache) {
|
|
660
|
+
cache = await fetchData();
|
|
661
|
+
}
|
|
662
|
+
return cache;
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### 4. Context Management
|
|
667
|
+
|
|
668
|
+
- Use `session.created` to initialize state
|
|
669
|
+
- Use `session.compacting` to preserve critical information
|
|
670
|
+
- Clear cache on session reset
|
|
671
|
+
|
|
672
|
+
```typescript
|
|
673
|
+
export const MyPlugin: Plugin = async ({ $ }) => {
|
|
674
|
+
let sessionState: any = null;
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
event: async ({ event }) => {
|
|
678
|
+
if (event.type === 'session.created') {
|
|
679
|
+
sessionState = await initializeState();
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
'experimental.session.compacting': async (_input, output) => {
|
|
683
|
+
// Save important state before compaction
|
|
684
|
+
output.context.push(`<saved-state>${JSON.stringify(sessionState)}</saved-state>`);
|
|
685
|
+
},
|
|
686
|
+
};
|
|
687
|
+
};
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### 5. Testing Your Plugin
|
|
691
|
+
|
|
692
|
+
```bash
|
|
693
|
+
# Test plugin loading
|
|
694
|
+
opencode --help
|
|
695
|
+
|
|
696
|
+
# Check if tools are registered
|
|
697
|
+
opencode run "List available tools" # Should show your new tools
|
|
698
|
+
|
|
699
|
+
# Manual testing
|
|
700
|
+
cd .opencode/plugin && npx tsc --noEmit # Type check
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## Troubleshooting
|
|
706
|
+
|
|
707
|
+
### Plugin Not Loading
|
|
708
|
+
|
|
709
|
+
1. Check file location: must be in `.opencode/plugin/`
|
|
710
|
+
2. Verify TypeScript compilation: `npm run build`
|
|
711
|
+
3. Check plugin syntax: `node -c .opencode/plugin/your-plugin.ts`
|
|
712
|
+
4. Review OpenCode logs for errors
|
|
713
|
+
|
|
714
|
+
### Tool Not Appearing to LLM
|
|
715
|
+
|
|
716
|
+
1. Ensure tool description is clear and comprehensive
|
|
717
|
+
2. Check tool name follows snake_case convention
|
|
718
|
+
3. Verify tool schema has all required fields
|
|
719
|
+
4. Restart OpenCode session after plugin changes
|
|
720
|
+
|
|
721
|
+
### Performance Issues
|
|
722
|
+
|
|
723
|
+
1. Add caching for expensive operations
|
|
724
|
+
2. Use `--quiet` flag on shell commands
|
|
725
|
+
3. Implement pagination for large results
|
|
726
|
+
4. Consider async loading for heavy computations
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## References
|
|
731
|
+
|
|
732
|
+
- **OpenCode Plugin API**: `@opencode-ai/plugin` package documentation
|
|
733
|
+
- **UAP Implementation**: See `.opencode/plugin/` in this repository for examples
|
|
734
|
+
- **Tool Schema Reference**: Check `uap-commands.ts` for tool definition patterns
|
|
735
|
+
- **Hook Examples**: See `uap-session-hooks.ts` and `uap-pattern-rag.ts`
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
**Last Updated:** 2026-03-17
|
|
740
|
+
**Version:** 1.0.0
|