nexusmem 0.5.0 → 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";
@@ -726,6 +726,12 @@ function insertDenyListEntry(db, input) {
726
726
  createdAt
727
727
  };
728
728
  }
729
+ function denyListEntryExists(db, projectId, input) {
730
+ const row = db.prepare(
731
+ `SELECT 1 FROM deny_list WHERE project_id = ? AND match_type = ? AND pattern = ? AND ignore_case = ? LIMIT 1`
732
+ ).get(projectId, input.matchType, input.pattern, input.ignoreCase ? 1 : 0);
733
+ return row !== void 0;
734
+ }
729
735
  function matchableText(node) {
730
736
  return `${node.title}
731
737
  ${node.body}
@@ -748,7 +754,9 @@ function firstMatchingEntry(entries, node) {
748
754
  }
749
755
 
750
756
  // src/cli/commands/forget.ts
757
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
751
758
  import pc from "picocolors";
759
+ import { z as z2 } from "zod";
752
760
 
753
761
  // src/store/store.ts
754
762
  import Database from "better-sqlite3";
@@ -756,6 +764,21 @@ import { mkdirSync } from "fs";
756
764
  import { dirname as dirname3 } from "path";
757
765
  import * as sqliteVec from "sqlite-vec";
758
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
+
759
782
  // src/core/ids.ts
760
783
  import { createHash } from "crypto";
761
784
  var KEY_SEP = "\0";
@@ -953,12 +976,21 @@ CREATE TABLE tombstones (
953
976
  );
954
977
  CREATE INDEX idx_tombstones_project ON tombstones (project_id, removed_at DESC);
955
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
+ `;
956
987
  var MIGRATIONS = [
957
988
  { version: 1, up: (db) => db.exec(V1) },
958
989
  { version: 2, up: (db) => db.exec(V2) },
959
990
  { version: 3, up: (db) => db.exec(V3) },
960
991
  { version: 4, up: (db) => db.exec(V4) },
961
- { version: 5, up: (db) => db.exec(V5) }
992
+ { version: 5, up: (db) => db.exec(V5) },
993
+ { version: 6, up: (db) => db.exec(V6) }
962
994
  ];
963
995
  var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
964
996
  function currentSchemaVersion(db) {
@@ -1050,11 +1082,12 @@ var MemoryStore = class _MemoryStore {
1050
1082
  "DELETE FROM nodes_vec WHERE rowid = (SELECT rowid FROM nodes WHERE id = ?)"
1051
1083
  );
1052
1084
  const insertNode = this.db.prepare(
1053
- `INSERT INTO nodes (id, kind, project_id, ts, ts_epoch, source, title, body, signal, meta, created_at)
1054
- 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)
1055
1087
  ON CONFLICT(id) DO UPDATE SET
1056
1088
  ts = excluded.ts, ts_epoch = excluded.ts_epoch, source = excluded.source,
1057
- 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`
1058
1091
  );
1059
1092
  const clearFiles = this.db.prepare("DELETE FROM node_files WHERE node_id = ?");
1060
1093
  const insertFile = this.db.prepare(
@@ -1100,6 +1133,8 @@ var MemoryStore = class _MemoryStore {
1100
1133
  body: node.body,
1101
1134
  signal: node.signal,
1102
1135
  meta: JSON.stringify(node.meta),
1136
+ provenance: node.provenance ?? defaultProvenanceForKind(node.kind),
1137
+ supersedes: node.supersedes ?? null,
1103
1138
  now
1104
1139
  });
1105
1140
  clearFiles.run(node.id);
@@ -1186,7 +1221,7 @@ var MemoryStore = class _MemoryStore {
1186
1221
  getNodesByIds(ids) {
1187
1222
  if (ids.length === 0) return [];
1188
1223
  return this.db.prepare(
1189
- `SELECT id, kind, project_id AS projectId, ts, title, body, signal
1224
+ `SELECT id, kind, project_id AS projectId, ts, title, body, signal, provenance
1190
1225
  FROM nodes WHERE id IN (SELECT value FROM json_each(?))`
1191
1226
  ).all(JSON.stringify(ids));
1192
1227
  }
@@ -1198,7 +1233,7 @@ var MemoryStore = class _MemoryStore {
1198
1233
  */
1199
1234
  listRecentNodes(projectId, limit = 20) {
1200
1235
  return this.db.prepare(
1201
- `SELECT id, kind, ts, source, title, signal
1236
+ `SELECT id, kind, ts, source, title, signal, provenance
1202
1237
  FROM nodes
1203
1238
  WHERE project_id = ?
1204
1239
  ORDER BY ts_epoch DESC
@@ -1348,6 +1383,59 @@ var MemoryStore = class _MemoryStore {
1348
1383
  listDenyList(projectId) {
1349
1384
  return listDenyListEntries(this.db, projectId);
1350
1385
  }
1386
+ /**
1387
+ * What `importDenyList` with these same entries would do, without writing
1388
+ * anything -- `forget --import`'s dry-run default, same convention as
1389
+ * `previewForget`.
1390
+ */
1391
+ previewImportDenyList(projectId, otherProjectIds, entries) {
1392
+ return entries.map((input) => {
1393
+ validatePattern(input);
1394
+ if (denyListEntryExists(this.db, projectId, input)) {
1395
+ return { matchType: input.matchType, pattern: input.pattern, alreadyPresent: true, wouldRemove: 0 };
1396
+ }
1397
+ const preview = this.previewForget(projectId, otherProjectIds, input);
1398
+ return {
1399
+ matchType: input.matchType,
1400
+ pattern: input.pattern,
1401
+ alreadyPresent: false,
1402
+ wouldRemove: preview.reduce((sum, p) => sum + p.count, 0)
1403
+ };
1404
+ });
1405
+ }
1406
+ /**
1407
+ * Re-apply a previously-exported deny-list against this project.
1408
+ *
1409
+ * This is the fix for `forget`'s per-checkout gap: `deny_list` lives in
1410
+ * `.nexusmem/memory.db`, which is gitignored and never travels with `git
1411
+ * clone`/`git push`, while the things a fresh `sync` re-derives from --
1412
+ * git history and the user-home shell-hook log -- both travel or persist
1413
+ * independently of any one checkout. A fresh clone or a restored backup
1414
+ * starts with an empty deny_list and no memory of what was forgotten. See
1415
+ * docs/forget-mechanism.md.
1416
+ *
1417
+ * Entries already active (same matchType+pattern+ignoreCase) are left
1418
+ * untouched. Every new one goes through `forget` itself, so an imported
1419
+ * value is deleted from this checkout's nodes too, not just blocked going
1420
+ * forward -- exactly what running `nexusmem forget <value>` fresh in this
1421
+ * checkout would have done.
1422
+ */
1423
+ importDenyList(projectId, otherProjectIds, entries) {
1424
+ let imported = 0;
1425
+ let skipped = 0;
1426
+ let removedNodes = 0;
1427
+ for (const input of entries) {
1428
+ validatePattern(input);
1429
+ if (denyListEntryExists(this.db, projectId, input)) {
1430
+ skipped += 1;
1431
+ continue;
1432
+ }
1433
+ const result = this.forget(projectId, otherProjectIds, input);
1434
+ imported += 1;
1435
+ removedNodes += result.removed;
1436
+ }
1437
+ return { imported, skipped, removedNodes };
1438
+ }
1351
1439
  /**
1352
1440
  * Replace this project's entire `file_edges` snapshot in one transaction.
1353
1441
  *
@@ -1433,7 +1521,7 @@ var MemoryStore = class _MemoryStore {
1433
1521
  vectorSearch(projectId, embedding, limit = 20) {
1434
1522
  const overfetch = Math.max(limit * 8, 50);
1435
1523
  return this.db.prepare(
1436
- `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
1437
1525
  FROM nodes_vec v
1438
1526
  JOIN nodes n ON n.rowid = v.rowid
1439
1527
  WHERE v.embedding MATCH ? AND k = ? AND n.project_id = ?
@@ -1468,7 +1556,7 @@ var MemoryStore = class _MemoryStore {
1468
1556
  const match = toMatchQuery(query);
1469
1557
  if (!match) return [];
1470
1558
  const rows = this.db.prepare(
1471
- `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,
1472
1560
  bm25(nodes_fts, 10.0, 1.0) AS rank
1473
1561
  FROM nodes_fts
1474
1562
  JOIN nodes n ON n.rowid = nodes_fts.rowid
@@ -1478,6 +1566,20 @@ var MemoryStore = class _MemoryStore {
1478
1566
  ).all(match, projectId, limit);
1479
1567
  return rows;
1480
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
+ }
1481
1583
  /** Escape hatch for tests and future modules. */
1482
1584
  get raw() {
1483
1585
  return this.db;
@@ -1509,6 +1611,17 @@ async function loadContext(cwd) {
1509
1611
  }
1510
1612
 
1511
1613
  // src/cli/commands/forget.ts
1614
+ var ExportPayloadSchema = z2.object({
1615
+ version: z2.literal(1),
1616
+ entries: z2.array(
1617
+ z2.object({
1618
+ matchType: z2.enum(["literal", "regex"]),
1619
+ pattern: z2.string(),
1620
+ ignoreCase: z2.boolean(),
1621
+ reason: z2.string().nullable()
1622
+ })
1623
+ )
1624
+ });
1512
1625
  async function runForget(opts) {
1513
1626
  const { ws, projectId } = await loadContext(opts.cwd);
1514
1627
  const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
@@ -1532,8 +1645,75 @@ async function runForget(opts) {
1532
1645
  );
1533
1646
  return 0;
1534
1647
  }
1648
+ if (opts.export) {
1649
+ const entries = store.listDenyList(projectId);
1650
+ const payload = {
1651
+ version: 1,
1652
+ entries: entries.map((e) => ({ matchType: e.matchType, pattern: e.pattern, ignoreCase: e.ignoreCase, reason: e.reason }))
1653
+ };
1654
+ await writeFile4(opts.export, `${JSON.stringify(payload, null, 2)}
1655
+ `, "utf8");
1656
+ out(
1657
+ [
1658
+ `${pc.green("exported")} ${entries.length} deny-list entrie(s) to ${opts.export}`,
1659
+ pc.yellow(
1660
+ "this file contains the raw forgotten value(s) in plaintext -- store it somewhere secure (password manager, encrypted note) and never commit it to git or share it publicly."
1661
+ ),
1662
+ ""
1663
+ ].join("\n")
1664
+ );
1665
+ return 0;
1666
+ }
1667
+ if (opts.import) {
1668
+ let raw;
1669
+ try {
1670
+ raw = await readFile4(opts.import, "utf8");
1671
+ } catch (err) {
1672
+ throw new DenyListError(`could not read ${opts.import}: ${err instanceof Error ? err.message : String(err)}`);
1673
+ }
1674
+ let parsed;
1675
+ try {
1676
+ parsed = JSON.parse(raw);
1677
+ } catch (err) {
1678
+ throw new DenyListError(`${opts.import} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
1679
+ }
1680
+ const validated = ExportPayloadSchema.safeParse(parsed);
1681
+ if (!validated.success) {
1682
+ throw new DenyListError(
1683
+ `${opts.import} is not a valid deny-list export: ${validated.error.issues.map((i) => i.message).join("; ")}`
1684
+ );
1685
+ }
1686
+ const entries = validated.data.entries;
1687
+ const otherProjectIds2 = store.listOtherProjectIds(projectId);
1688
+ if (!opts.yes) {
1689
+ const preview = store.previewImportDenyList(projectId, otherProjectIds2, entries);
1690
+ const toImport = preview.filter((p) => !p.alreadyPresent);
1691
+ const totalRemove = toImport.reduce((sum, p) => sum + p.wouldRemove, 0);
1692
+ if (toImport.length === 0) {
1693
+ out(`${pc.dim("forget --import")} all ${preview.length} entrie(s) in ${opts.import} are already active -- nothing to do
1694
+ `);
1695
+ return 0;
1696
+ }
1697
+ const describe = (p) => ` ${p.matchType === "regex" ? pc.dim("/") + p.pattern + pc.dim("/") : JSON.stringify(p.pattern)}: ${p.wouldRemove} node(s)`;
1698
+ out(
1699
+ [
1700
+ `${pc.yellow("would import")} ${toImport.length} new deny-list entrie(s)${preview.length > toImport.length ? ` (${preview.length - toImport.length} already active)` : ""}, removing ${totalRemove} node(s):`,
1701
+ ...toImport.map(describe),
1702
+ pc.dim("re-run with --yes to permanently deny-list these value(s) and delete matching node(s) -- this cannot be undone"),
1703
+ ""
1704
+ ].join("\n")
1705
+ );
1706
+ return 0;
1707
+ }
1708
+ const result2 = store.importDenyList(projectId, otherProjectIds2, entries);
1709
+ out(
1710
+ `${pc.green("imported")} ${result2.imported} deny-list entrie(s)${result2.skipped > 0 ? ` (${result2.skipped} already active)` : ""}, ${result2.removedNodes} node(s) deleted
1711
+ `
1712
+ );
1713
+ return 0;
1714
+ }
1535
1715
  if (!opts.value) {
1536
- throw new DenyListError("a value is required (or pass --list to see active deny-list entries)");
1716
+ throw new DenyListError("a value is required (or pass --list/--export/--import)");
1537
1717
  }
1538
1718
  const input = {
1539
1719
  matchType: opts.regex ? "regex" : "literal",
@@ -1663,20 +1843,20 @@ import pc4 from "picocolors";
1663
1843
 
1664
1844
  // src/config/registry.ts
1665
1845
  import { existsSync as existsSync2 } from "fs";
1666
- import { mkdir as mkdir4, readFile as readFile4, rename, writeFile as writeFile4 } from "fs/promises";
1846
+ import { mkdir as mkdir4, readFile as readFile5, rename, writeFile as writeFile5 } from "fs/promises";
1667
1847
  import { join as join5 } from "path";
1668
- import { z as z2 } from "zod";
1669
- var ENTRY_SCHEMA = z2.object({
1670
- projectId: z2.string().min(1),
1671
- root: z2.string().min(1),
1672
- dbPath: z2.string().min(1),
1673
- originUrl: z2.string().nullable().default(null),
1848
+ import { z as z3 } from "zod";
1849
+ var ENTRY_SCHEMA = z3.object({
1850
+ projectId: z3.string().min(1),
1851
+ root: z3.string().min(1),
1852
+ dbPath: z3.string().min(1),
1853
+ originUrl: z3.string().nullable().default(null),
1674
1854
  /** Epoch ms of the last `init`/`sync` that recorded this entry. */
1675
- lastSeenAt: z2.number().int().nonnegative()
1855
+ lastSeenAt: z3.number().int().nonnegative()
1676
1856
  });
1677
- var REGISTRY_SCHEMA = z2.object({
1678
- version: z2.literal(1),
1679
- projects: z2.array(ENTRY_SCHEMA).default([])
1857
+ var REGISTRY_SCHEMA = z3.object({
1858
+ version: z3.literal(1),
1859
+ projects: z3.array(ENTRY_SCHEMA).default([])
1680
1860
  });
1681
1861
  function registryPath() {
1682
1862
  return join5(globalWorkspaceDir(), "projects.json");
@@ -1684,7 +1864,7 @@ function registryPath() {
1684
1864
  async function readRegistry() {
1685
1865
  let raw;
1686
1866
  try {
1687
- raw = await readFile4(registryPath(), "utf8");
1867
+ raw = await readFile5(registryPath(), "utf8");
1688
1868
  } catch {
1689
1869
  return [];
1690
1870
  }
@@ -1724,7 +1904,7 @@ async function writeRegistry(projects) {
1724
1904
  const path = registryPath();
1725
1905
  const tmp = `${path}.${process.pid}.tmp`;
1726
1906
  await mkdir4(globalWorkspaceDir(), { recursive: true });
1727
- await writeFile4(tmp, `${JSON.stringify({ version: 1, projects }, null, 2)}
1907
+ await writeFile5(tmp, `${JSON.stringify({ version: 1, projects }, null, 2)}
1728
1908
  `, "utf8");
1729
1909
  await rename(tmp, path);
1730
1910
  }
@@ -1791,8 +1971,47 @@ async function runInit(opts) {
1791
1971
  return 0;
1792
1972
  }
1793
1973
 
1794
- // src/cli/commands/projects.ts
1974
+ // src/cli/commands/mark-stale.ts
1795
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";
1796
2015
  async function runProjects(opts) {
1797
2016
  const { entries, missing } = await readLiveRegistry();
1798
2017
  const rows = entries.map((entry) => {
@@ -1811,7 +2030,7 @@ async function runProjects(opts) {
1811
2030
  });
1812
2031
  if (opts.prune) {
1813
2032
  const removed = await forgetProjects(missing.map((entry) => entry.projectId));
1814
- 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
1815
2034
  `);
1816
2035
  }
1817
2036
  if (opts.json) {
@@ -1819,28 +2038,28 @@ async function runProjects(opts) {
1819
2038
  `);
1820
2039
  return 0;
1821
2040
  }
1822
- process.stderr.write(`${pc5.dim("registry")} ${registryPath()}
2041
+ process.stderr.write(`${pc6.dim("registry")} ${registryPath()}
1823
2042
 
1824
2043
  `);
1825
2044
  if (rows.length === 0) {
1826
- 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
1827
2046
  `);
1828
2047
  return 0;
1829
2048
  }
1830
2049
  for (const row of rows) {
1831
2050
  const seen = new Date(row.lastSeenAt).toISOString().slice(0, 16).replace("T", " ");
1832
- const count = row.nodes === null ? pc5.yellow("unreadable") : `${row.nodes} node(s)`;
1833
- process.stdout.write(`${pc5.cyan(row.projectId.slice(0, 8))} ${row.root}
1834
- ${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}`)}
1835
2054
  `);
1836
2055
  }
1837
2056
  if (!opts.prune && missing.length > 0) {
1838
2057
  process.stderr.write(
1839
2058
  `
1840
- ${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")}
1841
2060
  `
1842
2061
  );
1843
- 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)}
1844
2063
  `);
1845
2064
  }
1846
2065
  return 0;
@@ -1849,7 +2068,7 @@ ${pc5.yellow(`${missing.length} registered project(s) have no database on disk`)
1849
2068
  // src/mcp/server.ts
1850
2069
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1851
2070
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1852
- import { z as z3 } from "zod";
2071
+ import { z as z4 } from "zod";
1853
2072
 
1854
2073
  // src/mcp/tools.ts
1855
2074
  import { basename as basename3 } from "path";
@@ -2009,6 +2228,7 @@ function packContext(ranked, tokensBudget, opts = {}) {
2009
2228
  score: hit.score,
2010
2229
  summary,
2011
2230
  tokens,
2231
+ provenance: hit.provenance,
2012
2232
  ...hit.project ? { project: hit.project } : {}
2013
2233
  });
2014
2234
  tokensUsed += tokens;
@@ -2021,7 +2241,8 @@ function renderContextBlock(query, result) {
2021
2241
  const lines = [`Relevant history for: ${query}`, ""];
2022
2242
  for (const node of result.nodes) {
2023
2243
  const project = node.project ? `[${node.project}] ` : "";
2024
- 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}`);
2025
2246
  if (node.summary && node.summary !== node.title) {
2026
2247
  if (node.kind === "code_diff") {
2027
2248
  for (const line of node.summary.split("\n")) lines.push(` ${line}`);
@@ -2151,7 +2372,16 @@ function mergeSearchAndVectorHits(bm25Hits, vectorHits) {
2151
2372
  for (const hit of bm25Hits) byId.set(hit.id, hit);
2152
2373
  for (const hit of vectorHits) {
2153
2374
  if (byId.has(hit.id)) continue;
2154
- 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
+ });
2155
2385
  }
2156
2386
  return [...byId.values()];
2157
2387
  }
@@ -2162,6 +2392,7 @@ var SIGNAL_FLOOR = 0.2;
2162
2392
  var RECENCY_FLOOR = 0.3;
2163
2393
  var DEFAULT_HALF_LIFE_DAYS = 30;
2164
2394
  var MS_PER_DAY = 864e5;
2395
+ var SUPERSEDED_PENALTY = 0.5;
2165
2396
  var MAX_PRIOR_OVERTURN = 2;
2166
2397
  var PRIOR_COUNT = 2;
2167
2398
  var PER_PRIOR_OVERTURN = MAX_PRIOR_OVERTURN ** (1 / PRIOR_COUNT);
@@ -2202,7 +2433,8 @@ function rankHits(hits, opts = {}) {
2202
2433
  const signalWeight = SIGNAL_FLOOR + (1 - SIGNAL_FLOOR) * hit.signal;
2203
2434
  const ageDays = ageDaysOf(hit.ts, now);
2204
2435
  const recencyFactor = RECENCY_FLOOR + (1 - RECENCY_FLOOR) * 2 ** (-ageDays / halfLife);
2205
- 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;
2206
2438
  return { ...hit, relevance, signalWeight, ageDays, recencyFactor, score };
2207
2439
  });
2208
2440
  return ranked.sort((a, b) => b.score - a.score);
@@ -2231,6 +2463,7 @@ function pullLinkedResolutions(resolveStore, ranked) {
2231
2463
  title: resolution.title,
2232
2464
  body: resolution.body,
2233
2465
  signal: resolution.signal,
2466
+ provenance: resolution.provenance,
2234
2467
  rank: 0,
2235
2468
  // no bm25/vector rank of its own -- never read again past this point
2236
2469
  relevance: hit.relevance,
@@ -2252,6 +2485,7 @@ async function runCrossProjectQuery(sources, query, opts) {
2252
2485
  const perProject = [];
2253
2486
  let bm25Count = 0;
2254
2487
  let vectorCount = 0;
2488
+ const supersededIds = /* @__PURE__ */ new Set();
2255
2489
  for (const source of sources) {
2256
2490
  const label = (hit) => ({ ...hit, project: source.label });
2257
2491
  const bm25Hits = source.store.search(source.projectId, query, opts.candidates).map(label);
@@ -2264,12 +2498,13 @@ async function runCrossProjectQuery(sources, query, opts) {
2264
2498
  lists.push(vectorHits.map((hit) => ({ ...hit, rank: 0, project: source.label })));
2265
2499
  }
2266
2500
  hits.push(...mergeSearchAndVectorHits(bm25Hits, vectorHits).map(label));
2501
+ for (const id of source.store.getSupersededIds(source.projectId)) supersededIds.add(id);
2267
2502
  }
2268
2503
  const storeByLabel = new Map(sources.map((source) => [source.label, source.store]));
2269
2504
  const relevanceScores = reciprocalRankFusion(lists);
2270
2505
  const ranked = pullLinkedResolutions(
2271
2506
  (hit) => hit.project ? storeByLabel.get(hit.project) : void 0,
2272
- rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores })
2507
+ rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores, supersededIds })
2273
2508
  );
2274
2509
  const packed = packContext(ranked, opts.budget, { query });
2275
2510
  return { bm25Count, vectorCount, hits, packed, perProject };
@@ -2283,9 +2518,10 @@ async function runHybridQuery(store, projectId, query, opts) {
2283
2518
  }
2284
2519
  const hits = vectorHits.length > 0 ? mergeSearchAndVectorHits(bm25Hits, vectorHits) : bm25Hits;
2285
2520
  const relevanceScores = vectorHits.length > 0 ? reciprocalRankFusion([bm25Hits, vectorHits]) : void 0;
2521
+ const supersededIds = store.getSupersededIds(projectId);
2286
2522
  const ranked = pullLinkedResolutions(
2287
2523
  () => store,
2288
- rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores })
2524
+ rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores, supersededIds })
2289
2525
  );
2290
2526
  const packed = packContext(ranked, opts.budget, { query });
2291
2527
  return { bm25Count: bm25Hits.length, vectorCount: vectorHits.length, hits, packed };
@@ -2390,7 +2626,7 @@ var OllamaEmbeddingProvider = class {
2390
2626
  };
2391
2627
 
2392
2628
  // src/cli/commands/sync.ts
2393
- import pc6 from "picocolors";
2629
+ import pc7 from "picocolors";
2394
2630
 
2395
2631
  // src/conversation/chunk.ts
2396
2632
  var HEADING_LINE = /^#{1,6}\s+(.+)$/;
@@ -2525,6 +2761,8 @@ function toMemoryNodes(turn, projectId, opts = {}) {
2525
2761
  files: extractMentionedFiles(`${userRedacted.text}
2526
2762
  ${chunk2.text}`),
2527
2763
  signal: scoreConversationTurn(userRedacted.text, chunk2.text),
2764
+ provenance: "inferred",
2765
+ // discourse about what happened, not the event itself
2528
2766
  meta: {
2529
2767
  cwd: turn.cwd,
2530
2768
  source: turn.source,
@@ -2937,6 +3175,7 @@ function toMemoryNode(commit, projectId, opts = {}) {
2937
3175
  body: truncate(bodyParts.join("\n"), maxBody),
2938
3176
  files: keptFiles,
2939
3177
  signal: scoreCommit(commit),
3178
+ provenance: "observed",
2940
3179
  meta: {
2941
3180
  sha: commit.sha,
2942
3181
  shortSha: commit.shortSha,
@@ -3031,6 +3270,7 @@ function toMemoryNodes2(commit, projectId, opts = {}) {
3031
3270
  }
3032
3271
  ],
3033
3272
  signal: scoreFileDiff(commit.subject, file),
3273
+ provenance: "observed",
3034
3274
  meta: {
3035
3275
  sha: commit.sha,
3036
3276
  shortSha: commit.shortSha,
@@ -3094,6 +3334,8 @@ function toMemoryNodes3(file, projectId, opts = {}) {
3094
3334
  body: truncate(chunk2.text, maxBody),
3095
3335
  files: [{ path: file.path, insertions: null, deletions: null, binary: false }],
3096
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
3097
3339
  meta: {
3098
3340
  path: file.path,
3099
3341
  heading: chunk2.heading,
@@ -3255,6 +3497,8 @@ ${summary.body}`;
3255
3497
  files: extractMentionedFiles(session.turns.map((t) => `${t.userText}
3256
3498
  ${t.assistantText}`).join("\n")),
3257
3499
  signal: scoreSession(session.turns.length),
3500
+ provenance: "inferred",
3501
+ // a model's distillation, not a directly observed event
3258
3502
  meta: {
3259
3503
  sessionKey: session.sessionKey,
3260
3504
  source: session.source,
@@ -3363,6 +3607,7 @@ function toMemoryNode2(entry, projectId, opts = {}) {
3363
3607
  body: renderBody(entry, maxBody),
3364
3608
  files: [],
3365
3609
  signal: scoreShellCommand(entry),
3610
+ provenance: "observed",
3366
3611
  meta: {
3367
3612
  command: entry.command,
3368
3613
  cwd: entry.cwd,
@@ -3378,7 +3623,7 @@ function collectShellHistory(entries, projectId, opts = {}) {
3378
3623
  }
3379
3624
 
3380
3625
  // src/conversation/claude-code-reader.ts
3381
- import { readFile as readFile5 } from "fs/promises";
3626
+ import { readFile as readFile6 } from "fs/promises";
3382
3627
  import { basename as basename2 } from "path";
3383
3628
 
3384
3629
  // src/conversation/paths.ts
@@ -3471,14 +3716,14 @@ async function collectClaudeCodeTranscripts(repoRoot) {
3471
3716
  const files = await listTranscriptFiles(repoRoot);
3472
3717
  const turns = [];
3473
3718
  for (const file of files) {
3474
- const raw = await readFile5(file, "utf8");
3719
+ const raw = await readFile6(file, "utf8");
3475
3720
  turns.push(...parseClaudeCodeTranscript(raw, { sessionId: basename2(file, ".jsonl") }));
3476
3721
  }
3477
3722
  return turns;
3478
3723
  }
3479
3724
 
3480
3725
  // src/docs/read.ts
3481
- import { readFile as readFile6, stat } from "fs/promises";
3726
+ import { readFile as readFile7, stat } from "fs/promises";
3482
3727
  import { join as join7 } from "path";
3483
3728
  var DEFAULT_PATHSPECS = ["*.md"];
3484
3729
  async function listDocFiles(repoRoot, opts = {}) {
@@ -3496,7 +3741,7 @@ async function readDocFiles(repoRoot, opts = {}) {
3496
3741
  let content;
3497
3742
  let mtime;
3498
3743
  try {
3499
- [content, { mtime }] = await Promise.all([readFile6(absPath, "utf8"), stat(absPath)]);
3744
+ [content, { mtime }] = await Promise.all([readFile7(absPath, "utf8"), stat(absPath)]);
3500
3745
  } catch {
3501
3746
  unreadable.push(path);
3502
3747
  continue;
@@ -3508,10 +3753,10 @@ async function readDocFiles(repoRoot, opts = {}) {
3508
3753
 
3509
3754
  // src/shell/detect.ts
3510
3755
  import { existsSync as existsSync4 } from "fs";
3511
- import { readFile as readFile8, stat as stat2 } from "fs/promises";
3756
+ import { readFile as readFile9, stat as stat2 } from "fs/promises";
3512
3757
 
3513
3758
  // src/shell/hook-log.ts
3514
- import { appendFile, mkdir as mkdir5, readFile as readFile7 } from "fs/promises";
3759
+ import { appendFile, mkdir as mkdir5, readFile as readFile8 } from "fs/promises";
3515
3760
  import { dirname as dirname4 } from "path";
3516
3761
  function parseHookLogLine(line) {
3517
3762
  const trimmed = line.trim();
@@ -3536,7 +3781,7 @@ function parseHookLogLine(line) {
3536
3781
  async function readHookLog(path, fromLine) {
3537
3782
  let raw;
3538
3783
  try {
3539
- raw = await readFile7(path, "utf8");
3784
+ raw = await readFile8(path, "utf8");
3540
3785
  } catch {
3541
3786
  return { entries: [], totalLines: fromLine };
3542
3787
  }
@@ -3670,7 +3915,7 @@ function hookEntryToRaw(e) {
3670
3915
  }
3671
3916
  async function tryReadScrapeSource(path, parse, tailLines) {
3672
3917
  if (!existsSync4(path)) return null;
3673
- const [raw, stats] = await Promise.all([readFile8(path, "utf8"), stat2(path)]);
3918
+ const [raw, stats] = await Promise.all([readFile9(path, "utf8"), stat2(path)]);
3674
3919
  return parse(raw, stats.mtimeMs, { tailLines });
3675
3920
  }
3676
3921
  async function collectAvailableShellHistory(opts = {}) {
@@ -3824,7 +4069,7 @@ function reconcileProjectId(db, oldProjectId, newProjectId) {
3824
4069
  }
3825
4070
 
3826
4071
  // src/structure/collect.ts
3827
- import { readFile as readFile9 } from "fs/promises";
4072
+ import { readFile as readFile10 } from "fs/promises";
3828
4073
  import { join as join8 } from "path";
3829
4074
 
3830
4075
  // src/structure/extract.ts
@@ -3901,7 +4146,7 @@ async function collectFileEdges(repoRoot) {
3901
4146
  for (const path of paths) {
3902
4147
  let content;
3903
4148
  try {
3904
- content = await readFile9(join8(repoRoot, path), "utf8");
4149
+ content = await readFile10(join8(repoRoot, path), "utf8");
3905
4150
  } catch {
3906
4151
  unreadable.push(path);
3907
4152
  continue;
@@ -4005,25 +4250,25 @@ function addStats(into, from) {
4005
4250
  async function syncGit(store, projectId, opts, repo, config, log) {
4006
4251
  const totals = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
4007
4252
  if (!repo.head) {
4008
- log(`${pc6.yellow("git")} skipped -- repository has no commits yet`);
4253
+ log(`${pc7.yellow("git")} skipped -- repository has no commits yet`);
4009
4254
  return { totals, seen: 0 };
4010
4255
  }
4011
4256
  if (!config.sources.git.enabled) {
4012
- log(`${pc6.dim("git")} disabled in config`);
4257
+ log(`${pc7.dim("git")} disabled in config`);
4013
4258
  return { totals, seen: 0 };
4014
4259
  }
4015
4260
  let cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, GIT_SOURCE);
4016
4261
  if (cursor && !await isAncestor(repo.root, cursor, repo.head)) {
4017
- 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`);
4018
4263
  cursor = null;
4019
4264
  }
4020
4265
  if (cursor === repo.head) {
4021
- 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)}`);
4022
4267
  store.setSyncCursor(projectId, GIT_SOURCE, repo.head);
4023
4268
  return { totals, seen: 0 };
4024
4269
  }
4025
4270
  log(
4026
- `${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)"}`
4027
4272
  );
4028
4273
  let batch = [];
4029
4274
  let seen = 0;
@@ -4031,7 +4276,7 @@ async function syncGit(store, projectId, opts, repo, config, log) {
4031
4276
  if (batch.length === 0) return;
4032
4277
  addStats(totals, store.upsertNodes(batch));
4033
4278
  batch = [];
4034
- log(` ${pc6.dim(`${seen} commits read, ${totals.inserted} new`)}`);
4279
+ log(` ${pc7.dim(`${seen} commits read, ${totals.inserted} new`)}`);
4035
4280
  };
4036
4281
  const nodes = collectGitCommits(repo.root, projectId, {
4037
4282
  afterCommit: cursor,
@@ -4053,12 +4298,12 @@ async function syncDiffs(store, projectId, opts, repo, config, log) {
4053
4298
  const totals = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
4054
4299
  if (!repo.head) return { totals, seen: 0 };
4055
4300
  if (!config.sources.diff.enabled) {
4056
- log(`${pc6.dim("diff")} disabled in config`);
4301
+ log(`${pc7.dim("diff")} disabled in config`);
4057
4302
  return { totals, seen: 0 };
4058
4303
  }
4059
4304
  let cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, DIFF_SOURCE);
4060
4305
  if (cursor && !await isAncestor(repo.root, cursor, repo.head)) {
4061
- 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`);
4062
4307
  cursor = null;
4063
4308
  }
4064
4309
  if (cursor === repo.head) {
@@ -4087,13 +4332,13 @@ async function syncDiffs(store, projectId, opts, repo, config, log) {
4087
4332
  }
4088
4333
  flush();
4089
4334
  store.setSyncCursor(projectId, DIFF_SOURCE, repo.head);
4090
- log(` ${pc6.dim(`${DIFF_SOURCE}: ${seen} file diff(s) read`)}`);
4335
+ log(` ${pc7.dim(`${DIFF_SOURCE}: ${seen} file diff(s) read`)}`);
4091
4336
  return { totals, seen };
4092
4337
  }
4093
4338
  async function syncShell(store, projectId, opts, repoRoot, config, log) {
4094
4339
  const totals = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
4095
4340
  if (!config.sources.shell.enabled) {
4096
- log(`${pc6.dim("shell")} disabled in config`);
4341
+ log(`${pc7.dim("shell")} disabled in config`);
4097
4342
  return { totals, seen: 0 };
4098
4343
  }
4099
4344
  const results = await collectAvailableShellHistory({
@@ -4102,7 +4347,7 @@ async function syncShell(store, projectId, opts, repoRoot, config, log) {
4102
4347
  hookCursor: store.getSyncCursor(projectId, "shell:pwsh-hook")
4103
4348
  });
4104
4349
  if (results.length === 0) {
4105
- log(`${pc6.dim("shell")} no history source found on this machine`);
4350
+ log(`${pc7.dim("shell")} no history source found on this machine`);
4106
4351
  return { totals, seen: 0 };
4107
4352
  }
4108
4353
  let seen = 0;
@@ -4114,7 +4359,7 @@ async function syncShell(store, projectId, opts, repoRoot, config, log) {
4114
4359
  addStats(totals, store.upsertNodes(nodes));
4115
4360
  }
4116
4361
  store.setSyncCursor(projectId, sourceKey, result.cursorAfter ?? `scanned:${result.entries.length}`);
4117
- 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`)}`);
4118
4363
  }
4119
4364
  return { totals, seen };
4120
4365
  }
@@ -4126,13 +4371,13 @@ function syncConversation(store, projectId, turns, config, log, forceEnabled) {
4126
4371
  return { totals, seen: 0 };
4127
4372
  }
4128
4373
  if (turns.length === 0) {
4129
- log(`${pc6.dim("conversation")} no transcripts found`);
4374
+ log(`${pc7.dim("conversation")} no transcripts found`);
4130
4375
  return { totals, seen: 0 };
4131
4376
  }
4132
4377
  const nodes = collectConversationTurns(turns, projectId, { maxBodyChars: config.limits.maxBodyChars });
4133
4378
  if (nodes.length > 0) addStats(totals, store.upsertNodes(nodes));
4134
4379
  store.setSyncCursor(projectId, CONVERSATION_SOURCE, `scanned:${nodes.length}`);
4135
- 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`)}`);
4136
4381
  return { totals, seen: nodes.length };
4137
4382
  }
4138
4383
  var SESSION_SOURCE = "session:claude-code";
@@ -4141,7 +4386,7 @@ async function syncSessions(store, projectId, turns, config, log) {
4141
4386
  const settings = config.sources.session;
4142
4387
  if (!settings.enabled) return { totals, seen: 0 };
4143
4388
  if (turns.length === 0) {
4144
- log(`${pc6.dim("session")} no transcripts found`);
4389
+ log(`${pc7.dim("session")} no transcripts found`);
4145
4390
  return { totals, seen: 0 };
4146
4391
  }
4147
4392
  const result = await collectSessionSummaries(turns, projectId, new OllamaChatProvider({ model: settings.model }), {
@@ -4153,12 +4398,12 @@ async function syncSessions(store, projectId, turns, config, log) {
4153
4398
  const meta = store.getNodeMeta(makeNodeId(projectId, "session_summary", sessionKey));
4154
4399
  return typeof meta?.contentHash === "string" ? meta.contentHash : null;
4155
4400
  },
4156
- onProgress: (done, total) => log(` ${pc6.dim(`session: summarizing ${done}/${total}`)}`)
4401
+ onProgress: (done, total) => log(` ${pc7.dim(`session: summarizing ${done}/${total}`)}`)
4157
4402
  });
4158
4403
  if (result.nodes.length > 0) addStats(totals, store.upsertNodes(result.nodes));
4159
4404
  if (result.providerUnavailable) {
4160
4405
  log(
4161
- `${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`
4162
4407
  );
4163
4408
  } else {
4164
4409
  const parts = [`${result.nodes.length} summarized`];
@@ -4166,7 +4411,7 @@ async function syncSessions(store, projectId, turns, config, log) {
4166
4411
  if (result.deferred > 0) parts.push(`${result.deferred} queued for the next sync`);
4167
4412
  if (result.unsettled > 0) parts.push(`${result.unsettled} still active`);
4168
4413
  if (result.failed > 0) parts.push(`${result.failed} failed`);
4169
- log(` ${pc6.dim(`${SESSION_SOURCE}: ${parts.join(", ")}`)}`);
4414
+ log(` ${pc7.dim(`${SESSION_SOURCE}: ${parts.join(", ")}`)}`);
4170
4415
  }
4171
4416
  store.setSyncCursor(projectId, SESSION_SOURCE, `scanned:${result.nodes.length}`);
4172
4417
  return { totals, seen: result.nodes.length };
@@ -4175,7 +4420,7 @@ var DOCS_SOURCE = "docs";
4175
4420
  async function syncDocs(store, projectId, repoRoot, config, log) {
4176
4421
  const totals = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
4177
4422
  if (!config.sources.docs.enabled) {
4178
- log(`${pc6.dim("docs")} disabled in config`);
4423
+ log(`${pc7.dim("docs")} disabled in config`);
4179
4424
  return { totals, seen: 0 };
4180
4425
  }
4181
4426
  const { files, unreadable } = await readDocFiles(repoRoot, { include: config.sources.docs.include });
@@ -4189,23 +4434,23 @@ async function syncDocs(store, projectId, repoRoot, config, log) {
4189
4434
  );
4190
4435
  store.setSyncCursor(projectId, DOCS_SOURCE, `scanned:${nodes.length}`);
4191
4436
  if (files.length === 0 && unreadable.length === 0) {
4192
- log(`${pc6.dim("docs")} no tracked .md files found`);
4437
+ log(`${pc7.dim("docs")} no tracked .md files found`);
4193
4438
  } else {
4194
- const prunedPart = pruned > 0 ? `, ${pc6.yellow(`${pruned} stale removed`)}` : "";
4439
+ const prunedPart = pruned > 0 ? `, ${pc7.yellow(`${pruned} stale removed`)}` : "";
4195
4440
  const skippedPart = unreadable.length > 0 ? `, ${unreadable.length} unreadable (kept)` : "";
4196
- 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)}`);
4197
4442
  }
4198
4443
  return { totals, seen: nodes.length };
4199
4444
  }
4200
4445
  async function syncStructure(store, projectId, repoRoot, config, log) {
4201
4446
  if (!config.sources.structure.enabled) {
4202
- log(`${pc6.dim("structure")} disabled in config`);
4447
+ log(`${pc7.dim("structure")} disabled in config`);
4203
4448
  return { edges: 0, filesScanned: 0 };
4204
4449
  }
4205
4450
  const { edges, filesScanned, unreadable } = await collectFileEdges(repoRoot);
4206
4451
  store.replaceFileEdges(projectId, edges);
4207
4452
  const skippedPart = unreadable.length > 0 ? `, ${unreadable.length} unreadable (skipped)` : "";
4208
- 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)}`);
4209
4454
  return { edges: edges.length, filesScanned };
4210
4455
  }
4211
4456
  var STALE_SHELL_SOURCES = ["shell:pwsh", "shell:bash", "shell:zsh"];
@@ -4222,15 +4467,15 @@ function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
4222
4467
  const counts = sources.flatMap((source) => scopeIds.map((id) => ({ source, id, count: store.countSourceNodes(id, source) })));
4223
4468
  const total = counts.reduce((sum, c) => sum + c.count, 0);
4224
4469
  if (total === 0) {
4225
- 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
4226
4471
  `);
4227
4472
  return 0;
4228
4473
  }
4229
- 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)`;
4230
4475
  if (!yes) {
4231
4476
  const lines = counts.filter((c) => c.count > 0).map(describe);
4232
4477
  out(
4233
- [`${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(
4234
4479
  "\n"
4235
4480
  )
4236
4481
  );
