nexusmem 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/README.md +302 -254
- package/dist/cli/index.js +989 -229
- package/dist/cli/index.js.map +1 -1
- package/package.json +4 -2
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 pc13 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/config/workspace.ts
|
|
8
8
|
import { existsSync } from "fs";
|
|
@@ -51,12 +51,28 @@ var ConfigSchema = z.object({
|
|
|
51
51
|
enabled: z.boolean().default(true),
|
|
52
52
|
/** git pathspecs passed to `git ls-files`. */
|
|
53
53
|
include: z.array(z.string()).default(["*.md"])
|
|
54
|
-
}).default({ enabled: true, include: ["*.md"] })
|
|
54
|
+
}).default({ enabled: true, include: ["*.md"] }),
|
|
55
|
+
/**
|
|
56
|
+
* The patch text of each commit, one node per changed file.
|
|
57
|
+
*
|
|
58
|
+
* Bounded by `maxCommits` rather than by `git.since`, because patches
|
|
59
|
+
* are an order of magnitude bulkier than commit messages: an unbounded
|
|
60
|
+
* first sync of a long-lived repository would spend most of its time
|
|
61
|
+
* and database on code nobody will ask about. Later syncs walk only
|
|
62
|
+
* `cursor..HEAD`, so the cap effectively applies to the first run.
|
|
63
|
+
*/
|
|
64
|
+
diff: z.object({
|
|
65
|
+
enabled: z.boolean().default(true),
|
|
66
|
+
maxCommits: z.number().int().positive().default(200),
|
|
67
|
+
maxFilesPerCommit: z.number().int().positive().default(20),
|
|
68
|
+
contextLines: z.number().int().nonnegative().default(3)
|
|
69
|
+
}).default({ enabled: true, maxCommits: 200, maxFilesPerCommit: 20, contextLines: 3 })
|
|
55
70
|
}).default({
|
|
56
71
|
git: { enabled: true, since: null, includeMerges: true },
|
|
57
72
|
shell: { enabled: true, tailLines: 300 },
|
|
58
73
|
conversation: { enabled: false },
|
|
59
|
-
docs: { enabled: true, include: ["*.md"] }
|
|
74
|
+
docs: { enabled: true, include: ["*.md"] },
|
|
75
|
+
diff: { enabled: true, maxCommits: 200, maxFilesPerCommit: 20, contextLines: 3 }
|
|
60
76
|
}),
|
|
61
77
|
limits: z.object({
|
|
62
78
|
maxFilesPerNode: z.number().int().positive().default(40),
|
|
@@ -314,25 +330,31 @@ import { dirname } from "path";
|
|
|
314
330
|
|
|
315
331
|
// src/shell/paths.ts
|
|
316
332
|
import { execFile } from "child_process";
|
|
333
|
+
import { homedir as homedir2 } from "os";
|
|
334
|
+
import { join as join3 } from "path";
|
|
335
|
+
import { promisify } from "util";
|
|
336
|
+
|
|
337
|
+
// src/config/paths.ts
|
|
317
338
|
import { homedir } from "os";
|
|
318
339
|
import { join as join2 } from "path";
|
|
319
|
-
|
|
340
|
+
function globalWorkspaceDir() {
|
|
341
|
+
return process.env.NEXUSMEM_HOME ?? join2(homedir(), ".nexusmem");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/shell/paths.ts
|
|
320
345
|
var execFileAsync = promisify(execFile);
|
|
321
346
|
function psReadLineHistoryPath() {
|
|
322
|
-
const appData = process.env.APPDATA ??
|
|
323
|
-
return
|
|
347
|
+
const appData = process.env.APPDATA ?? join3(homedir2(), "AppData", "Roaming");
|
|
348
|
+
return join3(appData, "Microsoft", "Windows", "PowerShell", "PSReadLine", "ConsoleHost_history.txt");
|
|
324
349
|
}
|
|
325
350
|
function bashHistoryPath() {
|
|
326
|
-
return process.env.HISTFILE_BASH ??
|
|
351
|
+
return process.env.HISTFILE_BASH ?? join3(homedir2(), ".bash_history");
|
|
327
352
|
}
|
|
328
353
|
function zshHistoryPath() {
|
|
329
|
-
return process.env.HISTFILE ??
|
|
330
|
-
}
|
|
331
|
-
function globalWorkspaceDir() {
|
|
332
|
-
return join2(homedir(), ".nexusmem");
|
|
354
|
+
return process.env.HISTFILE ?? join3(homedir2(), ".zsh_history");
|
|
333
355
|
}
|
|
334
356
|
function hookLogPath() {
|
|
335
|
-
return
|
|
357
|
+
return join3(globalWorkspaceDir(), "shell-history.jsonl");
|
|
336
358
|
}
|
|
337
359
|
async function resolvePowerShellProfilePath(exe = "powershell") {
|
|
338
360
|
try {
|
|
@@ -486,6 +508,74 @@ async function runHookStatus(opts) {
|
|
|
486
508
|
import { relative } from "path";
|
|
487
509
|
import pc2 from "picocolors";
|
|
488
510
|
|
|
511
|
+
// src/config/registry.ts
|
|
512
|
+
import { existsSync as existsSync2 } from "fs";
|
|
513
|
+
import { mkdir as mkdir3, readFile as readFile3, rename, writeFile as writeFile3 } from "fs/promises";
|
|
514
|
+
import { join as join4 } from "path";
|
|
515
|
+
import { z as z2 } from "zod";
|
|
516
|
+
var ENTRY_SCHEMA = z2.object({
|
|
517
|
+
projectId: z2.string().min(1),
|
|
518
|
+
root: z2.string().min(1),
|
|
519
|
+
dbPath: z2.string().min(1),
|
|
520
|
+
originUrl: z2.string().nullable().default(null),
|
|
521
|
+
/** Epoch ms of the last `init`/`sync` that recorded this entry. */
|
|
522
|
+
lastSeenAt: z2.number().int().nonnegative()
|
|
523
|
+
});
|
|
524
|
+
var REGISTRY_SCHEMA = z2.object({
|
|
525
|
+
version: z2.literal(1),
|
|
526
|
+
projects: z2.array(ENTRY_SCHEMA).default([])
|
|
527
|
+
});
|
|
528
|
+
function registryPath() {
|
|
529
|
+
return join4(globalWorkspaceDir(), "projects.json");
|
|
530
|
+
}
|
|
531
|
+
async function readRegistry() {
|
|
532
|
+
let raw;
|
|
533
|
+
try {
|
|
534
|
+
raw = await readFile3(registryPath(), "utf8");
|
|
535
|
+
} catch {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
const parsed = REGISTRY_SCHEMA.safeParse(JSON.parse(raw));
|
|
540
|
+
if (!parsed.success) return [];
|
|
541
|
+
return [...parsed.data.projects].sort((a, b) => b.lastSeenAt - a.lastSeenAt);
|
|
542
|
+
} catch {
|
|
543
|
+
return [];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function readLiveRegistry() {
|
|
547
|
+
const all = await readRegistry();
|
|
548
|
+
const entries = [];
|
|
549
|
+
const missing = [];
|
|
550
|
+
for (const entry of all) {
|
|
551
|
+
(existsSync2(entry.dbPath) ? entries : missing).push(entry);
|
|
552
|
+
}
|
|
553
|
+
return { entries, missing };
|
|
554
|
+
}
|
|
555
|
+
async function recordProject(input) {
|
|
556
|
+
const existing = await readRegistry();
|
|
557
|
+
const entry = { ...input, lastSeenAt: Date.now() };
|
|
558
|
+
const projects = [entry, ...existing.filter((e) => e.projectId !== input.projectId)];
|
|
559
|
+
await writeRegistry(projects);
|
|
560
|
+
return projects;
|
|
561
|
+
}
|
|
562
|
+
async function forgetProjects(projectIds) {
|
|
563
|
+
const existing = await readRegistry();
|
|
564
|
+
const drop = new Set(projectIds);
|
|
565
|
+
const kept = existing.filter((e) => !drop.has(e.projectId));
|
|
566
|
+
if (kept.length === existing.length) return 0;
|
|
567
|
+
await writeRegistry(kept);
|
|
568
|
+
return existing.length - kept.length;
|
|
569
|
+
}
|
|
570
|
+
async function writeRegistry(projects) {
|
|
571
|
+
const path = registryPath();
|
|
572
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
573
|
+
await mkdir3(globalWorkspaceDir(), { recursive: true });
|
|
574
|
+
await writeFile3(tmp, `${JSON.stringify({ version: 1, projects }, null, 2)}
|
|
575
|
+
`, "utf8");
|
|
576
|
+
await rename(tmp, path);
|
|
577
|
+
}
|
|
578
|
+
|
|
489
579
|
// src/core/ids.ts
|
|
490
580
|
import { createHash } from "crypto";
|
|
491
581
|
var KEY_SEP = "\0";
|
|
@@ -905,6 +995,7 @@ async function runInit(opts) {
|
|
|
905
995
|
} finally {
|
|
906
996
|
store.close();
|
|
907
997
|
}
|
|
998
|
+
await recordProject({ projectId, root: repo.root, dbPath: ws.dbPath, originUrl: repo.originUrl });
|
|
908
999
|
const lines = [
|
|
909
1000
|
`${pc2.green("initialized")} ${ws.dir}`,
|
|
910
1001
|
` project ${pc2.cyan(projectId)}`,
|
|
@@ -939,10 +1030,68 @@ async function runInit(opts) {
|
|
|
939
1030
|
return 0;
|
|
940
1031
|
}
|
|
941
1032
|
|
|
1033
|
+
// src/cli/commands/projects.ts
|
|
1034
|
+
import pc3 from "picocolors";
|
|
1035
|
+
async function runProjects(opts) {
|
|
1036
|
+
const { entries, missing } = await readLiveRegistry();
|
|
1037
|
+
const rows = entries.map((entry) => {
|
|
1038
|
+
let nodes = null;
|
|
1039
|
+
try {
|
|
1040
|
+
const store = MemoryStore.open(entry.dbPath);
|
|
1041
|
+
try {
|
|
1042
|
+
nodes = store.stats(entry.projectId).total;
|
|
1043
|
+
} finally {
|
|
1044
|
+
store.close();
|
|
1045
|
+
}
|
|
1046
|
+
} catch {
|
|
1047
|
+
nodes = null;
|
|
1048
|
+
}
|
|
1049
|
+
return { ...entry, nodes };
|
|
1050
|
+
});
|
|
1051
|
+
if (opts.prune) {
|
|
1052
|
+
const removed = await forgetProjects(missing.map((entry) => entry.projectId));
|
|
1053
|
+
process.stderr.write(`${pc3.yellow("pruned")} ${removed} project(s) whose database is gone
|
|
1054
|
+
`);
|
|
1055
|
+
}
|
|
1056
|
+
if (opts.json) {
|
|
1057
|
+
process.stdout.write(`${JSON.stringify({ registry: registryPath(), projects: rows, missing }, null, 2)}
|
|
1058
|
+
`);
|
|
1059
|
+
return 0;
|
|
1060
|
+
}
|
|
1061
|
+
process.stderr.write(`${pc3.dim("registry")} ${registryPath()}
|
|
1062
|
+
|
|
1063
|
+
`);
|
|
1064
|
+
if (rows.length === 0) {
|
|
1065
|
+
process.stderr.write(`${pc3.yellow("no projects registered")} -- run ${pc3.bold("nexusmem sync")} in a repository
|
|
1066
|
+
`);
|
|
1067
|
+
return 0;
|
|
1068
|
+
}
|
|
1069
|
+
for (const row of rows) {
|
|
1070
|
+
const seen = new Date(row.lastSeenAt).toISOString().slice(0, 16).replace("T", " ");
|
|
1071
|
+
const count = row.nodes === null ? pc3.yellow("unreadable") : `${row.nodes} node(s)`;
|
|
1072
|
+
process.stdout.write(`${pc3.cyan(row.projectId.slice(0, 8))} ${row.root}
|
|
1073
|
+
${pc3.dim(`${count}, last seen ${seen}`)}
|
|
1074
|
+
`);
|
|
1075
|
+
}
|
|
1076
|
+
if (!opts.prune && missing.length > 0) {
|
|
1077
|
+
process.stderr.write(
|
|
1078
|
+
`
|
|
1079
|
+
${pc3.yellow(`${missing.length} registered project(s) have no database on disk`)} ${pc3.dim("-- run with --prune to forget them")}
|
|
1080
|
+
`
|
|
1081
|
+
);
|
|
1082
|
+
for (const entry of missing) process.stderr.write(` ${pc3.dim(entry.root)}
|
|
1083
|
+
`);
|
|
1084
|
+
}
|
|
1085
|
+
return 0;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
942
1088
|
// src/mcp/server.ts
|
|
943
1089
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
944
1090
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
945
|
-
import { z as
|
|
1091
|
+
import { z as z3 } from "zod";
|
|
1092
|
+
|
|
1093
|
+
// src/mcp/tools.ts
|
|
1094
|
+
import { basename as basename2 } from "path";
|
|
946
1095
|
|
|
947
1096
|
// src/core/text.ts
|
|
948
1097
|
function truncate(s, max) {
|
|
@@ -956,22 +1105,126 @@ function approxTokens(text) {
|
|
|
956
1105
|
var DEFAULT_SUMMARY_CHARS = 320;
|
|
957
1106
|
var NODE_OVERHEAD_TOKENS = 8;
|
|
958
1107
|
var CONVERSATION_ANSWER_MARKER = "\n\nA: ";
|
|
959
|
-
|
|
1108
|
+
var HUNK_BOUNDARY = "\n@@ ";
|
|
1109
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
1110
|
+
"the",
|
|
1111
|
+
"and",
|
|
1112
|
+
"for",
|
|
1113
|
+
"are",
|
|
1114
|
+
"was",
|
|
1115
|
+
"were",
|
|
1116
|
+
"that",
|
|
1117
|
+
"this",
|
|
1118
|
+
"with",
|
|
1119
|
+
"from",
|
|
1120
|
+
"into",
|
|
1121
|
+
"not",
|
|
1122
|
+
"but",
|
|
1123
|
+
"what",
|
|
1124
|
+
"why",
|
|
1125
|
+
"how",
|
|
1126
|
+
"when",
|
|
1127
|
+
"where",
|
|
1128
|
+
"which",
|
|
1129
|
+
"who",
|
|
1130
|
+
"does",
|
|
1131
|
+
"did",
|
|
1132
|
+
"has",
|
|
1133
|
+
"have",
|
|
1134
|
+
"had",
|
|
1135
|
+
"can",
|
|
1136
|
+
"could",
|
|
1137
|
+
"would",
|
|
1138
|
+
"should",
|
|
1139
|
+
"all",
|
|
1140
|
+
"any",
|
|
1141
|
+
"every",
|
|
1142
|
+
"each",
|
|
1143
|
+
"its",
|
|
1144
|
+
"our",
|
|
1145
|
+
"you",
|
|
1146
|
+
"your",
|
|
1147
|
+
"about"
|
|
1148
|
+
]);
|
|
1149
|
+
function queryTerms(query) {
|
|
1150
|
+
const words = query.toLowerCase().match(/[a-z0-9_]{3,}/g) ?? [];
|
|
1151
|
+
return [...new Set(words.filter((w) => !STOPWORDS.has(w)).map(singularize))];
|
|
1152
|
+
}
|
|
1153
|
+
function codeTokens(text) {
|
|
1154
|
+
const spaced = text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
1155
|
+
return new Set((spaced.match(/[a-z0-9]{2,}/g) ?? []).map(singularize));
|
|
1156
|
+
}
|
|
1157
|
+
function singularize(word) {
|
|
1158
|
+
return word.length > 3 && word.endsWith("s") && !word.endsWith("ss") ? word.slice(0, -1) : word;
|
|
1159
|
+
}
|
|
1160
|
+
function pickHunk(patch, query) {
|
|
1161
|
+
const first = patch.indexOf("@@ ");
|
|
1162
|
+
if (first === -1) return patch;
|
|
1163
|
+
const hunks = [];
|
|
1164
|
+
let rest = patch.slice(first);
|
|
1165
|
+
for (; ; ) {
|
|
1166
|
+
const next = rest.indexOf(HUNK_BOUNDARY, 1);
|
|
1167
|
+
if (next === -1) {
|
|
1168
|
+
hunks.push(rest);
|
|
1169
|
+
break;
|
|
1170
|
+
}
|
|
1171
|
+
hunks.push(rest.slice(0, next));
|
|
1172
|
+
rest = rest.slice(next + 1);
|
|
1173
|
+
}
|
|
1174
|
+
const terms = queryTerms(query);
|
|
1175
|
+
if (terms.length === 0) return hunks[0] ?? patch;
|
|
1176
|
+
let best = hunks[0] ?? patch;
|
|
1177
|
+
let bestScore = 0;
|
|
1178
|
+
for (const hunk of hunks) {
|
|
1179
|
+
const tokens = codeTokens(hunk);
|
|
1180
|
+
const score = terms.reduce((n, term) => n + (tokens.has(term) ? 1 : 0), 0);
|
|
1181
|
+
if (score > bestScore) {
|
|
1182
|
+
best = hunk;
|
|
1183
|
+
bestScore = score;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return focusHunk(best, terms);
|
|
1187
|
+
}
|
|
1188
|
+
function focusHunk(hunk, terms) {
|
|
1189
|
+
const lines = hunk.split("\n");
|
|
1190
|
+
const header = lines[0] ?? "";
|
|
1191
|
+
const body = lines.slice(1);
|
|
1192
|
+
const isChange = (line) => line.startsWith("+") || line.startsWith("-");
|
|
1193
|
+
let idx = terms.length ? body.findIndex((line) => {
|
|
1194
|
+
if (!isChange(line)) return false;
|
|
1195
|
+
const tokens = codeTokens(line);
|
|
1196
|
+
return terms.some((term) => tokens.has(term));
|
|
1197
|
+
}) : -1;
|
|
1198
|
+
if (idx === -1) idx = body.findIndex(isChange);
|
|
1199
|
+
if (idx <= 1) return hunk;
|
|
1200
|
+
return [header, ...body.slice(idx - 1)].join("\n");
|
|
1201
|
+
}
|
|
1202
|
+
function summarize(hit, maxChars, query) {
|
|
960
1203
|
const answerIdx = hit.body.indexOf(CONVERSATION_ANSWER_MARKER);
|
|
961
1204
|
if (answerIdx !== -1) {
|
|
962
1205
|
const answer = hit.body.slice(answerIdx + CONVERSATION_ANSWER_MARKER.length).trim();
|
|
963
1206
|
if (answer) return truncate(answer, maxChars);
|
|
964
1207
|
}
|
|
1208
|
+
if (hit.kind === "code_diff") {
|
|
1209
|
+
const patchStart = hit.body.indexOf(HUNK_BOUNDARY);
|
|
1210
|
+
if (patchStart !== -1) {
|
|
1211
|
+
const head = hit.body.slice(0, patchStart).trim();
|
|
1212
|
+
const hunk = pickHunk(hit.body.slice(patchStart + 1), query);
|
|
1213
|
+
return truncate(`${head}
|
|
1214
|
+
${hunk}`, maxChars);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
965
1217
|
const rest = hit.body.startsWith(hit.title) ? hit.body.slice(hit.title.length).trim() : hit.body;
|
|
966
1218
|
return truncate(rest || hit.title, maxChars);
|
|
967
1219
|
}
|
|
968
1220
|
function packContext(ranked, tokensBudget, opts = {}) {
|
|
969
1221
|
const summaryChars = opts.summaryChars ?? DEFAULT_SUMMARY_CHARS;
|
|
1222
|
+
const query = opts.query ?? "";
|
|
970
1223
|
const nodes = [];
|
|
971
1224
|
let tokensUsed = 0;
|
|
972
1225
|
let droppedForBudget = 0;
|
|
973
1226
|
for (const hit of ranked) {
|
|
974
|
-
const summary = summarize(hit, summaryChars);
|
|
1227
|
+
const summary = summarize(hit, summaryChars, query);
|
|
975
1228
|
const tokens = approxTokens(hit.title) + approxTokens(summary) + NODE_OVERHEAD_TOKENS;
|
|
976
1229
|
if (tokensUsed + tokens > tokensBudget) {
|
|
977
1230
|
droppedForBudget += 1;
|
|
@@ -985,7 +1238,8 @@ function packContext(ranked, tokensBudget, opts = {}) {
|
|
|
985
1238
|
signal: hit.signal,
|
|
986
1239
|
score: hit.score,
|
|
987
1240
|
summary,
|
|
988
|
-
tokens
|
|
1241
|
+
tokens,
|
|
1242
|
+
...hit.project ? { project: hit.project } : {}
|
|
989
1243
|
});
|
|
990
1244
|
tokensUsed += tokens;
|
|
991
1245
|
}
|
|
@@ -995,9 +1249,14 @@ function renderContextBlock(query, result) {
|
|
|
995
1249
|
if (result.nodes.length === 0) return `No remembered context matched "${query}".`;
|
|
996
1250
|
const lines = [`Relevant history for: ${query}`, ""];
|
|
997
1251
|
for (const node of result.nodes) {
|
|
998
|
-
|
|
1252
|
+
const project = node.project ? `[${node.project}] ` : "";
|
|
1253
|
+
lines.push(`- ${node.ts.slice(0, 10)} ${project}${node.title}`);
|
|
999
1254
|
if (node.summary && node.summary !== node.title) {
|
|
1000
|
-
|
|
1255
|
+
if (node.kind === "code_diff") {
|
|
1256
|
+
for (const line of node.summary.split("\n")) lines.push(` ${line}`);
|
|
1257
|
+
} else {
|
|
1258
|
+
lines.push(` ${node.summary.replace(/\n+/g, " ")}`);
|
|
1259
|
+
}
|
|
1001
1260
|
}
|
|
1002
1261
|
}
|
|
1003
1262
|
return lines.join("\n");
|
|
@@ -1076,6 +1335,31 @@ function rankHits(hits, opts = {}) {
|
|
|
1076
1335
|
}
|
|
1077
1336
|
|
|
1078
1337
|
// src/retrieval/query-pipeline.ts
|
|
1338
|
+
async function runCrossProjectQuery(sources, query, opts) {
|
|
1339
|
+
const queryVector = opts.embeddingProvider ? await opts.embeddingProvider.embed(query) : null;
|
|
1340
|
+
const lists = [];
|
|
1341
|
+
const hits = [];
|
|
1342
|
+
const perProject = [];
|
|
1343
|
+
let bm25Count = 0;
|
|
1344
|
+
let vectorCount = 0;
|
|
1345
|
+
for (const source of sources) {
|
|
1346
|
+
const label = (hit) => ({ ...hit, project: source.label });
|
|
1347
|
+
const bm25Hits = source.store.search(source.projectId, query, opts.candidates).map(label);
|
|
1348
|
+
const vectorHits = queryVector ? source.store.vectorSearch(source.projectId, queryVector, opts.candidates) : [];
|
|
1349
|
+
bm25Count += bm25Hits.length;
|
|
1350
|
+
vectorCount += vectorHits.length;
|
|
1351
|
+
perProject.push({ label: source.label, bm25: bm25Hits.length, vector: vectorHits.length });
|
|
1352
|
+
if (bm25Hits.length > 0) lists.push(bm25Hits);
|
|
1353
|
+
if (vectorHits.length > 0) {
|
|
1354
|
+
lists.push(vectorHits.map((hit) => ({ ...hit, rank: 0, project: source.label })));
|
|
1355
|
+
}
|
|
1356
|
+
hits.push(...mergeSearchAndVectorHits(bm25Hits, vectorHits).map(label));
|
|
1357
|
+
}
|
|
1358
|
+
const relevanceScores = reciprocalRankFusion(lists);
|
|
1359
|
+
const ranked = rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores });
|
|
1360
|
+
const packed = packContext(ranked, opts.budget, { query });
|
|
1361
|
+
return { bm25Count, vectorCount, hits, packed, perProject };
|
|
1362
|
+
}
|
|
1079
1363
|
async function runHybridQuery(store, projectId, query, opts) {
|
|
1080
1364
|
const bm25Hits = store.search(projectId, query, opts.candidates);
|
|
1081
1365
|
let vectorHits = [];
|
|
@@ -1086,10 +1370,55 @@ async function runHybridQuery(store, projectId, query, opts) {
|
|
|
1086
1370
|
const hits = vectorHits.length > 0 ? mergeSearchAndVectorHits(bm25Hits, vectorHits) : bm25Hits;
|
|
1087
1371
|
const relevanceScores = vectorHits.length > 0 ? reciprocalRankFusion([bm25Hits, vectorHits]) : void 0;
|
|
1088
1372
|
const ranked = rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores });
|
|
1089
|
-
const packed = packContext(ranked, opts.budget);
|
|
1373
|
+
const packed = packContext(ranked, opts.budget, { query });
|
|
1090
1374
|
return { bm25Count: bm25Hits.length, vectorCount: vectorHits.length, hits, packed };
|
|
1091
1375
|
}
|
|
1092
1376
|
|
|
1377
|
+
// src/retrieval/sources.ts
|
|
1378
|
+
import { basename } from "path";
|
|
1379
|
+
async function openAllProjectSources(current) {
|
|
1380
|
+
const { entries, missing } = await readLiveRegistry();
|
|
1381
|
+
const wanted = [current];
|
|
1382
|
+
for (const entry of entries) {
|
|
1383
|
+
if (entry.projectId === current.projectId) continue;
|
|
1384
|
+
wanted.push({ projectId: entry.projectId, root: entry.root, dbPath: entry.dbPath });
|
|
1385
|
+
}
|
|
1386
|
+
const labels = labelProjects(wanted);
|
|
1387
|
+
const sources = [];
|
|
1388
|
+
const unreadable = [];
|
|
1389
|
+
wanted.forEach((project, index) => {
|
|
1390
|
+
try {
|
|
1391
|
+
sources.push({
|
|
1392
|
+
store: MemoryStore.open(project.dbPath),
|
|
1393
|
+
projectId: project.projectId,
|
|
1394
|
+
label: labels[index] ?? project.projectId.slice(0, 8)
|
|
1395
|
+
});
|
|
1396
|
+
} catch (err) {
|
|
1397
|
+
const entry = entries.find((e) => e.projectId === project.projectId);
|
|
1398
|
+
if (entry) unreadable.push({ entry, reason: err.message });
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
return {
|
|
1402
|
+
sources,
|
|
1403
|
+
missing,
|
|
1404
|
+
unreadable,
|
|
1405
|
+
close: () => {
|
|
1406
|
+
for (const source of sources) source.store.close();
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function labelProjects(projects) {
|
|
1411
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1412
|
+
for (const project of projects) {
|
|
1413
|
+
const name = basename(project.root) || project.root;
|
|
1414
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
1415
|
+
}
|
|
1416
|
+
return projects.map((project) => {
|
|
1417
|
+
const name = basename(project.root) || project.root;
|
|
1418
|
+
return (counts.get(name) ?? 0) > 1 ? `${name}#${project.projectId.slice(0, 6)}` : name;
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1093
1422
|
// src/vector/embed.ts
|
|
1094
1423
|
var DEFAULT_BASE_URL = "http://127.0.0.1:11434";
|
|
1095
1424
|
var DEFAULT_MODEL = "nomic-embed-text";
|
|
@@ -1129,7 +1458,7 @@ var OllamaEmbeddingProvider = class {
|
|
|
1129
1458
|
};
|
|
1130
1459
|
|
|
1131
1460
|
// src/cli/commands/sync.ts
|
|
1132
|
-
import
|
|
1461
|
+
import pc4 from "picocolors";
|
|
1133
1462
|
|
|
1134
1463
|
// src/conversation/chunk.ts
|
|
1135
1464
|
var HEADING_LINE = /^#{1,6}\s+(.+)$/;
|
|
@@ -1169,21 +1498,31 @@ function chunkAssistantText(text, maxChars) {
|
|
|
1169
1498
|
|
|
1170
1499
|
// src/conversation/redact.ts
|
|
1171
1500
|
var RULES = [
|
|
1172
|
-
{
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1501
|
+
{
|
|
1502
|
+
name: "private-key-block",
|
|
1503
|
+
pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
1504
|
+
highConfidence: true
|
|
1505
|
+
},
|
|
1506
|
+
{ name: "aws-access-key", pattern: /\bAKIA[0-9A-Z]{16}\b/g, highConfidence: true },
|
|
1507
|
+
{ name: "github-token", pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, highConfidence: true },
|
|
1508
|
+
{ name: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, highConfidence: true },
|
|
1509
|
+
{
|
|
1510
|
+
name: "jwt",
|
|
1511
|
+
pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
|
|
1512
|
+
highConfidence: true
|
|
1513
|
+
},
|
|
1177
1514
|
// key/token/secret/password = "value" or : value, in code, JSON, env-file or prose form.
|
|
1178
1515
|
{
|
|
1179
1516
|
name: "key-value-secret",
|
|
1180
|
-
pattern: /\b((?:api[_-]?key|secret|password|passwd|token|access[_-]?key)s?)\s*[:=]\s*['"]?[A-Za-z0-9_\-./+=]{8,}['"]?/gi
|
|
1517
|
+
pattern: /\b((?:api[_-]?key|secret|password|passwd|token|access[_-]?key)s?)\s*[:=]\s*['"]?[A-Za-z0-9_\-./+=]{8,}['"]?/gi,
|
|
1518
|
+
highConfidence: false
|
|
1181
1519
|
}
|
|
1182
1520
|
];
|
|
1183
|
-
function redact(text) {
|
|
1521
|
+
function redact(text, profile = "all") {
|
|
1184
1522
|
let redactedCount = 0;
|
|
1185
1523
|
let out = text;
|
|
1186
1524
|
for (const rule of RULES) {
|
|
1525
|
+
if (profile === "high-confidence" && !rule.highConfidence) continue;
|
|
1187
1526
|
out = out.replace(rule.pattern, (_match, ...rest) => {
|
|
1188
1527
|
redactedCount += 1;
|
|
1189
1528
|
const key = typeof rest[0] === "string" ? rest[0] : null;
|
|
@@ -1198,8 +1537,8 @@ var DEFAULT_MAX_BODY_CHARS = 2500;
|
|
|
1198
1537
|
var DEFAULT_MAX_CHUNK_CHARS = 900;
|
|
1199
1538
|
var MAX_TITLE_CHARS = 200;
|
|
1200
1539
|
var MAX_FILES_PER_NODE = 20;
|
|
1201
|
-
var EXPLANATION_MARKERS = /\b(because|the reason|design decision|trade-?off|instead of|rationale|so that)\b
|
|
1202
|
-
var TRIVIAL_ACK = /^(ok|okay|thanks
|
|
1540
|
+
var EXPLANATION_MARKERS = /\b(because|the reason|design decision|trade-?off|instead of|rationale|so that)\b/i;
|
|
1541
|
+
var TRIVIAL_ACK = /^(ok|okay|thanks?|got it|sounds good|👍|done)\.?!?$/i;
|
|
1203
1542
|
function scoreConversationTurn(userText, replyText) {
|
|
1204
1543
|
const text = `${userText}
|
|
1205
1544
|
${replyText}`;
|
|
@@ -1273,63 +1612,6 @@ function collectConversationTurns(turns, projectId, opts = {}) {
|
|
|
1273
1612
|
return turns.filter((t) => t.assistantText.length > 0).flatMap((turn) => toMemoryNodes(turn, projectId, opts));
|
|
1274
1613
|
}
|
|
1275
1614
|
|
|
1276
|
-
// src/collectors/docs.ts
|
|
1277
|
-
var DEFAULT_MAX_BODY_CHARS2 = 2e3;
|
|
1278
|
-
var DEFAULT_MAX_CHUNK_CHARS2 = 1200;
|
|
1279
|
-
var MAX_TITLE_CHARS2 = 200;
|
|
1280
|
-
var EXPLANATION_MARKERS2 = /\b(because|the reason|design decision|trade-?off|instead of|rationale|why)\b/i;
|
|
1281
|
-
function scoreDocSection(path, heading, text) {
|
|
1282
|
-
let score = 0.45;
|
|
1283
|
-
if (EXPLANATION_MARKERS2.test(text)) score += 0.25;
|
|
1284
|
-
if (/(^|\/)readme\.md$/i.test(path)) score += 0.1;
|
|
1285
|
-
if (heading === null) score -= 0.1;
|
|
1286
|
-
if (text.length < 80) score -= 0.15;
|
|
1287
|
-
return Number(Math.min(1, Math.max(0.05, score)).toFixed(3));
|
|
1288
|
-
}
|
|
1289
|
-
function slugify(heading, index) {
|
|
1290
|
-
if (heading === null) return `_preamble-${index}`;
|
|
1291
|
-
const slug = heading.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1292
|
-
return slug || `_section-${index}`;
|
|
1293
|
-
}
|
|
1294
|
-
function sectionTitle(path, heading, index, count) {
|
|
1295
|
-
if (heading) return truncate(`${path} \u2014 ${heading}`, MAX_TITLE_CHARS2);
|
|
1296
|
-
if (count > 1) return truncate(`${path} (part ${index + 1}/${count})`, MAX_TITLE_CHARS2);
|
|
1297
|
-
return truncate(path, MAX_TITLE_CHARS2);
|
|
1298
|
-
}
|
|
1299
|
-
function toMemoryNodes2(file, projectId, opts = {}) {
|
|
1300
|
-
const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS2;
|
|
1301
|
-
const maxChunk = opts.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS2;
|
|
1302
|
-
const chunks = chunkAssistantText(file.content, maxChunk);
|
|
1303
|
-
if (chunks.length === 0) return [];
|
|
1304
|
-
const seenSlugs = /* @__PURE__ */ new Map();
|
|
1305
|
-
return chunks.map((chunk, index) => {
|
|
1306
|
-
const baseSlug = slugify(chunk.heading, index);
|
|
1307
|
-
const occurrence = seenSlugs.get(baseSlug) ?? 0;
|
|
1308
|
-
seenSlugs.set(baseSlug, occurrence + 1);
|
|
1309
|
-
const naturalKey = occurrence === 0 ? `${file.path}#${baseSlug}` : `${file.path}#${baseSlug}:${occurrence}`;
|
|
1310
|
-
return {
|
|
1311
|
-
id: makeNodeId(projectId, "doc_section", naturalKey),
|
|
1312
|
-
kind: "doc_section",
|
|
1313
|
-
projectId,
|
|
1314
|
-
ts: file.ts,
|
|
1315
|
-
source: "docs",
|
|
1316
|
-
title: sectionTitle(file.path, chunk.heading, index, chunks.length),
|
|
1317
|
-
body: truncate(chunk.text, maxBody),
|
|
1318
|
-
files: [{ path: file.path, insertions: null, deletions: null, binary: false }],
|
|
1319
|
-
signal: scoreDocSection(file.path, chunk.heading, chunk.text),
|
|
1320
|
-
meta: {
|
|
1321
|
-
path: file.path,
|
|
1322
|
-
heading: chunk.heading,
|
|
1323
|
-
chunkIndex: index,
|
|
1324
|
-
chunkCount: chunks.length
|
|
1325
|
-
}
|
|
1326
|
-
};
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
function collectDocFiles(files, projectId, opts = {}) {
|
|
1330
|
-
return files.flatMap((file) => toMemoryNodes2(file, projectId, opts));
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
1615
|
// src/git/parse.ts
|
|
1334
1616
|
var RECORD_SEP = "";
|
|
1335
1617
|
var UNIT_SEP = "";
|
|
@@ -1439,8 +1721,182 @@ function unquoteGitPath(p) {
|
|
|
1439
1721
|
return Buffer.from(bytes).toString("utf8");
|
|
1440
1722
|
}
|
|
1441
1723
|
|
|
1442
|
-
// src/git/
|
|
1724
|
+
// src/git/diff.ts
|
|
1725
|
+
var GIT_DIFF_LOG_FORMAT = "%x1e%H%x1f%h%x1f%aI%x1f%s%x1f";
|
|
1726
|
+
var FIELD_COUNT2 = 5;
|
|
1443
1727
|
var EMPTY_HISTORY = /does not have any commits yet|unknown revision|bad revision|ambiguous argument/i;
|
|
1728
|
+
function buildDiffLogArgs(opts = {}) {
|
|
1729
|
+
const { rev = "HEAD", afterCommit, since, maxCount, contextLines = 3, paths } = opts;
|
|
1730
|
+
const args = [
|
|
1731
|
+
"log",
|
|
1732
|
+
`--format=${GIT_DIFF_LOG_FORMAT}`,
|
|
1733
|
+
"--patch",
|
|
1734
|
+
"--no-color",
|
|
1735
|
+
// A merge produces no patch at all unless `-m`/`--cc` is passed, and the
|
|
1736
|
+
// combined diff those print is a different format from the one parsed
|
|
1737
|
+
// here. Excluding merges up front keeps the parser honest about what it
|
|
1738
|
+
// supports; the merge itself is still remembered as a `git_commit` node.
|
|
1739
|
+
"--no-merges",
|
|
1740
|
+
"--find-renames",
|
|
1741
|
+
// Never run a user-configured textconv filter: it would execute an
|
|
1742
|
+
// arbitrary program from repo config during a sync, and its output is not
|
|
1743
|
+
// the diff we claim to be indexing.
|
|
1744
|
+
"--no-textconv",
|
|
1745
|
+
`--unified=${Math.max(0, contextLines)}`
|
|
1746
|
+
];
|
|
1747
|
+
if (maxCount && maxCount > 0) args.push(`--max-count=${maxCount}`);
|
|
1748
|
+
if (since) args.push(`--since=${since}`);
|
|
1749
|
+
args.push(afterCommit ? `${afterCommit}..${rev}` : rev);
|
|
1750
|
+
if (paths?.length) args.push("--", ...paths);
|
|
1751
|
+
return args;
|
|
1752
|
+
}
|
|
1753
|
+
async function* readCommitDiffs(cwd, opts = {}) {
|
|
1754
|
+
const args = buildDiffLogArgs(opts);
|
|
1755
|
+
let buffer = "";
|
|
1756
|
+
try {
|
|
1757
|
+
for await (const chunk of gitStream(cwd, args)) {
|
|
1758
|
+
buffer += chunk;
|
|
1759
|
+
const { records, rest } = splitRecords(buffer);
|
|
1760
|
+
buffer = rest;
|
|
1761
|
+
for (const record of records) {
|
|
1762
|
+
const commit = parseCommitDiffRecord(record);
|
|
1763
|
+
if (commit) yield commit;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
} catch (err) {
|
|
1767
|
+
if (err instanceof GitError && EMPTY_HISTORY.test(err.stderr)) return;
|
|
1768
|
+
throw err;
|
|
1769
|
+
}
|
|
1770
|
+
for (const record of splitRecords(buffer, true).records) {
|
|
1771
|
+
const commit = parseCommitDiffRecord(record);
|
|
1772
|
+
if (commit) yield commit;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function parseCommitDiffRecord(record) {
|
|
1776
|
+
let payload = record;
|
|
1777
|
+
while (payload.startsWith(RECORD_SEP)) payload = payload.slice(RECORD_SEP.length);
|
|
1778
|
+
const parts = payload.split(UNIT_SEP);
|
|
1779
|
+
if (parts.length < FIELD_COUNT2) return null;
|
|
1780
|
+
const sha = parts[0] ?? "";
|
|
1781
|
+
if (!/^[0-9a-f]{7,64}$/i.test(sha)) return null;
|
|
1782
|
+
const patchBlock = parts[parts.length - 1] ?? "";
|
|
1783
|
+
const subject = parts.slice(3, parts.length - 1).join(UNIT_SEP).trim();
|
|
1784
|
+
return {
|
|
1785
|
+
sha,
|
|
1786
|
+
shortSha: parts[1] ?? "",
|
|
1787
|
+
authoredAt: parts[2] ?? "",
|
|
1788
|
+
subject,
|
|
1789
|
+
files: parseFileDiffs(patchBlock)
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
var FILE_HEADER = "diff --git ";
|
|
1793
|
+
var HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/;
|
|
1794
|
+
function parseFileDiffs(block) {
|
|
1795
|
+
const files = [];
|
|
1796
|
+
let section = null;
|
|
1797
|
+
const flush = () => {
|
|
1798
|
+
if (!section) return;
|
|
1799
|
+
const parsed = parseFileSection(section);
|
|
1800
|
+
if (parsed) files.push(parsed);
|
|
1801
|
+
section = null;
|
|
1802
|
+
};
|
|
1803
|
+
for (const raw of block.split("\n")) {
|
|
1804
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
1805
|
+
if (line.startsWith(FILE_HEADER)) {
|
|
1806
|
+
flush();
|
|
1807
|
+
section = [line];
|
|
1808
|
+
} else if (section) {
|
|
1809
|
+
section.push(line);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
flush();
|
|
1813
|
+
return files;
|
|
1814
|
+
}
|
|
1815
|
+
function parseFileSection(lines) {
|
|
1816
|
+
let status = "modified";
|
|
1817
|
+
let binary = false;
|
|
1818
|
+
let fromPath = null;
|
|
1819
|
+
let toPath = null;
|
|
1820
|
+
let renamedFrom = null;
|
|
1821
|
+
let hunkStart = -1;
|
|
1822
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1823
|
+
const line = lines[i] ?? "";
|
|
1824
|
+
if (HUNK_HEADER.test(line)) {
|
|
1825
|
+
hunkStart = i;
|
|
1826
|
+
break;
|
|
1827
|
+
}
|
|
1828
|
+
if (line.startsWith("new file mode")) status = "added";
|
|
1829
|
+
else if (line.startsWith("deleted file mode")) status = "deleted";
|
|
1830
|
+
else if (line.startsWith("rename from ")) renamedFrom = unquoteGitPath(line.slice("rename from ".length));
|
|
1831
|
+
else if (line.startsWith("rename to ")) status = "renamed";
|
|
1832
|
+
else if (line.startsWith("copy from ")) renamedFrom = unquoteGitPath(line.slice("copy from ".length));
|
|
1833
|
+
else if (line.startsWith("Binary files ") || line.startsWith("GIT binary patch")) binary = true;
|
|
1834
|
+
else if (line.startsWith("--- ")) fromPath = stripDiffPathPrefix(line.slice(4));
|
|
1835
|
+
else if (line.startsWith("+++ ")) toPath = stripDiffPathPrefix(line.slice(4));
|
|
1836
|
+
}
|
|
1837
|
+
const header = parseDiffGitPaths(lines[0] ?? "");
|
|
1838
|
+
const path = toPath ?? header.b ?? fromPath ?? header.a;
|
|
1839
|
+
if (!path) return null;
|
|
1840
|
+
const previousPath = renamedFrom ?? (status === "renamed" ? fromPath ?? header.a ?? void 0 : void 0);
|
|
1841
|
+
if (binary || hunkStart === -1) {
|
|
1842
|
+
return {
|
|
1843
|
+
path,
|
|
1844
|
+
...previousPath && previousPath !== path ? { previousPath } : {},
|
|
1845
|
+
status,
|
|
1846
|
+
binary,
|
|
1847
|
+
insertions: 0,
|
|
1848
|
+
deletions: 0,
|
|
1849
|
+
hunkCount: 0,
|
|
1850
|
+
patch: ""
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
const hunkLines = lines.slice(hunkStart);
|
|
1854
|
+
let insertions = 0;
|
|
1855
|
+
let deletions = 0;
|
|
1856
|
+
let hunkCount = 0;
|
|
1857
|
+
for (const line of hunkLines) {
|
|
1858
|
+
if (HUNK_HEADER.test(line)) hunkCount += 1;
|
|
1859
|
+
else if (line.startsWith("+")) insertions += 1;
|
|
1860
|
+
else if (line.startsWith("-")) deletions += 1;
|
|
1861
|
+
}
|
|
1862
|
+
return {
|
|
1863
|
+
path,
|
|
1864
|
+
...previousPath && previousPath !== path ? { previousPath } : {},
|
|
1865
|
+
status,
|
|
1866
|
+
binary: false,
|
|
1867
|
+
insertions,
|
|
1868
|
+
deletions,
|
|
1869
|
+
hunkCount,
|
|
1870
|
+
patch: hunkLines.join("\n").trimEnd()
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
function stripDiffPathPrefix(raw) {
|
|
1874
|
+
const cleaned = unquoteGitPath(raw.trim());
|
|
1875
|
+
if (cleaned === "/dev/null") return null;
|
|
1876
|
+
return cleaned.replace(/^[ab]\//, "");
|
|
1877
|
+
}
|
|
1878
|
+
function parseDiffGitPaths(headerLine) {
|
|
1879
|
+
const rest = headerLine.slice(FILE_HEADER.length);
|
|
1880
|
+
if (rest.startsWith('"')) {
|
|
1881
|
+
const match = /^("(?:[^"\\]|\\.)*")\s+("(?:[^"\\]|\\.)*"|\S+)$/.exec(rest);
|
|
1882
|
+
if (match) {
|
|
1883
|
+
return { a: stripDiffPathPrefix(match[1] ?? ""), b: stripDiffPathPrefix(match[2] ?? "") };
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const quotedSecond = /^(\S+)\s+("(?:[^"\\]|\\.)*")$/.exec(rest);
|
|
1887
|
+
if (quotedSecond) {
|
|
1888
|
+
return { a: stripDiffPathPrefix(quotedSecond[1] ?? ""), b: stripDiffPathPrefix(quotedSecond[2] ?? "") };
|
|
1889
|
+
}
|
|
1890
|
+
const split = / b\//.exec(rest);
|
|
1891
|
+
if (!split || split.index <= 0) return { a: null, b: null };
|
|
1892
|
+
return {
|
|
1893
|
+
a: stripDiffPathPrefix(rest.slice(0, split.index)),
|
|
1894
|
+
b: stripDiffPathPrefix(rest.slice(split.index + 1))
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// src/git/log.ts
|
|
1899
|
+
var EMPTY_HISTORY2 = /does not have any commits yet|unknown revision|bad revision|ambiguous argument/i;
|
|
1444
1900
|
function buildLogArgs(opts = {}) {
|
|
1445
1901
|
const { rev = "HEAD", afterCommit, since, maxCount, includeMerges = true, paths } = opts;
|
|
1446
1902
|
const args = ["log", `--format=${GIT_LOG_FORMAT}`, "--numstat", "--no-color"];
|
|
@@ -1465,7 +1921,7 @@ async function* readCommits(cwd, opts = {}) {
|
|
|
1465
1921
|
}
|
|
1466
1922
|
}
|
|
1467
1923
|
} catch (err) {
|
|
1468
|
-
if (err instanceof GitError &&
|
|
1924
|
+
if (err instanceof GitError && EMPTY_HISTORY2.test(err.stderr)) return;
|
|
1469
1925
|
throw err;
|
|
1470
1926
|
}
|
|
1471
1927
|
for (const record of splitRecords(buffer, true).records) {
|
|
@@ -1476,7 +1932,7 @@ async function* readCommits(cwd, opts = {}) {
|
|
|
1476
1932
|
|
|
1477
1933
|
// src/collectors/git-commits.ts
|
|
1478
1934
|
var DEFAULTS = { maxFilesPerNode: 40, maxBodyChars: 4e3 };
|
|
1479
|
-
var
|
|
1935
|
+
var MAX_TITLE_CHARS2 = 200;
|
|
1480
1936
|
var CONVENTIONAL = /^([a-z]+)(?:\(([^)]*)\))?(!)?:\s*(.+)$/i;
|
|
1481
1937
|
function parseConventionalHeader(subject) {
|
|
1482
1938
|
const m = CONVENTIONAL.exec(subject.trim());
|
|
@@ -1545,7 +2001,7 @@ function toMemoryNode(commit, projectId, opts = {}) {
|
|
|
1545
2001
|
projectId,
|
|
1546
2002
|
ts: commit.authoredAt,
|
|
1547
2003
|
source: "git",
|
|
1548
|
-
title: truncate(commit.subject || `(no subject) ${commit.shortSha}`,
|
|
2004
|
+
title: truncate(commit.subject || `(no subject) ${commit.shortSha}`, MAX_TITLE_CHARS2),
|
|
1549
2005
|
body: truncate(bodyParts.join("\n"), maxBody),
|
|
1550
2006
|
files: keptFiles,
|
|
1551
2007
|
signal: scoreCommit(commit),
|
|
@@ -1572,9 +2028,156 @@ async function* collectGitCommits(cwd, projectId, opts = {}) {
|
|
|
1572
2028
|
}
|
|
1573
2029
|
}
|
|
1574
2030
|
|
|
2031
|
+
// src/collectors/diffs.ts
|
|
2032
|
+
var DIFF_SOURCE = "diff";
|
|
2033
|
+
var DEFAULTS2 = { maxFilesPerCommit: 20, maxBodyChars: 2e3 };
|
|
2034
|
+
var MAX_TITLE_CHARS3 = 200;
|
|
2035
|
+
var GENERATED_PATHS = [
|
|
2036
|
+
/(^|\/)(node_modules|dist|build|out|coverage|vendor|third_party)\//,
|
|
2037
|
+
/(^|\/)(package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml|composer\.lock|Cargo\.lock|poetry\.lock|Gemfile\.lock|go\.sum)$/,
|
|
2038
|
+
/\.(min\.js|min\.css|map|snap)$/
|
|
2039
|
+
];
|
|
2040
|
+
function isGeneratedPath(path) {
|
|
2041
|
+
return GENERATED_PATHS.some((re) => re.test(path));
|
|
2042
|
+
}
|
|
2043
|
+
var TEST_PATHS = /(^|\/)(tests?|__tests__|spec|e2e)\/|\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
2044
|
+
function scoreFileDiff(subject, file) {
|
|
2045
|
+
const header = parseConventionalHeader(subject);
|
|
2046
|
+
let score = header.type ? TYPE_WEIGHTS[header.type] ?? 0.5 : 0.5;
|
|
2047
|
+
if (header.breaking) score += 0.1;
|
|
2048
|
+
if (TEST_PATHS.test(file.path)) score -= 0.1;
|
|
2049
|
+
if (file.status === "added") score += 0.05;
|
|
2050
|
+
if (file.status === "deleted") score -= 0.1;
|
|
2051
|
+
const churn = file.insertions + file.deletions;
|
|
2052
|
+
if (churn <= 2) score -= 0.05;
|
|
2053
|
+
if (churn > 400) score *= 0.75;
|
|
2054
|
+
return Number(Math.min(1, Math.max(0.05, score)).toFixed(3));
|
|
2055
|
+
}
|
|
2056
|
+
function byChurnDesc2(a, b) {
|
|
2057
|
+
return b.insertions + b.deletions - (a.insertions + a.deletions);
|
|
2058
|
+
}
|
|
2059
|
+
function indexableFiles(files) {
|
|
2060
|
+
return files.filter((f) => !f.binary && f.hunkCount > 0 && !isGeneratedPath(f.path));
|
|
2061
|
+
}
|
|
2062
|
+
var STATUS_LABEL = {
|
|
2063
|
+
added: "added",
|
|
2064
|
+
deleted: "deleted",
|
|
2065
|
+
renamed: "renamed",
|
|
2066
|
+
modified: "modified"
|
|
2067
|
+
};
|
|
2068
|
+
function fileTitle(shortSha, subject, file) {
|
|
2069
|
+
return truncate(`${file.path} @ ${shortSha} \u2014 ${subject}`, MAX_TITLE_CHARS3);
|
|
2070
|
+
}
|
|
2071
|
+
function toMemoryNodes2(commit, projectId, opts = {}) {
|
|
2072
|
+
const maxFiles = opts.maxFilesPerCommit ?? DEFAULTS2.maxFilesPerCommit;
|
|
2073
|
+
const maxBody = opts.maxBodyChars ?? DEFAULTS2.maxBodyChars;
|
|
2074
|
+
const kept = indexableFiles(commit.files).sort(byChurnDesc2).slice(0, maxFiles);
|
|
2075
|
+
return kept.map((file) => {
|
|
2076
|
+
const churn = `+${file.insertions}/-${file.deletions}`;
|
|
2077
|
+
const renamePart = file.previousPath ? `, renamed from ${file.previousPath}` : "";
|
|
2078
|
+
const head = [
|
|
2079
|
+
`${commit.subject} (${commit.shortSha})`,
|
|
2080
|
+
`${STATUS_LABEL[file.status]} ${file.path} (${churn}, ${file.hunkCount} hunk${file.hunkCount === 1 ? "" : "s"}${renamePart})`,
|
|
2081
|
+
""
|
|
2082
|
+
].join("\n");
|
|
2083
|
+
const { text: patch } = redact(file.patch, "high-confidence");
|
|
2084
|
+
return {
|
|
2085
|
+
id: makeNodeId(projectId, "code_diff", `${commit.sha}:${file.path}`),
|
|
2086
|
+
kind: "code_diff",
|
|
2087
|
+
projectId,
|
|
2088
|
+
ts: commit.authoredAt,
|
|
2089
|
+
source: DIFF_SOURCE,
|
|
2090
|
+
title: fileTitle(commit.shortSha, commit.subject, file),
|
|
2091
|
+
body: truncate(head + patch, maxBody),
|
|
2092
|
+
files: [
|
|
2093
|
+
{
|
|
2094
|
+
path: file.path,
|
|
2095
|
+
...file.previousPath ? { previousPath: file.previousPath } : {},
|
|
2096
|
+
insertions: file.insertions,
|
|
2097
|
+
deletions: file.deletions,
|
|
2098
|
+
binary: false
|
|
2099
|
+
}
|
|
2100
|
+
],
|
|
2101
|
+
signal: scoreFileDiff(commit.subject, file),
|
|
2102
|
+
meta: {
|
|
2103
|
+
sha: commit.sha,
|
|
2104
|
+
shortSha: commit.shortSha,
|
|
2105
|
+
path: file.path,
|
|
2106
|
+
status: file.status,
|
|
2107
|
+
hunkCount: file.hunkCount,
|
|
2108
|
+
insertions: file.insertions,
|
|
2109
|
+
deletions: file.deletions,
|
|
2110
|
+
subject: commit.subject
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
async function* collectCommitDiffs(cwd, projectId, opts = {}) {
|
|
2116
|
+
for await (const commit of readCommitDiffs(cwd, opts)) {
|
|
2117
|
+
for (const node of toMemoryNodes2(commit, projectId, opts)) yield node;
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
// src/collectors/docs.ts
|
|
2122
|
+
var DEFAULT_MAX_BODY_CHARS2 = 2e3;
|
|
2123
|
+
var DEFAULT_MAX_CHUNK_CHARS2 = 1200;
|
|
2124
|
+
var MAX_TITLE_CHARS4 = 200;
|
|
2125
|
+
var EXPLANATION_MARKERS2 = /\b(because|the reason|design decision|trade-?off|instead of|rationale|why)\b/i;
|
|
2126
|
+
function scoreDocSection(path, heading, text) {
|
|
2127
|
+
let score = 0.45;
|
|
2128
|
+
if (EXPLANATION_MARKERS2.test(text)) score += 0.25;
|
|
2129
|
+
if (/(^|\/)readme\.md$/i.test(path)) score += 0.1;
|
|
2130
|
+
if (heading === null) score -= 0.1;
|
|
2131
|
+
if (text.length < 80) score -= 0.15;
|
|
2132
|
+
return Number(Math.min(1, Math.max(0.05, score)).toFixed(3));
|
|
2133
|
+
}
|
|
2134
|
+
function slugify(heading, index) {
|
|
2135
|
+
if (heading === null) return `_preamble-${index}`;
|
|
2136
|
+
const slug = heading.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2137
|
+
return slug || `_section-${index}`;
|
|
2138
|
+
}
|
|
2139
|
+
function sectionTitle(path, heading, index, count) {
|
|
2140
|
+
if (heading) return truncate(`${path} \u2014 ${heading}`, MAX_TITLE_CHARS4);
|
|
2141
|
+
if (count > 1) return truncate(`${path} (part ${index + 1}/${count})`, MAX_TITLE_CHARS4);
|
|
2142
|
+
return truncate(path, MAX_TITLE_CHARS4);
|
|
2143
|
+
}
|
|
2144
|
+
function toMemoryNodes3(file, projectId, opts = {}) {
|
|
2145
|
+
const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS2;
|
|
2146
|
+
const maxChunk = opts.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS2;
|
|
2147
|
+
const chunks = chunkAssistantText(file.content, maxChunk);
|
|
2148
|
+
if (chunks.length === 0) return [];
|
|
2149
|
+
const seenSlugs = /* @__PURE__ */ new Map();
|
|
2150
|
+
return chunks.map((chunk, index) => {
|
|
2151
|
+
const baseSlug = slugify(chunk.heading, index);
|
|
2152
|
+
const occurrence = seenSlugs.get(baseSlug) ?? 0;
|
|
2153
|
+
seenSlugs.set(baseSlug, occurrence + 1);
|
|
2154
|
+
const naturalKey = occurrence === 0 ? `${file.path}#${baseSlug}` : `${file.path}#${baseSlug}:${occurrence}`;
|
|
2155
|
+
return {
|
|
2156
|
+
id: makeNodeId(projectId, "doc_section", naturalKey),
|
|
2157
|
+
kind: "doc_section",
|
|
2158
|
+
projectId,
|
|
2159
|
+
ts: file.ts,
|
|
2160
|
+
source: "docs",
|
|
2161
|
+
title: sectionTitle(file.path, chunk.heading, index, chunks.length),
|
|
2162
|
+
body: truncate(chunk.text, maxBody),
|
|
2163
|
+
files: [{ path: file.path, insertions: null, deletions: null, binary: false }],
|
|
2164
|
+
signal: scoreDocSection(file.path, chunk.heading, chunk.text),
|
|
2165
|
+
meta: {
|
|
2166
|
+
path: file.path,
|
|
2167
|
+
heading: chunk.heading,
|
|
2168
|
+
chunkIndex: index,
|
|
2169
|
+
chunkCount: chunks.length
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
function collectDocFiles(files, projectId, opts = {}) {
|
|
2175
|
+
return files.flatMap((file) => toMemoryNodes3(file, projectId, opts));
|
|
2176
|
+
}
|
|
2177
|
+
|
|
1575
2178
|
// src/collectors/shell-history.ts
|
|
1576
2179
|
var DEFAULT_MAX_BODY_CHARS3 = 1e3;
|
|
1577
|
-
var
|
|
2180
|
+
var MAX_TITLE_CHARS5 = 200;
|
|
1578
2181
|
var NOISE = /^(cd|ls|dir|pwd|clear|cls|exit|history|whoami|date|type|cat|more|less|ll|la)\b/i;
|
|
1579
2182
|
var BUILD_TEST = /^(npm|pnpm|yarn)\s+(run\s+)?(test|build|lint|typecheck|tsc)\b|^(pytest|go\s+test|cargo\s+(test|build)|mvn\s+test|gradle\s+test|dotnet\s+(test|build))\b/i;
|
|
1580
2183
|
var INSTALL = /^(npm|pnpm|yarn)\s+(install|add|remove|uninstall|ci)\b|^pip\s+install\b|^(cargo\s+add|go\s+get|composer\s+require)\b/i;
|
|
@@ -1615,7 +2218,7 @@ function toMemoryNode2(entry, projectId, opts = {}) {
|
|
|
1615
2218
|
projectId,
|
|
1616
2219
|
ts: entry.ts,
|
|
1617
2220
|
source: `shell:${entry.shell}`,
|
|
1618
|
-
title: truncate(titleLine,
|
|
2221
|
+
title: truncate(titleLine, MAX_TITLE_CHARS5),
|
|
1619
2222
|
body: renderBody(entry, maxBody),
|
|
1620
2223
|
files: [],
|
|
1621
2224
|
signal: scoreShellCommand(entry),
|
|
@@ -1634,24 +2237,24 @@ function collectShellHistory(entries, projectId, opts = {}) {
|
|
|
1634
2237
|
}
|
|
1635
2238
|
|
|
1636
2239
|
// src/conversation/claude-code-reader.ts
|
|
1637
|
-
import { readFile as
|
|
2240
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1638
2241
|
|
|
1639
2242
|
// src/conversation/paths.ts
|
|
1640
|
-
import { existsSync as
|
|
2243
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1641
2244
|
import { readdir } from "fs/promises";
|
|
1642
|
-
import { homedir as
|
|
1643
|
-
import { join as
|
|
2245
|
+
import { homedir as homedir3 } from "os";
|
|
2246
|
+
import { join as join5 } from "path";
|
|
1644
2247
|
function claudeProjectSlug(repoRoot) {
|
|
1645
2248
|
return repoRoot.replace(/[\\/:]/g, "-");
|
|
1646
2249
|
}
|
|
1647
2250
|
function claudeProjectTranscriptDir(repoRoot) {
|
|
1648
|
-
return
|
|
2251
|
+
return join5(homedir3(), ".claude", "projects", claudeProjectSlug(repoRoot));
|
|
1649
2252
|
}
|
|
1650
2253
|
async function listTranscriptFiles(repoRoot) {
|
|
1651
2254
|
const dir = claudeProjectTranscriptDir(repoRoot);
|
|
1652
|
-
if (!
|
|
2255
|
+
if (!existsSync3(dir)) return [];
|
|
1653
2256
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
1654
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) =>
|
|
2257
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => join5(dir, e.name));
|
|
1655
2258
|
}
|
|
1656
2259
|
|
|
1657
2260
|
// src/conversation/claude-code-reader.ts
|
|
@@ -1724,15 +2327,15 @@ async function collectClaudeCodeTranscripts(repoRoot) {
|
|
|
1724
2327
|
const files = await listTranscriptFiles(repoRoot);
|
|
1725
2328
|
const turns = [];
|
|
1726
2329
|
for (const file of files) {
|
|
1727
|
-
const raw = await
|
|
2330
|
+
const raw = await readFile4(file, "utf8");
|
|
1728
2331
|
turns.push(...parseClaudeCodeTranscript(raw));
|
|
1729
2332
|
}
|
|
1730
2333
|
return turns;
|
|
1731
2334
|
}
|
|
1732
2335
|
|
|
1733
2336
|
// src/docs/read.ts
|
|
1734
|
-
import { readFile as
|
|
1735
|
-
import { join as
|
|
2337
|
+
import { readFile as readFile5, stat } from "fs/promises";
|
|
2338
|
+
import { join as join6 } from "path";
|
|
1736
2339
|
var DEFAULT_PATHSPECS = ["*.md"];
|
|
1737
2340
|
async function listDocFiles(repoRoot, opts = {}) {
|
|
1738
2341
|
const pathspecs = opts.include ?? DEFAULT_PATHSPECS;
|
|
@@ -1745,11 +2348,11 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
1745
2348
|
const unreadable = [];
|
|
1746
2349
|
for (const relPath of paths) {
|
|
1747
2350
|
const path = relPath.replace(/\\/g, "/");
|
|
1748
|
-
const absPath =
|
|
2351
|
+
const absPath = join6(repoRoot, relPath);
|
|
1749
2352
|
let content;
|
|
1750
2353
|
let mtime;
|
|
1751
2354
|
try {
|
|
1752
|
-
[content, { mtime }] = await Promise.all([
|
|
2355
|
+
[content, { mtime }] = await Promise.all([readFile5(absPath, "utf8"), stat(absPath)]);
|
|
1753
2356
|
} catch {
|
|
1754
2357
|
unreadable.push(path);
|
|
1755
2358
|
continue;
|
|
@@ -1760,11 +2363,11 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
1760
2363
|
}
|
|
1761
2364
|
|
|
1762
2365
|
// src/shell/detect.ts
|
|
1763
|
-
import { existsSync as
|
|
1764
|
-
import { readFile as
|
|
2366
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2367
|
+
import { readFile as readFile7, stat as stat2 } from "fs/promises";
|
|
1765
2368
|
|
|
1766
2369
|
// src/shell/hook-log.ts
|
|
1767
|
-
import { appendFile, mkdir as
|
|
2370
|
+
import { appendFile, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
|
|
1768
2371
|
import { dirname as dirname3 } from "path";
|
|
1769
2372
|
function parseHookLogLine(line) {
|
|
1770
2373
|
const trimmed = line.trim();
|
|
@@ -1789,7 +2392,7 @@ function parseHookLogLine(line) {
|
|
|
1789
2392
|
async function readHookLog(path, fromLine) {
|
|
1790
2393
|
let raw;
|
|
1791
2394
|
try {
|
|
1792
|
-
raw = await
|
|
2395
|
+
raw = await readFile6(path, "utf8");
|
|
1793
2396
|
} catch {
|
|
1794
2397
|
return { entries: [], totalLines: fromLine };
|
|
1795
2398
|
}
|
|
@@ -1922,8 +2525,8 @@ function hookEntryToRaw(e) {
|
|
|
1922
2525
|
};
|
|
1923
2526
|
}
|
|
1924
2527
|
async function tryReadScrapeSource(path, parse, tailLines) {
|
|
1925
|
-
if (!
|
|
1926
|
-
const [raw, stats] = await Promise.all([
|
|
2528
|
+
if (!existsSync4(path)) return null;
|
|
2529
|
+
const [raw, stats] = await Promise.all([readFile7(path, "utf8"), stat2(path)]);
|
|
1927
2530
|
return parse(raw, stats.mtimeMs, { tailLines });
|
|
1928
2531
|
}
|
|
1929
2532
|
async function collectAvailableShellHistory(opts = {}) {
|
|
@@ -1931,7 +2534,7 @@ async function collectAvailableShellHistory(opts = {}) {
|
|
|
1931
2534
|
const tailLines = opts.tailLines ?? 300;
|
|
1932
2535
|
const preferHook = opts.preferHook ?? true;
|
|
1933
2536
|
const hookPath = hookLogPath();
|
|
1934
|
-
const hookExists =
|
|
2537
|
+
const hookExists = existsSync4(hookPath);
|
|
1935
2538
|
if (hookExists) {
|
|
1936
2539
|
const fromLine = Number(opts.hookCursor ?? "0") || 0;
|
|
1937
2540
|
const { entries, totalLines } = await readHookLog(hookPath, fromLine);
|
|
@@ -1987,25 +2590,25 @@ function addStats(into, from) {
|
|
|
1987
2590
|
async function syncGit(store, projectId, opts, repo, config, log) {
|
|
1988
2591
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
1989
2592
|
if (!repo.head) {
|
|
1990
|
-
log(`${
|
|
2593
|
+
log(`${pc4.yellow("git")} skipped -- repository has no commits yet`);
|
|
1991
2594
|
return { totals, seen: 0 };
|
|
1992
2595
|
}
|
|
1993
2596
|
if (!config.sources.git.enabled) {
|
|
1994
|
-
log(`${
|
|
2597
|
+
log(`${pc4.dim("git")} disabled in config`);
|
|
1995
2598
|
return { totals, seen: 0 };
|
|
1996
2599
|
}
|
|
1997
2600
|
let cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, GIT_SOURCE);
|
|
1998
2601
|
if (cursor && !await isAncestor(repo.root, cursor, repo.head)) {
|
|
1999
|
-
log(`${
|
|
2602
|
+
log(`${pc4.yellow("git cursor stale")} ${cursor.slice(0, 7)} is not an ancestor of HEAD \u2014 falling back to a full walk`);
|
|
2000
2603
|
cursor = null;
|
|
2001
2604
|
}
|
|
2002
2605
|
if (cursor === repo.head) {
|
|
2003
|
-
log(`${
|
|
2606
|
+
log(`${pc4.green("git up to date")} at ${repo.head.slice(0, 7)}`);
|
|
2004
2607
|
store.setSyncCursor(projectId, GIT_SOURCE, repo.head);
|
|
2005
2608
|
return { totals, seen: 0 };
|
|
2006
2609
|
}
|
|
2007
2610
|
log(
|
|
2008
|
-
`${
|
|
2611
|
+
`${pc4.dim("git syncing")} ${repo.branch ?? "HEAD"} ${cursor ? `${cursor.slice(0, 7)}..${repo.head.slice(0, 7)}` : "(full history)"}`
|
|
2009
2612
|
);
|
|
2010
2613
|
let batch = [];
|
|
2011
2614
|
let seen = 0;
|
|
@@ -2013,7 +2616,7 @@ async function syncGit(store, projectId, opts, repo, config, log) {
|
|
|
2013
2616
|
if (batch.length === 0) return;
|
|
2014
2617
|
addStats(totals, store.upsertNodes(batch));
|
|
2015
2618
|
batch = [];
|
|
2016
|
-
log(` ${
|
|
2619
|
+
log(` ${pc4.dim(`${seen} commits read, ${totals.inserted} new`)}`);
|
|
2017
2620
|
};
|
|
2018
2621
|
const nodes = collectGitCommits(repo.root, projectId, {
|
|
2019
2622
|
afterCommit: cursor,
|
|
@@ -2031,10 +2634,51 @@ async function syncGit(store, projectId, opts, repo, config, log) {
|
|
|
2031
2634
|
store.setSyncCursor(projectId, GIT_SOURCE, repo.head);
|
|
2032
2635
|
return { totals, seen };
|
|
2033
2636
|
}
|
|
2637
|
+
async function syncDiffs(store, projectId, opts, repo, config, log) {
|
|
2638
|
+
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
2639
|
+
if (!repo.head) return { totals, seen: 0 };
|
|
2640
|
+
if (!config.sources.diff.enabled) {
|
|
2641
|
+
log(`${pc4.dim("diff")} disabled in config`);
|
|
2642
|
+
return { totals, seen: 0 };
|
|
2643
|
+
}
|
|
2644
|
+
let cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, DIFF_SOURCE);
|
|
2645
|
+
if (cursor && !await isAncestor(repo.root, cursor, repo.head)) {
|
|
2646
|
+
log(`${pc4.yellow("diff cursor stale")} ${cursor.slice(0, 7)} is not an ancestor of HEAD \u2014 falling back to a bounded walk`);
|
|
2647
|
+
cursor = null;
|
|
2648
|
+
}
|
|
2649
|
+
if (cursor === repo.head) {
|
|
2650
|
+
store.setSyncCursor(projectId, DIFF_SOURCE, repo.head);
|
|
2651
|
+
return { totals, seen: 0 };
|
|
2652
|
+
}
|
|
2653
|
+
let batch = [];
|
|
2654
|
+
let seen = 0;
|
|
2655
|
+
const flush = () => {
|
|
2656
|
+
if (batch.length === 0) return;
|
|
2657
|
+
addStats(totals, store.upsertNodes(batch));
|
|
2658
|
+
batch = [];
|
|
2659
|
+
};
|
|
2660
|
+
const nodes = collectCommitDiffs(repo.root, projectId, {
|
|
2661
|
+
afterCommit: cursor,
|
|
2662
|
+
since: opts.since ?? config.sources.git.since,
|
|
2663
|
+
maxCount: config.sources.diff.maxCommits,
|
|
2664
|
+
maxFilesPerCommit: config.sources.diff.maxFilesPerCommit,
|
|
2665
|
+
contextLines: config.sources.diff.contextLines,
|
|
2666
|
+
maxBodyChars: config.limits.maxBodyChars
|
|
2667
|
+
});
|
|
2668
|
+
for await (const node of nodes) {
|
|
2669
|
+
batch.push(node);
|
|
2670
|
+
seen += 1;
|
|
2671
|
+
if (batch.length >= BATCH_SIZE) flush();
|
|
2672
|
+
}
|
|
2673
|
+
flush();
|
|
2674
|
+
store.setSyncCursor(projectId, DIFF_SOURCE, repo.head);
|
|
2675
|
+
log(` ${pc4.dim(`${DIFF_SOURCE}: ${seen} file diff(s) read`)}`);
|
|
2676
|
+
return { totals, seen };
|
|
2677
|
+
}
|
|
2034
2678
|
async function syncShell(store, projectId, opts, repoRoot, config, log) {
|
|
2035
2679
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
2036
2680
|
if (!config.sources.shell.enabled) {
|
|
2037
|
-
log(`${
|
|
2681
|
+
log(`${pc4.dim("shell")} disabled in config`);
|
|
2038
2682
|
return { totals, seen: 0 };
|
|
2039
2683
|
}
|
|
2040
2684
|
const results = await collectAvailableShellHistory({
|
|
@@ -2043,7 +2687,7 @@ async function syncShell(store, projectId, opts, repoRoot, config, log) {
|
|
|
2043
2687
|
hookCursor: store.getSyncCursor(projectId, "shell:pwsh-hook")
|
|
2044
2688
|
});
|
|
2045
2689
|
if (results.length === 0) {
|
|
2046
|
-
log(`${
|
|
2690
|
+
log(`${pc4.dim("shell")} no history source found on this machine`);
|
|
2047
2691
|
return { totals, seen: 0 };
|
|
2048
2692
|
}
|
|
2049
2693
|
let seen = 0;
|
|
@@ -2055,7 +2699,7 @@ async function syncShell(store, projectId, opts, repoRoot, config, log) {
|
|
|
2055
2699
|
addStats(totals, store.upsertNodes(nodes));
|
|
2056
2700
|
}
|
|
2057
2701
|
store.setSyncCursor(projectId, sourceKey, result.cursorAfter ?? `scanned:${result.entries.length}`);
|
|
2058
|
-
log(` ${
|
|
2702
|
+
log(` ${pc4.dim(`${sourceKey}: ${nodes.length} entr${nodes.length === 1 ? "y" : "ies"} read`)}`);
|
|
2059
2703
|
}
|
|
2060
2704
|
return { totals, seen };
|
|
2061
2705
|
}
|
|
@@ -2068,20 +2712,20 @@ async function syncConversation(store, projectId, repoRoot, config, log, forceEn
|
|
|
2068
2712
|
}
|
|
2069
2713
|
const turns = await collectClaudeCodeTranscripts(repoRoot);
|
|
2070
2714
|
if (turns.length === 0) {
|
|
2071
|
-
log(`${
|
|
2715
|
+
log(`${pc4.dim("conversation")} no transcripts found`);
|
|
2072
2716
|
return { totals, seen: 0 };
|
|
2073
2717
|
}
|
|
2074
2718
|
const nodes = collectConversationTurns(turns, projectId, { maxBodyChars: config.limits.maxBodyChars });
|
|
2075
2719
|
if (nodes.length > 0) addStats(totals, store.upsertNodes(nodes));
|
|
2076
2720
|
store.setSyncCursor(projectId, CONVERSATION_SOURCE, `scanned:${nodes.length}`);
|
|
2077
|
-
log(` ${
|
|
2721
|
+
log(` ${pc4.dim(`${CONVERSATION_SOURCE}: ${nodes.length} of ${turns.length} exchange(s) kept`)}`);
|
|
2078
2722
|
return { totals, seen: nodes.length };
|
|
2079
2723
|
}
|
|
2080
2724
|
var DOCS_SOURCE = "docs";
|
|
2081
2725
|
async function syncDocs(store, projectId, repoRoot, config, log) {
|
|
2082
2726
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
2083
2727
|
if (!config.sources.docs.enabled) {
|
|
2084
|
-
log(`${
|
|
2728
|
+
log(`${pc4.dim("docs")} disabled in config`);
|
|
2085
2729
|
return { totals, seen: 0 };
|
|
2086
2730
|
}
|
|
2087
2731
|
const { files, unreadable } = await readDocFiles(repoRoot, { include: config.sources.docs.include });
|
|
@@ -2095,11 +2739,11 @@ async function syncDocs(store, projectId, repoRoot, config, log) {
|
|
|
2095
2739
|
);
|
|
2096
2740
|
store.setSyncCursor(projectId, DOCS_SOURCE, `scanned:${nodes.length}`);
|
|
2097
2741
|
if (files.length === 0 && unreadable.length === 0) {
|
|
2098
|
-
log(`${
|
|
2742
|
+
log(`${pc4.dim("docs")} no tracked .md files found`);
|
|
2099
2743
|
} else {
|
|
2100
|
-
const prunedPart = pruned > 0 ? `, ${
|
|
2744
|
+
const prunedPart = pruned > 0 ? `, ${pc4.yellow(`${pruned} stale removed`)}` : "";
|
|
2101
2745
|
const skippedPart = unreadable.length > 0 ? `, ${unreadable.length} unreadable (kept)` : "";
|
|
2102
|
-
log(` ${
|
|
2746
|
+
log(` ${pc4.dim(`${DOCS_SOURCE}: ${nodes.length} section(s) from ${files.length} file(s)`)}${prunedPart}${pc4.dim(skippedPart)}`);
|
|
2103
2747
|
}
|
|
2104
2748
|
return { totals, seen: nodes.length };
|
|
2105
2749
|
}
|
|
@@ -2114,11 +2758,13 @@ async function runSync(opts) {
|
|
|
2114
2758
|
const started = Date.now();
|
|
2115
2759
|
try {
|
|
2116
2760
|
store.upsertProject({ id: projectId, root: repo.root, originUrl: repo.originUrl });
|
|
2761
|
+
await recordProject({ projectId, root: repo.root, dbPath: ws.dbPath, originUrl: repo.originUrl });
|
|
2117
2762
|
if (opts.rebuild) {
|
|
2118
2763
|
const removed = store.clearProject(projectId);
|
|
2119
|
-
log(`${
|
|
2764
|
+
log(`${pc4.dim("rebuild")} dropped ${removed} existing node(s)`);
|
|
2120
2765
|
}
|
|
2121
2766
|
const git2 = await syncGit(store, projectId, opts, repo, config, log);
|
|
2767
|
+
const diffs = await syncDiffs(store, projectId, opts, repo, config, log);
|
|
2122
2768
|
const shell = await syncShell(store, projectId, opts, repo.root, config, log);
|
|
2123
2769
|
const conversation = await syncConversation(store, projectId, repo.root, config, log, opts.conversationOverride);
|
|
2124
2770
|
const docs = await syncDocs(store, projectId, repo.root, config, log);
|
|
@@ -2126,15 +2772,16 @@ async function runSync(opts) {
|
|
|
2126
2772
|
if (!opts.noEmbed) {
|
|
2127
2773
|
const result = await embedPendingNodes(store, new OllamaEmbeddingProvider(), projectId);
|
|
2128
2774
|
if (result.embedded > 0) {
|
|
2129
|
-
embedLine = ` ${
|
|
2775
|
+
embedLine = ` ${pc4.dim(`vector: ${result.embedded} node(s) embedded`)}${result.skipped > 0 ? pc4.dim(`, ${result.skipped} skipped`) : ""}
|
|
2130
2776
|
`;
|
|
2131
2777
|
} else if (result.providerUnavailable) {
|
|
2132
|
-
log(`${
|
|
2778
|
+
log(`${pc4.dim("vector")} embedding provider unavailable (is Ollama running with nomic-embed-text pulled?) -- BM25-only for now`);
|
|
2133
2779
|
}
|
|
2134
2780
|
}
|
|
2135
2781
|
store.markSynced(projectId);
|
|
2136
2782
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
2137
2783
|
addStats(totals, git2.totals);
|
|
2784
|
+
addStats(totals, diffs.totals);
|
|
2138
2785
|
addStats(totals, shell.totals);
|
|
2139
2786
|
addStats(totals, conversation.totals);
|
|
2140
2787
|
addStats(totals, docs.totals);
|
|
@@ -2143,11 +2790,12 @@ async function runSync(opts) {
|
|
|
2143
2790
|
const conversationEnabled = opts.conversationOverride ?? config.sources.conversation.enabled;
|
|
2144
2791
|
const conversationPart = conversationEnabled ? `, ${conversation.seen} conversation exchange(s)` : "";
|
|
2145
2792
|
const docsPart = config.sources.docs.enabled ? `, ${docs.seen} doc section(s)` : "";
|
|
2793
|
+
const diffPart = config.sources.diff.enabled ? `, ${diffs.seen} file diff(s)` : "";
|
|
2146
2794
|
out(
|
|
2147
2795
|
[
|
|
2148
|
-
`${
|
|
2149
|
-
` ${
|
|
2150
|
-
` ${
|
|
2796
|
+
`${pc4.green("synced")} ${git2.seen} commit(s)${diffPart}, ${shell.seen} shell entr${shell.seen === 1 ? "y" : "ies"}${conversationPart}${docsPart} in ${elapsed}s`,
|
|
2797
|
+
` ${pc4.green(`+${totals.inserted} new`)} ${pc4.yellow(`~${totals.updated} updated`)} ${pc4.dim(`=${totals.unchanged} unchanged`)}`,
|
|
2798
|
+
` ${pc4.dim(`${stats.total} node(s) total across ${stats.distinctFiles} file path(s)`)}`,
|
|
2151
2799
|
""
|
|
2152
2800
|
].join("\n") + embedLine
|
|
2153
2801
|
);
|
|
@@ -2164,20 +2812,39 @@ async function searchMemory(input) {
|
|
|
2164
2812
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
2165
2813
|
const budget = input.budget ?? 2e3;
|
|
2166
2814
|
const candidates = input.candidates ?? 30;
|
|
2815
|
+
const queryOpts = {
|
|
2816
|
+
budget,
|
|
2817
|
+
candidates,
|
|
2818
|
+
embeddingProvider: input.noVector ? null : new OllamaEmbeddingProvider()
|
|
2819
|
+
};
|
|
2820
|
+
if (input.allProjects) {
|
|
2821
|
+
const opened = await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath });
|
|
2822
|
+
try {
|
|
2823
|
+
const { bm25Count, vectorCount, hits, packed } = await runCrossProjectQuery(opened.sources, input.query, queryOpts);
|
|
2824
|
+
return {
|
|
2825
|
+
text: renderContextBlock(input.query, packed),
|
|
2826
|
+
matched: hits.length,
|
|
2827
|
+
bm25Matched: bm25Count,
|
|
2828
|
+
vectorMatched: vectorCount,
|
|
2829
|
+
tokensUsed: packed.tokensUsed,
|
|
2830
|
+
tokensBudget: packed.tokensBudget,
|
|
2831
|
+
projectsSearched: opened.sources.map((s) => s.label)
|
|
2832
|
+
};
|
|
2833
|
+
} finally {
|
|
2834
|
+
opened.close();
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2167
2837
|
const store = MemoryStore.open(ws.dbPath);
|
|
2168
2838
|
try {
|
|
2169
|
-
const { bm25Count, vectorCount, hits, packed } = await runHybridQuery(store, projectId, input.query,
|
|
2170
|
-
budget,
|
|
2171
|
-
candidates,
|
|
2172
|
-
embeddingProvider: input.noVector ? null : new OllamaEmbeddingProvider()
|
|
2173
|
-
});
|
|
2839
|
+
const { bm25Count, vectorCount, hits, packed } = await runHybridQuery(store, projectId, input.query, queryOpts);
|
|
2174
2840
|
return {
|
|
2175
2841
|
text: renderContextBlock(input.query, packed),
|
|
2176
2842
|
matched: hits.length,
|
|
2177
2843
|
bm25Matched: bm25Count,
|
|
2178
2844
|
vectorMatched: vectorCount,
|
|
2179
2845
|
tokensUsed: packed.tokensUsed,
|
|
2180
|
-
tokensBudget: packed.tokensBudget
|
|
2846
|
+
tokensBudget: packed.tokensBudget,
|
|
2847
|
+
projectsSearched: [basename2(repo.root) || repo.root]
|
|
2181
2848
|
};
|
|
2182
2849
|
} finally {
|
|
2183
2850
|
store.close();
|
|
@@ -2220,15 +2887,18 @@ function createServer() {
|
|
|
2220
2887
|
"search_memory",
|
|
2221
2888
|
{
|
|
2222
2889
|
title: "Search remembered project history",
|
|
2223
|
-
description: "Search a NexusMem-tracked repository's remembered history: git commits, shell commands, tracked markdown docs, and (if enabled) conversation transcripts. Returns a token-budgeted, ranked context block -- not raw search results.",
|
|
2890
|
+
description: "Search a NexusMem-tracked repository's remembered history: git commits, code diffs (the patch of each changed file), shell commands, tracked markdown docs, and (if enabled) conversation transcripts. Returns a token-budgeted, ranked context block -- not raw search results.",
|
|
2224
2891
|
inputSchema: {
|
|
2225
|
-
projectRoot:
|
|
2226
|
-
query:
|
|
2227
|
-
budget:
|
|
2892
|
+
projectRoot: z3.string().describe("Absolute path to the repository root"),
|
|
2893
|
+
query: z3.string().describe("Free-text question or search terms"),
|
|
2894
|
+
budget: z3.number().int().positive().optional().describe("Max tokens in the returned context block. Default 2000."),
|
|
2895
|
+
allProjects: z3.boolean().optional().describe(
|
|
2896
|
+
"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."
|
|
2897
|
+
)
|
|
2228
2898
|
}
|
|
2229
2899
|
},
|
|
2230
|
-
async ({ projectRoot, query, budget }) => {
|
|
2231
|
-
const result = await searchMemory({ projectRoot, query, budget });
|
|
2900
|
+
async ({ projectRoot, query, budget, allProjects }) => {
|
|
2901
|
+
const result = await searchMemory({ projectRoot, query, budget, allProjects });
|
|
2232
2902
|
return {
|
|
2233
2903
|
content: [{ type: "text", text: result.text }],
|
|
2234
2904
|
structuredContent: {
|
|
@@ -2237,7 +2907,8 @@ function createServer() {
|
|
|
2237
2907
|
bm25Matched: result.bm25Matched,
|
|
2238
2908
|
vectorMatched: result.vectorMatched,
|
|
2239
2909
|
tokensUsed: result.tokensUsed,
|
|
2240
|
-
tokensBudget: result.tokensBudget
|
|
2910
|
+
tokensBudget: result.tokensBudget,
|
|
2911
|
+
projectsSearched: result.projectsSearched
|
|
2241
2912
|
}
|
|
2242
2913
|
};
|
|
2243
2914
|
}
|
|
@@ -2246,9 +2917,9 @@ function createServer() {
|
|
|
2246
2917
|
"sync_project",
|
|
2247
2918
|
{
|
|
2248
2919
|
title: "Sync remembered history",
|
|
2249
|
-
description: "Ingest new git, shell, docs and (if enabled) conversation history for a NexusMem-tracked repository into its local database.",
|
|
2920
|
+
description: "Ingest new git, diff, shell, docs and (if enabled) conversation history for a NexusMem-tracked repository into its local database.",
|
|
2250
2921
|
inputSchema: {
|
|
2251
|
-
projectRoot:
|
|
2922
|
+
projectRoot: z3.string().describe("Absolute path to the repository root")
|
|
2252
2923
|
}
|
|
2253
2924
|
},
|
|
2254
2925
|
async ({ projectRoot }) => {
|
|
@@ -2262,7 +2933,7 @@ function createServer() {
|
|
|
2262
2933
|
title: "Show what is remembered",
|
|
2263
2934
|
description: "Report how many nodes NexusMem currently remembers for a repository, broken down by kind and source.",
|
|
2264
2935
|
inputSchema: {
|
|
2265
|
-
projectRoot:
|
|
2936
|
+
projectRoot: z3.string().describe("Absolute path to the repository root")
|
|
2266
2937
|
}
|
|
2267
2938
|
},
|
|
2268
2939
|
async ({ projectRoot }) => {
|
|
@@ -2282,17 +2953,41 @@ async function runMcpServer() {
|
|
|
2282
2953
|
}
|
|
2283
2954
|
|
|
2284
2955
|
// src/cli/commands/query.ts
|
|
2285
|
-
import
|
|
2956
|
+
import pc5 from "picocolors";
|
|
2286
2957
|
async function runQuery(opts) {
|
|
2287
|
-
const { ws, projectId } = await loadContext(opts.cwd);
|
|
2288
|
-
const
|
|
2958
|
+
const { repo, ws, projectId } = await loadContext(opts.cwd);
|
|
2959
|
+
const opened = opts.allProjects ? await openAllProjectSources({ projectId, root: repo.root, dbPath: ws.dbPath }) : null;
|
|
2960
|
+
let store = null;
|
|
2289
2961
|
try {
|
|
2290
|
-
const
|
|
2962
|
+
const queryOpts = {
|
|
2291
2963
|
budget: opts.budget,
|
|
2292
2964
|
candidates: opts.candidates,
|
|
2293
2965
|
halfLifeDays: opts.halfLifeDays,
|
|
2294
2966
|
embeddingProvider: opts.noVector ? null : new OllamaEmbeddingProvider()
|
|
2295
|
-
}
|
|
2967
|
+
};
|
|
2968
|
+
let result;
|
|
2969
|
+
if (opened) {
|
|
2970
|
+
result = await runCrossProjectQuery(opened.sources, opts.query, queryOpts);
|
|
2971
|
+
} else {
|
|
2972
|
+
store = MemoryStore.open(ws.dbPath);
|
|
2973
|
+
result = await runHybridQuery(store, projectId, opts.query, queryOpts);
|
|
2974
|
+
}
|
|
2975
|
+
const { bm25Count, vectorCount, hits, packed } = result;
|
|
2976
|
+
if (opened && !opts.json) {
|
|
2977
|
+
const searched = opened.sources.map((s) => s.label).join(", ");
|
|
2978
|
+
process.stderr.write(`${pc5.dim("scope ")} ${opened.sources.length} project(s): ${searched}
|
|
2979
|
+
`);
|
|
2980
|
+
for (const { entry } of opened.unreadable) {
|
|
2981
|
+
process.stderr.write(`${pc5.yellow("unreadable")} ${entry.root} -- skipped
|
|
2982
|
+
`);
|
|
2983
|
+
}
|
|
2984
|
+
if (opened.missing.length > 0) {
|
|
2985
|
+
process.stderr.write(
|
|
2986
|
+
`${pc5.dim("skipped")} ${opened.missing.length} registered project(s) whose database is not on disk ${pc5.dim("(nexusmem projects --prune to forget them)")}
|
|
2987
|
+
`
|
|
2988
|
+
);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2296
2991
|
const matched = hits.length;
|
|
2297
2992
|
const rawTokens = hits.reduce((n, h) => n + approxTokens(h.body), 0);
|
|
2298
2993
|
const packerEfficiency = rawTokens > 0 ? 1 - packed.tokensUsed / rawTokens : 0;
|
|
@@ -2317,15 +3012,15 @@ async function runQuery(opts) {
|
|
|
2317
3012
|
return 0;
|
|
2318
3013
|
}
|
|
2319
3014
|
if (matched === 0) {
|
|
2320
|
-
process.stderr.write(`${
|
|
3015
|
+
process.stderr.write(`${pc5.yellow("no matches")} for "${opts.query}"
|
|
2321
3016
|
`);
|
|
2322
3017
|
return 0;
|
|
2323
3018
|
}
|
|
2324
3019
|
process.stderr.write(
|
|
2325
3020
|
[
|
|
2326
|
-
`${
|
|
2327
|
-
`${
|
|
2328
|
-
rawTokens > 0 ? `${
|
|
3021
|
+
`${pc5.dim("matched")} ${bm25Count} bm25${vectorCount > 0 ? ` + ${vectorCount} vector` : ""}, packed ${pc5.bold(String(packed.nodes.length))} into budget`,
|
|
3022
|
+
`${pc5.dim("tokens ")} ${packed.tokensUsed}/${packed.tokensBudget}` + (packed.droppedForBudget ? pc5.dim(` (${packed.droppedForBudget} dropped for budget)`) : ""),
|
|
3023
|
+
rawTokens > 0 ? `${pc5.dim("vs raw ")} ${rawTokens} tokens if these same matches were sent unpacked ${packerEfficiency >= 0 ? pc5.green(`(${(packerEfficiency * 100).toFixed(0)}% packer efficiency)`) : pc5.yellow(`(${(-packerEfficiency * 100).toFixed(0)}% larger -- overhead dominates on small result sets)`)}` : "",
|
|
2329
3024
|
""
|
|
2330
3025
|
].filter(Boolean).join("\n")
|
|
2331
3026
|
);
|
|
@@ -2333,28 +3028,30 @@ async function runQuery(opts) {
|
|
|
2333
3028
|
`);
|
|
2334
3029
|
return 0;
|
|
2335
3030
|
} finally {
|
|
2336
|
-
|
|
3031
|
+
opened?.close();
|
|
3032
|
+
store?.close();
|
|
2337
3033
|
}
|
|
2338
3034
|
}
|
|
2339
3035
|
|
|
2340
3036
|
// src/cli/commands/scan-conversation.ts
|
|
2341
|
-
import
|
|
3037
|
+
import pc7 from "picocolors";
|
|
2342
3038
|
|
|
2343
3039
|
// src/cli/format.ts
|
|
2344
|
-
import
|
|
3040
|
+
import pc6 from "picocolors";
|
|
2345
3041
|
var GIT_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
2346
3042
|
var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
2347
3043
|
var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
2348
3044
|
var DOCS_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
3045
|
+
var DIFF_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
2349
3046
|
function signalBand(signal, bands) {
|
|
2350
3047
|
if (signal >= bands.high) return "high";
|
|
2351
3048
|
if (signal >= bands.medium) return "medium";
|
|
2352
3049
|
return "low";
|
|
2353
3050
|
}
|
|
2354
3051
|
var BAND_COLOR = {
|
|
2355
|
-
high:
|
|
2356
|
-
medium:
|
|
2357
|
-
low:
|
|
3052
|
+
high: pc6.green,
|
|
3053
|
+
medium: pc6.yellow,
|
|
3054
|
+
low: pc6.dim
|
|
2358
3055
|
};
|
|
2359
3056
|
function formatSignal(signal, bands) {
|
|
2360
3057
|
return BAND_COLOR[signalBand(signal, bands)](signal.toFixed(2));
|
|
@@ -2367,9 +3064,9 @@ async function runScanConversation(opts) {
|
|
|
2367
3064
|
const files = await listTranscriptFiles(repo.root);
|
|
2368
3065
|
if (!opts.json) {
|
|
2369
3066
|
process.stderr.write(
|
|
2370
|
-
files.length ? `${
|
|
3067
|
+
files.length ? `${pc7.dim("transcripts")} ${files.length} file(s) in ${claudeProjectTranscriptDir(repo.root)}
|
|
2371
3068
|
|
|
2372
|
-
` : `${
|
|
3069
|
+
` : `${pc7.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
2373
3070
|
`
|
|
2374
3071
|
);
|
|
2375
3072
|
}
|
|
@@ -2386,7 +3083,7 @@ async function runScanConversation(opts) {
|
|
|
2386
3083
|
const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
2387
3084
|
process.stderr.write(
|
|
2388
3085
|
`
|
|
2389
|
-
${
|
|
3086
|
+
${pc7.bold(String(nodes.length))} of ${turns.length} exchange(s) above threshold ${pc7.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}` + (redactedTotal > 0 ? ` ${pc7.yellow(`${redactedTotal} secret-like value(s) redacted`)}` : "") + "\n"
|
|
2390
3087
|
);
|
|
2391
3088
|
return 0;
|
|
2392
3089
|
}
|
|
@@ -2394,44 +3091,8 @@ function formatNode(node) {
|
|
|
2394
3091
|
return [formatSignal(node.signal, CONVERSATION_SIGNAL_BANDS), node.ts.slice(0, 16).replace("T", " "), node.title].join(" ");
|
|
2395
3092
|
}
|
|
2396
3093
|
|
|
2397
|
-
// src/cli/commands/scan-
|
|
2398
|
-
import
|
|
2399
|
-
async function runScanDocs(opts) {
|
|
2400
|
-
const repo = await readRepoInfo(opts.cwd);
|
|
2401
|
-
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
2402
|
-
const { files, unreadable } = await readDocFiles(repo.root);
|
|
2403
|
-
if (!opts.json) {
|
|
2404
|
-
process.stderr.write(
|
|
2405
|
-
files.length ? `${pc7.dim("tracked .md files")} ${files.map((f) => f.path).join(", ")}
|
|
2406
|
-
|
|
2407
|
-
` : `${pc7.yellow("no tracked .md files found")}
|
|
2408
|
-
`
|
|
2409
|
-
);
|
|
2410
|
-
if (unreadable.length > 0) {
|
|
2411
|
-
process.stderr.write(`${pc7.yellow("unreadable")} ${unreadable.join(", ")}
|
|
2412
|
-
|
|
2413
|
-
`);
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
const nodes = collectDocFiles(files, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
2417
|
-
if (opts.json) {
|
|
2418
|
-
process.stdout.write(`${JSON.stringify(nodes, null, 2)}
|
|
2419
|
-
`);
|
|
2420
|
-
return 0;
|
|
2421
|
-
}
|
|
2422
|
-
for (const node of nodes) process.stdout.write(`${formatNode2(node)}
|
|
2423
|
-
`);
|
|
2424
|
-
const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
2425
|
-
process.stderr.write(
|
|
2426
|
-
`
|
|
2427
|
-
${pc7.bold(String(nodes.length))} section(s) from ${files.length} file(s) above threshold ${pc7.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
2428
|
-
`
|
|
2429
|
-
);
|
|
2430
|
-
return 0;
|
|
2431
|
-
}
|
|
2432
|
-
function formatNode2(node) {
|
|
2433
|
-
return [formatSignal(node.signal, DOCS_SIGNAL_BANDS), node.title].join(" ");
|
|
2434
|
-
}
|
|
3094
|
+
// src/cli/commands/scan-diff.ts
|
|
3095
|
+
import pc9 from "picocolors";
|
|
2435
3096
|
|
|
2436
3097
|
// src/cli/commands/scan-git.ts
|
|
2437
3098
|
import pc8 from "picocolors";
|
|
@@ -2458,7 +3119,7 @@ async function runScanGit(opts) {
|
|
|
2458
3119
|
for await (const node of collectGitCommits(repo.root, projectId, collectOpts)) {
|
|
2459
3120
|
if (node.signal < opts.minSignal) continue;
|
|
2460
3121
|
nodes.push(node);
|
|
2461
|
-
if (!opts.json) process.stdout.write(`${
|
|
3122
|
+
if (!opts.json) process.stdout.write(`${formatNode2(node)}
|
|
2462
3123
|
`);
|
|
2463
3124
|
}
|
|
2464
3125
|
if (opts.json) {
|
|
@@ -2471,7 +3132,7 @@ ${summarize2(nodes)}
|
|
|
2471
3132
|
`);
|
|
2472
3133
|
return 0;
|
|
2473
3134
|
}
|
|
2474
|
-
function
|
|
3135
|
+
function formatNode2(node) {
|
|
2475
3136
|
const sha = String(node.meta.shortSha ?? "").padEnd(9);
|
|
2476
3137
|
const date = node.ts.slice(0, 10);
|
|
2477
3138
|
const files = Number(node.meta.filesChanged ?? 0);
|
|
@@ -2502,17 +3163,103 @@ ${hottest.join("\n")}` : ""
|
|
|
2502
3163
|
].filter(Boolean).join("\n");
|
|
2503
3164
|
}
|
|
2504
3165
|
|
|
3166
|
+
// src/cli/commands/scan-diff.ts
|
|
3167
|
+
var DEFAULT_SCAN_COMMITS = 50;
|
|
3168
|
+
async function runScanDiff(opts) {
|
|
3169
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
3170
|
+
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
3171
|
+
if (!opts.json) {
|
|
3172
|
+
process.stderr.write(
|
|
3173
|
+
[
|
|
3174
|
+
`${pc9.dim("repo ")} ${repo.root}`,
|
|
3175
|
+
`${pc9.dim("branch ")} ${repo.branch ?? pc9.yellow("(detached)")}`,
|
|
3176
|
+
`${pc9.dim("project")} ${pc9.cyan(projectId)}`,
|
|
3177
|
+
""
|
|
3178
|
+
].join("\n")
|
|
3179
|
+
);
|
|
3180
|
+
}
|
|
3181
|
+
const nodes = [];
|
|
3182
|
+
for await (const node of collectCommitDiffs(repo.root, projectId, {
|
|
3183
|
+
since: opts.since ?? null,
|
|
3184
|
+
maxCount: opts.limit ?? DEFAULT_SCAN_COMMITS
|
|
3185
|
+
})) {
|
|
3186
|
+
if (node.signal < opts.minSignal) continue;
|
|
3187
|
+
nodes.push(node);
|
|
3188
|
+
if (!opts.json) process.stdout.write(`${formatNode3(node)}
|
|
3189
|
+
`);
|
|
3190
|
+
}
|
|
3191
|
+
if (opts.json) {
|
|
3192
|
+
process.stdout.write(`${JSON.stringify(nodes, null, 2)}
|
|
3193
|
+
`);
|
|
3194
|
+
return 0;
|
|
3195
|
+
}
|
|
3196
|
+
process.stderr.write(`
|
|
3197
|
+
${summarize2(nodes)}
|
|
3198
|
+
`);
|
|
3199
|
+
return 0;
|
|
3200
|
+
}
|
|
3201
|
+
function formatNode3(node) {
|
|
3202
|
+
const sha = String(node.meta.shortSha ?? "").padEnd(9);
|
|
3203
|
+
const churn = `+${node.meta.insertions ?? 0}/-${node.meta.deletions ?? 0}`;
|
|
3204
|
+
return [
|
|
3205
|
+
formatSignal(node.signal, DIFF_SIGNAL_BANDS),
|
|
3206
|
+
pc9.dim(node.ts.slice(0, 10)),
|
|
3207
|
+
pc9.magenta(sha),
|
|
3208
|
+
String(node.meta.path ?? ""),
|
|
3209
|
+
pc9.dim(`(${churn}, ${node.meta.hunkCount ?? 0} hunk(s))`)
|
|
3210
|
+
].join(" ");
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// src/cli/commands/scan-docs.ts
|
|
3214
|
+
import pc10 from "picocolors";
|
|
3215
|
+
async function runScanDocs(opts) {
|
|
3216
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
3217
|
+
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
3218
|
+
const { files, unreadable } = await readDocFiles(repo.root);
|
|
3219
|
+
if (!opts.json) {
|
|
3220
|
+
process.stderr.write(
|
|
3221
|
+
files.length ? `${pc10.dim("tracked .md files")} ${files.map((f) => f.path).join(", ")}
|
|
3222
|
+
|
|
3223
|
+
` : `${pc10.yellow("no tracked .md files found")}
|
|
3224
|
+
`
|
|
3225
|
+
);
|
|
3226
|
+
if (unreadable.length > 0) {
|
|
3227
|
+
process.stderr.write(`${pc10.yellow("unreadable")} ${unreadable.join(", ")}
|
|
3228
|
+
|
|
3229
|
+
`);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
const nodes = collectDocFiles(files, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
3233
|
+
if (opts.json) {
|
|
3234
|
+
process.stdout.write(`${JSON.stringify(nodes, null, 2)}
|
|
3235
|
+
`);
|
|
3236
|
+
return 0;
|
|
3237
|
+
}
|
|
3238
|
+
for (const node of nodes) process.stdout.write(`${formatNode4(node)}
|
|
3239
|
+
`);
|
|
3240
|
+
const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
3241
|
+
process.stderr.write(
|
|
3242
|
+
`
|
|
3243
|
+
${pc10.bold(String(nodes.length))} section(s) from ${files.length} file(s) above threshold ${pc10.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
3244
|
+
`
|
|
3245
|
+
);
|
|
3246
|
+
return 0;
|
|
3247
|
+
}
|
|
3248
|
+
function formatNode4(node) {
|
|
3249
|
+
return [formatSignal(node.signal, DOCS_SIGNAL_BANDS), node.title].join(" ");
|
|
3250
|
+
}
|
|
3251
|
+
|
|
2505
3252
|
// src/cli/commands/scan-shell.ts
|
|
2506
|
-
import
|
|
3253
|
+
import pc11 from "picocolors";
|
|
2507
3254
|
async function runScanShell(opts) {
|
|
2508
3255
|
const repo = await readRepoInfo(opts.cwd);
|
|
2509
3256
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
2510
3257
|
const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
|
|
2511
3258
|
if (!opts.json) {
|
|
2512
3259
|
process.stderr.write(
|
|
2513
|
-
results.length ? `${
|
|
3260
|
+
results.length ? `${pc11.dim("sources found")} ${results.map((r) => r.name).join(", ")}
|
|
2514
3261
|
|
|
2515
|
-
` : `${
|
|
3262
|
+
` : `${pc11.yellow("no shell history source found on this machine")}
|
|
2516
3263
|
`
|
|
2517
3264
|
);
|
|
2518
3265
|
}
|
|
@@ -2521,9 +3268,9 @@ async function runScanShell(opts) {
|
|
|
2521
3268
|
const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
2522
3269
|
allNodes.push(...nodes);
|
|
2523
3270
|
if (!opts.json) {
|
|
2524
|
-
process.stdout.write(`${
|
|
3271
|
+
process.stdout.write(`${pc11.bold(`shell:${result.name}`)} ${pc11.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
|
|
2525
3272
|
`);
|
|
2526
|
-
for (const node of nodes) process.stdout.write(`${
|
|
3273
|
+
for (const node of nodes) process.stdout.write(`${formatNode5(node)}
|
|
2527
3274
|
`);
|
|
2528
3275
|
process.stdout.write("\n");
|
|
2529
3276
|
}
|
|
@@ -2534,20 +3281,20 @@ async function runScanShell(opts) {
|
|
|
2534
3281
|
return 0;
|
|
2535
3282
|
}
|
|
2536
3283
|
const approxTotal = allNodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
2537
|
-
process.stderr.write(`${
|
|
3284
|
+
process.stderr.write(`${pc11.bold(String(allNodes.length))} node(s) total ${pc11.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
2538
3285
|
`);
|
|
2539
3286
|
return 0;
|
|
2540
3287
|
}
|
|
2541
|
-
function
|
|
2542
|
-
const approx = node.meta.tsApprox ?
|
|
3288
|
+
function formatNode5(node) {
|
|
3289
|
+
const approx = node.meta.tsApprox ? pc11.dim("~") : " ";
|
|
2543
3290
|
const exit = node.meta.exitCode;
|
|
2544
|
-
const exitLabel = typeof exit === "number" && exit !== 0 ?
|
|
3291
|
+
const exitLabel = typeof exit === "number" && exit !== 0 ? pc11.red(`exit ${exit}`) : "";
|
|
2545
3292
|
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
|
|
2546
3293
|
}
|
|
2547
3294
|
|
|
2548
3295
|
// src/cli/commands/status.ts
|
|
2549
3296
|
import { statSync } from "fs";
|
|
2550
|
-
import
|
|
3297
|
+
import pc12 from "picocolors";
|
|
2551
3298
|
function humanBytes(bytes) {
|
|
2552
3299
|
if (bytes < 1024) return `${bytes} B`;
|
|
2553
3300
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -2572,23 +3319,23 @@ async function runStatus(opts) {
|
|
|
2572
3319
|
const kinds = Object.entries(stats.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
2573
3320
|
process.stdout.write(
|
|
2574
3321
|
[
|
|
2575
|
-
`${
|
|
2576
|
-
`${
|
|
2577
|
-
`${
|
|
2578
|
-
`${
|
|
2579
|
-
`${
|
|
3322
|
+
`${pc12.dim("repo ")} ${repo.root}`,
|
|
3323
|
+
`${pc12.dim("branch ")} ${repo.branch ?? pc12.yellow("(detached)")}`,
|
|
3324
|
+
`${pc12.dim("project ")} ${pc12.cyan(projectId)}`,
|
|
3325
|
+
`${pc12.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc12.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
|
|
3326
|
+
`${pc12.dim("database")} ${ws.dbPath} ${pc12.dim(`(${humanBytes(dbBytes)})`)}`,
|
|
2580
3327
|
"",
|
|
2581
|
-
`${
|
|
3328
|
+
`${pc12.bold(String(stats.total))} node(s)${stats.total ? ` ${pc12.dim(`${stats.oldest?.slice(0, 10)} .. ${stats.newest?.slice(0, 10)}`)}` : ""}`,
|
|
2582
3329
|
...kinds,
|
|
2583
|
-
stats.total ? ` ${
|
|
3330
|
+
stats.total ? ` ${pc12.dim(`${stats.distinctFiles} distinct file path(s)`)}` : "",
|
|
2584
3331
|
"",
|
|
2585
|
-
sources.length ?
|
|
3332
|
+
sources.length ? pc12.dim("sources") : pc12.yellow("no sources synced yet"),
|
|
2586
3333
|
...sources.map((s) => {
|
|
2587
3334
|
const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
2588
3335
|
const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
|
|
2589
|
-
return ` ${s.source.padEnd(14)} ${
|
|
3336
|
+
return ` ${s.source.padEnd(14)} ${pc12.dim(`last run ${when}`)} ${pc12.dim(`cursor ${cursorLabel}`)}`;
|
|
2590
3337
|
}),
|
|
2591
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
3338
|
+
gitCursor && gitCursor !== repo.head ? `${pc12.yellow("git behind HEAD")} \u2014 run ${pc12.bold("nexusmem sync")}` : "",
|
|
2592
3339
|
""
|
|
2593
3340
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
2594
3341
|
);
|
|
@@ -2612,7 +3359,7 @@ function guard(run) {
|
|
|
2612
3359
|
process.exitCode = await run();
|
|
2613
3360
|
} catch (err) {
|
|
2614
3361
|
if (isExpected(err)) {
|
|
2615
|
-
process.stderr.write(`${
|
|
3362
|
+
process.stderr.write(`${pc13.red("error")} ${err.message}
|
|
2616
3363
|
`);
|
|
2617
3364
|
process.exitCode = 1;
|
|
2618
3365
|
return;
|
|
@@ -2650,7 +3397,7 @@ program.command("hook").description("Manage the opt-in PowerShell hook that logs
|
|
|
2650
3397
|
new Command("status").description("Show whether the hook is installed").option("--profile <path>", "override the auto-detected $PROFILE path").action((options) => guard(() => runHookStatus({ profile: options.profile }))())
|
|
2651
3398
|
);
|
|
2652
3399
|
program.command("status").description("Show what is currently remembered for this repository").option("-C, --cwd <path>", "repository path", process.cwd()).action((options) => guard(() => runStatus({ cwd: options.cwd }))());
|
|
2653
|
-
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("--json", "emit the packed result as JSON on stdout", false).action(
|
|
3400
|
+
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(
|
|
2654
3401
|
(text, options) => guard(
|
|
2655
3402
|
() => runQuery({
|
|
2656
3403
|
cwd: options.cwd,
|
|
@@ -2659,10 +3406,12 @@ program.command("query").description("Search remembered history and print a toke
|
|
|
2659
3406
|
candidates: options.candidates,
|
|
2660
3407
|
halfLifeDays: options.halfLife,
|
|
2661
3408
|
noVector: !options.vector,
|
|
3409
|
+
allProjects: options.allProjects,
|
|
2662
3410
|
json: options.json
|
|
2663
3411
|
})
|
|
2664
3412
|
)()
|
|
2665
3413
|
);
|
|
3414
|
+
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 }))());
|
|
2666
3415
|
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(
|
|
2667
3416
|
(options) => guard(
|
|
2668
3417
|
() => runScanGit({
|
|
@@ -2675,6 +3424,17 @@ program.command("scan-git").description("Preview the MemoryNodes git history wou
|
|
|
2675
3424
|
})
|
|
2676
3425
|
)()
|
|
2677
3426
|
);
|
|
3427
|
+
program.command("scan-diff").description("Preview the MemoryNodes commit patches would produce, one per changed file (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 (not N nodes)", (v) => Number.parseInt(v, 10)).option("--min-signal <score>", "drop nodes below this signal", (v) => Number.parseFloat(v), 0).option("--json", "emit MemoryNodes as JSON on stdout", false).action(
|
|
3428
|
+
(options) => guard(
|
|
3429
|
+
() => runScanDiff({
|
|
3430
|
+
cwd: options.cwd,
|
|
3431
|
+
since: options.since,
|
|
3432
|
+
limit: options.limit,
|
|
3433
|
+
json: options.json,
|
|
3434
|
+
minSignal: options.minSignal
|
|
3435
|
+
})
|
|
3436
|
+
)()
|
|
3437
|
+
);
|
|
2678
3438
|
program.command("scan-shell").description("Preview the MemoryNodes shell history would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("-n, --tail-lines <count>", "lines kept from each scrape-based source", (v) => Number.parseInt(v, 10), 300).option("--min-signal <score>", "drop nodes below this signal", (v) => Number.parseFloat(v), 0).option("--json", "emit MemoryNodes as JSON on stdout", false).action(
|
|
2679
3439
|
(options) => guard(
|
|
2680
3440
|
() => runScanShell({ cwd: options.cwd, tailLines: options.tailLines, minSignal: options.minSignal, json: options.json })
|
|
@@ -2687,7 +3447,7 @@ program.command("scan-docs").description("Preview the MemoryNodes tracked .md fi
|
|
|
2687
3447
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
2688
3448
|
program.parseAsync(process.argv).catch((err) => {
|
|
2689
3449
|
const message = err instanceof Error ? err.message : String(err);
|
|
2690
|
-
process.stderr.write(`${
|
|
3450
|
+
process.stderr.write(`${pc13.red("error")} ${message}
|
|
2691
3451
|
`);
|
|
2692
3452
|
process.exitCode = 1;
|
|
2693
3453
|
});
|