nexusmem 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -1
- package/README.md +51 -12
- package/dist/cli/index.js +314 -113
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -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,31 @@ 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
|
+
`;
|
|
998
|
+
var V11 = `
|
|
999
|
+
CREATE TEMP TABLE nodes_vec_stage AS
|
|
1000
|
+
SELECT v.rowid AS rowid, n.project_id AS project_id, v.embedding AS embedding
|
|
1001
|
+
FROM nodes_vec v
|
|
1002
|
+
JOIN nodes n ON n.rowid = v.rowid;
|
|
1003
|
+
|
|
1004
|
+
DROP TABLE nodes_vec;
|
|
1005
|
+
|
|
1006
|
+
CREATE VIRTUAL TABLE nodes_vec USING vec0 (
|
|
1007
|
+
project_id TEXT PARTITION KEY,
|
|
1008
|
+
embedding float[${EMBEDDING_DIM}]
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
INSERT INTO nodes_vec (rowid, project_id, embedding)
|
|
1012
|
+
SELECT rowid, project_id, embedding FROM nodes_vec_stage;
|
|
1013
|
+
|
|
1014
|
+
DROP TABLE nodes_vec_stage;
|
|
1015
|
+
`;
|
|
977
1016
|
var MIGRATIONS = [
|
|
978
1017
|
{ version: 1, up: (db) => db.exec(V1) },
|
|
979
1018
|
{ version: 2, up: (db) => db.exec(V2) },
|
|
@@ -982,7 +1021,10 @@ var MIGRATIONS = [
|
|
|
982
1021
|
{ version: 5, up: (db) => db.exec(V5) },
|
|
983
1022
|
{ version: 6, up: (db) => db.exec(V6) },
|
|
984
1023
|
{ version: 7, up: (db) => db.exec(V7) },
|
|
985
|
-
{ version: 8, up: (db) => db.exec(V8) }
|
|
1024
|
+
{ version: 8, up: (db) => db.exec(V8) },
|
|
1025
|
+
{ version: 9, up: (db) => db.exec(V9) },
|
|
1026
|
+
{ version: 10, up: (db) => db.exec(V10) },
|
|
1027
|
+
{ version: 11, up: (db) => db.exec(V11) }
|
|
986
1028
|
];
|
|
987
1029
|
var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
988
1030
|
function currentSchemaVersion(db) {
|
|
@@ -1000,6 +1042,36 @@ function migrate(db) {
|
|
|
1000
1042
|
return { from, to: currentSchemaVersion(db) };
|
|
1001
1043
|
}
|
|
1002
1044
|
|
|
1045
|
+
// src/store/audit.ts
|
|
1046
|
+
function recordMutationAudit(db, input) {
|
|
1047
|
+
return Number(
|
|
1048
|
+
db.prepare(
|
|
1049
|
+
`INSERT INTO mutation_audit (action, project_id, detail, affected_count, succeeded, error, started_at, finished_at)
|
|
1050
|
+
VALUES (@action, @projectId, @detail, @affectedCount, @succeeded, @error, @startedAt, @finishedAt)`
|
|
1051
|
+
).run({
|
|
1052
|
+
action: input.action,
|
|
1053
|
+
projectId: input.projectId,
|
|
1054
|
+
detail: JSON.stringify(input.detail),
|
|
1055
|
+
affectedCount: input.affectedCount,
|
|
1056
|
+
succeeded: input.succeeded ? 1 : 0,
|
|
1057
|
+
error: input.error ?? null,
|
|
1058
|
+
startedAt: input.startedAt,
|
|
1059
|
+
finishedAt: input.finishedAt
|
|
1060
|
+
}).lastInsertRowid
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
function listMutationAudit(db, projectId, opts = {}) {
|
|
1064
|
+
const rows = db.prepare(
|
|
1065
|
+
`SELECT id, action, project_id AS projectId, detail, affected_count AS affectedCount,
|
|
1066
|
+
succeeded, error, started_at AS startedAt, finished_at AS finishedAt
|
|
1067
|
+
FROM mutation_audit
|
|
1068
|
+
WHERE project_id = @projectId
|
|
1069
|
+
ORDER BY started_at DESC
|
|
1070
|
+
LIMIT @limit`
|
|
1071
|
+
).all({ projectId, limit: opts.limit ?? 50 });
|
|
1072
|
+
return rows.map((r) => ({ ...r, succeeded: r.succeeded === 1 }));
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1003
1075
|
// src/store/projects.ts
|
|
1004
1076
|
function upsertProject(db, project) {
|
|
1005
1077
|
db.prepare(
|
|
@@ -1152,7 +1224,7 @@ function clearProject(db, projectId) {
|
|
|
1152
1224
|
function getNodesByIds(db, ids) {
|
|
1153
1225
|
if (ids.length === 0) return [];
|
|
1154
1226
|
return db.prepare(
|
|
1155
|
-
`SELECT id, kind, project_id AS projectId, ts, title, body, signal, provenance
|
|
1227
|
+
`SELECT id, kind, project_id AS projectId, ts, title, body, signal, provenance, trust_state AS trustState
|
|
1156
1228
|
FROM nodes WHERE id IN (SELECT value FROM json_each(?))`
|
|
1157
1229
|
).all(JSON.stringify(ids));
|
|
1158
1230
|
}
|
|
@@ -1198,6 +1270,9 @@ function getSupersededIds(db, projectId) {
|
|
|
1198
1270
|
function setSupersedes(db, newNodeId, staleNodeId) {
|
|
1199
1271
|
db.prepare("UPDATE nodes SET supersedes = ? WHERE id = ?").run(staleNodeId, newNodeId);
|
|
1200
1272
|
}
|
|
1273
|
+
function setTrustState(db, nodeId, state) {
|
|
1274
|
+
return db.prepare("UPDATE nodes SET trust_state = ? WHERE id = ?").run(state, nodeId).changes > 0;
|
|
1275
|
+
}
|
|
1201
1276
|
function listStaleCandidates(db, projectId, opts = {}) {
|
|
1202
1277
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
1203
1278
|
const minAgeDays = opts.minAgeDays ?? 45;
|
|
@@ -1411,22 +1486,24 @@ function getEmbedding(db, nodeId) {
|
|
|
1411
1486
|
if (!row) return null;
|
|
1412
1487
|
return new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
|
|
1413
1488
|
}
|
|
1414
|
-
function upsertEmbedding(db, rowid, embedding) {
|
|
1415
|
-
db.prepare("INSERT OR REPLACE INTO nodes_vec (rowid, embedding) VALUES (?, ?)").run(BigInt(rowid), embedding);
|
|
1489
|
+
function upsertEmbedding(db, rowid, projectId, embedding) {
|
|
1490
|
+
db.prepare("INSERT OR REPLACE INTO nodes_vec (rowid, project_id, embedding) VALUES (?, ?, ?)").run(BigInt(rowid), projectId, embedding);
|
|
1416
1491
|
}
|
|
1417
1492
|
function dropAllEmbeddings(db) {
|
|
1418
1493
|
return db.prepare("DELETE FROM nodes_vec").run().changes;
|
|
1419
1494
|
}
|
|
1420
|
-
function vectorSearch(db, projectId, embedding, limit = 20) {
|
|
1421
|
-
const
|
|
1495
|
+
function vectorSearch(db, projectId, embedding, limit = 20, opts = {}) {
|
|
1496
|
+
const asOfEpoch = opts.asOfEpoch ?? null;
|
|
1497
|
+
const k = asOfEpoch === null ? limit : Math.max(limit * 8, 50);
|
|
1422
1498
|
return db.prepare(
|
|
1423
|
-
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, v.distance AS distance
|
|
1499
|
+
`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
1500
|
FROM nodes_vec v
|
|
1425
1501
|
JOIN nodes n ON n.rowid = v.rowid
|
|
1426
|
-
WHERE v.embedding MATCH ? AND k = ? AND
|
|
1502
|
+
WHERE v.embedding MATCH ? AND k = ? AND v.project_id = ?
|
|
1503
|
+
AND (? IS NULL OR n.created_at <= ?)
|
|
1427
1504
|
ORDER BY v.distance
|
|
1428
1505
|
LIMIT ?`
|
|
1429
|
-
).all(embedding,
|
|
1506
|
+
).all(embedding, k, projectId, asOfEpoch, asOfEpoch, limit);
|
|
1430
1507
|
}
|
|
1431
1508
|
|
|
1432
1509
|
// src/store/fts.ts
|
|
@@ -1445,18 +1522,20 @@ function toMatchQuery(input) {
|
|
|
1445
1522
|
}
|
|
1446
1523
|
|
|
1447
1524
|
// src/store/search.ts
|
|
1448
|
-
function search(db, projectId, query, limit = 20) {
|
|
1525
|
+
function search(db, projectId, query, limit = 20, opts = {}) {
|
|
1449
1526
|
const match = toMatchQuery(query);
|
|
1450
1527
|
if (!match) return [];
|
|
1528
|
+
const asOfEpoch = opts.asOfEpoch ?? null;
|
|
1451
1529
|
const rows = db.prepare(
|
|
1452
|
-
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance,
|
|
1530
|
+
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, n.trust_state AS trustState,
|
|
1453
1531
|
bm25(nodes_fts, 10.0, 1.0) AS rank
|
|
1454
1532
|
FROM nodes_fts
|
|
1455
1533
|
JOIN nodes n ON n.rowid = nodes_fts.rowid
|
|
1456
1534
|
WHERE nodes_fts MATCH ? AND n.project_id = ?
|
|
1535
|
+
AND (? IS NULL OR n.created_at <= ?)
|
|
1457
1536
|
ORDER BY rank
|
|
1458
1537
|
LIMIT ?`
|
|
1459
|
-
).all(match, projectId, limit);
|
|
1538
|
+
).all(match, projectId, asOfEpoch, asOfEpoch, limit);
|
|
1460
1539
|
return rows;
|
|
1461
1540
|
}
|
|
1462
1541
|
function stats(db, projectId) {
|
|
@@ -1514,7 +1593,7 @@ function listContradictionSuggestions(db, projectId, opts = {}) {
|
|
|
1514
1593
|
FROM contradiction_checks c
|
|
1515
1594
|
JOIN nodes n ON n.id = c.candidate_id
|
|
1516
1595
|
JOIN nodes a ON a.id = c.against_id
|
|
1517
|
-
WHERE n.project_id = @projectId AND c.contradicts = 1
|
|
1596
|
+
WHERE n.project_id = @projectId AND c.contradicts = 1 AND c.dismissed = 0
|
|
1518
1597
|
AND c.candidate_id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)
|
|
1519
1598
|
ORDER BY c.checked_at DESC
|
|
1520
1599
|
LIMIT @limit`
|
|
@@ -1525,11 +1604,19 @@ function countContradictionSuggestions(db, projectId) {
|
|
|
1525
1604
|
`SELECT COUNT(*) AS count
|
|
1526
1605
|
FROM contradiction_checks c
|
|
1527
1606
|
JOIN nodes n ON n.id = c.candidate_id
|
|
1528
|
-
WHERE n.project_id = @projectId AND c.contradicts = 1
|
|
1607
|
+
WHERE n.project_id = @projectId AND c.contradicts = 1 AND c.dismissed = 0
|
|
1529
1608
|
AND c.candidate_id NOT IN (SELECT supersedes FROM nodes WHERE project_id = @projectId AND supersedes IS NOT NULL)`
|
|
1530
1609
|
).get({ projectId });
|
|
1531
1610
|
return row.count;
|
|
1532
1611
|
}
|
|
1612
|
+
function dismissContradictionSuggestion(db, projectId, candidateId) {
|
|
1613
|
+
return db.prepare(
|
|
1614
|
+
`UPDATE contradiction_checks
|
|
1615
|
+
SET dismissed = 1
|
|
1616
|
+
WHERE candidate_id = @candidateId AND contradicts = 1 AND dismissed = 0
|
|
1617
|
+
AND candidate_id IN (SELECT id FROM nodes WHERE project_id = @projectId)`
|
|
1618
|
+
).run({ projectId, candidateId }).changes;
|
|
1619
|
+
}
|
|
1533
1620
|
|
|
1534
1621
|
// src/store/store.ts
|
|
1535
1622
|
var MemoryStore = class _MemoryStore {
|
|
@@ -1687,8 +1774,8 @@ var MemoryStore = class _MemoryStore {
|
|
|
1687
1774
|
countNodesNeedingEmbedding(projectId) {
|
|
1688
1775
|
return countNodesNeedingEmbedding(this.db, projectId);
|
|
1689
1776
|
}
|
|
1690
|
-
upsertEmbedding(rowid, embedding) {
|
|
1691
|
-
upsertEmbedding(this.db, rowid, embedding);
|
|
1777
|
+
upsertEmbedding(rowid, projectId, embedding) {
|
|
1778
|
+
upsertEmbedding(this.db, rowid, projectId, embedding);
|
|
1692
1779
|
}
|
|
1693
1780
|
/** The stored vector for one node, or null if it has not been embedded yet. */
|
|
1694
1781
|
getEmbedding(nodeId) {
|
|
@@ -1703,14 +1790,14 @@ var MemoryStore = class _MemoryStore {
|
|
|
1703
1790
|
setMeta(key, value) {
|
|
1704
1791
|
setMeta(this.db, key, value);
|
|
1705
1792
|
}
|
|
1706
|
-
vectorSearch(projectId, embedding, limit = 20) {
|
|
1707
|
-
return vectorSearch(this.db, projectId, embedding, limit);
|
|
1793
|
+
vectorSearch(projectId, embedding, limit = 20, opts = {}) {
|
|
1794
|
+
return vectorSearch(this.db, projectId, embedding, limit, opts);
|
|
1708
1795
|
}
|
|
1709
1796
|
stats(projectId) {
|
|
1710
1797
|
return stats(this.db, projectId);
|
|
1711
1798
|
}
|
|
1712
|
-
search(projectId, query, limit = 20) {
|
|
1713
|
-
return search(this.db, projectId, query, limit);
|
|
1799
|
+
search(projectId, query, limit = 20, opts = {}) {
|
|
1800
|
+
return search(this.db, projectId, query, limit, opts);
|
|
1714
1801
|
}
|
|
1715
1802
|
/** The project a node belongs to, or null if no node has this id. Used by `mark-stale` to validate both ids. */
|
|
1716
1803
|
getNodeProjectId(id) {
|
|
@@ -1724,6 +1811,10 @@ var MemoryStore = class _MemoryStore {
|
|
|
1724
1811
|
setSupersedes(newNodeId, staleNodeId) {
|
|
1725
1812
|
setSupersedes(this.db, newNodeId, staleNodeId);
|
|
1726
1813
|
}
|
|
1814
|
+
/** Record a human's verdict on one node -- the write behind `nexusmem review`. Caller validates project ownership first. */
|
|
1815
|
+
setTrustState(nodeId, state) {
|
|
1816
|
+
return setTrustState(this.db, nodeId, state);
|
|
1817
|
+
}
|
|
1727
1818
|
/** Aging non-`observed` nodes nothing supersedes yet -- candidates for `nexusmem mark-stale`, not auto-applied. */
|
|
1728
1819
|
listStaleCandidates(projectId, opts = {}) {
|
|
1729
1820
|
return listStaleCandidates(this.db, projectId, opts);
|
|
@@ -1746,6 +1837,18 @@ var MemoryStore = class _MemoryStore {
|
|
|
1746
1837
|
countContradictionSuggestions(projectId) {
|
|
1747
1838
|
return countContradictionSuggestions(this.db, projectId);
|
|
1748
1839
|
}
|
|
1840
|
+
/** Reject every open suggestion for this candidate; returns how many were actually dismissed. */
|
|
1841
|
+
dismissContradictionSuggestion(projectId, candidateId) {
|
|
1842
|
+
return dismissContradictionSuggestion(this.db, projectId, candidateId);
|
|
1843
|
+
}
|
|
1844
|
+
/** Record one `mutation_audit` row for a coarse/destructive operation outside `forget` (currently: `--prune-source`/`--prune-stale-shell`). */
|
|
1845
|
+
recordMutationAudit(input) {
|
|
1846
|
+
return recordMutationAudit(this.db, input);
|
|
1847
|
+
}
|
|
1848
|
+
/** Newest-first `mutation_audit` rows for this project -- every `forget` and `--prune-source` run, whether or not anything matched. */
|
|
1849
|
+
listMutationAudit(projectId, opts = {}) {
|
|
1850
|
+
return listMutationAudit(this.db, projectId, opts);
|
|
1851
|
+
}
|
|
1749
1852
|
/** Escape hatch for tests and future modules. */
|
|
1750
1853
|
get raw() {
|
|
1751
1854
|
return this.db;
|
|
@@ -2252,7 +2355,7 @@ var DEFAULT_SUMMARY_CHARS = 320;
|
|
|
2252
2355
|
var NODE_OVERHEAD_TOKENS = 8;
|
|
2253
2356
|
var CONVERSATION_ANSWER_MARKER = "\n\nA: ";
|
|
2254
2357
|
var MAX_PER_FAMILY = 2;
|
|
2255
|
-
var CHUNKED_KINDS = /* @__PURE__ */ new Set(["conversation_turn", "doc_section"]);
|
|
2358
|
+
var CHUNKED_KINDS = /* @__PURE__ */ new Set(["conversation_turn", "doc_section", "code_diff"]);
|
|
2256
2359
|
var HUNK_BOUNDARY = "\n@@ ";
|
|
2257
2360
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
2258
2361
|
"the",
|
|
@@ -2395,6 +2498,7 @@ function packContext(ranked, tokensBudget, opts = {}) {
|
|
|
2395
2498
|
summary,
|
|
2396
2499
|
tokens,
|
|
2397
2500
|
provenance: hit.provenance,
|
|
2501
|
+
trustState: hit.trustState,
|
|
2398
2502
|
...hit.project ? { project: hit.project } : {}
|
|
2399
2503
|
});
|
|
2400
2504
|
tokensUsed += tokens;
|
|
@@ -2408,7 +2512,8 @@ function renderContextBlock(query, result) {
|
|
|
2408
2512
|
for (const node of result.nodes) {
|
|
2409
2513
|
const project = node.project ? `[${node.project}] ` : "";
|
|
2410
2514
|
const provenance = `[${node.provenance}] `;
|
|
2411
|
-
|
|
2515
|
+
const trust = node.trustState !== "candidate" ? `[${node.trustState}] ` : "";
|
|
2516
|
+
lines.push(`- ${node.ts.slice(0, 10)} ${provenance}${trust}${project}${node.title}`);
|
|
2412
2517
|
if (node.summary && node.summary !== node.title) {
|
|
2413
2518
|
if (node.kind === "code_diff") {
|
|
2414
2519
|
for (const line of node.summary.split("\n")) lines.push(` ${line}`);
|
|
@@ -2546,6 +2651,7 @@ function mergeSearchAndVectorHits(bm25Hits, vectorHits) {
|
|
|
2546
2651
|
body: hit.body,
|
|
2547
2652
|
signal: hit.signal,
|
|
2548
2653
|
provenance: hit.provenance,
|
|
2654
|
+
trustState: hit.trustState,
|
|
2549
2655
|
rank: 0
|
|
2550
2656
|
});
|
|
2551
2657
|
}
|
|
@@ -2565,7 +2671,8 @@ var HALF_LIFE_RATIO = {
|
|
|
2565
2671
|
derived: 0.35
|
|
2566
2672
|
};
|
|
2567
2673
|
var SUPERSEDED_PENALTY = 0.5;
|
|
2568
|
-
var
|
|
2674
|
+
var REJECTED_TRUST_PENALTY = 0.3;
|
|
2675
|
+
var MAX_PRIOR_OVERTURN = 2.4;
|
|
2569
2676
|
var PRIOR_COUNT = 2;
|
|
2570
2677
|
var PER_PRIOR_OVERTURN = MAX_PRIOR_OVERTURN ** (1 / PRIOR_COUNT);
|
|
2571
2678
|
var SIGNAL_EXPONENT = Math.log(PER_PRIOR_OVERTURN) / Math.log(1 / SIGNAL_FLOOR);
|
|
@@ -2608,7 +2715,8 @@ function rankHits(hits, opts = {}) {
|
|
|
2608
2715
|
const effectiveHalfLife = halfLife * (ratios[hit.provenance] ?? 1);
|
|
2609
2716
|
const recencyFactor = RECENCY_FLOOR + (1 - RECENCY_FLOOR) * 2 ** (-ageDays / effectiveHalfLife);
|
|
2610
2717
|
const rawScore = relevance * signalWeight ** SIGNAL_EXPONENT * recencyFactor ** RECENCY_EXPONENT;
|
|
2611
|
-
const
|
|
2718
|
+
const supersededScore = opts.supersededIds?.has(hit.id) ? rawScore * SUPERSEDED_PENALTY : rawScore;
|
|
2719
|
+
const score = hit.trustState === "rejected" ? supersededScore * REJECTED_TRUST_PENALTY : supersededScore;
|
|
2612
2720
|
return { ...hit, relevance, signalWeight, ageDays, recencyFactor, score };
|
|
2613
2721
|
});
|
|
2614
2722
|
return ranked.sort((a, b) => b.score - a.score);
|
|
@@ -2638,6 +2746,7 @@ function pullLinkedResolutions(resolveStore, ranked) {
|
|
|
2638
2746
|
body: resolution.body,
|
|
2639
2747
|
signal: resolution.signal,
|
|
2640
2748
|
provenance: resolution.provenance,
|
|
2749
|
+
trustState: resolution.trustState,
|
|
2641
2750
|
rank: 0,
|
|
2642
2751
|
// no bm25/vector rank of its own -- never read again past this point
|
|
2643
2752
|
relevance: hit.relevance,
|
|
@@ -2662,8 +2771,8 @@ async function runCrossProjectQuery(sources, query, opts) {
|
|
|
2662
2771
|
const supersededIds = /* @__PURE__ */ new Set();
|
|
2663
2772
|
for (const source of sources) {
|
|
2664
2773
|
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) : [];
|
|
2774
|
+
const bm25Hits = source.store.search(source.projectId, query, opts.candidates, { asOfEpoch: opts.asOfEpoch }).map(label);
|
|
2775
|
+
const vectorHits = queryVector ? source.store.vectorSearch(source.projectId, queryVector, opts.candidates, { asOfEpoch: opts.asOfEpoch }) : [];
|
|
2667
2776
|
bm25Count += bm25Hits.length;
|
|
2668
2777
|
vectorCount += vectorHits.length;
|
|
2669
2778
|
perProject.push({ label: source.label, bm25: bm25Hits.length, vector: vectorHits.length });
|
|
@@ -2684,11 +2793,11 @@ async function runCrossProjectQuery(sources, query, opts) {
|
|
|
2684
2793
|
return { bm25Count, vectorCount, hits, packed, perProject };
|
|
2685
2794
|
}
|
|
2686
2795
|
async function runHybridQuery(store, projectId, query, opts) {
|
|
2687
|
-
const bm25Hits = store.search(projectId, query, opts.candidates);
|
|
2796
|
+
const bm25Hits = store.search(projectId, query, opts.candidates, { asOfEpoch: opts.asOfEpoch });
|
|
2688
2797
|
let vectorHits = [];
|
|
2689
2798
|
if (opts.embeddingProvider) {
|
|
2690
2799
|
const queryVector = await opts.embeddingProvider.embed(query);
|
|
2691
|
-
if (queryVector) vectorHits = store.vectorSearch(projectId, queryVector, opts.candidates);
|
|
2800
|
+
if (queryVector) vectorHits = store.vectorSearch(projectId, queryVector, opts.candidates, { asOfEpoch: opts.asOfEpoch });
|
|
2692
2801
|
}
|
|
2693
2802
|
const hits = vectorHits.length > 0 ? mergeSearchAndVectorHits(bm25Hits, vectorHits) : bm25Hits;
|
|
2694
2803
|
const relevanceScores = vectorHits.length > 0 ? reciprocalRankFusion([bm25Hits, vectorHits]) : void 0;
|
|
@@ -3386,11 +3495,13 @@ function isGeneratedPath(path) {
|
|
|
3386
3495
|
return GENERATED_PATHS.some((re) => re.test(path));
|
|
3387
3496
|
}
|
|
3388
3497
|
var TEST_PATHS = /(^|\/)(tests?|__tests__|spec|e2e)\/|\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
3498
|
+
var MANIFEST_PATHS = /(^|\/)(package\.json|tsconfig(\..+)?\.json)$/;
|
|
3389
3499
|
function scoreFileDiff(subject, file) {
|
|
3390
3500
|
const header = parseConventionalHeader(subject);
|
|
3391
3501
|
let score = header.type ? TYPE_WEIGHTS[header.type] ?? 0.5 : 0.5;
|
|
3392
3502
|
if (header.breaking) score += 0.1;
|
|
3393
3503
|
if (TEST_PATHS.test(file.path)) score -= 0.1;
|
|
3504
|
+
if (MANIFEST_PATHS.test(file.path)) score -= 0.1;
|
|
3394
3505
|
if (file.status === "added") score += 0.05;
|
|
3395
3506
|
if (file.status === "deleted") score -= 0.1;
|
|
3396
3507
|
const churn = file.insertions + file.deletions;
|
|
@@ -4849,7 +4960,7 @@ ${node.body}`)
|
|
|
4849
4960
|
for (const [index, node] of group.entries()) {
|
|
4850
4961
|
const vector = vectors[index];
|
|
4851
4962
|
if (vector && vector.length === provider.dimension) {
|
|
4852
|
-
store.upsertEmbedding(node.rowid, vector);
|
|
4963
|
+
store.upsertEmbedding(node.rowid, projectId, vector);
|
|
4853
4964
|
embedded += 1;
|
|
4854
4965
|
embeddedHere += 1;
|
|
4855
4966
|
} else {
|
|
@@ -5118,8 +5229,18 @@ function runPruneSources(store, projectId, otherProjectIds, sources, yes, out) {
|
|
|
5118
5229
|
);
|
|
5119
5230
|
return 0;
|
|
5120
5231
|
}
|
|
5232
|
+
const startedAt = Date.now();
|
|
5121
5233
|
let removed = 0;
|
|
5122
5234
|
for (const { source, id } of counts) removed += store.pruneSourceNodes(id, source, []);
|
|
5235
|
+
store.recordMutationAudit({
|
|
5236
|
+
action: "prune_source",
|
|
5237
|
+
projectId,
|
|
5238
|
+
detail: { sources, scopeProjectIds: scopeIds },
|
|
5239
|
+
affectedCount: removed,
|
|
5240
|
+
succeeded: true,
|
|
5241
|
+
startedAt,
|
|
5242
|
+
finishedAt: Date.now()
|
|
5243
|
+
});
|
|
5123
5244
|
const identityPart = otherProjectIds.length > 0 ? `, ${scopeIds.length} project identit${scopeIds.length === 1 ? "y" : "ies"}` : "";
|
|
5124
5245
|
out(`${pc7.green("pruned")} ${removed} node(s) across ${sources.length} source(s)${identityPart}
|
|
5125
5246
|
`);
|
|
@@ -5262,10 +5383,17 @@ async function searchMemory(input) {
|
|
|
5262
5383
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5263
5384
|
const budget = input.budget ?? 2e3;
|
|
5264
5385
|
const candidates = input.candidates ?? 30;
|
|
5386
|
+
let asOfEpoch;
|
|
5387
|
+
if (input.asOf) {
|
|
5388
|
+
const parsed = Date.parse(input.asOf);
|
|
5389
|
+
if (Number.isNaN(parsed)) throw new Error(`asOf "${input.asOf}" is not a parseable date`);
|
|
5390
|
+
asOfEpoch = parsed;
|
|
5391
|
+
}
|
|
5265
5392
|
const queryOpts = {
|
|
5266
5393
|
budget,
|
|
5267
5394
|
candidates,
|
|
5268
|
-
embeddingProvider: input.noVector ? null : new OllamaEmbeddingProvider()
|
|
5395
|
+
embeddingProvider: input.noVector ? null : new OllamaEmbeddingProvider(),
|
|
5396
|
+
asOfEpoch
|
|
5269
5397
|
};
|
|
5270
5398
|
if (input.allProjects) {
|
|
5271
5399
|
const opened = await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath });
|
|
@@ -5361,11 +5489,14 @@ function createServer() {
|
|
|
5361
5489
|
budget: z4.number().int().positive().optional().describe("Max tokens in the returned context block. Default 2000."),
|
|
5362
5490
|
allProjects: z4.boolean().optional().describe(
|
|
5363
5491
|
"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."
|
|
5492
|
+
),
|
|
5493
|
+
asOf: z4.string().optional().describe(
|
|
5494
|
+
'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
5495
|
)
|
|
5365
5496
|
}
|
|
5366
5497
|
},
|
|
5367
|
-
async ({ projectRoot, query, budget, allProjects }) => {
|
|
5368
|
-
const result = await searchMemory({ projectRoot, query, budget, allProjects });
|
|
5498
|
+
async ({ projectRoot, query, budget, allProjects, asOf }) => {
|
|
5499
|
+
const result = await searchMemory({ projectRoot, query, budget, allProjects, asOf });
|
|
5369
5500
|
return {
|
|
5370
5501
|
content: [{ type: "text", text: result.text }],
|
|
5371
5502
|
structuredContent: {
|
|
@@ -5550,8 +5681,20 @@ ${pc8.dim("-".repeat(40))}
|
|
|
5550
5681
|
|
|
5551
5682
|
// src/cli/commands/query.ts
|
|
5552
5683
|
import pc9 from "picocolors";
|
|
5684
|
+
var QueryError = class extends Error {
|
|
5685
|
+
constructor(message) {
|
|
5686
|
+
super(message);
|
|
5687
|
+
this.name = "QueryError";
|
|
5688
|
+
}
|
|
5689
|
+
};
|
|
5553
5690
|
async function runQuery(opts) {
|
|
5554
5691
|
const { repo, ws, projectId } = await loadContext(opts.cwd);
|
|
5692
|
+
let asOfEpoch;
|
|
5693
|
+
if (opts.asOf) {
|
|
5694
|
+
const parsed = Date.parse(opts.asOf);
|
|
5695
|
+
if (Number.isNaN(parsed)) throw new QueryError(`--as-of "${opts.asOf}" is not a parseable date`);
|
|
5696
|
+
asOfEpoch = parsed;
|
|
5697
|
+
}
|
|
5555
5698
|
const opened = opts.allProjects ? await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath }) : null;
|
|
5556
5699
|
let store = null;
|
|
5557
5700
|
try {
|
|
@@ -5559,7 +5702,8 @@ async function runQuery(opts) {
|
|
|
5559
5702
|
budget: opts.budget,
|
|
5560
5703
|
candidates: opts.candidates,
|
|
5561
5704
|
halfLifeDays: opts.halfLifeDays,
|
|
5562
|
-
embeddingProvider: opts.noVector ? null : new OllamaEmbeddingProvider()
|
|
5705
|
+
embeddingProvider: opts.noVector ? null : new OllamaEmbeddingProvider(),
|
|
5706
|
+
asOfEpoch
|
|
5563
5707
|
};
|
|
5564
5708
|
let result;
|
|
5565
5709
|
if (opened) {
|
|
@@ -5569,6 +5713,10 @@ async function runQuery(opts) {
|
|
|
5569
5713
|
result = await runHybridQuery(store, projectId, opts.query, queryOpts);
|
|
5570
5714
|
}
|
|
5571
5715
|
const { bm25Count, vectorCount, hits, packed } = result;
|
|
5716
|
+
if (asOfEpoch !== void 0 && !opts.json) {
|
|
5717
|
+
process.stderr.write(`${pc9.dim("as of ")} ${new Date(asOfEpoch).toISOString()} -- excludes anything recorded after
|
|
5718
|
+
`);
|
|
5719
|
+
}
|
|
5572
5720
|
if (opened && !opts.json) {
|
|
5573
5721
|
const searched = opened.sources.map((s) => s.label).join(", ");
|
|
5574
5722
|
process.stderr.write(`${pc9.dim("scope ")} ${opened.sources.length} project(s): ${searched}
|
|
@@ -5630,11 +5778,45 @@ async function runQuery(opts) {
|
|
|
5630
5778
|
}
|
|
5631
5779
|
}
|
|
5632
5780
|
|
|
5781
|
+
// src/cli/commands/review.ts
|
|
5782
|
+
import pc10 from "picocolors";
|
|
5783
|
+
var ReviewError = class extends Error {
|
|
5784
|
+
constructor(message) {
|
|
5785
|
+
super(message);
|
|
5786
|
+
this.name = "ReviewError";
|
|
5787
|
+
}
|
|
5788
|
+
};
|
|
5789
|
+
async function runReview(opts) {
|
|
5790
|
+
const { projectId, ws } = await loadContext(opts.cwd);
|
|
5791
|
+
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
5792
|
+
const store = MemoryStore.open(ws.dbPath);
|
|
5793
|
+
try {
|
|
5794
|
+
const owner = store.getNodeProjectId(opts.nodeId);
|
|
5795
|
+
if (owner === null) {
|
|
5796
|
+
throw new ReviewError(`no node found with id ${opts.nodeId}`);
|
|
5797
|
+
}
|
|
5798
|
+
if (owner !== projectId) {
|
|
5799
|
+
throw new ReviewError("node must belong to the current project");
|
|
5800
|
+
}
|
|
5801
|
+
store.setTrustState(opts.nodeId, opts.verdict);
|
|
5802
|
+
const verb = opts.verdict === "verified" ? "verified" : "rejected";
|
|
5803
|
+
out(
|
|
5804
|
+
`${pc10.green(verb)} ${opts.nodeId}
|
|
5805
|
+
` + (opts.verdict === "rejected" ? `${pc10.dim("down-weighted in ranking")} -- the node stays queryable, just ranked lower
|
|
5806
|
+
` : `${pc10.dim("labeled only")} -- verifying does not change ranking
|
|
5807
|
+
`)
|
|
5808
|
+
);
|
|
5809
|
+
return 0;
|
|
5810
|
+
} finally {
|
|
5811
|
+
store.close();
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
|
|
5633
5815
|
// src/cli/commands/scan-conversation.ts
|
|
5634
|
-
import
|
|
5816
|
+
import pc12 from "picocolors";
|
|
5635
5817
|
|
|
5636
5818
|
// src/cli/format.ts
|
|
5637
|
-
import
|
|
5819
|
+
import pc11 from "picocolors";
|
|
5638
5820
|
var GIT_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
5639
5821
|
var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
5640
5822
|
var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
@@ -5646,9 +5828,9 @@ function signalBand(signal, bands) {
|
|
|
5646
5828
|
return "low";
|
|
5647
5829
|
}
|
|
5648
5830
|
var BAND_COLOR = {
|
|
5649
|
-
high:
|
|
5650
|
-
medium:
|
|
5651
|
-
low:
|
|
5831
|
+
high: pc11.green,
|
|
5832
|
+
medium: pc11.yellow,
|
|
5833
|
+
low: pc11.dim
|
|
5652
5834
|
};
|
|
5653
5835
|
function formatSignal(signal, bands) {
|
|
5654
5836
|
return BAND_COLOR[signalBand(signal, bands)](signal.toFixed(2));
|
|
@@ -5657,7 +5839,7 @@ function approxTotalTokens(nodes) {
|
|
|
5657
5839
|
return nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
5658
5840
|
}
|
|
5659
5841
|
function summarize2(nodes) {
|
|
5660
|
-
if (nodes.length === 0) return
|
|
5842
|
+
if (nodes.length === 0) return pc11.yellow("no commits matched");
|
|
5661
5843
|
const timestamps = nodes.map((n) => n.ts).sort();
|
|
5662
5844
|
const avgSignal = nodes.reduce((n, x) => n + x.signal, 0) / nodes.length;
|
|
5663
5845
|
const totalTokens = approxTotalTokens(nodes);
|
|
@@ -5667,7 +5849,7 @@ function summarize2(nodes) {
|
|
|
5667
5849
|
}
|
|
5668
5850
|
const hottest = [...fileHits.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5).map(([path, count]) => ` ${String(count).padStart(3)}x ${path}`);
|
|
5669
5851
|
return [
|
|
5670
|
-
`${
|
|
5852
|
+
`${pc11.bold(String(nodes.length))} nodes ${pc11.dim(`${timestamps[0]?.slice(0, 10)} .. ${timestamps.at(-1)?.slice(0, 10)}`)}`,
|
|
5671
5853
|
` avg signal ${avgSignal.toFixed(3)} ~${totalTokens.toLocaleString()} tokens if sent raw`,
|
|
5672
5854
|
hottest.length ? ` hottest files:
|
|
5673
5855
|
${hottest.join("\n")}` : ""
|
|
@@ -5681,9 +5863,9 @@ async function runScanConversation(opts) {
|
|
|
5681
5863
|
const files = await listTranscriptFiles(repo.root);
|
|
5682
5864
|
if (!opts.json) {
|
|
5683
5865
|
process.stderr.write(
|
|
5684
|
-
files.length ? `${
|
|
5866
|
+
files.length ? `${pc12.dim("transcripts")} ${files.length} file(s) in ${claudeProjectTranscriptDir(repo.root)}
|
|
5685
5867
|
|
|
5686
|
-
` : `${
|
|
5868
|
+
` : `${pc12.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
5687
5869
|
`
|
|
5688
5870
|
);
|
|
5689
5871
|
}
|
|
@@ -5700,7 +5882,7 @@ async function runScanConversation(opts) {
|
|
|
5700
5882
|
const approxTotal = approxTotalTokens(nodes);
|
|
5701
5883
|
process.stderr.write(
|
|
5702
5884
|
`
|
|
5703
|
-
${
|
|
5885
|
+
${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
5886
|
);
|
|
5705
5887
|
return 0;
|
|
5706
5888
|
}
|
|
@@ -5709,7 +5891,7 @@ function formatNode(node) {
|
|
|
5709
5891
|
}
|
|
5710
5892
|
|
|
5711
5893
|
// src/cli/commands/scan-diff.ts
|
|
5712
|
-
import
|
|
5894
|
+
import pc13 from "picocolors";
|
|
5713
5895
|
var DEFAULT_SCAN_COMMITS = 50;
|
|
5714
5896
|
async function runScanDiff(opts) {
|
|
5715
5897
|
const repo = await readRepoInfo(opts.cwd);
|
|
@@ -5717,9 +5899,9 @@ async function runScanDiff(opts) {
|
|
|
5717
5899
|
if (!opts.json) {
|
|
5718
5900
|
process.stderr.write(
|
|
5719
5901
|
[
|
|
5720
|
-
`${
|
|
5721
|
-
`${
|
|
5722
|
-
`${
|
|
5902
|
+
`${pc13.dim("repo ")} ${repo.root}`,
|
|
5903
|
+
`${pc13.dim("branch ")} ${repo.branch ?? pc13.yellow("(detached)")}`,
|
|
5904
|
+
`${pc13.dim("project")} ${pc13.cyan(projectId)}`,
|
|
5723
5905
|
""
|
|
5724
5906
|
].join("\n")
|
|
5725
5907
|
);
|
|
@@ -5749,28 +5931,28 @@ function formatNode2(node) {
|
|
|
5749
5931
|
const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
|
|
5750
5932
|
return [
|
|
5751
5933
|
formatSignal(node.signal, DIFF_SIGNAL_BANDS),
|
|
5752
|
-
|
|
5753
|
-
|
|
5934
|
+
pc13.dim(node.ts.slice(0, 10)),
|
|
5935
|
+
pc13.magenta(sha),
|
|
5754
5936
|
String(node.meta.path ?? ""),
|
|
5755
|
-
|
|
5937
|
+
pc13.dim(`(${churn}, ${node.meta.hunkCount ?? 0} hunk(s))`)
|
|
5756
5938
|
].join(" ");
|
|
5757
5939
|
}
|
|
5758
5940
|
|
|
5759
5941
|
// src/cli/commands/scan-docs.ts
|
|
5760
|
-
import
|
|
5942
|
+
import pc14 from "picocolors";
|
|
5761
5943
|
async function runScanDocs(opts) {
|
|
5762
5944
|
const repo = await readRepoInfo(opts.cwd);
|
|
5763
5945
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5764
5946
|
const { files, unreadable } = await readDocFiles(repo.root);
|
|
5765
5947
|
if (!opts.json) {
|
|
5766
5948
|
process.stderr.write(
|
|
5767
|
-
files.length ? `${
|
|
5949
|
+
files.length ? `${pc14.dim("tracked .md files")} ${files.map((f) => f.path).join(", ")}
|
|
5768
5950
|
|
|
5769
|
-
` : `${
|
|
5951
|
+
` : `${pc14.yellow("no tracked .md files found")}
|
|
5770
5952
|
`
|
|
5771
5953
|
);
|
|
5772
5954
|
if (unreadable.length > 0) {
|
|
5773
|
-
process.stderr.write(`${
|
|
5955
|
+
process.stderr.write(`${pc14.yellow("unreadable")} ${unreadable.join(", ")}
|
|
5774
5956
|
|
|
5775
5957
|
`);
|
|
5776
5958
|
}
|
|
@@ -5786,7 +5968,7 @@ async function runScanDocs(opts) {
|
|
|
5786
5968
|
const approxTotal = approxTotalTokens(nodes);
|
|
5787
5969
|
process.stderr.write(
|
|
5788
5970
|
`
|
|
5789
|
-
${
|
|
5971
|
+
${pc14.bold(String(nodes.length))} section(s) from ${files.length} file(s) above threshold ${pc14.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
5790
5972
|
`
|
|
5791
5973
|
);
|
|
5792
5974
|
return 0;
|
|
@@ -5796,17 +5978,17 @@ function formatNode3(node) {
|
|
|
5796
5978
|
}
|
|
5797
5979
|
|
|
5798
5980
|
// src/cli/commands/scan-git.ts
|
|
5799
|
-
import
|
|
5981
|
+
import pc15 from "picocolors";
|
|
5800
5982
|
async function runScanGit(opts) {
|
|
5801
5983
|
const repo = await readRepoInfo(opts.cwd);
|
|
5802
5984
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5803
5985
|
if (!opts.json) {
|
|
5804
5986
|
process.stderr.write(
|
|
5805
5987
|
[
|
|
5806
|
-
`${
|
|
5807
|
-
`${
|
|
5808
|
-
`${
|
|
5809
|
-
`${
|
|
5988
|
+
`${pc15.dim("repo ")} ${repo.root}`,
|
|
5989
|
+
`${pc15.dim("branch ")} ${repo.branch ?? pc15.yellow("(detached)")}`,
|
|
5990
|
+
`${pc15.dim("origin ")} ${repo.originUrl ?? pc15.dim("(none)")}`,
|
|
5991
|
+
`${pc15.dim("project")} ${pc15.cyan(projectId)}`,
|
|
5810
5992
|
""
|
|
5811
5993
|
].join("\n")
|
|
5812
5994
|
);
|
|
@@ -5840,21 +6022,21 @@ function formatNode4(node) {
|
|
|
5840
6022
|
const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
|
|
5841
6023
|
return [
|
|
5842
6024
|
formatSignal(node.signal, GIT_SIGNAL_BANDS),
|
|
5843
|
-
|
|
5844
|
-
|
|
6025
|
+
pc15.dim(date),
|
|
6026
|
+
pc15.magenta(sha),
|
|
5845
6027
|
node.title,
|
|
5846
|
-
|
|
6028
|
+
pc15.dim(`(${files} file${files === 1 ? "" : "s"}, ${churn})`)
|
|
5847
6029
|
].join(" ");
|
|
5848
6030
|
}
|
|
5849
6031
|
|
|
5850
6032
|
// src/cli/commands/scan-session.ts
|
|
5851
|
-
import
|
|
6033
|
+
import pc16 from "picocolors";
|
|
5852
6034
|
async function runScanSession(opts) {
|
|
5853
6035
|
const repo = await readRepoInfo(opts.cwd);
|
|
5854
6036
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5855
6037
|
const turns = await collectClaudeCodeTranscripts(repo.root);
|
|
5856
6038
|
if (turns.length === 0) {
|
|
5857
|
-
process.stderr.write(`${
|
|
6039
|
+
process.stderr.write(`${pc16.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
5858
6040
|
`);
|
|
5859
6041
|
return 0;
|
|
5860
6042
|
}
|
|
@@ -5862,7 +6044,7 @@ async function runScanSession(opts) {
|
|
|
5862
6044
|
const settled = selectSettledSessions(sessions, opts.settleMinutes);
|
|
5863
6045
|
if (!opts.json) {
|
|
5864
6046
|
process.stderr.write(
|
|
5865
|
-
`${
|
|
6047
|
+
`${pc16.dim("sessions")} ${sessions.length} found, ${settled.length} settled (quiet ${opts.settleMinutes}m+)
|
|
5866
6048
|
|
|
5867
6049
|
`
|
|
5868
6050
|
);
|
|
@@ -5888,7 +6070,7 @@ async function runScanSession(opts) {
|
|
|
5888
6070
|
}
|
|
5889
6071
|
for (const preview of previews) {
|
|
5890
6072
|
process.stdout.write(
|
|
5891
|
-
`${
|
|
6073
|
+
`${pc16.bold(preview.sessionKey)} ${preview.startedAt.slice(0, 16).replace("T", " ")} ${preview.includedTurns}/${preview.turns} turn(s), ${preview.promptChars} chars
|
|
5892
6074
|
${preview.prompt}
|
|
5893
6075
|
|
|
5894
6076
|
`
|
|
@@ -5900,7 +6082,7 @@ ${preview.prompt}
|
|
|
5900
6082
|
settleMinutes: opts.settleMinutes,
|
|
5901
6083
|
maxSessions: opts.maxSessions,
|
|
5902
6084
|
onProgress: (done, total) => {
|
|
5903
|
-
if (!opts.json) process.stderr.write(` ${
|
|
6085
|
+
if (!opts.json) process.stderr.write(` ${pc16.dim(`summarizing ${done}/${total}`)}
|
|
5904
6086
|
`);
|
|
5905
6087
|
}
|
|
5906
6088
|
});
|
|
@@ -5910,21 +6092,21 @@ ${preview.prompt}
|
|
|
5910
6092
|
return 0;
|
|
5911
6093
|
}
|
|
5912
6094
|
for (const node of result.nodes) {
|
|
5913
|
-
process.stdout.write(`${
|
|
5914
|
-
${
|
|
6095
|
+
process.stdout.write(`${pc16.bold(node.title)}
|
|
6096
|
+
${pc16.dim(node.ts.slice(0, 16).replace("T", " "))}
|
|
5915
6097
|
${node.body}
|
|
5916
6098
|
|
|
5917
6099
|
`);
|
|
5918
6100
|
}
|
|
5919
6101
|
if (result.providerUnavailable) {
|
|
5920
6102
|
process.stderr.write(
|
|
5921
|
-
`${
|
|
6103
|
+
`${pc16.yellow("model unavailable")} -- is Ollama running with \`${opts.model}\` pulled? (\`ollama pull ${opts.model}\`)
|
|
5922
6104
|
`
|
|
5923
6105
|
);
|
|
5924
6106
|
return 0;
|
|
5925
6107
|
}
|
|
5926
6108
|
process.stderr.write(
|
|
5927
|
-
`${
|
|
6109
|
+
`${pc16.bold(String(result.nodes.length))} summarized` + (result.failed > 0 ? `, ${pc16.yellow(`${result.failed} failed`)}` : "") + ` ${pc16.dim(`(model ${opts.model})`)}
|
|
5928
6110
|
`
|
|
5929
6111
|
);
|
|
5930
6112
|
return 0;
|
|
@@ -5932,16 +6114,16 @@ ${node.body}
|
|
|
5932
6114
|
var SCAN_SESSION_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
5933
6115
|
|
|
5934
6116
|
// src/cli/commands/scan-shell.ts
|
|
5935
|
-
import
|
|
6117
|
+
import pc17 from "picocolors";
|
|
5936
6118
|
async function runScanShell(opts) {
|
|
5937
6119
|
const repo = await readRepoInfo(opts.cwd);
|
|
5938
6120
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
5939
6121
|
const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
|
|
5940
6122
|
if (!opts.json) {
|
|
5941
6123
|
process.stderr.write(
|
|
5942
|
-
results.length ? `${
|
|
6124
|
+
results.length ? `${pc17.dim("sources found")} ${results.map((r) => r.name).join(", ")}
|
|
5943
6125
|
|
|
5944
|
-
` : `${
|
|
6126
|
+
` : `${pc17.yellow("no shell history source found on this machine")}
|
|
5945
6127
|
`
|
|
5946
6128
|
);
|
|
5947
6129
|
}
|
|
@@ -5950,7 +6132,7 @@ async function runScanShell(opts) {
|
|
|
5950
6132
|
const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
5951
6133
|
allNodes.push(...nodes);
|
|
5952
6134
|
if (!opts.json) {
|
|
5953
|
-
process.stdout.write(`${
|
|
6135
|
+
process.stdout.write(`${pc17.bold(`shell:${result.name}`)} ${pc17.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
|
|
5954
6136
|
`);
|
|
5955
6137
|
for (const node of nodes) process.stdout.write(`${formatNode5(node)}
|
|
5956
6138
|
`);
|
|
@@ -5963,19 +6145,19 @@ async function runScanShell(opts) {
|
|
|
5963
6145
|
return 0;
|
|
5964
6146
|
}
|
|
5965
6147
|
const approxTotal = approxTotalTokens(allNodes);
|
|
5966
|
-
process.stderr.write(`${
|
|
6148
|
+
process.stderr.write(`${pc17.bold(String(allNodes.length))} node(s) total ${pc17.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
5967
6149
|
`);
|
|
5968
6150
|
return 0;
|
|
5969
6151
|
}
|
|
5970
6152
|
function formatNode5(node) {
|
|
5971
|
-
const approx = node.meta.tsApprox ?
|
|
6153
|
+
const approx = node.meta.tsApprox ? pc17.dim("~") : " ";
|
|
5972
6154
|
const exit = node.meta.exitCode;
|
|
5973
|
-
const exitLabel = typeof exit === "number" && exit !== 0 ?
|
|
6155
|
+
const exitLabel = typeof exit === "number" && exit !== 0 ? pc17.red(`exit ${exit}`) : "";
|
|
5974
6156
|
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
|
|
5975
6157
|
}
|
|
5976
6158
|
|
|
5977
6159
|
// src/cli/commands/scan-structure.ts
|
|
5978
|
-
import
|
|
6160
|
+
import pc18 from "picocolors";
|
|
5979
6161
|
var TRACKED_EXTENSIONS = TRACKED_PATHSPECS.map((p) => p.replace("*", "")).join("/");
|
|
5980
6162
|
async function runScanStructure(opts) {
|
|
5981
6163
|
const repo = await readRepoInfo(opts.cwd);
|
|
@@ -5986,33 +6168,42 @@ async function runScanStructure(opts) {
|
|
|
5986
6168
|
return 0;
|
|
5987
6169
|
}
|
|
5988
6170
|
if (unreadable.length > 0) {
|
|
5989
|
-
process.stderr.write(`${
|
|
6171
|
+
process.stderr.write(`${pc18.yellow("unreadable")} ${unreadable.join(", ")}
|
|
5990
6172
|
|
|
5991
6173
|
`);
|
|
5992
6174
|
}
|
|
5993
6175
|
for (const edge of edges) {
|
|
5994
|
-
process.stdout.write(`${edge.fromPath} ${
|
|
6176
|
+
process.stdout.write(`${edge.fromPath} ${pc18.dim("->")} ${edge.toPath}
|
|
5995
6177
|
`);
|
|
5996
6178
|
}
|
|
5997
6179
|
process.stderr.write(
|
|
5998
6180
|
`
|
|
5999
|
-
${
|
|
6181
|
+
${pc18.bold(String(edges.length))} edge(s) from ${filesScanned} tracked ${TRACKED_EXTENSIONS} file(s)
|
|
6000
6182
|
`
|
|
6001
6183
|
);
|
|
6002
6184
|
return 0;
|
|
6003
6185
|
}
|
|
6004
6186
|
|
|
6005
6187
|
// src/cli/commands/stale.ts
|
|
6006
|
-
import
|
|
6188
|
+
import pc19 from "picocolors";
|
|
6007
6189
|
var STALE_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
6008
6190
|
async function runStale(opts) {
|
|
6009
6191
|
const { projectId, ws } = await loadContext(opts.cwd);
|
|
6010
6192
|
const out = opts.out ?? ((chunk2) => void process.stdout.write(chunk2));
|
|
6011
6193
|
const store = MemoryStore.open(ws.dbPath);
|
|
6012
6194
|
try {
|
|
6195
|
+
if (opts.dismiss) {
|
|
6196
|
+
const dismissed = store.dismissContradictionSuggestion(projectId, opts.dismiss);
|
|
6197
|
+
out(
|
|
6198
|
+
dismissed > 0 ? `${pc19.green("dismissed")} the contradiction suggestion for ${opts.dismiss} -- it will not resurface
|
|
6199
|
+
` : `${pc19.dim("no open contradiction suggestion")} for ${opts.dismiss} -- nothing to dismiss
|
|
6200
|
+
`
|
|
6201
|
+
);
|
|
6202
|
+
return 0;
|
|
6203
|
+
}
|
|
6013
6204
|
const candidates = store.listStaleCandidates(projectId, { minAgeDays: opts.minAgeDays, limit: opts.limit });
|
|
6014
6205
|
if (candidates.length === 0) {
|
|
6015
|
-
out(`${
|
|
6206
|
+
out(`${pc19.dim("no stale candidates")} -- no unconfirmed node older than the threshold lacks a successor
|
|
6016
6207
|
`);
|
|
6017
6208
|
return 0;
|
|
6018
6209
|
}
|
|
@@ -6034,15 +6225,15 @@ async function runStale(opts) {
|
|
|
6034
6225
|
for (const s of suggestions) byCandidateId.set(s.candidateId, s);
|
|
6035
6226
|
out(
|
|
6036
6227
|
[
|
|
6037
|
-
`${
|
|
6228
|
+
`${pc19.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
|
|
6038
6229
|
...candidates.map((c) => {
|
|
6039
|
-
const line = ` ${
|
|
6230
|
+
const line = ` ${pc19.dim(c.id)} ${pc19.yellow(`${c.ageDays}d old`)} [${c.kind}] ${c.title}`;
|
|
6040
6231
|
const hit = byCandidateId.get(c.id);
|
|
6041
6232
|
return hit ? `${line}
|
|
6042
|
-
${
|
|
6233
|
+
${pc19.red("likely superseded by")} ${pc19.dim(hit.againstId)} ${hit.againstTitle} -- ${hit.reason}` : line;
|
|
6043
6234
|
}),
|
|
6044
6235
|
"",
|
|
6045
|
-
`run ${
|
|
6236
|
+
`run ${pc19.bold("nexusmem mark-stale <id> --supersedes <newId>")} on any that are actually wrong`
|
|
6046
6237
|
].join("\n").concat("\n")
|
|
6047
6238
|
);
|
|
6048
6239
|
return 0;
|
|
@@ -6054,7 +6245,7 @@ async function runStale(opts) {
|
|
|
6054
6245
|
// src/cli/commands/status.ts
|
|
6055
6246
|
import { basename as basename4 } from "path";
|
|
6056
6247
|
import { statSync } from "fs";
|
|
6057
|
-
import
|
|
6248
|
+
import pc20 from "picocolors";
|
|
6058
6249
|
function daySpan(oldest, newest) {
|
|
6059
6250
|
const oldestDay = Date.parse(oldest.slice(0, 10));
|
|
6060
6251
|
const newestDay = Date.parse(newest.slice(0, 10));
|
|
@@ -6114,34 +6305,34 @@ async function runStatus(opts) {
|
|
|
6114
6305
|
const flaggedCount = store.countContradictionSuggestions(projectId);
|
|
6115
6306
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
6116
6307
|
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 ? `${
|
|
6308
|
+
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
6309
|
"nexusmem sync --prune-source <name>"
|
|
6119
6310
|
)} to remove stale source data` : "";
|
|
6120
6311
|
out(
|
|
6121
6312
|
[
|
|
6122
|
-
`${
|
|
6123
|
-
`${
|
|
6124
|
-
`${
|
|
6125
|
-
`${
|
|
6126
|
-
`${
|
|
6313
|
+
`${pc20.dim("repo ")} ${repo.root}`,
|
|
6314
|
+
`${pc20.dim("branch ")} ${repo.branch ?? pc20.yellow("(detached)")}`,
|
|
6315
|
+
`${pc20.dim("project ")} ${pc20.cyan(projectId)}`,
|
|
6316
|
+
`${pc20.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc20.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
|
|
6317
|
+
`${pc20.dim("database")} ${ws.dbPath} ${pc20.dim(`(${humanBytes(dbBytes)})`)}`,
|
|
6127
6318
|
staleProjectWarning,
|
|
6128
6319
|
"",
|
|
6129
|
-
`${
|
|
6320
|
+
`${pc20.bold(String(stats2.total))} node(s)${stats2.total ? ` ${pc20.dim(`${stats2.oldest?.slice(0, 10)} .. ${stats2.newest?.slice(0, 10)}`)}` : ""}`,
|
|
6130
6321
|
...kinds,
|
|
6131
|
-
stats2.total ? ` ${
|
|
6322
|
+
stats2.total ? ` ${pc20.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
|
|
6132
6323
|
"",
|
|
6133
|
-
sources.length ?
|
|
6324
|
+
sources.length ? pc20.dim("sources") : pc20.yellow("no sources synced yet"),
|
|
6134
6325
|
...sources.map((s) => {
|
|
6135
6326
|
const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
6136
6327
|
const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
|
|
6137
|
-
return ` ${s.source.padEnd(14)} ${
|
|
6328
|
+
return ` ${s.source.padEnd(14)} ${pc20.dim(`last run ${when}`)} ${pc20.dim(`cursor ${cursorLabel}`)}`;
|
|
6138
6329
|
}),
|
|
6139
6330
|
"",
|
|
6140
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
6141
|
-
chains.failuresTotal ? `${
|
|
6142
|
-
structure.edges ? `${
|
|
6143
|
-
staleCount ? `${
|
|
6144
|
-
flaggedCount ? `${
|
|
6331
|
+
gitCursor && gitCursor !== repo.head ? `${pc20.yellow("git behind HEAD")} \u2014 run ${pc20.bold("nexusmem sync")}` : "",
|
|
6332
|
+
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` : ""}` : "",
|
|
6333
|
+
structure.edges ? `${pc20.dim("structure")} ${pc20.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : "",
|
|
6334
|
+
staleCount ? `${pc20.dim("aging ")} ${pc20.bold(String(staleCount))} unconfirmed node(s) worth a look \u2014 run ${pc20.bold("nexusmem stale")}` : "",
|
|
6335
|
+
flaggedCount ? `${pc20.dim("flagged ")} ${pc20.bold(String(flaggedCount))} likely-superseded node(s) awaiting review \u2014 run ${pc20.bold("nexusmem stale")} for detail` : ""
|
|
6145
6336
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
6146
6337
|
);
|
|
6147
6338
|
return 0;
|
|
@@ -6156,7 +6347,7 @@ function isExpected(err) {
|
|
|
6156
6347
|
// the user fixes, not stack traces they debug.
|
|
6157
6348
|
err instanceof GitSpawnError || // Survived every retry, so git is genuinely unstable on this machine
|
|
6158
6349
|
// (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;
|
|
6350
|
+
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
6351
|
}
|
|
6161
6352
|
function guard(run) {
|
|
6162
6353
|
return async () => {
|
|
@@ -6164,7 +6355,7 @@ function guard(run) {
|
|
|
6164
6355
|
process.exitCode = await run();
|
|
6165
6356
|
} catch (err) {
|
|
6166
6357
|
if (isExpected(err)) {
|
|
6167
|
-
process.stderr.write(`${
|
|
6358
|
+
process.stderr.write(`${pc21.red("error")} ${err.message}
|
|
6168
6359
|
`);
|
|
6169
6360
|
process.exitCode = 1;
|
|
6170
6361
|
return;
|
|
@@ -6227,7 +6418,7 @@ program.command("hook").description("Manage the opt-in PowerShell hook that logs
|
|
|
6227
6418
|
)
|
|
6228
6419
|
);
|
|
6229
6420
|
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(
|
|
6421
|
+
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
6422
|
(text, options) => guard(
|
|
6232
6423
|
() => runQuery({
|
|
6233
6424
|
cwd: options.cwd,
|
|
@@ -6237,6 +6428,7 @@ program.command("query").description("Search remembered history and print a toke
|
|
|
6237
6428
|
halfLifeDays: options.halfLife,
|
|
6238
6429
|
noVector: !options.vector,
|
|
6239
6430
|
allProjects: options.allProjects,
|
|
6431
|
+
asOf: options.asOf,
|
|
6240
6432
|
json: options.json
|
|
6241
6433
|
})
|
|
6242
6434
|
)()
|
|
@@ -6266,17 +6458,26 @@ program.command("mark-stale").description(
|
|
|
6266
6458
|
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
6459
|
"--check-contradictions",
|
|
6268
6460
|
"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(
|
|
6461
|
+
).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
6462
|
(options) => guard(
|
|
6271
6463
|
() => runStale({
|
|
6272
6464
|
cwd: options.cwd,
|
|
6273
6465
|
minAgeDays: options.minAgeDays,
|
|
6274
6466
|
limit: options.limit,
|
|
6275
6467
|
checkContradictions: options.checkContradictions,
|
|
6276
|
-
model: options.model
|
|
6468
|
+
model: options.model,
|
|
6469
|
+
dismiss: options.dismiss
|
|
6277
6470
|
})
|
|
6278
6471
|
)()
|
|
6279
6472
|
);
|
|
6473
|
+
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(
|
|
6474
|
+
(nodeId, options) => guard(async () => {
|
|
6475
|
+
if (options.verify === options.reject) {
|
|
6476
|
+
throw new ReviewError("pass exactly one of --verify or --reject");
|
|
6477
|
+
}
|
|
6478
|
+
return runReview({ cwd: options.cwd, nodeId, verdict: options.verify ? "verified" : "rejected" });
|
|
6479
|
+
})()
|
|
6480
|
+
);
|
|
6280
6481
|
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
6482
|
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
6483
|
(options) => guard(
|
|
@@ -6337,7 +6538,7 @@ program.command("scan-structure").description("Preview the JS/TS/Python/Go/Rust/
|
|
|
6337
6538
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
6338
6539
|
program.parseAsync(process.argv).catch((err) => {
|
|
6339
6540
|
const message = err instanceof Error ? err.message : String(err);
|
|
6340
|
-
process.stderr.write(`${
|
|
6541
|
+
process.stderr.write(`${pc21.red("error")} ${message}
|
|
6341
6542
|
`);
|
|
6342
6543
|
process.exitCode = 1;
|
|
6343
6544
|
});
|