codemem 0.20.0-alpha.2 → 0.20.0-alpha.4

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.
Files changed (34) hide show
  1. package/.opencode/{plugin → plugins}/codemem.js +98 -113
  2. package/dist/commands/claude-hook-ingest.d.ts.map +1 -1
  3. package/dist/commands/db.d.ts.map +1 -1
  4. package/dist/commands/enqueue-raw-event.d.ts.map +1 -1
  5. package/dist/commands/export-memories.d.ts.map +1 -1
  6. package/dist/commands/import-memories.d.ts.map +1 -1
  7. package/dist/commands/mcp.d.ts.map +1 -1
  8. package/dist/commands/memory.d.ts +3 -0
  9. package/dist/commands/memory.d.ts.map +1 -1
  10. package/dist/commands/memory.test.d.ts +2 -0
  11. package/dist/commands/memory.test.d.ts.map +1 -0
  12. package/dist/commands/pack.d.ts.map +1 -1
  13. package/dist/commands/recent.d.ts.map +1 -1
  14. package/dist/commands/search.d.ts.map +1 -1
  15. package/dist/commands/serve-invocation.d.ts +37 -0
  16. package/dist/commands/serve-invocation.d.ts.map +1 -0
  17. package/dist/commands/serve.d.ts +2 -0
  18. package/dist/commands/serve.d.ts.map +1 -1
  19. package/dist/commands/serve.test.d.ts +2 -0
  20. package/dist/commands/serve.test.d.ts.map +1 -0
  21. package/dist/commands/setup-config.d.ts +4 -0
  22. package/dist/commands/setup-config.d.ts.map +1 -0
  23. package/dist/commands/setup-config.test.d.ts +2 -0
  24. package/dist/commands/setup-config.test.d.ts.map +1 -0
  25. package/dist/commands/setup.d.ts.map +1 -1
  26. package/dist/commands/stats.d.ts.map +1 -1
  27. package/dist/commands/sync-helpers.d.ts +31 -0
  28. package/dist/commands/sync-helpers.d.ts.map +1 -0
  29. package/dist/commands/sync.d.ts.map +1 -1
  30. package/dist/commands/sync.test.d.ts +2 -0
  31. package/dist/commands/sync.test.d.ts.map +1 -0
  32. package/dist/index.js +801 -98
  33. package/dist/index.js.map +1 -1
  34. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { MemoryStore, ObserverClient, RawEventSweeper, VERSION, buildRawEventEnvelopeFromHook, connect, ensureDeviceIdentity, exportMemories, getRawEventStatus, importMemories, initDatabase, loadSqliteVec, rawEventsGate, readCodememConfigFile, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, schema, stripJsonComments, stripPrivateObj, stripTrailingCommas, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
2
+ import { MemoryStore, ObserverClient, RawEventSweeper, VERSION, buildRawEventEnvelopeFromHook, connect, ensureDeviceIdentity, exportMemories, fingerprintPublicKey, getRawEventStatus, importMemories, initDatabase, loadPublicKey, loadSqliteVec, rawEventsGate, readCodememConfigFile, readImportPayload, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command } from "commander";
4
4
  import omelette from "omelette";
5
- import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
6
6
  import { styleText } from "node:util";
7
7
  import * as p from "@clack/prompts";
8
- import { homedir } from "node:os";
8
+ import { homedir, networkInterfaces } from "node:os";
9
9
  import { dirname, join } from "node:path";
10
- import { desc } from "drizzle-orm";
10
+ import { spawn } from "node:child_process";
11
+ import net from "node:net";
12
+ import { desc, eq } from "drizzle-orm";
11
13
  import { drizzle } from "drizzle-orm/better-sqlite3";
12
14
  //#region src/help-style.ts
13
15
  /**
@@ -117,7 +119,7 @@ function directEnqueue(payload, dbPath) {
117
119
  db.close();
118
120
  }
119
121
  }
120
- var claudeHookIngestCommand = new Command("claude-hook-ingest").configureHelp(helpStyle).description("Ingest a Claude Code hook payload from stdin").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--host <host>", "viewer server host", "127.0.0.1").option("--port <port>", "viewer server port", "38888").action(async (opts) => {
122
+ var claudeHookIngestCommand = new Command("claude-hook-ingest").configureHelp(helpStyle).description("Ingest a Claude Code hook payload from stdin").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--host <host>", "viewer server host", "127.0.0.1").option("--port <port>", "viewer server port", "38888").action(async (opts) => {
121
123
  let raw;
122
124
  try {
123
125
  raw = readFileSync(0, "utf8").trim();
@@ -153,7 +155,7 @@ var claudeHookIngestCommand = new Command("claude-hook-ingest").configureHelp(he
153
155
  return;
154
156
  }
155
157
  try {
156
- const dbPath = resolveDbPath(opts.db);
158
+ const dbPath = resolveDbPath(opts.db ?? opts.dbPath);
157
159
  const directResult = directEnqueue(payload, dbPath);
158
160
  console.log(JSON.stringify({
159
161
  ...directResult,
@@ -165,19 +167,24 @@ var claudeHookIngestCommand = new Command("claude-hook-ingest").configureHelp(he
165
167
  });
166
168
  //#endregion
167
169
  //#region src/commands/db.ts
170
+ function formatBytes(bytes) {
171
+ if (bytes < 1024) return `${bytes} B`;
172
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
173
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
174
+ }
168
175
  var dbCommand = new Command("db").configureHelp(helpStyle).description("Database maintenance");
169
- dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("Verify the SQLite database is present and schema-ready").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action((opts) => {
170
- const result = initDatabase(opts.db);
176
+ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("Verify the SQLite database is present and schema-ready").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action((opts) => {
177
+ const result = initDatabase(opts.db ?? opts.dbPath);
171
178
  p.intro("codemem db init");
172
179
  p.log.success(`Database ready: ${result.path}`);
173
180
  p.outro(`Size: ${result.sizeBytes.toLocaleString()} bytes`);
174
- })).addCommand(new Command("vacuum").configureHelp(helpStyle).description("Run VACUUM on the SQLite database").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action((opts) => {
175
- const result = vacuumDatabase(opts.db);
181
+ })).addCommand(new Command("vacuum").configureHelp(helpStyle).description("Run VACUUM on the SQLite database").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action((opts) => {
182
+ const result = vacuumDatabase(opts.db ?? opts.dbPath);
176
183
  p.intro("codemem db vacuum");
177
184
  p.log.success(`Vacuumed: ${result.path}`);
178
185
  p.outro(`Size: ${result.sizeBytes.toLocaleString()} bytes`);
179
- })).addCommand(new Command("raw-events-status").configureHelp(helpStyle).description("Show pending raw-event backlog by source stream").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max rows to show", "25").option("--json", "output as JSON").action((opts) => {
180
- const result = getRawEventStatus(opts.db, Number.parseInt(opts.limit, 10) || 25);
186
+ })).addCommand(new Command("raw-events-status").configureHelp(helpStyle).description("Show pending raw-event backlog by source stream").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max rows to show", "25").option("--json", "output as JSON").action((opts) => {
187
+ const result = getRawEventStatus(opts.db ?? opts.dbPath, Number.parseInt(opts.limit, 10) || 25);
181
188
  if (opts.json) {
182
189
  console.log(JSON.stringify(result, null, 2));
183
190
  return;
@@ -190,12 +197,12 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("V
190
197
  }
191
198
  for (const item of result.items) p.log.message(`${item.source}:${item.stream_id} pending=${Math.max(0, item.last_received_event_seq - item.last_flushed_event_seq)} received=${item.last_received_event_seq} flushed=${item.last_flushed_event_seq} project=${item.project ?? ""}`);
192
199
  p.outro("done");
193
- })).addCommand(new Command("raw-events-retry").configureHelp(helpStyle).description("Requeue failed raw-event flush batches").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max failed batches to requeue", "25").action((opts) => {
194
- const result = retryRawEventFailures(opts.db, Number.parseInt(opts.limit, 10) || 25);
200
+ })).addCommand(new Command("raw-events-retry").configureHelp(helpStyle).description("Requeue failed raw-event flush batches").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max failed batches to requeue", "25").action((opts) => {
201
+ const result = retryRawEventFailures(opts.db ?? opts.dbPath, Number.parseInt(opts.limit, 10) || 25);
195
202
  p.intro("codemem db raw-events-retry");
196
203
  p.outro(`Requeued ${result.retried.toLocaleString()} failed batch(es)`);
197
- })).addCommand(new Command("raw-events-gate").configureHelp(helpStyle).description("Validate raw-event reliability thresholds (non-zero exit on failure)").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--min-flush-success-rate <rate>", "minimum flush success rate", "0.95").option("--max-dropped-event-rate <rate>", "maximum dropped event rate", "0.05").option("--min-session-boundary-accuracy <rate>", "minimum session boundary accuracy", "0.9").option("--window-hours <hours>", "lookback window in hours", "24").option("--json", "output as JSON").action((opts) => {
198
- const result = rawEventsGate(opts.db, {
204
+ })).addCommand(new Command("raw-events-gate").configureHelp(helpStyle).description("Validate raw-event reliability thresholds (non-zero exit on failure)").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--min-flush-success-rate <rate>", "minimum flush success rate", "0.95").option("--max-dropped-event-rate <rate>", "maximum dropped event rate", "0.05").option("--min-session-boundary-accuracy <rate>", "minimum session boundary accuracy", "0.9").option("--window-hours <hours>", "lookback window in hours", "24").option("--json", "output as JSON").action((opts) => {
205
+ const result = rawEventsGate(opts.db ?? opts.dbPath, {
199
206
  minFlushSuccessRate: Number.parseFloat(opts.minFlushSuccessRate),
200
207
  maxDroppedEventRate: Number.parseFloat(opts.maxDroppedEventRate),
201
208
  minSessionBoundaryAccuracy: Number.parseFloat(opts.minSessionBoundaryAccuracy),
@@ -219,6 +226,115 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("V
219
226
  p.outro("reliability gate FAILED");
220
227
  process.exitCode = 1;
221
228
  }
229
+ })).addCommand(new Command("rename-project").configureHelp(helpStyle).description("Rename a project across sessions and related tables").argument("<old-name>", "current project name").argument("<new-name>", "new project name").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--apply", "apply changes (default is dry-run)").action((oldName, newName, opts) => {
230
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
231
+ try {
232
+ const dryRun = !opts.apply;
233
+ const suffixPattern = `%/${oldName.replace(/%/g, "\\%").replace(/_/g, "\\_")}`;
234
+ const tables = ["sessions", "raw_event_sessions"];
235
+ const counts = {};
236
+ const run = () => {
237
+ for (const table of tables) {
238
+ const rows = store.db.prepare(`SELECT COUNT(*) as cnt FROM ${table} WHERE project = ? OR project LIKE ? ESCAPE '\\'`).get(oldName, suffixPattern);
239
+ counts[table] = rows.cnt;
240
+ if (!dryRun && rows.cnt > 0) {
241
+ store.db.prepare(`UPDATE ${table} SET project = ? WHERE project = ?`).run(newName, oldName);
242
+ store.db.prepare(`UPDATE ${table} SET project = ? WHERE project LIKE ? ESCAPE '\\' AND project != ?`).run(newName, suffixPattern, newName);
243
+ }
244
+ }
245
+ };
246
+ if (dryRun) run();
247
+ else store.db.transaction(run)();
248
+ const action = dryRun ? "Will rename" : "Renamed";
249
+ p.intro("codemem db rename-project");
250
+ p.log.info(`${action} ${oldName} → ${newName}`);
251
+ p.log.info([`Sessions: ${counts.sessions}`, `Raw event sessions: ${counts.raw_event_sessions}`].join("\n"));
252
+ if (dryRun) p.outro("Pass --apply to execute");
253
+ else p.outro("done");
254
+ } finally {
255
+ store.close();
256
+ }
257
+ })).addCommand(new Command("normalize-projects").configureHelp(helpStyle).description("Normalize path-like project identifiers to their basename").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--apply", "apply changes (default is dry-run)").action((opts) => {
258
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
259
+ try {
260
+ const dryRun = !opts.apply;
261
+ const tables = ["sessions", "raw_event_sessions"];
262
+ const rewrites = /* @__PURE__ */ new Map();
263
+ const counts = {};
264
+ const run = () => {
265
+ for (const table of tables) {
266
+ const projects = store.db.prepare(`SELECT DISTINCT project FROM ${table} WHERE project IS NOT NULL AND project LIKE '%/%'`).all();
267
+ let updated = 0;
268
+ for (const row of projects) {
269
+ const basename = row.project.split("/").pop() ?? row.project;
270
+ if (basename !== row.project) {
271
+ rewrites.set(row.project, basename);
272
+ if (!dryRun) {
273
+ const info = store.db.prepare(`UPDATE ${table} SET project = ? WHERE project = ?`).run(basename, row.project);
274
+ updated += info.changes;
275
+ } else {
276
+ const cnt = store.db.prepare(`SELECT COUNT(*) as cnt FROM ${table} WHERE project = ?`).get(row.project);
277
+ updated += cnt.cnt;
278
+ }
279
+ }
280
+ }
281
+ counts[table] = updated;
282
+ }
283
+ };
284
+ if (dryRun) run();
285
+ else store.db.transaction(run)();
286
+ p.intro("codemem db normalize-projects");
287
+ p.log.info(`Dry run: ${dryRun}`);
288
+ p.log.info([`Sessions to update: ${counts.sessions}`, `Raw event sessions to update: ${counts.raw_event_sessions}`].join("\n"));
289
+ if (rewrites.size > 0) {
290
+ p.log.info("Rewritten paths:");
291
+ for (const [from, to] of [...rewrites.entries()].sort()) p.log.message(` ${from} → ${to}`);
292
+ }
293
+ if (dryRun) p.outro("Pass --apply to execute");
294
+ else p.outro("done");
295
+ } finally {
296
+ store.close();
297
+ }
298
+ })).addCommand(new Command("size-report").configureHelp(helpStyle).description("Show SQLite file size and major storage consumers").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--limit <n>", "number of largest tables/indexes to show", "12").option("--json", "output as JSON").action((opts) => {
299
+ const dbPath = resolveDbPath(opts.db ?? opts.dbPath);
300
+ const db = connect(dbPath);
301
+ try {
302
+ const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 12);
303
+ const fileSizeBytes = statSync(dbPath).size;
304
+ const pageInfo = db.prepare("SELECT page_count * page_size as total FROM pragma_page_count, pragma_page_size").get();
305
+ const freePages = db.prepare("SELECT freelist_count FROM pragma_freelist_count").get();
306
+ const pageSize = db.prepare("PRAGMA page_size").get();
307
+ const tables = db.prepare(`SELECT name, SUM(pgsize) as size_bytes
308
+ FROM dbstat
309
+ GROUP BY name
310
+ ORDER BY size_bytes DESC
311
+ LIMIT ?`).all(limit);
312
+ if (opts.json) {
313
+ console.log(JSON.stringify({
314
+ file_size_bytes: fileSizeBytes,
315
+ db_size_bytes: pageInfo?.total ?? 0,
316
+ free_bytes: (freePages?.freelist_count ?? 0) * (pageSize?.page_size ?? 4096),
317
+ tables: tables.map((t) => ({
318
+ name: t.name,
319
+ size_bytes: t.size_bytes
320
+ }))
321
+ }, null, 2));
322
+ return;
323
+ }
324
+ p.intro("codemem db size-report");
325
+ p.log.info([
326
+ `File size: ${formatBytes(fileSizeBytes)}`,
327
+ `DB size: ${formatBytes(pageInfo?.total ?? 0)}`,
328
+ `Free space: ${formatBytes((freePages?.freelist_count ?? 0) * (pageSize?.page_size ?? 4096))}`
329
+ ].join("\n"));
330
+ if (tables.length > 0) {
331
+ p.log.info("Largest objects:");
332
+ for (const t of tables) p.log.message(` ${t.name.padEnd(40)} ${formatBytes(t.size_bytes).padStart(10)}`);
333
+ }
334
+ p.outro("done");
335
+ } finally {
336
+ db.close();
337
+ }
222
338
  }));
