opencodekit 0.17.13 → 0.18.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/dist/index.js +4 -6
- package/dist/template/.opencode/dcp.jsonc +81 -81
- package/dist/template/.opencode/memory/memory.db +0 -0
- package/dist/template/.opencode/memory.db +0 -0
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/opencode.json +199 -23
- package/dist/template/.opencode/opencode.json.tui-migration.bak +1380 -0
- package/dist/template/.opencode/package.json +1 -1
- package/dist/template/.opencode/plugin/lib/capture.ts +177 -0
- package/dist/template/.opencode/plugin/lib/context.ts +194 -0
- package/dist/template/.opencode/plugin/lib/curator.ts +234 -0
- package/dist/template/.opencode/plugin/lib/db/maintenance.ts +312 -0
- package/dist/template/.opencode/plugin/lib/db/observations.ts +299 -0
- package/dist/template/.opencode/plugin/lib/db/pipeline.ts +520 -0
- package/dist/template/.opencode/plugin/lib/db/schema.ts +356 -0
- package/dist/template/.opencode/plugin/lib/db/types.ts +211 -0
- package/dist/template/.opencode/plugin/lib/distill.ts +376 -0
- package/dist/template/.opencode/plugin/lib/inject.ts +126 -0
- package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +188 -0
- package/dist/template/.opencode/plugin/lib/memory-db.ts +54 -936
- package/dist/template/.opencode/plugin/lib/memory-helpers.ts +202 -0
- package/dist/template/.opencode/plugin/lib/memory-hooks.ts +240 -0
- package/dist/template/.opencode/plugin/lib/memory-tools.ts +341 -0
- package/dist/template/.opencode/plugin/memory.ts +56 -60
- package/dist/template/.opencode/plugin/sessions.ts +372 -93
- package/dist/template/.opencode/tui.json +15 -0
- package/package.json +1 -1
- package/dist/template/.opencode/tool/action-queue.ts +0 -313
- package/dist/template/.opencode/tool/memory-admin.ts +0 -445
- package/dist/template/.opencode/tool/memory-get.ts +0 -143
- package/dist/template/.opencode/tool/memory-read.ts +0 -45
- package/dist/template/.opencode/tool/memory-search.ts +0 -264
- package/dist/template/.opencode/tool/memory-timeline.ts +0 -105
- package/dist/template/.opencode/tool/memory-update.ts +0 -63
- package/dist/template/.opencode/tool/observation.ts +0 -357
|
@@ -1,445 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { tool } from "@opencode-ai/plugin";
|
|
4
|
-
import {
|
|
5
|
-
type ConfidenceLevel,
|
|
6
|
-
type ObservationInput,
|
|
7
|
-
type ObservationType,
|
|
8
|
-
archiveOldObservations,
|
|
9
|
-
checkpointWAL,
|
|
10
|
-
getDatabaseSizes,
|
|
11
|
-
getMemoryDB,
|
|
12
|
-
getObservationStats,
|
|
13
|
-
rebuildFTS5,
|
|
14
|
-
runFullMaintenance,
|
|
15
|
-
storeObservation,
|
|
16
|
-
vacuumDatabase,
|
|
17
|
-
} from "../plugin/lib/memory-db.js";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Consolidated memory administration tool.
|
|
21
|
-
* Operations: status, full, archive, checkpoint, vacuum, migrate
|
|
22
|
-
*/
|
|
23
|
-
export default tool({
|
|
24
|
-
description: `Memory system administration: maintenance and migration.
|
|
25
|
-
|
|
26
|
-
Operations:
|
|
27
|
-
- "status": Storage stats and recommendations
|
|
28
|
-
- "full": Full maintenance cycle (archive + checkpoint + vacuum)
|
|
29
|
-
- "archive": Archive old observations (>90 days default)
|
|
30
|
-
- "checkpoint": Checkpoint WAL file
|
|
31
|
-
- "vacuum": Vacuum database
|
|
32
|
-
- "migrate": Import .opencode/memory/observations/*.md into SQLite
|
|
33
|
-
|
|
34
|
-
Example:
|
|
35
|
-
memory-admin({ operation: "status" })
|
|
36
|
-
memory-admin({ operation: "migrate", dry_run: true })`,
|
|
37
|
-
args: {
|
|
38
|
-
operation: tool.schema
|
|
39
|
-
.string()
|
|
40
|
-
.optional()
|
|
41
|
-
.default("status")
|
|
42
|
-
.describe(
|
|
43
|
-
"Operation: status, full, archive, checkpoint, vacuum, migrate",
|
|
44
|
-
),
|
|
45
|
-
older_than_days: tool.schema
|
|
46
|
-
.number()
|
|
47
|
-
.optional()
|
|
48
|
-
.default(90)
|
|
49
|
-
.describe("Archive threshold in days (default: 90)"),
|
|
50
|
-
dry_run: tool.schema
|
|
51
|
-
.boolean()
|
|
52
|
-
.optional()
|
|
53
|
-
.default(false)
|
|
54
|
-
.describe("Preview changes without executing"),
|
|
55
|
-
force: tool.schema
|
|
56
|
-
.boolean()
|
|
57
|
-
.optional()
|
|
58
|
-
.describe("Force re-migration of all files"),
|
|
59
|
-
},
|
|
60
|
-
execute: async (args: {
|
|
61
|
-
operation?: string;
|
|
62
|
-
older_than_days?: number;
|
|
63
|
-
dry_run?: boolean;
|
|
64
|
-
force?: boolean;
|
|
65
|
-
}) => {
|
|
66
|
-
const operation = args.operation || "status";
|
|
67
|
-
const olderThanDays = args.older_than_days ?? 90;
|
|
68
|
-
const dryRun = args.dry_run ?? false;
|
|
69
|
-
|
|
70
|
-
// Helper to format bytes
|
|
71
|
-
const formatBytes = (bytes: number): string => {
|
|
72
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
73
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
74
|
-
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
// ===== MIGRATE =====
|
|
78
|
-
if (operation === "migrate") {
|
|
79
|
-
return await runMigration(args.dry_run, args.force);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const results: string[] = [];
|
|
83
|
-
|
|
84
|
-
// ===== STATUS =====
|
|
85
|
-
if (operation === "status") {
|
|
86
|
-
const sizes = getDatabaseSizes();
|
|
87
|
-
const stats = getObservationStats();
|
|
88
|
-
|
|
89
|
-
results.push("## Memory System Status\n");
|
|
90
|
-
results.push("### Database Size");
|
|
91
|
-
results.push(`- Main DB: ${formatBytes(sizes.mainDb)}`);
|
|
92
|
-
results.push(`- WAL file: ${formatBytes(sizes.wal)}`);
|
|
93
|
-
results.push(`- **Total: ${formatBytes(sizes.total)}**\n`);
|
|
94
|
-
|
|
95
|
-
results.push("### Observations");
|
|
96
|
-
results.push(`- Total: ${stats.total}`);
|
|
97
|
-
for (const [type, count] of Object.entries(stats)) {
|
|
98
|
-
if (type !== "total") {
|
|
99
|
-
results.push(`- ${type}: ${count}`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const archiveCandidates = archiveOldObservations({
|
|
104
|
-
olderThanDays,
|
|
105
|
-
includeSuperseded: true,
|
|
106
|
-
dryRun: true,
|
|
107
|
-
});
|
|
108
|
-
results.push(`\n### Maintenance Recommendations`);
|
|
109
|
-
results.push(
|
|
110
|
-
`- Archive candidates (>${olderThanDays} days): ${archiveCandidates}`,
|
|
111
|
-
);
|
|
112
|
-
if (sizes.wal > 1024 * 1024) {
|
|
113
|
-
results.push(`- WAL checkpoint recommended (WAL > 1MB)`);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return results.join("\n");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ===== FULL MAINTENANCE =====
|
|
120
|
-
if (operation === "full") {
|
|
121
|
-
results.push(
|
|
122
|
-
dryRun ? "## Full Maintenance (DRY RUN)\n" : "## Full Maintenance\n",
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const stats = runFullMaintenance({
|
|
126
|
-
olderThanDays,
|
|
127
|
-
includeSuperseded: true,
|
|
128
|
-
dryRun,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
results.push(`### Results`);
|
|
132
|
-
results.push(`- Archived observations: ${stats.archived}`);
|
|
133
|
-
results.push(`- WAL checkpointed: ${stats.checkpointed ? "Yes" : "No"}`);
|
|
134
|
-
results.push(`- Database vacuumed: ${stats.vacuumed ? "Yes" : "No"}`);
|
|
135
|
-
results.push(`- Space before: ${formatBytes(stats.dbSizeBefore)}`);
|
|
136
|
-
results.push(`- Space after: ${formatBytes(stats.dbSizeAfter)}`);
|
|
137
|
-
results.push(`- **Freed: ${formatBytes(stats.freedBytes)}**`);
|
|
138
|
-
|
|
139
|
-
return results.join("\n");
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ===== ARCHIVE ONLY =====
|
|
143
|
-
if (operation === "archive") {
|
|
144
|
-
const archived = archiveOldObservations({
|
|
145
|
-
olderThanDays,
|
|
146
|
-
includeSuperseded: true,
|
|
147
|
-
dryRun,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
if (dryRun) {
|
|
151
|
-
return `## Archive Preview\n\nWould archive ${archived} observations older than ${olderThanDays} days.\n\nRun without dry_run to execute.`;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return `## Archive Complete\n\nArchived ${archived} observations to observations_archive table.`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ===== CHECKPOINT ONLY =====
|
|
158
|
-
if (operation === "checkpoint") {
|
|
159
|
-
if (dryRun) {
|
|
160
|
-
const sizes = getDatabaseSizes();
|
|
161
|
-
return `## Checkpoint Preview\n\nWAL size: ${formatBytes(sizes.wal)}\n\nRun without dry_run to checkpoint.`;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const result = checkpointWAL();
|
|
165
|
-
return `## Checkpoint Complete\n\nCheckpointed: ${result.checkpointed ? "Yes" : "No"}\nWAL pages processed: ${result.walSize}`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// ===== VACUUM ONLY =====
|
|
169
|
-
if (operation === "vacuum") {
|
|
170
|
-
if (dryRun) {
|
|
171
|
-
const sizes = getDatabaseSizes();
|
|
172
|
-
return `## Vacuum Preview\n\nCurrent size: ${formatBytes(sizes.total)}\n\nRun without dry_run to vacuum.`;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const before = getDatabaseSizes();
|
|
176
|
-
const success = vacuumDatabase();
|
|
177
|
-
const after = getDatabaseSizes();
|
|
178
|
-
|
|
179
|
-
return `## Vacuum Complete\n\nSuccess: ${success ? "Yes" : "No"}\nBefore: ${formatBytes(before.total)}\nAfter: ${formatBytes(after.total)}\nFreed: ${formatBytes(before.total - after.total)}`;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return `Unknown operation: ${operation}. Use: status, full, archive, checkpoint, vacuum, migrate`;
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// ===== MIGRATION HELPERS =====
|
|
187
|
-
|
|
188
|
-
interface ParsedObservation {
|
|
189
|
-
type: ObservationType;
|
|
190
|
-
title: string;
|
|
191
|
-
subtitle?: string;
|
|
192
|
-
facts: string[];
|
|
193
|
-
narrative: string;
|
|
194
|
-
concepts: string[];
|
|
195
|
-
files_read: string[];
|
|
196
|
-
files_modified: string[];
|
|
197
|
-
confidence: ConfidenceLevel;
|
|
198
|
-
bead_id?: string;
|
|
199
|
-
supersedes?: string;
|
|
200
|
-
markdown_file: string;
|
|
201
|
-
created_at: string;
|
|
202
|
-
created_at_epoch: number;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function parseYAML(yamlContent: string): Record<string, unknown> {
|
|
206
|
-
const result: Record<string, unknown> = {};
|
|
207
|
-
|
|
208
|
-
for (const line of yamlContent.split("\n")) {
|
|
209
|
-
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
210
|
-
if (match) {
|
|
211
|
-
const [, key, value] = match;
|
|
212
|
-
if (value.startsWith("[")) {
|
|
213
|
-
try {
|
|
214
|
-
result[key] = JSON.parse(value);
|
|
215
|
-
} catch {
|
|
216
|
-
result[key] = value;
|
|
217
|
-
}
|
|
218
|
-
} else if (value === "null" || value === "") {
|
|
219
|
-
result[key] = null;
|
|
220
|
-
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
221
|
-
result[key] = value.slice(1, -1);
|
|
222
|
-
} else {
|
|
223
|
-
result[key] = value;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return result;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function extractFacts(narrative: string): string[] {
|
|
232
|
-
const facts: string[] = [];
|
|
233
|
-
const lines = narrative.split("\n");
|
|
234
|
-
|
|
235
|
-
for (const line of lines) {
|
|
236
|
-
const bulletMatch = line.match(/^[-*]\s+(.+)$/);
|
|
237
|
-
if (bulletMatch) {
|
|
238
|
-
facts.push(bulletMatch[1].trim());
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return facts;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function parseMarkdownObservation(
|
|
246
|
-
content: string,
|
|
247
|
-
filename: string,
|
|
248
|
-
): ParsedObservation {
|
|
249
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
250
|
-
if (!frontmatterMatch) {
|
|
251
|
-
throw new Error(`Invalid format: ${filename} - no YAML frontmatter`);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const yaml = parseYAML(frontmatterMatch[1]);
|
|
255
|
-
const narrative = frontmatterMatch[2].trim();
|
|
256
|
-
|
|
257
|
-
const titleMatch = narrative.match(/^#\s+.+?\s+(.+)$/m);
|
|
258
|
-
const title = titleMatch
|
|
259
|
-
? titleMatch[1]
|
|
260
|
-
: (yaml.title as string) || "Untitled";
|
|
261
|
-
|
|
262
|
-
const validTypes: ObservationType[] = [
|
|
263
|
-
"decision",
|
|
264
|
-
"bugfix",
|
|
265
|
-
"feature",
|
|
266
|
-
"pattern",
|
|
267
|
-
"discovery",
|
|
268
|
-
"learning",
|
|
269
|
-
"warning",
|
|
270
|
-
];
|
|
271
|
-
const type = (yaml.type as string)?.toLowerCase() as ObservationType;
|
|
272
|
-
if (!validTypes.includes(type)) {
|
|
273
|
-
throw new Error(`Invalid type '${yaml.type}' in ${filename}`);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const validConfidence: ConfidenceLevel[] = ["high", "medium", "low"];
|
|
277
|
-
const confidence = ((yaml.confidence as string)?.toLowerCase() ||
|
|
278
|
-
"high") as ConfidenceLevel;
|
|
279
|
-
if (!validConfidence.includes(confidence)) {
|
|
280
|
-
throw new Error(`Invalid confidence '${yaml.confidence}' in ${filename}`);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const createdStr = yaml.created as string;
|
|
284
|
-
if (!createdStr) {
|
|
285
|
-
throw new Error(`Missing created date in ${filename}`);
|
|
286
|
-
}
|
|
287
|
-
const createdAt = new Date(createdStr);
|
|
288
|
-
if (Number.isNaN(createdAt.getTime())) {
|
|
289
|
-
throw new Error(`Invalid created date '${createdStr}' in ${filename}`);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const facts = extractFacts(narrative);
|
|
293
|
-
const files = (yaml.files as string[]) || [];
|
|
294
|
-
|
|
295
|
-
return {
|
|
296
|
-
type,
|
|
297
|
-
title,
|
|
298
|
-
subtitle: yaml.subtitle as string | undefined,
|
|
299
|
-
facts,
|
|
300
|
-
narrative,
|
|
301
|
-
concepts: (yaml.concepts as string[]) || [],
|
|
302
|
-
files_read: files,
|
|
303
|
-
files_modified: files,
|
|
304
|
-
confidence,
|
|
305
|
-
bead_id: yaml.bead_id as string | undefined,
|
|
306
|
-
supersedes: yaml.supersedes as string | undefined,
|
|
307
|
-
markdown_file: filename,
|
|
308
|
-
created_at: createdAt.toISOString(),
|
|
309
|
-
created_at_epoch: createdAt.getTime(),
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async function runMigration(
|
|
314
|
-
dryRun?: boolean,
|
|
315
|
-
force?: boolean,
|
|
316
|
-
): Promise<string> {
|
|
317
|
-
const obsDir = path.join(process.cwd(), ".opencode/memory/observations");
|
|
318
|
-
const migrationMarker = path.join(obsDir, ".migrated");
|
|
319
|
-
|
|
320
|
-
if (!force) {
|
|
321
|
-
try {
|
|
322
|
-
await fs.access(migrationMarker);
|
|
323
|
-
const markerContent = await fs.readFile(migrationMarker, "utf-8");
|
|
324
|
-
return `Migration already complete.\n\n${markerContent}\n\nUse force: true to re-migrate.`;
|
|
325
|
-
} catch {
|
|
326
|
-
// No marker, proceed with migration
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
let files: string[];
|
|
331
|
-
try {
|
|
332
|
-
const entries = await fs.readdir(obsDir);
|
|
333
|
-
files = entries.filter((f) => f.endsWith(".md") && !f.startsWith("."));
|
|
334
|
-
} catch {
|
|
335
|
-
return "No observations directory found at .opencode/memory/observations/";
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
if (files.length === 0) {
|
|
339
|
-
return "No markdown files found to migrate.";
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const db = getMemoryDB();
|
|
343
|
-
const results: {
|
|
344
|
-
migrated: string[];
|
|
345
|
-
skipped: string[];
|
|
346
|
-
errors: { file: string; error: string }[];
|
|
347
|
-
} = {
|
|
348
|
-
migrated: [],
|
|
349
|
-
skipped: [],
|
|
350
|
-
errors: [],
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
files.sort();
|
|
354
|
-
|
|
355
|
-
for (const file of files) {
|
|
356
|
-
try {
|
|
357
|
-
const content = await fs.readFile(path.join(obsDir, file), "utf-8");
|
|
358
|
-
const parsed = parseMarkdownObservation(content, file);
|
|
359
|
-
|
|
360
|
-
if (dryRun) {
|
|
361
|
-
results.migrated.push(
|
|
362
|
-
`${file} → ${parsed.type}: ${parsed.title.substring(0, 50)}`,
|
|
363
|
-
);
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const existing = db
|
|
368
|
-
.query("SELECT id FROM observations WHERE markdown_file = ?")
|
|
369
|
-
.get(file);
|
|
370
|
-
|
|
371
|
-
if (existing && !force) {
|
|
372
|
-
results.skipped.push(file);
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const input: ObservationInput = {
|
|
377
|
-
type: parsed.type,
|
|
378
|
-
title: parsed.title,
|
|
379
|
-
subtitle: parsed.subtitle,
|
|
380
|
-
facts: parsed.facts,
|
|
381
|
-
narrative: parsed.narrative,
|
|
382
|
-
concepts: parsed.concepts,
|
|
383
|
-
files_read: parsed.files_read,
|
|
384
|
-
files_modified: parsed.files_modified,
|
|
385
|
-
confidence: parsed.confidence,
|
|
386
|
-
bead_id: parsed.bead_id,
|
|
387
|
-
markdown_file: file,
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
storeObservation(input);
|
|
391
|
-
results.migrated.push(file);
|
|
392
|
-
} catch (e) {
|
|
393
|
-
results.errors.push({
|
|
394
|
-
file,
|
|
395
|
-
error: e instanceof Error ? e.message : String(e),
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (!dryRun && results.migrated.length > 0) {
|
|
401
|
-
try {
|
|
402
|
-
rebuildFTS5();
|
|
403
|
-
} catch {
|
|
404
|
-
// FTS5 rebuild failed, continue
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (!dryRun) {
|
|
409
|
-
const markerContent = [
|
|
410
|
-
`Migrated ${results.migrated.length} observations on ${new Date().toISOString()}`,
|
|
411
|
-
`Skipped: ${results.skipped.length}`,
|
|
412
|
-
`Errors: ${results.errors.length}`,
|
|
413
|
-
].join("\n");
|
|
414
|
-
await fs.writeFile(migrationMarker, markerContent, "utf-8");
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
let output = dryRun
|
|
418
|
-
? "# Migration Preview (Dry Run)\n\n"
|
|
419
|
-
: "# Migration Complete\n\n";
|
|
420
|
-
|
|
421
|
-
output += `**Total files**: ${files.length}\n`;
|
|
422
|
-
output += `**Migrated**: ${results.migrated.length}\n`;
|
|
423
|
-
output += `**Skipped**: ${results.skipped.length}\n`;
|
|
424
|
-
output += `**Errors**: ${results.errors.length}\n\n`;
|
|
425
|
-
|
|
426
|
-
if (results.errors.length > 0) {
|
|
427
|
-
output += "## Errors\n\n";
|
|
428
|
-
for (const { file, error } of results.errors) {
|
|
429
|
-
output += `- **${file}**: ${error}\n`;
|
|
430
|
-
}
|
|
431
|
-
output += "\n";
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (dryRun && results.migrated.length > 0) {
|
|
435
|
-
output += "## Files to migrate\n\n";
|
|
436
|
-
for (const item of results.migrated.slice(0, 20)) {
|
|
437
|
-
output += `- ${item}\n`;
|
|
438
|
-
}
|
|
439
|
-
if (results.migrated.length > 20) {
|
|
440
|
-
output += `- ... and ${results.migrated.length - 20} more\n`;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return output;
|
|
445
|
-
}
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
import {
|
|
3
|
-
type ObservationRow,
|
|
4
|
-
getObservationsByIds,
|
|
5
|
-
} from "../plugin/lib/memory-db";
|
|
6
|
-
|
|
7
|
-
const TYPE_ICONS: Record<string, string> = {
|
|
8
|
-
decision: "🎯",
|
|
9
|
-
bugfix: "🐛",
|
|
10
|
-
feature: "✨",
|
|
11
|
-
pattern: "🔄",
|
|
12
|
-
discovery: "💡",
|
|
13
|
-
learning: "📚",
|
|
14
|
-
warning: "⚠️",
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const CONFIDENCE_ICONS: Record<string, string> = {
|
|
18
|
-
high: "🟢",
|
|
19
|
-
medium: "🟡",
|
|
20
|
-
low: "🔴",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function parseJsonArray(jsonStr: string | null): string[] {
|
|
24
|
-
if (!jsonStr) return [];
|
|
25
|
-
try {
|
|
26
|
-
return JSON.parse(jsonStr);
|
|
27
|
-
} catch {
|
|
28
|
-
return [];
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function formatFullObservation(obs: ObservationRow): string {
|
|
33
|
-
const icon = TYPE_ICONS[obs.type] || "📝";
|
|
34
|
-
const confIcon = CONFIDENCE_ICONS[obs.confidence] || "🟢";
|
|
35
|
-
const date = obs.created_at.split("T")[0];
|
|
36
|
-
|
|
37
|
-
let output = `# ${icon} #${obs.id}: ${obs.title}\n\n`;
|
|
38
|
-
|
|
39
|
-
// Metadata
|
|
40
|
-
output += `**Type**: ${obs.type} | **Confidence**: ${confIcon} ${obs.confidence} | **Created**: ${date}\n\n`;
|
|
41
|
-
|
|
42
|
-
if (obs.subtitle) {
|
|
43
|
-
output += `*${obs.subtitle}*\n\n`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Concepts
|
|
47
|
-
const concepts = parseJsonArray(obs.concepts);
|
|
48
|
-
if (concepts.length > 0) {
|
|
49
|
-
output += `**Concepts**: ${concepts.join(", ")}\n\n`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Files
|
|
53
|
-
const filesRead = parseJsonArray(obs.files_read);
|
|
54
|
-
const filesModified = parseJsonArray(obs.files_modified);
|
|
55
|
-
if (filesRead.length > 0) {
|
|
56
|
-
output += `**Files Read**: ${filesRead.join(", ")}\n`;
|
|
57
|
-
}
|
|
58
|
-
if (filesModified.length > 0) {
|
|
59
|
-
output += `**Files Modified**: ${filesModified.join(", ")}\n`;
|
|
60
|
-
}
|
|
61
|
-
if (filesRead.length > 0 || filesModified.length > 0) {
|
|
62
|
-
output += "\n";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Facts
|
|
66
|
-
const facts = parseJsonArray(obs.facts);
|
|
67
|
-
if (facts.length > 0) {
|
|
68
|
-
output += "## Key Facts\n\n";
|
|
69
|
-
for (const fact of facts) {
|
|
70
|
-
output += `- ${fact}\n`;
|
|
71
|
-
}
|
|
72
|
-
output += "\n";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Narrative
|
|
76
|
-
if (obs.narrative) {
|
|
77
|
-
output += "## Content\n\n";
|
|
78
|
-
output += obs.narrative;
|
|
79
|
-
output += "\n\n";
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Relationships
|
|
83
|
-
if (obs.bead_id) {
|
|
84
|
-
output += `**Linked Bead**: ${obs.bead_id}\n`;
|
|
85
|
-
}
|
|
86
|
-
if (obs.supersedes) {
|
|
87
|
-
output += `**Supersedes**: #${obs.supersedes}\n`;
|
|
88
|
-
}
|
|
89
|
-
if (obs.superseded_by) {
|
|
90
|
-
output += `⚠️ **Superseded by**: #${obs.superseded_by}\n`;
|
|
91
|
-
}
|
|
92
|
-
if (obs.valid_until) {
|
|
93
|
-
output += `**Valid until**: ${obs.valid_until}\n`;
|
|
94
|
-
}
|
|
95
|
-
if (obs.markdown_file) {
|
|
96
|
-
output += `**Source file**: ${obs.markdown_file}\n`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return output;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export default tool({
|
|
103
|
-
description: `Get full observation details by ID.
|
|
104
|
-
|
|
105
|
-
Purpose:
|
|
106
|
-
- Progressive disclosure: fetch full details after identifying relevant observations via search
|
|
107
|
-
- Get complete narrative, facts, and metadata
|
|
108
|
-
- Supports multiple IDs for batch retrieval
|
|
109
|
-
|
|
110
|
-
Example:
|
|
111
|
-
memory-get({ ids: "42" }) // Single observation
|
|
112
|
-
memory-get({ ids: "1,5,10" }) // Multiple observations`,
|
|
113
|
-
args: {
|
|
114
|
-
ids: tool.schema
|
|
115
|
-
.string()
|
|
116
|
-
.describe("Comma-separated observation IDs to retrieve"),
|
|
117
|
-
},
|
|
118
|
-
execute: async (args: { ids: string }) => {
|
|
119
|
-
const ids = args.ids
|
|
120
|
-
.split(",")
|
|
121
|
-
.map((id) => Number.parseInt(id.trim(), 10))
|
|
122
|
-
.filter((id) => !Number.isNaN(id));
|
|
123
|
-
|
|
124
|
-
if (ids.length === 0) {
|
|
125
|
-
return "No valid observation IDs provided.";
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const observations = getObservationsByIds(ids);
|
|
129
|
-
|
|
130
|
-
if (observations.length === 0) {
|
|
131
|
-
return `No observations found for IDs: ${args.ids}`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let output = `# Retrieved ${observations.length} Observation(s)\n\n`;
|
|
135
|
-
|
|
136
|
-
for (const obs of observations) {
|
|
137
|
-
output += formatFullObservation(obs);
|
|
138
|
-
output += "\n---\n\n";
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return output;
|
|
142
|
-
},
|
|
143
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
import { getMemoryFile } from "../plugin/lib/memory-db.js";
|
|
3
|
-
|
|
4
|
-
export default tool({
|
|
5
|
-
description: `Read memory files for persistent cross-session context.
|
|
6
|
-
|
|
7
|
-
Purpose:
|
|
8
|
-
- Retrieve project state, learnings, and active tasks
|
|
9
|
-
- Reads from SQLite database
|
|
10
|
-
- Supports subdirectories: handoffs/, research/
|
|
11
|
-
|
|
12
|
-
Example:
|
|
13
|
-
memory-read({ file: "handoffs/2024-01-20-phase-1" })
|
|
14
|
-
memory-read({ file: "research/2024-01-topic" })`,
|
|
15
|
-
args: {
|
|
16
|
-
file: tool.schema
|
|
17
|
-
.string()
|
|
18
|
-
.optional()
|
|
19
|
-
.describe(
|
|
20
|
-
"Memory file to read: handoffs/YYYY-MM-DD-phase, research/YYYY-MM-DD-topic",
|
|
21
|
-
),
|
|
22
|
-
},
|
|
23
|
-
execute: async (args: { file?: string }) => {
|
|
24
|
-
const fileName = args.file || "memory";
|
|
25
|
-
|
|
26
|
-
// Normalize: strip .md extension if present
|
|
27
|
-
const normalizedFile = fileName.replace(/\.md$/i, "");
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const dbRecord = getMemoryFile(normalizedFile);
|
|
31
|
-
if (dbRecord) {
|
|
32
|
-
const updatedInfo = dbRecord.updated_at
|
|
33
|
-
? ` (updated: ${dbRecord.updated_at})`
|
|
34
|
-
: "";
|
|
35
|
-
return `[${normalizedFile}${updatedInfo}]\n\n${dbRecord.content}`;
|
|
36
|
-
}
|
|
37
|
-
} catch (error) {
|
|
38
|
-
if (error instanceof Error) {
|
|
39
|
-
return `Error reading memory: ${error.message}`;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return `Memory file '${normalizedFile}' not found.\n\nStructure:\n- handoffs/YYYY-MM-DD-phase (phase transitions)\n- research/YYYY-MM-DD-topic (research findings)`;
|
|
44
|
-
},
|
|
45
|
-
});
|