micode 0.7.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 (92) hide show
  1. package/package.json +7 -13
  2. package/src/agents/artifact-searcher.ts +46 -0
  3. package/src/agents/brainstormer.ts +145 -0
  4. package/src/agents/codebase-analyzer.ts +75 -0
  5. package/src/agents/codebase-locator.ts +71 -0
  6. package/src/agents/commander.ts +138 -0
  7. package/src/agents/executor.ts +215 -0
  8. package/src/agents/implementer.ts +99 -0
  9. package/src/agents/index.ts +44 -0
  10. package/src/agents/ledger-creator.ts +113 -0
  11. package/src/agents/pattern-finder.ts +70 -0
  12. package/src/agents/planner.ts +230 -0
  13. package/src/agents/project-initializer.ts +264 -0
  14. package/src/agents/reviewer.ts +102 -0
  15. package/src/config-loader.ts +89 -0
  16. package/src/hooks/artifact-auto-index.ts +111 -0
  17. package/src/hooks/auto-clear-ledger.ts +230 -0
  18. package/src/hooks/auto-compact.ts +241 -0
  19. package/src/hooks/comment-checker.ts +120 -0
  20. package/src/hooks/context-injector.ts +163 -0
  21. package/src/hooks/context-window-monitor.ts +106 -0
  22. package/src/hooks/file-ops-tracker.ts +96 -0
  23. package/src/hooks/ledger-loader.ts +78 -0
  24. package/src/hooks/preemptive-compaction.ts +183 -0
  25. package/src/hooks/session-recovery.ts +258 -0
  26. package/src/hooks/token-aware-truncation.ts +189 -0
  27. package/src/index.ts +258 -0
  28. package/src/tools/artifact-index/index.ts +269 -0
  29. package/src/tools/artifact-index/schema.sql +44 -0
  30. package/src/tools/artifact-search.ts +49 -0
  31. package/src/tools/ast-grep/index.ts +189 -0
  32. package/src/tools/background-task/manager.ts +374 -0
  33. package/src/tools/background-task/tools.ts +145 -0
  34. package/src/tools/background-task/types.ts +68 -0
  35. package/src/tools/btca/index.ts +82 -0
  36. package/src/tools/look-at.ts +210 -0
  37. package/src/tools/pty/buffer.ts +49 -0
  38. package/src/tools/pty/index.ts +34 -0
  39. package/src/tools/pty/manager.ts +159 -0
  40. package/src/tools/pty/tools/kill.ts +68 -0
  41. package/src/tools/pty/tools/list.ts +55 -0
  42. package/src/tools/pty/tools/read.ts +152 -0
  43. package/src/tools/pty/tools/spawn.ts +78 -0
  44. package/src/tools/pty/tools/write.ts +97 -0
  45. package/src/tools/pty/types.ts +62 -0
  46. package/src/utils/model-limits.ts +36 -0
  47. package/dist/agents/artifact-searcher.d.ts +0 -2
  48. package/dist/agents/brainstormer.d.ts +0 -2
  49. package/dist/agents/codebase-analyzer.d.ts +0 -2
  50. package/dist/agents/codebase-locator.d.ts +0 -2
  51. package/dist/agents/commander.d.ts +0 -3
  52. package/dist/agents/executor.d.ts +0 -2
  53. package/dist/agents/implementer.d.ts +0 -2
  54. package/dist/agents/index.d.ts +0 -15
  55. package/dist/agents/ledger-creator.d.ts +0 -2
  56. package/dist/agents/pattern-finder.d.ts +0 -2
  57. package/dist/agents/planner.d.ts +0 -2
  58. package/dist/agents/project-initializer.d.ts +0 -2
  59. package/dist/agents/reviewer.d.ts +0 -2
  60. package/dist/config-loader.d.ts +0 -20
  61. package/dist/hooks/artifact-auto-index.d.ts +0 -19
  62. package/dist/hooks/auto-clear-ledger.d.ts +0 -11
  63. package/dist/hooks/auto-compact.d.ts +0 -9
  64. package/dist/hooks/comment-checker.d.ts +0 -9
  65. package/dist/hooks/context-injector.d.ts +0 -15
  66. package/dist/hooks/context-window-monitor.d.ts +0 -15
  67. package/dist/hooks/file-ops-tracker.d.ts +0 -26
  68. package/dist/hooks/ledger-loader.d.ts +0 -16
  69. package/dist/hooks/preemptive-compaction.d.ts +0 -9
  70. package/dist/hooks/session-recovery.d.ts +0 -9
  71. package/dist/hooks/token-aware-truncation.d.ts +0 -15
  72. package/dist/index.d.ts +0 -3
  73. package/dist/index.js +0 -17089
  74. package/dist/tools/artifact-index/index.d.ts +0 -38
  75. package/dist/tools/artifact-search.d.ts +0 -17
  76. package/dist/tools/ast-grep/index.d.ts +0 -88
  77. package/dist/tools/background-task/manager.d.ts +0 -27
  78. package/dist/tools/background-task/tools.d.ts +0 -41
  79. package/dist/tools/background-task/types.d.ts +0 -53
  80. package/dist/tools/btca/index.d.ts +0 -19
  81. package/dist/tools/look-at.d.ts +0 -11
  82. package/dist/tools/pty/buffer.d.ts +0 -11
  83. package/dist/tools/pty/index.d.ts +0 -74
  84. package/dist/tools/pty/manager.d.ts +0 -14
  85. package/dist/tools/pty/tools/kill.d.ts +0 -12
  86. package/dist/tools/pty/tools/list.d.ts +0 -6
  87. package/dist/tools/pty/tools/read.d.ts +0 -18
  88. package/dist/tools/pty/tools/spawn.d.ts +0 -20
  89. package/dist/tools/pty/tools/write.d.ts +0 -12
  90. package/dist/tools/pty/types.d.ts +0 -54
  91. package/dist/utils/model-limits.d.ts +0 -7
  92. /package/{dist/tools/background-task/index.d.ts → src/tools/background-task/index.ts} +0 -0