223
339
  //#endregion
224
340
  //#region src/commands/enqueue-raw-event.ts
@@ -253,7 +369,7 @@ async function readStdinJson() {
253
369
  if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("payload must be an object");
254
370
  return parsed;
255
371
  }
256
- var enqueueRawEventCommand = new Command("enqueue-raw-event").configureHelp(helpStyle).description("Enqueue one raw event from stdin into the durable queue").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action(async (opts) => {
372
+ var enqueueRawEventCommand = new Command("enqueue-raw-event").configureHelp(helpStyle).description("Enqueue one raw event from stdin into the durable queue").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action(async (opts) => {
257
373
  const payload = await readStdinJson();
258
374
  const sessionId = resolveSessionStreamId(payload);
259
375
  if (!sessionId) throw new Error("session id required");
@@ -267,7 +383,7 @@ var enqueueRawEventCommand = new Command("enqueue-raw-event").configureHelp(help
267
383
  const tsMonoMs = Number.isFinite(Number(payload.ts_mono_ms)) ? Number(payload.ts_mono_ms) : null;
268
384
  const eventId = typeof payload.event_id === "string" ? payload.event_id.trim() : "";
269
385
  const eventPayload = payload.payload && typeof payload.payload === "object" && !Array.isArray(payload.payload) ? stripPrivateObj(payload.payload) : {};
270
- const store = new MemoryStore(resolveDbPath(opts.db));
386
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
271
387
  try {
272
388
  store.updateRawEventSessionMeta({
273
389
  opencodeSessionId: sessionId,
@@ -293,9 +409,9 @@ var enqueueRawEventCommand = new Command("enqueue-raw-event").configureHelp(help
293
409
  function expandUserPath(value) {
294
410
  return value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
295
411
  }
296
- var exportMemoriesCommand = new Command("export-memories").configureHelp(helpStyle).description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--project <project>", "filter by project (defaults to git repo root)").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp").action((output, opts) => {
412
+ var exportMemoriesCommand = new Command("export-memories").configureHelp(helpStyle).description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--project <project>", "filter by project (defaults to git repo root)").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp").action((output, opts) => {
297
413
  const payload = exportMemories({
298
- dbPath: resolveDbPath(opts.db),
414
+ dbPath: resolveDbPath(opts.db ?? opts.dbPath),
299
415
  project: opts.project,
300
416
  allProjects: opts.allProjects,
301
417
  includeInactive: opts.includeInactive,
@@ -320,7 +436,7 @@ var exportMemoriesCommand = new Command("export-memories").configureHelp(helpSty
320
436
  });
321
437
  //#endregion
322
438
  //#region src/commands/import-memories.ts
323
- var importMemoriesCommand = new Command("import-memories").configureHelp(helpStyle).description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing").action((inputFile, opts) => {
439
+ var importMemoriesCommand = new Command("import-memories").configureHelp(helpStyle).description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing").action((inputFile, opts) => {
324
440
  let payload;
325
441
  try {
326
442
  payload = readImportPayload(inputFile);
@@ -339,7 +455,7 @@ var importMemoriesCommand = new Command("import-memories").configureHelp(helpSty
339
455
  `Prompts: ${payload.user_prompts.length.toLocaleString()}`
340
456
  ].join("\n"));
341
457
  const result = importMemories(payload, {
342
- dbPath: resolveDbPath(opts.db),
458
+ dbPath: resolveDbPath(opts.db ?? opts.dbPath),
343
459
  remapProject: opts.remapProject,
344
460
  dryRun: opts.dryRun
345
461
  });
@@ -357,8 +473,9 @@ var importMemoriesCommand = new Command("import-memories").configureHelp(helpSty
357
473
  });
358
474
  //#endregion
359
475
  //#region src/commands/mcp.ts
360
- var mcpCommand = new Command("mcp").configureHelp(helpStyle).description("Start the MCP stdio server").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action(async (opts) => {
361
- if (opts.db) process.env.CODEMEM_DB = opts.db;
476
+ var mcpCommand = new Command("mcp").configureHelp(helpStyle).description("Start the MCP stdio server").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").action(async (opts) => {
477
+ const dbPath = opts.db ?? opts.dbPath;
478
+ if (dbPath) process.env.CODEMEM_DB = dbPath;
362
479
  await import("@codemem/mcp");
363
480
  });
364
481
  //#endregion
@@ -376,15 +493,14 @@ function parseStrictPositiveId(value) {
376
493
  const n = Number(value.trim());
377
494
  return Number.isFinite(n) && n >= 1 && Number.isInteger(n) ? n : null;
378
495
  }
379
- var memoryCommand = new Command("memory").configureHelp(helpStyle).description("Memory item management");
380
- memoryCommand.addCommand(new Command("show").configureHelp(helpStyle).description("Print a memory item as JSON").argument("<id>", "memory ID").option("--db <path>", "database path").action((idStr, opts) => {
496
+ function showMemoryAction(idStr, opts) {
381
497
  const memoryId = parseStrictPositiveId(idStr);
382
498
  if (memoryId === null) {
383
499
  p.log.error(`Invalid memory ID: ${idStr}`);
384
500
  process.exitCode = 1;
385
501
  return;
386
502
  }
387
- const store = new MemoryStore(resolveDbPath(opts.db));
503
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
388
504
  try {
389
505
  const item = store.get(memoryId);
390
506
  if (!item) {
@@ -396,24 +512,24 @@ memoryCommand.addCommand(new Command("show").configureHelp(helpStyle).descriptio
396
512
  } finally {
397
513
  store.close();
398
514
  }
399
- }));
400
- memoryCommand.addCommand(new Command("forget").configureHelp(helpStyle).description("Deactivate a memory item").argument("<id>", "memory ID").option("--db <path>", "database path").action((idStr, opts) => {
515
+ }
516
+ function forgetMemoryAction(idStr, opts) {
401
517
  const memoryId = parseStrictPositiveId(idStr);
402
518
  if (memoryId === null) {
403
519
  p.log.error(`Invalid memory ID: ${idStr}`);
404
520
  process.exitCode = 1;
405
521
  return;
406
522
  }
407
- const store = new MemoryStore(resolveDbPath(opts.db));
523
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
408
524
  try {
409
525
  store.forget(memoryId);
410
526
  p.log.success(`Memory ${memoryId} marked inactive`);
411
527
  } finally {
412
528
  store.close();
413
529
  }
414
- }));
415
- memoryCommand.addCommand(new Command("remember").configureHelp(helpStyle).description("Manually add a memory item").requiredOption("-k, --kind <kind>", "memory kind (discovery, decision, feature, bugfix, etc.)").requiredOption("-t, --title <title>", "memory title").requiredOption("-b, --body <body>", "memory body text").option("--tags <tags...>", "tags (space-separated)").option("--project <project>", "project name (defaults to git repo root)").option("--db <path>", "database path").action((opts) => {
416
- const store = new MemoryStore(resolveDbPath(opts.db));
530
+ }
531
+ function rememberMemoryAction(opts) {
532
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
417
533
  let sessionId = null;
418
534
  try {
419
535
  const project = resolveProject(process.cwd(), opts.project ?? null);
@@ -438,15 +554,41 @@ memoryCommand.addCommand(new Command("remember").configureHelp(helpStyle).descri
438
554
  } finally {
439
555
  store.close();
440
556
  }
441
- }));
557
+ }
558
+ function createShowMemoryCommand() {
559
+ return new Command("show").configureHelp(helpStyle).description("Print a memory item as JSON").argument("<id>", "memory ID").option("--db <path>", "database path").option("--db-path <path>", "database path").action(showMemoryAction);
560
+ }
561
+ function createForgetMemoryCommand() {
562
+ return new Command("forget").configureHelp(helpStyle).description("Deactivate a memory item").argument("<id>", "memory ID").option("--db <path>", "database path").option("--db-path <path>", "database path").action(forgetMemoryAction);
563
+ }
564
+ function createRememberMemoryCommand() {
565
+ return new Command("remember").configureHelp(helpStyle).description("Manually add a memory item").requiredOption("-k, --kind <kind>", "memory kind (discovery, decision, feature, bugfix, etc.)").requiredOption("-t, --title <title>", "memory title").requiredOption("-b, --body <body>", "memory body text").option("--tags <tags...>", "tags (space-separated)").option("--project <project>", "project name (defaults to git repo root)").option("--db <path>", "database path").option("--db-path <path>", "database path").action(rememberMemoryAction);
566
+ }
567
+ var showMemoryCommand = createShowMemoryCommand();
568
+ var forgetMemoryCommand = createForgetMemoryCommand();
569
+ var rememberMemoryCommand = createRememberMemoryCommand();
570
+ var memoryCommand = new Command("memory").configureHelp(helpStyle).description("Memory item management");
571
+ memoryCommand.addCommand(createShowMemoryCommand());
572
+ memoryCommand.addCommand(createForgetMemoryCommand());
573
+ memoryCommand.addCommand(createRememberMemoryCommand());
442
574
  //#endregion
443
575
  //#region src/commands/pack.ts
444
- var packCommand = new Command("pack").configureHelp(helpStyle).description("Build a context-aware memory pack").argument("<context>", "context string to search for").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max items", "10").option("--budget <tokens>", "token budget").option("--json", "output as JSON").action((context, opts) => {
445
- const store = new MemoryStore(resolveDbPath(opts.db));
576
+ function collectWorkingSetFile(value, previous) {
577
+ return [...previous, value];
578
+ }
579
+ var packCommand = new Command("pack").configureHelp(helpStyle).description("Build a context-aware memory pack").argument("<context>", "context string to search for").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max items", "10").option("--budget <tokens>", "token budget").option("--token-budget <tokens>", "token budget").option("--working-set-file <path>", "recently modified file path used as ranking hint", collectWorkingSetFile, []).option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").option("--json", "output as JSON").action((context, opts) => {
580
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
446
581
  try {
447
582
  const limit = Number.parseInt(opts.limit, 10) || 10;
448
- const budget = opts.budget ? Number.parseInt(opts.budget, 10) : void 0;
449
- const result = store.buildMemoryPack(context, limit, budget);
583
+ const budgetRaw = opts.tokenBudget ?? opts.budget;
584
+ const budget = budgetRaw ? Number.parseInt(budgetRaw, 10) : void 0;
585
+ const filters = {};
586
+ if (!opts.allProjects) {
587
+ const project = process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), opts.project ?? null);
588
+ if (project) filters.project = project;
589
+ }
590
+ if ((opts.workingSetFile?.length ?? 0) > 0) p.log.warn("--working-set-file is accepted for compatibility but is not yet used by the TS pack builder.");
591
+ const result = store.buildMemoryPack(context, limit, budget, filters);
450
592
  if (opts.json) {
451
593
  console.log(JSON.stringify(result, null, 2));
452
594
  return;
@@ -468,11 +610,16 @@ var packCommand = new Command("pack").configureHelp(helpStyle).description("Buil
468
610
  });
469
611
  //#endregion
470
612
  //#region src/commands/recent.ts
471
- var recentCommand = new Command("recent").configureHelp(helpStyle).description("Show recent memories").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--limit <n>", "max results", "5").option("--kind <kind>", "filter by memory kind").action((opts) => {
472
- const store = new MemoryStore(resolveDbPath(opts.db));
613
+ var recentCommand = new Command("recent").configureHelp(helpStyle).description("Show recent memories").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--limit <n>", "max results", "5").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").option("--kind <kind>", "filter by memory kind").action((opts) => {
614
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
473
615
  try {
474
616
  const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
475
- const filters = opts.kind ? { kind: opts.kind } : void 0;
617
+ const filters = {};
618
+ if (opts.kind) filters.kind = opts.kind;
619
+ if (!opts.allProjects) {
620
+ const project = process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), opts.project ?? null);
621
+ if (project) filters.project = project;
622
+ }
476
623
  const items = store.recent(limit, filters);
477
624
  for (const item of items) console.log(`#${item.id} [${item.kind}] ${item.title}`);
478
625
  } finally {
@@ -481,11 +628,16 @@ var recentCommand = new Command("recent").configureHelp(helpStyle).description("
481
628
  });
482
629
  //#endregion
483
630
  //#region src/commands/search.ts
484
- var searchCommand = new Command("search").configureHelp(helpStyle).description("Search memories by query").argument("<query>", "search query").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max results", "10").option("--kind <kind>", "filter by memory kind").option("--json", "output as JSON").action((query, opts) => {
485
- const store = new MemoryStore(resolveDbPath(opts.db));
631
+ var searchCommand = new Command("search").configureHelp(helpStyle).description("Search memories by query").argument("<query>", "search query").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("-n, --limit <n>", "max results", "5").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").option("--kind <kind>", "filter by memory kind").option("--json", "output as JSON").action((query, opts) => {
632
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
486
633
  try {
487
- const limit = Number.parseInt(opts.limit, 10) || 10;
488
- const filters = opts.kind ? { kind: opts.kind } : void 0;
634
+ const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
635
+ const filters = {};
636
+ if (opts.kind) filters.kind = opts.kind;
637
+ if (!opts.allProjects) {
638
+ const project = process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), opts.project ?? null);
639
+ if (project) filters.project = project;
640
+ }
489
641
  const results = store.search(query, limit, filters);
490
642
  if (opts.json) {
491
643
  console.log(JSON.stringify(results, null, 2));
@@ -520,6 +672,49 @@ function timeSince(isoDate) {
520
672
  return `${Math.floor(days / 30)}mo ago`;
521
673
  }
522
674
  //#endregion
675
+ //#region src/commands/serve-invocation.ts
676
+ function parsePort(rawPort) {
677
+ const port = Number.parseInt(rawPort, 10);
678
+ if (!Number.isFinite(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${rawPort}`);
679
+ return port;
680
+ }
681
+ function resolveLegacyServeInvocation(opts) {
682
+ if (opts.stop && opts.restart) throw new Error("Use only one of --stop or --restart");
683
+ if (opts.foreground && opts.background) throw new Error("Use only one of --background or --foreground");
684
+ const dbPath = opts.db ?? opts.dbPath ?? null;
685
+ return {
686
+ mode: opts.stop ? "stop" : opts.restart ? "restart" : "start",
687
+ dbPath,
688
+ host: opts.host,
689
+ port: parsePort(opts.port),
690
+ background: opts.restart ? true : opts.background ? true : false
691
+ };
692
+ }
693
+ function resolveServeInvocation(action, opts) {
694
+ if (action === void 0) return resolveLegacyServeInvocation(opts);
695
+ if (opts.stop || opts.restart) throw new Error("Do not combine lifecycle flags with a serve action");
696
+ if (action === "start") return resolveStartServeInvocation(opts);
697
+ return resolveStopRestartInvocation(action, opts);
698
+ }
699
+ function resolveStartServeInvocation(opts) {
700
+ return {
701
+ mode: "start",
702
+ dbPath: opts.db ?? opts.dbPath ?? null,
703
+ host: opts.host,
704
+ port: parsePort(opts.port),
705
+ background: !opts.foreground
706
+ };
707
+ }
708
+ function resolveStopRestartInvocation(mode, opts) {
709
+ return {
710
+ mode,
711
+ dbPath: opts.db ?? opts.dbPath ?? null,
712
+ host: opts.host,
713
+ port: parsePort(opts.port),
714
+ background: mode === "restart"
715
+ };
716
+ }
717
+ //#endregion
523
718
  //#region src/commands/serve.ts
524
719
  function pidFilePath(dbPath) {
525
720
  return join(dirname(dbPath), "viewer.pid");
@@ -545,6 +740,14 @@ function readViewerPidRecord(dbPath) {
545
740
  }
546
741
  return null;
547
742
  }
743
+ function isProcessRunning(pid) {
744
+ try {
745
+ process.kill(pid, 0);
746
+ return true;
747
+ } catch {
748
+ return false;
749
+ }
750
+ }
548
751
  async function respondsLikeCodememViewer(record) {
549
752
  try {
550
753
  const controller = new AbortController();
@@ -556,19 +759,37 @@ async function respondsLikeCodememViewer(record) {
556
759
  return false;
557
760
  }
558
761
  }
762
+ async function isPortOpen(host, port) {
763
+ return new Promise((resolve) => {
764
+ const socket = net.createConnection({
765
+ host,
766
+ port
767
+ });
768
+ const done = (open) => {
769
+ socket.removeAllListeners();
770
+ socket.destroy();
771
+ resolve(open);
772
+ };
773
+ socket.setTimeout(300);
774
+ socket.once("connect", () => done(true));
775
+ socket.once("timeout", () => done(false));
776
+ socket.once("error", () => done(false));
777
+ });
778
+ }
559
779
  async function waitForProcessExit(pid, timeoutMs = 5e3) {
560
780
  const deadline = Date.now() + timeoutMs;
561
- while (Date.now() < deadline) try {
562
- process.kill(pid, 0);
781
+ while (Date.now() < deadline) {
782
+ if (!isProcessRunning(pid)) return;
563
783
  await new Promise((resolve) => setTimeout(resolve, 100));
564
- } catch {
565
- return;
566
784
  }
567
785
  }
568
786
  async function stopExistingViewer(dbPath) {
569
787
  const pidPath = pidFilePath(dbPath);
570
788
  const record = readViewerPidRecord(dbPath);
571
- if (!record) return;
789
+ if (!record) return {
790
+ stopped: false,
791
+ pid: null
792
+ };
572
793
  if (await respondsLikeCodememViewer(record)) try {
573
794
  process.kill(record.pid, "SIGTERM");
574
795
  await waitForProcessExit(record.pid);
@@ -576,17 +797,60 @@ async function stopExistingViewer(dbPath) {
576
797
  try {
577
798
  rmSync(pidPath);
578
799
  } catch {}
800
+ return {
801
+ stopped: true,
802
+ pid: record.pid
803
+ };
804
+ }
805
+ function buildForegroundRunnerArgs(scriptPath, invocation, execArgv = process.execArgv) {
806
+ const args = [
807
+ ...execArgv,
808
+ scriptPath,
809
+ "serve",
810
+ "start",
811
+ "--foreground",
812
+ "--host",
813
+ invocation.host,
814
+ "--port",
815
+ String(invocation.port)
816
+ ];
817
+ if (invocation.dbPath) args.push("--db-path", invocation.dbPath);
818
+ return args;
819
+ }
820
+ async function startBackgroundViewer(invocation) {
821
+ if (await isPortOpen(invocation.host, invocation.port)) {
822
+ p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
823
+ return;
824
+ }
825
+ const scriptPath = process.argv[1];
826
+ if (!scriptPath) throw new Error("Unable to resolve CLI entrypoint for background launch");
827
+ const child = spawn(process.execPath, buildForegroundRunnerArgs(scriptPath, invocation), {
828
+ cwd: process.cwd(),
829
+ detached: true,
830
+ stdio: "ignore",
831
+ env: {
832
+ ...process.env,
833
+ ...invocation.dbPath ? { CODEMEM_DB: invocation.dbPath } : {}
834
+ }
835
+ });
836
+ child.unref();
837
+ if (invocation.dbPath) writeFileSync(pidFilePath(invocation.dbPath), JSON.stringify({
838
+ pid: child.pid,
839
+ host: invocation.host,
840
+ port: invocation.port
841
+ }), "utf-8");
842
+ p.intro("codemem viewer");
843
+ p.outro(`Viewer started in background (pid ${child.pid}) at http://${invocation.host}:${invocation.port}`);
579
844
  }
580
- var serveCommand = new Command("serve").configureHelp(helpStyle).description("Start the viewer server").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--host <host>", "bind host", "127.0.0.1").option("--port <port>", "bind port", "38888").option("--background", "run under a caller-managed background process").option("--stop", "stop an existing viewer process").option("--restart", "restart an existing viewer process").action(async (opts) => {
845
+ async function startForegroundViewer(invocation) {
581
846
  const { createApp, closeStore, getStore } = await import("@codemem/server");
582
847
  const { serve } = await import("@hono/node-server");
583
- const dbPath = resolveDbPath(opts.db);
584
- if (opts.stop || opts.restart) {
585
- await stopExistingViewer(dbPath);
586
- if (opts.stop && !opts.restart) return;
848
+ if (invocation.dbPath) process.env.CODEMEM_DB = invocation.dbPath;
849
+ if (await isPortOpen(invocation.host, invocation.port)) {
850
+ p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
851
+ process.exitCode = 1;
852
+ return;
587
853
  }
588
- process.env.CODEMEM_DB = dbPath;
589
- const port = Number.parseInt(opts.port, 10);
590
854
  const observer = new ObserverClient();
591
855
  const sweeper = new RawEventSweeper(getStore(), { observer });
592
856
  sweeper.start();
@@ -595,11 +859,12 @@ var serveCommand = new Command("serve").configureHelp(helpStyle).description("St
595
859
  const config = readCodememConfigFile();
596
860
  if (config.sync_enabled === true || process.env.CODEMEM_SYNC_ENABLED?.toLowerCase() === "true" || process.env.CODEMEM_SYNC_ENABLED === "1") {
597
861
  syncRunning = true;
862
+ const syncIntervalS = typeof config.sync_interval_s === "number" ? config.sync_interval_s : 120;
598
863
  runSyncDaemon({
599
- dbPath,
600
- intervalS: typeof config.sync_interval_s === "number" ? config.sync_interval_s : 120,
601
- host: opts.host,
602
- port,
864
+ dbPath: resolveDbPath(invocation.dbPath ?? void 0),
865
+ intervalS: syncIntervalS,
866
+ host: invocation.host,
867
+ port: invocation.port,
603
868
  signal: syncAbort.signal
604
869
  }).catch((err) => {
605
870
  const msg = err instanceof Error ? err.message : String(err);
@@ -612,16 +877,17 @@ var serveCommand = new Command("serve").configureHelp(helpStyle).description("St
612
877
  storeFactory: getStore,
613
878
  sweeper
614
879
  });
880
+ const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
615
881
  const pidPath = pidFilePath(dbPath);
616
882
  const server = serve({
617
883
  fetch: app.fetch,
618
- hostname: opts.host,
619
- port
884
+ hostname: invocation.host,
885
+ port: invocation.port
620
886
  }, (info) => {
621
887
  writeFileSync(pidPath, JSON.stringify({
622
888
  pid: process.pid,
623
- host: opts.host,
624
- port
889
+ host: invocation.host,
890
+ port: invocation.port
625
891
  }), "utf-8");
626
892
  p.intro("codemem viewer");
627
893
  p.log.success(`Listening on http://${info.address}:${info.port}`);
@@ -629,6 +895,11 @@ var serveCommand = new Command("serve").configureHelp(helpStyle).description("St
629
895
  p.log.step("Raw event sweeper started");
630
896
  if (syncRunning) p.log.step("Sync daemon started");
631
897
  });
898
+ server.on("error", (err) => {
899
+ if (err.code === "EADDRINUSE") p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
900
+ else p.log.error(err.message);
901
+ process.exit(1);
902
+ });
632
903
  const shutdown = async () => {
633
904
  p.outro("shutting down");
634
905
  syncAbort.abort();
@@ -654,8 +925,74 @@ var serveCommand = new Command("serve").configureHelp(helpStyle).description("St
654
925
  process.on("SIGTERM", () => {
655
926
  shutdown();
656
927
  });
928
+ }
929
+ async function runServeInvocation(invocation) {
930
+ const dbPath = resolveDbPath(invocation.dbPath ?? void 0);
931
+ if (invocation.mode === "stop" || invocation.mode === "restart") {
932
+ const result = await stopExistingViewer(dbPath);
933
+ if (result.stopped) {
934
+ p.intro("codemem viewer");
935
+ p.log.success(`Stopped viewer${result.pid ? ` (pid ${result.pid})` : ""}`);
936
+ if (invocation.mode === "stop") {
937
+ p.outro("done");
938
+ return;
939
+ }
940
+ } else if (invocation.mode === "stop") {
941
+ p.intro("codemem viewer");
942
+ p.outro("No background viewer found");
943
+ return;
944
+ }
945
+ }
946
+ if (invocation.mode === "start" || invocation.mode === "restart") {
947
+ if (invocation.background) {
948
+ await startBackgroundViewer({
949
+ ...invocation,
950
+ dbPath
951
+ });
952
+ return;
953
+ }
954
+ await startForegroundViewer({
955
+ ...invocation,
956
+ dbPath
957
+ });
958
+ }
959
+ }
960
+ function addSharedServeOptions(command) {
961
+ return command.option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--host <host>", "bind host", "127.0.0.1").option("--port <port>", "bind port", "38888");
962
+ }
963
+ var serveCommand = addSharedServeOptions(new Command("serve").configureHelp(helpStyle).description("Run or manage the viewer").argument("[action]", "lifecycle action (start|stop|restart)")).option("--background", "run viewer in background").option("--foreground", "run viewer in foreground").option("--stop", "stop background viewer").option("--restart", "restart background viewer").action(async (action, opts) => {
964
+ const normalizedAction = action === void 0 ? void 0 : action === "start" || action === "stop" || action === "restart" ? action : null;
965
+ if (normalizedAction === null) {
966
+ p.log.error(`Unknown serve action: ${action}`);
967
+ process.exitCode = 1;
968
+ return;
969
+ }
970
+ await runServeInvocation(resolveServeInvocation(normalizedAction, opts));
657
971
  });
658
972
  //#endregion
973
+ //#region src/commands/setup-config.ts
974
+ function resolveOpencodeConfigPath(configDir) {
975
+ const jsonPath = join(configDir, "opencode.json");
976
+ if (existsSync(jsonPath)) return jsonPath;
977
+ const jsoncPath = join(configDir, "opencode.jsonc");
978
+ if (existsSync(jsoncPath)) return jsoncPath;
979
+ return jsoncPath;
980
+ }
981
+ function loadJsoncConfig(path) {
982
+ if (!existsSync(path)) return {};
983
+ const raw = readFileSync(path, "utf-8");
984
+ try {
985
+ return JSON.parse(raw);
986
+ } catch {
987
+ const cleaned = stripTrailingCommas(stripJsonComments(raw));
988
+ return JSON.parse(cleaned);
989
+ }
990
+ }
991
+ function writeJsonConfig(path, data) {
992
+ mkdirSync(dirname(path), { recursive: true });
993
+ writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
994
+ }
995
+ //#endregion
659
996
  //#region src/commands/setup.ts
660
997
  /**
661
998
  * codemem setup — one-command installation for OpenCode plugin + MCP config.
@@ -677,17 +1014,15 @@ function claudeConfigDir() {
677
1014
  }
678
1015
  /**
679
1016
  * Find the plugin source file — walk up from this module's location
680
- * to find the .opencode/plugin/codemem.js in the package tree.
1017
+ * to find the .opencode/plugins/codemem.js in the package tree.
681
1018
  */
682
1019
  function findPluginSource() {
683
1020
  let dir = dirname(import.meta.url.replace("file://", ""));
684
1021
  for (let i = 0; i < 6; i++) {
685
- const candidate = join(dir, ".opencode", "plugin", "codemem.js");
1022
+ const candidate = join(dir, ".opencode", "plugins", "codemem.js");
686
1023
  if (existsSync(candidate)) return candidate;
687
- const nmCandidate = join(dir, "node_modules", "codemem", ".opencode", "plugin", "codemem.js");
1024
+ const nmCandidate = join(dir, "node_modules", "codemem", ".opencode", "plugins", "codemem.js");
688
1025
  if (existsSync(nmCandidate)) return nmCandidate;
689
- const legacyCandidate = join(dir, "node_modules", "@kunickiaj", "codemem", ".opencode", "plugin", "codemem.js");
690
- if (existsSync(legacyCandidate)) return legacyCandidate;
691
1026
  const parent = dirname(dir);
692
1027
  if (parent === dir) break;
693
1028
  dir = parent;
@@ -709,27 +1044,13 @@ function findCompatSource() {
709
1044
  }
710
1045
  return null;
711
1046
  }
712
- function loadJsoncConfig(path) {
713
- if (!existsSync(path)) return {};
714
- const raw = readFileSync(path, "utf-8");
715
- try {
716
- return JSON.parse(raw);
717
- } catch {
718
- const cleaned = stripTrailingCommas(stripJsonComments(raw));
719
- return JSON.parse(cleaned);
720
- }
721
- }
722
- function writeJsonConfig(path, data) {
723
- mkdirSync(dirname(path), { recursive: true });
724
- writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
725
- }
726
1047
  function installPlugin(force) {
727
1048
  const source = findPluginSource();
728
1049
  if (!source) {
729
1050
  p.log.error("Plugin file not found in package tree");
730
1051
  return false;
731
1052
  }
732
- const destDir = join(opencodeConfigDir(), "plugin");
1053
+ const destDir = join(opencodeConfigDir(), "plugins");
733
1054
  const dest = join(destDir, "codemem.js");
734
1055
  if (existsSync(dest) && !force) p.log.info(`Plugin already installed at ${dest}`);
735
1056
  else {
@@ -746,7 +1067,7 @@ function installPlugin(force) {
746
1067
  return true;
747
1068
  }
748
1069
  function installMcp(force) {
749
- const configPath = join(opencodeConfigDir(), "opencode.json");
1070
+ const configPath = resolveOpencodeConfigPath(opencodeConfigDir());
750
1071
  let config;
751
1072
  try {
752
1073
  config = loadJsoncConfig(configPath);
@@ -838,8 +1159,8 @@ function fmtTokens(n) {
838
1159
  if (n >= 1e3) return `~${(n / 1e3).toFixed(0)}K`;
839
1160
  return `${n}`;
840
1161
  }
841
- var statsCommand = new Command("stats").configureHelp(helpStyle).description("Show database statistics").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--json", "output as JSON").action((opts) => {
842
- const store = new MemoryStore(resolveDbPath(opts.db));
1162
+ var statsCommand = new Command("stats").configureHelp(helpStyle).description("Show database statistics").option("--db <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--db-path <path>", "database path (default: $CODEMEM_DB or ~/.codemem/mem.sqlite)").option("--json", "output as JSON").action((opts) => {
1163
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
843
1164
  try {
844
1165
  const result = store.stats();
845
1166
  if (opts.json) {
@@ -876,14 +1197,382 @@ var statsCommand = new Command("stats").configureHelp(helpStyle).description("Sh
876
1197
  }
877
1198
  });
878
1199
  //#endregion
1200
+ //#region src/commands/sync-helpers.ts
1201
+ function formatSyncAttempt(row) {
1202
+ const status = row.ok ? "ok" : "error";
1203
+ const error = String(row.error || "");
1204
+ const suffix = error ? ` | ${error}` : "";
1205
+ return `${row.peer_device_id}|${status}|in=${row.ops_in}|out=${row.ops_out}|${row.finished_at ?? ""}${suffix}`;
1206
+ }
1207
+ function buildServeLifecycleArgs(action, opts, scriptPath, execArgv = []) {
1208
+ if (!scriptPath) throw new Error("Unable to resolve CLI entrypoint for sync lifecycle command");
1209
+ const args = [
1210
+ ...execArgv,
1211
+ scriptPath,
1212
+ "serve"
1213
+ ];
1214
+ if (action === "start") args.push("--restart");
1215
+ else if (action === "stop") args.push("--stop");
1216
+ else args.push("--restart");
1217
+ if (opts.db ?? opts.dbPath) args.push("--db-path", opts.db ?? opts.dbPath ?? "");
1218
+ if (opts.host) args.push("--host", opts.host);
1219
+ if (opts.port) args.push("--port", opts.port);
1220
+ return args;
1221
+ }
1222
+ function formatSyncOnceResult(peerDeviceId, result) {
1223
+ if (result.ok) return `- ${peerDeviceId}: ok`;
1224
+ return `- ${peerDeviceId}: error${result.error ? `: ${result.error}` : ""}`;
1225
+ }
1226
+ function parseProjectList(value) {
1227
+ if (!value) return [];
1228
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
1229
+ }
1230
+ function collectAdvertiseAddresses(explicitAddress, configuredHost, port, interfaces) {
1231
+ if (explicitAddress && !["auto", "default"].includes(explicitAddress.toLowerCase())) return [explicitAddress];
1232
+ if (configuredHost && configuredHost !== "0.0.0.0") return [`${configuredHost}:${port}`];
1233
+ const addresses = Object.values(interfaces).flatMap((entries) => entries ?? []).filter((entry) => !entry.internal).map((entry) => entry.address).filter((address) => address && address !== "127.0.0.1" && address !== "::1").map((address) => `${address}:${port}`);
1234
+ return [...new Set(addresses)];
1235
+ }
1236
+ //#endregion
879
1237
  //#region src/commands/sync.ts
880
1238
  /**
881
1239
  * Sync CLI commands — enable/disable/status/peers/connect.
882
1240
  */
1241
+ function parseAttemptsLimit(value) {
1242
+ if (!/^\d+$/.test(value.trim())) throw new Error(`Invalid --limit: ${value}`);
1243
+ return Number.parseInt(value, 10);
1244
+ }
1245
+ async function portOpen(host, port) {
1246
+ return new Promise((resolve) => {
1247
+ const socket = net.createConnection({
1248
+ host,
1249
+ port
1250
+ });
1251
+ const done = (ok) => {
1252
+ socket.removeAllListeners();
1253
+ socket.destroy();
1254
+ resolve(ok);
1255
+ };
1256
+ socket.setTimeout(300);
1257
+ socket.once("connect", () => done(true));
1258
+ socket.once("timeout", () => done(false));
1259
+ socket.once("error", () => done(false));
1260
+ });
1261
+ }
1262
+ function readViewerBinding(dbPath) {
1263
+ try {
1264
+ const raw = readFileSync(join(dirname(dbPath), "viewer.pid"), "utf8");
1265
+ const parsed = JSON.parse(raw);
1266
+ if (typeof parsed.host === "string" && typeof parsed.port === "number") return {
1267
+ host: parsed.host,
1268
+ port: parsed.port
1269
+ };
1270
+ } catch {}
1271
+ return null;
1272
+ }
1273
+ function parseStoredAddressEndpoint(value) {
1274
+ try {
1275
+ const normalized = value.includes("://") ? value : `http://${value}`;
1276
+ const url = new URL(normalized);
1277
+ const port = url.port ? Number.parseInt(url.port, 10) : url.protocol === "https:" ? 443 : 80;
1278
+ if (!url.hostname || !Number.isFinite(port)) return null;
1279
+ return {
1280
+ host: url.hostname,
1281
+ port
1282
+ };
1283
+ } catch {
1284
+ return null;
1285
+ }
1286
+ }
1287
+ async function runServeLifecycle(action, opts) {
1288
+ if (opts.user === false || opts.system === true) p.log.warn("TS sync lifecycle currently manages the local viewer process, not separate user/system services.");
1289
+ if (action === "start") {
1290
+ const config = readCodememConfigFile();
1291
+ if (config.sync_enabled !== true) {
1292
+ p.log.error("Sync is disabled. Run `codemem sync enable` first.");
1293
+ process.exitCode = 1;
1294
+ return;
1295
+ }
1296
+ const configuredHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
1297
+ const configuredPort = typeof config.sync_port === "number" ? String(config.sync_port) : "7337";
1298
+ opts.host ??= configuredHost;
1299
+ opts.port ??= configuredPort;
1300
+ } else if (action === "restart") {
1301
+ const config = readCodememConfigFile();
1302
+ const configuredHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
1303
+ const configuredPort = typeof config.sync_port === "number" ? String(config.sync_port) : "7337";
1304
+ opts.host ??= configuredHost;
1305
+ opts.port ??= configuredPort;
1306
+ }
1307
+ const args = buildServeLifecycleArgs(action, opts, process.argv[1] ?? "", process.execArgv);
1308
+ await new Promise((resolve, reject) => {
1309
+ const child = spawn(process.execPath, args, {
1310
+ cwd: process.cwd(),
1311
+ stdio: "inherit",
1312
+ env: {
1313
+ ...process.env,
1314
+ ...opts.db ?? opts.dbPath ? { CODEMEM_DB: opts.db ?? opts.dbPath } : {}
1315
+ }
1316
+ });
1317
+ child.once("error", reject);
1318
+ child.once("exit", (code) => {
1319
+ if (code && code !== 0) process.exitCode = code;
1320
+ resolve();
1321
+ });
1322
+ });
1323
+ }
883
1324
  var syncCommand = new Command("sync").configureHelp(helpStyle).description("Sync configuration and peer management");
884
- syncCommand.addCommand(new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary").option("--db <path>", "database path").option("--json", "output as JSON").action((opts) => {
1325
+ syncCommand.addCommand(new Command("attempts").configureHelp(helpStyle).description("Show recent sync attempts").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--limit <n>", "max attempts", "10").option("--json", "output as JSON").action((opts) => {
1326
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1327
+ try {
1328
+ const d = drizzle(store.db, { schema });
1329
+ const limit = parseAttemptsLimit(opts.limit);
1330
+ const rows = d.select({
1331
+ peer_device_id: schema.syncAttempts.peer_device_id,
1332
+ ok: schema.syncAttempts.ok,
1333
+ ops_in: schema.syncAttempts.ops_in,
1334
+ ops_out: schema.syncAttempts.ops_out,
1335
+ error: schema.syncAttempts.error,
1336
+ finished_at: schema.syncAttempts.finished_at
1337
+ }).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(limit).all();
1338
+ if (opts.json) {
1339
+ console.log(JSON.stringify(rows, null, 2));
1340
+ return;
1341
+ }
1342
+ for (const row of rows) console.log(formatSyncAttempt(row));
1343
+ } finally {
1344
+ store.close();
1345
+ }
1346
+ }));
1347
+ syncCommand.addCommand(new Command("start").configureHelp(helpStyle).description("Start sync daemon").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--host <host>", "viewer host").option("--port <port>", "viewer port").option("--user", "accepted for compatibility", true).option("--system", "accepted for compatibility").action(async (opts) => {
1348
+ await runServeLifecycle("start", opts);
1349
+ }));
1350
+ syncCommand.addCommand(new Command("stop").configureHelp(helpStyle).description("Stop sync daemon").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--host <host>", "viewer host").option("--port <port>", "viewer port").option("--user", "accepted for compatibility", true).option("--system", "accepted for compatibility").action(async (opts) => {
1351
+ await runServeLifecycle("stop", opts);
1352
+ }));
1353
+ syncCommand.addCommand(new Command("restart").configureHelp(helpStyle).description("Restart sync daemon").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--host <host>", "viewer host").option("--port <port>", "viewer port").option("--user", "accepted for compatibility", true).option("--system", "accepted for compatibility").action(async (opts) => {
1354
+ await runServeLifecycle("restart", opts);
1355
+ }));
1356
+ syncCommand.addCommand(new Command("once").configureHelp(helpStyle).description("Run a single sync pass").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--peer <peer>", "peer device id or name").action(async (opts) => {
1357
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1358
+ try {
1359
+ syncPassPreflight(store.db);
1360
+ const d = drizzle(store.db, { schema });
1361
+ const rows = opts.peer ? (() => {
1362
+ const deviceMatches = d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, opts.peer)).all();
1363
+ if (deviceMatches.length > 0) return deviceMatches;
1364
+ const nameMatches = d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).where(eq(schema.syncPeers.name, opts.peer)).all();
1365
+ if (nameMatches.length > 1) {
1366
+ p.log.error(`Peer name is ambiguous: ${opts.peer}`);
1367
+ process.exitCode = 1;
1368
+ return [];
1369
+ }
1370
+ return nameMatches;
1371
+ })() : d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).all();
1372
+ if (rows.length === 0) {
1373
+ p.log.warn("No peers available for sync");
1374
+ process.exitCode = 1;
1375
+ return;
1376
+ }
1377
+ let hadFailure = false;
1378
+ for (const row of rows) {
1379
+ const result = await runSyncPass(store.db, row.peer_device_id);
1380
+ if (!result.ok) hadFailure = true;
1381
+ console.log(formatSyncOnceResult(row.peer_device_id, result));
1382
+ }
1383
+ if (hadFailure) process.exitCode = 1;
1384
+ } finally {
1385
+ store.close();
1386
+ }
1387
+ }));
1388
+ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description("Print pairing payload or accept a peer payload").option("--accept <json>", "accept pairing payload JSON from another device").option("--accept-file <path>", "accept pairing payload from file path, or '-' for stdin").option("--payload-only", "when generating pairing payload, print JSON only").option("--name <name>", "label for the peer").option("--address <host:port>", "override peer address (host:port)").option("--include <projects>", "outbound-only allowlist for accepted peer").option("--exclude <projects>", "outbound-only blocklist for accepted peer").option("--all", "with --accept, push all projects to that peer").option("--default", "with --accept, use default/global push filters").option("--db-path <path>", "database path").action(async (opts) => {
1389
+ const store = new MemoryStore(resolveDbPath(opts.dbPath));
1390
+ try {
1391
+ const acceptModeRequested = opts.accept != null || opts.acceptFile != null;
1392
+ if (opts.payloadOnly && acceptModeRequested) {
1393
+ p.log.error("--payload-only cannot be combined with --accept or --accept-file");
1394
+ process.exitCode = 1;
1395
+ return;
1396
+ }
1397
+ if (opts.accept && opts.acceptFile) {
1398
+ p.log.error("Use only one of --accept or --accept-file");
1399
+ process.exitCode = 1;
1400
+ return;
1401
+ }
1402
+ let acceptText = opts.accept;
1403
+ if (opts.acceptFile) try {
1404
+ acceptText = opts.acceptFile === "-" ? await new Promise((resolve, reject) => {
1405
+ let text = "";
1406
+ process.stdin.setEncoding("utf8");
1407
+ process.stdin.on("data", (chunk) => {
1408
+ text += chunk;
1409
+ });
1410
+ process.stdin.on("end", () => resolve(text));
1411
+ process.stdin.on("error", reject);
1412
+ }) : readFileSync(opts.acceptFile, "utf8");
1413
+ } catch (error) {
1414
+ p.log.error(error instanceof Error ? `Failed to read pairing payload from ${opts.acceptFile}: ${error.message}` : `Failed to read pairing payload from ${opts.acceptFile}`);
1415
+ process.exitCode = 1;
1416
+ return;
1417
+ }
1418
+ if (acceptModeRequested && !(acceptText ?? "").trim()) {
1419
+ p.log.error("Empty pairing payload; provide JSON via --accept or --accept-file");
1420
+ process.exitCode = 1;
1421
+ return;
1422
+ }
1423
+ if (!acceptText && (opts.include || opts.exclude || opts.all || opts.default)) {
1424
+ p.log.error("Project filters are outbound-only and must be set on the device running --accept");
1425
+ process.exitCode = 1;
1426
+ return;
1427
+ }
1428
+ if (acceptText?.trim()) {
1429
+ if (opts.all && opts.default) {
1430
+ p.log.error("Use only one of --all or --default");
1431
+ process.exitCode = 1;
1432
+ return;
1433
+ }
1434
+ if ((opts.all || opts.default) && (opts.include || opts.exclude)) {
1435
+ p.log.error("--include/--exclude cannot be combined with --all/--default");
1436
+ process.exitCode = 1;
1437
+ return;
1438
+ }
1439
+ let payload;
1440
+ try {
1441
+ payload = JSON.parse(acceptText);
1442
+ } catch (error) {
1443
+ p.log.error(error instanceof Error ? `Invalid pairing payload: ${error.message}` : "Invalid pairing payload");
1444
+ process.exitCode = 1;
1445
+ return;
1446
+ }
1447
+ const deviceId = String(payload.device_id || "").trim();
1448
+ const fingerprint = String(payload.fingerprint || "").trim();
1449
+ const publicKey = String(payload.public_key || "").trim();
1450
+ const resolvedAddresses = opts.address?.trim() ? [opts.address.trim()] : Array.isArray(payload.addresses) ? payload.addresses.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim()) : [];
1451
+ if (!deviceId || !fingerprint || !publicKey || resolvedAddresses.length === 0) {
1452
+ p.log.error("Pairing payload missing device_id, fingerprint, public_key, or addresses");
1453
+ process.exitCode = 1;
1454
+ return;
1455
+ }
1456
+ if (fingerprintPublicKey(publicKey) !== fingerprint) {
1457
+ p.log.error("Pairing payload fingerprint mismatch");
1458
+ process.exitCode = 1;
1459
+ return;
1460
+ }
1461
+ updatePeerAddresses(store.db, deviceId, resolvedAddresses, {
1462
+ name: opts.name,
1463
+ pinnedFingerprint: fingerprint,
1464
+ publicKey
1465
+ });
1466
+ if (opts.default) setPeerProjectFilter(store.db, deviceId, {
1467
+ include: null,
1468
+ exclude: null
1469
+ });
1470
+ else if (opts.all || opts.include || opts.exclude) setPeerProjectFilter(store.db, deviceId, {
1471
+ include: opts.all ? [] : parseProjectList(opts.include),
1472
+ exclude: opts.all ? [] : parseProjectList(opts.exclude)
1473
+ });
1474
+ p.log.success(`Paired with ${deviceId}`);
1475
+ return;
1476
+ }
1477
+ const keysDir = process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
1478
+ const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir });
1479
+ const publicKey = loadPublicKey(keysDir);
1480
+ if (!publicKey) {
1481
+ p.log.error("Public key missing");
1482
+ process.exitCode = 1;
1483
+ return;
1484
+ }
1485
+ const config = readCodememConfigFile();
1486
+ const explicitAddress = opts.address?.trim();
1487
+ const configuredHost = typeof config.sync_host === "string" ? config.sync_host : null;
1488
+ const configuredPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
1489
+ const addresses = collectAdvertiseAddresses(explicitAddress ?? null, configuredHost, configuredPort, networkInterfaces());
1490
+ const payload = {
1491
+ device_id: deviceId,
1492
+ fingerprint,
1493
+ public_key: publicKey,
1494
+ address: addresses[0] ?? "",
1495
+ addresses
1496
+ };
1497
+ const payloadText = JSON.stringify(payload);
1498
+ if (opts.payloadOnly) {
1499
+ process.stdout.write(`${payloadText}\n`);
1500
+ return;
1501
+ }
1502
+ const escaped = payloadText.replaceAll("'", "'\\''");
1503
+ console.log("Pairing payload");
1504
+ console.log(payloadText);
1505
+ console.log("On the other device, save this JSON to pairing.json, then run:");
1506
+ console.log(" codemem sync pair --accept-file pairing.json");
1507
+ console.log("If you prefer inline JSON, run:");
1508
+ console.log(` codemem sync pair --accept '${escaped}'`);
1509
+ console.log("For machine-friendly output next time, run:");
1510
+ console.log(" codemem sync pair --payload-only");
1511
+ console.log("On the accepting device, --include/--exclude only control what it sends to peers.");
1512
+ console.log("This device does not yet enforce incoming project filters.");
1513
+ } finally {
1514
+ store.close();
1515
+ }
1516
+ }));
1517
+ syncCommand.addCommand(new Command("doctor").configureHelp(helpStyle).description("Diagnose common sync setup and connectivity issues").option("--db <path>", "database path").option("--db-path <path>", "database path").action(async (opts) => {
885
1518
  const config = readCodememConfigFile();
886
- const store = new MemoryStore(resolveDbPath(opts.db));
1519
+ const dbPath = resolveDbPath(opts.db ?? opts.dbPath);
1520
+ const store = new MemoryStore(dbPath);
1521
+ try {
1522
+ const d = drizzle(store.db, { schema });
1523
+ const device = d.select({ device_id: schema.syncDevice.device_id }).from(schema.syncDevice).limit(1).get();
1524
+ const daemonState = d.select().from(schema.syncDaemonState).where(eq(schema.syncDaemonState.id, 1)).get();
1525
+ const peers = d.select({
1526
+ peer_device_id: schema.syncPeers.peer_device_id,
1527
+ addresses_json: schema.syncPeers.addresses_json,
1528
+ pinned_fingerprint: schema.syncPeers.pinned_fingerprint,
1529
+ public_key: schema.syncPeers.public_key
1530
+ }).from(schema.syncPeers).all();
1531
+ const issues = [];
1532
+ const syncHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
1533
+ const syncPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
1534
+ const viewerBinding = readViewerBinding(dbPath);
1535
+ console.log("Sync doctor");
1536
+ console.log(`- Enabled: ${config.sync_enabled === true}`);
1537
+ console.log(`- Listen: ${syncHost}:${syncPort}`);
1538
+ console.log(`- mDNS: ${process.env.CODEMEM_SYNC_MDNS ? "env-configured" : "default/off"}`);
1539
+ const reachable = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
1540
+ console.log(`- Daemon: ${reachable ? "running" : "not running"}`);
1541
+ if (!reachable) issues.push("daemon not running");
1542
+ if (!device) {
1543
+ console.log("- Identity: missing (run `codemem sync enable`)");
1544
+ issues.push("identity missing");
1545
+ } else console.log(`- Identity: ${device.device_id}`);
1546
+ if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) {
1547
+ console.log(`- Daemon error: ${daemonState.last_error} (at ${daemonState.last_error_at ?? "unknown"})`);
1548
+ issues.push("daemon error");
1549
+ }
1550
+ if (peers.length === 0) {
1551
+ console.log("- Peers: none (pair a device first)");
1552
+ issues.push("no peers");
1553
+ } else {
1554
+ console.log(`- Peers: ${peers.length}`);
1555
+ for (const peer of peers) {
1556
+ const addresses = peer.addresses_json ? JSON.parse(peer.addresses_json) : [];
1557
+ const endpoint = parseStoredAddressEndpoint(addresses[0] ?? "");
1558
+ const reach = endpoint ? await portOpen(endpoint.host, endpoint.port) ? "ok" : "unreachable" : "unknown";
1559
+ const pinned = Boolean(peer.pinned_fingerprint);
1560
+ const hasKey = Boolean(peer.public_key);
1561
+ console.log(` - ${peer.peer_device_id}: addresses=${addresses.length} reach=${reach} pinned=${pinned} public_key=${hasKey}`);
1562
+ if (reach !== "ok") issues.push(`peer ${peer.peer_device_id} unreachable`);
1563
+ if (!pinned || !hasKey) issues.push(`peer ${peer.peer_device_id} not pinned`);
1564
+ }
1565
+ }
1566
+ if (!config.sync_enabled) issues.push("sync is disabled");
1567
+ if (issues.length > 0) console.log(`WARN: ${[...new Set(issues)].slice(0, 3).join(", ")}`);
1568
+ else console.log("OK: sync looks healthy");
1569
+ } finally {
1570
+ store.close();
1571
+ }
1572
+ }));
1573
+ syncCommand.addCommand(new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--json", "output as JSON").action((opts) => {
1574
+ const config = readCodememConfigFile();
1575
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
887
1576
  try {
888
1577
  const d = drizzle(store.db, { schema });
889
1578
  const deviceRow = d.select({
@@ -934,8 +1623,8 @@ syncCommand.addCommand(new Command("status").configureHelp(helpStyle).descriptio
934
1623
  store.close();
935
1624
  }
936
1625
  }));
937
- syncCommand.addCommand(new Command("enable").configureHelp(helpStyle).description("Enable sync and initialize device identity").option("--db <path>", "database path").option("--host <host>", "sync listen host").option("--port <port>", "sync listen port").option("--interval <seconds>", "sync interval in seconds").action((opts) => {
938
- const store = new MemoryStore(resolveDbPath(opts.db));
1626
+ syncCommand.addCommand(new Command("enable").configureHelp(helpStyle).description("Enable sync and initialize device identity").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--host <host>", "sync listen host").option("--port <port>", "sync listen port").option("--interval <seconds>", "sync interval in seconds").action((opts) => {
1627
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
939
1628
  try {
940
1629
  const [deviceId, fingerprint] = ensureDeviceIdentity(store.db);
941
1630
  const config = readCodememConfigFile();
@@ -964,8 +1653,8 @@ syncCommand.addCommand(new Command("disable").configureHelp(helpStyle).descripti
964
1653
  p.intro("codemem sync disable");
965
1654
  p.outro("Sync disabled — restart `codemem serve` to take effect");
966
1655
  }));
967
- syncCommand.addCommand(new Command("peers").configureHelp(helpStyle).description("List known sync peers").option("--db <path>", "database path").option("--json", "output as JSON").action((opts) => {
968
- const store = new MemoryStore(resolveDbPath(opts.db));
1656
+ syncCommand.addCommand(new Command("peers").configureHelp(helpStyle).description("List known sync peers").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--json", "output as JSON").action((opts) => {
1657
+ const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
969
1658
  try {
970
1659
  const peers = drizzle(store.db, { schema }).select({
971
1660
  peer_device_id: schema.syncPeers.peer_device_id,
@@ -1026,12 +1715,15 @@ completion.on("command", ({ reply }) => {
1026
1715
  "claude-hook-ingest",
1027
1716
  "db",
1028
1717
  "export-memories",
1718
+ "forget",
1029
1719
  "memory",
1030
1720
  "import-memories",
1031
1721
  "setup",
1722
+ "show",
1032
1723
  "sync",
1033
1724
  "stats",
1034
1725
  "recent",
1726
+ "remember",
1035
1727
  "search",
1036
1728
  "pack",
1037
1729
  "serve",
@@ -1044,16 +1736,24 @@ completion.on("command", ({ reply }) => {
1044
1736
  ]);
1045
1737
  });
1046
1738
  completion.init();
1047
- if (process.argv.includes("--setup-completion")) {
1739
+ function hasRootFlag(flag) {
1740
+ for (const arg of process.argv.slice(2)) {
1741
+ if (arg === "--") return false;
1742
+ if (arg === flag) return true;
1743
+ if (!arg.startsWith("-")) return false;
1744
+ }
1745
+ return false;
1746
+ }
1747
+ var program = new Command();
1748
+ program.name("codemem").description("codemem — persistent memory for AI coding agents").version(VERSION).configureHelp(helpStyle);
1749
+ if (hasRootFlag("--setup-completion")) {
1048
1750
  completion.setupShellInitFile();
1049
1751
  process.exit(0);
1050
1752
  }
1051
- if (process.argv.includes("--cleanup-completion")) {
1753
+ if (hasRootFlag("--cleanup-completion")) {
1052
1754
  completion.cleanupShellInitFile();
1053
1755
  process.exit(0);
1054
1756
  }
1055
- var program = new Command();
1056
- program.name("codemem").description("codemem — persistent memory for AI coding agents").version(VERSION).configureHelp(helpStyle);
1057
1757
  program.addCommand(serveCommand);
1058
1758
  program.addCommand(mcpCommand);
1059
1759
  program.addCommand(claudeHookIngestCommand);
@@ -1064,6 +1764,9 @@ program.addCommand(statsCommand);
1064
1764
  program.addCommand(recentCommand);
1065
1765
  program.addCommand(searchCommand);
1066
1766
  program.addCommand(packCommand);
1767
+ program.addCommand(showMemoryCommand);
1768
+ program.addCommand(forgetMemoryCommand);
1769
+ program.addCommand(rememberMemoryCommand);
1067
1770
  program.addCommand(memoryCommand);
1068
1771
  program.addCommand(syncCommand);
1069
1772
  program.addCommand(setupCommand);