@@ -4239,7 +4484,7 @@ function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
4239
4484
  let removed = 0;
4240
4485
  for (const { source, id } of counts) removed += store.pruneSourceNodes(id, source, []);
4241
4486
  const identityPart = otherProjectIds.length > 0 ? `, ${scopeIds.length} project identit${scopeIds.length === 1 ? "y" : "ies"}` : "";
4242
- 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}
4243
4488
  `);
4244
4489
  return 0;
4245
4490
  }
@@ -4256,7 +4501,7 @@ async function runSync(opts) {
4256
4501
  store.upsertProject({ id: projectId, root: repo.root, originUrl: repo.originUrl });
4257
4502
  if (opts.rebuild) {
4258
4503
  const removed = store.clearProject(projectId);
4259
- log(`${pc6.dim("rebuild")} dropped ${removed} existing node(s)`);
4504
+ log(`${pc7.dim("rebuild")} dropped ${removed} existing node(s)`);
4260
4505
  }
4261
4506
  const staleProjectIds = store.listOtherProjectIds(projectId);
4262
4507
  for (const staleId of staleProjectIds) {
@@ -4270,7 +4515,7 @@ async function runSync(opts) {
4270
4515
  ].filter((part) => part !== null);
4271
4516
  if (parts.length > 0) {
4272
4517
  log(
4273
- `${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(", ")}`
4274
4519
  );
4275
4520
  }
4276
4521
  }
@@ -4300,26 +4545,26 @@ async function runSync(opts) {
4300
4545
  let lastLogged = 0;
4301
4546
  const result = await embedPendingNodes(store, new OllamaEmbeddingProvider(), projectId, {
4302
4547
  maxNodes: opts.embedLimit,
4303
- 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`),
4304
4549
  onProgress: (attempted, total) => {
4305
4550
  if (total < PROGRESS_THRESHOLD || attempted - lastLogged < PROGRESS_EVERY) return;
4306
4551
  lastLogged = attempted;
4307
- log(` ${pc6.dim(`vector: ${attempted}/${total} embedded`)}`);
4552
+ log(` ${pc7.dim(`vector: ${attempted}/${total} embedded`)}`);
4308
4553
  }
