codemem 0.22.4 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/index.js +2004 -566
  2. package/dist/index.js.map +1 -1
  3. package/package.json +10 -10
  4. package/dist/commands/claude-hook-ingest.d.ts +0 -51
  5. package/dist/commands/claude-hook-ingest.d.ts.map +0 -1
  6. package/dist/commands/claude-hook-ingest.test.d.ts +0 -2
  7. package/dist/commands/claude-hook-ingest.test.d.ts.map +0 -1
  8. package/dist/commands/db.d.ts +0 -3
  9. package/dist/commands/db.d.ts.map +0 -1
  10. package/dist/commands/db.test.d.ts +0 -2
  11. package/dist/commands/db.test.d.ts.map +0 -1
  12. package/dist/commands/embed.d.ts +0 -4
  13. package/dist/commands/embed.d.ts.map +0 -1
  14. package/dist/commands/embed.test.d.ts +0 -2
  15. package/dist/commands/embed.test.d.ts.map +0 -1
  16. package/dist/commands/enqueue-raw-event.d.ts +0 -3
  17. package/dist/commands/enqueue-raw-event.d.ts.map +0 -1
  18. package/dist/commands/export-memories.d.ts +0 -3
  19. package/dist/commands/export-memories.d.ts.map +0 -1
  20. package/dist/commands/import-memories.d.ts +0 -3
  21. package/dist/commands/import-memories.d.ts.map +0 -1
  22. package/dist/commands/mcp.d.ts +0 -3
  23. package/dist/commands/mcp.d.ts.map +0 -1
  24. package/dist/commands/memory.d.ts +0 -13
  25. package/dist/commands/memory.d.ts.map +0 -1
  26. package/dist/commands/memory.test.d.ts +0 -2
  27. package/dist/commands/memory.test.d.ts.map +0 -1
  28. package/dist/commands/pack.d.ts +0 -3
  29. package/dist/commands/pack.d.ts.map +0 -1
  30. package/dist/commands/recent.d.ts +0 -3
  31. package/dist/commands/recent.d.ts.map +0 -1
  32. package/dist/commands/search.d.ts +0 -3
  33. package/dist/commands/search.d.ts.map +0 -1
  34. package/dist/commands/serve-invocation.d.ts +0 -37
  35. package/dist/commands/serve-invocation.d.ts.map +0 -1
  36. package/dist/commands/serve.d.ts +0 -13
  37. package/dist/commands/serve.d.ts.map +0 -1
  38. package/dist/commands/serve.test.d.ts +0 -2
  39. package/dist/commands/serve.test.d.ts.map +0 -1
  40. package/dist/commands/setup-config.d.ts +0 -4
  41. package/dist/commands/setup-config.d.ts.map +0 -1
  42. package/dist/commands/setup-config.test.d.ts +0 -2
  43. package/dist/commands/setup-config.test.d.ts.map +0 -1
  44. package/dist/commands/setup.d.ts +0 -15
  45. package/dist/commands/setup.d.ts.map +0 -1
  46. package/dist/commands/stats.d.ts +0 -3
  47. package/dist/commands/stats.d.ts.map +0 -1
  48. package/dist/commands/sync-helpers.d.ts +0 -31
  49. package/dist/commands/sync-helpers.d.ts.map +0 -1
  50. package/dist/commands/sync.d.ts +0 -6
  51. package/dist/commands/sync.d.ts.map +0 -1
  52. package/dist/commands/sync.test.d.ts +0 -2
  53. package/dist/commands/sync.test.d.ts.map +0 -1
  54. package/dist/commands/version.d.ts +0 -3
  55. package/dist/commands/version.d.ts.map +0 -1
  56. package/dist/help-style.d.ts +0 -9
  57. package/dist/help-style.d.ts.map +0 -1
  58. package/dist/index.d.ts +0 -13
  59. package/dist/index.d.ts.map +0 -1
  60. package/dist/index.smoke.test.d.ts +0 -2
  61. package/dist/index.smoke.test.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_COORDINATOR_DB_PATH, MemoryStore, ObserverClient, RawEventSweeper, SyncRetentionRunner, VERSION, applyBootstrapSnapshot, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fetchAllSnapshotPages, fingerprintPublicKey, getRawEventStatus, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCoordinatorSyncConfig, readImportPayload, requestJson, resolveDbPath, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
