codemem 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
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";
2
+ import { DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, MemoryStore, ObserverClient, RawEventSweeper, SyncRetentionRunner, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildAuthHeaders, buildBaseUrl, buildRawEventEnvelopeFromHook, compareMemoryRoleReports, connect, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, ensureDeviceIdentity, exportMemories, fetchAllSnapshotPages, fingerprintPublicKey, getExtractionBenchmarkProfile, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSessionExtractionEval, getSessionExtractionEvalScenario, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, 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
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
5
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
@@ -242,7 +242,7 @@ function envNotDisabled(value) {
242
242
  const normalized = String(value ?? "").trim().toLowerCase();
243
243
  return normalized !== "0" && normalized !== "false" && normalized !== "off";
244
244
  }
245
- function parsePositiveInt(value, fallback) {
245
+ function parsePositiveInt$1(value, fallback) {
246
246
  const parsed = Number.parseInt(String(value ?? ""), 10);
247
247
  if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
248
248
  return parsed;
@@ -269,8 +269,8 @@ function resolveInjectProject(payload) {
269
269
  async function buildLocalPack(context, project, dbPath) {
270
270
  const store = new MemoryStore(dbPath);
271
271
  try {
272
- const limit = parsePositiveInt(process.env.CODEMEM_INJECT_LIMIT, 8);
273
- const budget = parsePositiveInt(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
272
+ const limit = parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8);
273
+ const budget = parsePositiveInt$1(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800);
274
274
  const filters = {};
275
275
  if (project) filters.project = project;
276
276
  const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
@@ -281,11 +281,11 @@ async function buildLocalPack(context, project, dbPath) {
281
281
  }
282
282
  async function tryHttpPack(context, project, maxTimeMs = DEFAULT_HTTP_MAX_TIME_S * 1e3) {
283
283
  const host = process.env.CODEMEM_VIEWER_HOST || DEFAULT_VIEWER_HOST;
284
- const port = parsePositiveInt(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
284
+ const port = parsePositiveInt$1(process.env.CODEMEM_VIEWER_PORT, DEFAULT_VIEWER_PORT);
285
285
  const url = new URL(`http://${host}:${port}/api/pack`);
286
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)));
287
+ url.searchParams.set("limit", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_LIMIT, 8)));
288
+ url.searchParams.set("token_budget", String(parsePositiveInt$1(process.env.CODEMEM_INJECT_TOKEN_BUDGET, 800)));
289
289
  if (project) url.searchParams.set("project", project);
290
290
  const controller = new AbortController();
291
291
  const timeout = setTimeout(() => controller.abort(), maxTimeMs);
@@ -308,8 +308,8 @@ async function buildClaudeHookInjection(payload, opts, deps = {}) {
308
308
  const httpPack = deps.httpPack ?? tryHttpPack;
309
309
  const resolveDb = deps.resolveDb ?? resolveDbPath;
310
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;
311
+ const maxChars = parsePositiveInt$1(process.env.CODEMEM_INJECT_MAX_CHARS, DEFAULT_MAX_CHARS);
312
+ const httpMaxTimeMs = parsePositiveInt$1(process.env.CODEMEM_INJECT_HTTP_MAX_TIME_S, DEFAULT_HTTP_MAX_TIME_S) * 1e3;
313
313
  let additionalContext = "";
314
314
  try {
315
315
  additionalContext = await buildPack(context, project, resolveDb(resolveDbOpt(opts)));
@@ -1361,6 +1361,119 @@ pruneMemCmd.action((opts) => {
1361
1361
  }
1362
1362
  });
1363
1363
  dbCommand.addCommand(pruneMemCmd);
1364
+ var dedupCmd = new Command("dedup-memories").configureHelp(helpStyle).description("Deactivate near-duplicate memories (cross-session, same normalized title within time window)").option("--window <ms>", "max time gap in milliseconds between duplicates (default: 3600000)").option("--limit <n>", "max pairs to check").option("--dry-run", "preview deactivations without writing");
1365
+ addDbOption(dedupCmd);
1366
+ addJsonOption(dedupCmd);
1367
+ dedupCmd.action((opts) => {
1368
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1369
+ try {
1370
+ const windowMs = parseOptionalPositiveInt$1(opts.window);
1371
+ const limit = parseOptionalPositiveInt$1(opts.limit);
1372
+ const result = dedupNearDuplicateMemories(store.db, {
1373
+ windowMs,
1374
+ limit,
1375
+ dryRun: opts.dryRun === true
1376
+ });
1377
+ if (opts.json) {
1378
+ console.log(JSON.stringify(result, null, 2));
1379
+ return;
1380
+ }
1381
+ const action = opts.dryRun ? "Would deactivate" : "Deactivated";
1382
+ p.intro("codemem db dedup-memories");
1383
+ if (result.pairs.length > 0 && result.pairs.length <= 20) for (const pair of result.pairs) p.log.info(`${action} [${pair.deactivated_id}] (kept [${pair.kept_id}]): ${pair.title.slice(0, 80)}`);
1384
+ p.outro(`${action} ${result.deactivated} duplicates from ${result.checked} pairs`);
1385
+ } catch (error) {
1386
+ p.log.error(error instanceof Error ? error.message : String(error));
1387
+ process.exitCode = 1;
1388
+ } finally {
1389
+ store.close();
1390
+ }
1391
+ });
1392
+ dbCommand.addCommand(dedupCmd);
1393
+ var backfillDedupKeysCmd = new Command("backfill-dedup-keys").configureHelp(helpStyle).description("Populate missing memory_items.dedup_key values for legacy rows").option("--limit <n>", "max memories to check").option("--dry-run", "preview updates without writing");
1394
+ addDbOption(backfillDedupKeysCmd);
1395
+ addJsonOption(backfillDedupKeysCmd);
1396
+ backfillDedupKeysCmd.action((opts) => {
1397
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1398
+ try {
1399
+ const limit = parseOptionalPositiveInt$1(opts.limit);
1400
+ const result = backfillMemoryDedupKeys(store.db, {
1401
+ limit,
1402
+ dryRun: opts.dryRun === true
1403
+ });
1404
+ if (opts.json) {
1405
+ console.log(JSON.stringify(result, null, 2));
1406
+ return;
1407
+ }
1408
+ const action = opts.dryRun ? "Would update" : "Updated";
1409
+ p.intro("codemem db backfill-dedup-keys");
1410
+ p.log.success(`${action} ${result.updated} memories (skipped ${result.skipped})`);
1411
+ p.outro(`Checked ${result.checked} memories`);
1412
+ } catch (error) {
1413
+ p.log.error(error instanceof Error ? error.message : String(error));
1414
+ process.exitCode = 1;
1415
+ } finally {
1416
+ store.close();
1417
+ }
1418
+ });
1419
+ dbCommand.addCommand(backfillDedupKeysCmd);
1420
+ var backfillNarrativeCmd = new Command("backfill-narrative").configureHelp(helpStyle).description("Extract narrative from session_summary body_text (## Completed / ## Learned sections)").option("--limit <n>", "max memories to check").option("--dry-run", "preview updates without writing");
1421
+ addDbOption(backfillNarrativeCmd);
1422
+ addJsonOption(backfillNarrativeCmd);
1423
+ backfillNarrativeCmd.action((opts) => {
1424
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1425
+ try {
1426
+ const limit = parseOptionalPositiveInt$1(opts.limit);
1427
+ const result = backfillNarrativeFromBody(store.db, {
1428
+ limit,
1429
+ dryRun: opts.dryRun === true
1430
+ });
1431
+ if (opts.json) {
1432
+ console.log(JSON.stringify(result, null, 2));
1433
+ return;
1434
+ }
1435
+ const action = opts.dryRun ? "Would update" : "Updated";
1436
+ p.intro("codemem db backfill-narrative");
1437
+ p.log.success(`${action} ${result.updated} memories (skipped ${result.skipped})`);
1438
+ p.outro(`Checked ${result.checked} memories`);
1439
+ } catch (error) {
1440
+ p.log.error(error instanceof Error ? error.message : String(error));
1441
+ process.exitCode = 1;
1442
+ } finally {
1443
+ store.close();
1444
+ }
1445
+ });
1446
+ dbCommand.addCommand(backfillNarrativeCmd);
1447
+ var aiBackfillStructuredCmd = new Command("ai-backfill-structured").configureHelp(helpStyle).description("Use GPT-5.4 to populate missing narrative/facts/concepts for older non-session-summary memories").option("--limit <n>", "max memories to check").option("--kinds <csv>", "comma-separated kinds to target").option("--overwrite", "overwrite existing structured fields instead of only filling missing ones").option("--dry-run", "preview updates without writing");
1448
+ addDbOption(aiBackfillStructuredCmd);
1449
+ addJsonOption(aiBackfillStructuredCmd);
1450
+ aiBackfillStructuredCmd.action(async (opts) => {
1451
+ const store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
1452
+ try {
1453
+ const limit = parseOptionalPositiveInt$1(opts.limit);
1454
+ const kinds = parseKindsCsv(opts.kinds);
1455
+ const result = await aiBackfillStructuredContent(store.db, {
1456
+ limit,
1457
+ kinds,
1458
+ overwrite: opts.overwrite === true,
1459
+ dryRun: opts.dryRun === true
1460
+ });
1461
+ if (opts.json) {
1462
+ console.log(JSON.stringify(result, null, 2));
1463
+ return;
1464
+ }
1465
+ const action = opts.dryRun ? "Would update" : "Updated";
1466
+ p.intro("codemem db ai-backfill-structured");
1467
+ p.log.success(`${action} ${result.updated} memories (skipped ${result.skipped}, failed ${result.failed})`);
1468
+ p.outro(`Checked ${result.checked} memories`);
1469
+ } catch (error) {
1470
+ p.log.error(error instanceof Error ? error.message : String(error));
1471
+ process.exitCode = 1;
1472
+ } finally {
1473
+ store.close();
1474
+ }
1475
+ });
1476
+ dbCommand.addCommand(aiBackfillStructuredCmd);
1364
1477
  //#endregion
1365
1478
  //#region src/commands/embed.ts
1366
1479
  function parseOptionalPositiveInt(value) {
@@ -1601,13 +1714,22 @@ var mcpCommand = mcpCmd.action(async (opts) => {
1601
1714
  });
1602
1715
  //#endregion
1603
1716
  //#region src/commands/pack-shared.ts
1717
+ var PackUsageError = class extends Error {
1718
+ constructor(message) {
1719
+ super(message);
1720
+ this.name = "PackUsageError";
1721
+ }
1722
+ };
1604
1723
  function collectWorkingSetFile(value, previous) {
1605
1724
  return [...previous, value];
1606
1725
  }
1726
+ function addPackRequestOptions(command) {
1727
+ return command.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("--compact", "render a scannable index with full detail only for top N items").option("--compact-detail <n>", "items to show in full detail in compact mode (default 3)");
1728
+ }
1607
1729
  function buildPackRequestOptions(opts, ctx = {}) {
1608
- const limit = Number.parseInt(opts.limit ?? "10", 10) || 10;
1730
+ const limit = parsePositiveInt(opts.limit ?? "10", "limit");
1609
1731
  const budgetRaw = opts.tokenBudget ?? opts.budget;
1610
- const budget = budgetRaw ? Number.parseInt(budgetRaw, 10) : void 0;
1732
+ const budget = budgetRaw ? parseNonNegativeInt(budgetRaw, "token budget") : void 0;
1611
1733
  const filters = {};
1612
1734
  if (!opts.allProjects) {
1613
1735
  const defaultProject = ctx.envProject?.trim() || null;
@@ -1617,12 +1739,30 @@ function buildPackRequestOptions(opts, ctx = {}) {
1617
1739
  if (project) filters.project = project;
1618
1740
  }
1619
1741
  if ((opts.workingSetFile?.length ?? 0) > 0) filters.working_set_paths = opts.workingSetFile;
1742
+ let renderOptions;
1743
+ if (opts.compact || opts.compactDetail != null) {
1744
+ renderOptions = { compact: true };
1745
+ if (opts.compactDetail != null) renderOptions.compactDetailCount = parseNonNegativeInt(opts.compactDetail, "compact detail count");
1746
+ }
1620
1747
  return {
1621
1748
  limit,
1622
1749
  budget,
1623
- filters
1750
+ filters,
1751
+ renderOptions
1624
1752
  };
1625
1753
  }
1754
+ function parsePositiveInt(value, label) {
1755
+ if (!/^\d+$/.test(value.trim())) throw new PackUsageError(`${label} must be a positive integer`);
1756
+ const parsed = Number.parseInt(value, 10);
1757
+ if (!Number.isFinite(parsed) || parsed < 1) throw new PackUsageError(`${label} must be a positive integer`);
1758
+ return parsed;
1759
+ }
1760
+ function parseNonNegativeInt(value, label) {
1761
+ if (!/^\d+$/.test(value.trim())) throw new PackUsageError(`${label} must be a non-negative integer`);
1762
+ const parsed = Number.parseInt(value, 10);
1763
+ if (!Number.isFinite(parsed) || parsed < 0) throw new PackUsageError(`${label} must be a non-negative integer`);
1764
+ return parsed;
1765
+ }
1626
1766
  //#endregion
1627
1767
  //#region src/commands/memory.ts
1628
1768
  /**
@@ -2235,14 +2375,79 @@ memoryCommand.addCommand(createMemoryRelinkReportCommand());
2235
2375
  memoryCommand.addCommand(createMemoryRelinkPlanCommand());
2236
2376
  //#endregion
2237
2377
  //#region src/commands/pack.ts
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)));
2378
+ function describeCandidate(candidate) {
2379
+ const scoreParts = [
2380
+ candidate.scores.combined_score != null ? `combined=${candidate.scores.combined_score.toFixed(2)}` : null,
2381
+ candidate.scores.base_score != null ? `base=${candidate.scores.base_score.toFixed(2)}` : null,
2382
+ candidate.scores.text_overlap > 0 ? `text=${candidate.scores.text_overlap}` : null,
2383
+ candidate.scores.tag_overlap > 0 ? `tag=${candidate.scores.tag_overlap}` : null,
2384
+ candidate.scores.working_set_overlap > 0 ? `working_set=${candidate.scores.working_set_overlap.toFixed(2)}` : null
2385
+ ].filter(Boolean).join(" ");
2386
+ const lines = [`${candidate.rank}. [${candidate.id}] (${candidate.kind}) ${candidate.title}`];
2387
+ if (candidate.section) lines.push(` - section: ${candidate.section}`);
2388
+ if (candidate.reasons.length > 0) lines.push(` - reasons: ${candidate.reasons.join(", ")}`);
2389
+ if (scoreParts) lines.push(` - scores: ${scoreParts}`);
2390
+ if (candidate.preview) lines.push(` - preview: ${candidate.preview}`);
2391
+ return lines;
2392
+ }
2393
+ function renderPackTrace(trace) {
2394
+ const workingSet = trace.inputs.working_set_files.length > 0 ? trace.inputs.working_set_files.join(", ") : "(none)";
2395
+ const lines = [
2396
+ "Pack trace",
2397
+ `- Query: ${trace.inputs.query}`,
2398
+ `- Project: ${trace.inputs.project ?? "(default)"}`,
2399
+ `- Working set: ${workingSet}`,
2400
+ `- Mode: ${trace.mode.selected}`,
2401
+ `- Mode reasons: ${trace.mode.reasons.join(", ") || "(none)"}`,
2402
+ `- Token budget: ${trace.inputs.token_budget ?? "(none)"}`,
2403
+ ""
2404
+ ];
2405
+ for (const disposition of [
2406
+ "selected",
2407
+ "dropped",
2408
+ "deduped",
2409
+ "trimmed"
2410
+ ]) {
2411
+ const group = trace.retrieval.candidates.filter((candidate) => candidate.disposition === disposition);
2412
+ if (group.length === 0) continue;
2413
+ lines.push(disposition.charAt(0).toUpperCase() + disposition.slice(1));
2414
+ for (const candidate of group) lines.push(...describeCandidate(candidate));
2415
+ lines.push("");
2416
+ }
2417
+ lines.push("Assembly");
2418
+ lines.push(`- deduped ids: ${trace.assembly.deduped_ids.join(", ") || "(none)"}`);
2419
+ lines.push(`- trimmed ids: ${trace.assembly.trimmed_ids.join(", ") || "(none)"}`);
2420
+ lines.push(`- trim reasons: ${trace.assembly.trim_reasons.join(", ") || "(none)"}`);
2421
+ lines.push(`- section counts: summary=${trace.output.section_counts.summary} timeline=${trace.output.section_counts.timeline} observations=${trace.output.section_counts.observations}`);
2422
+ lines.push(`- estimated tokens: ${trace.output.estimated_tokens}`);
2423
+ lines.push(`- truncated: ${trace.output.truncated ? "yes" : "no"}`);
2424
+ lines.push("");
2425
+ lines.push("Final pack");
2426
+ lines.push(trace.output.pack_text);
2427
+ return lines.join("\n");
2428
+ }
2429
+ async function withStore(opts, errorCode, run) {
2430
+ let store = null;
2243
2431
  try {
2244
- const { limit, budget, filters } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
2245
- const result = await store.buildMemoryPackAsync(context, limit, budget, filters);
2432
+ store = new MemoryStore(resolveDbPath(resolveDbOpt(opts)));
2433
+ await run(store);
2434
+ } catch (error) {
2435
+ const message = error instanceof Error ? error.message : String(error);
2436
+ const usageError = error instanceof PackUsageError;
2437
+ if (opts.json) {
2438
+ emitJsonError(usageError ? "usage_error" : errorCode, message, usageError ? 2 : 1);
2439
+ return;
2440
+ }
2441
+ p.log.error(message);
2442
+ process.exitCode = usageError ? 2 : 1;
2443
+ } finally {
2444
+ store?.close();
2445
+ }
2446
+ }
2447
+ async function packAction(context, opts) {
2448
+ await withStore(opts, "pack_failed", async (store) => {
2449
+ const { limit, budget, filters, renderOptions } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
2450
+ const result = await store.buildMemoryPackAsync(context, limit, budget, filters, renderOptions);
2246
2451
  if (opts.json) {
2247
2452
  console.log(JSON.stringify(result, null, 2));
2248
2453
  return;
@@ -2253,15 +2458,34 @@ var packCommand = packCmd.action(async (context, opts) => {
2253
2458
  p.outro("done");
2254
2459
  return;
2255
2460
  }
2256
- const m = result.metrics;
2257
- p.log.info(`${m.total_items} items, ~${m.pack_tokens} tokens` + (m.fallback_used ? " (fallback)" : "") + ` [fts:${m.sources.fts} sem:${m.sources.semantic} fuzzy:${m.sources.fuzzy}]`);
2461
+ const metrics = result.metrics;
2462
+ p.log.info(`${metrics.total_items} items, ~${metrics.pack_tokens} tokens` + (metrics.fallback_used ? " (fallback)" : "") + ` [fts:${metrics.sources.fts} sem:${metrics.sources.semantic} fuzzy:${metrics.sources.fuzzy}]`);
2258
2463
  for (const item of result.items) p.log.step(`#${item.id} ${item.kind} ${item.title}`);
2259
2464
  p.note(result.pack_text, "pack_text");
2260
2465
  p.outro("done");
2261
- } finally {
2262
- store.close();
2263
- }
2264
- });
2466
+ });
2467
+ }
2468
+ async function traceAction(context, opts) {
2469
+ await withStore(opts, "pack_trace_failed", async (store) => {
2470
+ const { limit, budget, filters, renderOptions } = buildPackRequestOptions(opts, { envProject: process.env.CODEMEM_PROJECT });
2471
+ const trace = await store.buildMemoryPackTraceAsync(context, limit, budget, filters, renderOptions);
2472
+ if (opts.json) {
2473
+ console.log(JSON.stringify(trace, null, 2));
2474
+ return;
2475
+ }
2476
+ console.log(renderPackTrace(trace));
2477
+ });
2478
+ }
2479
+ var packCmd = addPackRequestOptions(new Command("pack").enablePositionalOptions().configureHelp(helpStyle).description("Build a context-aware memory pack").argument("<context>", "context string to search for"));
2480
+ addDbOption(packCmd);
2481
+ addJsonOption(packCmd);
2482
+ packCmd.action(packAction);
2483
+ var traceCmd = addPackRequestOptions(new Command("trace").configureHelp(helpStyle).description("Trace retrieval and assembly for a memory pack").argument("<context>", "context string to trace"));
2484
+ addDbOption(traceCmd);
2485
+ addJsonOption(traceCmd);
2486
+ traceCmd.action(traceAction);
2487
+ packCmd.addCommand(traceCmd);
2488
+ var packCommand = packCmd;
2265
2489
  //#endregion
2266
2490
  //#region src/commands/recent.ts
2267
2491
  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");
@@ -2683,6 +2907,8 @@ async function startForegroundViewer(invocation) {
2683
2907
  dbPath: resolveDbPath(invocation.dbPath ?? void 0),
2684
2908
  signal: retentionAbort.signal
2685
2909
  });
2910
+ const vectorMigrationRunner = new VectorModelMigrationRunner({ dbPath: resolveDbPath(invocation.dbPath ?? void 0) });
2911
+ const dedupKeyBackfillRunner = new DedupKeyBackfillRunner({ dbPath: resolveDbPath(invocation.dbPath ?? void 0) });
2686
2912
  const syncRuntimeStatus = {
2687
2913
  phase: syncEnabled ? "starting" : "disabled",
2688
2914
  detail: syncEnabled ? "Waiting for viewer startup to finish" : "Sync is disabled"
@@ -2725,6 +2951,14 @@ async function startForegroundViewer(invocation) {
2725
2951
  p.log.success(`Listening on http://${info.address}:${info.port}`);
2726
2952
  p.log.info(`Database: ${preparedDb}`);
2727
2953
  p.log.step("Raw event sweeper started");
2954
+ if (!isEmbeddingDisabled()) {
2955
+ vectorMigrationRunner.start();
2956
+ p.log.step("Vector maintenance runner started");
2957
+ }
2958
+ if (hasPendingDedupKeyBackfill(store.db)) {
2959
+ dedupKeyBackfillRunner.start();
2960
+ p.log.step("Dedup-key maintenance runner started");
2961
+ }
2728
2962
  if (syncConfig.syncRetentionEnabled) {
2729
2963
  retentionRunner.start();
2730
2964
  p.log.step("Retention maintenance runner started");
@@ -2783,6 +3017,8 @@ async function startForegroundViewer(invocation) {
2783
3017
  syncAbort.abort();
2784
3018
  retentionAbort.abort();
2785
3019
  await sweeper.stop();
3020
+ await dedupKeyBackfillRunner.stop();
3021
+ await vectorMigrationRunner.stop();
2786
3022
  await retentionRunner.stop();
2787
3023
  await new Promise((resolve) => {
2788
3024
  let remaining = syncServer ? 2 : 1;
@@ -4106,7 +4342,7 @@ function getShellCompletionScript() {
4106
4342
  return completion.generateCompletionCode();
4107
4343
  }
4108
4344
  var program = new Command();
4109
- program.name("codemem").description("codemem — persistent memory for AI coding agents").option("--install-completion", "install shell completion").option("--show-completion", "show shell completion install guidance").version(VERSION).configureHelp(helpStyle);
4345
+ program.name("codemem").description("codemem — persistent memory for AI coding agents").enablePositionalOptions().option("--install-completion", "install shell completion").option("--show-completion", "show shell completion install guidance").version(VERSION).configureHelp(helpStyle);
4110
4346
  if (hasRootFlag("--setup-completion") || hasRootFlag("--install-completion")) {
4111
4347
  completion.setupShellInitFile();
4112
4348
  process.exit(0);