micode 0.6.0 → 0.7.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.
Files changed (84) hide show
  1. package/README.md +64 -331
  2. package/package.json +9 -14
  3. package/src/agents/artifact-searcher.ts +46 -0
  4. package/src/agents/brainstormer.ts +145 -0
  5. package/src/agents/codebase-analyzer.ts +75 -0
  6. package/src/agents/codebase-locator.ts +71 -0
  7. package/src/agents/commander.ts +138 -0
  8. package/src/agents/executor.ts +215 -0
  9. package/src/agents/implementer.ts +99 -0
  10. package/src/agents/index.ts +44 -0
  11. package/src/agents/ledger-creator.ts +113 -0
  12. package/src/agents/pattern-finder.ts +70 -0
  13. package/src/agents/planner.ts +230 -0
  14. package/src/agents/project-initializer.ts +264 -0
  15. package/src/agents/reviewer.ts +102 -0
  16. package/src/config-loader.ts +89 -0
  17. package/src/hooks/artifact-auto-index.ts +111 -0
  18. package/src/hooks/auto-clear-ledger.ts +230 -0
  19. package/src/hooks/auto-compact.ts +241 -0
  20. package/src/hooks/comment-checker.ts +120 -0
  21. package/src/hooks/context-injector.ts +163 -0
  22. package/src/hooks/context-window-monitor.ts +106 -0
  23. package/src/hooks/file-ops-tracker.ts +96 -0
  24. package/src/hooks/ledger-loader.ts +78 -0
  25. package/src/hooks/preemptive-compaction.ts +183 -0
  26. package/src/hooks/session-recovery.ts +258 -0
  27. package/src/hooks/token-aware-truncation.ts +189 -0
  28. package/src/index.ts +258 -0
  29. package/src/tools/artifact-index/index.ts +269 -0
  30. package/src/tools/artifact-index/schema.sql +44 -0
  31. package/src/tools/artifact-search.ts +49 -0
  32. package/src/tools/ast-grep/index.ts +189 -0
  33. package/src/tools/background-task/manager.ts +374 -0
  34. package/src/tools/background-task/tools.ts +145 -0
  35. package/src/tools/background-task/types.ts +68 -0
  36. package/src/tools/btca/index.ts +82 -0
  37. package/src/tools/look-at.ts +210 -0
  38. package/src/tools/pty/buffer.ts +49 -0
  39. package/src/tools/pty/index.ts +34 -0
  40. package/src/tools/pty/manager.ts +159 -0
  41. package/src/tools/pty/tools/kill.ts +68 -0
  42. package/src/tools/pty/tools/list.ts +55 -0
  43. package/src/tools/pty/tools/read.ts +152 -0
  44. package/src/tools/pty/tools/spawn.ts +78 -0
  45. package/src/tools/pty/tools/write.ts +97 -0
  46. package/src/tools/pty/types.ts +62 -0
  47. package/src/utils/model-limits.ts +36 -0
  48. package/dist/agents/artifact-searcher.d.ts +0 -2
  49. package/dist/agents/brainstormer.d.ts +0 -2
  50. package/dist/agents/codebase-analyzer.d.ts +0 -2
  51. package/dist/agents/codebase-locator.d.ts +0 -2
  52. package/dist/agents/commander.d.ts +0 -3
  53. package/dist/agents/executor.d.ts +0 -2
  54. package/dist/agents/implementer.d.ts +0 -2
  55. package/dist/agents/index.d.ts +0 -15
  56. package/dist/agents/ledger-creator.d.ts +0 -2
  57. package/dist/agents/pattern-finder.d.ts +0 -2
  58. package/dist/agents/planner.d.ts +0 -2
  59. package/dist/agents/project-initializer.d.ts +0 -2
  60. package/dist/agents/reviewer.d.ts +0 -2
  61. package/dist/config-loader.d.ts +0 -20
  62. package/dist/hooks/artifact-auto-index.d.ts +0 -19
  63. package/dist/hooks/auto-clear-ledger.d.ts +0 -11
  64. package/dist/hooks/auto-compact.d.ts +0 -9
  65. package/dist/hooks/comment-checker.d.ts +0 -9
  66. package/dist/hooks/context-injector.d.ts +0 -15
  67. package/dist/hooks/context-window-monitor.d.ts +0 -15
  68. package/dist/hooks/file-ops-tracker.d.ts +0 -26
  69. package/dist/hooks/ledger-loader.d.ts +0 -16
  70. package/dist/hooks/preemptive-compaction.d.ts +0 -9
  71. package/dist/hooks/session-recovery.d.ts +0 -9
  72. package/dist/hooks/token-aware-truncation.d.ts +0 -15
  73. package/dist/index.d.ts +0 -3
  74. package/dist/index.js +0 -16267
  75. package/dist/tools/artifact-index/index.d.ts +0 -38
  76. package/dist/tools/artifact-search.d.ts +0 -17
  77. package/dist/tools/ast-grep/index.d.ts +0 -88
  78. package/dist/tools/background-task/manager.d.ts +0 -27
  79. package/dist/tools/background-task/tools.d.ts +0 -41
  80. package/dist/tools/background-task/types.d.ts +0 -53
  81. package/dist/tools/btca/index.d.ts +0 -19
  82. package/dist/tools/look-at.d.ts +0 -11
  83. package/dist/utils/model-limits.d.ts +0 -7
  84. /package/{dist/tools/background-task/index.d.ts → src/tools/background-task/index.ts} +0 -0