- import { Command } from "commander";
2
+ import { DEFAULT_COORDINATOR_DB_PATH, MemoryStore, ObserverClient, RawEventSweeper, SyncRetentionRunner, VERSION, applyBootstrapSnapshot, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, ensureDeviceIdentity, exportMemories, fetchAllSnapshotPages, fingerprintPublicKey, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasUnsyncedSharedMemoryChanges, importMemories, initDatabase, isEmbeddingDisabled, loadObserverConfig, loadPublicKey, loadSqliteVec, planReplicationOpsAgePrune, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorSyncConfig, readImportPayload, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveDbPath, resolveHookProject, resolveProject, retryRawEventFailures, runSyncDaemon, runSyncPass, schema, setPeerProjectFilter, stripJsonComments, stripPrivateObj, stripTrailingCommas, syncPassPreflight, updatePeerAddresses, vacuumDatabase, writeCodememConfigFile } from "@codemem/core";
3
+ import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { 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 { serve } from "@hono/node-server";
8
9
  import { homedir, networkInterfaces } from "node:os";
9
10
  import { dirname, join } from "node:path";
10
11
  import { spawn, spawnSync } from "node:child_process";
11
12
  import net from "node:net";
12
- import { serve } from "@hono/node-server";
13
13
  import { desc, eq } from "drizzle-orm";
14
14
  import { drizzle } from "drizzle-orm/better-sqlite3";
15
15
  //#region src/help-style.ts
@@ -30,6 +30,52 @@ var helpStyle = {
30
30
  styleArgumentText: (str) => styleText("yellow", str)
31
31
  };
32
32
  //#endregion
33
+ //#region src/shared-options.ts
34
+ /** Add -d/--db-path and hidden --db alias to a command. */
35
+ function addDbOption(cmd) {
36
+ cmd.addOption(new Option("-d, --db-path <path>", "database path (overrides $CODEMEM_DB)"));
37
+ cmd.addOption(new Option("--db <path>", "database path").hideHelp());
38
+ return cmd;
39
+ }
40
+ /** Resolve the db path from parsed opts that may have --db or --db-path. */
41
+ function resolveDbOpt(opts) {
42
+ return opts.dbPath ?? opts.db;
43
+ }
44
+ /** Add -c/--config to a command. */
45
+ function addConfigOption(cmd) {
46
+ cmd.addOption(new Option("-c, --config <path>", "config file path (overrides $CODEMEM_CONFIG)"));
47
+ return cmd;
48
+ }
49
+ /** Add -j/--json to a command. */
50
+ function addJsonOption(cmd) {
51
+ cmd.addOption(new Option("-j, --json", "output as JSON"));
52
+ return cmd;
53
+ }
54
+ /** Add --host and --port for the viewer/serve service. */
55
+ function addViewerHostOptions(cmd, defaults = {}) {
56
+ cmd.option("--host <host>", "viewer host", defaults.host ?? "127.0.0.1");
57
+ cmd.option("--port <port>", "viewer port", defaults.port ?? "38888");
58
+ return cmd;
59
+ }
60
+ /** Add hidden --user/--system Typer-era compatibility flags. */
61
+ function addLegacyServiceFlags(cmd) {
62
+ cmd.addOption(new Option("--user", "accepted for compatibility").default(true).hideHelp());
63
+ cmd.addOption(new Option("--system", "accepted for compatibility").hideHelp());
64
+ return cmd;
65
+ }
66
+ /** Emit a deprecation warning to stderr. */
67
+ function emitDeprecationWarning(oldForm, newForm) {
68
+ console.error(`Warning: '${oldForm}' is deprecated, use '${newForm}' instead.`);
69
+ }
70
+ /** Print a structured JSON error to stdout and set the exit code. */
71
+ function emitJsonError(errorCode, message, exitCode = 1) {
72
+ console.log(JSON.stringify({
73
+ error: errorCode,
74
+ message
75
+ }));
76
+ process.exitCode = exitCode;
77
+ }
78
+ //#endregion
33
79
  //#region src/commands/claude-hook-ingest.ts
34
80
  /**
35
81
  * codemem claude-hook-ingest — read a single Claude Code hook payload
@@ -43,6 +89,13 @@ var helpStyle = {
43
89
  * echo '{"hook_event_name":"Stop","session_id":"...","last_assistant_message":"..."}' \
44
90
  * | codemem claude-hook-ingest
45
91
  */
92
+ function emitStructuredError$1(errorCode, message) {
93
+ console.log(JSON.stringify({
94
+ error: errorCode,
95
+ message
96
+ }));
97
+ process.exitCode = 1;
98
+ }
46
99
  /** Try to POST the hook payload to the running viewer server. */
47
100
  async function tryHttpIngest(payload, host, port) {
48
101
  const url = `http://${host}:${port}/api/claude-hooks`;
@@ -130,55 +183,823 @@ async function ingestClaudeHookPayload(payload, opts, deps = {}) {
130
183
  const httpIngest = deps.httpIngest ?? tryHttpIngest;
131
184
  const directIngest = deps.directIngest ?? directEnqueue;
132
185
  const resolveDb = deps.resolveDb ?? resolveDbPath;
133
- const httpResult = await httpIngest(payload, opts.host, opts.port);
186
+ const port = typeof opts.port === "number" ? opts.port : Number.parseInt(opts.port, 10);
187
+ const httpResult = await httpIngest(payload, opts.host, port);
134
188
  if (httpResult.ok) return {
135
189
  inserted: httpResult.inserted,
136
190
  skipped: httpResult.skipped,
137
191
  via: "http"
138
192
  };
139
193
  return {
140
- ...directIngest(payload, resolveDb(opts.db ?? opts.dbPath)),
194
+ ...directIngest(payload, resolveDb(resolveDbOpt(opts))),
141
195
  via: "direct"
142
196
  };
143
197
  }
144
- var claudeHookIngestCommand = new Command("claude-hook-ingest").configureHelp(helpStyle).description("Ingest Claude hook payload: HTTP first, direct DB fallback").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) => {
198
+ var claudeHookCmd = new Command("claude-hook-ingest").configureHelp(helpStyle).description("Ingest Claude hook payload: HTTP first, direct DB fallback");
199
+ addDbOption(claudeHookCmd);
200
+ addViewerHostOptions(claudeHookCmd);
201
+ var claudeHookIngestCommand = claudeHookCmd.action(async (opts) => {
145
202
  let raw;
146
203
  try {
147
204
  raw = readFileSync(0, "utf8").trim();
148
205
  } catch {
149
- process.exitCode = 1;
206
+ emitStructuredError$1("read_error", "failed to read stdin");
150
207
  return;
151
208
  }
152
209
  if (!raw) {
153
- process.exitCode = 1;
210
+ emitStructuredError$1("read_error", "empty stdin");
154
211
  return;
155
212
  }
156
213
  let payload;
157
214
  try {
158
215
  const parsed = JSON.parse(raw);
159
216
  if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
217
+ emitStructuredError$1("parse_error", "payload must be a JSON object");
218
+ return;
219
+ }
220
+ payload = parsed;
221
+ } catch {
222
+ emitStructuredError$1("parse_error", "invalid JSON");
223
+ return;
224
+ }
225
+ try {
226
+ const result = await ingestClaudeHookPayload(payload, opts);
227
+ console.log(JSON.stringify(result));
228
+ } catch (err) {
229
+ emitStructuredError$1("ingest_error", err instanceof Error ? err.message : String(err));
230
+ }
231
+ });
232
+ //#endregion
233
+ //#region src/commands/claude-hook-inject.ts
234
+ var DEFAULT_VIEWER_HOST = "127.0.0.1";
235
+ var DEFAULT_VIEWER_PORT = 38888;
236
+ var DEFAULT_MAX_CHARS = 16e3;
237
+ var DEFAULT_HTTP_MAX_TIME_S = 2;
238
+ function emitJson(value) {
239
+ console.log(JSON.stringify(value));
240
+ }
241
+ function envNotDisabled(value) {
242
+ const normalized = String(value ?? "").trim().toLowerCase();
243
+ return normalized !== "0" && normalized !== "false" && normalized !== "off";
244
+ }
245
+ function parsePositiveInt(value, fallback) {
246
+ const parsed = Number.parseInt(String(value ?? ""), 10);
247
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
248
+ return parsed;
249
+ }
250
+ function continueResult(additionalContext) {
251
+ if (!additionalContext) return { continue: true };
252
+ return {
253
+ continue: true,
254
+ hookSpecificOutput: { additionalContext }
255
+ };
256
+ }
257
+ function truncateAdditionalContext(text, maxChars) {
258
+ const normalized = text.trim();
259
+ if (!normalized) return "";
260
+ if (!Number.isFinite(maxChars) || maxChars <= 0 || normalized.length <= maxChars) return normalized;
261
+ return normalized.slice(0, maxChars);
262
+ }
263
+ function extractInjectContext(payload) {
264
+ return String(payload.prompt ?? "").trim() || null;
265
+ }
266
+ function resolveInjectProject(payload) {
267
+ return resolveHookProject(typeof payload.cwd === "string" ? payload.cwd : null, payload.project);
268
+ }
269
+ async function buildLocalPack(context, project, dbPath) {
270
+ const store = new MemoryStore(dbPath);
271
+ try {
272
+ const limit = parsePositiveInt(process.env.CODEMEM_INJECT_LIMIT, 8);
273
+ const budget = parsePositiveInt(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
274
+ const filters = {};
275
+ if (project) filters.project = project;
276
+ const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
277
+ return String(pack.pack_text ?? "").trim();
278
+ } finally {
279
+ store.close();
280
+ }
281
+ }
282
+ async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S * 1e3) {
283
+ const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
284
+ const port = parsePositiveInt(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
285
+ const url = new URL(`http://${host}:${port}/api/pack`);
286
+ url.searchParams.set("context", context);
287
+ url.searchParams.set("limit", String(parsePositiveInt(process.env.CODEMEM_INJECT_LIMIT, 8)));
288
+ url.searchParams.set("token_budget", String(parsePositiveInt(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800)));
289
+ if (project) url.searchParams.set("project", project);
290
+ const controller = new AbortController();
291
+ const timeout = setTimeout(() => controller.abort(), maxTimeMs);
292
+ try {
293
+ const res = await fetch(url, { signal: controller.signal });
294
+ if (!res.ok) return "";
295
+ const body = await res.json();
296
+ return String(body.pack_text ?? "").trim();
297
+ } catch {
298
+ return "";
299
+ } finally {
300
+ clearTimeout(timeout);
301
+ }
302
+ }
303
+ async function buildClaudeHookInjection(payload, opts, deps = {}) {
304
+ if (!envNotDisabled(process.env.CODEMEM_INJECT_CONTEXT || "1")) return continueResult();
305
+ const context = extractInjectContext(payload);
306
+ if (!context) return continueResult();
307
+ const buildPack = deps.buildLocalPack ?? buildLocalPack;
308
+ const httpPack = deps.httpPack ?? tryHttpPack;
309
+ const resolveDb = deps.resolveDb ?? resolveDbPath;
310
+ const project = resolveInjectProject(payload);
311
+ const maxChars = parsePositiveInt(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS);
312
+ const httpMaxTimeMs = parsePositiveInt(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S) * 1e3;
313
+ let additionalContext = "";
314
+ try {
315
+ additionalContext = await buildPack(context, project, resolveDb(resolveDbOpt(opts)));
316
+ } catch {
317
+ additionalContext = "";
318
+ }
319
+ if (!additionalContext && envNotDisabled(process.env.CODEMEM_INJECT_HTTP_FALLBACK || "1")) additionalContext = await httpPack(context, project, httpMaxTimeMs);
320
+ return continueResult(truncateAdditionalContext(additionalContext, maxChars));
321
+ }
322
+ var claudeHookInjectCmd = new Command("claude-hook-inject").configureHelp(helpStyle).description("Return Claude hook additionalContext from local pack generation");
323
+ addDbOption(claudeHookInjectCmd);
324
+ var claudeHookInjectCommand = claudeHookInjectCmd.action(async (opts) => {
325
+ let raw = "";
326
+ for await (const chunk of process.stdin) raw += String(chunk);
327
+ const trimmed = raw.trim();
328
+ if (!trimmed) {
329
+ emitJson(continueResult());
330
+ return;
331
+ }
332
+ let payload;
333
+ try {
334
+ const parsed = JSON.parse(trimmed);
335
+ if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
336
+ emitJson({
337
+ error: "parse_error",
338
+ message: "payload must be a JSON object"
339
+ });
160
340
  process.exitCode = 1;
161
341
  return;
162
342
  }
163
343
  payload = parsed;
164
344
  } catch {
345
+ emitJson({
346
+ error: "parse_error",
347
+ message: "invalid JSON"
348
+ });
165
349
  process.exitCode = 1;
166
350
  return;
167
351
  }
168
- const port = Number.parseInt(opts.port, 10);
169
- const host = opts.host;
352
+ emitJson(await buildClaudeHookInjection(payload, opts));
353
+ });
354
+ //#endregion
355
+ //#region src/commands/config.ts
356
+ function parseIntegerOption(value, flagName) {
357
+ if (value == null) return void 0;
358
+ const trimmed = value.trim();
359
+ if (!/^\d+$/.test(trimmed)) throw new Error(`Invalid ${flagName}: ${value}`);
360
+ return Number.parseInt(trimmed, 10);
361
+ }
362
+ function buildWorkspaceConfigPatch(opts) {
363
+ const patch = {};
364
+ if (opts.syncEnabled === true) patch.sync_enabled = true;
365
+ if (opts.syncEnabled === false) patch.sync_enabled = false;
366
+ if (opts.syncHost?.trim()) patch.sync_host = opts.syncHost.trim();
367
+ const syncPort = parseIntegerOption(opts.syncPort, "--sync-port");
368
+ if (syncPort != null) patch.sync_port = syncPort;
369
+ const syncInterval = parseIntegerOption(opts.syncIntervalS, "--sync-interval-s");
370
+ if (syncInterval != null) patch.sync_interval_s = syncInterval;
371
+ if (opts.coordinatorUrl?.trim()) patch.sync_coordinator_url = opts.coordinatorUrl.trim();
372
+ if (opts.coordinatorGroup?.trim()) patch.sync_coordinator_group = opts.coordinatorGroup.trim();
373
+ return patch;
374
+ }
375
+ function mergeWorkspaceConfig(existingConfig, patch) {
376
+ return {
377
+ ...existingConfig,
378
+ ...patch
379
+ };
380
+ }
381
+ function runWorkspaceConfigCommand(opts) {
382
+ if (opts.enableSync && opts.disableSync) throw new Error("Use only one of --enable-sync or --disable-sync");
383
+ const workspaceId = opts.workspaceId;
384
+ if (!workspaceId) throw new Error("workspace-id is required");
385
+ const configPath = getWorkspaceCodememConfigPath(workspaceId);
386
+ const existingConfig = existsSync(configPath) ? readCodememConfigFileAtPath(configPath) : readCodememConfigFile();
387
+ const patch = buildWorkspaceConfigPatch({
388
+ workspaceId,
389
+ syncEnabled: opts.enableSync ? true : opts.disableSync ? false : void 0,
390
+ syncHost: opts.syncHost,
391
+ syncPort: opts.syncPort,
392
+ syncIntervalS: opts.syncIntervalS,
393
+ coordinatorUrl: opts.coordinatorUrl,
394
+ coordinatorGroup: opts.coordinatorGroup,
395
+ json: opts.json
396
+ });
397
+ if (Object.keys(patch).length === 0) throw new Error("Provide at least one config field to update");
398
+ const nextConfig = mergeWorkspaceConfig(existingConfig, patch);
399
+ return {
400
+ workspace_id: workspaceId,
401
+ config_path: writeCodememConfigFile(nextConfig, configPath),
402
+ updated_keys: Object.keys(patch).sort(),
403
+ config: nextConfig
404
+ };
405
+ }
406
+ var configCommand = new Command("config").configureHelp(helpStyle).description("Manage codemem configuration");
407
+ var workspaceCmd = new Command("workspace").configureHelp(helpStyle).description("Create or update workspace-scoped codemem config").argument("[workspace-id]", "workspace identifier").option("--enable-sync", "set sync_enabled=true").option("--disable-sync", "set sync_enabled=false").option("--sync-host <host>", "set sync_host").option("--sync-port <port>", "set sync_port").option("--sync-interval-s <seconds>", "set sync_interval_s").option("--coordinator-url <url>", "set sync_coordinator_url").option("--coordinator-group <group>", "set sync_coordinator_group");
408
+ workspaceCmd.addOption(new Option("--workspace-id <id>", "workspace identifier (use positional instead)").hideHelp());
409
+ addJsonOption(workspaceCmd);
410
+ workspaceCmd.action((workspaceIdArg, opts) => {
170
411
  try {
171
- const result = await ingestClaudeHookPayload(payload, {
172
- host,
173
- port,
174
- db: opts.db,
175
- dbPath: opts.dbPath
412
+ const workspaceId = workspaceIdArg || opts.workspaceId;
413
+ if (!workspaceId) {
414
+ console.error("Error: missing required argument 'workspace-id'");
415
+ process.exitCode = 2;
416
+ return;
417
+ }
418
+ const result = runWorkspaceConfigCommand({
419
+ ...opts,
420
+ workspaceId
176
421
  });
177
- console.log(JSON.stringify(result));
178
- } catch {
422
+ if (opts.json) {
423
+ console.log(JSON.stringify(result, null, 2));
424
+ return;
425
+ }
426
+ console.log(`Updated workspace config: ${result.config_path}`);
427
+ console.log(`Updated keys: ${result.updated_keys.join(", ")}`);
428
+ } catch (err) {
429
+ if (opts.json) console.log(JSON.stringify({
430
+ error: "config_error",
431
+ message: err instanceof Error ? err.message : String(err)
432
+ }));
433
+ else console.error(err instanceof Error ? err.message : String(err));
179
434
  process.exitCode = 1;
180
435
  }
181
436
  });
437
+ configCommand.addCommand(workspaceCmd);
438
+ function formatWhereHuman(result) {
439
+ const lines = [];
440
+ const allEntries = [result.resolved, ...result.fallbackChain];
441
+ const sourceOrder = {
442
+ "cli-flag": 0,
443
+ "env-codemem-config": 1,
444
+ "env-runtime-root": 2,
445
+ "env-workspace-id": 3,
446
+ "legacy-global": 4
447
+ };
448
+ allEntries.sort((a, b) => (sourceOrder[a.source] ?? 99) - (sourceOrder[b.source] ?? 99));
449
+ for (const entry of allEntries) {
450
+ const marker = entry === result.resolved ? ">>>" : " ";
451
+ const existsLabel = entry.exists ? "exists" : "missing";
452
+ lines.push(`${marker} [${entry.source}] ${entry.path}`);
453
+ lines.push(` ${entry.reason} (${existsLabel})`);
454
+ }
455
+ return lines.join("\n");
456
+ }
457
+ var whereCmd = new Command("where").configureHelp(helpStyle).description("show resolved config file path");
458
+ addConfigOption(whereCmd);
459
+ addJsonOption(whereCmd);
460
+ whereCmd.action((opts) => {
461
+ try {
462
+ const result = resolveCodememConfigPath(opts.config, "read");
463
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
464
+ else console.log(formatWhereHuman(result));
465
+ } catch (err) {
466
+ if (opts.json) console.log(JSON.stringify({
467
+ error: "config_error",
468
+ message: err instanceof Error ? err.message : String(err)
469
+ }));
470
+ else console.error(err instanceof Error ? err.message : String(err));
471
+ process.exitCode = 1;
472
+ }
473
+ });
474
+ configCommand.addCommand(whereCmd);
475
+ //#endregion
476
+ //#region src/commands/coordinator.ts
477
+ /**
478
+ * Coordinator CLI commands — manage coordinator invites, join requests, and relay server.
479
+ *
480
+ * Extracted from sync.ts to give coordinator admin its own top-level group
481
+ * per cli-design-conventions.md (operator/admin surfaces belong in their own group).
482
+ *
483
+ * buildCoordinatorCommand() is a factory that creates a fresh command tree.
484
+ * This allows both the canonical top-level `coordinator` and the deprecated
485
+ * `sync coordinator` alias to have independent Commander instances (Commander
486
+ * re-parents commands on addCommand, so sharing instances between two parents
487
+ * is not possible).
488
+ */
489
+ function readCoordinatorPublicKey(opts) {
490
+ const inline = String(opts.publicKey ?? "").trim();
491
+ const filePath = String(opts.publicKeyFile ?? "").trim();
492
+ if (inline && filePath) throw new Error("Use only one of --public-key or --public-key-file");
493
+ if (filePath) {
494
+ const text = readFileSync(filePath, "utf8").trim();
495
+ if (!text) throw new Error(`Public key file is empty: ${filePath}`);
496
+ return text;
497
+ }
498
+ if (!inline) throw new Error("Public key required via --public-key or --public-key-file");
499
+ return inline;
500
+ }
501
+ /**
502
+ * Build a fresh coordinator command tree. Each call returns independent
503
+ * Commander instances so the tree can be mounted under multiple parents.
504
+ */
505
+ function buildCoordinatorCommand() {
506
+ const cmd = new Command("coordinator").configureHelp(helpStyle).description("Manage coordinator invites, join requests, and relay server");
507
+ const groupCreateCmd = new Command("group-create").configureHelp(helpStyle).description("Create a coordinator group in the local store").argument("<group>", "group id").option("--name <name>", "display name override");
508
+ addDbOption(groupCreateCmd);
509
+ addJsonOption(groupCreateCmd);
510
+ groupCreateCmd.action(async (groupId, opts) => {
511
+ try {
512
+ const group = await coordinatorCreateGroupAction({
513
+ groupId,
514
+ displayName: opts.name?.trim() || null,
515
+ dbPath: resolveDbOpt(opts) ?? null
516
+ });
517
+ if (opts.json) {
518
+ console.log(JSON.stringify(group, null, 2));
519
+ return;
520
+ }
521
+ p.intro("codemem coordinator group-create");
522
+ p.log.success(`Group ready: ${groupId.trim()}`);
523
+ p.outro(String(group.display_name ?? group.group_id ?? groupId.trim()));
524
+ } catch (err) {
525
+ if (opts.json) {
526
+ emitJsonError("group_create_failed", err instanceof Error ? err.message : String(err));
527
+ return;
528
+ }
529
+ p.log.error(err instanceof Error ? err.message : String(err));
530
+ process.exitCode = 1;
531
+ }
532
+ });
533
+ cmd.addCommand(groupCreateCmd);
534
+ const listGroupsCmd = new Command("list-groups").configureHelp(helpStyle).description("List coordinator groups from the local store");
535
+ addDbOption(listGroupsCmd);
536
+ addJsonOption(listGroupsCmd);
537
+ listGroupsCmd.action(async (opts) => {
538
+ try {
539
+ const groups = await coordinatorListGroupsAction({ dbPath: resolveDbOpt(opts) ?? null });
540
+ if (opts.json) {
541
+ console.log(JSON.stringify(groups, null, 2));
542
+ return;
543
+ }
544
+ p.intro("codemem coordinator list-groups");
545
+ if (groups.length === 0) {
546
+ p.outro("No coordinator groups found");
547
+ return;
548
+ }
549
+ for (const group of groups) p.log.message(`- ${String(group.group_id ?? "")}${group.display_name ? ` (${String(group.display_name)})` : ""}`);
550
+ p.outro(`${groups.length} group(s)`);
551
+ } catch (err) {
552
+ if (opts.json) {
553
+ emitJsonError("list_groups_failed", err instanceof Error ? err.message : String(err));
554
+ return;
555
+ }
556
+ p.log.error(err instanceof Error ? err.message : String(err));
557
+ process.exitCode = 1;
558
+ }
559
+ });
560
+ cmd.addCommand(listGroupsCmd);
561
+ const enrollDeviceCmd = new Command("enroll-device").configureHelp(helpStyle).description("Enroll a device in a local coordinator group").argument("<group>", "group id").argument("<device-id>", "device id").option("--fingerprint <fingerprint>", "device fingerprint").option("--public-key <key>", "device public key").option("--public-key-file <path>", "path to device public key").option("--name <name>", "display name");
562
+ addDbOption(enrollDeviceCmd);
563
+ addJsonOption(enrollDeviceCmd);
564
+ enrollDeviceCmd.action(async (groupId, deviceId, opts) => {
565
+ try {
566
+ const publicKey = readCoordinatorPublicKey(opts);
567
+ const fingerprint = String(opts.fingerprint ?? "").trim();
568
+ if (!fingerprint) {
569
+ if (opts.json) {
570
+ emitJsonError("usage_error", "Fingerprint required via --fingerprint", 2);
571
+ return;
572
+ }
573
+ p.log.error("Fingerprint required via --fingerprint");
574
+ process.exitCode = 1;
575
+ return;
576
+ }
577
+ if (fingerprintPublicKey(publicKey) !== fingerprint) {
578
+ if (opts.json) {
579
+ emitJsonError("fingerprint_mismatch", "Fingerprint does not match the provided public key");
580
+ return;
581
+ }
582
+ p.log.error("Fingerprint does not match the provided public key");
583
+ process.exitCode = 1;
584
+ return;
585
+ }
586
+ const enrollment = await coordinatorEnrollDeviceAction({
587
+ groupId,
588
+ deviceId,
589
+ fingerprint,
590
+ publicKey,
591
+ displayName: opts.name?.trim() || null,
592
+ dbPath: resolveDbOpt(opts) ?? null
593
+ });
594
+ if (opts.json) {
595
+ console.log(JSON.stringify(enrollment, null, 2));
596
+ return;
597
+ }
598
+ p.intro("codemem coordinator enroll-device");
599
+ p.log.success(`Enrolled ${deviceId.trim()} in ${groupId.trim()}`);
600
+ p.outro(String(enrollment.display_name ?? enrollment.device_id ?? deviceId.trim()));
601
+ } catch (err) {
602
+ if (opts.json) {
603
+ emitJsonError("enroll_failed", err instanceof Error ? err.message : String(err));
604
+ return;
605
+ }
606
+ p.log.error(err instanceof Error ? err.message : String(err));
607
+ process.exitCode = 1;
608
+ }
609
+ });
610
+ cmd.addCommand(enrollDeviceCmd);
611
+ const listDevicesCmd = new Command("list-devices").configureHelp(helpStyle).description("List enrolled devices in a local coordinator group").argument("<group>", "group id").option("--include-disabled", "include disabled devices");
612
+ addDbOption(listDevicesCmd);
613
+ addJsonOption(listDevicesCmd);
614
+ listDevicesCmd.action(async (groupId, opts) => {
615
+ try {
616
+ const rows = await coordinatorListDevicesAction({
617
+ groupId,
618
+ includeDisabled: opts.includeDisabled === true,
619
+ dbPath: resolveDbOpt(opts) ?? null
620
+ });
621
+ if (opts.json) {
622
+ console.log(JSON.stringify(rows, null, 2));
623
+ return;
624
+ }
625
+ p.intro("codemem coordinator list-devices");
626
+ if (rows.length === 0) {
627
+ p.outro(`No enrolled devices for ${groupId.trim()}`);
628
+ return;
629
+ }
630
+ for (const row of rows) {
631
+ const label = String(row.display_name ?? row.device_id ?? "").trim() || String(row.device_id ?? "");
632
+ const enabled = Number(row.enabled ?? 1) === 1 ? "enabled" : "disabled";
633
+ p.log.message(`- ${label} (${String(row.device_id ?? "")}) ${enabled}`);
634
+ }
635
+ p.outro(`${rows.length} device(s)`);
636
+ } catch (err) {
637
+ if (opts.json) {
638
+ emitJsonError("list_devices_failed", err instanceof Error ? err.message : String(err));
639
+ return;
640
+ }
641
+ p.log.error(err instanceof Error ? err.message : String(err));
642
+ process.exitCode = 1;
643
+ }
644
+ });
645
+ cmd.addCommand(listDevicesCmd);
646
+ const renameDeviceCmd = new Command("rename-device").configureHelp(helpStyle).description("Rename an enrolled device in the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id").requiredOption("--name <name>", "display name");
647
+ addDbOption(renameDeviceCmd);
648
+ addJsonOption(renameDeviceCmd);
649
+ renameDeviceCmd.action(async (groupId, deviceId, opts) => {
650
+ try {
651
+ const result = await coordinatorRenameDeviceAction({
652
+ groupId,
653
+ deviceId,
654
+ displayName: opts.name.trim(),
655
+ dbPath: resolveDbOpt(opts) ?? null
656
+ });
657
+ if (!result) {
658
+ if (opts.json) {
659
+ emitJsonError("device_not_found", `Device not found: ${deviceId.trim()}`);
660
+ return;
661
+ }
662
+ p.log.error(`Device not found: ${deviceId.trim()}`);
663
+ process.exitCode = 1;
664
+ return;
665
+ }
666
+ if (opts.json) {
667
+ console.log(JSON.stringify(result, null, 2));
668
+ return;
669
+ }
670
+ p.intro("codemem coordinator rename-device");
671
+ p.log.success(`Renamed ${deviceId.trim()} in ${groupId.trim()}`);
672
+ p.outro(String(result.display_name ?? result.device_id ?? deviceId.trim()));
673
+ } catch (err) {
674
+ if (opts.json) {
675
+ emitJsonError("rename_failed", err instanceof Error ? err.message : String(err));
676
+ return;
677
+ }
678
+ p.log.error(err instanceof Error ? err.message : String(err));
679
+ process.exitCode = 1;
680
+ }
681
+ });
682
+ cmd.addCommand(renameDeviceCmd);
683
+ const disableDeviceCmd = new Command("disable-device").configureHelp(helpStyle).description("Disable an enrolled device in the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id");
684
+ addDbOption(disableDeviceCmd);
685
+ addJsonOption(disableDeviceCmd);
686
+ disableDeviceCmd.action(async (groupId, deviceId, opts) => {
687
+ try {
688
+ if (!await coordinatorDisableDeviceAction({
689
+ groupId,
690
+ deviceId,
691
+ dbPath: resolveDbOpt(opts) ?? null
692
+ })) {
693
+ if (opts.json) {
694
+ emitJsonError("device_not_found", `Device not found: ${deviceId.trim()}`);
695
+ return;
696
+ }
697
+ p.log.error(`Device not found: ${deviceId.trim()}`);
698
+ process.exitCode = 1;
699
+ return;
700
+ }
701
+ if (opts.json) {
702
+ console.log(JSON.stringify({
703
+ ok: true,
704
+ group_id: groupId.trim(),
705
+ device_id: deviceId.trim()
706
+ }, null, 2));
707
+ return;
708
+ }
709
+ p.intro("codemem coordinator disable-device");
710
+ p.log.success(`Disabled ${deviceId.trim()} in ${groupId.trim()}`);
711
+ p.outro("disabled");
712
+ } catch (err) {
713
+ if (opts.json) {
714
+ emitJsonError("disable_failed", err instanceof Error ? err.message : String(err));
715
+ return;
716
+ }
717
+ p.log.error(err instanceof Error ? err.message : String(err));
718
+ process.exitCode = 1;
719
+ }
720
+ });
721
+ cmd.addCommand(disableDeviceCmd);
722
+ const removeDeviceCmd = new Command("remove-device").configureHelp(helpStyle).description("Remove an enrolled device from the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id");
723
+ addDbOption(removeDeviceCmd);
724
+ addJsonOption(removeDeviceCmd);
725
+ removeDeviceCmd.action(async (groupId, deviceId, opts) => {
726
+ try {
727
+ if (!await coordinatorRemoveDeviceAction({
728
+ groupId,
729
+ deviceId,
730
+ dbPath: resolveDbOpt(opts) ?? null
731
+ })) {
732
+ if (opts.json) {
733
+ emitJsonError("device_not_found", `Device not found: ${deviceId.trim()}`);
734
+ return;
735
+ }
736
+ p.log.error(`Device not found: ${deviceId.trim()}`);
737
+ process.exitCode = 1;
738
+ return;
739
+ }
740
+ if (opts.json) {
741
+ console.log(JSON.stringify({
742
+ ok: true,
743
+ group_id: groupId.trim(),
744
+ device_id: deviceId.trim()
745
+ }, null, 2));
746
+ return;
747
+ }
748
+ p.intro("codemem coordinator remove-device");
749
+ p.log.success(`Removed ${deviceId.trim()} from ${groupId.trim()}`);
750
+ p.outro("removed");
751
+ } catch (err) {
752
+ if (opts.json) {
753
+ emitJsonError("remove_failed", err instanceof Error ? err.message : String(err));
754
+ return;
755
+ }
756
+ p.log.error(err instanceof Error ? err.message : String(err));
757
+ process.exitCode = 1;
758
+ }
759
+ });
760
+ cmd.addCommand(removeDeviceCmd);
761
+ const coordServeCmd = new Command("serve").configureHelp(helpStyle).description("Run the coordinator relay HTTP server").option("--coordinator-host <host>", "bind host").option("--coordinator-port <port>", "bind port");
762
+ coordServeCmd.addOption(new Option("-d, --db-path <path>", "coordinator database path"));
763
+ coordServeCmd.addOption(new Option("--db <path>", "coordinator database path").hideHelp());
764
+ coordServeCmd.addOption(new Option("--host <host>", "bind host").hideHelp());
765
+ coordServeCmd.addOption(new Option("--port <port>", "bind port").hideHelp());
766
+ coordServeCmd.action(async (opts) => {
767
+ const host = String(opts.coordinatorHost ?? opts.host ?? "127.0.0.1").trim() || "127.0.0.1";
768
+ const port = Number.parseInt(String(opts.coordinatorPort ?? opts.port ?? "7347"), 10);
769
+ const dbPath = resolveDbOpt(opts) ?? DEFAULT_COORDINATOR_DB_PATH;
770
+ const app = createBetterSqliteCoordinatorApp({ dbPath });
771
+ p.intro("codemem coordinator serve");
772
+ p.log.success(`Coordinator listening at http://${host}:${port}`);
773
+ p.log.info(`DB: ${dbPath}`);
774
+ serve({
775
+ fetch: app.fetch,
776
+ hostname: host,
777
+ port
778
+ });
779
+ });
780
+ cmd.addCommand(coordServeCmd);
781
+ const listBootstrapGrantsCmd = new Command("list-bootstrap-grants").configureHelp(helpStyle).description("List bootstrap grants for a coordinator group").argument("<group>", "group id").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override");
782
+ addDbOption(listBootstrapGrantsCmd);
783
+ addJsonOption(listBootstrapGrantsCmd);
784
+ listBootstrapGrantsCmd.action(async (groupId, opts) => {
785
+ try {
786
+ const rows = await coordinatorListBootstrapGrantsAction({
787
+ groupId,
788
+ dbPath: resolveDbOpt(opts) ?? null,
789
+ remoteUrl: opts.remoteUrl?.trim() || null,
790
+ adminSecret: opts.adminSecret?.trim() || null
791
+ });
792
+ if (opts.json) {
793
+ console.log(JSON.stringify(rows, null, 2));
794
+ return;
795
+ }
796
+ p.intro("codemem coordinator list-bootstrap-grants");
797
+ if (rows.length === 0) {
798
+ p.outro(`No bootstrap grants for ${groupId.trim()}`);
799
+ return;
800
+ }
801
+ for (const row of rows) p.log.message(`- ${row.grant_id} seed=${row.seed_device_id} worker=${row.worker_device_id} expires=${row.expires_at} revoked=${row.revoked_at ?? "no"}`);
802
+ p.outro(`${rows.length} bootstrap grant(s)`);
803
+ } catch (err) {
804
+ if (opts.json) {
805
+ emitJsonError("list_bootstrap_grants_failed", err instanceof Error ? err.message : String(err));
806
+ return;
807
+ }
808
+ p.log.error(err instanceof Error ? err.message : String(err));
809
+ process.exitCode = 1;
810
+ }
811
+ });
812
+ cmd.addCommand(listBootstrapGrantsCmd);
813
+ const revokeBootstrapGrantCmd = new Command("revoke-bootstrap-grant").configureHelp(helpStyle).description("Revoke a bootstrap grant").argument("<grant-id>", "bootstrap grant id").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override");
814
+ addDbOption(revokeBootstrapGrantCmd);
815
+ addJsonOption(revokeBootstrapGrantCmd);
816
+ revokeBootstrapGrantCmd.action(async (grantId, opts) => {
817
+ try {
818
+ if (!await coordinatorRevokeBootstrapGrantAction({
819
+ grantId,
820
+ dbPath: resolveDbOpt(opts) ?? null,
821
+ remoteUrl: opts.remoteUrl?.trim() || null,
822
+ adminSecret: opts.adminSecret?.trim() || null
823
+ })) {
824
+ if (opts.json) {
825
+ emitJsonError("grant_not_found", `Bootstrap grant not found: ${grantId.trim()}`);
826
+ return;
827
+ }
828
+ p.log.error(`Bootstrap grant not found: ${grantId.trim()}`);
829
+ process.exitCode = 1;
830
+ return;
831
+ }
832
+ if (opts.json) {
833
+ console.log(JSON.stringify({
834
+ ok: true,
835
+ grant_id: grantId.trim()
836
+ }, null, 2));
837
+ return;
838
+ }
839
+ p.intro("codemem coordinator revoke-bootstrap-grant");
840
+ p.log.success(`Revoked ${grantId.trim()}`);
841
+ p.outro("revoked");
842
+ } catch (err) {
843
+ if (opts.json) {
844
+ emitJsonError("revoke_failed", err instanceof Error ? err.message : String(err));
845
+ return;
846
+ }
847
+ p.log.error(err instanceof Error ? err.message : String(err));
848
+ process.exitCode = 1;
849
+ }
850
+ });
851
+ cmd.addCommand(revokeBootstrapGrantCmd);
852
+ const createInviteCmd = new Command("create-invite").configureHelp(helpStyle).description("Create a coordinator team invite").argument("[group]", "group id").option("--group <group>", "group id").option("--coordinator-url <url>", "coordinator URL override").option("--policy <policy>", "invite policy", "auto_admit").option("--ttl-hours <hours>", "invite TTL hours", "24").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override");
853
+ addDbOption(createInviteCmd);
854
+ addJsonOption(createInviteCmd);
855
+ createInviteCmd.action(async (groupArg, opts) => {
856
+ try {
857
+ const ttlHours = Number.parseInt(String(opts.ttlHours ?? "24"), 10);
858
+ const groupId = String(opts.group ?? "").trim() || String(groupArg ?? "").trim();
859
+ const result = await coordinatorCreateInviteAction({
860
+ groupId,
861
+ coordinatorUrl: opts.coordinatorUrl?.trim() || null,
862
+ policy: String(opts.policy ?? "auto_admit").trim(),
863
+ ttlHours,
864
+ createdBy: null,
865
+ dbPath: resolveDbOpt(opts) ?? null,
866
+ remoteUrl: opts.remoteUrl?.trim() || null,
867
+ adminSecret: opts.adminSecret?.trim() || null
868
+ });
869
+ if (opts.json) {
870
+ console.log(JSON.stringify(result, null, 2));
871
+ return;
872
+ }
873
+ p.intro("codemem coordinator create-invite");
874
+ p.log.success(`Invite created for ${groupId}`);
875
+ if (typeof result.link === "string") p.log.message(`- link: ${result.link}`);
876
+ if (typeof result.encoded === "string") p.log.message(`- invite: ${result.encoded}`);
877
+ for (const warning of Array.isArray(result.warnings) ? result.warnings : []) p.log.warn(String(warning));
878
+ p.outro("Invite ready");
879
+ } catch (err) {
880
+ if (opts.json) {
881
+ emitJsonError("create_invite_failed", err instanceof Error ? err.message : String(err));
882
+ return;
883
+ }
884
+ p.log.error(err instanceof Error ? err.message : String(err));
885
+ process.exitCode = 1;
886
+ }
887
+ });
888
+ cmd.addCommand(createInviteCmd);
889
+ const importInviteCmd = new Command("import-invite").configureHelp(helpStyle).description("Import a coordinator invite").argument("<invite>", "invite value or link").option("--keys-dir <path>", "keys directory");
890
+ addDbOption(importInviteCmd);
891
+ addConfigOption(importInviteCmd);
892
+ addJsonOption(importInviteCmd);
893
+ importInviteCmd.action(async (invite, opts) => {
894
+ try {
895
+ const result = await coordinatorImportInviteAction({
896
+ inviteValue: invite,
897
+ dbPath: resolveDbOpt(opts) ?? null,
898
+ keysDir: opts.keysDir ?? null,
899
+ configPath: opts.config ?? null
900
+ });
901
+ if (opts.json) {
902
+ console.log(JSON.stringify(result, null, 2));
903
+ return;
904
+ }
905
+ p.intro("codemem coordinator import-invite");
906
+ p.log.success(`Invite imported for ${result.group_id}`);
907
+ p.log.message(`- coordinator: ${result.coordinator_url}`);
908
+ p.log.message(`- status: ${result.status}`);
909
+ p.outro("Coordinator config updated");
910
+ } catch (err) {
911
+ if (opts.json) {
912
+ emitJsonError("import_invite_failed", err instanceof Error ? err.message : String(err));
913
+ return;
914
+ }
915
+ p.log.error(err instanceof Error ? err.message : String(err));
916
+ process.exitCode = 1;
917
+ }
918
+ });
919
+ cmd.addCommand(importInviteCmd);
920
+ const listJoinRequestsCmd = new Command("list-join-requests").configureHelp(helpStyle).description("List pending coordinator join requests").argument("[group]", "group id").option("--group <group>", "group id").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override");
921
+ addDbOption(listJoinRequestsCmd);
922
+ addJsonOption(listJoinRequestsCmd);
923
+ listJoinRequestsCmd.action(async (groupArg, opts) => {
924
+ try {
925
+ const groupId = String(opts.group ?? "").trim() || String(groupArg ?? "").trim();
926
+ const rows = await coordinatorListJoinRequestsAction({
927
+ groupId,
928
+ dbPath: resolveDbOpt(opts) ?? null,
929
+ remoteUrl: opts.remoteUrl?.trim() || null,
930
+ adminSecret: opts.adminSecret?.trim() || null
931
+ });
932
+ if (opts.json) {
933
+ console.log(JSON.stringify(rows, null, 2));
934
+ return;
935
+ }
936
+ p.intro("codemem coordinator list-join-requests");
937
+ if (rows.length === 0) {
938
+ p.outro(`No pending join requests for ${groupId}`);
939
+ return;
940
+ }
941
+ for (const row of rows) {
942
+ const displayName = row.display_name || row.device_id;
943
+ p.log.message(`- ${displayName} (${row.device_id}) request_id=${row.request_id}`);
944
+ }
945
+ p.outro(`${rows.length} pending join request(s)`);
946
+ } catch (err) {
947
+ if (opts.json) {
948
+ emitJsonError("list_join_requests_failed", err instanceof Error ? err.message : String(err));
949
+ return;
950
+ }
951
+ p.log.error(err instanceof Error ? err.message : String(err));
952
+ process.exitCode = 1;
953
+ }
954
+ });
955
+ cmd.addCommand(listJoinRequestsCmd);
956
+ function addReviewJoinRequestCommand(name, approve) {
957
+ const reviewCmd = new Command(name).configureHelp(helpStyle).description(`${approve ? "Approve" : "Deny"} a coordinator join request`).argument("<request-id>", "join request id").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override");
958
+ addDbOption(reviewCmd);
959
+ addJsonOption(reviewCmd);
960
+ reviewCmd.action(async (requestId, opts) => {
961
+ try {
962
+ const request = await coordinatorReviewJoinRequestAction({
963
+ requestId: requestId.trim(),
964
+ approve,
965
+ reviewedBy: null,
966
+ dbPath: resolveDbOpt(opts) ?? null,
967
+ remoteUrl: opts.remoteUrl?.trim() || null,
968
+ adminSecret: opts.adminSecret?.trim() || null
969
+ });
970
+ if (!request) {
971
+ if (opts.json) {
972
+ emitJsonError("join_request_not_found", `Join request not found: ${requestId.trim()}`);
973
+ return;
974
+ }
975
+ p.log.error(`Join request not found: ${requestId.trim()}`);
976
+ process.exitCode = 1;
977
+ return;
978
+ }
979
+ if (opts.json) {
980
+ console.log(JSON.stringify(request, null, 2));
981
+ return;
982
+ }
983
+ p.intro(`codemem coordinator ${name}`);
984
+ p.log.success(`${approve ? "Approved" : "Denied"} join request ${requestId.trim()}`);
985
+ p.outro(String(request.status ?? "updated"));
986
+ } catch (err) {
987
+ if (opts.json) {
988
+ emitJsonError("review_failed", err instanceof Error ? err.message : String(err));
989
+ return;
990
+ }
991
+ p.log.error(err instanceof Error ? err.message : String(err));
992
+ process.exitCode = 1;
993
+ }
994
+ });
995
+ cmd.addCommand(reviewCmd);
996
+ }
997
+ addReviewJoinRequestCommand("approve-join-request", true);
998
+ addReviewJoinRequestCommand("deny-join-request", false);
999
+ return cmd;
1000
+ }
1001
+ /** Canonical top-level coordinator command for registration in index.ts. */
1002
+ var coordinatorCommand = buildCoordinatorCommand();
182
1003
  //#endregion
183
1004
  //#region src/commands/db.ts
184
1005
  function formatBytes(bytes) {
@@ -210,18 +1031,28 @@ function estimateReplicationOpsBytes(db) {
210
1031
  }
211
1032
  }
212
1033
  var dbCommand = new Command("db").configureHelp(helpStyle).description("Database maintenance");
213
- dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("Create or verify the SQLite database and schema").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) => {
214
- const result = initDatabase(opts.db ?? opts.dbPath);
1034
+ var initCmd = new Command("init").configureHelp(helpStyle).description("Create or verify the SQLite database and schema");
1035
+ addDbOption(initCmd);
1036
+ initCmd.action((opts) => {
1037
+ const result = initDatabase(resolveDbOpt(opts));
215
1038
  p.intro("codemem db init");
216
1039
  p.log.success(`Database ready: ${result.path}`);
217
1040
  p.outro(`Size: ${result.sizeBytes.toLocaleString()} bytes`);
218
- })).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) => {
219
- const result = vacuumDatabase(opts.db ?? opts.dbPath);
1041
+ });
1042
+ dbCommand.addCommand(initCmd);
1043
+ var vacuumCmd = new Command("vacuum").configureHelp(helpStyle).description("Run VACUUM on the SQLite database");
1044
+ addDbOption(vacuumCmd);
1045
+ vacuumCmd.action((opts) => {
1046
+ const result = vacuumDatabase(resolveDbOpt(opts));
220
1047
  p.intro("codemem db vacuum");
221
1048
  p.log.success(`Vacuumed: ${result.path}`);
222
1049
  p.outro(`Size: ${result.sizeBytes.toLocaleString()} bytes`);
223
- })).addCommand(new Command("prune-replication-ops").configureHelp(helpStyle).description("Prune replication op history with approximate oldest-first retention, dry-run, and progress reporting").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("--dry-run", "show current size/targets without deleting").option("--max-age-days <days>", "retention age threshold in days", "30").option("--max-size-mb <mb>", "target replication log budget in MB", "512").option("--batch-ops <n>", "max ops deleted per batch", "5000").option("--batch-runtime-ms <ms>", "max runtime per batch in ms", "2000").option("--vacuum", "run VACUUM explicitly after prune completes").action((opts) => {
224
- const dbPath = resolveDbPath(opts.db ?? opts.dbPath);
1050
+ });
1051
+ dbCommand.addCommand(vacuumCmd);
1052
+ var pruneReplCmd = new Command("prune-replication-ops").configureHelp(helpStyle).description("Prune replication op history with approximate oldest-first retention, dry-run, and progress reporting").option("--dry-run", "show current size/targets without deleting").option("--max-age-days <days>", "retention age threshold in days", "30").option("--max-size-mb <mb>", "target replication log budget in MB", "512").option("--batch-ops <n>", "max ops deleted per batch", "5000").option("--batch-runtime-ms <ms>", "max runtime per batch in ms", "2000").option("--vacuum", "run VACUUM explicitly after prune completes");
1053
+ addDbOption(pruneReplCmd);
1054
+ pruneReplCmd.action((opts) => {
1055
+ const dbPath = resolveDbPath(resolveDbOpt(opts));
225
1056
  const db = connect(dbPath);
226
1057
  let dbOpen = true;
227
1058
  try {
@@ -267,8 +1098,13 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
267
1098
  } finally {
268
1099
  if (dbOpen) db.close();
269
1100
  }
270
- })).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) => {
271
- const result = getRawEventStatus(opts.db ?? opts.dbPath, Number.parseInt(opts.limit, 10) || 25);
1101
+ });
1102
+ dbCommand.addCommand(pruneReplCmd);
1103
+ var rawEventsStatusCmd = new Command("raw-events-status").configureHelp(helpStyle).description("Show pending raw-event backlog by source stream").option("-n, --limit <n>", "max rows to show", "25");
1104
+ addDbOption(rawEventsStatusCmd);
1105
+ addJsonOption(rawEventsStatusCmd);
1106
+ rawEventsStatusCmd.action((opts) => {
1107
+ const result = getRawEventStatus(resolveDbOpt(opts), Number.parseInt(opts.limit, 10) || 25);
272
1108
  if (opts.json) {
273
1109
  console.log(JSON.stringify(result, null, 2));
274
1110
  return;
@@ -281,12 +1117,21 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
281
1117
  }
282
1118
  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 ?? ""}`);
283
1119
  p.outro("done");
284
- })).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) => {
285
- const result = retryRawEventFailures(opts.db ?? opts.dbPath, Number.parseInt(opts.limit, 10) || 25);
1120
+ });
1121
+ dbCommand.addCommand(rawEventsStatusCmd);
1122
+ var rawEventsRetryCmd = new Command("raw-events-retry").configureHelp(helpStyle).description("Requeue failed raw-event flush batches").option("-n, --limit <n>", "max failed batches to requeue", "25");
1123
+ addDbOption(rawEventsRetryCmd);
1124
+ rawEventsRetryCmd.action((opts) => {
1125
+ const result = retryRawEventFailures(resolveDbOpt(opts), Number.parseInt(opts.limit, 10) || 25);
286
1126
  p.intro("codemem db raw-events-retry");
287
1127
  p.outro(`Requeued ${result.retried.toLocaleString()} failed batch(es)`);
288
- })).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) => {
289
- const result = rawEventsGate(opts.db ?? opts.dbPath, {
1128
+ });
1129
+ dbCommand.addCommand(rawEventsRetryCmd);
1130
+ var rawEventsGateCmd = new Command("raw-events-gate").configureHelp(helpStyle).description("Validate raw-event reliability thresholds (non-zero exit on failure)").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");
1131
+ addDbOption(rawEventsGateCmd);
1132
+ addJsonOption(rawEventsGateCmd);
1133
+ rawEventsGateCmd.action((opts) => {
1134
+ const result = rawEventsGate(resolveDbOpt(opts), {
290
1135
  minFlushSuccessRate: Number.parseFloat(opts.minFlushSuccessRate),
291
1136
  maxDroppedEventRate: Number.parseFloat(opts.maxDroppedEventRate),
292
1137
  minSessionBoundaryAccuracy: Number.parseFloat(opts.minSessionBoundaryAccuracy),
@@ -310,8 +1155,12 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
310
1155
  p.outro("reliability gate FAILED");
311
1156
  process.exitCode = 1;
312
1157
  }
313
- })).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) => {
314
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1158
+ });
1159
+ dbCommand.addCommand(rawEventsGateCmd);
1160
+ var renameProjectCmd = 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("--apply", "apply changes (default is dry-run)");
1161
+ addDbOption(renameProjectCmd);
1162
+ renameProjectCmd.action((oldName, newName, opts) => {
1163
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
315
1164
  try {
316
1165
  const dryRun = !opts.apply;
317
1166
  const suffixPattern = `%/${oldName.replace(/%/g, "\\%").replace(/_/g, "\\_")}`;
@@ -338,8 +1187,12 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
338
1187
  } finally {
339
1188
  store.close();
340
1189
  }
341
- })).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) => {
342
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1190
+ });
1191
+ dbCommand.addCommand(renameProjectCmd);
1192
+ var normalizeProjectsCmd = new Command("normalize-projects").configureHelp(helpStyle).description("Normalize path-like project identifiers to their basename").option("--apply", "apply changes (default is dry-run)");
1193
+ addDbOption(normalizeProjectsCmd);
1194
+ normalizeProjectsCmd.action((opts) => {
1195
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
343
1196
  try {
344
1197
  const dryRun = !opts.apply;
345
1198
  const tables = ["sessions", "raw_event_sessions"];
@@ -379,8 +1232,13 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
379
1232
  } finally {
380
1233
  store.close();
381
1234
  }
382
- })).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) => {
383
- const dbPath = resolveDbPath(opts.db ?? opts.dbPath);
1235
+ });
1236
+ dbCommand.addCommand(normalizeProjectsCmd);
1237
+ var sizeReportCmd = new Command("size-report").configureHelp(helpStyle).description("Show SQLite file size and major storage consumers").option("--limit <n>", "number of largest tables/indexes to show", "12");
1238
+ addDbOption(sizeReportCmd);
1239
+ addJsonOption(sizeReportCmd);
1240
+ sizeReportCmd.action((opts) => {
1241
+ const dbPath = resolveDbPath(resolveDbOpt(opts));
384
1242
  const db = connect(dbPath);
385
1243
  try {
386
1244
  const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 12);
@@ -389,10 +1247,10 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
389
1247
  const freePages = db.prepare("SELECT freelist_count FROM pragma_freelist_count").get();
390
1248
  const pageSize = db.prepare("PRAGMA page_size").get();
391
1249
  const tables = db.prepare(`SELECT name, SUM(pgsize) as size_bytes
392
- FROM dbstat
393
- GROUP BY name
394
- ORDER BY size_bytes DESC
395
- LIMIT ?`).all(limit);
1250
+ FROM dbstat
1251
+ GROUP BY name
1252
+ ORDER BY size_bytes DESC
1253
+ LIMIT ?`).all(limit);
396
1254
  if (opts.json) {
397
1255
  console.log(JSON.stringify({
398
1256
  file_size_bytes: fileSizeBytes,
@@ -419,8 +1277,13 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
419
1277
  } finally {
420
1278
  db.close();
421
1279
  }
422
- })).addCommand(new Command("backfill-tags").configureHelp(helpStyle).description("Populate tags_text for memories where tags are empty").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 memories to check").option("--since <iso>", "only memories created at/after this ISO timestamp").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "backfill across all projects").option("--inactive", "include inactive memories").option("--dry-run", "preview updates without writing").option("--json", "output as JSON").action((opts) => {
423
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1280
+ });
1281
+ dbCommand.addCommand(sizeReportCmd);
1282
+ var backfillTagsCmd = new Command("backfill-tags").configureHelp(helpStyle).description("Populate tags_text for memories where tags are empty").option("--limit <n>", "max memories to check").option("--since <iso>", "only memories created at/after this ISO timestamp").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "backfill across all projects").option("--inactive", "include inactive memories").option("--dry-run", "preview updates without writing");
1283
+ addDbOption(backfillTagsCmd);
1284
+ addJsonOption(backfillTagsCmd);
1285
+ backfillTagsCmd.action((opts) => {
1286
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
424
1287
  try {
425
1288
  const limit = parseOptionalPositiveInt$1(opts.limit);
426
1289
  const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
@@ -445,8 +1308,13 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
445
1308
  } finally {
446
1309
  store.close();
447
1310
  }
448
- })).addCommand(new Command("prune-observations").configureHelp(helpStyle).description("Deactivate low-signal observations (does not delete rows)").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 observations to check").option("--dry-run", "preview deactivations without writing").option("--json", "output as JSON").action((opts) => {
449
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1311
+ });
1312
+ dbCommand.addCommand(backfillTagsCmd);
1313
+ var pruneObsCmd = new Command("prune-observations").configureHelp(helpStyle).description("Deactivate low-signal observations (does not delete rows)").option("--limit <n>", "max observations to check").option("--dry-run", "preview deactivations without writing");
1314
+ addDbOption(pruneObsCmd);
1315
+ addJsonOption(pruneObsCmd);
1316
+ pruneObsCmd.action((opts) => {
1317
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
450
1318
  try {
451
1319
  const limit = parseOptionalPositiveInt$1(opts.limit);
452
1320
  const result = deactivateLowSignalObservations(store.db, limit ?? null, opts.dryRun === true);
@@ -463,8 +1331,13 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
463
1331
  } finally {
464
1332
  store.close();
465
1333
  }
466
- })).addCommand(new Command("prune-memories").configureHelp(helpStyle).description("Deactivate low-signal memories across selected kinds").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 memories to check").option("--kinds <csv>", "comma-separated memory kinds (default set when omitted)").option("--dry-run", "preview deactivations without writing").option("--json", "output as JSON").action((opts) => {
467
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1334
+ });
1335
+ dbCommand.addCommand(pruneObsCmd);
1336
+ var pruneMemCmd = new Command("prune-memories").configureHelp(helpStyle).description("Deactivate low-signal memories across selected kinds").option("--limit <n>", "max memories to check").option("--kinds <csv>", "comma-separated memory kinds (default set when omitted)").option("--dry-run", "preview deactivations without writing");
1337
+ addDbOption(pruneMemCmd);
1338
+ addJsonOption(pruneMemCmd);
1339
+ pruneMemCmd.action((opts) => {
1340
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
468
1341
  try {
469
1342
  const limit = parseOptionalPositiveInt$1(opts.limit);
470
1343
  const kinds = parseKindsCsv(opts.kinds);
@@ -486,7 +1359,8 @@ dbCommand.addCommand(new Command("init").configureHelp(helpStyle).description("C
486
1359
  } finally {
487
1360
  store.close();
488
1361
  }
489
- }));
1362
+ });
1363
+ dbCommand.addCommand(pruneMemCmd);
490
1364
  //#endregion
