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.
- package/package.json +7 -13
- package/src/agents/artifact-searcher.ts +46 -0
- package/src/agents/brainstormer.ts +145 -0
- package/src/agents/codebase-analyzer.ts +75 -0
- package/src/agents/codebase-locator.ts +71 -0
- package/src/agents/commander.ts +138 -0
- package/src/agents/executor.ts +215 -0
- package/src/agents/implementer.ts +99 -0
- package/src/agents/index.ts +44 -0
- package/src/agents/ledger-creator.ts +113 -0
- package/src/agents/pattern-finder.ts +70 -0
- package/src/agents/planner.ts +230 -0
- package/src/agents/project-initializer.ts +264 -0
- package/src/agents/reviewer.ts +102 -0
- package/src/config-loader.ts +89 -0
- package/src/hooks/artifact-auto-index.ts +111 -0
- package/src/hooks/auto-clear-ledger.ts +230 -0
- package/src/hooks/auto-compact.ts +241 -0
- package/src/hooks/comment-checker.ts +120 -0
- package/src/hooks/context-injector.ts +163 -0
- package/src/hooks/context-window-monitor.ts +106 -0
- package/src/hooks/file-ops-tracker.ts +96 -0
- package/src/hooks/ledger-loader.ts +78 -0
- package/src/hooks/preemptive-compaction.ts +183 -0
- package/src/hooks/session-recovery.ts +258 -0
- package/src/hooks/token-aware-truncation.ts +189 -0
- package/src/index.ts +258 -0
- package/src/tools/artifact-index/index.ts +269 -0
- package/src/tools/artifact-index/schema.sql +44 -0
- package/src/tools/artifact-search.ts +49 -0
- package/src/tools/ast-grep/index.ts +189 -0
- package/src/tools/background-task/manager.ts +374 -0
- package/src/tools/background-task/tools.ts +145 -0
- package/src/tools/background-task/types.ts +68 -0
- package/src/tools/btca/index.ts +82 -0
- package/src/tools/look-at.ts +210 -0
- package/src/tools/pty/buffer.ts +49 -0
- package/src/tools/pty/index.ts +34 -0
- package/src/tools/pty/manager.ts +159 -0
- package/src/tools/pty/tools/kill.ts +68 -0
- package/src/tools/pty/tools/list.ts +55 -0
- package/src/tools/pty/tools/read.ts +152 -0
- package/src/tools/pty/tools/spawn.ts +78 -0
- package/src/tools/pty/tools/write.ts +97 -0
- package/src/tools/pty/types.ts +62 -0
- package/src/utils/model-limits.ts +36 -0
- package/dist/agents/artifact-searcher.d.ts +0 -2
- package/dist/agents/brainstormer.d.ts +0 -2
- package/dist/agents/codebase-analyzer.d.ts +0 -2
- package/dist/agents/codebase-locator.d.ts +0 -2
- package/dist/agents/commander.d.ts +0 -3
- package/dist/agents/executor.d.ts +0 -2
- package/dist/agents/implementer.d.ts +0 -2
- package/dist/agents/index.d.ts +0 -15
- package/dist/agents/ledger-creator.d.ts +0 -2
- package/dist/agents/pattern-finder.d.ts +0 -2
- package/dist/agents/planner.d.ts +0 -2
- package/dist/agents/project-initializer.d.ts +0 -2
- package/dist/agents/reviewer.d.ts +0 -2
- package/dist/config-loader.d.ts +0 -20
- package/dist/hooks/artifact-auto-index.d.ts +0 -19
- package/dist/hooks/auto-clear-ledger.d.ts +0 -11
- package/dist/hooks/auto-compact.d.ts +0 -9
- package/dist/hooks/comment-checker.d.ts +0 -9
- package/dist/hooks/context-injector.d.ts +0 -15
- package/dist/hooks/context-window-monitor.d.ts +0 -15
- package/dist/hooks/file-ops-tracker.d.ts +0 -26
- package/dist/hooks/ledger-loader.d.ts +0 -16
- package/dist/hooks/preemptive-compaction.d.ts +0 -9
- package/dist/hooks/session-recovery.d.ts +0 -9
- package/dist/hooks/token-aware-truncation.d.ts +0 -15
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -17089
- package/dist/tools/artifact-index/index.d.ts +0 -38
- package/dist/tools/artifact-search.d.ts +0 -17
- package/dist/tools/ast-grep/index.d.ts +0 -88
- package/dist/tools/background-task/manager.d.ts +0 -27
- package/dist/tools/background-task/tools.d.ts +0 -41
- package/dist/tools/background-task/types.d.ts +0 -53
- package/dist/tools/btca/index.d.ts +0 -19
- package/dist/tools/look-at.d.ts +0 -11
- package/dist/tools/pty/buffer.d.ts +0 -11
- package/dist/tools/pty/index.d.ts +0 -74
- package/dist/tools/pty/manager.d.ts +0 -14
- package/dist/tools/pty/tools/kill.d.ts +0 -12
- package/dist/tools/pty/tools/list.d.ts +0 -6
- package/dist/tools/pty/tools/read.d.ts +0 -18
- package/dist/tools/pty/tools/spawn.d.ts +0 -20
- package/dist/tools/pty/tools/write.d.ts +0 -12
- package/dist/tools/pty/types.d.ts +0 -54
- package/dist/utils/model-limits.d.ts +0 -7
- /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
|
+
});
|