package/src/index.ts ADDED
@@ -0,0 +1,258 @@
1
+ import type { Plugin } from "@opencode-ai/plugin";
2
+ import type { McpLocalConfig } from "@opencode-ai/sdk";
3
+
4
+ // Agents
5
+ import { agents, PRIMARY_AGENT_NAME } from "./agents";
6
+
7
+ // Tools
8
+ import { ast_grep_search, ast_grep_replace, checkAstGrepAvailable } from "./tools/ast-grep";
9
+ import { btca_ask, checkBtcaAvailable } from "./tools/btca";
10
+ import { look_at } from "./tools/look-at";
11
+ import { artifact_search } from "./tools/artifact-search";
12
+
13
+ // Hooks
14
+ import { createAutoCompactHook } from "./hooks/auto-compact";
15
+ import { createContextInjectorHook } from "./hooks/context-injector";
16
+ import { createSessionRecoveryHook } from "./hooks/session-recovery";
17
+ import { createTokenAwareTruncationHook } from "./hooks/token-aware-truncation";
18
+ import { createContextWindowMonitorHook } from "./hooks/context-window-monitor";
19
+ import { createCommentCheckerHook } from "./hooks/comment-checker";
20
+ import { createAutoClearLedgerHook } from "./hooks/auto-clear-ledger";
21
+ import { createLedgerLoaderHook } from "./hooks/ledger-loader";
22
+ import { createArtifactAutoIndexHook } from "./hooks/artifact-auto-index";
23
+ import { createFileOpsTrackerHook } from "./hooks/file-ops-tracker";
24
+
25
+ // Background Task System
26
+ import { BackgroundTaskManager, createBackgroundTaskTools } from "./tools/background-task";
27
+
28
+ // PTY System
29
+ import { PTYManager, createPtyTools } from "./tools/pty";
30
+
31
+ // Config loader
32
+ import { loadMicodeConfig, mergeAgentConfigs } from "./config-loader";
33
+
34
+ // Think mode: detect keywords and enable extended thinking
35
+ const THINK_KEYWORDS = [
36
+ /\bthink\s*(hard|deeply|carefully|through)\b/i,
37
+ /\bthink\b.*\b(about|on|through)\b/i,
38
+ /\b(deeply|carefully)\s*think\b/i,
39
+ /\blet('s|s)?\s*think\b/i,
40
+ ];
41
+
42
+ function detectThinkKeyword(text: string): boolean {
43
+ return THINK_KEYWORDS.some((pattern) => pattern.test(text));
44
+ }
45
+
46
+ // MCP server configurations
47
+ const MCP_SERVERS: Record<string, McpLocalConfig> = {
48
+ context7: {
49
+ type: "local",
50
+ command: ["npx", "-y", "@upstash/context7-mcp@latest"],
51
+ },
52
+ };
53
+
54
+ // Environment-gated research MCP servers
55
+ if (process.env.PERPLEXITY_API_KEY) {
56
+ MCP_SERVERS.perplexity = {
57
+ type: "local",
58
+ command: ["npx", "-y", "@anthropic/mcp-perplexity"],
59
+ };
60
+ }
61
+
62
+ if (process.env.FIRECRAWL_API_KEY) {
63
+ MCP_SERVERS.firecrawl = {
64
+ type: "local",
65
+ command: ["npx", "-y", "firecrawl-mcp"],
66
+ };
67
+ }
68
+
69
+ const OpenCodeConfigPlugin: Plugin = async (ctx) => {
70
+ // Validate external tool dependencies at startup
71
+ const astGrepStatus = await checkAstGrepAvailable();
72
+ if (!astGrepStatus.available) {
73
+ console.warn(`[micode] ${astGrepStatus.message}`);
74
+ }
75
+
76
+ const btcaStatus = await checkBtcaAvailable();
77
+ if (!btcaStatus.available) {
78
+ console.warn(`[micode] ${btcaStatus.message}`);
79
+ }
80
+
81
+ // Load user config for model overrides
82
+ const userConfig = await loadMicodeConfig();
83
+ if (userConfig?.agents) {
84
+ console.log(`[micode] Loaded model overrides for: ${Object.keys(userConfig.agents).join(", ")}`);
85
+ }
86
+
87
+ // Think mode state per session
88
+ const thinkModeState = new Map<string, boolean>();
89
+
90
+ // Hooks
91
+ const autoCompactHook = createAutoCompactHook(ctx);
92
+ const contextInjectorHook = createContextInjectorHook(ctx);
93
+ const autoClearLedgerHook = createAutoClearLedgerHook(ctx);
94
+ const ledgerLoaderHook = createLedgerLoaderHook(ctx);
95
+ const sessionRecoveryHook = createSessionRecoveryHook(ctx);
96
+ const tokenAwareTruncationHook = createTokenAwareTruncationHook(ctx);
97
+ const contextWindowMonitorHook = createContextWindowMonitorHook(ctx);
98
+ const commentCheckerHook = createCommentCheckerHook(ctx);
99
+ const artifactAutoIndexHook = createArtifactAutoIndexHook(ctx);
100
+ const fileOpsTrackerHook = createFileOpsTrackerHook(ctx);
101
+
102
+ // Background Task System
103
+ const backgroundTaskManager = new BackgroundTaskManager(ctx);
104
+ const backgroundTaskTools = createBackgroundTaskTools(backgroundTaskManager);
105
+
106
+ // PTY System
107
+ const ptyManager = new PTYManager();
108
+ const ptyTools = createPtyTools(ptyManager);
109
+
110
+ return {
111
+ // Tools
112
+ tool: {
113
+ ast_grep_search,
114
+ ast_grep_replace,
115
+ btca_ask,
116
+ look_at,
117
+ artifact_search,
118
+ ...backgroundTaskTools,
119
+ ...ptyTools,
120
+ },
121
+
122
+ config: async (config) => {
123
+ // Allow all permissions globally - no prompts
124
+ config.permission = {
125
+ ...config.permission,
126
+ edit: "allow",
127
+ bash: "allow",
128
+ webfetch: "allow",
129
+ doom_loop: "allow",
130
+ external_directory: "allow",
131
+ };
132
+
133
+ // Merge user config overrides into plugin agents
134
+ const mergedAgents = mergeAgentConfigs(agents, userConfig);
135
+
136
+ // Add our agents - our agents override OpenCode defaults, demote built-in build/plan to subagent
137
+ config.agent = {
138
+ ...config.agent, // OpenCode defaults first
139
+ build: { ...config.agent?.build, mode: "subagent" },
140
+ plan: { ...config.agent?.plan, mode: "subagent" },
141
+ triage: { ...config.agent?.triage, mode: "subagent" },
142
+ docs: { ...config.agent?.docs, mode: "subagent" },
143
+ // Our agents override - spread these LAST so they take precedence
144
+ ...Object.fromEntries(Object.entries(mergedAgents).filter(([k]) => k !== PRIMARY_AGENT_NAME)),
145
+ [PRIMARY_AGENT_NAME]: mergedAgents[PRIMARY_AGENT_NAME],
146
+ };
147
+
148
+ // Add MCP servers (plugin servers override defaults)
149
+ config.mcp = {
150
+ ...config.mcp,
151
+ ...MCP_SERVERS,
152
+ };
153
+
154
+ // Add commands
155
+ config.command = {
156
+ ...config.command,
157
+ init: {
158
+ description: "Initialize project with ARCHITECTURE.md and CODE_STYLE.md",
159
+ agent: "project-initializer",
160
+ template: `Initialize this project. $ARGUMENTS`,
161
+ },
162
+ ledger: {
163
+ description: "Create or update continuity ledger for session state",
164
+ agent: "ledger-creator",
165
+ template: `Update the continuity ledger. $ARGUMENTS`,
166
+ },
167
+ search: {
168
+ description: "Search past handoffs, plans, and ledgers",
169
+ agent: "artifact-searcher",
170
+ template: `Search for: $ARGUMENTS`,
171
+ },
172
+ };
173
+ },
174
+
175
+ "chat.message": async (input, output) => {
176
+ // Extract text from user message
177
+ const text = output.parts
178
+ .filter((p) => p.type === "text" && "text" in p)
179
+ .map((p) => (p as { text: string }).text)
180
+ .join(" ");
181
+
182
+ // Track if think mode was requested
183
+ thinkModeState.set(input.sessionID, detectThinkKeyword(text));
184
+ },
185
+
186
+ "chat.params": async (input, output) => {
187
+ // Inject ledger context first (highest priority)
188
+ await ledgerLoaderHook["chat.params"](input, output);
189
+
190
+ // Inject project context files
191
+ await contextInjectorHook["chat.params"](input, output);
192
+
193
+ // Inject context window status
194
+ await contextWindowMonitorHook["chat.params"](input, output);
195
+
196
+ // If think mode was requested, increase thinking budget
197
+ if (thinkModeState.get(input.sessionID)) {
198
+ output.options = {
199
+ ...output.options,
200
+ thinking: {
201
+ type: "enabled",
202
+ budget_tokens: 32000,
203
+ },
204
+ };
205
+ }
206
+ },
207
+
208
+ // Tool output processing
209
+ "tool.execute.after": async (
210
+ input: { tool: string; sessionID: string; callID: string; args?: Record<string, unknown> },
211
+ output: { output?: string },
212
+ ) => {
213
+ // Token-aware truncation
214
+ await tokenAwareTruncationHook["tool.execute.after"]({ name: input.tool, sessionID: input.sessionID }, output);
215
+
216
+ // Comment checker for Edit tool
217
+ await commentCheckerHook["tool.execute.after"]({ tool: input.tool, args: input.args }, output);
218
+
219
+ // Directory-aware context injection for Read/Edit
220
+ await contextInjectorHook["tool.execute.after"]({ tool: input.tool, args: input.args }, output);
221
+
222
+ // Auto-index artifacts when written to thoughts/ directories
223
+ await artifactAutoIndexHook["tool.execute.after"]({ tool: input.tool, args: input.args }, output);
224
+
225
+ // Track file operations for ledger
226
+ await fileOpsTrackerHook["tool.execute.after"](
227
+ { tool: input.tool, sessionID: input.sessionID, args: input.args },
228
+ output,
229
+ );
230
+ },
231
+
232
+ event: async ({ event }) => {
233
+ // Session cleanup (think mode + PTY)
234
+ if (event.type === "session.deleted") {
235
+ const props = event.properties as { info?: { id?: string } } | undefined;
236
+ if (props?.info?.id) {
237
+ thinkModeState.delete(props.info.id);
238
+ ptyManager.cleanupBySession(props.info.id);
239
+ }
240
+ }
241
+
242
+ // Run all event hooks
243
+ await autoCompactHook.event({ event });
244
+ await autoClearLedgerHook.event({ event });
245
+ await sessionRecoveryHook.event({ event });
246
+ await tokenAwareTruncationHook.event({ event });
247
+ await contextWindowMonitorHook.event({ event });
248
+
249
+ // Background task manager event handling
250
+ backgroundTaskManager.handleEvent(event);
251
+
252
+ // File ops tracker cleanup
253
+ await fileOpsTrackerHook.event({ event });
254
+ },
255
+ };
256
+ };
257
+
258
+ export default OpenCodeConfigPlugin;
@@ -0,0 +1,269 @@
1
+ // src/tools/artifact-index/index.ts
2
+ import { Database } from "bun:sqlite";
3
+ import { readFileSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { mkdirSync, existsSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+
8
+ const DEFAULT_DB_DIR = join(homedir(), ".config", "opencode", "artifact-index");
9
+ const DB_NAME = "context.db";
10
+
11
+ export interface PlanRecord {
12
+ id: string;
13
+ title?: string;
14
+ filePath: string;
15
+ overview?: string;
16
+ approach?: string;
17
+ }
18
+
19
+ export interface LedgerRecord {
20
+ id: string;
21
+ sessionName?: string;
22
+ filePath: string;
23
+ goal?: string;
24
+ stateNow?: string;
25
+ keyDecisions?: string;
26
+ filesRead?: string;
27
+ filesModified?: string;
28
+ }
29
+
30
+ export interface SearchResult {
31
+ type: "plan" | "ledger";
32
+ id: string;
33
+ filePath: string;
34
+ title?: string;
35
+ summary?: string;
36
+ score: number;
37
+ }
38
+
39
+ export class ArtifactIndex {
40
+ private db: Database | null = null;
41
+ private dbPath: string;
42
+
43
+ constructor(dbDir: string = DEFAULT_DB_DIR) {
44
+ this.dbPath = join(dbDir, DB_NAME);
45
+ }
46
+
47
+ async initialize(): Promise<void> {
48
+ // Ensure directory exists
49
+ const dir = dirname(this.dbPath);
50
+ if (!existsSync(dir)) {
51
+ mkdirSync(dir, { recursive: true });
52
+ }
53
+
54
+ this.db = new Database(this.dbPath);
55
+
56
+ // Load and execute schema
57
+ const schemaPath = join(dirname(import.meta.path), "schema.sql");
58
+ let schema: string;
59
+
60
+ try {
61
+ schema = readFileSync(schemaPath, "utf-8");
62
+ } catch {
63
+ // Fallback: inline schema for when bundled
64
+ schema = this.getInlineSchema();
65
+ }
66
+
67
+ // Execute schema - use exec for multi-statement support
68
+ this.db.exec(schema);
69
+ }
70
+
71
+ private getInlineSchema(): string {
72
+ return `
73
+ CREATE TABLE IF NOT EXISTS plans (
74
+ id TEXT PRIMARY KEY,
75
+ title TEXT,
76
+ file_path TEXT UNIQUE NOT NULL,
77
+ overview TEXT,
78
+ approach TEXT,
79
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
80
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
81
+ );
82
+ CREATE TABLE IF NOT EXISTS ledgers (
83
+ id TEXT PRIMARY KEY,
84
+ session_name TEXT,
85
+ file_path TEXT UNIQUE NOT NULL,
86
+ goal TEXT,
87
+ state_now TEXT,
88
+ key_decisions TEXT,
89
+ files_read TEXT,
90
+ files_modified TEXT,
91
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
92
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
93
+ );
94
+ CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5(id, title, overview, approach);
95
+ CREATE VIRTUAL TABLE IF NOT EXISTS ledgers_fts USING fts5(id, session_name, goal, state_now, key_decisions);
96
+ `;
97
+ }
98
+
99
+ async indexPlan(record: PlanRecord): Promise<void> {
100
+ if (!this.db) throw new Error("Database not initialized");
101
+
102
+ // Check for existing record by file_path to clean up old FTS entry
103
+ const existing = this.db
104
+ .query<{ id: string }, [string]>(`SELECT id FROM plans WHERE file_path = ?`)
105
+ .get(record.filePath);
106
+ if (existing) {
107
+ this.db.run(`DELETE FROM plans_fts WHERE id = ?`, [existing.id]);
108
+ }
109
+
110
+ this.db.run(
111
+ `
112
+ INSERT INTO plans (id, title, file_path, overview, approach, indexed_at)
113
+ VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
114
+ ON CONFLICT(file_path) DO UPDATE SET
115
+ id = excluded.id,
116
+ title = excluded.title,
117
+ overview = excluded.overview,
118
+ approach = excluded.approach,
119
+ indexed_at = CURRENT_TIMESTAMP
120
+ `,
121
+ [record.id, record.title ?? null, record.filePath, record.overview ?? null, record.approach ?? null],
122
+ );
123
+
124
+ this.db.run(
125
+ `
126
+ INSERT INTO plans_fts (id, title, overview, approach)
127
+ VALUES (?, ?, ?, ?)
128
+ `,
129
+ [record.id, record.title ?? null, record.overview ?? null, record.approach ?? null],
130
+ );
131
+ }
132
+
133
+ async indexLedger(record: LedgerRecord): Promise<void> {
134
+ if (!this.db) throw new Error("Database not initialized");
135
+
136
+ // Check for existing record by file_path to clean up old FTS entry
137
+ const existing = this.db
138
+ .query<{ id: string }, [string]>(`SELECT id FROM ledgers WHERE file_path = ?`)
139
+ .get(record.filePath);
140
+ if (existing) {
141
+ this.db.run(`DELETE FROM ledgers_fts WHERE id = ?`, [existing.id]);
142
+ }
143
+
144
+ this.db.run(
145
+ `
146
+ INSERT INTO ledgers (id, session_name, file_path, goal, state_now, key_decisions, files_read, files_modified, indexed_at)
147
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
148
+ ON CONFLICT(file_path) DO UPDATE SET
149
+ id = excluded.id,
150
+ session_name = excluded.session_name,
151
+ goal = excluded.goal,
152
+ state_now = excluded.state_now,
153
+ key_decisions = excluded.key_decisions,
154
+ files_read = excluded.files_read,
155
+ files_modified = excluded.files_modified,
156
+ indexed_at = CURRENT_TIMESTAMP
157
+ `,
158
+ [
159
+ record.id,
160
+ record.sessionName ?? null,
161
+ record.filePath,
162
+ record.goal ?? null,
163
+ record.stateNow ?? null,
164
+ record.keyDecisions ?? null,
165
+ record.filesRead ?? null,
166
+ record.filesModified ?? null,
167
+ ],
168
+ );
169
+
170
+ this.db.run(
171
+ `
172
+ INSERT INTO ledgers_fts (id, session_name, goal, state_now, key_decisions)
173
+ VALUES (?, ?, ?, ?, ?)
174
+ `,
175
+ [
176
+ record.id,
177
+ record.sessionName ?? null,
178
+ record.goal ?? null,
179
+ record.stateNow ?? null,
180
+ record.keyDecisions ?? null,
181
+ ],
182
+ );
183
+ }
184
+
185
+ async search(query: string, limit: number = 10): Promise<SearchResult[]> {
186
+ if (!this.db) throw new Error("Database not initialized");
187
+
188
+ const results: SearchResult[] = [];
189
+ const escapedQuery = this.escapeFtsQuery(query);
190
+
191
+ // Search plans
192
+ const plans = this.db
193
+ .query<{ id: string; file_path: string; title: string; rank: number }, [string, number]>(`
194
+ SELECT p.id, p.file_path, p.title, rank
195
+ FROM plans_fts
196
+ JOIN plans p ON plans_fts.id = p.id
197
+ WHERE plans_fts MATCH ?
198
+ ORDER BY rank
199
+ LIMIT ?
200
+ `)
201
+ .all(escapedQuery, limit);
202
+
203
+ for (const row of plans) {
204
+ results.push({
205
+ type: "plan",
206
+ id: row.id,
207
+ filePath: row.file_path,
208
+ title: row.title,
209
+ score: -row.rank,
210
+ });
211
+ }
212
+
213
+ // Search ledgers
214
+ const ledgers = this.db
215
+ .query<{ id: string; file_path: string; session_name: string; goal: string; rank: number }, [string, number]>(`
216
+ SELECT l.id, l.file_path, l.session_name, l.goal, rank
217
+ FROM ledgers_fts
218
+ JOIN ledgers l ON ledgers_fts.id = l.id
219
+ WHERE ledgers_fts MATCH ?
220
+ ORDER BY rank
221
+ LIMIT ?
222
+ `)
223
+ .all(escapedQuery, limit);
224
+
225
+ for (const row of ledgers) {
226
+ results.push({
227
+ type: "ledger",
228
+ id: row.id,
229
+ filePath: row.file_path,
230
+ title: row.session_name,
231
+ summary: row.goal,
232
+ score: -row.rank,
233
+ });
234
+ }
235
+
236
+ // Sort all results by score descending
237
+ results.sort((a, b) => b.score - a.score);
238
+
239
+ return results.slice(0, limit);
240
+ }
241
+
242
+ private escapeFtsQuery(query: string): string {
243
+ // Escape special FTS5 characters and wrap terms in quotes
244
+ return query
245
+ .replace(/['"]/g, "")
246
+ .split(/\s+/)
247
+ .filter((term) => term.length > 0)
248
+ .map((term) => `"${term}"`)
249
+ .join(" OR ");
250
+ }
251
+
252
+ async close(): Promise<void> {
253
+ if (this.db) {
254
+ this.db.close();
255
+ this.db = null;
256
+ }
257
+ }
258
+ }
259
+
260
+ // Singleton instance for global use
261
+ let globalIndex: ArtifactIndex | null = null;
262
+
263
+ export async function getArtifactIndex(): Promise<ArtifactIndex> {
264
+ if (!globalIndex) {
265
+ globalIndex = new ArtifactIndex();
266
+ await globalIndex.initialize();
267
+ }
268
+ return globalIndex;
269
+ }
@@ -0,0 +1,44 @@
1
+ -- src/tools/artifact-index/schema.sql
2
+ -- Artifact Index Schema for SQLite + FTS5
3
+ -- NOTE: FTS tables are standalone (not content-linked) and manually synced by code
4
+
5
+ -- Plans table
6
+ CREATE TABLE IF NOT EXISTS plans (
7
+ id TEXT PRIMARY KEY,
8
+ title TEXT,
9
+ file_path TEXT UNIQUE NOT NULL,
10
+ overview TEXT,
11
+ approach TEXT,
12
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
13
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
14
+ );
15
+
16
+ -- Ledgers table - with file operation tracking
17
+ CREATE TABLE IF NOT EXISTS ledgers (
18
+ id TEXT PRIMARY KEY,
19
+ session_name TEXT,
20
+ file_path TEXT UNIQUE NOT NULL,
21
+ goal TEXT,
22
+ state_now TEXT,
23
+ key_decisions TEXT,
24
+ files_read TEXT,
25
+ files_modified TEXT,
26
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
27
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
28
+ );
29
+
30
+ -- FTS5 virtual tables for full-text search (standalone, manually synced)
31
+ CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5(
32
+ id,
33
+ title,
34
+ overview,
35
+ approach
36
+ );
37
+
38
+ CREATE VIRTUAL TABLE IF NOT EXISTS ledgers_fts USING fts5(
39
+ id,
40
+ session_name,
41
+ goal,
42
+ state_now,
43
+ key_decisions
44
+ );
@@ -0,0 +1,49 @@
1
+ // src/tools/artifact-search.ts
2
+ import { tool } from "@opencode-ai/plugin/tool";
3
+ import { getArtifactIndex } from "./artifact-index";
4
+
5
+ export const artifact_search = tool({
6
+ description: `Search past plans and ledgers for relevant precedent.
7
+ Use this to find:
8
+ - Similar problems you've solved before
9
+ - Patterns and approaches that worked
10
+ - Lessons learned from past sessions
11
+ Returns ranked results with file paths for further reading.`,
12
+ args: {
13
+ query: tool.schema.string().describe("Search query - describe what you're looking for"),
14
+ limit: tool.schema.number().optional().describe("Max results to return (default: 10)"),
15
+ type: tool.schema.enum(["all", "plan", "ledger"]).optional().describe("Filter by artifact type (default: all)"),
16
+ },
17
+ execute: async (args) => {
18
+ try {
19
+ const index = await getArtifactIndex();
20
+ const results = await index.search(args.query, args.limit || 10);
21
+
22
+ // Filter by type if specified
23
+ const filtered = args.type && args.type !== "all" ? results.filter((r) => r.type === args.type) : results;
24
+
25
+ if (filtered.length === 0) {
26
+ return `No results found for "${args.query}". Try broader search terms.`;
27
+ }
28
+
29
+ let output = `## Search Results for "${args.query}"\n\n`;
30
+ output += `Found ${filtered.length} result(s):\n\n`;
31
+
32
+ for (const result of filtered) {
33
+ const typeLabel = result.type.charAt(0).toUpperCase() + result.type.slice(1);
34
+ output += `### ${typeLabel}: ${result.title || result.id}\n`;
35
+ output += `**File:** \`${result.filePath}\`\n`;
36
+ if (result.summary) {
37
+ output += `**Summary:** ${result.summary}\n`;
38
+ }
39
+ output += `**Relevance Score:** ${result.score.toFixed(2)}\n\n`;
40
+ }
41
+
42
+ output += `---\n*Use the Read tool to view full content of relevant files.*`;
43
+
44
+ return output;
45
+ } catch (e) {
46
+ return `Error searching artifacts: ${e instanceof Error ? e.message : String(e)}`;
47
+ }
48
+ },
49
+ });