4309
4554
  });
4310
4555
  if (result.embedded > 0) {
4311
- const skippedPart = result.skipped > 0 ? pc6.dim(`, ${result.skipped} skipped`) : "";
4312
- const remainingPart = result.remaining > 0 ? pc6.yellow(`, ${result.remaining} still pending`) : "";
4313
- 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}
4314
4559
  `;
4315
4560
  } else if (result.providerUnavailable) {
4316
- 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`);
4317
4562
  }
4318
4563
  }
4319
4564
  let linkLine = "";
4320
4565
  if (opts.linkFailures) {
4321
4566
  const linkStats = correlateFailures(store, projectId);
4322
- 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`)}
4323
4568
  `;
4324
4569
  }
4325
4570
  store.markSynced(projectId);
@@ -4337,12 +4582,12 @@ async function runSync(opts) {
4337
4582
  const docsPart = config.sources.docs.enabled ? `, ${docs.seen} doc section(s)` : "";
4338
4583
  const diffPart = config.sources.diff.enabled ? `, ${diffs.seen} file diff(s)` : "";
4339
4584
  const structurePart = config.sources.structure.enabled ? `, ${structure.edges} import edge(s)` : "";
4340
- const deniedPart = totals.denied > 0 ? ` ${pc6.red(`-${totals.denied} denied`)}` : "";
4585
+ const deniedPart = totals.denied > 0 ? ` ${pc7.red(`-${totals.denied} denied`)}` : "";
4341
4586
  out(
4342
4587
  [
4343
- `${pc6.green("synced")} ${git2.seen} commit(s)${diffPart}, ${shell.seen} shell entr${shell.seen === 1 ? "y" : "ies"}${conversationPart}${sessionPart}${docsPart}${structurePart} in ${elapsed}s`,
4344
- ` ${pc6.green(`+${totals.inserted} new`)} ${pc6.yellow(`~${totals.updated} updated`)} ${pc6.dim(`=${totals.unchanged} unchanged`)}${deniedPart}`,
4345
- ` ${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)`)}`,
4346
4591
  ""
4347
4592
  ].join("\n") + embedLine + linkLine
4348
4593
  );
@@ -4453,10 +4698,10 @@ function createServer() {
4453
4698
  title: "Search remembered project history",
4454
4699
  description: "Search a NexusMem-tracked repository's remembered history: git commits, code diffs (the patch of each changed file), shell commands, tracked markdown docs, and (if enabled) conversation transcripts and per-session summaries. Returns a token-budgeted, ranked context block -- not raw search results.",
4455
4700
  inputSchema: {
4456
- projectRoot: z3.string().describe("Absolute path to the repository root"),
4457
- query: z3.string().describe("Free-text question or search terms"),
4458
- budget: z3.number().int().positive().optional().describe("Max tokens in the returned context block. Default 2000."),
4459
- allProjects: z3.boolean().optional().describe(
4701
+ projectRoot: z4.string().describe("Absolute path to the repository root"),
4702
+ query: z4.string().describe("Free-text question or search terms"),
4703
+ budget: z4.number().int().positive().optional().describe("Max tokens in the returned context block. Default 2000."),
4704
+ allProjects: z4.boolean().optional().describe(
4460
4705
  "Search every repository NexusMem has been run in on this machine, not just projectRoot. Use when the answer may live in a different project (a pattern solved elsewhere, a tool that failed the same way before). Each result is tagged with its repository."
4461
4706
  )
4462
4707
  }
@@ -4483,10 +4728,10 @@ function createServer() {
4483
4728
  title: "Sync remembered history",
4484
4729
  description: "Ingest new git, diff, shell, docs and (if enabled) conversation history for a NexusMem-tracked repository into its local database. Pass pruneSource or pruneStaleShell instead to delete a dead source's nodes (e.g. the pre-hook shell scrape) rather than syncing -- dry-run unless yes is also true, since this is an irreversible full wipe of that source.",
4485
4730
  inputSchema: {
4486
- projectRoot: z3.string().describe("Absolute path to the repository root"),
4487
- pruneSource: z3.string().optional().describe('Delete every node from this exact source (e.g. "shell:pwsh") instead of syncing'),
4488
- pruneStaleShell: z3.boolean().optional().describe("Shortcut for pruneSource on shell:pwsh, shell:bash and shell:zsh at once -- the dead pre-hook scrape sources"),
4489
- yes: z3.boolean().optional().describe("Confirms the delete. Without it, pruneSource/pruneStaleShell only report the matching count.")
4731
+ projectRoot: z4.string().describe("Absolute path to the repository root"),
4732
+ pruneSource: z4.string().optional().describe('Delete every node from this exact source (e.g. "shell:pwsh") instead of syncing'),
4733
+ pruneStaleShell: z4.boolean().optional().describe("Shortcut for pruneSource on shell:pwsh, shell:bash and shell:zsh at once -- the dead pre-hook scrape sources"),
4734
+ yes: z4.boolean().optional().describe("Confirms the delete. Without it, pruneSource/pruneStaleShell only report the matching count.")
4490
4735
  }
4491
4736
  },
4492
4737
  async ({ projectRoot, pruneSource, pruneStaleShell, yes }) => {
@@ -4500,7 +4745,7 @@ function createServer() {
4500
4745
  title: "Show what is remembered",
4501
4746
  description: "Report how many nodes NexusMem currently remembers for a repository, broken down by kind and source.",
4502
4747
  inputSchema: {
4503
- projectRoot: z3.string().describe("Absolute path to the repository root")
4748
+ projectRoot: z4.string().describe("Absolute path to the repository root")
4504
4749
  }
4505
4750
  },
4506
4751
  async ({ projectRoot }) => {
@@ -4517,8 +4762,8 @@ function createServer() {
4517
4762
  title: "List recently remembered items",
4518
4763
  description: "List the most recently remembered items for a NexusMem-tracked repository -- git commits, code diffs, shell commands, tracked docs, and (if enabled) conversation transcripts and session summaries -- newest first. Chronological, not relevance-ranked: use search_memory instead for a specific question.",
4519
4764
  inputSchema: {
4520
- projectRoot: z3.string().describe("Absolute path to the repository root"),
4521
- limit: z3.number().int().positive().optional().describe("Max items to return, newest first. Default 20.")
4765
+ projectRoot: z4.string().describe("Absolute path to the repository root"),
4766
+ limit: z4.number().int().positive().optional().describe("Max items to return, newest first. Default 20.")
4522
4767
  }
4523
4768
  },
4524
4769
  async ({ projectRoot, limit }) => {
@@ -4538,7 +4783,7 @@ async function runMcpServer() {
4538
4783
  }
4539
4784
 
4540
4785
  // src/cli/commands/precheck.ts
4541
- import pc7 from "picocolors";
4786
+ import pc8 from "picocolors";
4542
4787
 
4543
4788
  // src/correlate/precheck.ts
4544
4789
  var DEFAULT_RECENT_DAYS = 30;
@@ -4595,7 +4840,7 @@ async function runPrecheck(opts) {
4595
4840
  const { repo, ws, projectId } = await loadContext(opts.cwd);
4596
4841
  const targetFiles = opts.files ?? (opts.working ? await workingTreeFiles(repo.root) : await stagedFiles(repo.root));
4597
4842
  if (targetFiles.length === 0) {
4598
- if (!opts.quiet) out(`${pc7.dim("precheck")} no files to check
4843
+ if (!opts.quiet) out(`${pc8.dim("precheck")} no files to check
4599
4844
  `);
4600
4845
  return 0;
4601
4846
  }
