nexusmem 0.5.1 → 0.5.2

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/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { Command } from "commander";
5
- import pc18 from "picocolors";
5
+ import pc19 from "picocolors";
6
6
 
7
7
  // src/config/workspace.ts
8
8
  import { existsSync } from "fs";
@@ -764,6 +764,21 @@ import { mkdirSync } from "fs";
764
764
  import { dirname as dirname3 } from "path";
765
765
  import * as sqliteVec from "sqlite-vec";
766
766
 
767
+ // src/core/types.ts
768
+ function defaultProvenanceForKind(kind) {
769
+ switch (kind) {
770
+ case "git_commit":
771
+ case "code_diff":
772
+ case "shell_command":
773
+ return "observed";
774
+ case "conversation_turn":
775
+ case "session_summary":
776
+ case "doc_section":
777
+ case "note":
778
+ return "inferred";
779
+ }
780
+ }
781
+
767
782
  // src/core/ids.ts
768
783
  import { createHash } from "crypto";
769
784
  var KEY_SEP = "\0";
@@ -961,12 +976,21 @@ CREATE TABLE tombstones (
961
976
  );
962
977
  CREATE INDEX idx_tombstones_project ON tombstones (project_id, removed_at DESC);
963
978
  `;
979
+ var V6 = `
980
+ ALTER TABLE nodes ADD COLUMN provenance TEXT NOT NULL DEFAULT 'inferred';
981
+ ALTER TABLE nodes ADD COLUMN supersedes TEXT;
982
+
983
+ UPDATE nodes SET provenance = 'observed' WHERE kind IN ('git_commit', 'code_diff', 'shell_command');
984
+
985
+ CREATE INDEX idx_nodes_supersedes ON nodes (supersedes) WHERE supersedes IS NOT NULL;
986
+ `;
964
987
  var MIGRATIONS = [
965
988
  { version: 1, up: (db) => db.exec(V1) },
966
989
  { version: 2, up: (db) => db.exec(V2) },
967
990
  { version: 3, up: (db) => db.exec(V3) },
968
991
  { version: 4, up: (db) => db.exec(V4) },
969
- { version: 5, up: (db) => db.exec(V5) }
992
+ { version: 5, up: (db) => db.exec(V5) },
993
+ { version: 6, up: (db) => db.exec(V6) }
970
994
  ];
971
995
  var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
972
996
  function currentSchemaVersion(db) {
@@ -1058,11 +1082,12 @@ var MemoryStore = class _MemoryStore {
1058
1082
  "DELETE FROM nodes_vec WHERE rowid = (SELECT rowid FROM nodes WHERE id = ?)"
1059
1083
  );
1060
1084
  const insertNode = this.db.prepare(
1061
- `INSERT INTO nodes (id, kind, project_id, ts, ts_epoch, source, title, body, signal, meta, created_at)
1062
- VALUES (@id, @kind, @projectId, @ts, @tsEpoch, @source, @title, @body, @signal, @meta, @now)
1085
+ `INSERT INTO nodes (id, kind, project_id, ts, ts_epoch, source, title, body, signal, meta, provenance, supersedes, created_at)
1086
+ VALUES (@id, @kind, @projectId, @ts, @tsEpoch, @source, @title, @body, @signal, @meta, @provenance, @supersedes, @now)
1063
1087
  ON CONFLICT(id) DO UPDATE SET
1064
1088
  ts = excluded.ts, ts_epoch = excluded.ts_epoch, source = excluded.source,
1065
- title = excluded.title, body = excluded.body, signal = excluded.signal, meta = excluded.meta`
1089
+ title = excluded.title, body = excluded.body, signal = excluded.signal, meta = excluded.meta,
1090
+ provenance = excluded.provenance`
1066
1091
  );
1067
1092
  const clearFiles = this.db.prepare("DELETE FROM node_files WHERE node_id = ?");
1068
1093
  const insertFile = this.db.prepare(
@@ -1108,6 +1133,8 @@ var MemoryStore = class _MemoryStore {
1108
1133
  body: node.body,
1109
1134
  signal: node.signal,
1110
1135
  meta: JSON.stringify(node.meta),
1136
+ provenance: node.provenance ?? defaultProvenanceForKind(node.kind),
1137
+ supersedes: node.supersedes ?? null,
1111
1138
  now
1112
1139
  });
1113
1140
  clearFiles.run(node.id);
@@ -1194,7 +1221,7 @@ var MemoryStore = class _MemoryStore {
1194
1221
  getNodesByIds(ids) {
1195
1222
  if (ids.length === 0) return [];
1196
1223
  return this.db.prepare(
1197
- `SELECT id, kind, project_id AS projectId, ts, title, body, signal
1224
+ `SELECT id, kind, project_id AS projectId, ts, title, body, signal, provenance
1198
1225
  FROM nodes WHERE id IN (SELECT value FROM json_each(?))`
1199
1226
  ).all(JSON.stringify(ids));
1200
1227
  }
