opencodekit 0.18.22 → 0.18.24

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 CHANGED
@@ -20,7 +20,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  //#endregion
22
22
  //#region package.json
23
- var version = "0.18.22";
23
+ var version = "0.18.24";
24
24
 
25
25
  //#endregion
26
26
  //#region src/utils/license.ts
Binary file
@@ -11,7 +11,7 @@
11
11
  "type-check": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
- "@opencode-ai/plugin": "1.2.27"
14
+ "@opencode-ai/plugin": "1.3.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/node": "^25.3.0",
@@ -6,8 +6,27 @@
6
6
  */
7
7
 
8
8
  import { Database } from "bun:sqlite";
9
+ import { appendFileSync, existsSync, renameSync } from "node:fs";
9
10
  import path from "node:path";
10
11
 
12
+ // ============================================================================
13
+ // Recovery Logger
14
+ // ============================================================================
15
+
16
+ /**
17
+ * Log recovery messages to a file instead of stderr.
18
+ * Writing to stderr corrupts the TUI in OpenCode.
19
+ */
20
+ function logRecovery(message: string): void {
21
+ try {
22
+ const logPath = path.join(process.cwd(), ".opencode/memory-recovery.log");
23
+ const timestamp = new Date().toISOString();
24
+ appendFileSync(logPath, `[${timestamp}] ${message}\n`);
25
+ } catch {
26
+ // If we can't even write a log file, silently continue
27
+ }
28
+ }
29
+
11
30
  // ============================================================================
12
31
  // Schema v2
13
32
  // ============================================================================