@@ -4608,45 +4853,45 @@ async function runPrecheck(opts) {
4608
4853
  }
4609
4854
  const flagged = risks.filter((r) => r.unresolvedFailures.length > 0 || r.commitsRecent >= HIGH_CHURN_THRESHOLD);
4610
4855
  if (flagged.length === 0) {
4611
- 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
4612
4857
  `);
4613
4858
  return 0;
4614
4859
  }
4615
4860
  out(`
4616
- ${pc7.bold("nexusmem precheck")}
4617
- ${pc7.dim("-".repeat(40))}
4861
+ ${pc8.bold("nexusmem precheck")}
4862
+ ${pc8.dim("-".repeat(40))}
4618
4863
 
4619
4864
  `);
4620
4865
  for (const risk of flagged) {
4621
- out(` ${pc7.bold(risk.path)}
4866
+ out(` ${pc8.bold(risk.path)}
4622
4867
  `);
4623
4868
  if (risk.unresolvedFailures.length > 0) {
4624
- 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):
4625
4870
  `);
4626
4871
  for (const f of risk.unresolvedFailures.slice(0, 3)) {
4627
- 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)}`)}
4628
4873
  `);
4629
4874
  }
4630
4875
  if (risk.unresolvedFailures.length > 3) {
4631
- out(` ${pc7.dim(`... and ${risk.unresolvedFailures.length - 3} more`)}
4876
+ out(` ${pc8.dim(`... and ${risk.unresolvedFailures.length - 3} more`)}
4632
4877
  `);