491
1365
  //#region src/commands/embed.ts
492
1366
  function parseOptionalPositiveInt(value) {
@@ -503,8 +1377,11 @@ function resolveEmbedProjectScope(cwd, projectOpt, allProjects) {
503
1377
  if (envProject) return envProject;
504
1378
  return resolveProject(cwd, null);
505
1379
  }
506
- var embedCommand = new Command("embed").configureHelp(helpStyle).description("Backfill semantic embeddings").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 memories to check").option("--since <iso>", "only memories created at/after this ISO timestamp").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "embed across all projects").option("--inactive", "include inactive memories").option("--dry-run", "preview work without writing vectors").option("--json", "output as JSON").action(async (opts) => {
507
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1380
+ var embedCmd = new Command("embed").configureHelp(helpStyle).description("Backfill semantic embeddings").option("--limit <n>", "max memories to check").option("--since <iso>", "only memories created at/after this ISO timestamp").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "embed across all projects").option("--inactive", "include inactive memories").option("--dry-run", "preview work without writing vectors");
1381
+ addDbOption(embedCmd);
1382
+ addJsonOption(embedCmd);
1383
+ var embedCommand = embedCmd.action(async (opts) => {
1384
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
508
1385
  try {
509
1386
  const limit = parseOptionalPositiveInt(opts.limit);
510
1387
  const project = resolveEmbedProjectScope(process.cwd(), opts.project, opts.allProjects);
@@ -547,7 +1424,7 @@ function resolveSessionStreamId(payload) {
547
1424
  if (text) values.set(key, text);
548
1425
  }
549
1426
  if (values.size === 0) return null;
550
- if (new Set(values.values()).size > 1) throw new Error("conflicting session id fields");
1427
+ if (new Set(values.values()).size > 1) return null;
551
1428
  for (const key of SESSION_ID_KEYS) {
552
1429
  const value = values.get(key);
553
1430
  if (value) return value;
@@ -563,39 +1440,61 @@ async function readStdinJson() {
563
1440
  if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("payload must be an object");
564
1441
  return parsed;
565
1442
  }
566
- 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) => {
567
- const payload = await readStdinJson();
568
- const sessionId = resolveSessionStreamId(payload);
569
- if (!sessionId) throw new Error("session id required");
570
- if (sessionId.startsWith("msg_")) throw new Error("invalid session id");
571
- const eventType = typeof payload.event_type === "string" ? payload.event_type.trim() : "";
572
- if (!eventType) throw new Error("event_type required");
573
- const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
574
- const project = typeof payload.project === "string" ? payload.project : null;
575
- const startedAt = typeof payload.started_at === "string" ? payload.started_at : null;
576
- const tsWallMs = Number.isFinite(Number(payload.ts_wall_ms)) ? Math.floor(Number(payload.ts_wall_ms)) : null;
577
- const tsMonoMs = Number.isFinite(Number(payload.ts_mono_ms)) ? Number(payload.ts_mono_ms) : null;
578
- const eventId = typeof payload.event_id === "string" ? payload.event_id.trim() : "";
579
- const eventPayload = payload.payload && typeof payload.payload === "object" && !Array.isArray(payload.payload) ? stripPrivateObj(payload.payload) : {};
580
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1443
+ function emitStructuredError(errorCode, message) {
1444
+ console.log(JSON.stringify({
1445
+ error: errorCode,
1446
+ message
1447
+ }));
1448
+ process.exitCode = 1;
1449
+ }
1450
+ var enqueueCmd = new Command("enqueue-raw-event").configureHelp(helpStyle).description("Enqueue one raw event from stdin into the durable queue");
1451
+ addDbOption(enqueueCmd);
1452
+ var enqueueRawEventCommand = enqueueCmd.action(async (opts) => {
581
1453
  try {
582
- store.updateRawEventSessionMeta({
583
- opencodeSessionId: sessionId,
584
- source: "opencode",
585
- cwd,
586
- project,
587
- startedAt,
588
- lastSeenTsWallMs: tsWallMs
589
- });
590
- store.recordRawEventsBatch(sessionId, [{
591
- event_id: eventId,
592
- event_type: eventType,
593
- payload: eventPayload,
594
- ts_wall_ms: tsWallMs,
595
- ts_mono_ms: tsMonoMs
596
- }]);
597
- } finally {
598
- store.close();
1454
+ const payload = await readStdinJson();
1455
+ const sessionId = resolveSessionStreamId(payload);
1456
+ if (!sessionId) {
1457
+ emitStructuredError("validation_error", "session id required");
1458
+ return;
1459
+ }
1460
+ if (sessionId.startsWith("msg_")) {
1461
+ emitStructuredError("validation_error", "invalid session id");
1462
+ return;
1463
+ }
1464
+ const eventType = typeof payload.event_type === "string" ? payload.event_type.trim() : "";
1465
+ if (!eventType) {
1466
+ emitStructuredError("validation_error", "event_type required");
1467
+ return;
1468
+ }
1469
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
1470
+ const project = typeof payload.project === "string" ? payload.project : null;
1471
+ const startedAt = typeof payload.started_at === "string" ? payload.started_at : null;
1472
+ const tsWallMs = Number.isFinite(Number(payload.ts_wall_ms)) ? Math.floor(Number(payload.ts_wall_ms)) : null;
1473
+ const tsMonoMs = Number.isFinite(Number(payload.ts_mono_ms)) ? Number(payload.ts_mono_ms) : null;
1474
+ const eventId = typeof payload.event_id === "string" ? payload.event_id.trim() : "";
1475
+ const eventPayload = payload.payload && typeof payload.payload === "object" && !Array.isArray(payload.payload) ? stripPrivateObj(payload.payload) : {};
1476
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1477
+ try {
1478
+ store.updateRawEventSessionMeta({
1479
+ opencodeSessionId: sessionId,
1480
+ source: "opencode",
1481
+ cwd,
1482
+ project,
1483
+ startedAt,
1484
+ lastSeenTsWallMs: tsWallMs
1485
+ });
1486
+ store.recordRawEventsBatch(sessionId, [{
1487
+ event_id: eventId,
1488
+ event_type: eventType,
1489
+ payload: eventPayload,
1490
+ ts_wall_ms: tsWallMs,
1491
+ ts_mono_ms: tsMonoMs
1492
+ }]);
1493
+ } finally {
1494
+ store.close();
1495
+ }
1496
+ } catch (err) {
1497
+ emitStructuredError("enqueue_error", err instanceof Error ? err.message : String(err));
599
1498
  }
600
1499
  });
601
1500
  //#endregion
@@ -603,9 +1502,11 @@ var enqueueRawEventCommand = new Command("enqueue-raw-event").configureHelp(help
603
1502
  function expandUserPath(value) {
604
1503
  return value.startsWith("~/") ? join(homedir(), value.slice(2)) : value;
605
1504
  }
606
- 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) => {
1505
+ var cmd$2 = 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("--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");
1506
+ addDbOption(cmd$2);
1507
+ cmd$2.action((output, opts) => {
607
1508
  const payload = exportMemories({
608
- dbPath: resolveDbPath(opts.db ?? opts.dbPath),
1509
+ dbPath: resolveDbPath(resolveDbOpt(opts)),
609
1510
  project: opts.project,
610
1511
  allProjects: opts.allProjects,
611
1512
  includeInactive: opts.includeInactive,
@@ -628,31 +1529,49 @@ var exportMemoriesCommand = new Command("export-memories").configureHelp(helpSty
628
1529
  ].join("\n"));
629
1530
  p.outro("done");
630
1531
  });
1532
+ var exportMemoriesCommand = cmd$2;
631
1533
  //#endregion
632
1534
  //#region src/commands/import-memories.ts
633
- 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) => {
1535
+ var cmd$1 = new Command("import-memories").configureHelp(helpStyle).description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing");
1536
+ addDbOption(cmd$1);
1537
+ addJsonOption(cmd$1);
1538
+ cmd$1.action((inputFile, opts) => {
634
1539
  let payload;
635
1540
  try {
636
1541
  payload = readImportPayload(inputFile);
637
1542
  } catch (error) {
638
- p.log.error(error instanceof Error ? error.message : "Invalid import file");
639
- process.exitCode = 1;
1543
+ const message = error instanceof Error ? error.message : "Invalid import file";
1544
+ if (opts.json) emitJsonError("invalid_input", message);
1545
+ else {
1546
+ p.log.error(message);
1547
+ process.exitCode = 1;
1548
+ }
640
1549
  return;
641
1550
  }
642
- p.intro("codemem import-memories");
643
- p.log.info([
644
- `Export version: ${payload.version}`,
645
- `Exported at: ${payload.exported_at}`,
646
- `Sessions: ${payload.sessions.length.toLocaleString()}`,
647
- `Memories: ${payload.memory_items.length.toLocaleString()}`,
648
- `Summaries: ${payload.session_summaries.length.toLocaleString()}`,
649
- `Prompts: ${payload.user_prompts.length.toLocaleString()}`
650
- ].join("\n"));
1551
+ if (!opts.json) {
1552
+ p.intro("codemem import-memories");
1553
+ p.log.info([
1554
+ `Export version: ${payload.version}`,
1555
+ `Exported at: ${payload.exported_at}`,
1556
+ `Sessions: ${payload.sessions.length.toLocaleString()}`,
1557
+ `Memories: ${payload.memory_items.length.toLocaleString()}`,
1558
+ `Summaries: ${payload.session_summaries.length.toLocaleString()}`,
1559
+ `Prompts: ${payload.user_prompts.length.toLocaleString()}`
1560
+ ].join("\n"));
1561
+ }
651
1562
  const result = importMemories(payload, {
652
- dbPath: resolveDbPath(opts.db ?? opts.dbPath),
1563
+ dbPath: resolveDbPath(resolveDbOpt(opts)),
653
1564
  remapProject: opts.remapProject,
654
1565
  dryRun: opts.dryRun
655
1566
  });
1567
+ if (opts.json) {
1568
+ console.log(JSON.stringify({
1569
+ sessions: result.sessions,
1570
+ memory_items: result.memory_items,
1571
+ skipped: result.dryRun
1572
+ }));
1573
+ return;
1574
+ }
656
1575
  if (result.dryRun) {
657
1576
  p.outro("dry run complete");
658
1577
  return;
@@ -665,25 +1584,53 @@ var importMemoriesCommand = new Command("import-memories").configureHelp(helpSty
665
1584
  ].join("\n"));
666
1585
  p.outro("done");
667
1586
  });
1587
+ var importMemoriesCommand = cmd$1;
668
1588
  //#endregion
669
1589
  //#region src/commands/mcp.ts
670
- 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) => {
671
- const dbPath = opts.db ?? opts.dbPath;
1590
+ var mcpCmd = new Command("mcp").configureHelp(helpStyle).description("Start the MCP stdio server");
1591
+ addDbOption(mcpCmd);
1592
+ var mcpCommand = mcpCmd.action(async (opts) => {
1593
+ const dbPath = resolveDbOpt(opts);
672
1594
  if (dbPath) process.env.CODEMEM_DB = dbPath;
673
- await import("@codemem/mcp");
1595
+ try {
1596
+ await import("@codemem/mcp");
1597
+ } catch (err) {
1598
+ console.error(`Failed to start MCP server: ${err instanceof Error ? err.message : String(err)}`);
1599
+ process.exitCode = 1;
1600
+ }
674
1601
  });
675
1602
  //#endregion
1603
+ //#region src/commands/pack-shared.ts
1604
+ function collectWorkingSetFile(value, previous) {
1605
+ return [...previous, value];
1606
+ }
1607
+ function buildPackRequestOptions(opts, ctx = {}) {
1608
+ const limit = Number.parseInt(opts.limit ?? "10", 10) || 10;
1609
+ const budgetRaw = opts.tokenBudget ?? opts.budget;
1610
+ const budget = budgetRaw ? Number.parseInt(budgetRaw, 10) : void 0;
1611
+ const filters = {};
1612
+ if (!opts.allProjects) {
1613
+ const defaultProject = ctx.envProject?.trim() || null;
1614
+ const resolveProjectFn = ctx.resolveProjectFn ?? resolveProject;
1615
+ const cwd = ctx.cwd ?? process.cwd();
1616
+ const project = defaultProject || resolveProjectFn(cwd, opts.project ?? null);
1617
+ if (project) filters.project = project;
1618
+ }
1619
+ if ((opts.workingSetFile?.length ?? 0) > 0) filters.working_set_paths = opts.workingSetFile;
1620
+ return {
1621
+ limit,
1622
+ budget,
1623
+ filters
1624
+ };
1625
+ }
1626
+ //#endregion
676
1627
  //#region src/commands/memory.ts
677
1628
  /**
678
- * Memory management CLI commands — show, forget, remember.
1629
+ * Memory management CLI commands — show, forget, remember, inject.
679
1630
  *
680
1631
  * Ports codemem/commands/memory_cmds.py (show_cmd, forget_cmd, remember_cmd).
681
- * Compact and inject are deferred compact requires the summarizer pipeline,
682
- * and inject is just pack_text output which the existing pack command covers.
1632
+ * Inject is deprecated in favor of `codemem pack`.
683
1633
  */
684
- function collectWorkingSetFile$1(value, previous) {
685
- return [...previous, value];
686
- }
687
1634
  /** Parse a strict positive integer, rejecting prefixes like "12abc". */
688
1635
  function parseStrictPositiveId(value) {
689
1636
  if (!/^\d+$/.test(value.trim())) return null;
@@ -693,19 +1640,35 @@ function parseStrictPositiveId(value) {
693
1640
  function showMemoryAction(idStr, opts) {
694
1641
  const memoryId = parseStrictPositiveId(idStr);
695
1642
  if (memoryId === null) {
696
- p.log.error(`Invalid memory ID: ${idStr}`);
697
- process.exitCode = 1;
1643
+ if (opts.json) emitJsonError("invalid_id", `Invalid memory ID: ${idStr}`);
1644
+ else {
1645
+ p.log.error(`Invalid memory ID: ${idStr}`);
1646
+ process.exitCode = 1;
1647
+ }
698
1648
  return;
699
1649
  }
700
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1650
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
701
1651
  try {
702
1652
  const item = store.get(memoryId);
703
1653
  if (!item) {
704
- p.log.error(`Memory ${memoryId} not found`);
705
- process.exitCode = 1;
1654
+ if (opts.json) emitJsonError("not_found", `Memory ${memoryId} not found`);
1655
+ else {
1656
+ p.log.error(`Memory ${memoryId} not found`);
1657
+ process.exitCode = 1;
1658
+ }
706
1659
  return;
707
1660
  }
708
- console.log(JSON.stringify(item, null, 2));
1661
+ if (opts.json) console.log(JSON.stringify(item, null, 2));
1662
+ else {
1663
+ console.log(`#${item.id} [${item.kind}] ${item.title}`);
1664
+ if (item.subtitle) console.log(` ${item.subtitle}`);
1665
+ console.log(` created: ${item.created_at} confidence: ${item.confidence}`);
1666
+ if (item.tags_text) console.log(` tags: ${item.tags_text}`);
1667
+ if (item.body_text) {
1668
+ const preview = item.body_text.length > 300 ? `${item.body_text.slice(0, 300)}…` : item.body_text;
1669
+ console.log(`\n${preview}`);
1670
+ }
1671
+ }
709
1672
  } finally {
710
1673
  store.close();
711
1674
  }
@@ -713,20 +1676,27 @@ function showMemoryAction(idStr, opts) {
713
1676
  function forgetMemoryAction(idStr, opts) {
714
1677
  const memoryId = parseStrictPositiveId(idStr);
715
1678
  if (memoryId === null) {
716
- p.log.error(`Invalid memory ID: ${idStr}`);
717
- process.exitCode = 1;
1679
+ if (opts.json) emitJsonError("invalid_id", `Invalid memory ID: ${idStr}`);
1680
+ else {
1681
+ p.log.error(`Invalid memory ID: ${idStr}`);
1682
+ process.exitCode = 1;
1683
+ }
718
1684
  return;
719
1685
  }
720
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1686
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
721
1687
  try {
722
1688
  store.forget(memoryId);
723
- p.log.success(`Memory ${memoryId} marked inactive`);
1689
+ if (opts.json) console.log(JSON.stringify({
1690
+ id: memoryId,
1691
+ status: "forgotten"
1692
+ }));
1693
+ else p.log.success(`Memory ${memoryId} marked inactive`);
724
1694
  } finally {
725
1695
  store.close();
726
1696
  }
727
1697
  }
728
1698
  async function rememberMemoryAction(opts) {
729
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
1699
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
730
1700
  let sessionId = null;
731
1701
  try {
732
1702
  const project = resolveProject(process.cwd(), opts.project ?? null);
@@ -740,7 +1710,8 @@ async function rememberMemoryAction(opts) {
740
1710
  const memId = store.remember(sessionId, opts.kind, opts.title, opts.body, .5, opts.tags);
741
1711
  await store.flushPendingVectorWrites();
742
1712
  store.endSession(sessionId, { manual: true });
743
- p.log.success(`Stored memory ${memId}`);
1713
+ if (opts.json) console.log(JSON.stringify({ id: memId }));
1714
+ else p.log.success(`Stored memory ${memId}`);
744
1715
  } catch (err) {
745
1716
  if (sessionId !== null) try {
746
1717
  store.endSession(sessionId, {
@@ -748,46 +1719,504 @@ async function rememberMemoryAction(opts) {
748
1719
  error: true
749
1720
  });
750
1721
  } catch {}
751
- throw err;
1722
+ const message = err instanceof Error ? err.message : String(err);
1723
+ if (opts.json) emitJsonError("remember_failed", message);
1724
+ else {
1725
+ p.log.error(`Failed to store memory: ${message}`);
1726
+ process.exitCode = 1;
1727
+ }
752
1728
  } finally {
753
1729
  store.close();
754
1730
  }
755
1731
  }
756
1732
  function createShowMemoryCommand() {
757
- 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);
1733
+ const cmd = new Command("show").configureHelp(helpStyle).description("Show a memory item").argument("<id>", "memory ID");
1734
+ addDbOption(cmd);
1735
+ addJsonOption(cmd);
1736
+ cmd.action(showMemoryAction);
1737
+ return cmd;
758
1738
  }
759
1739
  function createForgetMemoryCommand() {
760
- 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);
1740
+ const cmd = new Command("forget").configureHelp(helpStyle).description("Deactivate a memory item").argument("<id>", "memory ID");
1741
+ addDbOption(cmd);
1742
+ addJsonOption(cmd);
1743
+ cmd.action(forgetMemoryAction);
1744
+ return cmd;
761
1745
  }
762
1746
  function createRememberMemoryCommand() {
763
- 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);
1747
+ const cmd = 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)");
1748
+ addDbOption(cmd);
1749
+ addJsonOption(cmd);
1750
+ cmd.action(rememberMemoryAction);
1751
+ return cmd;
1752
+ }
1753
+ function createInjectMemoryCommand() {
1754
+ const cmd = new Command("inject").configureHelp(helpStyle).description("Build raw memory context text for manual prompt injection").argument("<context>", "context string to search for").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").allowUnknownOption(true).allowExcessArguments(true);
1755
+ addDbOption(cmd);
1756
+ cmd.action(async (context, opts) => {
1757
+ emitDeprecationWarning("codemem memory inject", "codemem pack");
1758
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1759
+ try {
1760
+ const { limit, budget, filters } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
1761
+ const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
1762
+ console.log(pack.pack_text ?? "");
1763
+ } finally {
1764
+ store.close();
1765
+ }
1766
+ });
1767
+ return cmd;
1768
+ }
1769
+ function createMemoryRoleReportCommand() {
1770
+ const cmd = new Command("role-report").configureHelp(helpStyle).description("Analyze inferred memory roles in a DB snapshot").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--probe <query>", "run a retrieval probe query against the snapshot", (value, prev) => [...prev, value], []).option("--scenario <id>", "run a named injection-first eval scenario pack (can be repeated)", (value, prev) => [...prev, value], []).option("--inactive", "include inactive memories");
1771
+ addDbOption(cmd);
1772
+ addJsonOption(cmd);
1773
+ cmd.action((opts) => {
1774
+ const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
1775
+ const invalidScenario = (opts.scenario ?? []).find((id) => getInjectionEvalScenarioPack(id) == null);
1776
+ if (invalidScenario) throw new Error(`Unknown eval scenario pack: ${invalidScenario}`);
1777
+ const probes = [...opts.probe ?? [], ...getInjectionEvalScenarioPrompts(opts.scenario ?? [])];
1778
+ const result = getMemoryRoleReport(resolveDbOpt(opts), {
1779
+ project,
1780
+ allProjects: opts.allProjects === true,
1781
+ includeInactive: opts.inactive === true,
1782
+ probes
1783
+ });
1784
+ if (opts.json) {
1785
+ console.log(JSON.stringify(result, null, 2));
1786
+ return;
1787
+ }
1788
+ p.intro("codemem memory role-report");
1789
+ p.log.info([
1790
+ `Memories: ${result.totals.memories}`,
1791
+ `Active: ${result.totals.active}`,
1792
+ `Sessions: ${result.totals.sessions}`
1793
+ ].join("\n"));
1794
+ p.log.info("Counts by role:");
1795
+ for (const [role, count] of Object.entries(result.counts_by_role)) p.log.message(` ${role.padEnd(10)} ${String(count)}`);
1796
+ p.log.info("Counts by mapping:");
1797
+ p.log.message(` mapped ${result.counts_by_mapping.mapped}`);
1798
+ p.log.message(` unmapped ${result.counts_by_mapping.unmapped}`);
1799
+ p.log.info("Summary lineages:");
1800
+ p.log.message(` session_summary ${result.summary_lineages.session_summary}`);
1801
+ p.log.message(` legacy_metadata_summary ${result.summary_lineages.legacy_metadata_summary}`);
1802
+ p.log.message(` summary_mapped ${result.summary_mapping.mapped}`);
1803
+ p.log.message(` summary_unmapped ${result.summary_mapping.unmapped}`);
1804
+ p.log.info("Project quality:");
1805
+ for (const [bucket, count] of Object.entries(result.project_quality)) p.log.message(` ${bucket.padEnd(12)} ${String(count)}`);
1806
+ p.log.info("Session classes:");
1807
+ for (const [bucket, count] of Object.entries(result.session_class_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
1808
+ p.log.info("Summary dispositions:");
1809
+ for (const [bucket, count] of Object.entries(result.summary_disposition_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
1810
+ if (result.probe_results.length > 0) {
1811
+ p.log.info("Probe results:");
1812
+ for (const probe of result.probe_results) {
1813
+ p.log.message(` query: ${probe.query}`);
1814
+ if (probe.scenario_id) p.log.message(` scenario: ${probe.scenario_id} (${probe.scenario_category ?? "unknown"})${probe.scenario_title ? ` — ${probe.scenario_title}` : ""}`);
1815
+ p.log.message(` mode: ${probe.mode}`);
1816
+ p.log.message(` top roles: durable=${probe.top_role_counts.durable} recap=${probe.top_role_counts.recap} ephemeral=${probe.top_role_counts.ephemeral} general=${probe.top_role_counts.general}`);
1817
+ p.log.message(` top mapping: mapped=${probe.top_mapping_counts.mapped} unmapped=${probe.top_mapping_counts.unmapped}`);
1818
+ p.log.message(` burden: recap_share=${probe.top_burden.recap_share.toFixed(2)} unmapped_share=${probe.top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.top_burden.recap_unmapped_share.toFixed(2)}`);
1819
+ if (probe.simulated_demoted_unmapped_recap) p.log.message(` simulated demote-unmapped-recap burden: recap_share=${probe.simulated_demoted_unmapped_recap.top_burden.recap_share.toFixed(2)} unmapped_share=${probe.simulated_demoted_unmapped_recap.top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.simulated_demoted_unmapped_recap.top_burden.recap_unmapped_share.toFixed(2)}`);
1820
+ if (probe.simulated_demoted_unmapped_recap_and_ephemeral) p.log.message(` simulated demote-unmapped-recap+ephemeral burden: recap_share=${probe.simulated_demoted_unmapped_recap_and_ephemeral.top_burden.recap_share.toFixed(2)} unmapped_share=${probe.simulated_demoted_unmapped_recap_and_ephemeral.top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.simulated_demoted_unmapped_recap_and_ephemeral.top_burden.recap_unmapped_share.toFixed(2)}`);
1821
+ if (probe.scenario_score) p.log.message(` scenario score: mode_match=${probe.scenario_score.mode_match ? "yes" : "no"} top1_primary=${probe.scenario_score.primary_in_top1 ? "yes" : "no"} top3_primary=${probe.scenario_score.primary_in_top3_count} top1_anti=${probe.scenario_score.anti_signal_in_top1 ? "yes" : "no"} primary=${probe.scenario_score.primary_match_count} anti=${probe.scenario_score.anti_signal_count} recap=${probe.scenario_score.recap_count} unmapped_recap=${probe.scenario_score.unmapped_recap_count} chatter=${probe.scenario_score.administrative_chatter_count} net=${probe.scenario_score.score}`);
1822
+ for (const item of probe.items.slice(0, 5)) p.log.message(` [${item.id}] (${item.kind}/${item.role}/${item.mapping}/${item.session_class}/${item.summary_disposition}) ${item.title} — ${item.role_reason}`);
1823
+ }
1824
+ }
1825
+ p.outro("done");
1826
+ });
1827
+ return cmd;
1828
+ }
1829
+ function createMemoryRoleCompareCommand() {
1830
+ const cmd = new Command("role-compare").configureHelp(helpStyle).description("Compare inferred memory-role and probe metrics across two DB snapshots").argument("<baseline_db>", "baseline sqlite database path").argument("<candidate_db>", "candidate sqlite database path").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--probe <query>", "run a retrieval probe query against both snapshots", (value, prev) => [...prev, value], []).option("--scenario <id>", "run a named injection-first eval scenario pack (can be repeated)", (value, prev) => [...prev, value], []).option("--inactive", "include inactive memories");
1831
+ addJsonOption(cmd);
1832
+ cmd.action((baselineDb, candidateDb, opts) => {
1833
+ const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
1834
+ const invalidScenario = (opts.scenario ?? []).find((id) => getInjectionEvalScenarioPack(id) == null);
1835
+ if (invalidScenario) throw new Error(`Unknown eval scenario pack: ${invalidScenario}`);
1836
+ const probes = [...opts.probe ?? [], ...getInjectionEvalScenarioPrompts(opts.scenario ?? [])];
1837
+ const result = compareMemoryRoleReports(baselineDb, candidateDb, {
1838
+ project,
1839
+ allProjects: opts.allProjects === true,
1840
+ includeInactive: opts.inactive === true,
1841
+ probes
1842
+ });
1843
+ if (opts.json) {
1844
+ console.log(JSON.stringify(result, null, 2));
1845
+ return;
1846
+ }
1847
+ p.intro("codemem memory role-compare");
1848
+ p.log.info([
1849
+ `Baseline sessions: ${result.baseline.totals.sessions}`,
1850
+ `Candidate sessions: ${result.candidate.totals.sessions}`,
1851
+ `Delta sessions: ${result.delta.totals.sessions}`,
1852
+ `Mapped delta: ${result.delta.counts_by_mapping.mapped}`,
1853
+ `Unmapped delta: ${result.delta.counts_by_mapping.unmapped}`,
1854
+ `Summary mapped delta: ${result.delta.summary_mapping.mapped}`,
1855
+ `Summary unmapped delta: ${result.delta.summary_mapping.unmapped}`
1856
+ ].join("\n"));
1857
+ p.log.info("Role deltas:");
1858
+ for (const [role, count] of Object.entries(result.delta.counts_by_role)) p.log.message(` ${role.padEnd(10)} ${String(count)}`);
1859
+ p.log.info("Session class deltas:");
1860
+ for (const [bucket, count] of Object.entries(result.delta.session_class_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
1861
+ p.log.info("Summary disposition deltas:");
1862
+ for (const [bucket, count] of Object.entries(result.delta.summary_disposition_buckets)) p.log.message(` ${bucket.padEnd(20)} ${String(count)}`);
1863
+ if (result.probe_comparisons.length > 0) {
1864
+ p.log.info("Probe comparisons:");
1865
+ for (const probe of result.probe_comparisons) {
1866
+ p.log.message(` query: ${probe.query}`);
1867
+ p.log.message(` modes: baseline=${probe.baseline_mode ?? "-"} candidate=${probe.candidate_mode ?? "-"}`);
1868
+ p.log.message(` overlap: shared_top_keys=${probe.shared_item_keys.length} baseline_top=${probe.baseline_item_ids.slice(0, 5).join(",") || "-"} candidate_top=${probe.candidate_item_ids.slice(0, 5).join(",") || "-"}`);
1869
+ if (probe.delta_top_burden) p.log.message(` burden delta: recap_share=${probe.delta_top_burden.recap_share.toFixed(2)} unmapped_share=${probe.delta_top_burden.unmapped_share.toFixed(2)} recap_unmapped_share=${probe.delta_top_burden.recap_unmapped_share.toFixed(2)}`);
1870
+ if (probe.delta_top_mapping_counts) p.log.message(` mapping delta: mapped=${probe.delta_top_mapping_counts.mapped} unmapped=${probe.delta_top_mapping_counts.unmapped}`);
1871
+ if (probe.baseline_scenario_score || probe.candidate_scenario_score) p.log.message(` scenario scores: baseline=${probe.baseline_scenario_score?.score ?? "-"} candidate=${probe.candidate_scenario_score?.score ?? "-"}`);
1872
+ if (probe.delta_scenario_score) p.log.message(` scenario delta: mode_match=${probe.delta_scenario_score.mode_match ?? "-"} top1_primary=${probe.delta_scenario_score.primary_in_top1 ?? "-"} top3_primary=${probe.delta_scenario_score.primary_in_top3_count ?? "-"} top1_anti=${probe.delta_scenario_score.anti_signal_in_top1 ?? "-"} primary=${probe.delta_scenario_score.primary_match_count ?? "-"} anti=${probe.delta_scenario_score.anti_signal_count ?? "-"} recap=${probe.delta_scenario_score.recap_count ?? "-"} unmapped_recap=${probe.delta_scenario_score.unmapped_recap_count ?? "-"} chatter=${probe.delta_scenario_score.administrative_chatter_count ?? "-"} net=${probe.delta_scenario_score.score ?? "-"}`);
1873
+ }
1874
+ }
1875
+ p.outro("done");
1876
+ });
1877
+ return cmd;
1878
+ }
1879
+ function createMemoryExtractionReportCommand() {
1880
+ const cmd = new Command("extraction-report").configureHelp(helpStyle).description("Score extracted memories for a session against a built-in extraction eval rubric").option("--session-id <id>", "session ID to evaluate").option("--batch-id <id>", "raw-event flush batch ID to evaluate").requiredOption("--scenario <id>", "built-in extraction eval scenario ID").option("--inactive", "include inactive memories");
1881
+ addDbOption(cmd);
1882
+ addJsonOption(cmd);
1883
+ cmd.action((opts) => {
1884
+ const sessionIdInput = opts.sessionId?.trim() ?? "";
1885
+ const batchIdInput = opts.batchId?.trim() ?? "";
1886
+ const hasSessionId = sessionIdInput.length > 0;
1887
+ const hasBatchId = batchIdInput.length > 0;
1888
+ if (hasSessionId === hasBatchId) throw new Error("Provide exactly one of --session-id or --batch-id");
1889
+ const sessionId = hasSessionId ? parseStrictPositiveId(sessionIdInput) : null;
1890
+ if (hasSessionId && sessionId === null) throw new Error(`Invalid session ID: ${sessionIdInput || opts.sessionId}`);
1891
+ const batchId = hasBatchId ? parseStrictPositiveId(batchIdInput) : null;
1892
+ if (hasBatchId && batchId === null) throw new Error(`Invalid batch ID: ${batchIdInput || opts.batchId}`);
1893
+ const scenarioId = opts.scenario?.trim() ?? "";
1894
+ const scenario = getSessionExtractionEvalScenario(scenarioId);
1895
+ if (!scenario) throw new Error(`Unknown extraction eval scenario: ${scenarioId || opts.scenario}`);
1896
+ const result = batchId != null ? getSessionExtractionEval(resolveDbOpt(opts), {
1897
+ batchId,
1898
+ scenarioId: scenario.id,
1899
+ includeInactive: opts.inactive === true
1900
+ }) : getSessionExtractionEval(resolveDbOpt(opts), {
1901
+ sessionId,
1902
+ scenarioId: scenario.id,
1903
+ includeInactive: opts.inactive === true
1904
+ });
1905
+ if (opts.json) {
1906
+ console.log(JSON.stringify(result, null, 2));
1907
+ return;
1908
+ }
1909
+ p.intro("codemem memory extraction-report");
1910
+ p.log.info([
1911
+ `Scenario: ${result.scenario.id} — ${result.scenario.title}`,
1912
+ `Target: ${result.target.type}${result.target.batchId != null ? ` #${result.target.batchId}` : ""}`,
1913
+ `Session: ${result.session.id} (${result.session.project ?? "no-project"})`,
1914
+ `Session class: ${result.session.sessionClass}`,
1915
+ `Summary disposition: ${result.session.summaryDisposition}`
1916
+ ].join("\n"));
1917
+ p.log.info([
1918
+ `Pass: ${result.pass ? "yes" : "no"}`,
1919
+ `Summary count: ${result.counts.summaries}`,
1920
+ `Observation count: ${result.counts.observations}`,
1921
+ `Summary thread coverage: ${result.coverage.summaryThreadCoverage}`,
1922
+ `Observation thread coverage: ${result.coverage.observationThreadCoverage}`,
1923
+ `Total thread coverage: ${result.coverage.totalThreadCoverage}`,
1924
+ `Duplicate observation threads: ${result.coverage.duplicateObservationThreads}`
1925
+ ].join("\n"));
1926
+ if (result.failureReasons.length > 0) {
1927
+ p.log.warn("Failure reasons:");
1928
+ for (const reason of result.failureReasons) p.log.message(` - ${reason}`);
1929
+ }
1930
+ p.log.info("Thread coverage:");
1931
+ for (const thread of result.threads) p.log.message(` ${thread.id.padEnd(22)} summary=${thread.summaryMatch ? "yes" : "no"} observations=${thread.observationMatch ? "yes" : "no"}`);
1932
+ p.outro("done");
1933
+ });
1934
+ return cmd;
1935
+ }
1936
+ function createMemoryExtractionReplayCommand() {
1937
+ const cmd = new Command("extraction-replay").configureHelp(helpStyle).description("Re-run the observer on a historical flush batch without persisting, then score the fresh output").requiredOption("--batch-id <id>", "raw-event flush batch ID to replay").option("--transcript-budget <chars>", "override replay transcript budget in characters (replay only)").option("--observer-tier-routing", "use replay-only benchmark-backed observer tier routing").option("--observer-temperature <value>", "override observer temperature for replay only").option("--openai-responses", "use OpenAI Responses API for replay only").option("--reasoning-effort <level>", "set OpenAI reasoning.effort for replay only (responses path)").option("--reasoning-summary <mode>", "set OpenAI reasoning.summary for replay only (responses path)").option("--max-output-tokens <n>", "override OpenAI max_output_tokens for replay only (responses path)").requiredOption("--scenario <id>", "built-in extraction eval scenario ID");
1938
+ addDbOption(cmd);
1939
+ addJsonOption(cmd);
1940
+ cmd.action(async (opts) => {
1941
+ const batchIdInput = opts.batchId?.trim() ?? "";
1942
+ const batchId = parseStrictPositiveId(batchIdInput);
1943
+ if (batchId === null) throw new Error(`Invalid batch ID: ${batchIdInput || opts.batchId}`);
1944
+ const scenarioId = opts.scenario?.trim() ?? "";
1945
+ const scenario = getSessionExtractionEvalScenario(scenarioId);
1946
+ if (!scenario) throw new Error(`Unknown extraction eval scenario: ${scenarioId || opts.scenario}`);
1947
+ const transcriptBudgetInput = opts.transcriptBudget?.trim() ?? "";
1948
+ const transcriptBudget = transcriptBudgetInput.length > 0 ? parseStrictPositiveId(transcriptBudgetInput) : null;
1949
+ if (transcriptBudgetInput.length > 0 && transcriptBudget === null) throw new Error(`Invalid transcript budget: ${transcriptBudgetInput || opts.transcriptBudget}`);
1950
+ const observerTemperatureInput = opts.observerTemperature?.trim() ?? "";
1951
+ let observerTemperature;
1952
+ if (observerTemperatureInput.length > 0) {
1953
+ const parsed = Number(observerTemperatureInput);
1954
+ if (!Number.isFinite(parsed)) throw new Error(`Invalid observer temperature: ${observerTemperatureInput || opts.observerTemperature}`);
1955
+ observerTemperature = parsed;
1956
+ }
1957
+ const maxOutputTokensInput = opts.maxOutputTokens?.trim() ?? "";
1958
+ const maxOutputTokens = maxOutputTokensInput.length > 0 ? parseStrictPositiveId(maxOutputTokensInput) : null;
1959
+ if (maxOutputTokensInput.length > 0 && maxOutputTokens === null) throw new Error(`Invalid max output tokens: ${maxOutputTokensInput || opts.maxOutputTokens}`);
1960
+ const observerConfig = loadObserverConfig();
1961
+ const observerConfigWithOverrides = {
1962
+ ...observerConfig,
1963
+ observerTemperature: observerTemperature ?? observerConfig.observerTemperature,
1964
+ observerOpenAIUseResponses: opts.openaiResponses === true,
1965
+ observerReasoningEffort: opts.reasoningEffort?.trim() || null,
1966
+ observerReasoningSummary: opts.reasoningSummary?.trim() || null,
1967
+ observerMaxOutputTokens: maxOutputTokens ?? observerConfig.observerMaxTokens
1968
+ };
1969
+ const observer = new ObserverClient(observerConfigWithOverrides);
1970
+ const result = opts.observerTierRouting === true ? await replayBatchExtractionWithTierRouting(resolveDbOpt(opts), observerConfigWithOverrides, {
1971
+ batchId,
1972
+ scenarioId: scenario.id,
1973
+ transcriptBudget: transcriptBudget ?? void 0
1974
+ }) : await replayBatchExtraction(resolveDbOpt(opts), observer, {
1975
+ batchId,
1976
+ scenarioId: scenario.id,
1977
+ transcriptBudget: transcriptBudget ?? void 0
1978
+ });
1979
+ if (opts.json) {
1980
+ console.log(JSON.stringify(result, null, 2));
1981
+ return;
1982
+ }
1983
+ p.intro("codemem memory extraction-replay");
1984
+ p.log.info([
1985
+ `Scenario: ${result.scenario.id} — ${result.scenario.title}`,
1986
+ `Batch: ${result.target.batchId}`,
1987
+ `Session: ${result.target.sessionId}`,
1988
+ `Observer: ${result.observer.provider}/${result.observer.model}`,
1989
+ `Tier: ${result.observer.tier ?? "manual"}`,
1990
+ `OpenAI Responses: ${result.observer.openaiUseResponses ? "yes" : "no"}`,
1991
+ `Reasoning effort: ${result.observer.reasoningEffort ?? "none"}`,
1992
+ `Classification: ${result.classification.status}`,
1993
+ `Pass: ${result.evaluation.pass ? "yes" : "no"}`
1994
+ ].join("\n"));
1995
+ if (result.classification.reason) p.log.message(`Classification reason: ${result.classification.reason}`);
1996
+ if (result.evaluation.failureReasons.length > 0) {
1997
+ p.log.warn("Failure reasons:");
1998
+ for (const reason of result.evaluation.failureReasons) p.log.message(` - ${reason}`);
1999
+ }
2000
+ p.log.info([
2001
+ `Fresh summaries: ${result.evaluation.counts.summaries}`,
2002
+ `Fresh observations: ${result.evaluation.counts.observations}`,
2003
+ `Summary thread coverage: ${result.evaluation.coverage.summaryThreadCoverage}`,
2004
+ `Observation thread coverage: ${result.evaluation.coverage.observationThreadCoverage}`,
2005
+ `Total thread coverage: ${result.evaluation.coverage.totalThreadCoverage}`
2006
+ ].join("\n"));
2007
+ p.outro("done");
2008
+ });
2009
+ return cmd;
2010
+ }
2011
+ function createMemoryExtractionBenchmarkCommand() {
2012
+ const cmd = new Command("extraction-benchmark").configureHelp(helpStyle).description("Run the formal extraction replay benchmark set and print a cost/quality scoreboard").requiredOption("--benchmark <id>", "benchmark profile id").option("--observer-provider <provider>", "override observer provider for this benchmark run").option("--observer-model <model>", "override observer model for this benchmark run").option("--observer-tier-routing", "use replay-only benchmark-backed observer tier routing").option("--openai-responses", "use OpenAI Responses API for this benchmark run").option("--reasoning-effort <level>", "set OpenAI reasoning.effort for this benchmark run (responses path)").option("--reasoning-summary <mode>", "set OpenAI reasoning.summary for this benchmark run (responses path)").option("--max-output-tokens <n>", "override OpenAI max_output_tokens for this benchmark run (responses path)").option("--observer-temperature <value>", "override observer temperature for this benchmark run").option("--transcript-budget <chars>", "override replay transcript budget in characters for this benchmark run");
2013
+ addDbOption(cmd);
2014
+ addJsonOption(cmd);
2015
+ cmd.action(async (opts) => {
2016
+ const benchmarkId = opts.benchmark?.trim() ?? "";
2017
+ const benchmark = getExtractionBenchmarkProfile(benchmarkId);
2018
+ if (!benchmark) throw new Error(`Unknown extraction benchmark: ${benchmarkId || opts.benchmark}`);
2019
+ const transcriptBudgetInput = opts.transcriptBudget?.trim() ?? "";
2020
+ const transcriptBudget = transcriptBudgetInput.length > 0 ? parseStrictPositiveId(transcriptBudgetInput) : null;
2021
+ if (transcriptBudgetInput.length > 0 && transcriptBudget === null) throw new Error(`Invalid transcript budget: ${transcriptBudgetInput || opts.transcriptBudget}`);
2022
+ const observerTemperatureInput = opts.observerTemperature?.trim() ?? "";
2023
+ let observerTemperature;
2024
+ if (observerTemperatureInput.length > 0) {
2025
+ const parsed = Number(observerTemperatureInput);
2026
+ if (!Number.isFinite(parsed)) throw new Error(`Invalid observer temperature: ${observerTemperatureInput || opts.observerTemperature}`);
2027
+ observerTemperature = parsed;
2028
+ }
2029
+ const maxOutputTokensInput = opts.maxOutputTokens?.trim() ?? "";
2030
+ const maxOutputTokens = maxOutputTokensInput.length > 0 ? parseStrictPositiveId(maxOutputTokensInput) : null;
2031
+ if (maxOutputTokensInput.length > 0 && maxOutputTokens === null) throw new Error(`Invalid max output tokens: ${maxOutputTokensInput || opts.maxOutputTokens}`);
2032
+ const observerConfig = loadObserverConfig();
2033
+ const observerConfigWithOverrides = {
2034
+ ...observerConfig,
2035
+ observerProvider: opts.observerProvider?.trim() || observerConfig.observerProvider,
2036
+ observerModel: opts.observerModel?.trim() || observerConfig.observerModel,
2037
+ observerTemperature: observerTemperature ?? observerConfig.observerTemperature,
2038
+ observerOpenAIUseResponses: opts.openaiResponses === true,
2039
+ observerReasoningEffort: opts.reasoningEffort?.trim() || null,
2040
+ observerReasoningSummary: opts.reasoningSummary?.trim() || null,
2041
+ observerMaxOutputTokens: maxOutputTokens ?? observerConfig.observerMaxTokens
2042
+ };
2043
+ const observer = new ObserverClient(observerConfigWithOverrides);
2044
+ const runs = [];
2045
+ for (const batch of benchmark.batches) {
2046
+ const scenarioId = batch.scenarioId ?? benchmark.scenarioId;
2047
+ const result = opts.observerTierRouting === true ? await replayBatchExtractionWithTierRouting(resolveDbOpt(opts), observerConfigWithOverrides, {
2048
+ batchId: batch.batchId,
2049
+ scenarioId,
2050
+ transcriptBudget: transcriptBudget ?? void 0
2051
+ }) : await replayBatchExtraction(resolveDbOpt(opts), observer, {
2052
+ batchId: batch.batchId,
2053
+ scenarioId,
2054
+ transcriptBudget: transcriptBudget ?? void 0
2055
+ });
2056
+ runs.push({
2057
+ batchId: batch.batchId,
2058
+ sessionId: batch.sessionId,
2059
+ label: batch.label,
2060
+ purpose: batch.purpose,
2061
+ complexity: batch.complexity,
2062
+ scenarioId,
2063
+ expectedTier: batch.expectedTier ?? null,
2064
+ analysis: {
2065
+ eventSpan: result.analysis.eventSpan,
2066
+ promptCount: result.analysis.promptCount,
2067
+ toolCount: result.analysis.toolCount,
2068
+ transcriptLength: result.analysis.transcriptLength
2069
+ },
2070
+ status: result.classification.status,
2071
+ reason: result.classification.reason,
2072
+ tier: result.observer.tier ?? "manual",
2073
+ provider: result.observer.provider,
2074
+ model: result.observer.model,
2075
+ openaiUseResponses: result.observer.openaiUseResponses,
2076
+ reasoningEffort: result.observer.reasoningEffort,
2077
+ reasoningSummary: result.observer.reasoningSummary,
2078
+ maxOutputTokens: result.observer.maxOutputTokens,
2079
+ temperature: result.observer.temperature,
2080
+ summaries: result.evaluation.counts.summaries,
2081
+ observations: result.evaluation.counts.observations,
2082
+ repairApplied: result.observer.repairApplied
2083
+ });
2084
+ }
2085
+ const summary = {
2086
+ total: runs.length,
2087
+ shapeQualityTotal: runs.filter((run) => run.purpose === "shape_quality").length,
2088
+ shapeQualityPasses: runs.filter((run) => run.purpose === "shape_quality" && run.status === "pass").length,
2089
+ shapeQualityFails: runs.filter((run) => run.purpose === "shape_quality" && run.status === "shape_fail").length,
2090
+ expectedTierTotal: runs.filter((run) => run.expectedTier != null).length,
2091
+ expectedTierMatches: runs.filter((run) => run.expectedTier != null && run.expectedTier === run.tier).length,
2092
+ robustnessNoOutput: runs.filter((run) => run.status === "observer_no_output").length
2093
+ };
2094
+ const uniqueObserverKeys = Array.from(new Set(runs.map((run) => `${run.provider}::${run.model}::${run.openaiUseResponses ? "responses" : "chat"}`)));
2095
+ const observerSummary = opts.observerTierRouting === true ? {
2096
+ provider: uniqueObserverKeys.length === 1 ? runs[0]?.provider ?? observer.provider : "mixed",
2097
+ model: uniqueObserverKeys.length === 1 ? runs[0]?.model ?? observer.model : "mixed",
2098
+ tierRouting: true,
2099
+ openaiUseResponses: uniqueObserverKeys.length === 1 ? runs[0]?.openaiUseResponses ?? observer.openaiUseResponses : null,
2100
+ reasoningEffort: uniqueObserverKeys.length === 1 ? runs[0]?.reasoningEffort ?? observer.reasoningEffort : "mixed",
2101
+ reasoningSummary: uniqueObserverKeys.length === 1 ? runs[0]?.reasoningSummary ?? observer.reasoningSummary : "mixed",
2102
+ maxOutputTokens: uniqueObserverKeys.length === 1 ? runs[0]?.maxOutputTokens ?? observer.maxOutputTokens : null,
2103
+ temperature: uniqueObserverKeys.length === 1 ? runs[0]?.temperature ?? observer.temperature : null,
2104
+ transcriptBudget: transcriptBudget ?? null,
2105
+ selectedObservers: uniqueObserverKeys
2106
+ } : {
2107
+ provider: observer.provider,
2108
+ model: observer.model,
2109
+ tierRouting: false,
2110
+ openaiUseResponses: observer.openaiUseResponses,
2111
+ reasoningEffort: observer.reasoningEffort,
2112
+ reasoningSummary: observer.reasoningSummary,
2113
+ maxOutputTokens: observer.maxOutputTokens,
2114
+ temperature: observer.temperature,
2115
+ transcriptBudget: transcriptBudget ?? null,
2116
+ selectedObservers: uniqueObserverKeys
2117
+ };
2118
+ const output = {
2119
+ benchmark: {
2120
+ id: benchmark.id,
2121
+ title: benchmark.title,
2122
+ scenarioId: benchmark.scenarioId
2123
+ },
2124
+ observer: observerSummary,
2125
+ summary,
2126
+ runs
2127
+ };
2128
+ if (opts.json) {
2129
+ console.log(JSON.stringify(output, null, 2));
2130
+ return;
2131
+ }
2132
+ p.intro("codemem memory extraction-benchmark");
2133
+ p.log.info([
2134
+ `Benchmark: ${benchmark.id} — ${benchmark.title}`,
2135
+ `Observer: ${observerSummary.provider}/${observerSummary.model}`,
2136
+ `Tier routing: ${opts.observerTierRouting === true ? "yes" : "no"}`,
2137
+ `OpenAI Responses: ${observerSummary.openaiUseResponses === null ? "mixed" : observerSummary.openaiUseResponses ? "yes" : "no"}`,
2138
+ `Reasoning effort: ${observerSummary.reasoningEffort ?? "none"}`,
2139
+ `Reasoning summary: ${observerSummary.reasoningSummary ?? "none"}`,
2140
+ `Max output tokens: ${observerSummary.maxOutputTokens ?? "mixed"}`,
2141
+ `Temperature: ${observerSummary.temperature ?? "mixed"}`,
2142
+ `Transcript budget override: ${transcriptBudget ?? "default"}`,
2143
+ `Shape-quality passes: ${summary.shapeQualityPasses}/${summary.shapeQualityTotal}`,
2144
+ `Shape-quality fails: ${summary.shapeQualityFails}`,
2145
+ `Expected-tier matches: ${summary.expectedTierMatches}/${summary.expectedTierTotal}`,
2146
+ `Observer no-output cases: ${summary.robustnessNoOutput}`
2147
+ ].join("\n"));
2148
+ for (const run of runs) p.log.message(` [${run.batchId}] ${run.status.padEnd(18)} ${run.complexity.padEnd(10)} tier=${run.tier.padEnd(6)} expected=${(run.expectedTier ?? "n/a").padEnd(6)} span=${String(run.analysis.eventSpan).padEnd(3)} prompts=${run.analysis.promptCount} tools=${String(run.analysis.toolCount).padEnd(2)} transcript=${run.analysis.transcriptLength} ${run.provider}/${run.model}${run.openaiUseResponses ? " [responses]" : ""} summaries=${run.summaries} observations=${run.observations} repair=${run.repairApplied ? "yes" : "no"} — ${run.label}`);
2149
+ p.outro("done");
2150
+ });
2151
+ return cmd;
764
2152
  }
765
- function createInjectMemoryCommand() {
766
- return new Command("inject").configureHelp(helpStyle).description("Build raw memory context text for manual prompt injection").argument("<context>", "context string to search for").option("--db <path>", "database path").option("--db-path <path>", "database path").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$1, []).option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "search across all projects").allowUnknownOption(true).allowExcessArguments(true).action(async (context, opts) => {
767
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
768
- try {
769
- const limit = Number.parseInt(opts.limit ?? "10", 10) || 10;
770
- const budgetRaw = opts.tokenBudget ?? opts.budget;
771
- const budget = budgetRaw ? Number.parseInt(budgetRaw, 10) : void 0;
772
- const filters = {};
773
- if (!opts.allProjects) {
774
- const project = process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), opts.project ?? null);
775
- if (project) filters.project = project;
776
- }
777
- if ((opts.workingSetFile?.length ?? 0) > 0) filters.working_set_paths = opts.workingSetFile;
778
- const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
779
- console.log(pack.pack_text ?? "");
780
- } finally {
781
- store.close();
2153
+ function createMemoryRelinkReportCommand() {
2154
+ const cmd = new Command("relink-report").configureHelp(helpStyle).description("Analyze dry-run raw-event session relinking and compaction opportunities").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--limit <n>", "max groups to print", "25");
2155
+ addDbOption(cmd);
2156
+ addJsonOption(cmd);
2157
+ cmd.action((opts) => {
2158
+ const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
2159
+ const limit = Number.parseInt(opts.limit ?? "25", 10) || 25;
2160
+ const result = getRawEventRelinkReport(resolveDbOpt(opts), {
2161
+ project,
2162
+ allProjects: opts.allProjects === true,
2163
+ limit
2164
+ });
2165
+ if (opts.json) {
2166
+ console.log(JSON.stringify(result, null, 2));
2167
+ return;
782
2168
  }
2169
+ p.intro("codemem memory relink-report");
2170
+ p.log.info([
2171
+ `Recoverable sessions: ${result.totals.recoverable_sessions}`,
2172
+ `Distinct stable ids: ${result.totals.distinct_stable_ids}`,
2173
+ `Groups with multiple sessions: ${result.totals.groups_with_multiple_sessions}`,
2174
+ `Groups with mapped session: ${result.totals.groups_with_mapped_session}`,
2175
+ `Groups without mapped session: ${result.totals.groups_without_mapped_session}`,
2176
+ `Active memories in groups: ${result.totals.active_memories}`,
2177
+ `Repointable active memories: ${result.totals.repointable_active_memories}`
2178
+ ].join("\n"));
2179
+ p.log.info("Top relink groups:");
2180
+ for (const group of result.groups) p.log.message(` ${group.stable_id} -> canonical ${group.canonical_session_id} | local=${group.local_sessions} mapped=${group.mapped_sessions} unmapped=${group.unmapped_sessions} active=${group.active_memories} repointable=${group.repointable_active_memories}`);
2181
+ p.outro("done");
783
2182
  });
2183
+ return cmd;
784
2184
  }
785
- function createCompactMemoryCommand() {
786
- return new Command("compact").configureHelp(helpStyle).description("Deferred command guidance for memory compaction").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--session-id <id>", "session ID").option("--limit <n>", "max sessions to compact", "10").allowUnknownOption(true).allowExcessArguments(true).action(() => {
787
- p.log.warn("`codemem memory compact` is not implemented in the TypeScript CLI yet.");
788
- p.log.info("Current workaround: rely on automatic ingestion; for manual context use `codemem memory inject <context>`.");
789
- process.exitCode = 2;
2185
+ function createMemoryRelinkPlanCommand() {
2186
+ const cmd = new Command("relink-plan").configureHelp(helpStyle).description("Emit dry-run raw-event relink remediation actions").option("--project <project>", "project identifier (defaults to git repo root)").option("--all-projects", "analyze across all projects").option("--limit <n>", "max groups to include", "25");
2187
+ addDbOption(cmd);
2188
+ addJsonOption(cmd);
2189
+ cmd.action((opts) => {
2190
+ const project = opts.allProjects === true ? null : opts.project?.trim() || process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), null);
2191
+ const limit = Number.parseInt(opts.limit ?? "25", 10) || 25;
2192
+ const result = getRawEventRelinkPlan(resolveDbOpt(opts), {
2193
+ project,
2194
+ allProjects: opts.allProjects === true,
2195
+ limit
2196
+ });
2197
+ if (opts.json) {
2198
+ console.log(JSON.stringify(result, null, 2));
2199
+ return;
2200
+ }
2201
+ p.intro("codemem memory relink-plan");
2202
+ p.log.info([
2203
+ `Groups: ${result.totals.groups}`,
2204
+ `Eligible groups: ${result.totals.eligible_groups}`,
2205
+ `Skipped groups: ${result.totals.skipped_groups}`,
2206
+ `Actions: ${result.totals.actions}`,
2207
+ `Bridge creations: ${result.totals.bridge_creations}`,
2208
+ `Memory repoints: ${result.totals.memory_repoints}`,
2209
+ `Session compactions: ${result.totals.session_compactions}`
2210
+ ].join("\n"));
2211
+ p.log.info("Planned actions:");
2212
+ for (const action of result.actions.slice(0, 15)) p.log.message(` ${action.action} ${action.stable_id} -> canonical ${action.canonical_session_id} | sessions=${action.session_ids.join(",") || "-"} memories=${action.memory_count} reason=${action.reason}`);
2213
+ if (result.skipped_groups.length > 0) {
2214
+ p.log.info("Skipped groups:");
2215
+ for (const group of result.skipped_groups.slice(0, 10)) p.log.message(` ${group.stable_id} | blockers=${group.blockers.join(",")}`);
2216
+ }
2217
+ p.outro("done");
790
2218
  });
2219
+ return cmd;
791
2220
  }
792
2221
  var showMemoryCommand = createShowMemoryCommand();
793
2222
  var forgetMemoryCommand = createForgetMemoryCommand();
@@ -797,24 +2226,22 @@ memoryCommand.addCommand(createShowMemoryCommand());
797
2226
  memoryCommand.addCommand(createForgetMemoryCommand());
798
2227
  memoryCommand.addCommand(createRememberMemoryCommand());
799
2228
  memoryCommand.addCommand(createInjectMemoryCommand());
800
- memoryCommand.addCommand(createCompactMemoryCommand());
2229
+ memoryCommand.addCommand(createMemoryRoleReportCommand());
2230
+ memoryCommand.addCommand(createMemoryRoleCompareCommand());
2231
+ memoryCommand.addCommand(createMemoryExtractionReportCommand());
2232
+ memoryCommand.addCommand(createMemoryExtractionReplayCommand());
2233
+ memoryCommand.addCommand(createMemoryExtractionBenchmarkCommand());
2234
+ memoryCommand.addCommand(createMemoryRelinkReportCommand());
2235
+ memoryCommand.addCommand(createMemoryRelinkPlanCommand());
801
2236
  //#endregion
802
2237
  //#region src/commands/pack.ts
803
- function collectWorkingSetFile(value, previous) {
804
- return [...previous, value];
805
- }
806
- 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(async (context, opts) => {
807
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
2238
+ var packCmd = new Command("pack").configureHelp(helpStyle).description("Build a context-aware memory pack").argument("<context>", "context string to search for").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");
2239
+ addDbOption(packCmd);
2240
+ addJsonOption(packCmd);
2241
+ var packCommand = packCmd.action(async (context, opts) => {
2242
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
808
2243
  try {
809
- const limit = Number.parseInt(opts.limit, 10) || 10;
810
- const budgetRaw = opts.tokenBudget ?? opts.budget;
811
- const budget = budgetRaw ? Number.parseInt(budgetRaw, 10) : void 0;
812
- const filters = {};
813
- if (!opts.allProjects) {
814
- const project = process.env.CODEMEM_PROJECT?.trim() || resolveProject(process.cwd(), opts.project ?? null);
815
- if (project) filters.project = project;
816
- }
817
- if ((opts.workingSetFile?.length ?? 0) > 0) filters.working_set_paths = opts.workingSetFile;
2244
+ const { limit, budget, filters } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
818
2245
  const result = await store.buildMemoryPackAsync(context, limit, budget, filters);
819
2246
  if (opts.json) {
820
2247
  console.log(JSON.stringify(result, null, 2));
@@ -837,8 +2264,11 @@ var packCommand = new Command("pack").configureHelp(helpStyle).description("Buil
837
2264
  });
838
2265
  //#endregion
839
2266
  //#region src/commands/recent.ts
840
- 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) => {
841
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
2267
+ var cmd = new Command("recent").configureHelp(helpStyle).description("Show recent memories").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");
2268
+ addDbOption(cmd);
2269
+ addJsonOption(cmd);
2270
+ cmd.action((opts) => {
2271
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
842
2272
  try {
843
2273
  const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
844
2274
  const filters = {};
@@ -848,15 +2278,20 @@ var recentCommand = new Command("recent").configureHelp(helpStyle).description("
848
2278
  if (project) filters.project = project;
849
2279
  }
850
2280
  const items = store.recent(limit, filters);
851
- for (const item of items) console.log(`#${item.id} [${item.kind}] ${item.title}`);
2281
+ if (opts.json) console.log(JSON.stringify(items));
2282
+ else for (const item of items) console.log(`#${item.id} [${item.kind}] ${item.title}`);
852
2283
  } finally {
853
2284
  store.close();
854
2285
  }
855
2286
  });
2287
+ var recentCommand = cmd;
856
2288
  //#endregion
857
2289
  //#region src/commands/search.ts
858
- 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) => {
859
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
2290
+ var searchCmd = new Command("search").configureHelp(helpStyle).description("Search memories by query").argument("<query>", "search query").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");
2291
+ addDbOption(searchCmd);
2292
+ addJsonOption(searchCmd);
2293
+ var searchCommand = searchCmd.action((query, opts) => {
2294
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
860
2295
  try {
861
2296
  const limit = Math.max(1, Number.parseInt(opts.limit, 10) || 5);
862
2297
  const filters = {};
@@ -900,6 +2335,10 @@ function timeSince(isoDate) {
900
2335
  }
901
2336
  //#endregion
902
2337
  //#region src/commands/serve-invocation.ts
2338
+ /**
2339
+ * Parse and validate a port string. Throws a user-friendly message (no stack trace)
2340
+ * that callers in serve.ts catch at the action boundary.
2341
+ */
903
2342
  function parsePort(rawPort) {
904
2343
  const port = Number.parseInt(rawPort, 10);
905
2344
  if (!Number.isFinite(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${rawPort}`);
@@ -908,10 +2347,11 @@ function parsePort(rawPort) {
908
2347
  function resolveLegacyServeInvocation(opts) {
909
2348
  if (opts.stop && opts.restart) throw new Error("Use only one of --stop or --restart");
910
2349
  if (opts.foreground && opts.background) throw new Error("Use only one of --background or --foreground");
911
- const dbPath = opts.db ?? opts.dbPath ?? null;
2350
+ const dbPath = resolveDbOpt(opts) ?? null;
912
2351
  return {
913
2352
  mode: opts.stop ? "stop" : opts.restart ? "restart" : "start",
914
2353
  dbPath,
2354
+ configPath: opts.config ?? null,
915
2355
  host: opts.host,
916
2356
  port: parsePort(opts.port),
917
2357
  background: opts.restart ? true : opts.background ? true : false
@@ -926,7 +2366,8 @@ function resolveServeInvocation(action, opts) {
926
2366
  function resolveStartServeInvocation(opts) {
927
2367
  return {
928
2368
  mode: "start",
929
- dbPath: opts.db ?? opts.dbPath ?? null,
2369
+ dbPath: resolveDbOpt(opts) ?? null,
2370
+ configPath: opts.config ?? null,
930
2371
  host: opts.host,
931
2372
  port: parsePort(opts.port),
932
2373
  background: !opts.foreground
@@ -935,7 +2376,8 @@ function resolveStartServeInvocation(opts) {
935
2376
  function resolveStopRestartInvocation(mode, opts) {
936
2377
  return {
937
2378
  mode,
938
- dbPath: opts.db ?? opts.dbPath ?? null,
2379
+ dbPath: resolveDbOpt(opts) ?? null,
2380
+ configPath: opts.config ?? null,
939
2381
  host: opts.host,
940
2382
  port: parsePort(opts.port),
941
2383
  background: mode === "restart"
@@ -1182,14 +2624,19 @@ async function startBackgroundViewer(invocation) {
1182
2624
  return;
1183
2625
  }
1184
2626
  const scriptPath = process.argv[1];
1185
- if (!scriptPath) throw new Error("Unable to resolve CLI entrypoint for background launch");
2627
+ if (!scriptPath) {
2628
+ p.log.error("Unable to resolve CLI entrypoint for background launch");
2629
+ process.exitCode = 1;
2630
+ return;
2631
+ }
1186
2632
  const child = spawn(process.execPath, buildForegroundRunnerArgs(scriptPath, invocation), {
1187
2633
  cwd: process.cwd(),
1188
2634
  detached: true,
1189
2635
  stdio: "ignore",
1190
2636
  env: {
1191
2637
  ...process.env,
1192
- ...invocation.dbPath ? { CODEMEM_DB: invocation.dbPath } : {}
2638
+ ...invocation.dbPath ? { CODEMEM_DB: invocation.dbPath } : {},
2639
+ ...invocation.configPath ? { CODEMEM_CONFIG: invocation.configPath } : {}
1193
2640
  }
1194
2641
  });
1195
2642
  child.unref();
@@ -1205,6 +2652,7 @@ async function startForegroundViewer(invocation) {
1205
2652
  const { createApp, createSyncApp, closeStore, getStore } = await import("@codemem/server");
1206
2653
  const { serve } = await import("@hono/node-server");
1207
2654
  if (invocation.dbPath) process.env.CODEMEM_DB = invocation.dbPath;
2655
+ if (invocation.configPath) process.env.CODEMEM_CONFIG = invocation.configPath;
1208
2656
  warnIfViewerExposed(invocation.host, invocation.port);
1209
2657
  if (await isPortOpen(invocation.host, invocation.port)) {
1210
2658
  p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
@@ -1229,7 +2677,7 @@ async function startForegroundViewer(invocation) {
1229
2677
  sweeper.start();
1230
2678
  const syncAbort = new AbortController();
1231
2679
  const retentionAbort = new AbortController();
1232
- const syncConfig = readCoordinatorSyncConfig(readCodememConfigFile());
2680
+ const syncConfig = readCoordinatorSyncConfig(invocation.configPath ? readCodememConfigFileAtPath(invocation.configPath) : readCodememConfigFile());
1233
2681
  const syncEnabled = syncConfig.syncEnabled;
1234
2682
  const retentionRunner = new SyncRetentionRunner({
1235
2683
  dbPath: resolveDbPath(invocation.dbPath ?? void 0),
@@ -1408,17 +2856,31 @@ async function runServeInvocation(invocation) {
1408
2856
  });
1409
2857
  }
1410
2858
  }
1411
- function addSharedServeOptions(command) {
1412
- 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");
1413
- }
1414
- 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) => {
1415
- const normalizedAction = action === void 0 ? void 0 : action === "start" || action === "stop" || action === "restart" ? action : null;
1416
- if (normalizedAction === null) {
1417
- p.log.error(`Unknown serve action: ${action}`);
2859
+ var serveCmd = new Command("serve").configureHelp(helpStyle).description("Run or manage the viewer").argument("[action]", "lifecycle action (start|stop|restart)");
2860
+ addDbOption(serveCmd);
2861
+ addConfigOption(serveCmd);
2862
+ addViewerHostOptions(serveCmd);
2863
+ serveCmd.addOption(new Option("--background", "run viewer in background").hideHelp());
2864
+ serveCmd.addOption(new Option("--foreground", "run viewer in foreground").hideHelp());
2865
+ serveCmd.addOption(new Option("--stop", "stop background viewer").hideHelp());
2866
+ serveCmd.addOption(new Option("--restart", "restart background viewer").hideHelp());
2867
+ var serveCommand = serveCmd.action(async (action, opts) => {
2868
+ try {
2869
+ if (opts.stop) emitDeprecationWarning("--stop", "codemem serve stop");
2870
+ if (opts.restart) emitDeprecationWarning("--restart", "codemem serve restart");
2871
+ if (opts.background) emitDeprecationWarning("--background", "codemem serve start");
2872
+ if (opts.foreground) emitDeprecationWarning("--foreground", "codemem serve start --foreground");
2873
+ const normalizedAction = action === void 0 ? void 0 : action === "start" || action === "stop" || action === "restart" ? action : null;
2874
+ if (normalizedAction === null) {
2875
+ p.log.error(`Unknown serve action: ${action}`);
2876
+ process.exitCode = 1;
2877
+ return;
2878
+ }
2879
+ await runServeInvocation(resolveServeInvocation(normalizedAction, opts));
2880
+ } catch (err) {
2881
+ p.log.error(err instanceof Error ? err.message : String(err));
1418
2882
  process.exitCode = 1;
1419
- return;
1420
2883
  }
1421
- await runServeInvocation(resolveServeInvocation(normalizedAction, opts));
1422
2884
  });
1423
2885
  //#endregion
1424
2886
  //#region src/commands/setup-config.ts
@@ -1673,8 +3135,11 @@ function fmtTokens(n) {
1673
3135
  if (n >= 1e3) return `~${(n / 1e3).toFixed(0)}K`;
1674
3136
  return `${n}`;
1675
3137
  }
1676
- 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) => {
1677
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
3138
+ var statsCmd = new Command("stats").configureHelp(helpStyle).description("Show database statistics");
3139
+ addDbOption(statsCmd);
3140
+ addJsonOption(statsCmd);
3141
+ var statsCommand = statsCmd.action((opts) => {
3142
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1678
3143
  try {
1679
3144
  const result = store.stats();
1680
3145
  if (opts.json) {
@@ -1728,7 +3193,9 @@ function buildServeLifecycleArgs(action, opts, scriptPath, execArgv = []) {
1728
3193
  if (action === "start") args.push("--restart");
1729
3194
  else if (action === "stop") args.push("--stop");
1730
3195
  else args.push("--restart");
1731
- if (opts.db ?? opts.dbPath) args.push("--db-path", opts.db ?? opts.dbPath ?? "");
3196
+ const dbResolved = resolveDbOpt(opts);
3197
+ if (dbResolved) args.push("--db-path", dbResolved);
3198
+ if (opts.config) args.push("--config", opts.config);
1732
3199
  if (opts.host) args.push("--host", opts.host);
1733
3200
  if (opts.port) args.push("--port", opts.port);
1734
3201
  return args;
@@ -1752,10 +3219,22 @@ function collectAdvertiseAddresses(explicitAddress, configuredHost, port, interf
1752
3219
  /**
1753
3220
  * Sync CLI commands — enable/disable/status/peers/connect.
1754
3221
  */
3222
+ function readCliConfig(configPath) {
3223
+ return configPath ? readCodememConfigFileAtPath(configPath) : readCodememConfigFile();
3224
+ }
3225
+ function writeCliConfig(config, configPath) {
3226
+ return writeCodememConfigFile(config, configPath || void 0);
3227
+ }
1755
3228
  function parseAttemptsLimit(value) {
1756
3229
  if (!/^\d+$/.test(value.trim())) throw new Error(`Invalid --limit: ${value}`);
1757
3230
  return Number.parseInt(value, 10);
1758
3231
  }
3232
+ function parsePositiveIntegerOption(value, flagName) {
3233
+ if (value == null) return void 0;
3234
+ const trimmed = value.trim();
3235
+ if (!/^\d+$/.test(trimmed)) throw new Error(`Invalid ${flagName}: ${value}`);
3236
+ return Number.parseInt(trimmed, 10);
3237
+ }
1759
3238
  function resolvePeerMatch(db, peerRef) {
1760
3239
  const trimmed = peerRef.trim();
1761
3240
  if (!trimmed) return null;
@@ -1771,18 +3250,6 @@ function resolvePeerMatch(db, peerRef) {
1771
3250
  if (byName.length > 1) return "ambiguous";
1772
3251
  return byName[0] ?? null;
1773
3252
  }
1774
- function readCoordinatorPublicKey(opts) {
1775
- const inline = String(opts.publicKey ?? "").trim();
1776
- const filePath = String(opts.publicKeyFile ?? "").trim();
1777
- if (inline && filePath) throw new Error("Use only one of --public-key or --public-key-file");
1778
- if (filePath) {
1779
- const text = readFileSync(filePath, "utf8").trim();
1780
- if (!text) throw new Error(`Public key file is empty: ${filePath}`);
1781
- return text;
1782
- }
1783
- if (!inline) throw new Error("Public key required via --public-key or --public-key-file");
1784
- return inline;
1785
- }
1786
3253
  async function portOpen(host, port) {
1787
3254
  return new Promise((resolve) => {
1788
3255
  const socket = net.createConnection({
@@ -1828,12 +3295,13 @@ function parseStoredAddressEndpoint(value) {
1828
3295
  async function runServeLifecycle(action, opts) {
1829
3296
  if (opts.user === false || opts.system === true) p.log.warn("TS sync lifecycle currently manages the local viewer process, not separate user/system services.");
1830
3297
  if (action === "start") {
1831
- if (readCodememConfigFile().sync_enabled !== true) {
3298
+ if (readCoordinatorSyncConfig(readCliConfig(opts.config)).syncEnabled !== true) {
1832
3299
  p.log.error("Sync is disabled. Run `codemem sync enable` first.");
1833
3300
  process.exitCode = 1;
1834
3301
  return;
1835
3302
  }
1836
3303
  }
3304
+ const dbResolved = resolveDbOpt(opts);
1837
3305
  const args = buildServeLifecycleArgs(action, opts, process.argv[1] ?? "", process.execArgv);
1838
3306
  await new Promise((resolve, reject) => {
1839
3307
  const child = spawn(process.execPath, args, {
@@ -1841,7 +3309,8 @@ async function runServeLifecycle(action, opts) {
1841
3309
  stdio: "inherit",
1842
3310
  env: {
1843
3311
  ...process.env,
1844
- ...opts.db ?? opts.dbPath ? { CODEMEM_DB: opts.db ?? opts.dbPath } : {}
3312
+ ...dbResolved ? { CODEMEM_DB: dbResolved } : {},
3313
+ ...opts.config ? { CODEMEM_CONFIG: opts.config } : {}
1845
3314
  }
1846
3315
  });
1847
3316
  child.once("error", reject);
@@ -1852,8 +3321,11 @@ async function runServeLifecycle(action, opts) {
1852
3321
  });
1853
3322
  }
1854
3323
  var syncCommand = new Command("sync").configureHelp(helpStyle).description("Sync configuration and peer management");
1855
- 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) => {
1856
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
3324
+ var attemptsCmd = new Command("attempts").configureHelp(helpStyle).description("Show recent sync attempts").option("--limit <n>", "max attempts", "10");
3325
+ addDbOption(attemptsCmd);
3326
+ addJsonOption(attemptsCmd);
3327
+ attemptsCmd.action((opts) => {
3328
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1857
3329
  try {
1858
3330
  const d = drizzle(store.db, { schema });
1859
3331
  const limit = parseAttemptsLimit(opts.limit);
@@ -1873,19 +3345,30 @@ syncCommand.addCommand(new Command("attempts").configureHelp(helpStyle).descript
1873
3345
  } finally {
1874
3346
  store.close();
1875
3347
  }
1876
- }));
1877
- 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) => {
1878
- await runServeLifecycle("start", opts);
1879
- }));
1880
- 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) => {
1881
- await runServeLifecycle("stop", opts);
1882
- }));
1883
- 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) => {
1884
- await runServeLifecycle("restart", opts);
1885
- }));
1886
- 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) => {
1887
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
3348
+ });
3349
+ syncCommand.addCommand(attemptsCmd);
3350
+ function addDeprecatedLifecycleCommand(action) {
3351
+ const cmd = new Command(action).configureHelp(helpStyle).description(`[deprecated] ${action} sync daemon — use 'codemem serve ${action}'`);
3352
+ addDbOption(cmd);
3353
+ addConfigOption(cmd);
3354
+ addViewerHostOptions(cmd);
3355
+ addLegacyServiceFlags(cmd);
3356
+ cmd.action(async (opts) => {
3357
+ emitDeprecationWarning(`codemem sync ${action}`, `codemem serve ${action}`);
3358
+ await runServeLifecycle(action, opts);
3359
+ });
3360
+ syncCommand.addCommand(cmd, { hidden: true });
3361
+ }
3362
+ addDeprecatedLifecycleCommand("start");
3363
+ addDeprecatedLifecycleCommand("stop");
3364
+ addDeprecatedLifecycleCommand("restart");
3365
+ var onceCmd = new Command("once").configureHelp(helpStyle).description("Run a single sync pass").option("--peer <peer>", "peer device id or name");
3366
+ addDbOption(onceCmd);
3367
+ addJsonOption(onceCmd);
3368
+ onceCmd.action(async (opts) => {
3369
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1888
3370
  try {
3371
+ const keysDir = process.env.CODEMEM_KEYS_DIR?.trim() || void 0;
1889
3372
  syncPassPreflight(store.db);
1890
3373
  const d = drizzle(store.db, { schema });
1891
3374
  const rows = opts.peer ? (() => {
@@ -1893,6 +3376,10 @@ syncCommand.addCommand(new Command("once").configureHelp(helpStyle).description(
1893
3376
  if (deviceMatches.length > 0) return deviceMatches;
1894
3377
  const nameMatches = d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).where(eq(schema.syncPeers.name, opts.peer)).all();
1895
3378
  if (nameMatches.length > 1) {
3379
+ if (opts.json) {
3380
+ emitJsonError("ambiguous_peer", `Peer name is ambiguous: ${opts.peer}`);
3381
+ return [];
3382
+ }
1896
3383
  p.log.error(`Peer name is ambiguous: ${opts.peer}`);
1897
3384
  process.exitCode = 1;
1898
3385
  return [];
@@ -1900,31 +3387,59 @@ syncCommand.addCommand(new Command("once").configureHelp(helpStyle).description(
1900
3387
  return nameMatches;
1901
3388
  })() : d.select({ peer_device_id: schema.syncPeers.peer_device_id }).from(schema.syncPeers).all();
1902
3389
  if (rows.length === 0) {
3390
+ if (process.exitCode) return;
3391
+ if (opts.json) {
3392
+ emitJsonError("no_peers", "No peers available for sync");
3393
+ return;
3394
+ }
1903
3395
  p.log.warn("No peers available for sync");
1904
3396
  process.exitCode = 1;
1905
3397
  return;
1906
3398
  }
1907
3399
  let hadFailure = false;
3400
+ const results = [];
1908
3401
  for (const row of rows) {
1909
- const result = await runSyncPass(store.db, row.peer_device_id);
3402
+ const result = await runSyncPass(store.db, row.peer_device_id, { keysDir });
1910
3403
  if (!result.ok) hadFailure = true;
1911
- console.log(formatSyncOnceResult(row.peer_device_id, result));
3404
+ results.push({
3405
+ peer_device_id: row.peer_device_id,
3406
+ ok: result.ok,
3407
+ ...result.error ? { error: result.error } : {}
3408
+ });
3409
+ if (!opts.json) console.log(formatSyncOnceResult(row.peer_device_id, result));
1912
3410
  }
3411
+ if (opts.json) console.log(JSON.stringify({
3412
+ ok: !hadFailure,
3413
+ results
3414
+ }, null, 2));
1913
3415
  if (hadFailure) process.exitCode = 1;
1914
3416
  } finally {
1915
3417
  store.close();
1916
3418
  }
1917
- }));
1918
- 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) => {
1919
- const store = new MemoryStore(resolveDbPath(opts.dbPath));
3419
+ });
3420
+ syncCommand.addCommand(onceCmd);
3421
+ var pairCmd = 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");
3422
+ addDbOption(pairCmd);
3423
+ addConfigOption(pairCmd);
3424
+ addJsonOption(pairCmd);
3425
+ pairCmd.action(async (opts) => {
3426
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1920
3427
  try {
1921
3428
  const acceptModeRequested = opts.accept != null || opts.acceptFile != null;
1922
3429
  if (opts.payloadOnly && acceptModeRequested) {
3430
+ if (opts.json) {
3431
+ emitJsonError("usage_error", "--payload-only cannot be combined with --accept or --accept-file", 2);
3432
+ return;
3433
+ }
1923
3434
  p.log.error("--payload-only cannot be combined with --accept or --accept-file");
1924
3435
  process.exitCode = 1;
1925
3436
  return;
1926
3437
  }
1927
3438
  if (opts.accept && opts.acceptFile) {
3439
+ if (opts.json) {
3440
+ emitJsonError("usage_error", "Use only one of --accept or --accept-file", 2);
3441
+ return;
3442
+ }
1928
3443
  p.log.error("Use only one of --accept or --accept-file");
1929
3444
  process.exitCode = 1;
1930
3445
  return;
@@ -1941,27 +3456,48 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
1941
3456
  process.stdin.on("error", reject);
1942
3457
  }) : readFileSync(opts.acceptFile, "utf8");
1943
3458
  } catch (error) {
1944
- p.log.error(error instanceof Error ? `Failed to read pairing payload from ${opts.acceptFile}: ${error.message}` : `Failed to read pairing payload from ${opts.acceptFile}`);
3459
+ const msg = error instanceof Error ? `Failed to read pairing payload from ${opts.acceptFile}: ${error.message}` : `Failed to read pairing payload from ${opts.acceptFile}`;
3460
+ if (opts.json) {
3461
+ emitJsonError("read_error", msg);
3462
+ return;
3463
+ }
3464
+ p.log.error(msg);
1945
3465
  process.exitCode = 1;
1946
3466
  return;
1947
3467
  }
1948
3468
  if (acceptModeRequested && !(acceptText ?? "").trim()) {
3469
+ if (opts.json) {
3470
+ emitJsonError("usage_error", "Empty pairing payload; provide JSON via --accept or --accept-file", 2);
3471
+ return;
3472
+ }
1949
3473
  p.log.error("Empty pairing payload; provide JSON via --accept or --accept-file");
1950
3474
  process.exitCode = 1;
1951
3475
  return;
1952
3476
  }
1953
3477
  if (!acceptText && (opts.include || opts.exclude || opts.all || opts.default)) {
3478
+ if (opts.json) {
3479
+ emitJsonError("usage_error", "Project filters are outbound-only and must be set on the device running --accept", 2);
3480
+ return;
3481
+ }
1954
3482
  p.log.error("Project filters are outbound-only and must be set on the device running --accept");
1955
3483
  process.exitCode = 1;
1956
3484
  return;
1957
3485
  }
1958
3486
  if (acceptText?.trim()) {
1959
3487
  if (opts.all && opts.default) {
3488
+ if (opts.json) {
3489
+ emitJsonError("usage_error", "Use only one of --all or --default", 2);
3490
+ return;
3491
+ }
1960
3492
  p.log.error("Use only one of --all or --default");
1961
3493
  process.exitCode = 1;
1962
3494
  return;
1963
3495
  }
1964
3496
  if ((opts.all || opts.default) && (opts.include || opts.exclude)) {
3497
+ if (opts.json) {
3498
+ emitJsonError("usage_error", "--include/--exclude cannot be combined with --all/--default", 2);
3499
+ return;
3500
+ }
1965
3501
  p.log.error("--include/--exclude cannot be combined with --all/--default");
1966
3502
  process.exitCode = 1;
1967
3503
  return;
@@ -1970,7 +3506,12 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
1970
3506
  try {
1971
3507
  payload = JSON.parse(acceptText);
1972
3508
  } catch (error) {
1973
- p.log.error(error instanceof Error ? `Invalid pairing payload: ${error.message}` : "Invalid pairing payload");
3509
+ const msg = error instanceof Error ? `Invalid pairing payload: ${error.message}` : "Invalid pairing payload";
3510
+ if (opts.json) {
3511
+ emitJsonError("invalid_payload", msg);
3512
+ return;
3513
+ }
3514
+ p.log.error(msg);
1974
3515
  process.exitCode = 1;
1975
3516
  return;
1976
3517
  }
@@ -1979,11 +3520,20 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
1979
3520
  const publicKey = String(payload.public_key || "").trim();
1980
3521
  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()) : [];
1981
3522
  if (!deviceId || !fingerprint || !publicKey || resolvedAddresses.length === 0) {
1982
- p.log.error("Pairing payload missing device_id, fingerprint, public_key, or addresses");
3523
+ const msg = "Pairing payload missing device_id, fingerprint, public_key, or addresses";
3524
+ if (opts.json) {
3525
+ emitJsonError("invalid_payload", msg);
3526
+ return;
3527
+ }
3528
+ p.log.error(msg);
1983
3529
  process.exitCode = 1;
1984
3530
  return;
1985
3531
  }
1986
3532
  if (fingerprintPublicKey(publicKey) !== fingerprint) {
3533
+ if (opts.json) {
3534
+ emitJsonError("fingerprint_mismatch", "Pairing payload fingerprint mismatch");
3535
+ return;
3536
+ }
1987
3537
  p.log.error("Pairing payload fingerprint mismatch");
1988
3538
  process.exitCode = 1;
1989
3539
  return;
@@ -2001,6 +3551,13 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
2001
3551
  include: opts.all ? [] : parseProjectList(opts.include),
2002
3552
  exclude: opts.all ? [] : parseProjectList(opts.exclude)
2003
3553
  });
3554
+ if (opts.json) {
3555
+ console.log(JSON.stringify({
3556
+ ok: true,
3557
+ peer_device_id: deviceId
3558
+ }));
3559
+ return;
3560
+ }
2004
3561
  p.log.success(`Paired with ${deviceId}`);
2005
3562
  return;
2006
3563
  }
@@ -2008,24 +3565,28 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
2008
3565
  const [deviceId, fingerprint] = ensureDeviceIdentity(store.db, { keysDir });
2009
3566
  const publicKey = loadPublicKey(keysDir);
2010
3567
  if (!publicKey) {
3568
+ if (opts.json) {
3569
+ emitJsonError("missing_key", "Public key missing");
3570
+ return;
3571
+ }
2011
3572
  p.log.error("Public key missing");
2012
3573
  process.exitCode = 1;
2013
3574
  return;
2014
3575
  }
2015
- const config = readCodememConfigFile();
3576
+ const config = readCliConfig(opts.config);
2016
3577
  const explicitAddress = opts.address?.trim();
2017
3578
  const configuredHost = typeof config.sync_host === "string" ? config.sync_host : null;
2018
3579
  const configuredPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
2019
3580
  const addresses = collectAdvertiseAddresses(explicitAddress ?? null, configuredHost, configuredPort, networkInterfaces());
2020
- const payload = {
3581
+ const payloadObj = {
2021
3582
  device_id: deviceId,
2022
3583
  fingerprint,
2023
3584
  public_key: publicKey,
2024
3585
  address: addresses[0] ?? "",
2025
3586
  addresses
2026
3587
  };
2027
- const payloadText = JSON.stringify(payload);
2028
- if (opts.payloadOnly) {
3588
+ const payloadText = JSON.stringify(payloadObj);
3589
+ if (opts.payloadOnly || opts.json) {
2029
3590
  process.stdout.write(`${payloadText}\n`);
2030
3591
  return;
2031
3592
  }
@@ -2042,10 +3603,15 @@ syncCommand.addCommand(new Command("pair").configureHelp(helpStyle).description(
2042
3603
  } finally {
2043
3604
  store.close();
2044
3605
  }
2045
- }));
2046
- 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) => {
2047
- const config = readCodememConfigFile();
2048
- const dbPath = resolveDbPath(opts.db ?? opts.dbPath);
3606
+ });
3607
+ syncCommand.addCommand(pairCmd);
3608
+ var doctorCmd = new Command("doctor").configureHelp(helpStyle).description("Diagnose common sync setup and connectivity issues");
3609
+ addDbOption(doctorCmd);
3610
+ addConfigOption(doctorCmd);
3611
+ addJsonOption(doctorCmd);
3612
+ doctorCmd.action(async (opts) => {
3613
+ const config = readCliConfig(opts.config);
3614
+ const dbPath = resolveDbPath(resolveDbOpt(opts));
2049
3615
  const store = new MemoryStore(dbPath);
2050
3616
  try {
2051
3617
  const d = drizzle(store.db, { schema });
@@ -2061,47 +3627,70 @@ syncCommand.addCommand(new Command("doctor").configureHelp(helpStyle).descriptio
2061
3627
  const syncHost = typeof config.sync_host === "string" ? config.sync_host : "0.0.0.0";
2062
3628
  const syncPort = typeof config.sync_port === "number" ? config.sync_port : 7337;
2063
3629
  const viewerBinding = readViewerBinding(dbPath);
3630
+ const reachable = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
3631
+ if (!reachable) issues.push("daemon not running");
3632
+ if (!device) issues.push("identity missing");
3633
+ if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) issues.push("daemon error");
3634
+ if (peers.length === 0) issues.push("no peers");
3635
+ if (!config.sync_enabled) issues.push("sync is disabled");
3636
+ const peerDetails = [];
3637
+ for (const peer of peers) {
3638
+ const addresses = peer.addresses_json ? JSON.parse(peer.addresses_json) : [];
3639
+ const endpoint = parseStoredAddressEndpoint(addresses[0] ?? "");
3640
+ const reach = endpoint ? await portOpen(endpoint.host, endpoint.port) ? "ok" : "unreachable" : "unknown";
3641
+ const pinned = Boolean(peer.pinned_fingerprint);
3642
+ const hasKey = Boolean(peer.public_key);
3643
+ peerDetails.push({
3644
+ peer_device_id: peer.peer_device_id,
3645
+ addresses: addresses.length,
3646
+ reachable: reach,
3647
+ pinned,
3648
+ has_public_key: hasKey
3649
+ });
3650
+ if (reach !== "ok") issues.push(`peer ${peer.peer_device_id} unreachable`);
3651
+ if (!pinned || !hasKey) issues.push(`peer ${peer.peer_device_id} not pinned`);
3652
+ }
3653
+ if (opts.json) {
3654
+ console.log(JSON.stringify({
3655
+ enabled: config.sync_enabled === true,
3656
+ listen: `${syncHost}:${syncPort}`,
3657
+ mdns: process.env.CODEMEM_SYNC_MDNS ? "env-configured" : "default/off",
3658
+ daemon: reachable ? "running" : "not running",
3659
+ identity: device?.device_id ?? null,
3660
+ daemon_error: daemonState?.last_error ?? null,
3661
+ peers: peerDetails,
3662
+ issues: [...new Set(issues)],
3663
+ ok: issues.length === 0
3664
+ }));
3665
+ return;
3666
+ }
2064
3667
  console.log("Sync doctor");
2065
3668
  console.log(`- Enabled: ${config.sync_enabled === true}`);
2066
3669
  console.log(`- Listen: ${syncHost}:${syncPort}`);
2067
3670
  console.log(`- mDNS: ${process.env.CODEMEM_SYNC_MDNS ? "env-configured" : "default/off"}`);
2068
- const reachable = viewerBinding ? await portOpen(viewerBinding.host, viewerBinding.port) : false;
2069
3671
  console.log(`- Daemon: ${reachable ? "running" : "not running"}`);
2070
- if (!reachable) issues.push("daemon not running");
2071
- if (!device) {
2072
- console.log("- Identity: missing (run `codemem sync enable`)");
2073
- issues.push("identity missing");
2074
- } else console.log(`- Identity: ${device.device_id}`);
2075
- if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) {
2076
- console.log(`- Daemon error: ${daemonState.last_error} (at ${daemonState.last_error_at ?? "unknown"})`);
2077
- issues.push("daemon error");
2078
- }
2079
- if (peers.length === 0) {
2080
- console.log("- Peers: none (pair a device first)");
2081
- issues.push("no peers");
2082
- } else {
3672
+ if (!device) console.log("- Identity: missing (run `codemem sync enable`)");
3673
+ else console.log(`- Identity: ${device.device_id}`);
3674
+ if (daemonState?.last_error && (!daemonState.last_ok_at || daemonState.last_ok_at < (daemonState.last_error_at ?? ""))) console.log(`- Daemon error: ${daemonState.last_error} (at ${daemonState.last_error_at ?? "unknown"})`);
3675
+ if (peers.length === 0) console.log("- Peers: none (pair a device first)");
3676
+ else {
2083
3677
  console.log(`- Peers: ${peers.length}`);
2084
- for (const peer of peers) {
2085
- const addresses = peer.addresses_json ? JSON.parse(peer.addresses_json) : [];
2086
- const endpoint = parseStoredAddressEndpoint(addresses[0] ?? "");
2087
- const reach = endpoint ? await portOpen(endpoint.host, endpoint.port) ? "ok" : "unreachable" : "unknown";
2088
- const pinned = Boolean(peer.pinned_fingerprint);
2089
- const hasKey = Boolean(peer.public_key);
2090
- console.log(` - ${peer.peer_device_id}: addresses=${addresses.length} reach=${reach} pinned=${pinned} public_key=${hasKey}`);
2091
- if (reach !== "ok") issues.push(`peer ${peer.peer_device_id} unreachable`);
2092
- if (!pinned || !hasKey) issues.push(`peer ${peer.peer_device_id} not pinned`);
2093
- }
3678
+ for (const detail of peerDetails) console.log(` - ${detail.peer_device_id}: addresses=${detail.addresses} reach=${detail.reachable} pinned=${detail.pinned} public_key=${detail.has_public_key}`);
2094
3679
  }
2095
- if (!config.sync_enabled) issues.push("sync is disabled");
2096
3680
  if (issues.length > 0) console.log(`WARN: ${[...new Set(issues)].slice(0, 3).join(", ")}`);
2097
3681
  else console.log("OK: sync looks healthy");
2098
3682
  } finally {
2099
3683
  store.close();
2100
3684
  }
2101
- }));
2102
- 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) => {
2103
- const config = readCodememConfigFile();
2104
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
3685
+ });
3686
+ syncCommand.addCommand(doctorCmd);
3687
+ var statusCmd = new Command("status").configureHelp(helpStyle).description("Show sync configuration and peer summary");
3688
+ addDbOption(statusCmd);
3689
+ addConfigOption(statusCmd);
3690
+ addJsonOption(statusCmd);
3691
+ statusCmd.action((opts) => {
3692
+ const config = readCliConfig(opts.config);
3693
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
2105
3694
  try {
2106
3695
  const d = drizzle(store.db, { schema });
2107
3696
  const deviceRow = d.select({
@@ -2151,17 +3740,41 @@ syncCommand.addCommand(new Command("status").configureHelp(helpStyle).descriptio
2151
3740
  } finally {
2152
3741
  store.close();
2153
3742
  }
2154
- }));
2155
- 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) => {
2156
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
3743
+ });
3744
+ syncCommand.addCommand(statusCmd);
3745
+ var enableCmd = new Command("enable").configureHelp(helpStyle).description("Enable sync and initialize device identity").option("--sync-host <host>", "sync listen host").option("--sync-port <port>", "sync listen port").option("--interval <seconds>", "sync interval in seconds");
3746
+ addDbOption(enableCmd);
3747
+ addConfigOption(enableCmd);
3748
+ addJsonOption(enableCmd);
3749
+ enableCmd.addOption(new Option("--host <host>", "sync listen host").hideHelp());
3750
+ enableCmd.addOption(new Option("--port <port>", "sync listen port").hideHelp());
3751
+ enableCmd.action((opts) => {
3752
+ if (opts.host && !opts.syncHost) emitDeprecationWarning("--host on sync enable", "--sync-host");
3753
+ if (opts.port && !opts.syncPort) emitDeprecationWarning("--port on sync enable", "--sync-port");
3754
+ const effectiveHost = opts.syncHost ?? opts.host;
3755
+ const effectivePort = opts.syncPort ?? opts.port;
3756
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
2157
3757
  try {
2158
3758
  const [deviceId, fingerprint] = ensureDeviceIdentity(store.db);
2159
- const config = readCodememConfigFile();
3759
+ const config = readCliConfig(opts.config);
2160
3760
  config.sync_enabled = true;
2161
- if (opts.host) config.sync_host = opts.host;
2162
- if (opts.port) config.sync_port = Number.parseInt(opts.port, 10);
2163
- if (opts.interval) config.sync_interval_s = Number.parseInt(opts.interval, 10);
2164
- writeCodememConfigFile(config);
3761
+ if (effectiveHost) config.sync_host = effectiveHost;
3762
+ const syncPort = parsePositiveIntegerOption(effectivePort, "--sync-port");
3763
+ const syncInterval = parsePositiveIntegerOption(opts.interval, "--interval");
3764
+ if (syncPort != null) config.sync_port = syncPort;
3765
+ if (syncInterval != null) config.sync_interval_s = syncInterval;
3766
+ writeCliConfig(config, opts.config);
3767
+ if (opts.json) {
3768
+ console.log(JSON.stringify({
3769
+ ok: true,
3770
+ device_id: deviceId,
3771
+ fingerprint,
3772
+ host: config.sync_host ?? "0.0.0.0",
3773
+ port: config.sync_port ?? 7337,
3774
+ interval_s: config.sync_interval_s ?? 120
3775
+ }));
3776
+ return;
3777
+ }
2165
3778
  p.intro("codemem sync enable");
2166
3779
  p.log.success([
2167
3780
  `Device ID: ${deviceId}`,
@@ -2174,16 +3787,23 @@ syncCommand.addCommand(new Command("enable").configureHelp(helpStyle).descriptio
2174
3787
  } finally {
2175
3788
  store.close();
2176
3789
  }
2177
- }));
2178
- syncCommand.addCommand(new Command("disable").configureHelp(helpStyle).description("Disable sync without deleting keys or peers").action(() => {
2179
- const config = readCodememConfigFile();
3790
+ });
3791
+ syncCommand.addCommand(enableCmd);
3792
+ var disableCmd = new Command("disable").configureHelp(helpStyle).description("Disable sync without deleting keys or peers");
3793
+ addConfigOption(disableCmd);
3794
+ disableCmd.action((opts) => {
3795
+ const config = readCliConfig(opts.config);
2180
3796
  config.sync_enabled = false;
2181
- writeCodememConfigFile(config);
3797
+ writeCliConfig(config, opts.config);
2182
3798
  p.intro("codemem sync disable");
2183
3799
  p.outro("Sync disabled — restart `codemem serve` to take effect");
2184
- }));
2185
- var peersCommand = 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) => {
2186
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
3800
+ });
3801
+ syncCommand.addCommand(disableCmd);
3802
+ var peersCommand = new Command("peers").configureHelp(helpStyle).description("List known sync peers");
3803
+ addDbOption(peersCommand);
3804
+ addJsonOption(peersCommand);
3805
+ peersCommand.action((opts) => {
3806
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
2187
3807
  try {
2188
3808
  const peers = drizzle(store.db, { schema }).select({
2189
3809
  peer_device_id: schema.syncPeers.peer_device_id,
@@ -2211,17 +3831,28 @@ var peersCommand = new Command("peers").configureHelp(helpStyle).description("Li
2211
3831
  store.close();
2212
3832
  }
2213
3833
  });
2214
- peersCommand.addCommand(new Command("remove").configureHelp(helpStyle).description("Remove a sync peer by device id or exact name").argument("<peer>", "peer device id or exact name").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--json", "output as JSON").action((peerRef, opts) => {
2215
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
3834
+ var peersRemoveCmd = new Command("remove").configureHelp(helpStyle).description("Remove a sync peer by device id or exact name").argument("<peer>", "peer device id or exact name");
3835
+ addDbOption(peersRemoveCmd);
3836
+ addJsonOption(peersRemoveCmd);
3837
+ peersRemoveCmd.action((peerRef, opts) => {
3838
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
2216
3839
  try {
2217
3840
  const d = drizzle(store.db, { schema });
2218
3841
  const match = resolvePeerMatch(d, peerRef);
2219
3842
  if (match === "ambiguous") {
3843
+ if (opts.json) {
3844
+ emitJsonError("ambiguous_peer", `Peer name is ambiguous: ${peerRef.trim()}`);
3845
+ return;
3846
+ }
2220
3847
  p.log.error(`Peer name is ambiguous: ${peerRef.trim()}`);
2221
3848
  process.exitCode = 1;
2222
3849
  return;
2223
3850
  }
2224
3851
  if (!match) {
3852
+ if (opts.json) {
3853
+ emitJsonError("peer_not_found", `Peer not found: ${peerRef.trim()}`);
3854
+ return;
3855
+ }
2225
3856
  p.log.error(`Peer not found: ${peerRef.trim()}`);
2226
3857
  process.exitCode = 1;
2227
3858
  return;
@@ -2243,29 +3874,37 @@ peersCommand.addCommand(new Command("remove").configureHelp(helpStyle).descripti
2243
3874
  } finally {
2244
3875
  store.close();
2245
3876
  }
2246
- }));
3877
+ });
3878
+ peersCommand.addCommand(peersRemoveCmd);
2247
3879
  syncCommand.addCommand(peersCommand);
2248
- syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).description("Fast-bootstrap memories from a peer (full snapshot transfer)").requiredOption("--peer <device-id>", "peer device ID to bootstrap from").option("--page-size <n>", "items per snapshot page (default: 2000)", "2000").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--keys-dir <path>", "keys directory").option("--force", "skip dirty-local-state safety check").option("--json", "output as JSON").action(async (opts) => {
2249
- const store = new MemoryStore(resolveDbPath(opts.db ?? opts.dbPath));
3880
+ var bootstrapCmd = new Command("bootstrap").configureHelp(helpStyle).description("Fast-bootstrap memories from a peer (full snapshot transfer)").argument("[peer-device-id]", "peer device ID to bootstrap from").option("--bootstrap-grant <grant-id>", "bootstrap grant id for seed-authorized bootstrap").option("--page-size <n>", "items per snapshot page (default: 2000)", "2000").option("--keys-dir <path>", "keys directory").option("--force", "skip dirty-local-state safety check");
3881
+ addDbOption(bootstrapCmd);
3882
+ addJsonOption(bootstrapCmd);
3883
+ bootstrapCmd.addOption(new Option("--peer <device-id>", "peer device ID").hideHelp());
3884
+ bootstrapCmd.action(async (peerArg, opts) => {
3885
+ const peerDeviceId = (peerArg || opts.peer || "").trim();
3886
+ if (!peerDeviceId) {
3887
+ if (opts.json) {
3888
+ emitJsonError("usage_error", "missing required argument: <peer-device-id>", 2);
3889
+ return;
3890
+ }
3891
+ p.log.error("missing required argument: <peer-device-id>");
3892
+ process.exitCode = 2;
3893
+ return;
3894
+ }
3895
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
2250
3896
  try {
2251
- const peerDeviceId = opts.peer.trim();
2252
3897
  const pageSize = Math.max(1, Number.parseInt(opts.pageSize, 10) || 2e3);
2253
3898
  const keysDir = opts.keysDir ?? void 0;
2254
3899
  const peer = drizzle(store.db, { schema }).select().from(schema.syncPeers).where(eq(schema.syncPeers.peer_device_id, peerDeviceId)).get();
2255
3900
  if (!peer) {
2256
- if (opts.json) console.log(JSON.stringify({
2257
- ok: false,
2258
- error: "peer not found"
2259
- }));
3901
+ if (opts.json) emitJsonError("peer_not_found", `Peer ${peerDeviceId} not found in sync_peers.`);
2260
3902
  else p.log.error(`Peer ${peerDeviceId} not found in sync_peers.`);
2261
3903
  process.exitCode = 1;
2262
3904
  return;
2263
3905
  }
2264
3906
  if (!peer.pinned_fingerprint) {
2265
- if (opts.json) console.log(JSON.stringify({
2266
- ok: false,
2267
- error: "peer not pinned"
2268
- }));
3907
+ if (opts.json) emitJsonError("peer_not_pinned", `Peer ${peerDeviceId} has no pinned fingerprint. Accept it first.`);
2269
3908
  else p.log.error(`Peer ${peerDeviceId} has no pinned fingerprint. Accept it first.`);
2270
3909
  process.exitCode = 1;
2271
3910
  return;
@@ -2273,11 +3912,7 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
2273
3912
  if (!opts.force) {
2274
3913
  const dirty = hasUnsyncedSharedMemoryChanges(store.db);
2275
3914
  if (dirty.dirty) {
2276
- if (opts.json) console.log(JSON.stringify({
2277
- ok: false,
2278
- error: "local_unsynced_changes",
2279
- count: dirty.count
2280
- }));
3915
+ if (opts.json) emitJsonError("local_unsynced_changes", `${dirty.count} unsynced shared memory change(s) would be lost. Use --force to override.`);
2281
3916
  else p.log.error(`${dirty.count} unsynced shared memory change(s) would be lost. Use --force to override.`);
2282
3917
  process.exitCode = 1;
2283
3918
  return;
@@ -2286,17 +3921,14 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
2286
3921
  const [deviceId] = ensureDeviceIdentity(store.db, { keysDir });
2287
3922
  const addresses = JSON.parse(String(peer.addresses_json ?? "[]"));
2288
3923
  if (!addresses.length) {
2289
- if (opts.json) console.log(JSON.stringify({
2290
- ok: false,
2291
- error: "no peer addresses"
2292
- }));
3924
+ if (opts.json) emitJsonError("no_peer_addresses", "Peer has no known addresses. Run a sync first or add addresses.");
2293
3925
  else p.log.error("Peer has no known addresses. Run a sync first or add addresses.");
2294
3926
  process.exitCode = 1;
2295
3927
  return;
2296
3928
  }
2297
3929
  let boundary = null;
2298
3930
  let baseUrl = "";
2299
- let lastAddressError = "";
3931
+ const addressResults = [];
2300
3932
  for (const address of addresses) {
2301
3933
  const candidate = buildBaseUrl(address);
2302
3934
  if (!candidate) continue;
@@ -2306,16 +3938,23 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
2306
3938
  method: "GET",
2307
3939
  url: statusUrl,
2308
3940
  bodyBytes: Buffer.alloc(0),
3941
+ bootstrapGrantId: opts.bootstrapGrant,
2309
3942
  keysDir
2310
3943
  });
2311
3944
  try {
2312
3945
  const [code, payload] = await requestJson("GET", statusUrl, { headers });
2313
3946
  if (code !== 200 || !payload) {
2314
- lastAddressError = `${candidate}: status ${code}`;
3947
+ addressResults.push({
3948
+ address: candidate,
3949
+ result: `status ${code}`
3950
+ });
2315
3951
  continue;
2316
3952
  }
2317
3953
  if (payload.fingerprint !== peer.pinned_fingerprint) {
2318
- lastAddressError = `${candidate}: fingerprint mismatch`;
3954
+ addressResults.push({
3955
+ address: candidate,
3956
+ result: "fingerprint mismatch"
3957
+ });
2319
3958
  continue;
2320
3959
  }
2321
3960
  const reset = payload.sync_reset;
@@ -2326,18 +3965,30 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
2326
3965
  baseline_cursor: typeof reset.baseline_cursor === "string" ? reset.baseline_cursor : null
2327
3966
  };
2328
3967
  baseUrl = candidate;
3968
+ addressResults.push({
3969
+ address: candidate,
3970
+ result: "ok"
3971
+ });
2329
3972
  break;
2330
3973
  }
2331
- lastAddressError = `${candidate}: missing sync_reset boundary`;
3974
+ addressResults.push({
3975
+ address: candidate,
3976
+ result: "missing sync_reset boundary"
3977
+ });
2332
3978
  } catch (err) {
2333
- lastAddressError = `${candidate}: ${err instanceof Error ? err.message : String(err)}`;
3979
+ addressResults.push({
3980
+ address: candidate,
3981
+ result: err instanceof Error ? err.message : String(err)
3982
+ });
2334
3983
  }
2335
3984
  }
2336
3985
  if (!boundary || !baseUrl) {
2337
- const detail = lastAddressError ? `peer unreachable or missing reset boundary (${lastAddressError})` : "peer unreachable or missing reset boundary";
3986
+ const summary = addressResults.map((r) => `${r.address}: ${r.result}`).join("; ");
3987
+ const detail = summary ? `no reachable peer with valid reset boundary. Tried: ${summary}` : "peer unreachable or missing reset boundary";
2338
3988
  if (opts.json) console.log(JSON.stringify({
2339
3989
  ok: false,
2340
- error: detail
3990
+ error: detail,
3991
+ addresses: addressResults
2341
3992
  }));
2342
3993
  else p.log.error(detail);
2343
3994
  process.exitCode = 1;
@@ -2357,6 +4008,7 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
2357
4008
  };
2358
4009
  const { items } = await fetchAllSnapshotPages(baseUrl, resetInfo, deviceId, {
2359
4010
  keysDir,
4011
+ bootstrapGrantId: opts.bootstrapGrant,
2360
4012
  pageSize
2361
4013
  });
2362
4014
  const result = applyBootstrapSnapshot(store.db, peerDeviceId, items, resetInfo);
@@ -2375,266 +4027,27 @@ syncCommand.addCommand(new Command("bootstrap").configureHelp(helpStyle).descrip
2375
4027
  } finally {
2376
4028
  store.close();
2377
4029
  }
2378
- }));
2379
- syncCommand.addCommand(new Command("connect").configureHelp(helpStyle).description("Configure coordinator URL for cloud sync").argument("<url>", "coordinator URL (e.g. https://coordinator.example.com)").option("--group <group>", "sync group ID").action((url, opts) => {
2380
- const config = readCodememConfigFile();
4030
+ });
4031
+ syncCommand.addCommand(bootstrapCmd);
4032
+ var connectCmd = new Command("connect").configureHelp(helpStyle).description("Configure coordinator URL for cloud sync").argument("<url>", "coordinator URL (e.g. https://coordinator.example.com)").option("--group <group>", "sync group ID");
4033
+ addConfigOption(connectCmd);
4034
+ connectCmd.action((url, opts) => {
4035
+ const config = readCliConfig(opts.config);
2381
4036
  config.sync_coordinator_url = url.trim();
2382
4037
  if (opts.group) config.sync_coordinator_group = opts.group.trim();
2383
- writeCodememConfigFile(config);
4038
+ writeCliConfig(config, opts.config);
2384
4039
  p.intro("codemem sync connect");
2385
4040
  p.log.success(`Coordinator: ${url.trim()}`);
2386
4041
  if (opts.group) p.log.info(`Group: ${opts.group.trim()}`);
2387
4042
  p.outro("Restart `codemem serve` to activate coordinator sync");
2388
- }));
2389
- var coordinatorCommand = new Command("coordinator").configureHelp(helpStyle).description("Manage coordinator invites, join requests, and relay server");
2390
- coordinatorCommand.addCommand(new Command("group-create").configureHelp(helpStyle).description("Create a coordinator group in the local store").argument("<group>", "group id").option("--name <name>", "display name override").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, opts) => {
2391
- const group = await coordinatorCreateGroupAction({
2392
- groupId,
2393
- displayName: opts.name?.trim() || null,
2394
- dbPath: opts.db ?? opts.dbPath ?? null
2395
- });
2396
- if (opts.json) {
2397
- console.log(JSON.stringify(group, null, 2));
2398
- return;
2399
- }
2400
- p.intro("codemem sync coordinator group-create");
2401
- p.log.success(`Group ready: ${groupId.trim()}`);
2402
- p.outro(String(group.display_name ?? group.group_id ?? groupId.trim()));
2403
- }));
2404
- coordinatorCommand.addCommand(new Command("list-groups").configureHelp(helpStyle).description("List coordinator groups from the local store").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (opts) => {
2405
- const groups = await coordinatorListGroupsAction({ dbPath: opts.db ?? opts.dbPath ?? null });
2406
- if (opts.json) {
2407
- console.log(JSON.stringify(groups, null, 2));
2408
- return;
2409
- }
2410
- p.intro("codemem sync coordinator list-groups");
2411
- if (groups.length === 0) {
2412
- p.outro("No coordinator groups found");
2413
- return;
2414
- }
2415
- for (const group of groups) p.log.message(`- ${String(group.group_id ?? "")}${group.display_name ? ` (${String(group.display_name)})` : ""}`);
2416
- p.outro(`${groups.length} group(s)`);
2417
- }));
2418
- coordinatorCommand.addCommand(new Command("enroll-device").configureHelp(helpStyle).description("Enroll a device in a local coordinator group").argument("<group>", "group id").argument("<device-id>", "device id").option("--fingerprint <fingerprint>", "device fingerprint").option("--public-key <key>", "device public key").option("--public-key-file <path>", "path to device public key").option("--name <name>", "display name").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, deviceId, opts) => {
2419
- const publicKey = readCoordinatorPublicKey(opts);
2420
- const fingerprint = String(opts.fingerprint ?? "").trim();
2421
- if (!fingerprint) {
2422
- p.log.error("Fingerprint required via --fingerprint");
2423
- process.exitCode = 1;
2424
- return;
2425
- }
2426
- if (fingerprintPublicKey(publicKey) !== fingerprint) {
2427
- p.log.error("Fingerprint does not match the provided public key");
2428
- process.exitCode = 1;
2429
- return;
2430
- }
2431
- const enrollment = await coordinatorEnrollDeviceAction({
2432
- groupId,
2433
- deviceId,
2434
- fingerprint,
2435
- publicKey,
2436
- displayName: opts.name?.trim() || null,
2437
- dbPath: opts.db ?? opts.dbPath ?? null
2438
- });
2439
- if (opts.json) {
2440
- console.log(JSON.stringify(enrollment, null, 2));
2441
- return;
2442
- }
2443
- p.intro("codemem sync coordinator enroll-device");
2444
- p.log.success(`Enrolled ${deviceId.trim()} in ${groupId.trim()}`);
2445
- p.outro(String(enrollment.display_name ?? enrollment.device_id ?? deviceId.trim()));
2446
- }));
2447
- coordinatorCommand.addCommand(new Command("list-devices").configureHelp(helpStyle).description("List enrolled devices in a local coordinator group").argument("<group>", "group id").option("--include-disabled", "include disabled devices").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, opts) => {
2448
- const rows = await coordinatorListDevicesAction({
2449
- groupId,
2450
- includeDisabled: opts.includeDisabled === true,
2451
- dbPath: opts.db ?? opts.dbPath ?? null
2452
- });
2453
- if (opts.json) {
2454
- console.log(JSON.stringify(rows, null, 2));
2455
- return;
2456
- }
2457
- p.intro("codemem sync coordinator list-devices");
2458
- if (rows.length === 0) {
2459
- p.outro(`No enrolled devices for ${groupId.trim()}`);
2460
- return;
2461
- }
2462
- for (const row of rows) {
2463
- const label = String(row.display_name ?? row.device_id ?? "").trim() || String(row.device_id ?? "");
2464
- const enabled = Number(row.enabled ?? 1) === 1 ? "enabled" : "disabled";
2465
- p.log.message(`- ${label} (${String(row.device_id ?? "")}) ${enabled}`);
2466
- }
2467
- p.outro(`${rows.length} device(s)`);
2468
- }));
2469
- coordinatorCommand.addCommand(new Command("rename-device").configureHelp(helpStyle).description("Rename an enrolled device in the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id").requiredOption("--name <name>", "display name").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, deviceId, opts) => {
2470
- const result = await coordinatorRenameDeviceAction({
2471
- groupId,
2472
- deviceId,
2473
- displayName: opts.name.trim(),
2474
- dbPath: opts.db ?? opts.dbPath ?? null
2475
- });
2476
- if (!result) {
2477
- p.log.error(`Device not found: ${deviceId.trim()}`);
2478
- process.exitCode = 1;
2479
- return;
2480
- }
2481
- if (opts.json) {
2482
- console.log(JSON.stringify(result, null, 2));
2483
- return;
2484
- }
2485
- p.intro("codemem sync coordinator rename-device");
2486
- p.log.success(`Renamed ${deviceId.trim()} in ${groupId.trim()}`);
2487
- p.outro(String(result.display_name ?? result.device_id ?? deviceId.trim()));
2488
- }));
2489
- coordinatorCommand.addCommand(new Command("disable-device").configureHelp(helpStyle).description("Disable an enrolled device in the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, deviceId, opts) => {
2490
- if (!await coordinatorDisableDeviceAction({
2491
- groupId,
2492
- deviceId,
2493
- dbPath: opts.db ?? opts.dbPath ?? null
2494
- })) {
2495
- p.log.error(`Device not found: ${deviceId.trim()}`);
2496
- process.exitCode = 1;
2497
- return;
2498
- }
2499
- if (opts.json) {
2500
- console.log(JSON.stringify({
2501
- ok: true,
2502
- group_id: groupId.trim(),
2503
- device_id: deviceId.trim()
2504
- }, null, 2));
2505
- return;
2506
- }
2507
- p.intro("codemem sync coordinator disable-device");
2508
- p.log.success(`Disabled ${deviceId.trim()} in ${groupId.trim()}`);
2509
- p.outro("disabled");
2510
- }));
2511
- coordinatorCommand.addCommand(new Command("remove-device").configureHelp(helpStyle).description("Remove an enrolled device from the local coordinator store").argument("<group>", "group id").argument("<device-id>", "device id").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--json", "output as JSON").action(async (groupId, deviceId, opts) => {
2512
- if (!await coordinatorRemoveDeviceAction({
2513
- groupId,
2514
- deviceId,
2515
- dbPath: opts.db ?? opts.dbPath ?? null
2516
- })) {
2517
- p.log.error(`Device not found: ${deviceId.trim()}`);
2518
- process.exitCode = 1;
2519
- return;
2520
- }
2521
- if (opts.json) {
2522
- console.log(JSON.stringify({
2523
- ok: true,
2524
- group_id: groupId.trim(),
2525
- device_id: deviceId.trim()
2526
- }, null, 2));
2527
- return;
2528
- }
2529
- p.intro("codemem sync coordinator remove-device");
2530
- p.log.success(`Removed ${deviceId.trim()} from ${groupId.trim()}`);
2531
- p.outro("removed");
2532
- }));
2533
- coordinatorCommand.addCommand(new Command("serve").configureHelp(helpStyle).description("Run the coordinator relay HTTP server").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--host <host>", "bind host", "127.0.0.1").option("--port <port>", "bind port", "7347").action(async (opts) => {
2534
- const host = String(opts.host ?? "127.0.0.1").trim() || "127.0.0.1";
2535
- const port = Number.parseInt(String(opts.port ?? "7347"), 10);
2536
- const dbPath = opts.db ?? opts.dbPath ?? DEFAULT_COORDINATOR_DB_PATH;
2537
- const app = createBetterSqliteCoordinatorApp({ dbPath });
2538
- p.intro("codemem sync coordinator serve");
2539
- p.log.success(`Coordinator listening at http://${host}:${port}`);
2540
- p.log.info(`DB: ${dbPath}`);
2541
- serve({
2542
- fetch: app.fetch,
2543
- hostname: host,
2544
- port
2545
- });
2546
- }));
2547
- coordinatorCommand.addCommand(new Command("create-invite").configureHelp(helpStyle).description("Create a coordinator team invite").argument("[group]", "group id").option("--group <group>", "group id").option("--coordinator-url <url>", "coordinator URL override").option("--policy <policy>", "invite policy", "auto_admit").option("--ttl-hours <hours>", "invite TTL hours", "24").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override").option("--json", "output as JSON").action(async (groupArg, opts) => {
2548
- const ttlHours = Number.parseInt(String(opts.ttlHours ?? "24"), 10);
2549
- const groupId = String(opts.group ?? "").trim() || String(groupArg ?? "").trim();
2550
- const result = await coordinatorCreateInviteAction({
2551
- groupId,
2552
- coordinatorUrl: opts.coordinatorUrl?.trim() || null,
2553
- policy: String(opts.policy ?? "auto_admit").trim(),
2554
- ttlHours,
2555
- createdBy: null,
2556
- dbPath: opts.db ?? opts.dbPath ?? null,
2557
- remoteUrl: opts.remoteUrl?.trim() || null,
2558
- adminSecret: opts.adminSecret?.trim() || null
2559
- });
2560
- if (opts.json) {
2561
- console.log(JSON.stringify(result, null, 2));
2562
- return;
2563
- }
2564
- p.intro("codemem sync coordinator create-invite");
2565
- p.log.success(`Invite created for ${groupId}`);
2566
- if (typeof result.link === "string") p.log.message(`- link: ${result.link}`);
2567
- if (typeof result.encoded === "string") p.log.message(`- invite: ${result.encoded}`);
2568
- for (const warning of Array.isArray(result.warnings) ? result.warnings : []) p.log.warn(String(warning));
2569
- p.outro("Invite ready");
2570
- }));
2571
- coordinatorCommand.addCommand(new Command("import-invite").configureHelp(helpStyle).description("Import a coordinator invite").argument("<invite>", "invite value or link").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--keys-dir <path>", "keys directory").option("--config <path>", "config path").option("--json", "output as JSON").action(async (invite, opts) => {
2572
- const result = await coordinatorImportInviteAction({
2573
- inviteValue: invite,
2574
- dbPath: opts.db ?? opts.dbPath ?? null,
2575
- keysDir: opts.keysDir ?? null,
2576
- configPath: opts.config ?? null
2577
- });
2578
- if (opts.json) {
2579
- console.log(JSON.stringify(result, null, 2));
2580
- return;
2581
- }
2582
- p.intro("codemem sync coordinator import-invite");
2583
- p.log.success(`Invite imported for ${result.group_id}`);
2584
- p.log.message(`- coordinator: ${result.coordinator_url}`);
2585
- p.log.message(`- status: ${result.status}`);
2586
- p.outro("Coordinator config updated");
2587
- }));
2588
- coordinatorCommand.addCommand(new Command("list-join-requests").configureHelp(helpStyle).description("List pending coordinator join requests").argument("[group]", "group id").option("--group <group>", "group id").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override").option("--json", "output as JSON").action(async (groupArg, opts) => {
2589
- const groupId = String(opts.group ?? "").trim() || String(groupArg ?? "").trim();
2590
- const rows = await coordinatorListJoinRequestsAction({
2591
- groupId,
2592
- dbPath: opts.db ?? opts.dbPath ?? null,
2593
- remoteUrl: opts.remoteUrl?.trim() || null,
2594
- adminSecret: opts.adminSecret?.trim() || null
2595
- });
2596
- if (opts.json) {
2597
- console.log(JSON.stringify(rows, null, 2));
2598
- return;
2599
- }
2600
- p.intro("codemem sync coordinator list-join-requests");
2601
- if (rows.length === 0) {
2602
- p.outro(`No pending join requests for ${groupId}`);
2603
- return;
2604
- }
2605
- for (const row of rows) {
2606
- const displayName = row.display_name || row.device_id;
2607
- p.log.message(`- ${displayName} (${row.device_id}) request_id=${row.request_id}`);
2608
- }
2609
- p.outro(`${rows.length} pending join request(s)`);
2610
- }));
2611
- function addReviewJoinRequestCommand(name, approve) {
2612
- coordinatorCommand.addCommand(new Command(name).configureHelp(helpStyle).description(`${approve ? "Approve" : "Deny"} a coordinator join request`).argument("<request-id>", "join request id").option("--db <path>", "coordinator database path").option("--db-path <path>", "coordinator database path").option("--remote-url <url>", "remote coordinator URL override").option("--admin-secret <secret>", "remote coordinator admin secret override").option("--json", "output as JSON").action(async (requestId, opts) => {
2613
- const request = await coordinatorReviewJoinRequestAction({
2614
- requestId: requestId.trim(),
2615
- approve,
2616
- reviewedBy: null,
2617
- dbPath: opts.db ?? opts.dbPath ?? null,
2618
- remoteUrl: opts.remoteUrl?.trim() || null,
2619
- adminSecret: opts.adminSecret?.trim() || null
2620
- });
2621
- if (!request) {
2622
- p.log.error(`Join request not found: ${requestId.trim()}`);
2623
- process.exitCode = 1;
2624
- return;
2625
- }
2626
- if (opts.json) {
2627
- console.log(JSON.stringify(request, null, 2));
2628
- return;
2629
- }
2630
- p.intro(`codemem sync coordinator ${name}`);
2631
- p.log.success(`${approve ? "Approved" : "Denied"} join request ${requestId.trim()}`);
2632
- p.outro(String(request.status ?? "updated"));
2633
- }));
2634
- }
2635
- addReviewJoinRequestCommand("approve-join-request", true);
2636
- addReviewJoinRequestCommand("deny-join-request", false);
2637
- syncCommand.addCommand(coordinatorCommand);
4043
+ });
4044
+ syncCommand.addCommand(connectCmd);
4045
+ var syncCoordinatorAlias = buildCoordinatorCommand();
4046
+ syncCoordinatorAlias.hook("preAction", (_thisCmd, actionCmd) => {
4047
+ const subName = actionCmd.name();
4048
+ emitDeprecationWarning(`codemem sync coordinator ${subName}`, `codemem coordinator ${subName}`);
4049
+ });
4050
+ syncCommand.addCommand(syncCoordinatorAlias);
2638
4051
  //#endregion
2639
4052
  //#region src/commands/version.ts
2640
4053
  var versionCommand = new Command("version").configureHelp(helpStyle).description("Print codemem version").action(() => {
@@ -2655,24 +4068,24 @@ var versionCommand = new Command("version").configureHelp(helpStyle).description
2655
4068
  var completion = omelette("codemem <command>");
2656
4069
  completion.on("command", ({ reply }) => {
2657
4070
  reply([
4071
+ "claude-hook-inject",
2658
4072
  "claude-hook-ingest",
4073
+ "config",
4074
+ "coordinator",
2659
4075
  "db",
2660
4076
  "embed",
4077
+ "enqueue-raw-event",
2661
4078
  "export-memories",
2662
- "forget",
2663
- "memory",
2664
4079
  "import-memories",
2665
- "setup",
2666
- "show",
2667
- "sync",
2668
- "stats",
4080
+ "mcp",
4081
+ "memory",
4082
+ "pack",
2669
4083
  "recent",
2670
- "remember",
2671
4084
  "search",
2672
- "pack",
2673
4085
  "serve",
2674
- "mcp",
2675
- "enqueue-raw-event",
4086
+ "setup",
4087
+ "stats",
4088
+ "sync",
2676
4089
  "version",
2677
4090
  "help",
2678
4091
  "--help",
@@ -2706,8 +4119,33 @@ if (hasRootFlag("--cleanup-completion")) {
2706
4119
  completion.cleanupShellInitFile();
2707
4120
  process.exit(0);
2708
4121
  }
4122
+ {
4123
+ const memExport = new Command("export").description("Export memories to a JSON file for sharing or backup").argument("<output>", "output file path (use '-' for stdout)").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--project <project>", "filter by project").option("--all-projects", "export all projects").option("--include-inactive", "include deactivated memories").option("--since <iso>", "only export memories created after this ISO timestamp").configureHelp(helpStyle).allowUnknownOption(true).allowExcessArguments(true).action(async () => {
4124
+ const idx = process.argv.indexOf("export");
4125
+ const tail = idx >= 0 ? process.argv.slice(idx + 1) : [];
4126
+ await exportMemoriesCommand.parseAsync([
4127
+ "node",
4128
+ "export-memories",
4129
+ ...tail
4130
+ ]);
4131
+ });
4132
+ const memImport = new Command("import").description("Import memories from an exported JSON file").argument("<inputFile>", "input JSON file (use '-' for stdin)").option("--db <path>", "database path").option("--db-path <path>", "database path").option("--remap-project <path>", "remap all projects to this path on import").option("--dry-run", "preview import without writing").configureHelp(helpStyle).allowUnknownOption(true).allowExcessArguments(true).action(async () => {
4133
+ const idx = process.argv.indexOf("import");
4134
+ const tail = idx >= 0 ? process.argv.slice(idx + 1) : [];
4135
+ await importMemoriesCommand.parseAsync([
4136
+ "node",
4137
+ "import-memories",
4138
+ ...tail
4139
+ ]);
4140
+ });
4141
+ memoryCommand.addCommand(memExport);
4142
+ memoryCommand.addCommand(memImport);
4143
+ }
2709
4144
  program.addCommand(serveCommand);
4145
+ program.addCommand(configCommand);
4146
+ program.addCommand(coordinatorCommand);
2710
4147
  program.addCommand(mcpCommand);
4148
+ program.addCommand(claudeHookInjectCommand);
2711
4149
  program.addCommand(claudeHookIngestCommand);
2712
4150
  program.addCommand(dbCommand);
2713
4151
  program.addCommand(exportMemoriesCommand);
@@ -2717,9 +4155,9 @@ program.addCommand(embedCommand);
2717
4155
  program.addCommand(recentCommand);
2718
4156
  program.addCommand(searchCommand);
2719
4157
  program.addCommand(packCommand);
2720
- program.addCommand(showMemoryCommand);
2721
- program.addCommand(forgetMemoryCommand);
2722
- program.addCommand(rememberMemoryCommand);
4158
+ program.addCommand(showMemoryCommand, { hidden: true });
4159
+ program.addCommand(forgetMemoryCommand, { hidden: true });
4160
+ program.addCommand(rememberMemoryCommand, { hidden: true });
2723
4161
  program.addCommand(memoryCommand);
2724
4162
  program.addCommand(syncCommand);
2725
4163
  program.addCommand(setupCommand);