@@ -238,7 +257,64 @@ export function getMemoryDB(): Database {
238
257
  if (dbInstance) return dbInstance;
239
258
 
240
259
  const dbPath = path.join(process.cwd(), ".opencode/memory.db");
241
- dbInstance = new Database(dbPath, { create: true });
260
+
261
+ try {
262
+ dbInstance = new Database(dbPath, { create: true });
263
+ } catch (err) {
264
+ // Database file may be corrupted — attempt recovery
265
+ const recovered = attemptDBRecovery(dbPath, err);
266
+ if (!recovered) {
267
+ throw new Error(
268
+ `Failed to open memory database: ${err instanceof Error ? err.message : String(err)}. ` +
269
+ `Recovery also failed. Try manually deleting ${dbPath} to start fresh.`,
270
+ );
271
+ }
272
+ dbInstance = recovered;
273
+ }
274
+
275
+ // Verify database integrity
276
+ try {
277
+ const result = dbInstance.query("PRAGMA integrity_check").get() as {
278
+ integrity_check: string;
279
+ } | null;
280
+ if (result && result.integrity_check !== "ok") {
281
+ logRecovery(
282
+ `[memory-db] Integrity check failed: ${result.integrity_check}`,
283
+ );
284
+ // Close bad instance and attempt recovery
285
+ dbInstance.close();
286
+ dbInstance = null;
287
+ const recovered = attemptDBRecovery(dbPath, new Error("integrity check failed"));
288
+ if (!recovered) {
289
+ throw new Error(
290
+ `Memory database integrity check failed and recovery failed. ` +
291
+ `Try manually deleting ${dbPath} to start fresh.`,
292
+ );
293
+ }
294
+ dbInstance = recovered;
295
+ }
296
+ } catch (err) {
297
+ if (
298
+ err instanceof Error &&
299
+ err.message.includes("recovery failed")
300
+ ) {
301
+ throw err;
302
+ }
303
+ // integrity_check itself failed — try recovery
304
+ logRecovery(
305
+ `[memory-db] Integrity check query failed: ${err instanceof Error ? err.message : String(err)}`,
306
+ );
307
+ dbInstance?.close();
308
+ dbInstance = null;
309
+ const recovered = attemptDBRecovery(dbPath, err);
310
+ if (!recovered) {
311
+ throw new Error(
312
+ `Memory database is corrupted and recovery failed. ` +
313
+ `Try manually deleting ${dbPath} to start fresh.`,
314
+ );
315
+ }
316
+ dbInstance = recovered;
317
+ }
242
318
 
243
319
  // Enable WAL mode for better concurrency
244
320
  dbInstance.run("PRAGMA journal_mode = WAL");
@@ -250,6 +326,77 @@ export function getMemoryDB(): Database {
250
326
  return dbInstance;
251
327
  }
252
328
 
329
+ /**
330
+ * Attempt to recover from a corrupted database.
331
+ * Strategy: WAL checkpoint first, then backup corrupt file + create fresh.
332
+ */
333
+ function attemptDBRecovery(
334
+ dbPath: string,
335
+ originalError: unknown,
336
+ ): Database | null {
337
+ logRecovery(
338
+ `[memory-db] Database recovery triggered: ${originalError instanceof Error ? originalError.message : String(originalError)}`,
339
+ );
340
+
341
+ // Step 1: Try WAL checkpoint recovery (if file exists and is openable)
342
+ try {
343
+ if (existsSync(dbPath)) {
344
+ let tempDB: Database | undefined;
345
+ try {
346
+ tempDB = new Database(dbPath);
347
+ tempDB.run("PRAGMA wal_checkpoint(TRUNCATE)");
348
+ tempDB.close();
349
+ tempDB = undefined;
350
+ } catch {
351
+ tempDB?.close();
352
+ throw new Error("WAL checkpoint failed");
353
+ }
354
+ // Try reopening after WAL checkpoint
355
+ const db = new Database(dbPath, { create: true });
356
+ const check = db.query("PRAGMA integrity_check").get() as {
357
+ integrity_check: string;
358
+ } | null;
359
+ if (check?.integrity_check === "ok") {
360
+ logRecovery("[memory-db] WAL checkpoint recovery succeeded.");
361
+ return db;
362
+ }
363
+ db.close();
364
+ }
365
+ } catch {
366
+ // WAL recovery failed, continue to backup + recreate
367
+ }
368
+
369
+ // Step 2: Backup corrupt file and create fresh database
370
+ try {
371
+ if (existsSync(dbPath)) {
372
+ const backupPath = `${dbPath}.corrupt.${Date.now()}`;
373
+ renameSync(dbPath, backupPath);
374
+ logRecovery(
375
+ `[memory-db] Corrupt database backed up to: ${backupPath}`,
376
+ );
377
+
378
+ // Also clean up WAL/SHM files
379
+ for (const suffix of ["-wal", "-shm"]) {
380
+ const walPath = dbPath + suffix;
381
+ if (existsSync(walPath)) {
382
+ renameSync(walPath, `${backupPath}${suffix}`);
383
+ }
384
+ }
385
+ }
386
+
387
+ const freshDB = new Database(dbPath, { create: true });
388
+ logRecovery(
389
+ "[memory-db] Fresh database created. Previous observations are in the backup file.",
390
+ );
391
+ return freshDB;
392
+ } catch (backupErr) {
393
+ logRecovery(
394
+ `[memory-db] Recovery failed: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`,
395
+ );
396
+ return null;
397
+ }
398
+ }
399
+
253
400
  /**
254
401
  * Close the database connection (for cleanup).
255
402
  */
@@ -54,133 +54,148 @@ export function createAdminTools(deps: AdminToolDeps) {
54
54
  force: tool.schema.boolean().optional().describe("Force re-migration"),
55
55
  },
56
56
  execute: async (args, ctx) => {
57
- const op = args.operation ?? "status";
58
- const dryRun = args.dry_run ?? false;
59
- const olderThanDays = args.older_than_days ?? 90;
57
+ try {
58
+ const op = args.operation ?? "status";
59
+ const dryRun = args.dry_run ?? false;
60
+ const olderThanDays = args.older_than_days ?? 90;
60
61
 
61
- switch (op) {
62
- case "status": {
63
- const sizes = getDatabaseSizes();
64
- const stats = getObservationStats();
65
- const archivable = archiveOldObservations({
66
- olderThanDays,
67
- dryRun: true,
68
- });
69
- const captureStats = getCaptureStats();
70
- const distillStats = getDistillationStats();
71
- return [
72
- "## Memory System Status\n",
73
- `**Database**: ${(sizes.total / 1024).toFixed(1)} KB`,
74
- `**FTS5**: ${checkFTS5Available() ? "Available (porter stemming)" : "Unavailable"}`,
75
- `**Schema**: v2 (4-tier storage)\n`,
76
- "### Observations",
77
- ...Object.entries(stats).map(([k, v]) => ` ${k}: ${v}`),
78
- ` Archivable (>${olderThanDays}d): ${archivable}\n`,
79
- "### Capture Pipeline",
80
- ` Messages: ${captureStats.total} (undistilled: ${captureStats.undistilled})`,
81
- ` Sessions: ${captureStats.sessions}\n`,
82
- "### Distillations",
83
- ` Total: ${distillStats.total} (${distillStats.sessions} sessions)`,
84
- ` Avg compression: ${(distillStats.avgCompression * 100).toFixed(1)}%`,
85
- ].join("\n");
86
- }
87
- case "full": {
88
- if (dryRun)
89
- return `Dry run: would archive, purge, optimize, checkpoint, vacuum.`;
90
- const r = runFullMaintenance({
91
- olderThanDays,
92
- includeSuperseded: true,
93
- });
94
- return `Done: archived ${r.archived}, purged ${r.purgedMessages} msgs, freed ${(r.freedBytes / 1024).toFixed(1)} KB.`;
95
- }
96
- case "archive": {
97
- const c = archiveOldObservations({
98
- olderThanDays,
99
- includeSuperseded: true,
100
- dryRun,
101
- });
102
- return dryRun
103
- ? `Would archive ${c} observations.`
104
- : `Archived ${c} observations.`;
105
- }
106
- case "checkpoint": {
107
- const r = checkpointWAL();
108
- return r.checkpointed
109
- ? `WAL checkpointed (${r.walSize} pages).`
110
- : "Checkpoint failed or busy.";
111
- }
112
- case "vacuum":
113
- return vacuumDatabase() ? "Vacuumed." : "Vacuum failed.";
114
- case "capture-stats":
115
- return JSON.stringify(getCaptureStats(), null, 2);
116
- case "distill-now": {
117
- const sid = ctx?.sessionID;
118
- if (!sid) return "Error: No session ID.";
119
- const did = distillSession(sid);
120
- return did
121
- ? `Distillation #${did} created.`
122
- : "Not enough undistilled messages.";
123
- }
124
- case "curate-now": {
125
- const r = curateFromDistillations();
126
- return `Created ${r.created}, skipped ${r.skipped}. Patterns: ${JSON.stringify(r.patterns)}`;
127
- }
128
- case "migrate": {
129
- const obsDir = path.join(
130
- directory,
131
- ".opencode",
132
- "memory",
133
- "observations",
134
- );
135
- let mdFiles: string[] = [];
136
- try {
137
- mdFiles = (await readdir(obsDir)).filter((f) =>
138
- f.endsWith(".md"),
139
- );
140
- } catch {
141
- return "No observations directory found.";
62
+ switch (op) {
63
+ case "status": {
64
+ const sizes = getDatabaseSizes();
65
+ const stats = getObservationStats();
66
+ const archivable = archiveOldObservations({
67
+ olderThanDays,
68
+ dryRun: true,
69
+ });
70
+ const captureStats = getCaptureStats();
71
+ const distillStats = getDistillationStats();
72
+ return [
73
+ "## Memory System Status\n",
74
+ `**Database**: ${(sizes.total / 1024).toFixed(1)} KB`,
75
+ `**FTS5**: ${checkFTS5Available() ? "Available (porter stemming)" : "Unavailable"}`,
76
+ `**Schema**: v2 (4-tier storage)\n`,
77
+ "### Observations",
78
+ ...Object.entries(stats).map(([k, v]) => ` ${k}: ${v}`),
79
+ ` Archivable (>${olderThanDays}d): ${archivable}\n`,
80
+ "### Capture Pipeline",
81
+ ` Messages: ${captureStats.total} (undistilled: ${captureStats.undistilled})`,
82
+ ` Sessions: ${captureStats.sessions}\n`,
83
+ "### Distillations",
84
+ ` Total: ${distillStats.total} (${distillStats.sessions} sessions)`,
85
+ ` Avg compression: ${(distillStats.avgCompression * 100).toFixed(1)}%`,
86
+ ].join("\n");
87
+ }
88
+ case "full": {
89
+ if (dryRun)
90
+ return `Dry run: would archive, purge, optimize, checkpoint, vacuum.`;
91
+ const r = runFullMaintenance({
92
+ olderThanDays,
93
+ includeSuperseded: true,
94
+ });
95
+ return `Done: archived ${r.archived}, purged ${r.purgedMessages} msgs, freed ${(r.freedBytes / 1024).toFixed(1)} KB.`;
142
96
  }
143
- if (mdFiles.length === 0) return "No files to migrate.";
144
- const existing = new Set(getMarkdownFilesInSqlite());
145
- const toMigrate = args.force
146
- ? mdFiles
147
- : mdFiles.filter((f) => !existing.has(f));
148
- if (toMigrate.length === 0) return "All files already migrated.";
149
- if (dryRun) return `Would migrate ${toMigrate.length} files.`;
150
- let migrated = 0;
151
- for (const file of toMigrate) {
97
+ case "archive": {
98
+ const c = archiveOldObservations({
99
+ olderThanDays,
100
+ includeSuperseded: true,
101
+ dryRun,
102
+ });
103
+ return dryRun
104
+ ? `Would archive ${c} observations.`
105
+ : `Archived ${c} observations.`;
106
+ }
107
+ case "checkpoint": {
108
+ const r = checkpointWAL();
109
+ return r.checkpointed
110
+ ? `WAL checkpointed (${r.walSize} pages).`
111
+ : "Checkpoint failed or busy.";
112
+ }
113
+ case "vacuum":
114
+ return vacuumDatabase() ? "Vacuumed." : "Vacuum failed.";
115
+ case "capture-stats":
116
+ return JSON.stringify(getCaptureStats(), null, 2);
117
+ case "distill-now": {
118
+ const sid = ctx?.sessionID;
119
+ if (!sid) return "Error: No session ID.";
120
+ const did = distillSession(sid);
121
+ return did
122
+ ? `Distillation #${did} created.`
123
+ : "Not enough undistilled messages.";
124
+ }
125
+ case "curate-now": {
126
+ const r = curateFromDistillations();
127
+ return `Created ${r.created}, skipped ${r.skipped}. Patterns: ${JSON.stringify(r.patterns)}`;
128
+ }
129
+ case "migrate": {
130
+ const obsDir = path.join(
131
+ directory,
132
+ ".opencode",
133
+ "memory",
134
+ "observations",
135
+ );
136
+ let mdFiles: string[] = [];
152
137
  try {
153
- const content = await readFile(
154
- path.join(obsDir, file),
155
- "utf-8",
138
+ mdFiles = (await readdir(obsDir)).filter((f) =>
139
+ f.endsWith(".md"),
156
140
  );
157
- const fmMatch = content.match(
158
- /^---\n([\s\S]*?)\n---\n([\s\S]*)$/,
159
- );
160
- const body = fmMatch ? fmMatch[2].trim() : content.trim();
161
- const fm = fmMatch ? fmMatch[1] : "";
162
- storeObservation({
163
- type: (fm.match(/type:\s*(\w+)/)?.[1] ??
164
- "discovery") as ObservationType,
165
- title:
166
- fm.match(/title:\s*(.+)/)?.[1]?.trim() ??
167
- file.replace(/\.md$/, ""),
168
- narrative: body,
169
- confidence: (fm.match(/confidence:\s*(\w+)/)?.[1] ??
170
- "medium") as ConfidenceLevel,
171
- markdown_file: file,
172
- source: "imported",
173
- });
174
- migrated++;
175
141
  } catch {
176
- /* Skip failed files */
142
+ return "No observations directory found.";
143
+ }
144
+ if (mdFiles.length === 0) return "No files to migrate.";
145
+ const existing = new Set(getMarkdownFilesInSqlite());
146
+ const toMigrate = args.force
147
+ ? mdFiles
148
+ : mdFiles.filter((f) => !existing.has(f));
149
+ if (toMigrate.length === 0) return "All files already migrated.";
150
+ if (dryRun) return `Would migrate ${toMigrate.length} files.`;
151
+ let migrated = 0;
152
+ for (const file of toMigrate) {
153
+ try {
154
+ const content = await readFile(
155
+ path.join(obsDir, file),
156
+ "utf-8",
157
+ );
158
+ const fmMatch = content.match(
159
+ /^---\n([\s\S]*?)\n---\n([\s\S]*)$/,
160
+ );
161
+ const body = fmMatch ? fmMatch[2].trim() : content.trim();
162
+ const fm = fmMatch ? fmMatch[1] : "";
163
+ storeObservation({
164
+ type: (fm.match(/type:\s*(\w+)/)?.[1] ??
165
+ "discovery") as ObservationType,
166
+ title:
167
+ fm.match(/title:\s*(.+)/)?.[1]?.trim() ??
168
+ file.replace(/\.md$/, ""),
169
+ narrative: body,
170
+ confidence: (fm.match(/confidence:\s*(\w+)/)?.[1] ??
171
+ "medium") as ConfidenceLevel,
172
+ markdown_file: file,
173
+ source: "imported",
174
+ });
175
+ migrated++;
176
+ } catch {
177
+ /* Skip failed files */
178
+ }
177
179
  }
180
+ if (migrated > 0) rebuildFTS5();
181
+ return `Migrated ${migrated}/${toMigrate.length} files.`;
178
182
  }
179
- if (migrated > 0) rebuildFTS5();
180
- return `Migrated ${migrated}/${toMigrate.length} files.`;
183
+ default:
184
+ return `Unknown operation: "${op}".`;
185
+ }
186
+ } catch (err) {
187
+ const message = err instanceof Error ? err.message : String(err);
188
+ if (
189
+ message.includes("database disk image is malformed") ||
190
+ message.includes("SQLITE_CORRUPT") ||
191
+ message.includes("integrity check failed")
192
+ ) {
193
+ return (
194
+ `Error: Memory database is corrupted. ` +
195
+ `Automatic repair failed. Delete .opencode/memory.db to start fresh. Details: ${message}`
196
+ );
181
197
  }
182
- default:
183
- return `Unknown operation: "${op}".`;
198
+ return `Error: Admin operation failed: ${message}`;
184
199
  }
185
200
  },
186
201
  }),
@@ -43,15 +43,29 @@ interface HookDeps {
43
43
  function extractErrorMessage(value: unknown, maxLen = 200): string {
44
44
  if (!value) return "Unknown error";
45
45
  if (typeof value === "string") return value.slice(0, maxLen);
46
- if (value instanceof Error) return (value.message || value.name || "Error").slice(0, maxLen);
46
+ if (value instanceof Error)
47
+ return (value.message || value.name || "Error").slice(0, maxLen);
47
48
  if (typeof value === "object" && value !== null) {
48
49
  const obj = value as Record<string, unknown>;
50
+
51
+ // Handle OpenCode error structure: { name, data: { message, statusCode } }
52
+ if (typeof obj.data === "object" && obj.data !== null) {
53
+ const data = obj.data as Record<string, unknown>;
54
+ if (typeof data.message === "string") {
55
+ const prefix = typeof obj.name === "string" ? `${obj.name}: ` : "";
56
+ const status =
57
+ typeof data.statusCode === "number" ? ` (${data.statusCode})` : "";
58
+ return `${prefix}${data.message}${status}`.slice(0, maxLen);
59
+ }
60
+ }
61
+
49
62
  // Common error shapes: { message }, { error }, { error: { message } }
50
63
  if (typeof obj.message === "string") return obj.message.slice(0, maxLen);
51
64
  if (typeof obj.error === "string") return obj.error.slice(0, maxLen);
52
65
  if (typeof obj.error === "object" && obj.error !== null) {
53
66
  const inner = obj.error as Record<string, unknown>;
54
- if (typeof inner.message === "string") return inner.message.slice(0, maxLen);
67
+ if (typeof inner.message === "string")
68
+ return inner.message.slice(0, maxLen);
55
69
  }
56
70
  // Last resort: JSON stringify with truncation
57
71
  try {
@@ -123,7 +137,22 @@ export function createHooks(deps: HookDeps) {
123
137
  // --- Session error: classify and guide ---
124
138
  if (event.type === "session.error") {
125
139
  const props = event.properties as Record<string, unknown> | undefined;
126
- const errorMsg = extractErrorMessage(props?.error ?? props?.message ?? "Unknown error");
140
+ const errorObj = props?.error ?? props?.message ?? "Unknown error";
141
+ const errorMsg = extractErrorMessage(errorObj);
142
+
143
+ // Extract status code from error object for classification
144
+ const rawCode =
145
+ typeof errorObj === "object" && errorObj !== null
146
+ ? ((errorObj as Record<string, unknown>).data as Record<string, unknown>
147
+ )?.statusCode ??
148
+ (errorObj as Record<string, unknown>).statusCode
149
+ : undefined;
150
+ const statusCode =
151
+ typeof rawCode === "number"
152
+ ? rawCode
153
+ : typeof rawCode === "string"
154
+ ? Number(rawCode) || undefined
155
+ : undefined;
127
156
 
128
157
  // Log full error for debugging
129
158
  await log(`Session error: ${errorMsg}`, "warn");
@@ -136,13 +165,22 @@ export function createHooks(deps: HookDeps) {
136
165
  ) {
137
166
  guidance = "Context too large — use /compact or start a new session";
138
167
  } else if (
139
- /rate.?limit|429|too many requests/i.test(errorMsg)
168
+ /rate.?limit|too many requests/i.test(errorMsg) ||
169
+ statusCode === 429
140
170
  ) {
141
171
  guidance = "Rate limited — wait a moment and retry";
142
172
  } else if (
143
- /unauthorized|401|403|auth/i.test(errorMsg)
173
+ /unauthorized|auth/i.test(errorMsg) ||
174
+ statusCode === 401 ||
175
+ statusCode === 403
144
176
  ) {
145
177
  guidance = "Auth error — check API key or token";
178
+ } else if (
179
+ statusCode === 400 ||
180
+ /bad request|invalid.*request/i.test(errorMsg)
181
+ ) {
182
+ guidance =
183
+ "Bad request — try starting a new session or using /compact";
146
184
  } else if (
147
185
  /timeout|ETIMEDOUT|ECONNRESET|network|fetch failed/i.test(errorMsg)
148
186
  ) {
@@ -152,14 +190,20 @@ export function createHooks(deps: HookDeps) {
152
190
  ) {
153
191
  guidance = "API format error — try starting a new session";
154
192
  } else if (
155
- /500|502|503|504|internal server|service unavailable/i.test(errorMsg)
193
+ statusCode === 500 ||
194
+ statusCode === 502 ||
195
+ statusCode === 503 ||
196
+ statusCode === 504 ||
197
+ /internal server|service unavailable/i.test(errorMsg)
156
198
  ) {
157
199
  guidance = "Server error — retry in a few seconds";
158
200
  } else {
159
- guidance = "Unexpected error — save work with observation tool if needed";
201
+ guidance =
202
+ "Unexpected error — save work with observation tool if needed";
160
203
  }
161
204
 
162
- const short = errorMsg.length > 80 ? `${errorMsg.slice(0, 80)}…` : errorMsg;
205
+ const short =
206
+ errorMsg.length > 100 ? `${errorMsg.slice(0, 100)}…` : errorMsg;
163
207
  await showToast("Session Error", `${guidance} (${short})`, "warning");
164
208
  }
165
209
  },
@@ -32,6 +32,35 @@ import {
32
32
  VALID_TYPES,
33
33
  } from "./memory-helpers.js";
34
34
 
35
+ /**
36
+ * Wrap a memory tool execute function with DB error handling.
37
+ * Returns a user-friendly error message instead of raw SQLite crashes.
38
+ */
39
+ function withDBErrorHandling<T extends Record<string, unknown>>(
40
+ fn: (args: T) => Promise<string>,
41
+ ): (args: T) => Promise<string> {
42
+ return async (args: T) => {
43
+ try {
44
+ return await fn(args);
45
+ } catch (err) {
46
+ const message =
47
+ err instanceof Error ? err.message : String(err);
48
+ if (
49
+ message.includes("database disk image is malformed") ||
50
+ message.includes("SQLITE_CORRUPT") ||
51
+ message.includes("integrity check failed")
52
+ ) {
53
+ return (
54
+ `Error: Memory database is corrupted. ` +
55
+ `Run \`memory-admin({ operation: "full" })\` to attempt repair, ` +
56
+ `or delete .opencode/memory.db to start fresh. Details: ${message}`
57
+ );
58
+ }
59
+ return `Error: Memory operation failed: ${message}`;
60
+ }
61
+ };
62
+ }
63
+
35
64
  interface CoreToolDeps {
36
65
  handoffDir: string;
37
66
  }
@@ -89,7 +118,7 @@ export function createCoreTools(deps: CoreToolDeps) {
89
118
  .optional()
90
119
  .describe("manual, curator, imported"),
91
120
  },
92
- execute: async (args) => {
121
+ execute: withDBErrorHandling(async (args) => {
93
122
  const obsType = args.type as ObservationType;
94
123
  if (!VALID_TYPES.includes(obsType)) {
95
124
  return `Error: Invalid type "${args.type}". Valid: ${VALID_TYPES.join(", ")}`;
@@ -144,7 +173,7 @@ export function createCoreTools(deps: CoreToolDeps) {
144
173
  });
145
174
 
146
175
  return `${TYPE_ICONS[obsType] ?? "\uD83D\uDCCC"} Observation #${id} stored [${obsType}] "${args.title}" (confidence: ${confidence}, source: ${source})`;
147
- },
176
+ }),
148
177
  }),
149
178
 
150
179
  "memory-search": tool({
@@ -160,7 +189,7 @@ export function createCoreTools(deps: CoreToolDeps) {
160
189
  .optional()
161
190
  .describe("Max results (default: 10)"),
162
191
  },
163
- execute: async (args) => {
192
+ execute: withDBErrorHandling(async (args) => {
164
193
  const query = args.query.trim();
165
194
  if (!query) return "Error: Empty search query";
166
195
  const limit = args.limit ?? 10;
@@ -237,7 +266,7 @@ export function createCoreTools(deps: CoreToolDeps) {
237
266
  }
238
267
 
239
268
  return lines.length > 0 ? lines.join("\n") : "No results found.";
240
- },
269
+ }),
241
270
  }),
242
271
 
243
272
  "memory-get": tool({
@@ -245,7 +274,7 @@ export function createCoreTools(deps: CoreToolDeps) {
245
274
  args: {
246
275
  ids: tool.schema.string().describe("Comma-separated observation IDs"),
247
276
  },
248
- execute: async (args) => {
277
+ execute: withDBErrorHandling(async (args) => {
249
278
  const idList = args.ids
250
279
  .split(",")
251
280
  .map((s) => Number.parseInt(s.trim(), 10))
@@ -257,7 +286,7 @@ export function createCoreTools(deps: CoreToolDeps) {
257
286
  return observations
258
287
  .map((obs) => formatObservation(obs))
259
288
  .join("\n\n---\n\n");
260
- },
289
+ }),
261
290
  }),
262
291
 
263
292
  "memory-read": tool({
@@ -265,12 +294,12 @@ export function createCoreTools(deps: CoreToolDeps) {
265
294
  args: {
266
295
  file: tool.schema.string().optional().describe("Memory file path"),
267
296
  },
268
- execute: async (args) => {
297
+ execute: withDBErrorHandling(async (args) => {
269
298
  const filePath = (args.file ?? "").replace(/\.md$/, "");
270
299
  if (!filePath) return "Error: No file path provided";
271
300
  const row = getMemoryFile(filePath);
272
301
  return row ? row.content : `Memory file "${filePath}" not found.`;
273
- },
302
+ }),
274
303
  }),
275
304
 
276
305
  "memory-update": tool({
@@ -283,7 +312,7 @@ export function createCoreTools(deps: CoreToolDeps) {
283
312
  .optional()
284
313
  .describe("replace (default) or append"),
285
314
  },
286
- execute: async (args) => {
315
+ execute: withDBErrorHandling(async (args) => {
287
316
  const filePath = args.file.replace(/\.md$/, "");
288
317
  const mode = (args.mode ?? "replace") as "replace" | "append";
289
318
  let finalContent = args.content;
@@ -292,7 +321,7 @@ export function createCoreTools(deps: CoreToolDeps) {
292
321
  }
293
322
  upsertMemoryFile(filePath, finalContent, mode);
294
323
  return `Memory file "${filePath}" updated (mode: ${mode}).`;
295
- },
324
+ }),
296
325
  }),
297
326
 
298
327
  "memory-timeline": tool({
@@ -308,7 +337,7 @@ export function createCoreTools(deps: CoreToolDeps) {
308
337
  .optional()
309
338
  .describe("Later observations (default: 5)"),
310
339
  },
311
- execute: async (args) => {
340
+ execute: withDBErrorHandling(async (args) => {
312
341
  const { anchor, before, after } = getTimelineAroundObservation(
313
342
  args.anchor_id,
314
343
  args.depth_before ?? 5,
@@ -335,7 +364,7 @@ export function createCoreTools(deps: CoreToolDeps) {
335
364
  );
336
365
  }
337
366
  return lines.join("\n");
338
- },
367
+ }),
339
368
  }),
340
369
  };
341
370
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencodekit",
3
- "version": "0.18.22",
3
+ "version": "0.18.24",
4
4
  "description": "CLI tool for bootstrapping and managing OpenCodeKit projects",
5
5
  "keywords": [
6
6
  "agents",