4633
4878
  }
4634
4879
  }
4635
4880
  if (risk.commitsRecent >= HIGH_CHURN_THRESHOLD) {
4636
- 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
4637
4882
  `);
4638
4883
  }
4639
4884
  out("\n");
4640
4885
  }
4641
4886
  const failureCount = flagged.filter((r) => r.unresolvedFailures.length > 0).length;
4642
- 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.`)}
4643
4888
  `);
4644
4889
  if (opts.strict && failureCount > 0) return 1;
4645
4890
  return 0;
4646
4891
  }
4647
4892
 
4648
4893
  // src/cli/commands/query.ts
4649
- import pc8 from "picocolors";
4894
+ import pc9 from "picocolors";
4650
4895
  async function runQuery(opts) {
4651
4896
  const { repo, ws, projectId } = await loadContext(opts.cwd);
4652
4897
  const opened = opts.allProjects ? await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath }) : null;
@@ -4668,15 +4913,15 @@ async function runQuery(opts) {
4668
4913
  const { bm25Count, vectorCount, hits, packed } = result;
4669
4914
  if (opened && !opts.json) {
4670
4915
  const searched = opened.sources.map((s) => s.label).join(", ");
4671
- 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}
4672
4917
  `);
4673
4918
  for (const { entry } of opened.unreadable) {
4674
- process.stderr.write(`${pc8.yellow("unreadable")} ${entry.root} -- skipped
4919
+ process.stderr.write(`${pc9.yellow("unreadable")} ${entry.root} -- skipped
4675
4920
  `);
4676
4921
  }
4677
4922
  if (opened.missing.length > 0) {
4678
4923
  process.stderr.write(
4679
- `${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)")}
4680
4925
  `
4681
4926
  );
4682
4927
  }
@@ -4706,15 +4951,15 @@ async function runQuery(opts) {
4706
4951
  return 0;
4707
4952
  }
4708
4953
  if (matched === 0) {
4709
- process.stderr.write(`${pc8.yellow("no matches")} for "${opts.query}"
4954
+ process.stderr.write(`${pc9.yellow("no matches")} for "${opts.query}"
4710
4955
  `);
4711
4956
  return 0;
4712
4957
  }
4713
4958
  process.stderr.write(
4714
4959
  [
4715
- `${pc8.dim("matched")} ${bm25Count} bm25${vectorCount > 0 ? ` + ${vectorCount} vector` : ""}, packed ${pc8.bold(String(packed.nodes.length))} into budget`,
4716
- `${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)`) : ""),
4717
- 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)`)}` : "",
4718
4963
  ""
