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/CHANGELOG.md +49 -1
- package/README.md +84 -57
- package/dist/cli/index.js +440 -188
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
|
1669
|
-
var ENTRY_SCHEMA =
|
|
1670
|
-
projectId:
|
|
1671
|
-
root:
|
|
1672
|
-
dbPath:
|
|
1673
|
-
originUrl:
|
|
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:
|
|
1855
|
+
lastSeenAt: z3.number().int().nonnegative()
|
|
1676
1856
|
});
|
|
1677
|
-
var REGISTRY_SCHEMA =
|
|
1678
|
-
version:
|
|
1679
|
-
projects:
|
|
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
|
|
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
|
|
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/
|
|
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(`${
|
|
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(`${
|
|
2041
|
+
process.stderr.write(`${pc6.dim("registry")} ${registryPath()}
|
|
1823
2042
|
|
|
1824
2043
|
`);
|
|
1825
2044
|
if (rows.length === 0) {
|
|
1826
|
-
process.stderr.write(`${
|
|
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 ?
|
|
1833
|
-
process.stdout.write(`${
|
|
1834
|
-
${
|
|
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
|
-
${
|
|
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(` ${
|
|
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
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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([
|
|
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
|
|
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
|
|
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
|
|
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([
|
|
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
|
|
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
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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(`${
|
|
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
|
-
`${
|
|
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(` ${
|
|
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(`${
|
|
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(`${
|
|
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(` ${
|
|
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(`${
|
|
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(`${
|
|
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(` ${
|
|
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(`${
|
|
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(` ${
|
|
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(`${
|
|
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(` ${
|
|
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
|
-
`${
|
|
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(` ${
|
|
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(`${
|
|
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(`${
|
|
4437
|
+
log(`${pc7.dim("docs")} no tracked .md files found`);
|
|
4193
4438
|
} else {
|
|
4194
|
-
const prunedPart = pruned > 0 ? `, ${
|
|
4439
|
+
const prunedPart = pruned > 0 ? `, ${pc7.yellow(`${pruned} stale removed`)}` : "";
|
|
4195
4440
|
const skippedPart = unreadable.length > 0 ? `, ${unreadable.length} unreadable (kept)` : "";
|
|
4196
|
-
log(` ${
|
|
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(`${
|
|
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(` ${
|
|
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(`${
|
|
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) => ` ${
|
|
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
|
-
[`${
|
|
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(`${
|
|
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(`${
|
|
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
|
-
`${
|
|
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(`${
|
|
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(` ${
|
|
4552
|
+
log(` ${pc7.dim(`vector: ${attempted}/${total} embedded`)}`);
|
|
4308
4553
|
}
|
|
4309
4554
|
});
|
|
4310
4555
|
if (result.embedded > 0) {
|
|
4311
|
-
const skippedPart = result.skipped > 0 ?
|
|
4312
|
-
const remainingPart = result.remaining > 0 ?
|
|
4313
|
-
embedLine = ` ${
|
|
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(`${
|
|
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 = ` ${
|
|
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 ? ` ${
|
|
4585
|
+
const deniedPart = totals.denied > 0 ? ` ${pc7.red(`-${totals.denied} denied`)}` : "";
|
|
4341
4586
|
out(
|
|
4342
4587
|
[
|
|
4343
|
-
`${
|
|
4344
|
-
` ${
|
|
4345
|
-
` ${
|
|
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:
|
|
4457
|
-
query:
|
|
4458
|
-
budget:
|
|
4459
|
-
allProjects:
|
|
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:
|
|
4487
|
-
pruneSource:
|
|
4488
|
-
pruneStaleShell:
|
|
4489
|
-
yes:
|
|
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:
|
|
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:
|
|
4521
|
-
limit:
|
|
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
|
|
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(`${
|
|
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(`${
|
|
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
|
-
${
|
|
4617
|
-
${
|
|
4861
|
+
${pc8.bold("nexusmem precheck")}
|
|
4862
|
+
${pc8.dim("-".repeat(40))}
|
|
4618
4863
|
|
|
4619
4864
|
`);
|
|
4620
4865
|
for (const risk of flagged) {
|
|
4621
|
-
out(` ${
|
|
4866
|
+
out(` ${pc8.bold(risk.path)}
|
|
4622
4867
|
`);
|
|
4623
4868
|
if (risk.unresolvedFailures.length > 0) {
|
|
4624
|
-
out(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(`${
|
|
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
|
|
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(`${
|
|
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(`${
|
|
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
|
-
`${
|
|
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(`${
|
|
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
|
-
`${
|
|
4716
|
-
`${
|
|
4717
|
-
rawTokens > 0 ? `${
|
|
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
|
|
4976
|
+
import pc11 from "picocolors";
|
|
4732
4977
|
|
|
4733
4978
|
// src/cli/format.ts
|
|
4734
|
-
import
|
|
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:
|
|
4747
|
-
medium:
|
|
4748
|
-
low:
|
|
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 ? `${
|
|
5006
|
+
files.length ? `${pc11.dim("transcripts")} ${files.length} file(s) in ${claudeProjectTranscriptDir(repo.root)}
|
|
4762
5007
|
|
|
4763
|
-
` : `${
|
|
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
|
-
${
|
|
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
|
|
5034
|
+
import pc13 from "picocolors";
|
|
4790
5035
|
|
|
4791
5036
|
// src/cli/commands/scan-git.ts
|
|
4792
|
-
import
|
|
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
|
-
`${
|
|
4800
|
-
`${
|
|
4801
|
-
`${
|
|
4802
|
-
`${
|
|
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
|
-
|
|
4837
|
-
|
|
5081
|
+
pc12.dim(date),
|
|
5082
|
+
pc12.magenta(sha),
|
|
4838
5083
|
node.title,
|
|
4839
|
-
|
|
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
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
4869
|
-
`${
|
|
4870
|
-
`${
|
|
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
|
-
|
|
4901
|
-
|
|
5145
|
+
pc13.dim(node.ts.slice(0, 10)),
|
|
5146
|
+
pc13.magenta(sha),
|
|
4902
5147
|
String(node.meta.path ?? ""),
|
|
4903
|
-
|
|
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
|
|
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 ? `${
|
|
5160
|
+
files.length ? `${pc14.dim("tracked .md files")} ${files.map((f) => f.path).join(", ")}
|
|
4916
5161
|
|
|
4917
|
-
` : `${
|
|
5162
|
+
` : `${pc14.yellow("no tracked .md files found")}
|
|
4918
5163
|
`
|
|
4919
5164
|
);
|
|
4920
5165
|
if (unreadable.length > 0) {
|
|
4921
|
-
process.stderr.write(`${
|
|
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
|
-
${
|
|
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
|
|
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(`${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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(` ${
|
|
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(`${
|
|
5010
|
-
${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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
|
|
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 ? `${
|
|
5283
|
+
results.length ? `${pc16.dim("sources found")} ${results.map((r) => r.name).join(", ")}
|
|
5039
5284
|
|
|
5040
|
-
` : `${
|
|
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(`${
|
|
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(`${
|
|
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 ?
|
|
5312
|
+
const approx = node.meta.tsApprox ? pc16.dim("~") : " ";
|
|
5068
5313
|
const exit = node.meta.exitCode;
|
|
5069
|
-
const exitLabel = typeof exit === "number" && exit !== 0 ?
|
|
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
|
|
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(`${
|
|
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} ${
|
|
5334
|
+
process.stdout.write(`${edge.fromPath} ${pc17.dim("->")} ${edge.toPath}
|
|
5090
5335
|
`);
|
|
5091
5336
|
}
|
|
5092
5337
|
process.stderr.write(
|
|
5093
5338
|
`
|
|
5094
|
-
${
|
|
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
|
|
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 ? `${
|
|
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
|
-
`${
|
|
5136
|
-
`${
|
|
5137
|
-
`${
|
|
5138
|
-
`${
|
|
5139
|
-
`${
|
|
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
|
-
`${
|
|
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 ? ` ${
|
|
5389
|
+
stats.total ? ` ${pc18.dim(`${stats.distinctFiles} distinct file path(s)`)}` : "",
|
|
5145
5390
|
"",
|
|
5146
|
-
sources.length ?
|
|
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)} ${
|
|
5395
|
+
return ` ${s.source.padEnd(14)} ${pc18.dim(`last run ${when}`)} ${pc18.dim(`cursor ${cursorLabel}`)}`;
|
|
5151
5396
|
}),
|
|
5152
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
5397
|
+
gitCursor && gitCursor !== repo.head ? `${pc18.yellow("git behind HEAD")} \u2014 run ${pc18.bold("nexusmem sync")}` : "",
|
|
5153
5398
|
"",
|
|
5154
|
-
chains.failuresTotal ? `${
|
|
5155
|
-
structure.edges ? `${
|
|
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(`${
|
|
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(`${
|
|
5582
|
+
process.stderr.write(`${pc19.red("error")} ${message}
|
|
5331
5583
|
`);
|
|
5332
5584
|
process.exitCode = 1;
|
|
5333
5585
|
});
|