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