4719
4964
  ].filter(Boolean).join("\n")
4720
4965
  );
@@ -4728,10 +4973,10 @@ async function runQuery(opts) {
4728
4973
  }
4729
4974
 
4730
4975
  // src/cli/commands/scan-conversation.ts
4731
- import pc10 from "picocolors";
4976
+ import pc11 from "picocolors";
4732
4977
 
4733
4978
  // src/cli/format.ts
4734
- import pc9 from "picocolors";
4979
+ import pc10 from "picocolors";
4735
4980
  var GIT_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
4736
4981
  var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
4737
4982
  var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
@@ -4743,9 +4988,9 @@ function signalBand(signal, bands) {
4743
4988
  return "low";
4744
4989
  }
4745
4990
  var BAND_COLOR = {
4746
- high: pc9.green,
4747
- medium: pc9.yellow,
4748
- low: pc9.dim
4991
+ high: pc10.green,
4992
+ medium: pc10.yellow,
4993
+ low: pc10.dim
4749
4994
  };
4750
4995
  function formatSignal(signal, bands) {
4751
4996
  return BAND_COLOR[signalBand(signal, bands)](signal.toFixed(2));
@@ -4758,9 +5003,9 @@ async function runScanConversation(opts) {
4758
5003
  const files = await listTranscriptFiles(repo.root);
4759
5004
  if (!opts.json) {
4760
5005
  process.stderr.write(
4761
- 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)}
4762
5007
 
4763
- ` : `${pc10.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
5008
+ ` : `${pc11.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
4764
5009
  `
4765
5010
  );
4766
5011
  }
@@ -4777,7 +5022,7 @@ async function runScanConversation(opts) {
4777
5022
  const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
4778
5023
  process.stderr.write(
4779
5024
  `
4780
- ${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"
4781
5026
  );
4782
5027
  return 0;
4783
5028
  }
@@ -4786,20 +5031,20 @@ function formatNode(node) {
4786
5031
  }
4787
5032
 
4788
5033
  // src/cli/commands/scan-diff.ts
4789
- import pc12 from "picocolors";
5034
+ import pc13 from "picocolors";
4790
5035
 
4791
5036
  // src/cli/commands/scan-git.ts
4792
- import pc11 from "picocolors";
5037
+ import pc12 from "picocolors";
4793
5038
  async function runScanGit(opts) {
4794
5039
  const repo = await readRepoInfo(opts.cwd);
4795
5040
  const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
4796
5041
  if (!opts.json) {
4797
5042
  process.stderr.write(
4798
5043
  [
4799
- `${pc11.dim("repo ")} ${repo.root}`,
4800
- `${pc11.dim("branch ")} ${repo.branch ?? pc11.yellow("(detached)")}`,
4801
- `${pc11.dim("origin ")} ${repo.originUrl ?? pc11.dim("(none)")}`,
4802
- `${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)}`,
4803
5048
  ""
4804
5049
  ].join("\n")
4805
5050
  );
@@ -4833,14 +5078,14 @@ function formatNode2(node) {
4833
5078
  const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
4834
5079
  return [
4835
5080
  formatSignal(node.signal, GIT_SIGNAL_BANDS),
4836
- pc11.dim(date),
4837
- pc11.magenta(sha),
5081
+ pc12.dim(date),
5082
+ pc12.magenta(sha),
4838
5083
  node.title,
4839
- pc11.dim(`(${files} file${files === 1 ? "" : "s"}, ${churn})`)
5084
+ pc12.dim(`(${files} file${files === 1 ? "" : "s"}, ${churn})`)
4840
5085
  ].join(" ");
4841
5086
  }
4842
5087
  function summarize2(nodes) {
4843
- if (nodes.length === 0) return pc11.yellow("no commits matched");
5088
+ if (nodes.length === 0) return pc12.yellow("no commits matched");
4844
5089
  const timestamps = nodes.map((n) => n.ts).sort();
4845
5090
  const avgSignal = nodes.reduce((n, x) => n + x.signal, 0) / nodes.length;
4846
5091
  const totalTokens = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
@@ -4850,7 +5095,7 @@ function summarize2(nodes) {
4850
5095
  }
4851
5096
  const hottest = [...fileHits.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([path, count]) => ` ${String(count).padStart(3)}x ${path}`);
4852
5097
  return [
4853
- `${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)}`)}`,
4854
5099
  ` avg signal ${avgSignal.toFixed(3)} ~${totalTokens.toLocaleString()} tokens if sent raw`,
4855
5100
  hottest.length ? ` hottest files:
4856
5101
  ${hottest.join("\n")}` : ""
@@ -4865,9 +5110,9 @@ async function runScanDiff(opts) {
4865
5110
  if (!opts.json) {
4866
5111
  process.stderr.write(
4867
5112
  [
4868
- `${pc12.dim("repo ")} ${repo.root}`,
4869
- `${pc12.dim("branch ")} ${repo.branch ?? pc12.yellow("(detached)")}`,
4870
- `${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)}`,
4871
5116
  ""
4872
5117
  ].join("\n")
4873
5118
  );
@@ -4897,28 +5142,28 @@ function formatNode3(node) {
4897
5142
  const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
4898
5143
  return [
4899
5144
  formatSignal(node.signal, DIFF_SIGNAL_BANDS),
4900
- pc12.dim(node.ts.slice(0, 10)),
4901
- pc12.magenta(sha),
5145
+ pc13.dim(node.ts.slice(0, 10)),
5146
+ pc13.magenta(sha),
4902
5147
  String(node.meta.path ?? ""),
4903
- pc12.dim(`(${churn}, ${node.meta.hunkCount ?? 0} hunk(s))`)
5148
+ pc13.dim(`(${churn}, ${node.meta.hunkCount ?? 0} hunk(s))`)
4904
5149
  ].join(" ");
4905
5150
  }
4906
5151
 
4907
5152
  // src/cli/commands/scan-docs.ts
4908
- import pc13 from "picocolors";
5153
+ import pc14 from "picocolors";
4909
5154
  async function runScanDocs(opts) {
4910
5155
  const repo = await readRepoInfo(opts.cwd);
4911
5156
  const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
4912
5157
  const { files, unreadable } = await readDocFiles(repo.root);
4913
5158
  if (!opts.json) {
4914
5159
  process.stderr.write(
4915
- 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(", ")}
4916
5161
 
4917
- ` : `${pc13.yellow("no tracked .md files found")}
5162
+ ` : `${pc14.yellow("no tracked .md files found")}
4918
5163
  `
4919
5164
  );
4920
5165
  if (unreadable.length > 0) {
4921
- process.stderr.write(`${pc13.yellow("unreadable")} ${unreadable.join(", ")}
5166
+ process.stderr.write(`${pc14.yellow("unreadable")} ${unreadable.join(", ")}
4922
5167
 
4923
5168
  `);
4924
5169
  }
@@ -4934,7 +5179,7 @@ async function runScanDocs(opts) {
4934
5179
  const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
4935
5180
  process.stderr.write(
4936
5181
  `
4937
- ${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`)}
4938
5183
  `
4939
5184
  );
4940
5185
  return 0;
@@ -4944,13 +5189,13 @@ function formatNode4(node) {
4944
5189
  }
4945
5190
 
4946
5191
  // src/cli/commands/scan-session.ts
4947
- import pc14 from "picocolors";
5192
+ import pc15 from "picocolors";
4948
5193
  async function runScanSession(opts) {
4949
5194
  const repo = await readRepoInfo(opts.cwd);
4950
5195
  const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
4951
5196
  const turns = await collectClaudeCodeTranscripts(repo.root);
4952
5197
  if (turns.length === 0) {
4953
- 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)}
4954
5199
  `);
4955
5200
  return 0;
4956
5201
  }
@@ -4958,7 +5203,7 @@ async function runScanSession(opts) {
4958
5203
  const settled = selectSettledSessions(sessions, opts.settleMinutes);
4959
5204
  if (!opts.json) {
4960
5205
  process.stderr.write(
4961
- `${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+)
4962
5207
 
4963
5208
  `
4964
5209
  );
@@ -4984,7 +5229,7 @@ async function runScanSession(opts) {
4984
5229
  }
4985
5230
  for (const preview of previews) {
4986
5231
  process.stdout.write(
4987
- `${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
4988
5233
  ${preview.prompt}
4989
5234
 
4990
5235
  `
@@ -4996,7 +5241,7 @@ ${preview.prompt}
4996
5241
  settleMinutes: opts.settleMinutes,
4997
5242
  maxSessions: opts.maxSessions,
4998
5243
  onProgress: (done, total) => {
4999
- if (!opts.json) process.stderr.write(` ${pc14.dim(`summarizing ${done}/${total}`)}
5244
+ if (!opts.json) process.stderr.write(` ${pc15.dim(`summarizing ${done}/${total}`)}
5000
5245
  `);
5001
5246
  }
5002
5247
  });
@@ -5006,21 +5251,21 @@ ${preview.prompt}
5006
5251
  return 0;
5007
5252
  }
5008
5253
  for (const node of result.nodes) {
5009
- process.stdout.write(`${pc14.bold(node.title)}
5010
- ${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", " "))}
5011
5256
  ${node.body}
5012
5257
 
5013
5258
  `);
5014
5259
  }
5015
5260
  if (result.providerUnavailable) {
5016
5261
  process.stderr.write(
5017
- `${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}\`)
5018
5263
  `
5019
5264
  );
5020
5265
  return 0;
5021
5266
  }
5022
5267
  process.stderr.write(
5023
- `${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})`)}
5024
5269
  `
5025
5270
  );
5026
5271
  return 0;
@@ -5028,16 +5273,16 @@ ${node.body}
5028
5273
  var SCAN_SESSION_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
5029
5274
 
5030
5275
  // src/cli/commands/scan-shell.ts
5031
- import pc15 from "picocolors";
5276
+ import pc16 from "picocolors";
5032
5277
  async function runScanShell(opts) {
5033
5278
  const repo = await readRepoInfo(opts.cwd);
5034
5279
  const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
5035
5280
  const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
5036
5281
  if (!opts.json) {
5037
5282
  process.stderr.write(
5038
- 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(", ")}
5039
5284
 
5040
- ` : `${pc15.yellow("no shell history source found on this machine")}
5285
+ ` : `${pc16.yellow("no shell history source found on this machine")}
5041
5286
  `
5042
5287
  );