@@ -1206,7 +1233,7 @@ var MemoryStore = class _MemoryStore {
1206
1233
  */
1207
1234
  listRecentNodes(projectId, limit = 20) {
1208
1235
  return this.db.prepare(
1209
- `SELECT id, kind, ts, source, title, signal
1236
+ `SELECT id, kind, ts, source, title, signal, provenance
1210
1237
  FROM nodes
1211
1238
  WHERE project_id = ?
1212
1239
  ORDER BY ts_epoch DESC
@@ -1494,7 +1521,7 @@ var MemoryStore = class _MemoryStore {
1494
1521
  vectorSearch(projectId, embedding, limit = 20) {
1495
1522
  const overfetch = Math.max(limit * 8, 50);
1496
1523
  return this.db.prepare(
1497
- `SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, v.distance AS distance
1524
+ `SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, v.distance AS distance
1498
1525
  FROM nodes_vec v
1499
1526
  JOIN nodes n ON n.rowid = v.rowid
1500
1527
  WHERE v.embedding MATCH ? AND k = ? AND n.project_id = ?
@@ -1529,7 +1556,7 @@ var MemoryStore = class _MemoryStore {
1529
1556
  const match = toMatchQuery(query);
1530
1557
  if (!match) return [];
1531
1558
  const rows = this.db.prepare(
1532
- `SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal,
1559
+ `SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance,
1533
1560
  bm25(nodes_fts, 10.0, 1.0) AS rank
1534
1561
  FROM nodes_fts
1535
1562
  JOIN nodes n ON n.rowid = nodes_fts.rowid
@@ -1539,6 +1566,20 @@ var MemoryStore = class _MemoryStore {
1539
1566
  ).all(match, projectId, limit);
1540
1567
  return rows;
1541
1568
  }
1569
+ /** The project a node belongs to, or null if no node has this id. Used by `mark-stale` to validate both ids. */
1570
+ getNodeProjectId(id) {
1571
+ const row = this.db.prepare("SELECT project_id FROM nodes WHERE id = ?").get(id);
1572
+ return row?.project_id ?? null;
1573
+ }
1574
+ /** Every node id some other node's `supersedes` points at, for one project -- what the ranker should down-weight. */
1575
+ getSupersededIds(projectId) {
1576
+ const rows = this.db.prepare("SELECT DISTINCT supersedes AS id FROM nodes WHERE project_id = ? AND supersedes IS NOT NULL").all(projectId);
1577
+ return new Set(rows.map((r) => r.id));
1578
+ }
1579
+ /** Record that `newNodeId` supersedes `staleNodeId` -- the write behind `nexusmem mark-stale`. Caller validates both ids first. */
1580
+ setSupersedes(newNodeId, staleNodeId) {
1581
+ this.db.prepare("UPDATE nodes SET supersedes = ? WHERE id = ?").run(staleNodeId, newNodeId);
1582
+ }
1542
1583
  /** Escape hatch for tests and future modules. */
1543
1584
  get raw() {
1544
1585
  return this.db;
@@ -1930,8 +1971,47 @@ async function runInit(opts) {
1930
1971
  return 0;
1931
1972
  }
1932
1973
 
1933
- // src/cli/commands/projects.ts
1974
+ // src/cli/commands/mark-stale.ts
1934
1975
  import pc5 from "picocolors";
1976
+ var MarkStaleError = class extends Error {
1977
+ constructor(message) {
1978
+ super(message);
1979
+ this.name = "MarkStaleError";
1980
+ }
1981
+ };
1982
+ async function runMarkStale(opts) {
1983
+ const { projectId, ws } = await loadContext(opts.cwd);
1984
+ const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
1985
+ if (opts.nodeId === opts.supersedesId) {
1986
+ throw new MarkStaleError("a node cannot supersede itself");
1987
+ }
1988
+ const store = MemoryStore.open(ws.dbPath);
1989
+ try {
1990
+ const staleProject = store.getNodeProjectId(opts.nodeId);
1991
+ if (staleProject === null) {
1992
+ throw new MarkStaleError(`no node found with id ${opts.nodeId}`);
1993
+ }
1994
+ const supersedingProject = store.getNodeProjectId(opts.supersedesId);
1995
+ if (supersedingProject === null) {
1996
+ throw new MarkStaleError(`no node found with id ${opts.supersedesId}`);
1997
+ }
1998
+ if (staleProject !== projectId || supersedingProject !== projectId) {
1999
+ throw new MarkStaleError("both nodes must belong to the current project");
2000
+ }
2001
+ store.setSupersedes(opts.supersedesId, opts.nodeId);
2002
+ out(
2003
+ `${pc5.green("marked stale")} ${opts.nodeId}
2004
+ ${pc5.dim("superseded by")} ${opts.supersedesId} -- the old node stays queryable, just ranked lower
2005
+ `
2006
+ );
2007
+ return 0;
2008
+ } finally {
2009
+ store.close();
2010
+ }
2011
+ }
2012
+
2013
+ // src/cli/commands/projects.ts
2014
+ import pc6 from "picocolors";
1935
2015
  async function runProjects(opts) {
1936
2016
  const { entries, missing } = await readLiveRegistry();
1937
2017
  const rows = entries.map((entry) => {
@@ -1950,7 +2030,7 @@ async function runProjects(opts) {
1950
2030
  });
1951
2031
  if (opts.prune) {
1952
2032
  const removed = await forgetProjects(missing.map((entry) => entry.projectId));
1953
- process.stderr.write(`${pc5.yellow("pruned")} ${removed} project(s) whose database is gone
2033
+ process.stderr.write(`${pc6.yellow("pruned")} ${removed} project(s) whose database is gone
1954
2034
  `);
1955
2035
  }
1956
2036
  if (opts.json) {
@@ -1958,28 +2038,28 @@ async function runProjects(opts) {
1958
2038
  `);
1959
2039
  return 0;
1960
2040
  }
1961
- process.stderr.write(`${pc5.dim("registry")} ${registryPath()}
2041
+ process.stderr.write(`${pc6.dim("registry")} ${registryPath()}
1962
2042
 
1963
2043
  `);
1964
2044
  if (rows.length === 0) {
1965
- process.stderr.write(`${pc5.yellow("no projects registered")} -- run ${pc5.bold("nexusmem sync")} in a repository
2045
+ process.stderr.write(`${pc6.yellow("no projects registered")} -- run ${pc6.bold("nexusmem sync")} in a repository
1966
2046
  `);
1967
2047
  return 0;
1968
2048
  }
1969
2049
  for (const row of rows) {
1970
2050
  const seen = new Date(row.lastSeenAt).toISOString().slice(0, 16).replace("T", " ");
1971
- const count = row.nodes === null ? pc5.yellow("unreadable") : `${row.nodes} node(s)`;
1972
- process.stdout.write(`${pc5.cyan(row.projectId.slice(0, 8))} ${row.root}
1973
- ${pc5.dim(`${count}, last seen ${seen}`)}
2051
+ const count = row.nodes === null ? pc6.yellow("unreadable") : `${row.nodes} node(s)`;
2052
+ process.stdout.write(`${pc6.cyan(row.projectId.slice(0, 8))} ${row.root}
2053
+ ${pc6.dim(`${count}, last seen ${seen}`)}
1974
2054
  `);
1975
2055
  }
1976
2056
  if (!opts.prune && missing.length > 0) {
1977
2057
  process.stderr.write(
1978
2058
  `
1979
- ${pc5.yellow(`${missing.length} registered project(s) have no database on disk`)} ${pc5.dim("-- run with --prune to forget them")}
2059
+ ${pc6.yellow(`${missing.length} registered project(s) have no database on disk`)} ${pc6.dim("-- run with --prune to forget them")}
1980
2060
  `
1981
2061
  );
1982
- for (const entry of missing) process.stderr.write(` ${pc5.dim(entry.root)}
2062
+ for (const entry of missing) process.stderr.write(` ${pc6.dim(entry.root)}
1983
2063
  `);
1984
2064
  }
1985
2065
  return 0;
@@ -2148,6 +2228,7 @@ function packContext(ranked, tokensBudget, opts = {}) {
2148
2228
  score: hit.score,
2149
2229
  summary,
2150
2230
  tokens,
2231
+ provenance: hit.provenance,
2151
2232
  ...hit.project ? { project: hit.project } : {}
2152
2233
  });
2153
2234
  tokensUsed += tokens;
@@ -2160,7 +2241,8 @@ function renderContextBlock(query, result) {
2160
2241
  const lines = [`Relevant history for: ${query}`, ""];
2161
2242
  for (const node of result.nodes) {
2162
2243
  const project = node.project ? `[${node.project}] ` : "";
2163
- lines.push(`- ${node.ts.slice(0, 10)} ${project}${node.title}`);
2244
+ const provenance = `[${node.provenance}] `;
2245
+ lines.push(`- ${node.ts.slice(0, 10)} ${provenance}${project}${node.title}`);
2164
2246
  if (node.summary && node.summary !== node.title) {
2165
2247
  if (node.kind === "code_diff") {
2166
2248
  for (const line of node.summary.split("\n")) lines.push(` ${line}`);
@@ -2290,7 +2372,16 @@ function mergeSearchAndVectorHits(bm25Hits, vectorHits) {
2290
2372
  for (const hit of bm25Hits) byId.set(hit.id, hit);
2291
2373
  for (const hit of vectorHits) {
2292
2374
  if (byId.has(hit.id)) continue;
2293
- byId.set(hit.id, { id: hit.id, kind: hit.kind, ts: hit.ts, title: hit.title, body: hit.body, signal: hit.signal, rank: 0 });
2375
+ byId.set(hit.id, {
2376
+ id: hit.id,
2377
+ kind: hit.kind,
2378
+ ts: hit.ts,
2379
+ title: hit.title,
2380
+ body: hit.body,
2381
+ signal: hit.signal,
2382
+ provenance: hit.provenance,
2383
+ rank: 0
2384
+ });
2294
2385
  }
2295
2386
  return [...byId.values()];
2296
2387
  }
@@ -2301,6 +2392,7 @@ var SIGNAL_FLOOR = 0.2;
2301
2392
  var RECENCY_FLOOR = 0.3;
2302
2393
  var DEFAULT_HALF_LIFE_DAYS = 30;
2303
2394
  var MS_PER_DAY = 864e5;
2395
+ var SUPERSEDED_PENALTY = 0.5;
2304
2396
  var MAX_PRIOR_OVERTURN = 2;
2305
2397
  var PRIOR_COUNT = 2;
2306
2398
  var PER_PRIOR_OVERTURN = MAX_PRIOR_OVERTURN ** (1 / PRIOR_COUNT);
@@ -2341,7 +2433,8 @@ function rankHits(hits, opts = {}) {
2341
2433
  const signalWeight = SIGNAL_FLOOR + (1 - SIGNAL_FLOOR) * hit.signal;
2342
2434
  const ageDays = ageDaysOf(hit.ts, now);
2343
2435
  const recencyFactor = RECENCY_FLOOR + (1 - RECENCY_FLOOR) * 2 ** (-ageDays / halfLife);
2344
- const score = relevance * signalWeight ** SIGNAL_EXPONENT * recencyFactor ** RECENCY_EXPONENT;
2436
+ const rawScore = relevance * signalWeight ** SIGNAL_EXPONENT * recencyFactor ** RECENCY_EXPONENT;
2437
+ const score = opts.supersededIds?.has(hit.id) ? rawScore * SUPERSEDED_PENALTY : rawScore;
2345
2438
  return { ...hit, relevance, signalWeight, ageDays, recencyFactor, score };
2346
2439
  });
2347
2440
  return ranked.sort((a, b) => b.score - a.score);
@@ -2370,6 +2463,7 @@ function pullLinkedResolutions(resolveStore, ranked) {
2370
2463
  title: resolution.title,
2371
2464
  body: resolution.body,
2372
2465
  signal: resolution.signal,
2466
+ provenance: resolution.provenance,
2373
2467
  rank: 0,
2374
2468
  // no bm25/vector rank of its own -- never read again past this point
2375
2469
  relevance: hit.relevance,
@@ -2391,6 +2485,7 @@ async function runCrossProjectQuery(sources, query, opts) {
2391
2485
  const perProject = [];
2392
2486
  let bm25Count = 0;
2393
2487
  let vectorCount = 0;
2488
+ const supersededIds = /* @__PURE__ */ new Set();
2394
2489
  for (const source of sources) {
2395
2490
  const label = (hit) => ({ ...hit, project: source.label });
2396
2491
  const bm25Hits = source.store.search(source.projectId, query, opts.candidates).map(label);
@@ -2403,12 +2498,13 @@ async function runCrossProjectQuery(sources, query, opts) {
2403
2498
  lists.push(vectorHits.map((hit) => ({ ...hit, rank: 0, project: source.label })));
2404
2499
  }
2405
2500
  hits.push(...mergeSearchAndVectorHits(bm25Hits, vectorHits).map(label));
2501
+ for (const id of source.store.getSupersededIds(source.projectId)) supersededIds.add(id);
2406
2502
  }
2407
2503
  const storeByLabel = new Map(sources.map((source) => [source.label, source.store]));
2408
2504
  const relevanceScores = reciprocalRankFusion(lists);
2409
2505
  const ranked = pullLinkedResolutions(
2410
2506
  (hit) => hit.project ? storeByLabel.get(hit.project) : void 0,
2411
- rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores })
2507
+ rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores, supersededIds })
2412
2508
  );
2413
2509
  const packed = packContext(ranked, opts.budget, { query });
2414
2510
  return { bm25Count, vectorCount, hits, packed, perProject };
@@ -2422,9 +2518,10 @@ async function runHybridQuery(store, projectId, query, opts) {
2422
2518
  }
2423
2519
  const hits = vectorHits.length > 0 ? mergeSearchAndVectorHits(bm25Hits, vectorHits) : bm25Hits;
2424
2520
  const relevanceScores = vectorHits.length > 0 ? reciprocalRankFusion([bm25Hits, vectorHits]) : void 0;
2521
+ const supersededIds = store.getSupersededIds(projectId);
2425
2522
  const ranked = pullLinkedResolutions(
2426
2523
  () => store,
2427
- rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores })
2524
+ rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores, supersededIds })
2428
2525
  );
2429
2526
  const packed = packContext(ranked, opts.budget, { query });
2430
2527
  return { bm25Count: bm25Hits.length, vectorCount: vectorHits.length, hits, packed };
@@ -2529,7 +2626,7 @@ var OllamaEmbeddingProvider = class {
2529
2626
  };
2530
2627
 
2531
2628
  // src/cli/commands/sync.ts
2532
- import pc6 from "picocolors";
2629
+ import pc7 from "picocolors";
2533
2630
 
2534
2631
  // src/conversation/chunk.ts
2535
2632
  var HEADING_LINE = /^#{1,6}\s+(.+)$/;
@@ -2664,6 +2761,8 @@ function toMemoryNodes(turn, projectId, opts = {}) {
2664
2761
  files: extractMentionedFiles(`${userRedacted.text}
2665
2762
  ${chunk2.text}`),
2666
2763
  signal: scoreConversationTurn(userRedacted.text, chunk2.text),
2764
+ provenance: "inferred",
2765
+ // discourse about what happened, not the event itself
2667
2766
  meta: {
2668
2767
  cwd: turn.cwd,
2669
2768
  source: turn.source,
@@ -3076,6 +3175,7 @@ function toMemoryNode(commit, projectId, opts = {}) {
3076
3175
  body: truncate(bodyParts.join("\n"), maxBody),
3077
3176
  files: keptFiles,
3078
3177
  signal: scoreCommit(commit),
3178
+ provenance: "observed",
3079
3179
  meta: {
3080
3180
  sha: commit.sha,
3081
3181
  shortSha: commit.shortSha,
@@ -3170,6 +3270,7 @@ function toMemoryNodes2(commit, projectId, opts = {}) {
3170
3270
  }
3171
3271
  ],
3172
3272
  signal: scoreFileDiff(commit.subject, file),
3273
+ provenance: "observed",
3173
3274
  meta: {
3174
3275
  sha: commit.sha,
3175
3276
  shortSha: commit.shortSha,
@@ -3233,6 +3334,8 @@ function toMemoryNodes3(file, projectId, opts = {}) {
3233
3334
  body: truncate(chunk2.text, maxBody),
3234
3335
  files: [{ path: file.path, insertions: null, deletions: null, binary: false }],
3235
3336
  signal: scoreDocSection(file.path, chunk2.heading, chunk2.text),
3337
+ provenance: "inferred",
3338
+ // a written claim, and the kind of content most likely to go stale
3236
3339
  meta: {
3237
3340
  path: file.path,
3238
3341
  heading: chunk2.heading,
@@ -3394,6 +3497,8 @@ ${summary.body}`;
3394
3497
  files: extractMentionedFiles(session.turns.map((t) => `${t.userText}
3395
3498
  ${t.assistantText}`).join("\n")),
3396
3499
  signal: scoreSession(session.turns.length),
3500
+ provenance: "inferred",
3501
+ // a model's distillation, not a directly observed event
3397
3502
  meta: {
3398
3503
  sessionKey: session.sessionKey,
3399
3504
  source: session.source,
@@ -3502,6 +3607,7 @@ function toMemoryNode2(entry, projectId, opts = {}) {
3502
3607
  body: renderBody(entry, maxBody),
3503
3608
  files: [],
3504
3609
  signal: scoreShellCommand(entry),
3610
+ provenance: "observed",
3505
3611
  meta: {
3506
3612
  command: entry.command,
3507
3613
  cwd: entry.cwd,
@@ -4144,25 +4250,25 @@ function addStats(into, from) {
4144
4250
  async function syncGit(store, projectId, opts, repo, config, log) {
4145
4251
  const totals = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
4146
4252
  if (!repo.head) {
4147
- log(`${pc6.yellow("git")} skipped -- repository has no commits yet`);
4253
+ log(`${pc7.yellow("git")} skipped -- repository has no commits yet`);
4148
4254
  return { totals, seen: 0 };
4149
4255
  }
4150
4256
  if (!config.sources.git.enabled) {
4151
- log(`${pc6.dim("git")} disabled in config`);
4257
+ log(`${pc7.dim("git")} disabled in config`);
4152
4258
  return { totals, seen: 0 };
4153
4259
  }
4154
4260
  let cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, GIT_SOURCE);
4155
4261
  if (cursor && !await isAncestor(repo.root, cursor, repo.head)) {
4156
- log(`${pc6.yellow("git cursor stale")} ${cursor.slice(0, 7)} is not an ancestor of HEAD \u2014 falling back to a full walk`);
4262
+ log(`${pc7.yellow("git cursor stale")} ${cursor.slice(0, 7)} is not an ancestor of HEAD \u2014 falling back to a full walk`);
4157
4263
  cursor = null;
4158
4264
  }
4159
4265
  if (cursor === repo.head) {
4160
- log(`${pc6.green("git up to date")} at ${repo.head.slice(0, 7)}`);
4266
+ log(`${pc7.green("git up to date")} at ${repo.head.slice(0, 7)}`);
4161
4267
  store.setSyncCursor(projectId, GIT_SOURCE, repo.head);
4162
4268
  return { totals, seen: 0 };
4163
4269
  }
4164
4270
  log(
4165
- `${pc6.dim("git syncing")} ${repo.branch ?? "HEAD"} ${cursor ? `${cursor.slice(0, 7)}..${repo.head.slice(0, 7)}` : "(full history)"}`
4271
+ `${pc7.dim("git syncing")} ${repo.branch ?? "HEAD"} ${cursor ? `${cursor.slice(0, 7)}..${repo.head.slice(0, 7)}` : "(full history)"}`
4166
4272
  );
4167
4273
  let batch = [];
4168
4274
  let seen = 0;
@@ -4170,7 +4276,7 @@ async function syncGit(store, projectId, opts, repo, config, log) {
4170
4276
  if (batch.length === 0) return;
4171
4277
  addStats(totals, store.upsertNodes(batch));
4172
4278
  batch = [];
4173
- log(` ${pc6.dim(`${seen} commits read, ${totals.inserted} new`)}`);
4279
+ log(` ${pc7.dim(`${seen} commits read, ${totals.inserted} new`)}`);
4174
4280
  };
4175
4281
  const nodes = collectGitCommits(repo.root, projectId, {
4176
4282
  afterCommit: cursor,
@@ -4192,12 +4298,12 @@ async function syncDiffs(store, projectId, opts, repo, config, log) {
4192
4298
  const totals = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
4193
4299
  if (!repo.head) return { totals, seen: 0 };
4194
4300
  if (!config.sources.diff.enabled) {
4195
- log(`${pc6.dim("diff")} disabled in config`);
4301
+ log(`${pc7.dim("diff")} disabled in config`);
4196
4302
  return { totals, seen: 0 };
4197
4303
  }
4198
4304
  let cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, DIFF_SOURCE);
4199
4305
  if (cursor && !await isAncestor(repo.root, cursor, repo.head)) {
4200
- log(`${pc6.yellow("diff cursor stale")} ${cursor.slice(0, 7)} is not an ancestor of HEAD \u2014 falling back to a bounded walk`);
4306
+ log(`${pc7.yellow("diff cursor stale")} ${cursor.slice(0, 7)} is not an ancestor of HEAD \u2014 falling back to a bounded walk`);
4201
4307
  cursor = null;
4202
4308
  }
4203
4309
  if (cursor === repo.head) {
@@ -4226,13 +4332,13 @@ async function syncDiffs(store, projectId, opts, repo, config, log) {
4226
4332
  }
4227
4333
  flush();
4228
4334
  store.setSyncCursor(projectId, DIFF_SOURCE, repo.head);
4229
- log(` ${pc6.dim(`${DIFF_SOURCE}: ${seen} file diff(s) read`)}`);
4335
+ log(` ${pc7.dim(`${DIFF_SOURCE}: ${seen} file diff(s) read`)}`);
4230
4336
  return { totals, seen };
4231
4337
  }
4232
4338
  async function syncShell(store, projectId, opts, repoRoot, config, log) {
4233
4339
  const totals = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
4234
4340
  if (!config.sources.shell.enabled) {
4235
- log(`${pc6.dim("shell")} disabled in config`);
4341
+ log(`${pc7.dim("shell")} disabled in config`);
4236
4342
  return { totals, seen: 0 };
4237
4343
  }
4238
4344
  const results = await collectAvailableShellHistory({
@@ -4241,7 +4347,7 @@ async function syncShell(store, projectId, opts, repoRoot, config, log) {
4241
4347
  hookCursor: store.getSyncCursor(projectId, "shell:pwsh-hook")
4242
4348
  });
4243
4349
  if (results.length === 0) {
4244
- log(`${pc6.dim("shell")} no history source found on this machine`);
4350
+ log(`${pc7.dim("shell")} no history source found on this machine`);
4245
4351
  return { totals, seen: 0 };
4246
4352
  }
4247
4353
  let seen = 0;
@@ -4253,7 +4359,7 @@ async function syncShell(store, projectId, opts, repoRoot, config, log) {
4253
4359
  addStats(totals, store.upsertNodes(nodes));
4254
4360
  }
4255
4361
  store.setSyncCursor(projectId, sourceKey, result.cursorAfter ?? `scanned:${result.entries.length}`);
4256
- log(` ${pc6.dim(`${sourceKey}: ${nodes.length} entr${nodes.length === 1 ? "y" : "ies"} read`)}`);
4362
+ log(` ${pc7.dim(`${sourceKey}: ${nodes.length} entr${nodes.length === 1 ? "y" : "ies"} read`)}`);
4257
4363
  }
4258
4364
  return { totals, seen };
4259
4365
  }
@@ -4265,13 +4371,13 @@ function syncConversation(store, projectId, turns, config, log, forceEnabled) {
4265
4371
  return { totals, seen: 0 };
4266
4372
  }
4267
4373
  if (turns.length === 0) {
4268
- log(`${pc6.dim("conversation")} no transcripts found`);
4374
+ log(`${pc7.dim("conversation")} no transcripts found`);
4269
4375
  return { totals, seen: 0 };
4270
4376
  }
4271
4377
  const nodes = collectConversationTurns(turns, projectId, { maxBodyChars: config.limits.maxBodyChars });
4272
4378
  if (nodes.length > 0) addStats(totals, store.upsertNodes(nodes));
4273
4379
  store.setSyncCursor(projectId, CONVERSATION_SOURCE, `scanned:${nodes.length}`);
4274
- log(` ${pc6.dim(`${CONVERSATION_SOURCE}: ${nodes.length} of ${turns.length} exchange(s) kept`)}`);
4380
+ log(` ${pc7.dim(`${CONVERSATION_SOURCE}: ${nodes.length} of ${turns.length} exchange(s) kept`)}`);
4275
4381
  return { totals, seen: nodes.length };
4276
4382
  }
4277
4383
  var SESSION_SOURCE = "session:claude-code";
@@ -4280,7 +4386,7 @@ async function syncSessions(store, projectId, turns, config, log) {
4280
4386
  const settings = config.sources.session;
4281
4387
  if (!settings.enabled) return { totals, seen: 0 };
4282
4388
  if (turns.length === 0) {
4283
- log(`${pc6.dim("session")} no transcripts found`);
4389
+ log(`${pc7.dim("session")} no transcripts found`);
4284
4390
  return { totals, seen: 0 };
4285
4391
  }
4286
4392
  const result = await collectSessionSummaries(turns, projectId, new OllamaChatProvider({ model: settings.model }), {
@@ -4292,12 +4398,12 @@ async function syncSessions(store, projectId, turns, config, log) {
4292
4398
  const meta = store.getNodeMeta(makeNodeId(projectId, "session_summary", sessionKey));
4293
4399
  return typeof meta?.contentHash === "string" ? meta.contentHash : null;
4294
4400
  },
4295
- onProgress: (done, total) => log(` ${pc6.dim(`session: summarizing ${done}/${total}`)}`)
4401
+ onProgress: (done, total) => log(` ${pc7.dim(`session: summarizing ${done}/${total}`)}`)
4296
4402
  });
4297
4403
  if (result.nodes.length > 0) addStats(totals, store.upsertNodes(result.nodes));
4298
4404
  if (result.providerUnavailable) {
4299
4405
  log(
4300
- `${pc6.dim("session")} summarization model unavailable (is Ollama running with \`${settings.model}\` pulled?) -- skipped`
4406
+ `${pc7.dim("session")} summarization model unavailable (is Ollama running with \`${settings.model}\` pulled?) -- skipped`
4301
4407
  );
4302
4408
  } else {
4303
4409
  const parts = [`${result.nodes.length} summarized`];
@@ -4305,7 +4411,7 @@ async function syncSessions(store, projectId, turns, config, log) {
4305
4411
  if (result.deferred > 0) parts.push(`${result.deferred} queued for the next sync`);
4306
4412
  if (result.unsettled > 0) parts.push(`${result.unsettled} still active`);
4307
4413
  if (result.failed > 0) parts.push(`${result.failed} failed`);
4308
- log(` ${pc6.dim(`${SESSION_SOURCE}: ${parts.join(", ")}`)}`);
4414
+ log(` ${pc7.dim(`${SESSION_SOURCE}: ${parts.join(", ")}`)}`);
4309
4415
  }
4310
4416
  store.setSyncCursor(projectId, SESSION_SOURCE, `scanned:${result.nodes.length}`);
4311
4417
  return { totals, seen: result.nodes.length };
@@ -4314,7 +4420,7 @@ var DOCS_SOURCE = "docs";
4314
4420
  async function syncDocs(store, projectId, repoRoot, config, log) {
4315
4421
  const totals = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
4316
4422
  if (!config.sources.docs.enabled) {
4317
- log(`${pc6.dim("docs")} disabled in config`);
4423
+ log(`${pc7.dim("docs")} disabled in config`);
4318
4424
  return { totals, seen: 0 };
4319
4425
  }
4320
4426
  const { files, unreadable } = await readDocFiles(repoRoot, { include: config.sources.docs.include });
@@ -4328,23 +4434,23 @@ async function syncDocs(store, projectId, repoRoot, config, log) {
4328
4434
  );
4329
4435
  store.setSyncCursor(projectId, DOCS_SOURCE, `scanned:${nodes.length}`);
4330
4436
  if (files.length === 0 && unreadable.length === 0) {
4331
- log(`${pc6.dim("docs")} no tracked .md files found`);
4437
+ log(`${pc7.dim("docs")} no tracked .md files found`);
4332
4438
  } else {
4333
- const prunedPart = pruned > 0 ? `, ${pc6.yellow(`${pruned} stale removed`)}` : "";
4439
+ const prunedPart = pruned > 0 ? `, ${pc7.yellow(`${pruned} stale removed`)}` : "";
4334
4440
  const skippedPart = unreadable.length > 0 ? `, ${unreadable.length} unreadable (kept)` : "";
4335
- log(` ${pc6.dim(`${DOCS_SOURCE}: ${nodes.length} section(s) from ${files.length} file(s)`)}${prunedPart}${pc6.dim(skippedPart)}`);
4441
+ log(` ${pc7.dim(`${DOCS_SOURCE}: ${nodes.length} section(s) from ${files.length} file(s)`)}${prunedPart}${pc7.dim(skippedPart)}`);
4336
4442
  }
4337
4443
  return { totals, seen: nodes.length };
4338
4444
  }
4339
4445
  async function syncStructure(store, projectId, repoRoot, config, log) {
4340
4446
  if (!config.sources.structure.enabled) {
4341
- log(`${pc6.dim("structure")} disabled in config`);
4447
+ log(`${pc7.dim("structure")} disabled in config`);
4342
4448
  return { edges: 0, filesScanned: 0 };
4343
4449
  }
4344
4450
  const { edges, filesScanned, unreadable } = await collectFileEdges(repoRoot);
4345
4451
  store.replaceFileEdges(projectId, edges);
4346
4452
  const skippedPart = unreadable.length > 0 ? `, ${unreadable.length} unreadable (skipped)` : "";
4347
- log(` ${pc6.dim(`structure: ${edges.length} edge(s) from ${filesScanned} file(s)`)}${pc6.dim(skippedPart)}`);
4453
+ log(` ${pc7.dim(`structure: ${edges.length} edge(s) from ${filesScanned} file(s)`)}${pc7.dim(skippedPart)}`);
4348
4454
  return { edges: edges.length, filesScanned };
4349
4455
  }
4350
4456
  var STALE_SHELL_SOURCES = ["shell:pwsh", "shell:bash", "shell:zsh"];
@@ -4361,15 +4467,15 @@ function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
4361
4467
  const counts = sources.flatMap((source) => scopeIds.map((id) => ({ source, id, count: store.countSourceNodes(id, source) })));
4362
4468
  const total = counts.reduce((sum, c) => sum + c.count, 0);
4363
4469
  if (total === 0) {
4364
- out(`${pc6.dim("prune-source")} no node(s) match ${sources.join(", ")} -- nothing to do
4470
+ out(`${pc7.dim("prune-source")} no node(s) match ${sources.join(", ")} -- nothing to do
4365
4471
  `);
4366
4472
  return 0;
4367
4473
  }
4368
- const describe = (c) => ` ${pc6.dim(c.source)}${c.id !== projectId ? pc6.dim(` (prior identity ${c.id.slice(0, 8)})`) : ""}: ${c.count} node(s)`;
4474
+ const describe = (c) => ` ${pc7.dim(c.source)}${c.id !== projectId ? pc7.dim(` (prior identity ${c.id.slice(0, 8)})`) : ""}: ${c.count} node(s)`;
4369
4475
  if (!yes) {
4370
4476
  const lines = counts.filter((c) => c.count > 0).map(describe);
4371
4477
  out(
4372
- [`${pc6.yellow("would remove")} ${total} node(s):`, ...lines, pc6.dim("re-run with --yes to actually delete these -- this cannot be undone"), ""].join(
4478
+ [`${pc7.yellow("would remove")} ${total} node(s):`, ...lines, pc7.dim("re-run with --yes to actually delete these -- this cannot be undone"), ""].join(
4373
4479
  "\n"
4374
4480
  )
4375
4481
  );
@@ -4378,7 +4484,7 @@ function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
4378
4484
  let removed = 0;
4379
4485
  for (const { source, id } of counts) removed += store.pruneSourceNodes(id, source, []);
4380
4486
  const identityPart = otherProjectIds.length > 0 ? `, ${scopeIds.length} project identit${scopeIds.length === 1 ? "y" : "ies"}` : "";
4381
- out(`${pc6.green("pruned")} ${removed} node(s) across ${sources.length} source(s)${identityPart}
4487
+ out(`${pc7.green("pruned")} ${removed} node(s) across ${sources.length} source(s)${identityPart}
4382
4488
  `);
4383
4489
  return 0;
4384
4490
  }
@@ -4395,7 +4501,7 @@ async function runSync(opts) {
4395
4501
  store.upsertProject({ id: projectId, root: repo.root, originUrl: repo.originUrl });
4396
4502
  if (opts.rebuild) {
4397
4503
  const removed = store.clearProject(projectId);
4398
- log(`${pc6.dim("rebuild")} dropped ${removed} existing node(s)`);
4504
+ log(`${pc7.dim("rebuild")} dropped ${removed} existing node(s)`);
4399
4505
  }
4400
4506
  const staleProjectIds = store.listOtherProjectIds(projectId);
4401
4507
  for (const staleId of staleProjectIds) {
@@ -4409,7 +4515,7 @@ async function runSync(opts) {
4409
4515
  ].filter((part) => part !== null);
4410
4516
  if (parts.length > 0) {
4411
4517
  log(
4412
- `${pc6.yellow("reconciled")} previous project identity ${pc6.dim(staleId)} (remote URL likely changed): ${parts.join(", ")}`
4518
+ `${pc7.yellow("reconciled")} previous project identity ${pc7.dim(staleId)} (remote URL likely changed): ${parts.join(", ")}`
4413
4519
  );
4414
4520
  }
4415
4521
  }
@@ -4439,26 +4545,26 @@ async function runSync(opts) {
4439
4545
  let lastLogged = 0;
4440
4546
  const result = await embedPendingNodes(store, new OllamaEmbeddingProvider(), projectId, {
4441
4547
  maxNodes: opts.embedLimit,
4442
- onInvalidated: (count) => log(`${pc6.yellow("vector")} embedding model changed -- dropped ${count} vector(s), re-embedding from scratch`),
4548
+ onInvalidated: (count) => log(`${pc7.yellow("vector")} embedding model changed -- dropped ${count} vector(s), re-embedding from scratch`),
4443
4549
  onProgress: (attempted, total) => {
4444
4550
  if (total < PROGRESS_THRESHOLD || attempted - lastLogged < PROGRESS_EVERY) return;
4445
4551
  lastLogged = attempted;
4446
- log(` ${pc6.dim(`vector: ${attempted}/${total} embedded`)}`);
4552
+ log(` ${pc7.dim(`vector: ${attempted}/${total} embedded`)}`);
4447
4553
  }
4448
4554
  });
4449
4555
  if (result.embedded > 0) {
4450
- const skippedPart = result.skipped > 0 ? pc6.dim(`, ${result.skipped} skipped`) : "";
4451
- const remainingPart = result.remaining > 0 ? pc6.yellow(`, ${result.remaining} still pending`) : "";
4452
- embedLine = ` ${pc6.dim(`vector: ${result.embedded} node(s) embedded`)}${skippedPart}${remainingPart}
4556
+ const skippedPart = result.skipped > 0 ? pc7.dim(`, ${result.skipped} skipped`) : "";
4557
+ const remainingPart = result.remaining > 0 ? pc7.yellow(`, ${result.remaining} still pending`) : "";
4558
+ embedLine = ` ${pc7.dim(`vector: ${result.embedded} node(s) embedded`)}${skippedPart}${remainingPart}
4453
4559
  `;
4454
4560
  } else if (result.providerUnavailable) {
4455
- log(`${pc6.dim("vector")} embedding provider unavailable (is Ollama running with nomic-embed-text pulled?) -- BM25-only for now`);
4561
+ log(`${pc7.dim("vector")} embedding provider unavailable (is Ollama running with nomic-embed-text pulled?) -- BM25-only for now`);
4456
4562
  }
4457
4563
  }
4458
4564
  let linkLine = "";
4459
4565
  if (opts.linkFailures) {
4460
4566
  const linkStats = correlateFailures(store, projectId);
4461
- linkLine = ` ${pc6.dim(`chains: ${linkStats.failuresExamined} failure(s) examined, ${linkStats.linkedByRetry} linked by retry, ${linkStats.linkedByDiscussion} by discussion`)}
4567
+ linkLine = ` ${pc7.dim(`chains: ${linkStats.failuresExamined} failure(s) examined, ${linkStats.linkedByRetry} linked by retry, ${linkStats.linkedByDiscussion} by discussion`)}
4462
4568
  `;
4463
4569
  }
4464
4570
  store.markSynced(projectId);
@@ -4476,12 +4582,12 @@ async function runSync(opts) {
4476
4582
  const docsPart = config.sources.docs.enabled ? `, ${docs.seen} doc section(s)` : "";
4477
4583
  const diffPart = config.sources.diff.enabled ? `, ${diffs.seen} file diff(s)` : "";
4478
4584
  const structurePart = config.sources.structure.enabled ? `, ${structure.edges} import edge(s)` : "";
4479
- const deniedPart = totals.denied > 0 ? ` ${pc6.red(`-${totals.denied} denied`)}` : "";
4585
+ const deniedPart = totals.denied > 0 ? ` ${pc7.red(`-${totals.denied} denied`)}` : "";
4480
4586
  out(
4481
4587
  [
4482
- `${pc6.green("synced")} ${git2.seen} commit(s)${diffPart}, ${shell.seen} shell entr${shell.seen === 1 ? "y" : "ies"}${conversationPart}${sessionPart}${docsPart}${structurePart} in ${elapsed}s`,
4483
- ` ${pc6.green(`+${totals.inserted} new`)} ${pc6.yellow(`~${totals.updated} updated`)} ${pc6.dim(`=${totals.unchanged} unchanged`)}${deniedPart}`,
4484
- ` ${pc6.dim(`${stats.total} node(s) total across ${stats.distinctFiles} file path(s)`)}`,
4588
+ `${pc7.green("synced")} ${git2.seen} commit(s)${diffPart}, ${shell.seen} shell entr${shell.seen === 1 ? "y" : "ies"}${conversationPart}${sessionPart}${docsPart}${structurePart} in ${elapsed}s`,
4589
+ ` ${pc7.green(`+${totals.inserted} new`)} ${pc7.yellow(`~${totals.updated} updated`)} ${pc7.dim(`=${totals.unchanged} unchanged`)}${deniedPart}`,
4590
+ ` ${pc7.dim(`${stats.total} node(s) total across ${stats.distinctFiles} file path(s)`)}`,
4485
4591
  ""
4486
4592
  ].join("\n") + embedLine + linkLine
4487
4593
  );
@@ -4677,7 +4783,7 @@ async function runMcpServer() {
4677
4783
  }
4678
4784
 
4679
4785
  // src/cli/commands/precheck.ts
4680
- import pc7 from "picocolors";
4786
+ import pc8 from "picocolors";
4681
4787
 
4682
4788
  // src/correlate/precheck.ts
4683
4789
  var DEFAULT_RECENT_DAYS = 30;
@@ -4734,7 +4840,7 @@ async function runPrecheck(opts) {
4734
4840
  const { repo, ws, projectId } = await loadContext(opts.cwd);
4735
4841
  const targetFiles = opts.files ?? (opts.working ? await workingTreeFiles(repo.root) : await stagedFiles(repo.root));
4736
4842
  if (targetFiles.length === 0) {
4737
- if (!opts.quiet) out(`${pc7.dim("precheck")} no files to check
4843
+ if (!opts.quiet) out(`${pc8.dim("precheck")} no files to check
4738
4844
  `);
4739
4845
  return 0;
4740
4846
  }
@@ -4747,45 +4853,45 @@ async function runPrecheck(opts) {
4747
4853
  }
4748
4854
  const flagged = risks.filter((r) => r.unresolvedFailures.length > 0 || r.commitsRecent >= HIGH_CHURN_THRESHOLD);
4749
4855
  if (flagged.length === 0) {
4750
- if (!opts.quiet) out(`${pc7.green("precheck")} no warnings \u2014 looking good
4856
+ if (!opts.quiet) out(`${pc8.green("precheck")} no warnings \u2014 looking good
4751
4857
  `);
4752
4858
  return 0;
4753
4859
  }
4754
4860
  out(`
4755
- ${pc7.bold("nexusmem precheck")}
4756
- ${pc7.dim("-".repeat(40))}
4861
+ ${pc8.bold("nexusmem precheck")}
4862
+ ${pc8.dim("-".repeat(40))}
4757
4863
 
4758
4864
  `);
4759
4865
  for (const risk of flagged) {
4760
- out(` ${pc7.bold(risk.path)}
4866
+ out(` ${pc8.bold(risk.path)}
4761
4867
  `);
4762
4868
  if (risk.unresolvedFailures.length > 0) {
4763
- out(` ${pc7.yellow("WARN")} What already failed here (${risk.unresolvedFailures.length} unresolved):
4869
+ out(` ${pc8.yellow("WARN")} What already failed here (${risk.unresolvedFailures.length} unresolved):
4764
4870
  `);
4765
4871
  for (const f of risk.unresolvedFailures.slice(0, 3)) {
4766
- out(` ${pc7.dim(`${f.ts.slice(0, 10)} ${f.command.slice(0, 90)}`)}
4872
+ out(` ${pc8.dim(`${f.ts.slice(0, 10)} ${f.command.slice(0, 90)}`)}
4767
4873
  `);
4768
4874
  }
4769
4875
  if (risk.unresolvedFailures.length > 3) {
4770
- out(` ${pc7.dim(`... and ${risk.unresolvedFailures.length - 3} more`)}
4876
+ out(` ${pc8.dim(`... and ${risk.unresolvedFailures.length - 3} more`)}
4771
4877
  `);
4772
4878
  }
4773
4879
  }
4774
4880
  if (risk.commitsRecent >= HIGH_CHURN_THRESHOLD) {
4775
- out(` ${pc7.yellow("WARN")} high churn: ${risk.commitsRecent} commits touched this file recently
4881
+ out(` ${pc8.yellow("WARN")} high churn: ${risk.commitsRecent} commits touched this file recently
4776
4882
  `);
4777
4883
  }
4778
4884
  out("\n");
4779
4885
  }
4780
4886
  const failureCount = flagged.filter((r) => r.unresolvedFailures.length > 0).length;
4781
- out(`${pc7.dim(`${flagged.length} file(s) flagged, ${failureCount} with unresolved failures.`)}
4887
+ out(`${pc8.dim(`${flagged.length} file(s) flagged, ${failureCount} with unresolved failures.`)}
4782
4888
  `);
4783
4889
  if (opts.strict && failureCount > 0) return 1;
4784
4890
  return 0;
4785
4891
  }
4786
4892
 
4787
4893
  // src/cli/commands/query.ts
4788
- import pc8 from "picocolors";
4894
+ import pc9 from "picocolors";
4789
4895
  async function runQuery(opts) {
4790
4896
  const { repo, ws, projectId } = await loadContext(opts.cwd);
4791
4897
  const opened = opts.allProjects ? await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath }) : null;
@@ -4807,15 +4913,15 @@ async function runQuery(opts) {
4807
4913
  const { bm25Count, vectorCount, hits, packed } = result;
4808
4914
  if (opened && !opts.json) {
4809
4915
  const searched = opened.sources.map((s) => s.label).join(", ");
4810
- process.stderr.write(`${pc8.dim("scope ")} ${opened.sources.length} project(s): ${searched}
4916
+ process.stderr.write(`${pc9.dim("scope ")} ${opened.sources.length} project(s): ${searched}
4811
4917
  `);
4812
4918
  for (const { entry } of opened.unreadable) {
4813
- process.stderr.write(`${pc8.yellow("unreadable")} ${entry.root} -- skipped
4919
+ process.stderr.write(`${pc9.yellow("unreadable")} ${entry.root} -- skipped
4814
4920
  `);
4815
4921
  }
4816
4922
  if (opened.missing.length > 0) {
4817
4923
  process.stderr.write(
4818
- `${pc8.dim("skipped")} ${opened.missing.length} registered project(s) whose database is not on disk ${pc8.dim("(nexusmem projects --prune to forget them)")}
4924
+ `${pc9.dim("skipped")} ${opened.missing.length} registered project(s) whose database is not on disk ${pc9.dim("(nexusmem projects --prune to forget them)")}
4819
4925
  `
4820
4926
  );
4821
4927
  }
@@ -4845,15 +4951,15 @@ async function runQuery(opts) {
4845
4951
  return 0;
4846
4952
  }
4847
4953
  if (matched === 0) {
4848
- process.stderr.write(`${pc8.yellow("no matches")} for "${opts.query}"
4954
+ process.stderr.write(`${pc9.yellow("no matches")} for "${opts.query}"
4849
4955
  `);
4850
4956
  return 0;
4851
4957
  }
4852
4958
  process.stderr.write(
4853
4959
  [
4854
- `${pc8.dim("matched")} ${bm25Count} bm25${vectorCount > 0 ? ` + ${vectorCount} vector` : ""}, packed ${pc8.bold(String(packed.nodes.length))} into budget`,
4855
- `${pc8.dim("tokens ")} ${packed.tokensUsed}/${packed.tokensBudget}` + (packed.droppedForBudget ? pc8.dim(` (${packed.droppedForBudget} dropped for budget)`) : "") + (packed.droppedForDiversity ? pc8.dim(` (${packed.droppedForDiversity} dropped for diversity)`) : ""),
4856
- rawTokens > 0 ? `${pc8.dim("vs raw ")} ${rawTokens} tokens if these same matches were sent unpacked ${packerEfficiency >= 0 ? pc8.green(`(${(packerEfficiency * 100).toFixed(0)}% packer efficiency)`) : pc8.yellow(`(${(-packerEfficiency * 100).toFixed(0)}% larger -- overhead dominates on small result sets)`)}` : "",
4960
+ `${pc9.dim("matched")} ${bm25Count} bm25${vectorCount > 0 ? ` + ${vectorCount} vector` : ""}, packed ${pc9.bold(String(packed.nodes.length))} into budget`,
4961
+ `${pc9.dim("tokens ")} ${packed.tokensUsed}/${packed.tokensBudget}` + (packed.droppedForBudget ? pc9.dim(` (${packed.droppedForBudget} dropped for budget)`) : "") + (packed.droppedForDiversity ? pc9.dim(` (${packed.droppedForDiversity} dropped for diversity)`) : ""),
4962
+ rawTokens > 0 ? `${pc9.dim("vs raw ")} ${rawTokens} tokens if these same matches were sent unpacked ${packerEfficiency >= 0 ? pc9.green(`(${(packerEfficiency * 100).toFixed(0)}% packer efficiency)`) : pc9.yellow(`(${(-packerEfficiency * 100).toFixed(0)}% larger -- overhead dominates on small result sets)`)}` : "",
4857
4963
  ""
4858
4964
  ].filter(Boolean).join("\n")
4859
4965
  );
@@ -4867,10 +4973,10 @@ async function runQuery(opts) {
4867
4973
  }
4868
4974
 
4869
4975
  // src/cli/commands/scan-conversation.ts
4870
- import pc10 from "picocolors";
4976
+ import pc11 from "picocolors";
4871
4977
 
4872
4978
  // src/cli/format.ts
4873
- import pc9 from "picocolors";
4979
+ import pc10 from "picocolors";
4874
4980
  var GIT_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
4875
4981
  var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
4876
4982
  var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
@@ -4882,9 +4988,9 @@ function signalBand(signal, bands) {
4882
4988
  return "low";
4883
4989
  }
4884
4990
  var BAND_COLOR = {
4885
- high: pc9.green,
4886
- medium: pc9.yellow,
4887
- low: pc9.dim
4991
+ high: pc10.green,
4992
+ medium: pc10.yellow,
4993
+ low: pc10.dim
4888
4994
  };
4889
4995
  function formatSignal(signal, bands) {
4890
4996
  return BAND_COLOR[signalBand(signal, bands)](signal.toFixed(2));
@@ -4897,9 +5003,9 @@ async function runScanConversation(opts) {
4897
5003
  const files = await listTranscriptFiles(repo.root);
4898
5004
  if (!opts.json) {
4899
5005
  process.stderr.write(
4900
- files.length ? `${pc10.dim("transcripts")} ${files.length} file(s) in ${claudeProjectTranscriptDir(repo.root)}
5006
+ files.length ? `${pc11.dim("transcripts")} ${files.length} file(s) in ${claudeProjectTranscriptDir(repo.root)}
4901
5007
 
4902
- ` : `${pc10.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
5008
+ ` : `${pc11.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
4903
5009
  `
4904
5010
  );
4905
5011
  }
@@ -4916,7 +5022,7 @@ async function runScanConversation(opts) {
4916
5022
  const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
4917
5023
  process.stderr.write(
4918
5024
  `
4919
- ${pc10.bold(String(nodes.length))} of ${turns.length} exchange(s) above threshold ${pc10.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}` + (redactedTotal > 0 ? ` ${pc10.yellow(`${redactedTotal} secret-like value(s) redacted`)}` : "") + "\n"
5025
+ ${pc11.bold(String(nodes.length))} of ${turns.length} exchange(s) above threshold ${pc11.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}` + (redactedTotal > 0 ? ` ${pc11.yellow(`${redactedTotal} secret-like value(s) redacted`)}` : "") + "\n"
4920
5026
  );
4921
5027
  return 0;
4922
5028
  }
@@ -4925,20 +5031,20 @@ function formatNode(node) {
4925
5031
  }
4926
5032
 
4927
5033
  // src/cli/commands/scan-diff.ts
4928
- import pc12 from "picocolors";
5034
+ import pc13 from "picocolors";
4929
5035
 
4930
5036
  // src/cli/commands/scan-git.ts
4931
- import pc11 from "picocolors";
5037
+ import pc12 from "picocolors";
4932
5038
  async function runScanGit(opts) {
4933
5039
  const repo = await readRepoInfo(opts.cwd);
4934
5040
  const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
4935
5041
  if (!opts.json) {
4936
5042
  process.stderr.write(
4937
5043
  [
4938
- `${pc11.dim("repo ")} ${repo.root}`,
4939
- `${pc11.dim("branch ")} ${repo.branch ?? pc11.yellow("(detached)")}`,
4940
- `${pc11.dim("origin ")} ${repo.originUrl ?? pc11.dim("(none)")}`,
4941
- `${pc11.dim("project")} ${pc11.cyan(projectId)}`,
5044
+ `${pc12.dim("repo ")} ${repo.root}`,
5045
+ `${pc12.dim("branch ")} ${repo.branch ?? pc12.yellow("(detached)")}`,
5046
+ `${pc12.dim("origin ")} ${repo.originUrl ?? pc12.dim("(none)")}`,
5047
+ `${pc12.dim("project")} ${pc12.cyan(projectId)}`,
4942
5048
  ""
4943
5049
  ].join("\n")
4944
5050
  );
@@ -4972,14 +5078,14 @@ function formatNode2(node) {
4972
5078
  const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
4973
5079
  return [
4974
5080
  formatSignal(node.signal, GIT_SIGNAL_BANDS),
4975
- pc11.dim(date),
4976
- pc11.magenta(sha),
5081
+ pc12.dim(date),
5082
+ pc12.magenta(sha),
4977
5083
  node.title,
4978
- pc11.dim(`(${files} file${files === 1 ? "" : "s"}, ${churn})`)
5084
+ pc12.dim(`(${files} file${files === 1 ? "" : "s"}, ${churn})`)
4979
5085
  ].join(" ");
4980
5086
  }
4981
5087
  function summarize2(nodes) {
4982
- if (nodes.length === 0) return pc11.yellow("no commits matched");
5088
+ if (nodes.length === 0) return pc12.yellow("no commits matched");
4983
5089
  const timestamps = nodes.map((n) => n.ts).sort();
4984
5090
  const avgSignal = nodes.reduce((n, x) => n + x.signal, 0) / nodes.length;
4985
5091
  const totalTokens = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
@@ -4989,7 +5095,7 @@ function summarize2(nodes) {
4989
5095
  }
4990
5096
  const hottest = [...fileHits.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([path, count]) => ` ${String(count).padStart(3)}x ${path}`);
4991
5097
  return [
4992
- `${pc11.bold(String(nodes.length))} nodes ${pc11.dim(`${timestamps[0]?.slice(0, 10)} .. ${timestamps.at(-1)?.slice(0, 10)}`)}`,
5098
+ `${pc12.bold(String(nodes.length))} nodes ${pc12.dim(`${timestamps[0]?.slice(0, 10)} .. ${timestamps.at(-1)?.slice(0, 10)}`)}`,
4993
5099
  ` avg signal ${avgSignal.toFixed(3)} ~${totalTokens.toLocaleString()} tokens if sent raw`,
4994
5100
  hottest.length ? ` hottest files:
4995
5101
  ${hottest.join("\n")}` : ""
@@ -5004,9 +5110,9 @@ async function runScanDiff(opts) {
5004
5110
  if (!opts.json) {
5005
5111
  process.stderr.write(
5006
5112
  [
5007
- `${pc12.dim("repo ")} ${repo.root}`,
5008
- `${pc12.dim("branch ")} ${repo.branch ?? pc12.yellow("(detached)")}`,
5009
- `${pc12.dim("project")} ${pc12.cyan(projectId)}`,
5113
+ `${pc13.dim("repo ")} ${repo.root}`,
5114
+ `${pc13.dim("branch ")} ${repo.branch ?? pc13.yellow("(detached)")}`,
5115
+ `${pc13.dim("project")} ${pc13.cyan(projectId)}`,
5010
5116
  ""
5011
5117
  ].join("\n")
5012
5118
  );
@@ -5036,28 +5142,28 @@ function formatNode3(node) {
5036
5142
  const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
5037
5143
  return [
5038
5144
  formatSignal(node.signal, DIFF_SIGNAL_BANDS),
5039
- pc12.dim(node.ts.slice(0, 10)),
5040
- pc12.magenta(sha),
5145
+ pc13.dim(node.ts.slice(0, 10)),
5146
+ pc13.magenta(sha),
5041
5147
  String(node.meta.path ?? ""),
5042
- pc12.dim(`(${churn}, ${node.meta.hunkCount ?? 0} hunk(s))`)
5148
+ pc13.dim(`(${churn}, ${node.meta.hunkCount ?? 0} hunk(s))`)
5043
5149
  ].join(" ");
5044
5150
  }
5045
5151
 
5046
5152
  // src/cli/commands/scan-docs.ts
5047
- import pc13 from "picocolors";
5153
+ import pc14 from "picocolors";
5048
5154
  async function runScanDocs(opts) {
5049
5155
  const repo = await readRepoInfo(opts.cwd);
5050
5156
  const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
5051
5157
  const { files, unreadable } = await readDocFiles(repo.root);
5052
5158
  if (!opts.json) {
5053
5159
  process.stderr.write(
5054
- files.length ? `${pc13.dim("tracked .md files")} ${files.map((f) => f.path).join(", ")}
5160
+ files.length ? `${pc14.dim("tracked .md files")} ${files.map((f) => f.path).join(", ")}
5055
5161
 
5056
- ` : `${pc13.yellow("no tracked .md files found")}
5162
+ ` : `${pc14.yellow("no tracked .md files found")}
5057
5163
  `
5058
5164
  );
5059
5165
  if (unreadable.length > 0) {
5060
- process.stderr.write(`${pc13.yellow("unreadable")} ${unreadable.join(", ")}
5166
+ process.stderr.write(`${pc14.yellow("unreadable")} ${unreadable.join(", ")}
5061
5167
 
5062
5168
  `);
5063
5169
  }
@@ -5073,7 +5179,7 @@ async function runScanDocs(opts) {
5073
5179
  const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
5074
5180
  process.stderr.write(
5075
5181
  `
5076
- ${pc13.bold(String(nodes.length))} section(s) from ${files.length} file(s) above threshold ${pc13.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
5182
+ ${pc14.bold(String(nodes.length))} section(s) from ${files.length} file(s) above threshold ${pc14.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
5077
5183
  `
5078
5184
  );
5079
5185
  return 0;
@@ -5083,13 +5189,13 @@ function formatNode4(node) {
5083
5189
  }
5084
5190
 
5085
5191
  // src/cli/commands/scan-session.ts
5086
- import pc14 from "picocolors";
5192
+ import pc15 from "picocolors";
5087
5193
  async function runScanSession(opts) {
5088
5194
  const repo = await readRepoInfo(opts.cwd);
5089
5195
  const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
5090
5196
  const turns = await collectClaudeCodeTranscripts(repo.root);
5091
5197
  if (turns.length === 0) {
5092
- process.stderr.write(`${pc14.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
5198
+ process.stderr.write(`${pc15.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
5093
5199
  `);
5094
5200
  return 0;
5095
5201
  }
@@ -5097,7 +5203,7 @@ async function runScanSession(opts) {
5097
5203
  const settled = selectSettledSessions(sessions, opts.settleMinutes);
5098
5204
  if (!opts.json) {
5099
5205
  process.stderr.write(
5100
- `${pc14.dim("sessions")} ${sessions.length} found, ${settled.length} settled (quiet ${opts.settleMinutes}m+)
5206
+ `${pc15.dim("sessions")} ${sessions.length} found, ${settled.length} settled (quiet ${opts.settleMinutes}m+)
5101
5207
 
5102
5208
  `
5103
5209
  );
@@ -5123,7 +5229,7 @@ async function runScanSession(opts) {
5123
5229
  }
5124
5230
  for (const preview of previews) {
5125
5231
  process.stdout.write(
5126
- `${pc14.bold(preview.sessionKey)} ${preview.startedAt.slice(0, 16).replace("T", " ")} ${preview.includedTurns}/${preview.turns} turn(s), ${preview.promptChars} chars
5232
+ `${pc15.bold(preview.sessionKey)} ${preview.startedAt.slice(0, 16).replace("T", " ")} ${preview.includedTurns}/${preview.turns} turn(s), ${preview.promptChars} chars
5127
5233
  ${preview.prompt}
5128
5234
 
5129
5235
  `
@@ -5135,7 +5241,7 @@ ${preview.prompt}
5135
5241
  settleMinutes: opts.settleMinutes,
5136
5242
  maxSessions: opts.maxSessions,
5137
5243
  onProgress: (done, total) => {
5138
- if (!opts.json) process.stderr.write(` ${pc14.dim(`summarizing ${done}/${total}`)}
5244
+ if (!opts.json) process.stderr.write(` ${pc15.dim(`summarizing ${done}/${total}`)}
5139
5245
  `);
5140
5246
  }
5141
5247
  });
@@ -5145,21 +5251,21 @@ ${preview.prompt}
5145
5251
  return 0;
5146
5252
  }
5147
5253
  for (const node of result.nodes) {
5148
- process.stdout.write(`${pc14.bold(node.title)}
5149
- ${pc14.dim(node.ts.slice(0, 16).replace("T", " "))}
5254
+ process.stdout.write(`${pc15.bold(node.title)}
5255
+ ${pc15.dim(node.ts.slice(0, 16).replace("T", " "))}
5150
5256
  ${node.body}
5151
5257
 
5152
5258
  `);
5153
5259
  }
5154
5260
  if (result.providerUnavailable) {
5155
5261
  process.stderr.write(
5156
- `${pc14.yellow("model unavailable")} -- is Ollama running with \`${opts.model}\` pulled? (\`ollama pull ${opts.model}\`)
5262
+ `${pc15.yellow("model unavailable")} -- is Ollama running with \`${opts.model}\` pulled? (\`ollama pull ${opts.model}\`)
5157
5263
  `
5158
5264
  );
5159
5265
  return 0;
5160
5266
  }
5161
5267
  process.stderr.write(
5162
- `${pc14.bold(String(result.nodes.length))} summarized` + (result.failed > 0 ? `, ${pc14.yellow(`${result.failed} failed`)}` : "") + ` ${pc14.dim(`(model ${opts.model})`)}
5268
+ `${pc15.bold(String(result.nodes.length))} summarized` + (result.failed > 0 ? `, ${pc15.yellow(`${result.failed} failed`)}` : "") + ` ${pc15.dim(`(model ${opts.model})`)}
5163
5269
  `
5164
5270
  );
5165
5271
  return 0;
@@ -5167,16 +5273,16 @@ ${node.body}
5167
5273
  var SCAN_SESSION_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
5168
5274
 
5169
5275
  // src/cli/commands/scan-shell.ts
5170
- import pc15 from "picocolors";
5276
+ import pc16 from "picocolors";
5171
5277
  async function runScanShell(opts) {
5172
5278
  const repo = await readRepoInfo(opts.cwd);
5173
5279
  const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
5174
5280
  const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
5175
5281
  if (!opts.json) {
5176
5282
  process.stderr.write(
5177
- results.length ? `${pc15.dim("sources found")} ${results.map((r) => r.name).join(", ")}
5283
+ results.length ? `${pc16.dim("sources found")} ${results.map((r) => r.name).join(", ")}
5178
5284
 
5179
- ` : `${pc15.yellow("no shell history source found on this machine")}
5285
+ ` : `${pc16.yellow("no shell history source found on this machine")}
5180
5286
  `
5181
5287
  );
5182
5288
  }
@@ -5185,7 +5291,7 @@ async function runScanShell(opts) {
5185
5291
  const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
5186
5292
  allNodes.push(...nodes);
5187
5293
  if (!opts.json) {
5188
- process.stdout.write(`${pc15.bold(`shell:${result.name}`)} ${pc15.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
5294
+ process.stdout.write(`${pc16.bold(`shell:${result.name}`)} ${pc16.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
5189
5295
  `);
5190
5296
  for (const node of nodes) process.stdout.write(`${formatNode5(node)}
5191
5297
  `);
@@ -5198,19 +5304,19 @@ async function runScanShell(opts) {
5198
5304
  return 0;
5199
5305
  }
5200
5306
  const approxTotal = allNodes.reduce((n, x) => n + approxTokens(x.body), 0);
5201
- process.stderr.write(`${pc15.bold(String(allNodes.length))} node(s) total ${pc15.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
5307
+ process.stderr.write(`${pc16.bold(String(allNodes.length))} node(s) total ${pc16.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
5202
5308
  `);
5203
5309
  return 0;
5204
5310
  }
5205
5311
  function formatNode5(node) {
5206
- const approx = node.meta.tsApprox ? pc15.dim("~") : " ";
5312
+ const approx = node.meta.tsApprox ? pc16.dim("~") : " ";
5207
5313
  const exit = node.meta.exitCode;
5208
- const exitLabel = typeof exit === "number" && exit !== 0 ? pc15.red(`exit ${exit}`) : "";
5314
+ const exitLabel = typeof exit === "number" && exit !== 0 ? pc16.red(`exit ${exit}`) : "";
5209
5315
  return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
5210
5316
  }
5211
5317
 
5212
5318
  // src/cli/commands/scan-structure.ts
5213
- import pc16 from "picocolors";
5319
+ import pc17 from "picocolors";
5214
5320
  async function runScanStructure(opts) {
5215
5321
  const repo = await readRepoInfo(opts.cwd);
5216
5322
  const { edges, filesScanned, unreadable } = await collectFileEdges(repo.root);
@@ -5220,17 +5326,17 @@ async function runScanStructure(opts) {
5220
5326
  return 0;
5221
5327
  }
5222
5328
  if (unreadable.length > 0) {
5223
- process.stderr.write(`${pc16.yellow("unreadable")} ${unreadable.join(", ")}
5329
+ process.stderr.write(`${pc17.yellow("unreadable")} ${unreadable.join(", ")}
5224
5330
 
5225
5331
  `);
5226
5332
  }
5227
5333
  for (const edge of edges) {
5228
- process.stdout.write(`${edge.fromPath} ${pc16.dim("->")} ${edge.toPath}
5334
+ process.stdout.write(`${edge.fromPath} ${pc17.dim("->")} ${edge.toPath}
5229
5335
  `);
5230
5336
  }
5231
5337
  process.stderr.write(
5232
5338
  `
5233
- ${pc16.bold(String(edges.length))} edge(s) from ${filesScanned} tracked .ts/.tsx/.js/.jsx file(s)
5339
+ ${pc17.bold(String(edges.length))} edge(s) from ${filesScanned} tracked .ts/.tsx/.js/.jsx file(s)
5234
5340
  `
5235
5341
  );
5236
5342
  return 0;
@@ -5238,7 +5344,7 @@ ${pc16.bold(String(edges.length))} edge(s) from ${filesScanned} tracked .ts/.tsx
5238
5344
 
5239
5345
  // src/cli/commands/status.ts
5240
5346
  import { statSync } from "fs";
5241
- import pc17 from "picocolors";
5347
+ import pc18 from "picocolors";
5242
5348
  function humanBytes(bytes) {
5243
5349
  if (bytes < 1024) return `${bytes} B`;
5244
5350
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -5266,32 +5372,32 @@ async function runStatus(opts) {
5266
5372
  const structure = store.fileEdgeStats(projectId);
5267
5373
  const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
5268
5374
  const kinds = Object.entries(stats.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
5269
- const staleProjectWarning = otherProjectIds.length ? `${pc17.yellow("stale ")} ${otherProjectIds.length} prior project ${otherProjectIds.length === 1 ? "identity holds" : "identities hold"} ${otherProjectNodes} node(s) \u2014 run ${pc17.bold(
5375
+ const staleProjectWarning = otherProjectIds.length ? `${pc18.yellow("stale ")} ${otherProjectIds.length} prior project ${otherProjectIds.length === 1 ? "identity holds" : "identities hold"} ${otherProjectNodes} node(s) \u2014 run ${pc18.bold(
5270
5376
  "nexusmem sync --prune-source <name>"
5271
5377
  )} to remove stale source data` : "";
5272
5378
  out(
5273
5379
  [
5274
- `${pc17.dim("repo ")} ${repo.root}`,
5275
- `${pc17.dim("branch ")} ${repo.branch ?? pc17.yellow("(detached)")}`,
5276
- `${pc17.dim("project ")} ${pc17.cyan(projectId)}`,
5277
- `${pc17.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc17.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
5278
- `${pc17.dim("database")} ${ws.dbPath} ${pc17.dim(`(${humanBytes(dbBytes)})`)}`,
5380
+ `${pc18.dim("repo ")} ${repo.root}`,
5381
+ `${pc18.dim("branch ")} ${repo.branch ?? pc18.yellow("(detached)")}`,
5382
+ `${pc18.dim("project ")} ${pc18.cyan(projectId)}`,
5383
+ `${pc18.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc18.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
5384
+ `${pc18.dim("database")} ${ws.dbPath} ${pc18.dim(`(${humanBytes(dbBytes)})`)}`,
5279
5385
  staleProjectWarning,
5280
5386
  "",
5281
- `${pc17.bold(String(stats.total))} node(s)${stats.total ? ` ${pc17.dim(`${stats.oldest?.slice(0, 10)} .. ${stats.newest?.slice(0, 10)}`)}` : ""}`,
5387
+ `${pc18.bold(String(stats.total))} node(s)${stats.total ? ` ${pc18.dim(`${stats.oldest?.slice(0, 10)} .. ${stats.newest?.slice(0, 10)}`)}` : ""}`,
5282
5388
  ...kinds,
5283
- stats.total ? ` ${pc17.dim(`${stats.distinctFiles} distinct file path(s)`)}` : "",
5389
+ stats.total ? ` ${pc18.dim(`${stats.distinctFiles} distinct file path(s)`)}` : "",
5284
5390
  "",
5285
- sources.length ? pc17.dim("sources") : pc17.yellow("no sources synced yet"),
5391
+ sources.length ? pc18.dim("sources") : pc18.yellow("no sources synced yet"),
5286
5392
  ...sources.map((s) => {
5287
5393
  const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
5288
5394
  const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
5289
- return ` ${s.source.padEnd(14)} ${pc17.dim(`last run ${when}`)} ${pc17.dim(`cursor ${cursorLabel}`)}`;
5395
+ return ` ${s.source.padEnd(14)} ${pc18.dim(`last run ${when}`)} ${pc18.dim(`cursor ${cursorLabel}`)}`;
5290
5396
  }),
5291
- gitCursor && gitCursor !== repo.head ? `${pc17.yellow("git behind HEAD")} \u2014 run ${pc17.bold("nexusmem sync")}` : "",
5397
+ gitCursor && gitCursor !== repo.head ? `${pc18.yellow("git behind HEAD")} \u2014 run ${pc18.bold("nexusmem sync")}` : "",
5292
5398
  "",
5293
- chains.failuresTotal ? `${pc17.dim("chains ")} ${pc17.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc17.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc17.bold("nexusmem sync --link-failures")} to link more` : ""}` : "",
5294
- structure.edges ? `${pc17.dim("structure")} ${pc17.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : ""
5399
+ chains.failuresTotal ? `${pc18.dim("chains ")} ${pc18.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc18.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc18.bold("nexusmem sync --link-failures")} to link more` : ""}` : "",
5400
+ structure.edges ? `${pc18.dim("structure")} ${pc18.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : ""
5295
5401
  ].filter((line) => line !== "").join("\n").concat("\n")
5296
5402
  );
5297
5403
  return 0;
@@ -5306,7 +5412,7 @@ function isExpected(err) {
5306
5412
  // the user fixes, not stack traces they debug.
5307
5413
  err instanceof GitSpawnError || // Survived every retry, so git is genuinely unstable on this machine
5308
5414
  // (antivirus, a bad install). Actionable, and not our stack to print.
5309
- err instanceof GitCrashError || err instanceof ConfigError || err instanceof ProfileNotFoundError || err instanceof ForeignGitHookError || err instanceof DenyListError;
5415
+ err instanceof GitCrashError || err instanceof ConfigError || err instanceof ProfileNotFoundError || err instanceof ForeignGitHookError || err instanceof DenyListError || err instanceof MarkStaleError;
5310
5416
  }
5311
5417
  function guard(run) {
5312
5418
  return async () => {
@@ -5314,7 +5420,7 @@ function guard(run) {
5314
5420
  process.exitCode = await run();
5315
5421
  } catch (err) {
5316
5422
  if (isExpected(err)) {
5317
- process.stderr.write(`${pc18.red("error")} ${err.message}
5423
+ process.stderr.write(`${pc19.red("error")} ${err.message}
5318
5424
  `);
5319
5425
  process.exitCode = 1;
5320
5426
  return;
@@ -5408,6 +5514,11 @@ program.command("forget").description(
5408
5514
  })
5409
5515
  )()
5410
5516
  );
5517
+ program.command("mark-stale").description(
5518
+ "Mark a node as superseded by another -- the ranker down-weights it (never deletes it) so its replacement usually outranks it"
5519
+ ).argument("<nodeId>", "id of the node to mark stale").requiredOption("--supersedes <newNodeId>", "id of the node that supersedes it").option("-C, --cwd <path>", "repository path", process.cwd()).action(
5520
+ (nodeId, options) => guard(() => runMarkStale({ cwd: options.cwd, nodeId, supersedesId: options.supersedes }))()
5521
+ );
5411
5522
  program.command("projects").description("List the repositories `query --all-projects` would search").option("--prune", "forget registered projects whose database is no longer on disk", false).option("--json", "emit the registry as JSON on stdout", false).action((options) => guard(() => runProjects({ prune: options.prune, json: options.json }))());
5412
5523
  program.command("scan-git").description("Preview the MemoryNodes git history would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--since <date>", "only commits newer than this git date expression, e.g. 90.days.ago").option("-n, --limit <count>", "stop after N commits", (v) => Number.parseInt(v, 10)).option("--no-merges", "skip merge commits").option("--min-signal <score>", "drop nodes below this signal", (v) => Number.parseFloat(v), 0).option("--json", "emit MemoryNodes as JSON on stdout", false).action(
5413
5524
  (options) => guard(
@@ -5468,7 +5579,7 @@ program.command("scan-structure").description("Preview the JS/TS import-graph ed
5468
5579
  program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
5469
5580
  program.parseAsync(process.argv).catch((err) => {
5470
5581
  const message = err instanceof Error ? err.message : String(err);
5471
- process.stderr.write(`${pc18.red("error")} ${message}
5582
+ process.stderr.write(`${pc19.red("error")} ${message}
5472
5583
  `);
5473
5584
  process.exitCode = 1;
5474
5585
  });