nexusmem 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -1
- package/README.md +51 -12
- package/dist/cli/index.js +284 -104
- 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 pc21 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/config/workspace.ts
|
|
8
8
|
import { existsSync } from "fs";
|
|
@@ -290,6 +290,7 @@ function crashStatus(code, signal) {
|
|
|
290
290
|
return null;
|
|
291
291
|
}
|
|
292
292
|
var TRANSIENT_SPAWN_CODES = /* @__PURE__ */ new Set(["EAGAIN", "EPERM", "EACCES", "EMFILE", "ENFILE", "ENOMEM", "EBUSY", "ETXTBSY"]);
|
|
293
|
+
var GIT_LAUNCH_FAILURE = /error launching git/i;
|
|
293
294
|
function toSpawnError(err, cwd, args) {
|
|
294
295
|
const code = err?.code;
|
|
295
296
|
if (code === "ENOENT") {
|
|
@@ -366,6 +367,15 @@ async function* runGitOnce(cwd, args, opts) {
|
|
|
366
367
|
}
|
|
367
368
|
if (code !== 0) {
|
|
368
369
|
const trimmed = stderr.trim();
|
|
370
|
+
if (GIT_LAUNCH_FAILURE.test(trimmed)) {
|
|
371
|
+
throw new GitSpawnError(
|
|
372
|
+
`Could not start git (${trimmed.split("\n")[0]}) in ${cwd}. This is usually transient on Windows -- retrying the same command often succeeds.`,
|
|
373
|
+
fullArgs,
|
|
374
|
+
void 0,
|
|
375
|
+
true,
|
|
376
|
+
null
|
|
377
|
+
);
|
|
378
|
+
}
|
|
369
379
|
const detail = trimmed.split("\n")[0];
|
|
370
380
|
throw new GitError(
|
|
371
381
|
`git ${args.join(" ")} exited with code ${code}${detail ? `: ${detail}` : ""}`,
|
|
@@ -491,6 +501,9 @@ function renderHookSnippet(logPath) {
|
|
|
491
501
|
"$global:__ssd_last_history_id = -1",
|
|
492
502
|
`$global:__ssd_log_path = ${toPowerShellLiteral(logPath)}`,
|
|
493
503
|
"function global:prompt {",
|
|
504
|
+
// Must be first: Get-History (or anything else) below would overwrite $?.
|
|
505
|
+
" $__ssd_ok = $?",
|
|
506
|
+
" $__ssd_exit = $LASTEXITCODE",
|
|
494
507
|
" $__ssd_h = Get-History -Count 1 -ErrorAction SilentlyContinue",
|
|
495
508
|
" if ($__ssd_h -and $__ssd_h.Id -ne $global:__ssd_last_history_id) {",
|
|
496
509
|
" $global:__ssd_last_history_id = $__ssd_h.Id",
|
|
@@ -498,7 +511,8 @@ function renderHookSnippet(logPath) {
|
|
|
498
511
|
" $__ssd_entry = [ordered]@{",
|
|
499
512
|
' ts = (Get-Date).ToString("o")',
|
|
500
513
|
" cwd = (Get-Location).Path",
|
|
501
|
-
|
|
514
|
+
// $LASTEXITCODE alone misses cmdlet failures and goes stale after them; $? catches both.
|
|
515
|
+
" exitCode = if ($__ssd_ok) { 0 } elseif ($__ssd_exit) { $__ssd_exit } else { 1 }",
|
|
502
516
|
" durationMs = [int](($__ssd_h.EndExecutionTime - $__ssd_h.StartExecutionTime).TotalMilliseconds)",
|
|
503
517
|
" command = $__ssd_h.CommandLine",
|
|
504
518
|
" }",
|
|
@@ -974,6 +988,13 @@ CREATE TABLE contradiction_checks (
|
|
|
974
988
|
PRIMARY KEY (candidate_id, against_id)
|
|
975
989
|
);
|
|
976
990
|
`;
|
|
991
|
+
var V9 = `
|
|
992
|
+
ALTER TABLE contradiction_checks ADD COLUMN dismissed INTEGER NOT NULL DEFAULT 0;
|
|
993
|
+
`;
|
|
994
|
+
var V10 = `
|
|
995
|
+
ALTER TABLE nodes ADD COLUMN trust_state TEXT NOT NULL DEFAULT 'candidate';
|
|
996
|
+
CREATE INDEX idx_nodes_trust_state ON nodes (project_id, trust_state) WHERE trust_state != 'candidate';
|
|
997
|
+
`;
|
|
977
998
|
var MIGRATIONS = [
|
|
978
999
|
{ version: 1, up: (db) => db.exec(V1) },
|
|
979
1000
|
{ version: 2, up: (db) => db.exec(V2) },
|
|
@@ -982,7 +1003,9 @@ var MIGRATIONS = [
|
|
|
982
1003
|
{ version: 5, up: (db) => db.exec(V5) },
|
|
983
1004
|
{ version: 6, up: (db) => db.exec(V6) },
|
|
984
1005
|
{ version: 7, up: (db) => db.exec(V7) },
|
|
985
|
-
{ version: 8, up: (db) => db.exec(V8) }
|
|
1006
|
+
{ version: 8, up: (db) => db.exec(V8) },
|
|
1007
|
+
{ version: 9, up: (db) => db.exec(V9) },
|
|
1008
|
+
{ version: 10, up: (db) => db.exec(V10) }
|
|
986
1009
|
];
|
|
987
1010
|
var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
988
1011
|
function currentSchemaVersion(db) {
|
|
@@ -1000,6 +1023,36 @@ function migrate(db) {
|
|
|
1000
1023
|
return { from, to: currentSchemaVersion(db) };
|
|
1001
1024
|
}
|
|
1002
1025
|
|
|
1026
|
+
// src/store/audit.ts
|
|
1027
|
+
function recordMutationAudit(db, input) {
|
|
1028
|
+
return Number(
|
|
1029
|
+
db.prepare(
|
|
1030
|
+
`INSERT INTO mutation_audit (action, project_id, detail, affected_count, succeeded, error, started_at, finished_at)
|
|
1031
|
+
VALUES (@action, @projectId, @detail, @affectedCount, @succeeded, @error, @startedAt, @finishedAt)`
|
|
1032
|
+
).run({
|
|
1033
|
+
action: input.action,
|
|
1034
|
+
projectId: input.projectId,
|
|
1035
|
+
detail: JSON.stringify(input.detail),
|
|
1036
|
+
affectedCount: input.affectedCount,
|
|
1037
|
+
succeeded: input.succeeded ? 1 : 0,
|
|
1038
|
+
error: input.error ?? null,
|
|
1039
|
+
startedAt: input.startedAt,
|
|
1040
|
+
finishedAt: input.finishedAt
|
|
1041
|
+
}).lastInsertRowid
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
function listMutationAudit(db, projectId, opts = {}) {
|
|
1045
|
+
const rows = db.prepare(
|
|
1046
|
+
`SELECT id, action, project_id AS projectId, detail, affected_count AS affectedCount,
|
|
1047
|
+
succeeded, error, started_at AS startedAt, finished_at AS finishedAt
|
|
1048
|
+
FROM mutation_audit
|
|
1049
|
+
WHERE project_id = @projectId
|
|
1050
|
+
ORDER BY started_at DESC
|
|
1051
|
+
LIMIT @limit`
|
|
1052
|
+
).all({ projectId, limit: opts.limit ?? 50 });
|
|
1053
|
+
return rows.map((r) => ({ ...r, succeeded: r.succeeded === 1 }));
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1003
1056
|
// src/store/projects.ts
|
|
1004
1057
|
function upsertProject(db, project) {
|
|
1005
1058
|
db.prepare(
|
|
@@ -1152,7 +1205,7 @@ function clearProject(db, projectId) {
|
|
|
1152
1205
|
function getNodesByIds(db, ids) {
|
|
1153
1206
|
if (ids.length === 0) return [];
|
|
1154
1207
|
return db.prepare(
|
|
1155
|
-
`SELECT id, kind, project_id AS projectId, ts, title, body, signal, provenance
|
|
1208
|
+
`SELECT id, kind, project_id AS projectId, ts, title, body, signal, provenance, trust_state AS trustState
|
|
1156
1209
|
FROM nodes WHERE id IN (SELECT value FROM json_each(?))`
|
|
1157
1210
|
).all(JSON.stringify(ids));
|
|
1158
1211
|
}
|
|
@@ -1198,6 +1251,9 @@ function getSupersededIds(db, projectId) {
|
|
|
1198
1251
|
function setSupersedes(db, newNodeId, staleNodeId) {
|
|
1199
1252
|
db.prepare("UPDATE nodes SET supersedes = ? WHERE id = ?").run(staleNodeId, newNodeId);
|
|
1200
1253
|
}
|
|
1254
|
+
function setTrustState(db, nodeId, state) {
|
|
1255
|
+
return db.prepare("UPDATE nodes SET trust_state = ? WHERE id = ?").run(state, nodeId).changes > 0;
|
|
1256
|
+
}
|
|
1201
1257
|
function listStaleCandidates(db, projectId, opts = {}) {
|
|
1202
1258
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
1203
1259
|
const minAgeDays = opts.minAgeDays ?? 45;
|
|
@@ -1417,16 +1473,18 @@ function upsertEmbedding(db, rowid, embedding) {
|
|
|
1417
1473
|
function dropAllEmbeddings(db) {
|
|
1418
1474
|
return db.prepare("DELETE FROM nodes_vec").run().changes;
|
|
1419
1475
|
}
|
|
1420
|
-
function vectorSearch(db, projectId, embedding, limit = 20) {
|
|
1476
|
+
function vectorSearch(db, projectId, embedding, limit = 20, opts = {}) {
|
|
1421
1477
|
const overfetch = Math.max(limit * 8, 50);
|
|
1478
|
+
const asOfEpoch = opts.asOfEpoch ?? null;
|
|
1422
1479
|
return db.prepare(
|
|
1423
|
-
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, v.distance AS distance
|
|
1480
|
+
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, n.trust_state AS trustState, v.distance AS distance
|
|
1424
1481
|
FROM nodes_vec v
|
|
1425
1482
|
JOIN nodes n ON n.rowid = v.rowid
|
|
1426
1483
|
WHERE v.embedding MATCH ? AND k = ? AND n.project_id = ?
|
|
1484
|
+
AND (? IS NULL OR n.created_at <= ?)
|
|
1427
1485
|
ORDER BY v.distance
|
|
1428
1486
|
LIMIT ?`
|
|
1429
|
-
).all(embedding, overfetch, projectId, limit);
|
|
1487
|
+
).all(embedding, overfetch, projectId, asOfEpoch, asOfEpoch, limit);
|
|
1430
1488
|
}
|
|
1431
1489
|
|
|
1432
1490
|
// src/store/fts.ts
|
|
@@ -1445,18 +1503,20 @@ function toMatchQuery(input) {
|
|
|
1445
1503
|
}
|
|
1446
1504
|
|
|
1447
1505
|
// src/store/search.ts
|
|
1448
|
-
function search(db, projectId, query, limit = 20) {
|
|
1506
|
+
function search(db, projectId, query, limit = 20, opts = {}) {
|
|
1449
1507
|
const match = toMatchQuery(query);
|
|
1450
1508
|
if (!match) return [];
|
|
1509
|
+
const asOfEpoch = opts.asOfEpoch ?? null;
|
|
1451
1510
|
const rows = db.prepare(
|
|
1452
|
-
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance,
|
|
1511
|
+
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, n.trust_state AS trustState,
|
|
1453
1512
|
bm25(nodes_fts, 10.0, 1.0) AS rank
|
|
1454
1513
|
FROM nodes_fts
|
|
1455
1514
|
JOIN nodes n ON n.rowid = nodes_fts.rowid
|
|
1456
1515
|
WHERE nodes_fts MATCH ? AND n.project_id = ?
|
|
1516
|
+
AND (? IS NULL OR n.created_at <= ?)
|
|
1457
1517
|
ORDER BY rank
|
|
1458
1518
|
LIMIT ?`
|
|
1459
|
-
).all(match, projectId, limit);
|
|
1519
|
+
).all(match, projectId, asOfEpoch, asOfEpoch, limit);
|
|
1460
1520
|
return rows;
|
|
1461
1521
|
}
|
|
1462
1522
|
function stats(db, projectId) {
|
|
@@ -1514,7 +1574,7 @@ function listContradictionSuggestions(db, projectId, opts = {}) {
|
|
|
1514
1574
|
FROM contradiction_checks c
|
|
1515
1575
|
JOIN nodes n ON n.id = c.candidate_id
|
|
1516
1576
|
JOIN nodes a ON a.id = c.against_id
|
|
1517
|
-
WHERE n.project_id = @projectId AND c.contradicts = 1
|
|
1577
|
+
WHERE n.project_id = @projectId AND c.contradicts = 1 AND c.dismissed = 0
|
|
1518
1578
|
AND c.candidate_id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)
|
|
1519
1579
|
ORDER BY c.checked_at DESC
|
|
1520
1580
|
LIMIT @limit`
|
|
@@ -1525,11 +1585,19 @@ function countContradictionSuggestions(db, projectId) {
|
|
|
1525
1585
|
`SELECT COUNT(*) AS count
|
|
1526
1586
|
FROM contradiction_checks c
|
|
1527
1587
|
JOIN nodes n ON n.id = c.candidate_id
|
|
1528
|
-
WHERE n.project_id = @projectId AND c.contradicts = 1
|
|
1588
|
+
WHERE n.project_id = @projectId AND c.contradicts = 1 AND c.dismissed = 0
|
|
1529
1589
|
AND c.candidate_id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)`
|
|
1530
1590
|
).get({ projectId });
|
|
1531
1591
|
return row.count;
|
|
1532
1592
|
}
|
|
1593
|
+
function dismissContradictionSuggestion(db, projectId, candidateId) {
|
|
1594
|
+
return db.prepare(
|
|
1595
|
+
`UPDATE contradiction_checks
|
|
1596
|
+
SET dismissed = 1
|
|
1597
|
+
WHERE candidate_id = @candidateId AND contradicts = 1 AND dismissed = 0
|
|
1598
|
+
AND candidate_id IN (SELECT id FROM nodes WHERE project_id = @projectId)`
|
|
1599
|
+
).run({ projectId, candidateId }).changes;
|
|
1600
|
+
}
|
|
1533
1601
|
|
|
1534
1602
|
// src/store/store.ts
|
|
1535
1603
|
var MemoryStore = class _MemoryStore {
|
|
@@ -1703,14 +1771,14 @@ var MemoryStore = class _MemoryStore {
|
|
|
1703
1771
|
setMeta(key, value) {
|
|
1704
1772
|
setMeta(this.db, key, value);
|
|
1705
1773
|
}
|
|
1706
|
-
vectorSearch(projectId, embedding, limit = 20) {
|
|
1707
|
-
return vectorSearch(this.db, projectId, embedding, limit);
|
|
1774
|
+
vectorSearch(projectId, embedding, limit = 20, opts = {}) {
|
|
1775
|
+
return vectorSearch(this.db, projectId, embedding, limit, opts);
|
|
1708
1776
|
}
|
|
1709
1777
|
stats(projectId) {
|
|
1710
1778
|
return stats(this.db, projectId);
|
|
1711
1779
|
}
|
|
1712
|
-
search(projectId, query, limit = 20) {
|
|
1713
|
-
return search(this.db, projectId, query, limit);
|
|
1780
|
+
search(projectId, query, limit = 20, opts = {}) {
|
|
1781
|
+
return search(this.db, projectId, query, limit, opts);
|
|
1714
1782
|
}
|
|
1715
1783
|
/** The project a node belongs to, or null if no node has this id. Used by `mark-stale` to validate both ids. */
|
|
1716
1784
|
getNodeProjectId(id) {
|
|
@@ -1724,6 +1792,10 @@ var MemoryStore = class _MemoryStore {
|
|
|
1724
1792
|
setSupersedes(newNodeId, staleNodeId) {
|
|
1725
1793
|
setSupersedes(this.db, newNodeId, staleNodeId);
|
|
1726
1794
|
}
|
|
1795
|
+
/** Record a human's verdict on one node -- the write behind `nexusmem review`. Caller validates project ownership first. */
|
|
1796
|
+
setTrustState(nodeId, state) {
|
|
1797
|
+
return setTrustState(this.db, nodeId, state);
|
|
1798
|
+
}
|
|
1727
1799
|
/** Aging non-`observed` nodes nothing supersedes yet -- candidates for `nexusmem mark-stale`, not auto-applied. */
|
|
1728
1800
|
listStaleCandidates(projectId, opts = {}) {
|
|
1729
1801
|
return listStaleCandidates(this.db, projectId, opts);
|
|
@@ -1746,6 +1818,18 @@ var MemoryStore = class _MemoryStore {
|
|
|
1746
1818
|
countContradictionSuggestions(projectId) {
|
|
1747
1819
|
return countContradictionSuggestions(this.db, projectId);
|
|
1748
1820
|
}
|
|
1821
|
+
/** Reject every open suggestion for this candidate; returns how many were actually dismissed. */
|
|
1822
|
+
dismissContradictionSuggestion(projectId, candidateId) {
|
|
1823
|
+
return dismissContradictionSuggestion(this.db, projectId, candidateId);
|
|
1824
|
+
}
|
|
1825
|
+
/** Record one `mutation_audit` row for a coarse/destructive operation outside `forget` (currently: `--prune-source`/`--prune-stale-shell`). */
|
|
1826
|
+
recordMutationAudit(input) {
|
|
1827
|
+
return recordMutationAudit(this.db, input);
|
|
1828
|
+
}
|
|
1829
|
+
/** Newest-first `mutation_audit` rows for this project -- every `forget` and `--prune-source` run, whether or not anything matched. */
|
|
1830
|
+
listMutationAudit(projectId, opts = {}) {
|
|
1831
|
+
return listMutationAudit(this.db, projectId, opts);
|
|
1832
|
+
}
|
|
1749
1833
|
/** Escape hatch for tests and future modules. */
|
|
1750
1834
|
get raw() {
|
|
1751
1835
|
return this.db;
|
|
@@ -2395,6 +2479,7 @@ function packContext(ranked, tokensBudget, opts = {}) {
|
|
|
2395
2479
|
summary,
|
|
2396
2480
|
tokens,
|
|
2397
2481
|
provenance: hit.provenance,
|
|
2482
|
+
trustState: hit.trustState,
|
|
2398
2483
|
...hit.project ? { project: hit.project } : {}
|
|
2399
2484
|
});
|
|
2400
2485
|
tokensUsed += tokens;
|
|
@@ -2408,7 +2493,8 @@ function renderContextBlock(query, result) {
|
|
|
2408
2493
|
for (const node of result.nodes) {
|
|
2409
2494
|
const project = node.project ? `[${node.project}] ` : "";
|
|
2410
2495
|
const provenance = `[${node.provenance}] `;
|
|
2411
|
-
|
|
2496
|
+
const trust = node.trustState !== "candidate" ? `[${node.trustState}] ` : "";
|
|
2497
|
+
lines.push(`- ${node.ts.slice(0, 10)} ${provenance}${trust}${project}${node.title}`);
|
|
2412
2498
|
if (node.summary && node.summary !== node.title) {
|
|
2413
2499
|
if (node.kind === "code_diff") {
|
|
2414
2500
|
for (const line of node.summary.split("\n")) lines.push(` ${line}`);
|
|
@@ -2546,6 +2632,7 @@ function mergeSearchAndVectorHits(bm25Hits, vectorHits) {
|
|
|
2546
2632
|
body: hit.body,
|
|
2547
2633
|
signal: hit.signal,
|
|
2548
2634
|
provenance: hit.provenance,
|
|
2635
|
+
trustState: hit.trustState,
|
|
2549
2636
|
rank: 0
|
|
2550
2637
|
});
|
|
2551
2638
|
}
|
|
@@ -2565,6 +2652,7 @@ var HALF_LIFE_RATIO = {
|
|
|
2565
2652
|
derived: 0.35
|
|
2566
2653
|
};
|
|
2567
2654
|
var SUPERSEDED_PENALTY = 0.5;
|
|
2655
|
+
var REJECTED_TRUST_PENALTY = 0.3;
|
|
2568
2656
|
var MAX_PRIOR_OVERTURN = 2;
|
|
2569
2657
|
var PRIOR_COUNT = 2;
|
|
2570
2658
|
var PER_PRIOR_OVERTURN = MAX_PRIOR_OVERTURN ** (1 / PRIOR_COUNT);
|
|
@@ -2608,7 +2696,8 @@ function rankHits(hits, opts = {}) {
|
|
|
2608
2696
|
const effectiveHalfLife = halfLife * (ratios[hit.provenance] ?? 1);
|
|
2609
2697
|
const recencyFactor = RECENCY_FLOOR + (1 - RECENCY_FLOOR) * 2 ** (-ageDays / effectiveHalfLife);
|
|
2610
2698
|
const rawScore = relevance * signalWeight ** SIGNAL_EXPONENT * recencyFactor ** RECENCY_EXPONENT;
|
|
2611
|
-
const
|
|
2699
|
+
const supersededScore = opts.supersededIds?.has(hit.id) ? rawScore * SUPERSEDED_PENALTY : rawScore;
|
|
2700
|
+
const score = hit.trustState === "rejected" ? supersededScore * REJECTED_TRUST_PENALTY : supersededScore;
|
|
2612
2701
|
return { ...hit, relevance, signalWeight, ageDays, recencyFactor, score };
|
|
2613
2702
|
});
|
|
2614
2703
|
return ranked.sort((a, b) => b.score - a.score);
|
|
@@ -2638,6 +2727,7 @@ function pullLinkedResolutions(resolveStore, ranked) {
|
|
|
2638
2727
|
body: resolution.body,
|
|
2639
2728
|
signal: resolution.signal,
|
|
2640
2729
|
provenance: resolution.provenance,
|
|
2730
|
+
trustState: resolution.trustState,
|
|
2641
2731
|
rank: 0,
|
|
2642
2732
|
// no bm25/vector rank of its own -- never read again past this point
|
|
2643
2733
|
relevance: hit.relevance,
|
|
@@ -2662,8 +2752,8 @@ async function runCrossProjectQuery(sources, query, opts) {
|
|
|
2662
2752
|
const supersededIds = /* @__PURE__ */ new Set();
|
|
2663
2753
|
for (const source of sources) {
|
|
2664
2754
|
const label = (hit) => ({ ...hit, project: source.label });
|
|
2665
|
-
const bm25Hits = source.store.search(source.projectId, query, opts.candidates).map(label);
|
|
2666
|
-
const vectorHits = queryVector ? source.store.vectorSearch(source.projectId, queryVector, opts.candidates) : [];
|
|
2755
|
+
const bm25Hits = source.store.search(source.projectId, query, opts.candidates, { asOfEpoch: opts.asOfEpoch }).map(label);
|
|
2756
|
+
const vectorHits = queryVector ? source.store.vectorSearch(source.projectId, queryVector, opts.candidates, { asOfEpoch: opts.asOfEpoch }) : [];
|
|
2667
2757
|
bm25Count += bm25Hits.length;
|
|
2668
2758
|
vectorCount += vectorHits.length;
|
|
2669
2759
|
perProject.push({ label: source.label, bm25: bm25Hits.length, vector: vectorHits.length });
|
|
@@ -2684,11 +2774,11 @@ async function runCrossProjectQuery(sources, query, opts) {
|
|
|
2684
2774
|
return { bm25Count, vectorCount, hits, packed, perProject };
|
|
2685
2775
|
}
|
|
2686
2776
|
async function runHybridQuery(store, projectId, query, opts) {
|
|
2687
|
-
const bm25Hits = store.search(projectId, query, opts.candidates);
|
|
2777
|
+
const bm25Hits = store.search(projectId, query, opts.candidates, { asOfEpoch: opts.asOfEpoch });
|
|
2688
2778
|
let vectorHits = [];
|
|
2689
2779
|
if (opts.embeddingProvider) {
|
|
2690
2780
|
const queryVector = await opts.embeddingProvider.embed(query);
|
|
2691
|
-
if (queryVector) vectorHits = store.vectorSearch(projectId, queryVector, opts.candidates);
|
|
2781
|
+
if (queryVector) vectorHits = store.vectorSearch(projectId, queryVector, opts.candidates, { asOfEpoch: opts.asOfEpoch });
|
|
2692
2782
|
}
|
|
2693
2783
|
const hits = vectorHits.length > 0 ? mergeSearchAndVectorHits(bm25Hits, vectorHits) : bm25Hits;
|
|
2694
2784
|
const relevanceScores = vectorHits.length > 0 ? reciprocalRankFusion([bm25Hits, vectorHits]) : void 0;
|
|
@@ -5118,8 +5208,18 @@ function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
|
|
|
5118
5208
|
);
|
|
5119
5209
|
return 0;
|
|
5120
5210
|
}
|
|
5211
|
+
const startedAt = Date.now();
|
|
5121
5212
|
let removed = 0;
|
|
5122
5213
|
for (const { source, id } of counts) removed += store.pruneSourceNodes(id, source, []);
|
|
5214
|
+
store.recordMutationAudit({
|
|
5215
|
+
action: "prune_source",
|
|
5216
|
+
projectId,
|
|
5217
|
+
detail: { sources, scopeProjectIds: scopeIds },
|
|
5218
|
+
affectedCount: removed,
|
|
5219
|
+
succeeded: true,
|
|
5220
|
+
startedAt,
|
|
5221
|
+
finishedAt: Date.now()
|
|
5222
|
+
});
|
|
5123
5223
|
const identityPart = otherProjectIds.length > 0 ? `, ${scopeIds.length} project identit${scopeIds.length === 1 ? "y" : "ies"}` : "";
|
|
5124
5224
|
out(`${pc7.green("pruned")} ${removed} node(s) across ${sources.length} source(s)${identityPart}
|
|
5125
5225
|
`);
|
|
@@ -5262,10 +5362,17 @@ async function searchMemory(input) {
|
|
|
5262
5362
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5263
5363
|
const budget = input.budget ?? 2e3;
|
|
5264
5364
|
const candidates = input.candidates ?? 30;
|
|
5365
|
+
let asOfEpoch;
|
|
5366
|
+
if (input.asOf) {
|
|
5367
|
+
const parsed = Date.parse(input.asOf);
|
|
5368
|
+
if (Number.isNaN(parsed)) throw new Error(`asOf "${input.asOf}" is not a parseable date`);
|
|
5369
|
+
asOfEpoch = parsed;
|
|
5370
|
+
}
|
|
5265
5371
|
const queryOpts = {
|
|
5266
5372
|
budget,
|
|
5267
5373
|
candidates,
|
|
5268
|
-
embeddingProvider: input.noVector ? null : new OllamaEmbeddingProvider()
|
|
5374
|
+
embeddingProvider: input.noVector ? null : new OllamaEmbeddingProvider(),
|
|
5375
|
+
asOfEpoch
|
|
5269
5376
|
};
|
|
5270
5377
|
if (input.allProjects) {
|
|
5271
5378
|
const opened = await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath });
|
|
@@ -5361,11 +5468,14 @@ function createServer() {
|
|
|
5361
5468
|
budget: z4.number().int().positive().optional().describe("Max tokens in the returned context block. Default 2000."),
|
|
5362
5469
|
allProjects: z4.boolean().optional().describe(
|
|
5363
5470
|
"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."
|
|
5471
|
+
),
|
|
5472
|
+
asOf: z4.string().optional().describe(
|
|
5473
|
+
'ISO-8601 date/time. Restricts results to nodes recorded at or before this instant -- "what did memory hold as of then", not "what happened then". Omit for the normal, unrestricted read.'
|
|
5364
5474
|
)
|
|
5365
5475
|
}
|
|
5366
5476
|
},
|
|
5367
|
-
async ({ projectRoot, query, budget, allProjects }) => {
|
|
5368
|
-
const result = await searchMemory({ projectRoot, query, budget, allProjects });
|
|
5477
|
+
async ({ projectRoot, query, budget, allProjects, asOf }) => {
|
|
5478
|
+
const result = await searchMemory({ projectRoot, query, budget, allProjects, asOf });
|
|
5369
5479
|
return {
|
|
5370
5480
|
content: [{ type: "text", text: result.text }],
|
|
5371
5481
|
structuredContent: {
|
|
@@ -5550,8 +5660,20 @@ ${pc8.dim("-".repeat(40))}
|
|
|
5550
5660
|
|
|
5551
5661
|
// src/cli/commands/query.ts
|
|
5552
5662
|
import pc9 from "picocolors";
|
|
5663
|
+
var QueryError = class extends Error {
|
|
5664
|
+
constructor(message) {
|
|
5665
|
+
super(message);
|
|
5666
|
+
this.name = "QueryError";
|
|
5667
|
+
}
|
|
5668
|
+
};
|
|
5553
5669
|
async function runQuery(opts) {
|
|
5554
5670
|
const { repo, ws, projectId } = await loadContext(opts.cwd);
|
|
5671
|
+
let asOfEpoch;
|
|
5672
|
+
if (opts.asOf) {
|
|
5673
|
+
const parsed = Date.parse(opts.asOf);
|
|
5674
|
+
if (Number.isNaN(parsed)) throw new QueryError(`--as-of "${opts.asOf}" is not a parseable date`);
|
|
5675
|
+
asOfEpoch = parsed;
|
|
5676
|
+
}
|
|
5555
5677
|
const opened = opts.allProjects ? await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath }) : null;
|
|
5556
5678
|
let store = null;
|
|
5557
5679
|
try {
|
|
@@ -5559,7 +5681,8 @@ async function runQuery(opts) {
|
|
|
5559
5681
|
budget: opts.budget,
|
|
5560
5682
|
candidates: opts.candidates,
|
|
5561
5683
|
halfLifeDays: opts.halfLifeDays,
|
|
5562
|
-
embeddingProvider: opts.noVector ? null : new OllamaEmbeddingProvider()
|
|
5684
|
+
embeddingProvider: opts.noVector ? null : new OllamaEmbeddingProvider(),
|
|
5685
|
+
asOfEpoch
|
|
5563
5686
|
};
|
|
5564
5687
|
let result;
|
|
5565
5688
|
if (opened) {
|
|
@@ -5569,6 +5692,10 @@ async function runQuery(opts) {
|
|
|
5569
5692
|
result = await runHybridQuery(store, projectId, opts.query, queryOpts);
|
|
5570
5693
|
}
|
|
5571
5694
|
const { bm25Count, vectorCount, hits, packed } = result;
|
|
5695
|
+
if (asOfEpoch !== void 0 && !opts.json) {
|
|
5696
|
+
process.stderr.write(`${pc9.dim("as of ")} ${new Date(asOfEpoch).toISOString()} -- excludes anything recorded after
|
|
5697
|
+
`);
|
|
5698
|
+
}
|
|
5572
5699
|
if (opened && !opts.json) {
|
|
5573
5700
|
const searched = opened.sources.map((s) => s.label).join(", ");
|
|
5574
5701
|
process.stderr.write(`${pc9.dim("scope ")} ${opened.sources.length} project(s): ${searched}
|
|
@@ -5630,11 +5757,45 @@ async function runQuery(opts) {
|
|
|
5630
5757
|
}
|
|
5631
5758
|
}
|
|
5632
5759
|
|
|
5760
|
+
// src/cli/commands/review.ts
|
|
5761
|
+
import pc10 from "picocolors";
|
|
5762
|
+
var ReviewError = class extends Error {
|
|
5763
|
+
constructor(message) {
|
|
5764
|
+
super(message);
|
|
5765
|
+
this.name = "ReviewError";
|
|
5766
|
+
}
|
|
5767
|
+
};
|
|
5768
|
+
async function runReview(opts) {
|
|
5769
|
+
const { projectId, ws } = await loadContext(opts.cwd);
|
|
5770
|
+
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
5771
|
+
const store = MemoryStore.open(ws.dbPath);
|
|
5772
|
+
try {
|
|
5773
|
+
const owner = store.getNodeProjectId(opts.nodeId);
|
|
5774
|
+
if (owner === null) {
|
|
5775
|
+
throw new ReviewError(`no node found with id ${opts.nodeId}`);
|
|
5776
|
+
}
|
|
5777
|
+
if (owner !== projectId) {
|
|
5778
|
+
throw new ReviewError("node must belong to the current project");
|
|
5779
|
+
}
|
|
5780
|
+
store.setTrustState(opts.nodeId, opts.verdict);
|
|
5781
|
+
const verb = opts.verdict === "verified" ? "verified" : "rejected";
|
|
5782
|
+
out(
|
|
5783
|
+
`${pc10.green(verb)} ${opts.nodeId}
|
|
5784
|
+
` + (opts.verdict === "rejected" ? `${pc10.dim("down-weighted in ranking")} -- the node stays queryable, just ranked lower
|
|
5785
|
+
` : `${pc10.dim("labeled only")} -- verifying does not change ranking
|
|
5786
|
+
`)
|
|
5787
|
+
);
|
|
5788
|
+
return 0;
|
|
5789
|
+
} finally {
|
|
5790
|
+
store.close();
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5633
5794
|
// src/cli/commands/scan-conversation.ts
|
|
5634
|
-
import
|
|
5795
|
+
import pc12 from "picocolors";
|
|
5635
5796
|
|
|
5636
5797
|
// src/cli/format.ts
|
|
5637
|
-
import
|
|
5798
|
+
import pc11 from "picocolors";
|
|
5638
5799
|
var GIT_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
5639
5800
|
var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
5640
5801
|
var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
@@ -5646,9 +5807,9 @@ function signalBand(signal, bands) {
|
|
|
5646
5807
|
return "low";
|
|
5647
5808
|
}
|
|
5648
5809
|
var BAND_COLOR = {
|
|
5649
|
-
high:
|
|
5650
|
-
medium:
|
|
5651
|
-
low:
|
|
5810
|
+
high: pc11.green,
|
|
5811
|
+
medium: pc11.yellow,
|
|
5812
|
+
low: pc11.dim
|
|
5652
5813
|
};
|
|
5653
5814
|
function formatSignal(signal, bands) {
|
|
5654
5815
|
return BAND_COLOR[signalBand(signal, bands)](signal.toFixed(2));
|
|
@@ -5657,7 +5818,7 @@ function approxTotalTokens(nodes) {
|
|
|
5657
5818
|
return nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
5658
5819
|
}
|
|
5659
5820
|
function summarize2(nodes) {
|
|
5660
|
-
if (nodes.length === 0) return
|
|
5821
|
+
if (nodes.length === 0) return pc11.yellow("no commits matched");
|
|
5661
5822
|
const timestamps = nodes.map((n) => n.ts).sort();
|
|
5662
5823
|
const avgSignal = nodes.reduce((n, x) => n + x.signal, 0) / nodes.length;
|
|
5663
5824
|
const totalTokens = approxTotalTokens(nodes);
|
|
@@ -5667,7 +5828,7 @@ function summarize2(nodes) {
|
|
|
5667
5828
|
}
|
|
5668
5829
|
const hottest = [...fileHits.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([path, count]) => ` ${String(count).padStart(3)}x ${path}`);
|
|
5669
5830
|
return [
|
|
5670
|
-
`${
|
|
5831
|
+
`${pc11.bold(String(nodes.length))} nodes ${pc11.dim(`${timestamps[0]?.slice(0, 10)} .. ${timestamps.at(-1)?.slice(0, 10)}`)}`,
|
|
5671
5832
|
` avg signal ${avgSignal.toFixed(3)} ~${totalTokens.toLocaleString()} tokens if sent raw`,
|
|
5672
5833
|
hottest.length ? ` hottest files:
|
|
5673
5834
|
${hottest.join("\n")}` : ""
|
|
@@ -5681,9 +5842,9 @@ async function runScanConversation(opts) {
|
|
|
5681
5842
|
const files = await listTranscriptFiles(repo.root);
|
|
5682
5843
|
if (!opts.json) {
|
|
5683
5844
|
process.stderr.write(
|
|
5684
|
-
files.length ? `${
|
|
5845
|
+
files.length ? `${pc12.dim("transcripts")} ${files.length} file(s) in ${claudeProjectTranscriptDir(repo.root)}
|
|
5685
5846
|
|
|
5686
|
-
` : `${
|
|
5847
|
+
` : `${pc12.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
5687
5848
|
`
|
|
5688
5849
|
);
|
|
5689
5850
|
}
|
|
@@ -5700,7 +5861,7 @@ async function runScanConversation(opts) {
|
|
|
5700
5861
|
const approxTotal = approxTotalTokens(nodes);
|
|
5701
5862
|
process.stderr.write(
|
|
5702
5863
|
`
|
|
5703
|
-
${
|
|
5864
|
+
${pc12.bold(String(nodes.length))} of ${turns.length} exchange(s) above threshold ${pc12.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}` + (redactedTotal > 0 ? ` ${pc12.yellow(`${redactedTotal} secret-like value(s) redacted`)}` : "") + "\n"
|
|
5704
5865
|
);
|
|
5705
5866
|
return 0;
|
|
5706
5867
|
}
|
|
@@ -5709,7 +5870,7 @@ function formatNode(node) {
|
|
|
5709
5870
|
}
|
|
5710
5871
|
|
|
5711
5872
|
// src/cli/commands/scan-diff.ts
|
|
5712
|
-
import
|
|
5873
|
+
import pc13 from "picocolors";
|
|
5713
5874
|
var DEFAULT_SCAN_COMMITS = 50;
|
|
5714
5875
|
async function runScanDiff(opts) {
|
|
5715
5876
|
const repo = await readRepoInfo(opts.cwd);
|
|
@@ -5717,9 +5878,9 @@ async function runScanDiff(opts) {
|
|
|
5717
5878
|
if (!opts.json) {
|
|
5718
5879
|
process.stderr.write(
|
|
5719
5880
|
[
|
|
5720
|
-
`${
|
|
5721
|
-
`${
|
|
5722
|
-
`${
|
|
5881
|
+
`${pc13.dim("repo ")} ${repo.root}`,
|
|
5882
|
+
`${pc13.dim("branch ")} ${repo.branch ?? pc13.yellow("(detached)")}`,
|
|
5883
|
+
`${pc13.dim("project")} ${pc13.cyan(projectId)}`,
|
|
5723
5884
|
""
|
|
5724
5885
|
].join("\n")
|
|
5725
5886
|
);
|
|
@@ -5749,28 +5910,28 @@ function formatNode2(node) {
|
|
|
5749
5910
|
const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
|
|
5750
5911
|
return [
|
|
5751
5912
|
formatSignal(node.signal, DIFF_SIGNAL_BANDS),
|
|
5752
|
-
|
|
5753
|
-
|
|
5913
|
+
pc13.dim(node.ts.slice(0, 10)),
|
|
5914
|
+
pc13.magenta(sha),
|
|
5754
5915
|
String(node.meta.path ?? ""),
|
|
5755
|
-
|
|
5916
|
+
pc13.dim(`(${churn}, ${node.meta.hunkCount ?? 0} hunk(s))`)
|
|
5756
5917
|
].join(" ");
|
|
5757
5918
|
}
|
|
5758
5919
|
|
|
5759
5920
|
// src/cli/commands/scan-docs.ts
|
|
5760
|
-
import
|
|
5921
|
+
import pc14 from "picocolors";
|
|
5761
5922
|
async function runScanDocs(opts) {
|
|
5762
5923
|
const repo = await readRepoInfo(opts.cwd);
|
|
5763
5924
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5764
5925
|
const { files, unreadable } = await readDocFiles(repo.root);
|
|
5765
5926
|
if (!opts.json) {
|
|
5766
5927
|
process.stderr.write(
|
|
5767
|
-
files.length ? `${
|
|
5928
|
+
files.length ? `${pc14.dim("tracked .md files")} ${files.map((f) => f.path).join(", ")}
|
|
5768
5929
|
|
|
5769
|
-
` : `${
|
|
5930
|
+
` : `${pc14.yellow("no tracked .md files found")}
|
|
5770
5931
|
`
|
|
5771
5932
|
);
|
|
5772
5933
|
if (unreadable.length > 0) {
|
|
5773
|
-
process.stderr.write(`${
|
|
5934
|
+
process.stderr.write(`${pc14.yellow("unreadable")} ${unreadable.join(", ")}
|
|
5774
5935
|
|
|
5775
5936
|
`);
|
|
5776
5937
|
}
|
|
@@ -5786,7 +5947,7 @@ async function runScanDocs(opts) {
|
|
|
5786
5947
|
const approxTotal = approxTotalTokens(nodes);
|
|
5787
5948
|
process.stderr.write(
|
|
5788
5949
|
`
|
|
5789
|
-
${
|
|
5950
|
+
${pc14.bold(String(nodes.length))} section(s) from ${files.length} file(s) above threshold ${pc14.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
5790
5951
|
`
|
|
5791
5952
|
);
|
|
5792
5953
|
return 0;
|
|
@@ -5796,17 +5957,17 @@ function formatNode3(node) {
|
|
|
5796
5957
|
}
|
|
5797
5958
|
|
|
5798
5959
|
// src/cli/commands/scan-git.ts
|
|
5799
|
-
import
|
|
5960
|
+
import pc15 from "picocolors";
|
|
5800
5961
|
async function runScanGit(opts) {
|
|
5801
5962
|
const repo = await readRepoInfo(opts.cwd);
|
|
5802
5963
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5803
5964
|
if (!opts.json) {
|
|
5804
5965
|
process.stderr.write(
|
|
5805
5966
|
[
|
|
5806
|
-
`${
|
|
5807
|
-
`${
|
|
5808
|
-
`${
|
|
5809
|
-
`${
|
|
5967
|
+
`${pc15.dim("repo ")} ${repo.root}`,
|
|
5968
|
+
`${pc15.dim("branch ")} ${repo.branch ?? pc15.yellow("(detached)")}`,
|
|
5969
|
+
`${pc15.dim("origin ")} ${repo.originUrl ?? pc15.dim("(none)")}`,
|
|
5970
|
+
`${pc15.dim("project")} ${pc15.cyan(projectId)}`,
|
|
5810
5971
|
""
|
|
5811
5972
|
].join("\n")
|
|
5812
5973
|
);
|
|
@@ -5840,21 +6001,21 @@ function formatNode4(node) {
|
|
|
5840
6001
|
const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
|
|
5841
6002
|
return [
|
|
5842
6003
|
formatSignal(node.signal, GIT_SIGNAL_BANDS),
|
|
5843
|
-
|
|
5844
|
-
|
|
6004
|
+
pc15.dim(date),
|
|
6005
|
+
pc15.magenta(sha),
|
|
5845
6006
|
node.title,
|
|
5846
|
-
|
|
6007
|
+
pc15.dim(`(${files} file${files === 1 ? "" : "s"}, ${churn})`)
|
|
5847
6008
|
].join(" ");
|
|
5848
6009
|
}
|
|
5849
6010
|
|
|
5850
6011
|
// src/cli/commands/scan-session.ts
|
|
5851
|
-
import
|
|
6012
|
+
import pc16 from "picocolors";
|
|
5852
6013
|
async function runScanSession(opts) {
|
|
5853
6014
|
const repo = await readRepoInfo(opts.cwd);
|
|
5854
6015
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5855
6016
|
const turns = await collectClaudeCodeTranscripts(repo.root);
|
|
5856
6017
|
if (turns.length === 0) {
|
|
5857
|
-
process.stderr.write(`${
|
|
6018
|
+
process.stderr.write(`${pc16.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
5858
6019
|
`);
|
|
5859
6020
|
return 0;
|
|
5860
6021
|
}
|
|
@@ -5862,7 +6023,7 @@ async function runScanSession(opts) {
|
|
|
5862
6023
|
const settled = selectSettledSessions(sessions, opts.settleMinutes);
|
|
5863
6024
|
if (!opts.json) {
|
|
5864
6025
|
process.stderr.write(
|
|
5865
|
-
`${
|
|
6026
|
+
`${pc16.dim("sessions")} ${sessions.length} found, ${settled.length} settled (quiet ${opts.settleMinutes}m+)
|
|
5866
6027
|
|
|
5867
6028
|
`
|
|
5868
6029
|
);
|
|
@@ -5888,7 +6049,7 @@ async function runScanSession(opts) {
|
|
|
5888
6049
|
}
|
|
5889
6050
|
for (const preview of previews) {
|
|
5890
6051
|
process.stdout.write(
|
|
5891
|
-
`${
|
|
6052
|
+
`${pc16.bold(preview.sessionKey)} ${preview.startedAt.slice(0, 16).replace("T", " ")} ${preview.includedTurns}/${preview.turns} turn(s), ${preview.promptChars} chars
|
|
5892
6053
|
${preview.prompt}
|
|
5893
6054
|
|
|
5894
6055
|
`
|
|
@@ -5900,7 +6061,7 @@ ${preview.prompt}
|
|
|
5900
6061
|
settleMinutes: opts.settleMinutes,
|
|
5901
6062
|
maxSessions: opts.maxSessions,
|
|
5902
6063
|
onProgress: (done, total) => {
|
|
5903
|
-
if (!opts.json) process.stderr.write(` ${
|
|
6064
|
+
if (!opts.json) process.stderr.write(` ${pc16.dim(`summarizing ${done}/${total}`)}
|
|
5904
6065
|
`);
|
|
5905
6066
|
}
|
|
5906
6067
|
});
|
|
@@ -5910,21 +6071,21 @@ ${preview.prompt}
|
|
|
5910
6071
|
return 0;
|
|
5911
6072
|
}
|
|
5912
6073
|
for (const node of result.nodes) {
|
|
5913
|
-
process.stdout.write(`${
|
|
5914
|
-
${
|
|
6074
|
+
process.stdout.write(`${pc16.bold(node.title)}
|
|
6075
|
+
${pc16.dim(node.ts.slice(0, 16).replace("T", " "))}
|
|
5915
6076
|
${node.body}
|
|
5916
6077
|
|
|
5917
6078
|
`);
|
|
5918
6079
|
}
|
|
5919
6080
|
if (result.providerUnavailable) {
|
|
5920
6081
|
process.stderr.write(
|
|
5921
|
-
`${
|
|
6082
|
+
`${pc16.yellow("model unavailable")} -- is Ollama running with \`${opts.model}\` pulled? (\`ollama pull ${opts.model}\`)
|
|
5922
6083
|
`
|
|
5923
6084
|
);
|
|
5924
6085
|
return 0;
|
|
5925
6086
|
}
|
|
5926
6087
|
process.stderr.write(
|
|
5927
|
-
`${
|
|
6088
|
+
`${pc16.bold(String(result.nodes.length))} summarized` + (result.failed > 0 ? `, ${pc16.yellow(`${result.failed} failed`)}` : "") + ` ${pc16.dim(`(model ${opts.model})`)}
|
|
5928
6089
|
`
|
|
5929
6090
|
);
|
|
5930
6091
|
return 0;
|
|
@@ -5932,16 +6093,16 @@ ${node.body}
|
|
|
5932
6093
|
var SCAN_SESSION_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
5933
6094
|
|
|
5934
6095
|
// src/cli/commands/scan-shell.ts
|
|
5935
|
-
import
|
|
6096
|
+
import pc17 from "picocolors";
|
|
5936
6097
|
async function runScanShell(opts) {
|
|
5937
6098
|
const repo = await readRepoInfo(opts.cwd);
|
|
5938
6099
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5939
6100
|
const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
|
|
5940
6101
|
if (!opts.json) {
|
|
5941
6102
|
process.stderr.write(
|
|
5942
|
-
results.length ? `${
|
|
6103
|
+
results.length ? `${pc17.dim("sources found")} ${results.map((r) => r.name).join(", ")}
|
|
5943
6104
|
|
|
5944
|
-
` : `${
|
|
6105
|
+
` : `${pc17.yellow("no shell history source found on this machine")}
|
|
5945
6106
|
`
|
|
5946
6107
|
);
|
|
5947
6108
|
}
|
|
@@ -5950,7 +6111,7 @@ async function runScanShell(opts) {
|
|
|
5950
6111
|
const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
5951
6112
|
allNodes.push(...nodes);
|
|
5952
6113
|
if (!opts.json) {
|
|
5953
|
-
process.stdout.write(`${
|
|
6114
|
+
process.stdout.write(`${pc17.bold(`shell:${result.name}`)} ${pc17.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
|
|
5954
6115
|
`);
|
|
5955
6116
|
for (const node of nodes) process.stdout.write(`${formatNode5(node)}
|
|
5956
6117
|
`);
|
|
@@ -5963,19 +6124,19 @@ async function runScanShell(opts) {
|
|
|
5963
6124
|
return 0;
|
|
5964
6125
|
}
|
|
5965
6126
|
const approxTotal = approxTotalTokens(allNodes);
|
|
5966
|
-
process.stderr.write(`${
|
|
6127
|
+
process.stderr.write(`${pc17.bold(String(allNodes.length))} node(s) total ${pc17.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
5967
6128
|
`);
|
|
5968
6129
|
return 0;
|
|
5969
6130
|
}
|
|
5970
6131
|
function formatNode5(node) {
|
|
5971
|
-
const approx = node.meta.tsApprox ?
|
|
6132
|
+
const approx = node.meta.tsApprox ? pc17.dim("~") : " ";
|
|
5972
6133
|
const exit = node.meta.exitCode;
|
|
5973
|
-
const exitLabel = typeof exit === "number" && exit !== 0 ?
|
|
6134
|
+
const exitLabel = typeof exit === "number" && exit !== 0 ? pc17.red(`exit ${exit}`) : "";
|
|
5974
6135
|
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
|
|
5975
6136
|
}
|
|
5976
6137
|
|
|
5977
6138
|
// src/cli/commands/scan-structure.ts
|
|
5978
|
-
import
|
|
6139
|
+
import pc18 from "picocolors";
|
|
5979
6140
|
var TRACKED_EXTENSIONS = TRACKED_PATHSPECS.map((p) => p.replace("*", "")).join("/");
|
|
5980
6141
|
async function runScanStructure(opts) {
|
|
5981
6142
|
const repo = await readRepoInfo(opts.cwd);
|
|
@@ -5986,33 +6147,42 @@ async function runScanStructure(opts) {
|
|
|
5986
6147
|
return 0;
|
|
5987
6148
|
}
|
|
5988
6149
|
if (unreadable.length > 0) {
|
|
5989
|
-
process.stderr.write(`${
|
|
6150
|
+
process.stderr.write(`${pc18.yellow("unreadable")} ${unreadable.join(", ")}
|
|
5990
6151
|
|
|
5991
6152
|
`);
|
|
5992
6153
|
}
|
|
5993
6154
|
for (const edge of edges) {
|
|
5994
|
-
process.stdout.write(`${edge.fromPath} ${
|
|
6155
|
+
process.stdout.write(`${edge.fromPath} ${pc18.dim("->")} ${edge.toPath}
|
|
5995
6156
|
`);
|
|
5996
6157
|
}
|
|
5997
6158
|
process.stderr.write(
|
|
5998
6159
|
`
|
|
5999
|
-
${
|
|
6160
|
+
${pc18.bold(String(edges.length))} edge(s) from ${filesScanned} tracked ${TRACKED_EXTENSIONS} file(s)
|
|
6000
6161
|
`
|
|
6001
6162
|
);
|
|
6002
6163
|
return 0;
|
|
6003
6164
|
}
|
|
6004
6165
|
|
|
6005
6166
|
// src/cli/commands/stale.ts
|
|
6006
|
-
import
|
|
6167
|
+
import pc19 from "picocolors";
|
|
6007
6168
|
var STALE_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
6008
6169
|
async function runStale(opts) {
|
|
6009
6170
|
const { projectId, ws } = await loadContext(opts.cwd);
|
|
6010
6171
|
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
6011
6172
|
const store = MemoryStore.open(ws.dbPath);
|
|
6012
6173
|
try {
|
|
6174
|
+
if (opts.dismiss) {
|
|
6175
|
+
const dismissed = store.dismissContradictionSuggestion(projectId, opts.dismiss);
|
|
6176
|
+
out(
|
|
6177
|
+
dismissed > 0 ? `${pc19.green("dismissed")} the contradiction suggestion for ${opts.dismiss} -- it will not resurface
|
|
6178
|
+
` : `${pc19.dim("no open contradiction suggestion")} for ${opts.dismiss} -- nothing to dismiss
|
|
6179
|
+
`
|
|
6180
|
+
);
|
|
6181
|
+
return 0;
|
|
6182
|
+
}
|
|
6013
6183
|
const candidates = store.listStaleCandidates(projectId, { minAgeDays: opts.minAgeDays, limit: opts.limit });
|
|
6014
6184
|
if (candidates.length === 0) {
|
|
6015
|
-
out(`${
|
|
6185
|
+
out(`${pc19.dim("no stale candidates")} -- no unconfirmed node older than the threshold lacks a successor
|
|
6016
6186
|
`);
|
|
6017
6187
|
return 0;
|
|
6018
6188
|
}
|
|
@@ -6034,15 +6204,15 @@ async function runStale(opts) {
|
|
|
6034
6204
|
for (const s of suggestions) byCandidateId.set(s.candidateId, s);
|
|
6035
6205
|
out(
|
|
6036
6206
|
[
|
|
6037
|
-
`${
|
|
6207
|
+
`${pc19.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
|
|
6038
6208
|
...candidates.map((c) => {
|
|
6039
|
-
const line = ` ${
|
|
6209
|
+
const line = ` ${pc19.dim(c.id)} ${pc19.yellow(`${c.ageDays}d old`)} [${c.kind}] ${c.title}`;
|
|
6040
6210
|
const hit = byCandidateId.get(c.id);
|
|
6041
6211
|
return hit ? `${line}
|
|
6042
|
-
${
|
|
6212
|
+
${pc19.red("likely superseded by")} ${pc19.dim(hit.againstId)} ${hit.againstTitle} -- ${hit.reason}` : line;
|
|
6043
6213
|
}),
|
|
6044
6214
|
"",
|
|
6045
|
-
`run ${
|
|
6215
|
+
`run ${pc19.bold("nexusmem mark-stale <id> --supersedes <newId>")} on any that are actually wrong`
|
|
6046
6216
|
].join("\n").concat("\n")
|
|
6047
6217
|
);
|
|
6048
6218
|
return 0;
|
|
@@ -6054,7 +6224,7 @@ async function runStale(opts) {
|
|
|
6054
6224
|
// src/cli/commands/status.ts
|
|
6055
6225
|
import { basename as basename4 } from "path";
|
|
6056
6226
|
import { statSync } from "fs";
|
|
6057
|
-
import
|
|
6227
|
+
import pc20 from "picocolors";
|
|
6058
6228
|
function daySpan(oldest, newest) {
|
|
6059
6229
|
const oldestDay = Date.parse(oldest.slice(0, 10));
|
|
6060
6230
|
const newestDay = Date.parse(newest.slice(0, 10));
|
|
@@ -6114,34 +6284,34 @@ async function runStatus(opts) {
|
|
|
6114
6284
|
const flaggedCount = store.countContradictionSuggestions(projectId);
|
|
6115
6285
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
6116
6286
|
const kinds = Object.entries(stats2.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
6117
|
-
const staleProjectWarning = otherProjectIds.length ? `${
|
|
6287
|
+
const staleProjectWarning = otherProjectIds.length ? `${pc20.yellow("stale ")} ${otherProjectIds.length} prior project ${otherProjectIds.length === 1 ? "identity holds" : "identities hold"} ${otherProjectNodes} node(s) \u2014 run ${pc20.bold(
|
|
6118
6288
|
"nexusmem sync --prune-source <name>"
|
|
6119
6289
|
)} to remove stale source data` : "";
|
|
6120
6290
|
out(
|
|
6121
6291
|
[
|
|
6122
|
-
`${
|
|
6123
|
-
`${
|
|
6124
|
-
`${
|
|
6125
|
-
`${
|
|
6126
|
-
`${
|
|
6292
|
+
`${pc20.dim("repo ")} ${repo.root}`,
|
|
6293
|
+
`${pc20.dim("branch ")} ${repo.branch ?? pc20.yellow("(detached)")}`,
|
|
6294
|
+
`${pc20.dim("project ")} ${pc20.cyan(projectId)}`,
|
|
6295
|
+
`${pc20.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc20.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
|
|
6296
|
+
`${pc20.dim("database")} ${ws.dbPath} ${pc20.dim(`(${humanBytes(dbBytes)})`)}`,
|
|
6127
6297
|
staleProjectWarning,
|
|
6128
6298
|
"",
|
|
6129
|
-
`${
|
|
6299
|
+
`${pc20.bold(String(stats2.total))} node(s)${stats2.total ? ` ${pc20.dim(`${stats2.oldest?.slice(0, 10)} .. ${stats2.newest?.slice(0, 10)}`)}` : ""}`,
|
|
6130
6300
|
...kinds,
|
|
6131
|
-
stats2.total ? ` ${
|
|
6301
|
+
stats2.total ? ` ${pc20.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
|
|
6132
6302
|
"",
|
|
6133
|
-
sources.length ?
|
|
6303
|
+
sources.length ? pc20.dim("sources") : pc20.yellow("no sources synced yet"),
|
|
6134
6304
|
...sources.map((s) => {
|
|
6135
6305
|
const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
6136
6306
|
const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
|
|
6137
|
-
return ` ${s.source.padEnd(14)} ${
|
|
6307
|
+
return ` ${s.source.padEnd(14)} ${pc20.dim(`last run ${when}`)} ${pc20.dim(`cursor ${cursorLabel}`)}`;
|
|
6138
6308
|
}),
|
|
6139
6309
|
"",
|
|
6140
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
6141
|
-
chains.failuresTotal ? `${
|
|
6142
|
-
structure.edges ? `${
|
|
6143
|
-
staleCount ? `${
|
|
6144
|
-
flaggedCount ? `${
|
|
6310
|
+
gitCursor && gitCursor !== repo.head ? `${pc20.yellow("git behind HEAD")} \u2014 run ${pc20.bold("nexusmem sync")}` : "",
|
|
6311
|
+
chains.failuresTotal ? `${pc20.dim("chains ")} ${pc20.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc20.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc20.bold("nexusmem sync --link-failures")} to link more` : ""}` : "",
|
|
6312
|
+
structure.edges ? `${pc20.dim("structure")} ${pc20.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : "",
|
|
6313
|
+
staleCount ? `${pc20.dim("aging ")} ${pc20.bold(String(staleCount))} unconfirmed node(s) worth a look \u2014 run ${pc20.bold("nexusmem stale")}` : "",
|
|
6314
|
+
flaggedCount ? `${pc20.dim("flagged ")} ${pc20.bold(String(flaggedCount))} likely-superseded node(s) awaiting review \u2014 run ${pc20.bold("nexusmem stale")} for detail` : ""
|
|
6145
6315
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
6146
6316
|
);
|
|
6147
6317
|
return 0;
|
|
@@ -6156,7 +6326,7 @@ function isExpected(err) {
|
|
|
6156
6326
|
// the user fixes, not stack traces they debug.
|
|
6157
6327
|
err instanceof GitSpawnError || // Survived every retry, so git is genuinely unstable on this machine
|
|
6158
6328
|
// (antivirus, a bad install). Actionable, and not our stack to print.
|
|
6159
|
-
err instanceof GitCrashError || err instanceof ConfigError || err instanceof ProfileNotFoundError || err instanceof ForeignGitHookError || err instanceof DenyListError || err instanceof MarkStaleError;
|
|
6329
|
+
err instanceof GitCrashError || err instanceof ConfigError || err instanceof ProfileNotFoundError || err instanceof ForeignGitHookError || err instanceof DenyListError || err instanceof MarkStaleError || err instanceof QueryError || err instanceof ReviewError;
|
|
6160
6330
|
}
|
|
6161
6331
|
function guard(run) {
|
|
6162
6332
|
return async () => {
|
|
@@ -6164,7 +6334,7 @@ function guard(run) {
|
|
|
6164
6334
|
process.exitCode = await run();
|
|
6165
6335
|
} catch (err) {
|
|
6166
6336
|
if (isExpected(err)) {
|
|
6167
|
-
process.stderr.write(`${
|
|
6337
|
+
process.stderr.write(`${pc21.red("error")} ${err.message}
|
|
6168
6338
|
`);
|
|
6169
6339
|
process.exitCode = 1;
|
|
6170
6340
|
return;
|
|
@@ -6227,7 +6397,7 @@ program.command("hook").description("Manage the opt-in PowerShell hook that logs
|
|
|
6227
6397
|
)
|
|
6228
6398
|
);
|
|
6229
6399
|
program.command("status").description("Show what is currently remembered for this repository").option("-C, --cwd <path>", "repository path", process.cwd()).option("--share", "print a plain-text summary formatted for sharing, e.g. on X or Reddit").action((options) => guard(() => runStatus({ cwd: options.cwd, share: options.share }))());
|
|
6230
|
-
program.command("query").description("Search remembered history and print a token-budgeted context block").argument("<text>", "free-text query").option("-C, --cwd <path>", "repository path", process.cwd()).option("-b, --budget <tokens>", "max tokens in the packed context", (v) => Number.parseInt(v, 10), 2e3).option("-n, --candidates <count>", "how many search hits to rank before packing", (v) => Number.parseInt(v, 10), 30).option("--half-life <days>", "days for a node's recency weight to halve", (v) => Number.parseFloat(v)).option("--no-vector", "BM25 only -- skip embedding the query and vector search").option("-a, --all-projects", "search every registered repository, not just this one", false).option("--json", "emit the packed result as JSON on stdout", false).action(
|
|
6400
|
+
program.command("query").description("Search remembered history and print a token-budgeted context block").argument("<text>", "free-text query").option("-C, --cwd <path>", "repository path", process.cwd()).option("-b, --budget <tokens>", "max tokens in the packed context", (v) => Number.parseInt(v, 10), 2e3).option("-n, --candidates <count>", "how many search hits to rank before packing", (v) => Number.parseInt(v, 10), 30).option("--half-life <days>", "days for a node's recency weight to halve", (v) => Number.parseFloat(v)).option("--no-vector", "BM25 only -- skip embedding the query and vector search").option("-a, --all-projects", "search every registered repository, not just this one", false).option("--as-of <date>", 'bi-temporal read: only nodes recorded at or before this date -- "what did the store hold then", not "what happened then"').option("--json", "emit the packed result as JSON on stdout", false).action(
|
|
6231
6401
|
(text, options) => guard(
|
|
6232
6402
|
() => runQuery({
|
|
6233
6403
|
cwd: options.cwd,
|
|
@@ -6237,6 +6407,7 @@ program.command("query").description("Search remembered history and print a toke
|
|
|
6237
6407
|
halfLifeDays: options.halfLife,
|
|
6238
6408
|
noVector: !options.vector,
|
|
6239
6409
|
allProjects: options.allProjects,
|
|
6410
|
+
asOf: options.asOf,
|
|
6240
6411
|
json: options.json
|
|
6241
6412
|
})
|
|
6242
6413
|
)()
|
|
@@ -6266,17 +6437,26 @@ program.command("mark-stale").description(
|
|
|
6266
6437
|
program.command("stale").description("List unconfirmed (non-observed) nodes old enough to be worth double-checking (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--min-age-days <days>", "only nodes at least this old", (v) => Number.parseFloat(v)).option("-n, --limit <count>", "stop after N candidates", (v) => Number.parseInt(v, 10)).option(
|
|
6267
6438
|
"--check-contradictions",
|
|
6268
6439
|
"ask the local SLM whether a similar newer node actually contradicts each candidate (needs Ollama)"
|
|
6269
|
-
).option("--model <name>", "Ollama chat model for --check-contradictions", STALE_DEFAULT_MODEL).action(
|
|
6440
|
+
).option("--model <name>", "Ollama chat model for --check-contradictions", STALE_DEFAULT_MODEL).option("--dismiss <candidateId>", "reject the open contradiction suggestion for this node id so it stops resurfacing").action(
|
|
6270
6441
|
(options) => guard(
|
|
6271
6442
|
() => runStale({
|
|
6272
6443
|
cwd: options.cwd,
|
|
6273
6444
|
minAgeDays: options.minAgeDays,
|
|
6274
6445
|
limit: options.limit,
|
|
6275
6446
|
checkContradictions: options.checkContradictions,
|
|
6276
|
-
model: options.model
|
|
6447
|
+
model: options.model,
|
|
6448
|
+
dismiss: options.dismiss
|
|
6277
6449
|
})
|
|
6278
6450
|
)()
|
|
6279
6451
|
);
|
|
6452
|
+
program.command("review").description("Record a human verdict on one node: --verify or --reject (a rejected node is down-weighted in ranking, never deleted)").argument("<nodeId>", "id of the node being reviewed").option("-C, --cwd <path>", "repository path", process.cwd()).option("--verify", "mark the node verified (label only, no ranking change)", false).option("--reject", "mark the node rejected (down-weighted in ranking, still queryable)", false).action(
|
|
6453
|
+
(nodeId, options) => guard(async () => {
|
|
6454
|
+
if (options.verify === options.reject) {
|
|
6455
|
+
throw new ReviewError("pass exactly one of --verify or --reject");
|
|
6456
|
+
}
|
|
6457
|
+
return runReview({ cwd: options.cwd, nodeId, verdict: options.verify ? "verified" : "rejected" });
|
|
6458
|
+
})()
|
|
6459
|
+
);
|
|
6280
6460
|
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 }))());
|
|
6281
6461
|
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(
|
|
6282
6462
|
(options) => guard(
|
|
@@ -6337,7 +6517,7 @@ program.command("scan-structure").description("Preview the JS/TS/Python/Go/Rust/
|
|
|
6337
6517
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
6338
6518
|
program.parseAsync(process.argv).catch((err) => {
|
|
6339
6519
|
const message = err instanceof Error ? err.message : String(err);
|
|
6340
|
-
process.stderr.write(`${
|
|
6520
|
+
process.stderr.write(`${pc21.red("error")} ${message}
|
|
6341
6521
|
`);
|
|
6342
6522
|
process.exitCode = 1;
|
|
6343
6523
|
});
|