5043
5288
  }
@@ -5046,7 +5291,7 @@ async function runScanShell(opts) {
5046
5291
  const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
5047
5292
  allNodes.push(...nodes);
5048
5293
  if (!opts.json) {
5049
- 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)`)}
5050
5295
  `);
5051
5296
  for (const node of nodes) process.stdout.write(`${formatNode5(node)}
5052
5297
  `);
@@ -5059,19 +5304,19 @@ async function runScanShell(opts) {
5059
5304
  return 0;
5060
5305
  }
5061
5306
  const approxTotal = allNodes.reduce((n, x) => n + approxTokens(x.body), 0);
5062
- 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`)}
5063
5308
  `);
5064
5309
  return 0;
5065
5310
  }
5066
5311
  function formatNode5(node) {
5067
- const approx = node.meta.tsApprox ? pc15.dim("~") : " ";
5312
+ const approx = node.meta.tsApprox ? pc16.dim("~") : " ";
5068
5313
  const exit = node.meta.exitCode;
5069
- const exitLabel = typeof exit === "number" && exit !== 0 ? pc15.red(`exit ${exit}`) : "";
5314
+ const exitLabel = typeof exit === "number" && exit !== 0 ? pc16.red(`exit ${exit}`) : "";
5070
5315
  return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
5071
5316
  }
