opencodekit 0.13.2 → 0.14.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/index.js +50 -3
- package/dist/template/.opencode/AGENTS.md +51 -7
- package/dist/template/.opencode/README.md +98 -2
- package/dist/template/.opencode/agent/build.md +44 -1
- package/dist/template/.opencode/agent/explore.md +1 -0
- package/dist/template/.opencode/agent/planner.md +40 -1
- package/dist/template/.opencode/agent/review.md +1 -0
- package/dist/template/.opencode/agent/rush.md +35 -0
- package/dist/template/.opencode/agent/scout.md +1 -0
- package/dist/template/.opencode/command/brainstorm.md +83 -5
- package/dist/template/.opencode/command/finish.md +39 -12
- package/dist/template/.opencode/command/fix.md +24 -15
- package/dist/template/.opencode/command/handoff.md +17 -0
- package/dist/template/.opencode/command/implement.md +81 -18
- package/dist/template/.opencode/command/import-plan.md +30 -8
- package/dist/template/.opencode/command/new-feature.md +37 -4
- package/dist/template/.opencode/command/plan.md +51 -1
- package/dist/template/.opencode/command/pr.md +25 -15
- package/dist/template/.opencode/command/research.md +61 -5
- package/dist/template/.opencode/command/resume.md +31 -0
- package/dist/template/.opencode/command/revert-feature.md +15 -3
- package/dist/template/.opencode/command/skill-optimize.md +71 -7
- package/dist/template/.opencode/command/start.md +81 -5
- package/dist/template/.opencode/command/triage.md +16 -1
- package/dist/template/.opencode/dcp.jsonc +11 -7
- package/dist/template/.opencode/memory/observations/.gitkeep +0 -0
- package/dist/template/.opencode/memory/observations/2026-01-09-pattern-ampcode-mcp-json-includetools-pattern.md +42 -0
- package/dist/template/.opencode/memory/project/conventions.md +31 -0
- package/dist/template/.opencode/memory/project/gotchas.md +52 -5
- package/dist/template/.opencode/memory/vector_db/memories.lance/_transactions/0-0d25ba80-ba3b-4209-9046-b45d6093b4da.txn +0 -0
- package/dist/template/.opencode/memory/vector_db/memories.lance/_versions/1.manifest +0 -0
- package/dist/template/.opencode/memory/vector_db/memories.lance/data/1111100101010101011010004a9ef34df6b29f36a9a53a2892.lance +0 -0
- package/dist/template/.opencode/opencode.json +5 -3
- package/dist/template/.opencode/package.json +3 -1
- package/dist/template/.opencode/plugin/memory.ts +686 -0
- package/dist/template/.opencode/plugin/package.json +1 -1
- package/dist/template/.opencode/plugin/skill-mcp.ts +155 -36
- package/dist/template/.opencode/skill/chrome-devtools/SKILL.md +43 -65
- package/dist/template/.opencode/skill/chrome-devtools/mcp.json +19 -0
- package/dist/template/.opencode/skill/executing-plans/SKILL.md +32 -2
- package/dist/template/.opencode/skill/finishing-a-development-branch/SKILL.md +42 -17
- package/dist/template/.opencode/skill/playwright/SKILL.md +58 -133
- package/dist/template/.opencode/skill/playwright/mcp.json +16 -0
- package/dist/template/.opencode/tool/memory-embed.ts +183 -0
- package/dist/template/.opencode/tool/memory-index.ts +769 -0
- package/dist/template/.opencode/tool/memory-search.ts +358 -66
- package/dist/template/.opencode/tool/observation.ts +301 -12
- package/dist/template/.opencode/tool/repo-map.ts +451 -0
- package/package.json +1 -1
|
@@ -0,0 +1,769 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import * as lancedb from "@lancedb/lancedb";
|
|
6
|
+
import { tool } from "@opencode-ai/plugin";
|
|
7
|
+
import { generateEmbedding } from "./memory-embed";
|
|
8
|
+
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
|
|
11
|
+
// Configuration
|
|
12
|
+
const VECTOR_DB_PATH = ".opencode/memory/vector_db";
|
|
13
|
+
const TABLE_NAME = "memories";
|
|
14
|
+
|
|
15
|
+
interface MemoryDocument {
|
|
16
|
+
id: string;
|
|
17
|
+
file_path: string;
|
|
18
|
+
title: string;
|
|
19
|
+
content: string;
|
|
20
|
+
content_preview: string;
|
|
21
|
+
embedding: number[];
|
|
22
|
+
indexed_at: string;
|
|
23
|
+
file_type: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface IndexResult {
|
|
27
|
+
indexed: number;
|
|
28
|
+
skipped: number;
|
|
29
|
+
errors: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface CodeDefinition {
|
|
33
|
+
name: string;
|
|
34
|
+
type: string; // function, class, method, interface, type
|
|
35
|
+
file_path: string;
|
|
36
|
+
line_start: number;
|
|
37
|
+
line_end: number;
|
|
38
|
+
signature: string;
|
|
39
|
+
content: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// AST-grep patterns for different languages
|
|
43
|
+
// Based on OpenCode LSP support: https://opencode.ai/docs/lsp
|
|
44
|
+
const AST_PATTERNS: Record<
|
|
45
|
+
string,
|
|
46
|
+
{ lang: string; patterns: Record<string, string> }
|
|
47
|
+
> = {
|
|
48
|
+
// TypeScript/JavaScript family
|
|
49
|
+
ts: {
|
|
50
|
+
lang: "typescript",
|
|
51
|
+
patterns: {
|
|
52
|
+
function: "function $NAME($$$) { $$$ }",
|
|
53
|
+
async_function: "async function $NAME($$$) { $$$ }",
|
|
54
|
+
arrow_const: "const $NAME = ($$$) => $$$",
|
|
55
|
+
arrow_export: "export const $NAME = ($$$) => $$$",
|
|
56
|
+
class: "class $NAME { $$$ }",
|
|
57
|
+
interface: "interface $NAME { $$$ }",
|
|
58
|
+
type_alias: "type $NAME = $$$",
|
|
59
|
+
enum: "enum $NAME { $$$ }",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
tsx: {
|
|
63
|
+
lang: "tsx",
|
|
64
|
+
patterns: {
|
|
65
|
+
function: "function $NAME($$$) { $$$ }",
|
|
66
|
+
component: "export function $NAME($$$) { $$$ }",
|
|
67
|
+
class: "class $NAME { $$$ }",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
js: {
|
|
71
|
+
lang: "javascript",
|
|
72
|
+
patterns: {
|
|
73
|
+
function: "function $NAME($$$) { $$$ }",
|
|
74
|
+
async_function: "async function $NAME($$$) { $$$ }",
|
|
75
|
+
class: "class $NAME { $$$ }",
|
|
76
|
+
arrow_const: "const $NAME = ($$$) => $$$",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
jsx: {
|
|
80
|
+
lang: "javascript",
|
|
81
|
+
patterns: {
|
|
82
|
+
function: "function $NAME($$$) { $$$ }",
|
|
83
|
+
component: "export function $NAME($$$) { $$$ }",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
mjs: {
|
|
87
|
+
lang: "javascript",
|
|
88
|
+
patterns: { function: "function $NAME($$$) { $$$ }" },
|
|
89
|
+
},
|
|
90
|
+
cjs: {
|
|
91
|
+
lang: "javascript",
|
|
92
|
+
patterns: { function: "function $NAME($$$) { $$$ }" },
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// Python
|
|
96
|
+
py: {
|
|
97
|
+
lang: "python",
|
|
98
|
+
patterns: {
|
|
99
|
+
function: "def $NAME($$$):",
|
|
100
|
+
async_function: "async def $NAME($$$):",
|
|
101
|
+
class: "class $NAME:",
|
|
102
|
+
class_inherit: "class $NAME($$$):",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
pyi: {
|
|
106
|
+
lang: "python",
|
|
107
|
+
patterns: {
|
|
108
|
+
function: "def $NAME($$$) -> $$$:",
|
|
109
|
+
class: "class $NAME:",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// Go
|
|
114
|
+
go: {
|
|
115
|
+
lang: "go",
|
|
116
|
+
patterns: {
|
|
117
|
+
function: "func $NAME($$$) $$$",
|
|
118
|
+
method: "func ($$$) $NAME($$$) $$$",
|
|
119
|
+
struct: "type $NAME struct { $$$ }",
|
|
120
|
+
interface: "type $NAME interface { $$$ }",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// Rust
|
|
125
|
+
rs: {
|
|
126
|
+
lang: "rust",
|
|
127
|
+
patterns: {
|
|
128
|
+
function: "fn $NAME($$$) { $$$ }",
|
|
129
|
+
async_function: "async fn $NAME($$$) { $$$ }",
|
|
130
|
+
struct: "struct $NAME { $$$ }",
|
|
131
|
+
enum: "enum $NAME { $$$ }",
|
|
132
|
+
impl: "impl $NAME { $$$ }",
|
|
133
|
+
trait: "trait $NAME { $$$ }",
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Ruby
|
|
138
|
+
rb: {
|
|
139
|
+
lang: "ruby",
|
|
140
|
+
patterns: {
|
|
141
|
+
method: "def $NAME",
|
|
142
|
+
class: "class $NAME",
|
|
143
|
+
module: "module $NAME",
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
// PHP
|
|
148
|
+
php: {
|
|
149
|
+
lang: "php",
|
|
150
|
+
patterns: {
|
|
151
|
+
function: "function $NAME($$$)",
|
|
152
|
+
class: "class $NAME",
|
|
153
|
+
interface: "interface $NAME",
|
|
154
|
+
trait: "trait $NAME",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// Java
|
|
159
|
+
java: {
|
|
160
|
+
lang: "java",
|
|
161
|
+
patterns: {
|
|
162
|
+
method: "public $$$ $NAME($$$)",
|
|
163
|
+
class: "class $NAME",
|
|
164
|
+
interface: "interface $NAME",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// Kotlin
|
|
169
|
+
kt: {
|
|
170
|
+
lang: "kotlin",
|
|
171
|
+
patterns: {
|
|
172
|
+
function: "fun $NAME($$$)",
|
|
173
|
+
class: "class $NAME",
|
|
174
|
+
interface: "interface $NAME",
|
|
175
|
+
data_class: "data class $NAME($$$)",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
kts: {
|
|
179
|
+
lang: "kotlin",
|
|
180
|
+
patterns: {
|
|
181
|
+
function: "fun $NAME($$$)",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// C/C++
|
|
186
|
+
c: {
|
|
187
|
+
lang: "c",
|
|
188
|
+
patterns: {
|
|
189
|
+
function: "$$$ $NAME($$$) { $$$ }",
|
|
190
|
+
struct: "struct $NAME { $$$ }",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
cpp: {
|
|
194
|
+
lang: "cpp",
|
|
195
|
+
patterns: {
|
|
196
|
+
function: "$$$ $NAME($$$) { $$$ }",
|
|
197
|
+
class: "class $NAME { $$$ }",
|
|
198
|
+
struct: "struct $NAME { $$$ }",
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
h: { lang: "c", patterns: { struct: "struct $NAME { $$$ }" } },
|
|
202
|
+
hpp: { lang: "cpp", patterns: { class: "class $NAME { $$$ }" } },
|
|
203
|
+
|
|
204
|
+
// C#
|
|
205
|
+
cs: {
|
|
206
|
+
lang: "csharp",
|
|
207
|
+
patterns: {
|
|
208
|
+
method: "public $$$ $NAME($$$)",
|
|
209
|
+
class: "class $NAME",
|
|
210
|
+
interface: "interface $NAME",
|
|
211
|
+
struct: "struct $NAME",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
// Swift
|
|
216
|
+
swift: {
|
|
217
|
+
lang: "swift",
|
|
218
|
+
patterns: {
|
|
219
|
+
function: "func $NAME($$$)",
|
|
220
|
+
class: "class $NAME",
|
|
221
|
+
struct: "struct $NAME",
|
|
222
|
+
protocol: "protocol $NAME",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
// Dart
|
|
227
|
+
dart: {
|
|
228
|
+
lang: "dart",
|
|
229
|
+
patterns: {
|
|
230
|
+
function: "$$$ $NAME($$$) { $$$ }",
|
|
231
|
+
class: "class $NAME",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
// Lua
|
|
236
|
+
lua: {
|
|
237
|
+
lang: "lua",
|
|
238
|
+
patterns: {
|
|
239
|
+
function: "function $NAME($$$)",
|
|
240
|
+
local_function: "local function $NAME($$$)",
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// Elixir
|
|
245
|
+
ex: {
|
|
246
|
+
lang: "elixir",
|
|
247
|
+
patterns: {
|
|
248
|
+
function: "def $NAME($$$) do",
|
|
249
|
+
defp: "defp $NAME($$$) do",
|
|
250
|
+
module: "defmodule $NAME do",
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
exs: {
|
|
254
|
+
lang: "elixir",
|
|
255
|
+
patterns: {
|
|
256
|
+
function: "def $NAME($$$) do",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
// Shell/Bash
|
|
261
|
+
sh: {
|
|
262
|
+
lang: "bash",
|
|
263
|
+
patterns: {
|
|
264
|
+
function: "function $NAME()",
|
|
265
|
+
function_alt: "$NAME() {",
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
bash: {
|
|
269
|
+
lang: "bash",
|
|
270
|
+
patterns: {
|
|
271
|
+
function: "function $NAME()",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
// Vue/Svelte/Astro (component frameworks)
|
|
276
|
+
vue: {
|
|
277
|
+
lang: "vue",
|
|
278
|
+
patterns: {
|
|
279
|
+
script_setup: "<script setup>",
|
|
280
|
+
function: "function $NAME($$$)",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
svelte: {
|
|
284
|
+
lang: "svelte",
|
|
285
|
+
patterns: {
|
|
286
|
+
script: "<script>",
|
|
287
|
+
function: "function $NAME($$$)",
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
async function extractCodeDefinitions(
|
|
293
|
+
srcDir: string,
|
|
294
|
+
): Promise<{ definitions: CodeDefinition[]; errors: string[] }> {
|
|
295
|
+
const definitions: CodeDefinition[] = [];
|
|
296
|
+
const errors: string[] = [];
|
|
297
|
+
|
|
298
|
+
// Check if ast-grep is available
|
|
299
|
+
try {
|
|
300
|
+
await execAsync("sg --version");
|
|
301
|
+
} catch {
|
|
302
|
+
errors.push(
|
|
303
|
+
"ast-grep (sg) not installed. Run: npm install -g @ast-grep/cli",
|
|
304
|
+
);
|
|
305
|
+
return { definitions, errors };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Find source files - use all extensions from AST_PATTERNS
|
|
309
|
+
const extensions = Object.keys(AST_PATTERNS);
|
|
310
|
+
|
|
311
|
+
for (const ext of extensions) {
|
|
312
|
+
const config = AST_PATTERNS[ext] || AST_PATTERNS.ts;
|
|
313
|
+
|
|
314
|
+
for (const [defType, pattern] of Object.entries(config.patterns)) {
|
|
315
|
+
try {
|
|
316
|
+
const { stdout } = await execAsync(
|
|
317
|
+
`sg --pattern '${pattern}' --lang ${config.lang} --json "${srcDir}"`,
|
|
318
|
+
{ maxBuffer: 10 * 1024 * 1024 }, // 10MB buffer
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (!stdout.trim()) continue;
|
|
322
|
+
|
|
323
|
+
const matches = JSON.parse(stdout) as Array<{
|
|
324
|
+
file: string;
|
|
325
|
+
range: { start: { line: number }; end: { line: number } };
|
|
326
|
+
text: string;
|
|
327
|
+
metaVariables?: { single?: { NAME?: { text: string } } };
|
|
328
|
+
}>;
|
|
329
|
+
|
|
330
|
+
for (const match of matches) {
|
|
331
|
+
// Skip node_modules and common non-source directories
|
|
332
|
+
if (
|
|
333
|
+
match.file.includes("node_modules") ||
|
|
334
|
+
match.file.includes(".git") ||
|
|
335
|
+
match.file.includes("dist/") ||
|
|
336
|
+
match.file.includes("build/")
|
|
337
|
+
) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const name = match.metaVariables?.single?.NAME?.text || "anonymous";
|
|
342
|
+
const signature = match.text.split("\n")[0].substring(0, 200);
|
|
343
|
+
|
|
344
|
+
definitions.push({
|
|
345
|
+
name,
|
|
346
|
+
type: defType,
|
|
347
|
+
file_path: path.relative(process.cwd(), match.file),
|
|
348
|
+
line_start: match.range.start.line,
|
|
349
|
+
line_end: match.range.end.line,
|
|
350
|
+
signature,
|
|
351
|
+
content: match.text.substring(0, 2000), // Limit content size
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
} catch (err) {
|
|
355
|
+
// Pattern didn't match or other error, continue
|
|
356
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
357
|
+
if (!msg.includes("No matches found")) {
|
|
358
|
+
errors.push(`Pattern ${defType}: ${msg.substring(0, 100)}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return { definitions, errors };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function indexCodeDefinitions(srcDir: string): Promise<IndexResult> {
|
|
368
|
+
const result: IndexResult = { indexed: 0, skipped: 0, errors: [] };
|
|
369
|
+
|
|
370
|
+
const { definitions, errors } = await extractCodeDefinitions(srcDir);
|
|
371
|
+
result.errors.push(...errors);
|
|
372
|
+
|
|
373
|
+
if (definitions.length === 0) {
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Open database
|
|
378
|
+
const dbPath = path.join(process.cwd(), VECTOR_DB_PATH);
|
|
379
|
+
await fs.mkdir(dbPath, { recursive: true });
|
|
380
|
+
const db = await lancedb.connect(dbPath);
|
|
381
|
+
|
|
382
|
+
// Get existing table or create new one
|
|
383
|
+
let existingDocs: Record<string, unknown>[] = [];
|
|
384
|
+
try {
|
|
385
|
+
const table = await db.openTable(TABLE_NAME);
|
|
386
|
+
const allDocs = await table.search([0]).limit(10000).toArray();
|
|
387
|
+
// Keep non-code documents
|
|
388
|
+
existingDocs = allDocs.filter((doc) => doc.file_type !== "code");
|
|
389
|
+
} catch {
|
|
390
|
+
// Table doesn't exist
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const documents: Record<string, unknown>[] = [...existingDocs];
|
|
394
|
+
|
|
395
|
+
for (const def of definitions) {
|
|
396
|
+
try {
|
|
397
|
+
// Create searchable content
|
|
398
|
+
const searchContent = `${def.type} ${def.name}\n${def.signature}\n${def.content}`;
|
|
399
|
+
|
|
400
|
+
const embeddingResult = await generateEmbedding(
|
|
401
|
+
searchContent.substring(0, 8000),
|
|
402
|
+
);
|
|
403
|
+
if (!embeddingResult) {
|
|
404
|
+
result.skipped++;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
documents.push({
|
|
409
|
+
id: `code_${def.file_path}_${def.name}_${def.line_start}`.replace(
|
|
410
|
+
/[\/\\]/g,
|
|
411
|
+
"_",
|
|
412
|
+
),
|
|
413
|
+
file_path: def.file_path,
|
|
414
|
+
title: `${def.type}: ${def.name}`,
|
|
415
|
+
content: def.content,
|
|
416
|
+
content_preview: def.signature,
|
|
417
|
+
embedding: embeddingResult.embedding,
|
|
418
|
+
indexed_at: new Date().toISOString(),
|
|
419
|
+
file_type: "code",
|
|
420
|
+
// Code-specific fields
|
|
421
|
+
code_name: def.name,
|
|
422
|
+
code_type: def.type,
|
|
423
|
+
code_line_start: def.line_start,
|
|
424
|
+
code_line_end: def.line_end,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
result.indexed++;
|
|
428
|
+
} catch (err) {
|
|
429
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
430
|
+
result.errors.push(`${def.file_path}:${def.name}: ${msg}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (documents.length > 0) {
|
|
435
|
+
try {
|
|
436
|
+
await db.dropTable(TABLE_NAME);
|
|
437
|
+
} catch {
|
|
438
|
+
// Table doesn't exist
|
|
439
|
+
}
|
|
440
|
+
await db.createTable(TABLE_NAME, documents);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return result;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function extractTitle(content: string): Promise<string> {
|
|
447
|
+
// Extract first H1 or H2 heading, or first line
|
|
448
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
449
|
+
if (h1Match) return h1Match[1].trim();
|
|
450
|
+
|
|
451
|
+
const h2Match = content.match(/^##\s+(.+)$/m);
|
|
452
|
+
if (h2Match) return h2Match[1].trim();
|
|
453
|
+
|
|
454
|
+
const firstLine = content.split("\n")[0];
|
|
455
|
+
return firstLine?.trim().substring(0, 100) || "Untitled";
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function getMarkdownFiles(
|
|
459
|
+
dir: string,
|
|
460
|
+
files: string[] = [],
|
|
461
|
+
): Promise<string[]> {
|
|
462
|
+
try {
|
|
463
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
464
|
+
|
|
465
|
+
for (const entry of entries) {
|
|
466
|
+
const fullPath = path.join(dir, entry.name);
|
|
467
|
+
|
|
468
|
+
if (entry.isDirectory()) {
|
|
469
|
+
// Skip vector_db and hidden directories
|
|
470
|
+
if (entry.name === "vector_db" || entry.name.startsWith(".")) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
await getMarkdownFiles(fullPath, files);
|
|
474
|
+
} else if (entry.name.endsWith(".md")) {
|
|
475
|
+
files.push(fullPath);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} catch {
|
|
479
|
+
// Directory doesn't exist
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return files;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function getFileType(filePath: string): string {
|
|
486
|
+
if (filePath.includes("/observations/")) return "observation";
|
|
487
|
+
if (filePath.includes("/handoffs/")) return "handoff";
|
|
488
|
+
if (filePath.includes("/project/")) return "project";
|
|
489
|
+
if (filePath.includes("/_templates/")) return "template";
|
|
490
|
+
if (filePath.includes(".beads/")) return "bead";
|
|
491
|
+
return "memory";
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async function indexMemoryFiles(
|
|
495
|
+
memoryDir: string,
|
|
496
|
+
beadsDir: string,
|
|
497
|
+
): Promise<IndexResult> {
|
|
498
|
+
const result: IndexResult = { indexed: 0, skipped: 0, errors: [] };
|
|
499
|
+
|
|
500
|
+
// Collect all markdown files
|
|
501
|
+
const memoryFiles = await getMarkdownFiles(memoryDir);
|
|
502
|
+
const beadFiles = await getMarkdownFiles(beadsDir);
|
|
503
|
+
const allFiles = [...memoryFiles, ...beadFiles];
|
|
504
|
+
|
|
505
|
+
if (allFiles.length === 0) {
|
|
506
|
+
return result;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Open or create database
|
|
510
|
+
const dbPath = path.join(process.cwd(), VECTOR_DB_PATH);
|
|
511
|
+
await fs.mkdir(dbPath, { recursive: true });
|
|
512
|
+
|
|
513
|
+
const db = await lancedb.connect(dbPath);
|
|
514
|
+
|
|
515
|
+
const documents: Record<string, unknown>[] = [];
|
|
516
|
+
|
|
517
|
+
for (const filePath of allFiles) {
|
|
518
|
+
try {
|
|
519
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
520
|
+
|
|
521
|
+
// Skip empty files
|
|
522
|
+
if (content.trim().length === 0) {
|
|
523
|
+
result.skipped++;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Generate embedding
|
|
528
|
+
const embeddingResult = await generateEmbedding(
|
|
529
|
+
content.substring(0, 8000),
|
|
530
|
+
);
|
|
531
|
+
if (!embeddingResult) {
|
|
532
|
+
result.errors.push(`${filePath}: Failed to generate embedding`);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
537
|
+
const title = await extractTitle(content);
|
|
538
|
+
|
|
539
|
+
documents.push({
|
|
540
|
+
id: relativePath.replace(/[\/\\]/g, "_"),
|
|
541
|
+
file_path: relativePath,
|
|
542
|
+
title,
|
|
543
|
+
content,
|
|
544
|
+
content_preview: content.substring(0, 500),
|
|
545
|
+
embedding: embeddingResult.embedding,
|
|
546
|
+
indexed_at: new Date().toISOString(),
|
|
547
|
+
file_type: getFileType(filePath),
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
result.indexed++;
|
|
551
|
+
} catch (err) {
|
|
552
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
553
|
+
result.errors.push(`${filePath}: ${msg}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (documents.length > 0) {
|
|
558
|
+
// Create or overwrite table
|
|
559
|
+
try {
|
|
560
|
+
await db.dropTable(TABLE_NAME);
|
|
561
|
+
} catch {
|
|
562
|
+
// Table doesn't exist, that's fine
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
await db.createTable(TABLE_NAME, documents);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return result;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function searchVectorStore(
|
|
572
|
+
query: string,
|
|
573
|
+
topK = 5,
|
|
574
|
+
fileType?: string,
|
|
575
|
+
): Promise<MemoryDocument[]> {
|
|
576
|
+
const dbPath = path.join(process.cwd(), VECTOR_DB_PATH);
|
|
577
|
+
|
|
578
|
+
try {
|
|
579
|
+
await fs.access(dbPath);
|
|
580
|
+
} catch {
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const db = await lancedb.connect(dbPath);
|
|
585
|
+
|
|
586
|
+
let table: lancedb.Table;
|
|
587
|
+
try {
|
|
588
|
+
table = await db.openTable(TABLE_NAME);
|
|
589
|
+
} catch {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Generate query embedding
|
|
594
|
+
const embeddingResult = await generateEmbedding(query);
|
|
595
|
+
if (!embeddingResult) {
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
let searchQuery = table.search(embeddingResult.embedding).limit(topK);
|
|
600
|
+
|
|
601
|
+
if (fileType) {
|
|
602
|
+
searchQuery = searchQuery.where(`file_type = '${fileType}'`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const results = await searchQuery.toArray();
|
|
606
|
+
|
|
607
|
+
return results.map((row) => ({
|
|
608
|
+
id: row.id as string,
|
|
609
|
+
file_path: row.file_path as string,
|
|
610
|
+
title: row.title as string,
|
|
611
|
+
content: row.content as string,
|
|
612
|
+
content_preview: row.content_preview as string,
|
|
613
|
+
embedding: row.embedding as number[],
|
|
614
|
+
indexed_at: row.indexed_at as string,
|
|
615
|
+
file_type: row.file_type as string,
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export default tool({
|
|
620
|
+
description:
|
|
621
|
+
"Manage the vector store for semantic memory search. Rebuild index from memory files, index code definitions, or search for similar content.",
|
|
622
|
+
args: {
|
|
623
|
+
action: tool.schema
|
|
624
|
+
.enum(["rebuild", "search", "status", "index-code"])
|
|
625
|
+
.describe(
|
|
626
|
+
"Action: 'rebuild' to reindex memory files, 'index-code' to index code definitions, 'search' to find similar content, 'status' to check index state",
|
|
627
|
+
),
|
|
628
|
+
query: tool.schema
|
|
629
|
+
.string()
|
|
630
|
+
.optional()
|
|
631
|
+
.describe("Search query (required for 'search' action)"),
|
|
632
|
+
limit: tool.schema
|
|
633
|
+
.number()
|
|
634
|
+
.optional()
|
|
635
|
+
.describe("Max results for search (default: 5)"),
|
|
636
|
+
type: tool.schema
|
|
637
|
+
.string()
|
|
638
|
+
.optional()
|
|
639
|
+
.describe(
|
|
640
|
+
"Filter by type: observation, handoff, project, template, bead, memory, code",
|
|
641
|
+
),
|
|
642
|
+
path: tool.schema
|
|
643
|
+
.string()
|
|
644
|
+
.optional()
|
|
645
|
+
.describe("Source directory for 'index-code' action (default: 'src')"),
|
|
646
|
+
},
|
|
647
|
+
execute: async (args: {
|
|
648
|
+
action: "rebuild" | "search" | "status" | "index-code";
|
|
649
|
+
query?: string;
|
|
650
|
+
limit?: number;
|
|
651
|
+
type?: string;
|
|
652
|
+
path?: string;
|
|
653
|
+
}) => {
|
|
654
|
+
const memoryDir = path.join(process.cwd(), ".opencode/memory");
|
|
655
|
+
const beadsDir = path.join(process.cwd(), ".beads/artifacts");
|
|
656
|
+
|
|
657
|
+
if (args.action === "rebuild") {
|
|
658
|
+
const result = await indexMemoryFiles(memoryDir, beadsDir);
|
|
659
|
+
|
|
660
|
+
let output = "# Vector Store Rebuild Complete\n\n";
|
|
661
|
+
output += `- **Indexed:** ${result.indexed} files\n`;
|
|
662
|
+
output += `- **Skipped:** ${result.skipped} files (empty)\n`;
|
|
663
|
+
output += `- **Location:** ${VECTOR_DB_PATH}/\n`;
|
|
664
|
+
|
|
665
|
+
if (result.errors.length > 0) {
|
|
666
|
+
output += `\n## Errors (${result.errors.length})\n\n`;
|
|
667
|
+
for (const err of result.errors.slice(0, 10)) {
|
|
668
|
+
output += `- ${err}\n`;
|
|
669
|
+
}
|
|
670
|
+
if (result.errors.length > 10) {
|
|
671
|
+
output += `- ... and ${result.errors.length - 10} more\n`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return output;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (args.action === "index-code") {
|
|
679
|
+
const srcDir = args.path || "src";
|
|
680
|
+
const fullPath = path.join(process.cwd(), srcDir);
|
|
681
|
+
|
|
682
|
+
// Check if directory exists
|
|
683
|
+
try {
|
|
684
|
+
await fs.access(fullPath);
|
|
685
|
+
} catch {
|
|
686
|
+
return `Error: Directory '${srcDir}' not found.\n\nUsage: memory-index action=index-code path=src`;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const result = await indexCodeDefinitions(fullPath);
|
|
690
|
+
|
|
691
|
+
let output = "# Code Index Complete\n\n";
|
|
692
|
+
output += `- **Indexed:** ${result.indexed} code definitions\n`;
|
|
693
|
+
output += `- **Skipped:** ${result.skipped} (no embedding)\n`;
|
|
694
|
+
output += `- **Source:** ${srcDir}/\n`;
|
|
695
|
+
output += `- **Location:** ${VECTOR_DB_PATH}/\n`;
|
|
696
|
+
|
|
697
|
+
if (result.errors.length > 0) {
|
|
698
|
+
output += `\n## Errors (${result.errors.length})\n\n`;
|
|
699
|
+
for (const err of result.errors.slice(0, 10)) {
|
|
700
|
+
output += `- ${err}\n`;
|
|
701
|
+
}
|
|
702
|
+
if (result.errors.length > 10) {
|
|
703
|
+
output += `- ... and ${result.errors.length - 10} more\n`;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
output += "\n## Indexed Types\n\n";
|
|
708
|
+
output +=
|
|
709
|
+
"Functions, classes, interfaces, types from TypeScript/JavaScript/Python files.\n";
|
|
710
|
+
output +=
|
|
711
|
+
"\nSearch with: `memory-search query='function name' mode=semantic type=code`";
|
|
712
|
+
|
|
713
|
+
return output;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (args.action === "search") {
|
|
717
|
+
if (!args.query) {
|
|
718
|
+
return "Error: 'query' is required for search action";
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const results = await searchVectorStore(
|
|
722
|
+
args.query,
|
|
723
|
+
args.limit || 5,
|
|
724
|
+
args.type,
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
if (results.length === 0) {
|
|
728
|
+
return `No results found for "${args.query}".\n\nTip: Run 'vector-store rebuild' first to index memory files.`;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
let output = `# Semantic Search: "${args.query}"\n\n`;
|
|
732
|
+
output += `Found ${results.length} result(s).\n\n`;
|
|
733
|
+
|
|
734
|
+
for (const doc of results) {
|
|
735
|
+
output += `## ${doc.title}\n\n`;
|
|
736
|
+
output += `**File:** \`${doc.file_path}\`\n`;
|
|
737
|
+
output += `**Type:** ${doc.file_type}\n`;
|
|
738
|
+
output += `**Indexed:** ${doc.indexed_at}\n\n`;
|
|
739
|
+
output += `${doc.content_preview}...\n\n`;
|
|
740
|
+
output += "---\n\n";
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
return output;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (args.action === "status") {
|
|
747
|
+
const dbPath = path.join(process.cwd(), VECTOR_DB_PATH);
|
|
748
|
+
|
|
749
|
+
try {
|
|
750
|
+
await fs.access(dbPath);
|
|
751
|
+
const db = await lancedb.connect(dbPath);
|
|
752
|
+
const table = await db.openTable(TABLE_NAME);
|
|
753
|
+
const count = await table.countRows();
|
|
754
|
+
|
|
755
|
+
return `# Vector Store Status\n\n- **Location:** ${VECTOR_DB_PATH}/\n- **Documents:** ${count}\n- **Table:** ${TABLE_NAME}\n\nUse 'vector-store rebuild' to reindex.`;
|
|
756
|
+
} catch {
|
|
757
|
+
return `# Vector Store Status\n\n- **Status:** Not initialized\n- **Location:** ${VECTOR_DB_PATH}/ (does not exist)\n\nRun 'vector-store rebuild' to create the index.`;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return "Unknown action";
|
|
762
|
+
},
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// Export search function for use by memory-search.ts
|
|
766
|
+
export { searchVectorStore };
|
|
767
|
+
|
|
768
|
+
// Export rebuild function for use by memory-watcher plugin
|
|
769
|
+
export { indexMemoryFiles };
|