@@ -0,0 +1,374 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ import type {
3
+ BackgroundTask,
4
+ BackgroundTaskInput,
5
+ SessionCreateResponse,
6
+ SessionStatusResponse,
7
+ SessionMessagesResponse,
8
+ } from "./types";
9
+
10
+ const POLL_INTERVAL_MS = 2000;
11
+ const TASK_TTL_MS = 60 * 60 * 1000; // 1 hour
12
+
13
+ function generateTaskId(): string {
14
+ const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
15
+ let result = "bg_";
16
+ for (let i = 0; i < 8; i++) {
17
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
18
+ }
19
+ return result;
20
+ }
21
+
22
+ function formatDuration(start: Date, end?: Date): string {
23
+ const ms = (end || new Date()).getTime() - start.getTime();
24
+ const seconds = Math.floor(ms / 1000);
25
+ const minutes = Math.floor(seconds / 60);
26
+
27
+ if (minutes > 0) {
28
+ return `${minutes}m ${seconds % 60}s`;
29
+ }
30
+ return `${seconds}s`;
31
+ }
32
+
33
+ export class BackgroundTaskManager {
34
+ private tasks: Map<string, BackgroundTask> = new Map();
35
+ private notifications: Map<string, BackgroundTask[]> = new Map();
36
+ private pollingInterval?: ReturnType<typeof setInterval>;
37
+ private ctx: PluginInput;
38
+
39
+ constructor(ctx: PluginInput) {
40
+ this.ctx = ctx;
41
+ }
42
+
43
+ async launch(input: BackgroundTaskInput): Promise<BackgroundTask> {
44
+ const taskId = generateTaskId();
45
+
46
+ // Create new session for background task
47
+ const sessionResp = await this.ctx.client.session.create({
48
+ body: {},
49
+ query: { directory: this.ctx.directory },
50
+ });
51
+
52
+ const sessionData = sessionResp as SessionCreateResponse;
53
+ const sessionID = sessionData.data?.id;
54
+
55
+ if (!sessionID) {
56
+ throw new Error("Failed to create background session");
57
+ }
58
+
59
+ const task: BackgroundTask = {
60
+ id: taskId,
61
+ sessionID,
62
+ parentSessionID: input.parentSessionID,
63
+ parentMessageID: input.parentMessageID,
64
+ description: input.description,
65
+ prompt: input.prompt,
66
+ agent: input.agent,
67
+ status: "running",
68
+ startedAt: new Date(),
69
+ progress: {
70
+ toolCalls: 0,
71
+ lastUpdate: new Date(),
72
+ },
73
+ };
74
+
75
+ this.tasks.set(taskId, task);
76
+
77
+ // Fire-and-forget prompt
78
+ this.ctx.client.session
79
+ .prompt({
80
+ path: { id: sessionID },
81
+ body: {
82
+ parts: [{ type: "text", text: input.prompt }],
83
+ agent: input.agent,
84
+ },
85
+ query: { directory: this.ctx.directory },
86
+ })
87
+ .catch((error) => {
88
+ console.error(`[background-task] Failed to prompt session ${sessionID}:`, error);
89
+ task.status = "error";
90
+ task.error = error instanceof Error ? error.message : String(error);
91
+ task.completedAt = new Date();
92
+ this.markForNotification(task);
93
+ });
94
+
95
+ // Start polling if not already
96
+ this.startPolling();
97
+
98
+ return task;
99
+ }
100
+
101
+ async cancel(taskId: string): Promise<boolean> {
102
+ const task = this.tasks.get(taskId);
103
+ if (!task || task.status !== "running") {
104
+ return false;
105
+ }
106
+
107
+ try {
108
+ // Fire-and-forget abort
109
+ this.ctx.client.session
110
+ .abort({
111
+ path: { id: task.sessionID },
112
+ query: { directory: this.ctx.directory },
113
+ })
114
+ .catch((error) => {
115
+ console.error(`[background-task] Failed to abort session ${task.sessionID}:`, error);
116
+ });
117
+
118
+ task.status = "cancelled";
119
+ task.completedAt = new Date();
120
+ this.markForNotification(task);
121
+ return true;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ async cancelAll(): Promise<number> {
128
+ let cancelled = 0;
129
+ for (const task of this.tasks.values()) {
130
+ if (task.status === "running") {
131
+ if (await this.cancel(task.id)) {
132
+ cancelled++;
133
+ }
134
+ }
135
+ }
136
+ return cancelled;
137
+ }
138
+
139
+ getTask(taskId: string): BackgroundTask | undefined {
140
+ return this.tasks.get(taskId);
141
+ }
142
+
143
+ getAllTasks(): BackgroundTask[] {
144
+ return Array.from(this.tasks.values());
145
+ }
146
+
147
+ /**
148
+ * Poll all running tasks and update their status.
149
+ * Called by background_list to ensure fresh status.
150
+ */
151
+ async refreshTaskStatus(): Promise<void> {
152
+ const runningTasks = this.getRunningTasks();
153
+ if (runningTasks.length === 0) return;
154
+
155
+ try {
156
+ // Get all session statuses in one call
157
+ const resp = await this.ctx.client.session.status({
158
+ query: { directory: this.ctx.directory },
159
+ });
160
+
161
+ const statusResp = resp as SessionStatusResponse;
162
+ const statusMap = statusResp.data || {};
163
+
164
+ for (const task of runningTasks) {
165
+ const sessionStatus = statusMap[task.sessionID];
166
+ const statusType = sessionStatus?.type;
167
+
168
+ if (statusType === "idle") {
169
+ task.status = "completed";
170
+ task.completedAt = new Date();
171
+ await this.getTaskResult(task.id);
172
+ }
173
+ // Store last known session status for debugging
174
+ (task as BackgroundTask & { _sessionStatus?: string })._sessionStatus = statusType;
175
+ }
176
+ } catch (error) {
177
+ console.error("[background-task] Failed to refresh task status:", error);
178
+ // Don't mark all tasks as error - they may still be running
179
+ }
180
+ }
181
+
182
+ getRunningTasks(): BackgroundTask[] {
183
+ return this.getAllTasks().filter((t) => t.status === "running");
184
+ }
185
+
186
+ async getTaskResult(taskId: string): Promise<string | undefined> {
187
+ const task = this.tasks.get(taskId);
188
+ if (!task || task.status === "running") {
189
+ return undefined;
190
+ }
191
+
192
+ if (task.result) {
193
+ return task.result;
194
+ }
195
+
196
+ // Fetch result from session messages
197
+ try {
198
+ const resp = await this.ctx.client.session.messages({
199
+ path: { id: task.sessionID },
200
+ query: { directory: this.ctx.directory },
201
+ });
202
+
203
+ const messagesResp = resp as SessionMessagesResponse;
204
+ const messages = messagesResp.data || [];
205
+ const lastAssistant = [...messages].reverse().find((m) => m.info?.role === "assistant");
206
+
207
+ if (lastAssistant) {
208
+ const textParts = lastAssistant.parts?.filter((p) => p.type === "text") || [];
209
+ task.result = textParts.map((p) => p.text || "").join("\n");
210
+ return task.result;
211
+ }
212
+ } catch (error) {
213
+ console.error(`[background-task] Failed to fetch result for task ${taskId}:`, error);
214
+ }
215
+
216
+ return undefined;
217
+ }
218
+
219
+ formatTaskStatus(task: BackgroundTask): string {
220
+ const duration = formatDuration(task.startedAt, task.completedAt);
221
+ const status = task.status.toUpperCase();
222
+
223
+ let output = `## Task: ${task.description}\n\n`;
224
+ output += `| Field | Value |\n|-------|-------|\n`;
225
+ output += `| ID | ${task.id} |\n`;
226
+ output += `| Status | ${status} |\n`;
227
+ output += `| Agent | ${task.agent} |\n`;
228
+ output += `| Duration | ${duration} |\n`;
229
+
230
+ if (task.progress) {
231
+ output += `| Tool Calls | ${task.progress.toolCalls} |\n`;
232
+ if (task.progress.lastTool) {
233
+ output += `| Last Tool | ${task.progress.lastTool} |\n`;
234
+ }
235
+ }
236
+
237
+ if (task.error) {
238
+ output += `\n### Error\n${task.error}\n`;
239
+ }
240
+
241
+ return output;
242
+ }
243
+
244
+ private startPolling(): void {
245
+ if (this.pollingInterval) return;
246
+
247
+ this.pollingInterval = setInterval(() => {
248
+ this.pollRunningTasks();
249
+ }, POLL_INTERVAL_MS);
250
+ }
251
+
252
+ private stopPolling(): void {
253
+ if (this.pollingInterval) {
254
+ clearInterval(this.pollingInterval);
255
+ this.pollingInterval = undefined;
256
+ }
257
+ }
258
+
259
+ private cleanupOldTasks(): void {
260
+ const now = Date.now();
261
+ for (const [taskId, task] of this.tasks) {
262
+ // Only cleanup completed/cancelled/error tasks
263
+ if (task.status === "running") continue;
264
+
265
+ const completedAt = task.completedAt?.getTime() || 0;
266
+ if (now - completedAt > TASK_TTL_MS) {
267
+ this.tasks.delete(taskId);
268
+ }
269
+ }
270
+ }
271
+
272
+ private async pollRunningTasks(): Promise<void> {
273
+ // Cleanup old completed tasks to prevent memory leak
274
+ this.cleanupOldTasks();
275
+
276
+ const runningTasks = this.getRunningTasks();
277
+
278
+ if (runningTasks.length === 0) {
279
+ this.stopPolling();
280
+ return;
281
+ }
282
+
283
+ try {
284
+ // Get all session statuses in one call
285
+ const resp = await this.ctx.client.session.status({
286
+ query: { directory: this.ctx.directory },
287
+ });
288
+
289
+ const statusResp = resp as SessionStatusResponse;
290
+ const statusMap = statusResp.data || {};
291
+
292
+ for (const task of runningTasks) {
293
+ const sessionStatus = statusMap[task.sessionID];
294
+ const statusType = sessionStatus?.type;
295
+
296
+ console.log(`[background-task] Poll ${task.id}: session=${task.sessionID} status=${statusType}`);
297
+
298
+ if (statusType === "idle") {
299
+ // Task completed
300
+ task.status = "completed";
301
+ task.completedAt = new Date();
302
+ await this.getTaskResult(task.id); // Cache the result
303
+ this.markForNotification(task);
304
+
305
+ await this.ctx.client.tui
306
+ .showToast({
307
+ body: {
308
+ title: "Background Task Complete",
309
+ message: task.description,
310
+ variant: "success",
311
+ duration: 5000,
312
+ },
313
+ })
314
+ .catch((error) => {
315
+ console.error(`[background-task] Failed to show toast for task ${task.id}:`, error);
316
+ });
317
+ }
318
+ }
319
+ } catch (error) {
320
+ console.error("[background-task] Failed to poll tasks:", error);
321
+ // Don't mark tasks as error - they may still be running, just can't check
322
+ }
323
+ }
324
+
325
+ private markForNotification(task: BackgroundTask): void {
326
+ const existing = this.notifications.get(task.parentSessionID) || [];
327
+ existing.push(task);
328
+ this.notifications.set(task.parentSessionID, existing);
329
+ }
330
+
331
+ getPendingNotifications(parentSessionID: string): BackgroundTask[] {
332
+ const notifications = this.notifications.get(parentSessionID) || [];
333
+ this.notifications.delete(parentSessionID);
334
+ return notifications;
335
+ }
336
+
337
+ handleEvent(event: { type: string; properties?: unknown }): void {
338
+ const props = event.properties as Record<string, unknown> | undefined;
339
+
340
+ // Track tool usage for progress
341
+ if (event.type === "message.part.updated") {
342
+ const info = props?.info as Record<string, unknown> | undefined;
343
+ const sessionID = info?.sessionID as string | undefined;
344
+ const partType = info?.type as string | undefined;
345
+
346
+ if (sessionID && partType === "tool_use") {
347
+ for (const task of this.tasks.values()) {
348
+ if (task.sessionID === sessionID && task.status === "running") {
349
+ if (!task.progress) {
350
+ task.progress = { toolCalls: 0, lastUpdate: new Date() };
351
+ }
352
+ task.progress.toolCalls++;
353
+ task.progress.lastTool = (info?.name as string) || undefined;
354
+ task.progress.lastUpdate = new Date();
355
+ break;
356
+ }
357
+ }
358
+ }
359
+ }
360
+
361
+ // Cleanup on session delete
362
+ if (event.type === "session.deleted") {
363
+ const sessionInfo = props?.info as { id?: string } | undefined;
364
+ if (sessionInfo?.id) {
365
+ for (const task of this.tasks.values()) {
366
+ if (task.sessionID === sessionInfo.id && task.status === "running") {
367
+ task.status = "cancelled";
368
+ task.completedAt = new Date();
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
374
+ }
@@ -0,0 +1,145 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import type { BackgroundTaskManager } from "./manager";
3
+
4
+ export function createBackgroundTaskTools(manager: BackgroundTaskManager) {
5
+ const background_task = tool({
6
+ description: `Launch a task to run in the background using a subagent.
7
+ The task runs independently while you continue working.
8
+ Use background_output to check progress or get results when complete.
9
+ Useful for: parallel research, concurrent implementation, async reviews.`,
10
+ args: {
11
+ description: tool.schema.string().describe("Short description of the task (shown in status)"),
12
+ prompt: tool.schema.string().describe("Full prompt/instructions for the background agent"),
13
+ agent: tool.schema.string().describe("Agent to use (e.g., 'codebase-analyzer', 'implementer')"),
14
+ },
15
+ execute: async (args, ctx) => {
16
+ try {
17
+ const task = await manager.launch({
18
+ description: args.description,
19
+ prompt: args.prompt,
20
+ agent: args.agent,
21
+ parentSessionID: ctx.sessionID,
22
+ parentMessageID: ctx.messageID || "",
23
+ });
24
+
25
+ return `## Background Task Launched
26
+
27
+ | Field | Value |
28
+ |-------|-------|
29
+ | Task ID | ${task.id} |
30
+ | Agent | ${args.agent} |
31
+ | Status | RUNNING |
32
+
33
+ Use \`background_output\` with task_id="${task.id}" to check progress or get results.`;
34
+ } catch (error) {
35
+ return `Failed to launch background task: ${error instanceof Error ? error.message : String(error)}`;
36
+ }
37
+ },
38
+ });
39
+
40
+ const background_output = tool({
41
+ description: `Get status or results from a background task.
42
+ Returns immediately with current status. Use background_list to poll for completion.`,
43
+ args: {
44
+ task_id: tool.schema.string().describe("ID of the task to check (e.g., 'bg_abc12345')"),
45
+ },
46
+ execute: async (args) => {
47
+ const { task_id } = args;
48
+
49
+ const task = manager.getTask(task_id);
50
+ if (!task) {
51
+ return `Task not found: ${task_id}`;
52
+ }
53
+
54
+ // Format status
55
+ let output = manager.formatTaskStatus(task);
56
+
57
+ // Include result if completed
58
+ if (task.status === "completed") {
59
+ const result = await manager.getTaskResult(task_id);
60
+ if (result) {
61
+ output += `\n### Result\n${result}\n`;
62
+ }
63
+ }
64
+
65
+ return output;
66
+ },
67
+ });
68
+
69
+ const background_cancel = tool({
70
+ description: `Cancel a running background task or all tasks.`,
71
+ args: {
72
+ task_id: tool.schema.string().optional().describe("ID of the task to cancel (omit to cancel all)"),
73
+ all: tool.schema.boolean().optional().describe("Cancel all running tasks (default: false)"),
74
+ },
75
+ execute: async (args) => {
76
+ const { task_id, all = false } = args;
77
+
78
+ if (all) {
79
+ const cancelled = await manager.cancelAll();
80
+ return `Cancelled ${cancelled} running task(s).`;
81
+ }
82
+
83
+ if (!task_id) {
84
+ return "Provide task_id or set all=true to cancel tasks.";
85
+ }
86
+
87
+ const success = await manager.cancel(task_id);
88
+ if (success) {
89
+ return `Task ${task_id} cancelled.`;
90
+ }
91
+
92
+ return `Could not cancel task ${task_id}. It may already be completed or not exist.`;
93
+ },
94
+ });
95
+
96
+ const background_list = tool({
97
+ description: `List all background tasks and their status.`,
98
+ args: {},
99
+ execute: async () => {
100
+ // Refresh status of running tasks before returning
101
+ await manager.refreshTaskStatus();
102
+ const tasks = manager.getAllTasks();
103
+
104
+ if (tasks.length === 0) {
105
+ return "No background tasks.";
106
+ }
107
+
108
+ const completed = tasks.filter((t) => t.status === "completed").length;
109
+ const errored = tasks.filter((t) => t.status === "error").length;
110
+ const running = tasks.filter((t) => t.status === "running").length;
111
+ const total = tasks.length;
112
+ const allDone = running === 0;
113
+
114
+ let output = "## Background Tasks\n\n";
115
+ output += `**Status: ${completed + errored}/${total} done${allDone ? " ✓ ALL COMPLETE" : ` (${running} still running)`}**\n\n`;
116
+ output += "| ID | Description | Agent | Status | Duration | Session |\n";
117
+ output += "|----|-------------|-------|--------|----------|---------|";
118
+
119
+ for (const task of tasks) {
120
+ const duration = task.completedAt
121
+ ? `${Math.round((task.completedAt.getTime() - task.startedAt.getTime()) / 1000)}s`
122
+ : `${Math.round((Date.now() - task.startedAt.getTime()) / 1000)}s`;
123
+
124
+ // Show session status for debugging
125
+ const sessionStatus = (task as { _sessionStatus?: string })._sessionStatus || "?";
126
+ const statusDisplay = task.status === "running" ? `${task.status} (${sessionStatus})` : task.status;
127
+
128
+ output += `| ${task.id} | ${task.description} | ${task.agent} | ${statusDisplay} | ${duration} | ${task.sessionID} |\n`;
129
+ }
130
+
131
+ if (allDone) {
132
+ output += "\n**→ All tasks complete. Proceed to collect results with background_output.**";
133
+ }
134
+
135
+ return output;
136
+ },
137
+ });
138
+
139
+ return {
140
+ background_task,
141
+ background_output,
142
+ background_cancel,
143
+ background_list,
144
+ };
145
+ }
@@ -0,0 +1,68 @@
1
+ export interface BackgroundTask {
2
+ id: string;
3
+ sessionID: string;
4
+ parentSessionID: string;
5
+ parentMessageID: string;
6
+ description: string;
7
+ prompt: string;
8
+ agent: string;
9
+ status: "running" | "completed" | "error" | "cancelled";
10
+ startedAt: Date;
11
+ completedAt?: Date;
12
+ result?: string;
13
+ error?: string;
14
+ progress?: {
15
+ toolCalls: number;
16
+ lastTool?: string;
17
+ lastUpdate: Date;
18
+ };
19
+ }
20
+
21
+ export interface BackgroundTaskInput {
22
+ description: string;
23
+ prompt: string;
24
+ agent: string;
25
+ parentSessionID: string;
26
+ parentMessageID: string;
27
+ }
28
+
29
+ // API Response Types - SDK wraps responses in { data: T } format
30
+ export interface SessionCreateResponse {
31
+ data?: {
32
+ id?: string;
33
+ };
34
+ }
35
+
36
+ // SessionStatus from OpenCode SDK - status is a discriminated union with 'type' field
37
+ export type SessionStatus =
38
+ | { type: "idle" }
39
+ | { type: "retry"; attempt: number; message: string; next: number }
40
+ | { type: "busy" };
41
+
42
+ // session.status() returns { data: map of sessionID -> SessionStatus }
43
+ export interface SessionStatusResponse {
44
+ data?: {
45
+ [sessionID: string]: SessionStatus;
46
+ };
47
+ }
48
+
49
+ export interface MessagePart {
50
+ type: string;
51
+ text?: string;
52
+ }
53
+
54
+ export interface MessageInfo {
55
+ role?: "user" | "assistant";
56
+ sessionID?: string;
57
+ type?: string;
58
+ name?: string;
59
+ }
60
+
61
+ export interface SessionMessage {
62
+ info?: MessageInfo;
63
+ parts?: MessagePart[];
64
+ }
65
+
66
+ export interface SessionMessagesResponse {
67
+ data?: SessionMessage[];
68
+ }
@@ -0,0 +1,82 @@
1
+ import { spawn, which } from "bun";
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+
4
+ /**
5
+ * Check if btca CLI is available on the system.
6
+ * Returns installation instructions if not found.
7
+ */
8
+ export async function checkBtcaAvailable(): Promise<{ available: boolean; message?: string }> {
9
+ const btcaPath = which("btca");
10
+ if (btcaPath) {
11
+ return { available: true };
12
+ }
13
+ return {
14
+ available: false,
15
+ message:
16
+ "btca CLI not found. Library source code search will not work.\n" +
17
+ "Install from: https://github.com/davis7dotsh/better-context\n" +
18
+ " bun add -g btca",
19
+ };
20
+ }
21
+
22
+ const BTCA_TIMEOUT_MS = 120000; // 2 minutes for long clones
23
+
24
+ async function runBtca(args: string[]): Promise<{ output: string; error?: string }> {
25
+ try {
26
+ const proc = spawn(["btca", ...args], {
27
+ stdout: "pipe",
28
+ stderr: "pipe",
29
+ });
30
+
31
+ // Create timeout promise
32
+ const timeoutPromise = new Promise<never>((_, reject) => {
33
+ setTimeout(() => {
34
+ proc.kill();
35
+ reject(new Error("btca command timed out after 2 minutes"));
36
+ }, BTCA_TIMEOUT_MS);
37
+ });
38
+
39
+ // Race between process completion and timeout
40
+ const [stdout, stderr, exitCode] = await Promise.race([
41
+ Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]),
42
+ timeoutPromise,
43
+ ]);
44
+
45
+ if (exitCode !== 0) {
46
+ const errorMsg = stderr.trim() || `Exit code ${exitCode}`;
47
+ return { output: "", error: errorMsg };
48
+ }
49
+
50
+ return { output: stdout.trim() };
51
+ } catch (e) {
52
+ const err = e as Error;
53
+ if (err.message?.includes("ENOENT")) {
54
+ return {
55
+ output: "",
56
+ error:
57
+ "btca CLI not found. Install from: https://github.com/davis7dotsh/better-context\n" + " bun add -g btca",
58
+ };
59
+ }
60
+ return { output: "", error: err.message };
61
+ }
62
+ }
63
+
64
+ export const btca_ask = tool({
65
+ description:
66
+ "Ask questions about library/framework source code using btca. " +
67
+ "Clones repos locally and searches source code to answer questions. " +
68
+ "Use for understanding library internals, finding implementation details, or debugging.",
69
+ args: {
70
+ tech: tool.schema.string().describe("Resource name configured in btca (e.g., 'react', 'express')"),
71
+ question: tool.schema.string().describe("Question to ask about the library source code"),
72
+ },
73
+ execute: async (args) => {
74
+ const result = await runBtca(["ask", "-t", args.tech, "-q", args.question]);
75
+
76
+ if (result.error) {
77
+ return `Error: ${result.error}`;
78
+ }
79
+
80
+ return result.output || "No answer found";
81
+ },
82
+ });