opencodekit 0.13.1 ā 0.14.0
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/README.md +2 -2
- package/dist/index.js +16 -4
- package/dist/template/.opencode/AGENTS.md +13 -4
- package/dist/template/.opencode/README.md +100 -4
- package/dist/template/.opencode/command/brainstorm.md +25 -2
- package/dist/template/.opencode/command/finish.md +21 -4
- package/dist/template/.opencode/command/handoff.md +17 -0
- package/dist/template/.opencode/command/implement.md +38 -0
- package/dist/template/.opencode/command/plan.md +32 -0
- package/dist/template/.opencode/command/research.md +61 -5
- package/dist/template/.opencode/command/resume.md +31 -0
- package/dist/template/.opencode/command/start.md +31 -0
- package/dist/template/.opencode/command/triage.md +16 -1
- package/dist/template/.opencode/memory/observations/.gitkeep +0 -0
- package/dist/template/.opencode/memory/project/conventions.md +31 -0
- package/dist/template/.opencode/memory/vector_db/memories.lance/_transactions/0-8d00d272-cb80-463b-9774-7120a1c994e7.txn +0 -0
- package/dist/template/.opencode/memory/vector_db/memories.lance/_transactions/1-a3bea825-dad3-47dd-a6d6-ff41b76ff7b0.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/_versions/2.manifest +0 -0
- package/dist/template/.opencode/memory/vector_db/memories.lance/data/001010101000000101110001f998d04b63936ff83f9a34152d.lance +0 -0
- package/dist/template/.opencode/memory/vector_db/memories.lance/data/010000101010000000010010701b3840d38c2b5f275da99978.lance +0 -0
- package/dist/template/.opencode/opencode.json +587 -511
- package/dist/template/.opencode/package.json +3 -1
- package/dist/template/.opencode/plugin/memory.ts +610 -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 +16 -4
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import * as lancedb from "@lancedb/lancedb";
|
|
3
4
|
import { tool } from "@opencode-ai/plugin";
|
|
5
|
+
import { generateEmbedding } from "./memory-embed";
|
|
6
|
+
import { searchVectorStore } from "./memory-index";
|
|
4
7
|
|
|
5
8
|
// Observation types following claude-mem patterns
|
|
6
9
|
type ObservationType =
|
|
@@ -12,6 +15,8 @@ type ObservationType =
|
|
|
12
15
|
| "learning"
|
|
13
16
|
| "warning";
|
|
14
17
|
|
|
18
|
+
type ConfidenceLevel = "high" | "medium" | "low";
|
|
19
|
+
|
|
15
20
|
const TYPE_ICONS: Record<ObservationType, string> = {
|
|
16
21
|
decision: "šÆ",
|
|
17
22
|
bugfix: "š",
|
|
@@ -22,6 +27,173 @@ const TYPE_ICONS: Record<ObservationType, string> = {
|
|
|
22
27
|
warning: "ā ļø",
|
|
23
28
|
};
|
|
24
29
|
|
|
30
|
+
const CONFIDENCE_ICONS: Record<ConfidenceLevel, string> = {
|
|
31
|
+
high: "š¢",
|
|
32
|
+
medium: "š”",
|
|
33
|
+
low: "š“",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Patterns to detect file references in observation content
|
|
37
|
+
const FILE_PATTERNS = [
|
|
38
|
+
// file:line format (e.g., src/auth.ts:42)
|
|
39
|
+
/(?:^|\s)([a-zA-Z0-9_\-./]+\.[a-zA-Z]{2,4}):(\d+)/g,
|
|
40
|
+
// backtick file paths (e.g., `src/auth.ts`)
|
|
41
|
+
/`([a-zA-Z0-9_\-./]+\.[a-zA-Z]{2,4})`/g,
|
|
42
|
+
// common source paths
|
|
43
|
+
/(?:^|\s)(src\/[a-zA-Z0-9_\-./]+\.[a-zA-Z]{2,4})/g,
|
|
44
|
+
/(?:^|\s)(\.opencode\/[a-zA-Z0-9_\-./]+\.[a-zA-Z]{2,4})/g,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
interface FileReference {
|
|
48
|
+
file: string;
|
|
49
|
+
line?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface CodeLink {
|
|
53
|
+
name: string;
|
|
54
|
+
type: string;
|
|
55
|
+
file: string;
|
|
56
|
+
line: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Extract file references from observation content
|
|
60
|
+
function extractFileReferences(content: string): FileReference[] {
|
|
61
|
+
const refs: FileReference[] = [];
|
|
62
|
+
const seen = new Set<string>();
|
|
63
|
+
|
|
64
|
+
for (const pattern of FILE_PATTERNS) {
|
|
65
|
+
// Reset regex state
|
|
66
|
+
pattern.lastIndex = 0;
|
|
67
|
+
let match = pattern.exec(content);
|
|
68
|
+
|
|
69
|
+
while (match !== null) {
|
|
70
|
+
const file = match[1];
|
|
71
|
+
const line = match[2] ? Number.parseInt(match[2], 10) : undefined;
|
|
72
|
+
const key = `${file}:${line || ""}`;
|
|
73
|
+
|
|
74
|
+
if (!seen.has(key) && !file.includes("node_modules")) {
|
|
75
|
+
seen.add(key);
|
|
76
|
+
refs.push({ file, line });
|
|
77
|
+
}
|
|
78
|
+
match = pattern.exec(content);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return refs;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Find related code definitions for an observation
|
|
86
|
+
async function findRelatedCode(
|
|
87
|
+
title: string,
|
|
88
|
+
content: string,
|
|
89
|
+
fileRefs: FileReference[],
|
|
90
|
+
): Promise<CodeLink[]> {
|
|
91
|
+
const links: CodeLink[] = [];
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Search for code definitions related to the observation
|
|
95
|
+
const searchQuery = `${title} ${fileRefs.map((r) => r.file).join(" ")}`;
|
|
96
|
+
const results = await searchVectorStore(searchQuery, 5, "code");
|
|
97
|
+
|
|
98
|
+
for (const result of results) {
|
|
99
|
+
// Check if this code is in one of the referenced files
|
|
100
|
+
const isDirectRef = fileRefs.some(
|
|
101
|
+
(ref) =>
|
|
102
|
+
result.file_path.includes(ref.file) ||
|
|
103
|
+
ref.file.includes(result.file_path),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (isDirectRef) {
|
|
107
|
+
links.push({
|
|
108
|
+
name: result.title.replace(/^(function|class|type|interface): /, ""),
|
|
109
|
+
type: result.title.split(":")[0] || "code",
|
|
110
|
+
file: result.file_path,
|
|
111
|
+
line: 0, // Would need to parse from result
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// Vector store might not have code indexed yet
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return links.slice(0, 5); // Limit to 5 links
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Vector store configuration
|
|
123
|
+
const VECTOR_DB_PATH = ".opencode/memory/vector_db";
|
|
124
|
+
const TABLE_NAME = "memories";
|
|
125
|
+
|
|
126
|
+
async function addToVectorStore(
|
|
127
|
+
filePath: string,
|
|
128
|
+
title: string,
|
|
129
|
+
content: string,
|
|
130
|
+
fileType: string,
|
|
131
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
132
|
+
try {
|
|
133
|
+
const embeddingResult = await generateEmbedding(content.substring(0, 8000));
|
|
134
|
+
if (!embeddingResult) {
|
|
135
|
+
return { success: false, error: "Failed to generate embedding" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const dbPath = path.join(process.cwd(), VECTOR_DB_PATH);
|
|
139
|
+
await fs.mkdir(dbPath, { recursive: true });
|
|
140
|
+
|
|
141
|
+
const db = await lancedb.connect(dbPath);
|
|
142
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
143
|
+
|
|
144
|
+
const document: Record<string, unknown> = {
|
|
145
|
+
id: relativePath.replace(/[\/\\]/g, "_"),
|
|
146
|
+
file_path: relativePath,
|
|
147
|
+
title,
|
|
148
|
+
content,
|
|
149
|
+
content_preview: content.substring(0, 500),
|
|
150
|
+
embedding: embeddingResult.embedding,
|
|
151
|
+
indexed_at: new Date().toISOString(),
|
|
152
|
+
file_type: fileType,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
let table: lancedb.Table;
|
|
156
|
+
try {
|
|
157
|
+
table = await db.openTable(TABLE_NAME);
|
|
158
|
+
// Add to existing table
|
|
159
|
+
await table.add([document]);
|
|
160
|
+
} catch {
|
|
161
|
+
// Table doesn't exist, create it
|
|
162
|
+
await db.createTable(TABLE_NAME, [document]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { success: true };
|
|
166
|
+
} catch (err) {
|
|
167
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
168
|
+
return { success: false, error: msg };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check for potentially contradicting observations
|
|
173
|
+
async function findSimilarObservations(
|
|
174
|
+
title: string,
|
|
175
|
+
content: string,
|
|
176
|
+
concepts: string[],
|
|
177
|
+
): Promise<{ file: string; title: string; similarity: string }[]> {
|
|
178
|
+
try {
|
|
179
|
+
// Search for similar observations using semantic search
|
|
180
|
+
const searchQuery = `${title} ${concepts.join(" ")}`;
|
|
181
|
+
const similar = await searchVectorStore(searchQuery, 5, "observation");
|
|
182
|
+
|
|
183
|
+
// Filter to only observations with high similarity
|
|
184
|
+
return similar
|
|
185
|
+
.filter((doc) => doc.title !== title) // Exclude self
|
|
186
|
+
.slice(0, 3)
|
|
187
|
+
.map((doc) => ({
|
|
188
|
+
file: path.basename(doc.file_path),
|
|
189
|
+
title: doc.title,
|
|
190
|
+
similarity: "high",
|
|
191
|
+
}));
|
|
192
|
+
} catch {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
25
197
|
export default tool({
|
|
26
198
|
description:
|
|
27
199
|
"Create a structured observation for future reference. Observations are categorized by type (decision, bugfix, feature, pattern, discovery, learning, warning) and stored in .opencode/memory/observations/.",
|
|
@@ -51,6 +223,18 @@ export default tool({
|
|
|
51
223
|
.string()
|
|
52
224
|
.optional()
|
|
53
225
|
.describe("Related bead ID for traceability"),
|
|
226
|
+
confidence: tool.schema
|
|
227
|
+
.string()
|
|
228
|
+
.optional()
|
|
229
|
+
.describe(
|
|
230
|
+
"Confidence level: high (verified), medium (likely), low (uncertain). Defaults to high.",
|
|
231
|
+
),
|
|
232
|
+
supersedes: tool.schema
|
|
233
|
+
.string()
|
|
234
|
+
.optional()
|
|
235
|
+
.describe(
|
|
236
|
+
"Filename of observation this supersedes (for contradiction handling)",
|
|
237
|
+
),
|
|
54
238
|
},
|
|
55
239
|
execute: async (args: {
|
|
56
240
|
type: string;
|
|
@@ -59,6 +243,8 @@ export default tool({
|
|
|
59
243
|
concepts?: string;
|
|
60
244
|
files?: string;
|
|
61
245
|
bead_id?: string;
|
|
246
|
+
confidence?: string;
|
|
247
|
+
supersedes?: string;
|
|
62
248
|
}) => {
|
|
63
249
|
const obsDir = path.join(process.cwd(), ".opencode/memory/observations");
|
|
64
250
|
|
|
@@ -77,6 +263,14 @@ export default tool({
|
|
|
77
263
|
return `Error: Invalid observation type '${args.type}'.\nValid types: ${validTypes.join(", ")}`;
|
|
78
264
|
}
|
|
79
265
|
|
|
266
|
+
// Validate confidence level
|
|
267
|
+
const validConfidence: ConfidenceLevel[] = ["high", "medium", "low"];
|
|
268
|
+
const confidence = (args.confidence?.toLowerCase() ||
|
|
269
|
+
"high") as ConfidenceLevel;
|
|
270
|
+
if (!validConfidence.includes(confidence)) {
|
|
271
|
+
return `Error: Invalid confidence level '${args.confidence}'.\nValid levels: ${validConfidence.join(", ")}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
80
274
|
// Generate filename: YYYY-MM-DD-type-slug.md
|
|
81
275
|
const now = new Date();
|
|
82
276
|
const dateStr = now.toISOString().split("T")[0];
|
|
@@ -92,27 +286,59 @@ export default tool({
|
|
|
92
286
|
const concepts = args.concepts
|
|
93
287
|
? args.concepts.split(",").map((c) => c.trim())
|
|
94
288
|
: [];
|
|
95
|
-
|
|
289
|
+
let files = args.files ? args.files.split(",").map((f) => f.trim()) : [];
|
|
290
|
+
|
|
291
|
+
// Auto-detect file references from content (Phase 2: Link to code)
|
|
292
|
+
const detectedRefs = extractFileReferences(args.content);
|
|
293
|
+
const detectedFiles = detectedRefs.map((r) => r.file);
|
|
294
|
+
|
|
295
|
+
// Merge detected files with explicitly provided files
|
|
296
|
+
const allFiles = [...new Set([...files, ...detectedFiles])];
|
|
297
|
+
files = allFiles;
|
|
298
|
+
|
|
299
|
+
// Find related code definitions
|
|
300
|
+
const codeLinks = await findRelatedCode(
|
|
301
|
+
args.title,
|
|
302
|
+
args.content,
|
|
303
|
+
detectedRefs,
|
|
304
|
+
);
|
|
96
305
|
|
|
97
|
-
// Build observation content
|
|
306
|
+
// Build observation content with YAML frontmatter for temporal tracking
|
|
98
307
|
const icon = TYPE_ICONS[obsType];
|
|
99
|
-
|
|
100
|
-
observation += `**Type:** ${obsType}\n`;
|
|
101
|
-
observation += `**Created:** ${now.toISOString()}\n`;
|
|
308
|
+
const confidenceIcon = CONFIDENCE_ICONS[confidence];
|
|
102
309
|
|
|
310
|
+
// YAML frontmatter for temporal validity (Graphiti-inspired)
|
|
311
|
+
let observation = "---\n";
|
|
312
|
+
observation += `type: ${obsType}\n`;
|
|
313
|
+
observation += `created: ${now.toISOString()}\n`;
|
|
314
|
+
observation += `confidence: ${confidence}\n`;
|
|
315
|
+
observation += "valid_until: null\n"; // null = still valid
|
|
316
|
+
observation += "superseded_by: null\n"; // null = not superseded
|
|
317
|
+
if (args.supersedes) {
|
|
318
|
+
observation += `supersedes: ${args.supersedes}\n`;
|
|
319
|
+
}
|
|
103
320
|
if (args.bead_id) {
|
|
104
|
-
observation +=
|
|
321
|
+
observation += `bead_id: ${args.bead_id}\n`;
|
|
105
322
|
}
|
|
106
|
-
|
|
107
323
|
if (concepts.length > 0) {
|
|
108
|
-
observation +=
|
|
324
|
+
observation += `concepts: [${concepts.map((c) => `"${c}"`).join(", ")}]\n`;
|
|
109
325
|
}
|
|
110
|
-
|
|
111
326
|
if (files.length > 0) {
|
|
112
|
-
observation +=
|
|
327
|
+
observation += `files: [${files.map((f) => `"${f}"`).join(", ")}]\n`;
|
|
328
|
+
}
|
|
329
|
+
if (codeLinks.length > 0) {
|
|
330
|
+
observation += "code_links:\n";
|
|
331
|
+
for (const link of codeLinks) {
|
|
332
|
+
observation += ` - name: "${link.name}"\n`;
|
|
333
|
+
observation += ` type: "${link.type}"\n`;
|
|
334
|
+
observation += ` file: "${link.file}"\n`;
|
|
335
|
+
}
|
|
113
336
|
}
|
|
337
|
+
observation += "---\n\n";
|
|
114
338
|
|
|
115
|
-
|
|
339
|
+
// Content
|
|
340
|
+
observation += `# ${icon} ${args.title}\n\n`;
|
|
341
|
+
observation += `${confidenceIcon} **Confidence:** ${confidence}\n\n`;
|
|
116
342
|
observation += args.content;
|
|
117
343
|
observation += "\n";
|
|
118
344
|
|
|
@@ -123,8 +349,59 @@ export default tool({
|
|
|
123
349
|
// Write observation
|
|
124
350
|
await fs.writeFile(filePath, observation, "utf-8");
|
|
125
351
|
|
|
352
|
+
// Generate embedding and add to vector store
|
|
353
|
+
let embeddingStatus = "";
|
|
354
|
+
const vectorResult = await addToVectorStore(
|
|
355
|
+
filePath,
|
|
356
|
+
args.title,
|
|
357
|
+
observation,
|
|
358
|
+
"observation",
|
|
359
|
+
);
|
|
360
|
+
if (vectorResult.success) {
|
|
361
|
+
embeddingStatus = "\nEmbedding: ā Added to vector store";
|
|
362
|
+
} else {
|
|
363
|
+
embeddingStatus = `\nEmbedding: ā ${vectorResult.error || "Failed"}`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Handle supersedes - update old observation's superseded_by field
|
|
367
|
+
let supersedesStatus = "";
|
|
368
|
+
if (args.supersedes) {
|
|
369
|
+
try {
|
|
370
|
+
const oldPath = path.join(obsDir, args.supersedes);
|
|
371
|
+
const oldContent = await fs.readFile(oldPath, "utf-8");
|
|
372
|
+
// Update superseded_by in frontmatter
|
|
373
|
+
const updatedContent = oldContent.replace(
|
|
374
|
+
/superseded_by: null/,
|
|
375
|
+
`superseded_by: "${filename}"`,
|
|
376
|
+
);
|
|
377
|
+
if (updatedContent !== oldContent) {
|
|
378
|
+
await fs.writeFile(oldPath, updatedContent, "utf-8");
|
|
379
|
+
supersedesStatus = `\nSupersedes: ā Marked ${args.supersedes} as superseded`;
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
supersedesStatus = `\nSupersedes: ā Could not update ${args.supersedes}`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
126
386
|
let beadUpdate = "";
|
|
127
387
|
|
|
388
|
+
// Check for similar/potentially contradicting observations
|
|
389
|
+
let contradictionWarning = "";
|
|
390
|
+
const similarObs = await findSimilarObservations(
|
|
391
|
+
args.title,
|
|
392
|
+
args.content,
|
|
393
|
+
concepts,
|
|
394
|
+
);
|
|
395
|
+
if (similarObs.length > 0) {
|
|
396
|
+
contradictionWarning =
|
|
397
|
+
"\n\nā ļø **Similar observations found** (potential contradictions):";
|
|
398
|
+
for (const obs of similarObs) {
|
|
399
|
+
contradictionWarning += `\n- ${obs.title} (\`${obs.file}\`)`;
|
|
400
|
+
}
|
|
401
|
+
contradictionWarning +=
|
|
402
|
+
"\n\nIf this supersedes an older observation, use `supersedes` arg to mark it.";
|
|
403
|
+
}
|
|
404
|
+
|
|
128
405
|
// Update bead notes if bead_id provided
|
|
129
406
|
if (args.bead_id) {
|
|
130
407
|
try {
|
|
@@ -144,7 +421,19 @@ export default tool({
|
|
|
144
421
|
}
|
|
145
422
|
}
|
|
146
423
|
|
|
147
|
-
|
|
424
|
+
// Build code links info
|
|
425
|
+
let codeLinksInfo = "";
|
|
426
|
+
if (codeLinks.length > 0) {
|
|
427
|
+
codeLinksInfo = `\nCode Links: ${codeLinks.map((l) => `${l.type}:${l.name}`).join(", ")}`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Build auto-detected files info
|
|
431
|
+
let autoDetectedInfo = "";
|
|
432
|
+
if (detectedFiles.length > 0) {
|
|
433
|
+
autoDetectedInfo = `\nAuto-detected: ${detectedFiles.length} file reference(s)`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return `ā Observation saved: ${filename}\n\nType: ${icon} ${obsType}\nTitle: ${args.title}\nConfidence: ${confidenceIcon} ${confidence}\nConcepts: ${concepts.join(", ") || "none"}\nFiles: ${files.join(", ") || "none"}${codeLinksInfo}${autoDetectedInfo}${embeddingStatus}${supersedesStatus}${beadUpdate}${contradictionWarning}\n\nPath: ${filePath}`;
|
|
148
437
|
} catch (error) {
|
|
149
438
|
if (error instanceof Error) {
|
|
150
439
|
return `Error saving observation: ${error.message}`;
|