5072
5317
 
5073
5318
  // src/cli/commands/scan-structure.ts
5074
- import pc16 from "picocolors";
5319
+ import pc17 from "picocolors";
5075
5320
  async function runScanStructure(opts) {
5076
5321
  const repo = await readRepoInfo(opts.cwd);
5077
5322
  const { edges, filesScanned, unreadable } = await collectFileEdges(repo.root);
@@ -5081,17 +5326,17 @@ async function runScanStructure(opts) {
5081
5326
  return 0;
5082
5327
  }
5083
5328
  if (unreadable.length > 0) {
5084
- process.stderr.write(`${pc16.yellow("unreadable")} ${unreadable.join(", ")}
5329
+ process.stderr.write(`${pc17.yellow("unreadable")} ${unreadable.join(", ")}
5085
5330
 
5086
5331
  `);
5087
5332
  }
5088
5333
  for (const edge of edges) {
5089
- process.stdout.write(`${edge.fromPath} ${pc16.dim("->")} ${edge.toPath}
5334
+ process.stdout.write(`${edge.fromPath} ${pc17.dim("->")} ${edge.toPath}
5090
5335
  `);
5091
5336
  }
5092
5337
  process.stderr.write(
5093
5338
  `
5094
- ${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)
5095
5340
  `
5096
5341
  );
5097
5342
  return 0;
@@ -5099,7 +5344,7 @@ ${pc16.bold(String(edges.length))} edge(s) from ${filesScanned} tracked .ts/.tsx
5099
5344
 
5100
5345
  // src/cli/commands/status.ts
5101
5346
  import { statSync } from "fs";
5102
- import pc17 from "picocolors";
5347
+ import pc18 from "picocolors";
5103
5348
  function humanBytes(bytes) {
5104
5349
  if (bytes < 1024) return `${bytes} B`;
5105
5350
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -5127,32 +5372,32 @@ async function runStatus(opts) {
5127
5372
  const structure = store.fileEdgeStats(projectId);
5128
5373
  const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
5129
5374
  const kinds = Object.entries(stats.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
5130
- 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(
5131
5376
  "nexusmem sync --prune-source <name>"
5132
5377
  )} to remove stale source data` : "";
5133
5378
  out(
5134
5379
  [
5135
- `${pc17.dim("repo ")} ${repo.root}`,
5136
- `${pc17.dim("branch ")} ${repo.branch ?? pc17.yellow("(detached)")}`,
5137
- `${pc17.dim("project ")} ${pc17.cyan(projectId)}`,
5138
- `${pc17.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc17.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
5139
- `${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)})`)}`,
5140
5385
  staleProjectWarning,
5141
5386
  "",
5142
- `${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)}`)}` : ""}`,
5143
5388
  ...kinds,
5144
- stats.total ? ` ${pc17.dim(`${stats.distinctFiles} distinct file path(s)`)}` : "",
5389
+ stats.total ? ` ${pc18.dim(`${stats.distinctFiles} distinct file path(s)`)}` : "",
5145
5390
  "",
5146
- sources.length ? pc17.dim("sources") : pc17.yellow("no sources synced yet"),
5391
+ sources.length ? pc18.dim("sources") : pc18.yellow("no sources synced yet"),
5147
5392
  ...sources.map((s) => {
5148
5393
  const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
5149
5394
  const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
5150
- 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}`)}`;
5151
5396
  }),
5152
- 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")}` : "",
5153
5398
  "",
5154
- 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` : ""}` : "",
5155
- 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)` : ""
5156
5401
  ].filter((line) => line !== "").join("\n").concat("\n")
5157
5402
  );
5158
5403
  return 0;
@@ -5167,7 +5412,7 @@ function isExpected(err) {
5167
5412
  // the user fixes, not stack traces they debug.
5168
5413
  err instanceof GitSpawnError || // Survived every retry, so git is genuinely unstable on this machine
5169
5414
  // (antivirus, a bad install). Actionable, and not our stack to print.
5170
- 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;
5171
5416
  }
5172
5417
  function guard(run) {
5173
5418
  return async () => {
@@ -5175,7 +5420,7 @@ function guard(run) {
5175
5420
  process.exitCode = await run();
5176
5421
  } catch (err) {
5177
5422
  if (isExpected(err)) {
5178
- process.stderr.write(`${pc18.red("error")} ${err.message}
5423
+ process.stderr.write(`${pc19.red("error")} ${err.message}
5179
5424
  `);
5180
5425
  process.exitCode = 1;
5181
5426
  return;
@@ -5254,7 +5499,7 @@ program.command("query").description("Search remembered history and print a toke
5254
5499
  );
5255
5500
  program.command("forget").description(
5256
5501
  "Permanently deny-list a value: deletes matching nodes now and blocks it from ever being re-ingested (irreversible)"
5257
- ).argument("[value]", "exact text to forget (see --regex); omit with --list").option("-C, --cwd <path>", "repository path", process.cwd()).option("--regex", "treat <value> as a regular expression instead of a literal substring", false).option("--ignore-case", "case-insensitive match", false).option("--reason <text>", "free-text note stored with the deny-list entry").option("--list", "list active deny-list entries instead of forgetting a new value", false).option("--yes", "confirm the irreversible delete + deny-list write", false).action(
5502
+ ).argument("[value]", "exact text to forget (see --regex); omit with --list").option("-C, --cwd <path>", "repository path", process.cwd()).option("--regex", "treat <value> as a regular expression instead of a literal substring", false).option("--ignore-case", "case-insensitive match", false).option("--reason <text>", "free-text note stored with the deny-list entry").option("--list", "list active deny-list entries instead of forgetting a new value", false).option("--export <path>", "write this project's deny-list to a JSON file, for --import in another checkout").option("--import <path>", "re-apply a deny-list JSON file (from --export) against this project").option("--yes", "confirm the irreversible delete + deny-list write", false).action(
5258
5503
  (value, options) => guard(
5259
5504
  () => runForget({
5260
5505
  cwd: options.cwd,
@@ -5263,10 +5508,17 @@ program.command("forget").description(
5263
5508
  ignoreCase: options.ignoreCase,
5264
5509
  reason: options.reason,
5265
5510
  list: options.list,
5511
+ export: options.export,
5512
+ import: options.import,
5266
5513
  yes: options.yes
5267
5514
  })
5268
5515
  )()
5269
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
+ );
5270
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 }))());
5271
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(
5272
5524
  (options) => guard(
@@ -5327,7 +5579,7 @@ program.command("scan-structure").description("Preview the JS/TS import-graph ed
5327
5579
  program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
5328
5580
  program.parseAsync(process.argv).catch((err) => {
5329
5581
  const message = err instanceof Error ? err.message : String(err);
5330
- process.stderr.write(`${pc18.red("error")} ${message}
5582
+ process.stderr.write(`${pc19.red("error")} ${message}
5331
5583
  `);
5332
5584
  process.exitCode = 1;
5333
5585
  });