nexusmem 0.1.1 → 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 +999 -231
- 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),
|
|
@@ -103,6 +119,14 @@ async function writeWorkspaceGitignore(ws) {
|
|
|
103
119
|
await writeFile(join(ws.dir, ".gitignore"), "# Machine-local derived data.\n*\n", "utf8");
|
|
104
120
|
}
|
|
105
121
|
|
|
122
|
+
// src/core/version.ts
|
|
123
|
+
import { readFileSync } from "fs";
|
|
124
|
+
import { fileURLToPath } from "url";
|
|
125
|
+
function readOwnVersion() {
|
|
126
|
+
const pkgPath = fileURLToPath(new URL("../../package.json", import.meta.url));
|
|
127
|
+
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
128
|
+
}
|
|
129
|
+
|
|
106
130
|
// src/git/exec.ts
|
|
107
131
|
import { spawn } from "child_process";
|
|
108
132
|
var GitError = class extends Error {
|
|
@@ -306,25 +330,31 @@ import { dirname } from "path";
|
|
|
306
330
|
|
|
307
331
|
// src/shell/paths.ts
|
|
308
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
|
|
309
338
|
import { homedir } from "os";
|
|
310
339
|
import { join as join2 } from "path";
|
|
311
|
-
|
|
340
|
+
function globalWorkspaceDir() {
|
|
341
|
+
return process.env.NEXUSMEM_HOME ?? join2(homedir(), ".nexusmem");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/shell/paths.ts
|
|
312
345
|
var execFileAsync = promisify(execFile);
|
|
313
346
|
function psReadLineHistoryPath() {
|
|
314
|
-
const appData = process.env.APPDATA ??
|
|
315
|
-
return
|
|
347
|
+
const appData = process.env.APPDATA ?? join3(homedir2(), "AppData", "Roaming");
|
|
348
|
+
return join3(appData, "Microsoft", "Windows", "PowerShell", "PSReadLine", "ConsoleHost_history.txt");
|
|
316
349
|
}
|
|
317
350
|
function bashHistoryPath() {
|
|
318
|
-
return process.env.HISTFILE_BASH ??
|
|
351
|
+
return process.env.HISTFILE_BASH ?? join3(homedir2(), ".bash_history");
|
|
319
352
|
}
|
|
320
353
|
function zshHistoryPath() {
|
|
321
|
-
return process.env.HISTFILE ??
|
|
322
|
-
}
|
|
323
|
-
function globalWorkspaceDir() {
|
|
324
|
-
return join2(homedir(), ".nexusmem");
|
|
354
|
+
return process.env.HISTFILE ?? join3(homedir2(), ".zsh_history");
|
|
325
355
|
}
|
|
326
356
|
function hookLogPath() {
|
|
327
|
-
return
|
|
357
|
+
return join3(globalWorkspaceDir(), "shell-history.jsonl");
|
|
328
358
|
}
|
|
329
359
|
async function resolvePowerShellProfilePath(exe = "powershell") {
|
|
330
360
|
try {
|
|
@@ -478,6 +508,74 @@ async function runHookStatus(opts) {
|
|
|
478
508
|
import { relative } from "path";
|
|
479
509
|
import pc2 from "picocolors";
|
|
480
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
|
+
|
|
481
579
|
// src/core/ids.ts
|
|
482
580
|
import { createHash } from "crypto";
|
|
483
581
|
var KEY_SEP = "\0";
|
|
@@ -897,6 +995,7 @@ async function runInit(opts) {
|
|
|
897
995
|
} finally {
|
|
898
996
|
store.close();
|
|
899
997
|
}
|
|
998
|
+
await recordProject({ projectId, root: repo.root, dbPath: ws.dbPath, originUrl: repo.originUrl });
|
|
900
999
|
const lines = [
|
|
901
1000
|
`${pc2.green("initialized")} ${ws.dir}`,
|
|
902
1001
|
` project ${pc2.cyan(projectId)}`,
|
|
@@ -931,10 +1030,68 @@ async function runInit(opts) {
|
|
|
931
1030
|
return 0;
|
|
932
1031
|
}
|
|
933
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
|
+
|
|
934
1088
|
// src/mcp/server.ts
|
|
935
1089
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
936
1090
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
937
|
-
import { z as
|
|
1091
|
+
import { z as z3 } from "zod";
|
|
1092
|
+
|
|
1093
|
+
// src/mcp/tools.ts
|
|
1094
|
+
import { basename as basename2 } from "path";
|
|
938
1095
|
|
|
939
1096
|
// src/core/text.ts
|
|
940
1097
|
function truncate(s, max) {
|
|
@@ -948,22 +1105,126 @@ function approxTokens(text) {
|
|
|
948
1105
|
var DEFAULT_SUMMARY_CHARS = 320;
|
|
949
1106
|
var NODE_OVERHEAD_TOKENS = 8;
|
|
950
1107
|
var CONVERSATION_ANSWER_MARKER = "\n\nA: ";
|
|
951
|
-
|
|
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) {
|
|
952
1203
|
const answerIdx = hit.body.indexOf(CONVERSATION_ANSWER_MARKER);
|
|
953
1204
|
if (answerIdx !== -1) {
|
|
954
1205
|
const answer = hit.body.slice(answerIdx + CONVERSATION_ANSWER_MARKER.length).trim();
|
|
955
1206
|
if (answer) return truncate(answer, maxChars);
|
|
956
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
|
+
}
|
|
957
1217
|
const rest = hit.body.startsWith(hit.title) ? hit.body.slice(hit.title.length).trim() : hit.body;
|
|
958
1218
|
return truncate(rest || hit.title, maxChars);
|
|
959
1219
|
}
|
|
960
1220
|
function packContext(ranked, tokensBudget, opts = {}) {
|
|
961
1221
|
const summaryChars = opts.summaryChars ?? DEFAULT_SUMMARY_CHARS;
|
|
1222
|
+
const query = opts.query ?? "";
|
|
962
1223
|
const nodes = [];
|
|
963
1224
|
let tokensUsed = 0;
|
|
964
1225
|
let droppedForBudget = 0;
|
|
965
1226
|
for (const hit of ranked) {
|
|
966
|
-
const summary = summarize(hit, summaryChars);
|
|
1227
|
+
const summary = summarize(hit, summaryChars, query);
|
|
967
1228
|
const tokens = approxTokens(hit.title) + approxTokens(summary) + NODE_OVERHEAD_TOKENS;
|
|
968
1229
|
if (tokensUsed + tokens > tokensBudget) {
|
|
969
1230
|
droppedForBudget += 1;
|
|
@@ -977,7 +1238,8 @@ function packContext(ranked, tokensBudget, opts = {}) {
|
|
|
977
1238
|
signal: hit.signal,
|
|
978
1239
|
score: hit.score,
|
|
979
1240
|
summary,
|
|
980
|
-
tokens
|
|
1241
|
+
tokens,
|
|
1242
|
+
...hit.project ? { project: hit.project } : {}
|
|
981
1243
|
});
|
|
982
1244
|
tokensUsed += tokens;
|
|
983
1245
|
}
|
|
@@ -987,9 +1249,14 @@ function renderContextBlock(query, result) {
|
|
|
987
1249
|
if (result.nodes.length === 0) return `No remembered context matched "${query}".`;
|
|
988
1250
|
const lines = [`Relevant history for: ${query}`, ""];
|
|
989
1251
|
for (const node of result.nodes) {
|
|
990
|
-
|
|
1252
|
+
const project = node.project ? `[${node.project}] ` : "";
|
|
1253
|
+
lines.push(`- ${node.ts.slice(0, 10)} ${project}${node.title}`);
|
|
991
1254
|
if (node.summary && node.summary !== node.title) {
|
|
992
|
-
|
|
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
|
+
}
|
|
993
1260
|
}
|
|
994
1261
|
}
|
|
995
1262
|
return lines.join("\n");
|
|
@@ -1068,6 +1335,31 @@ function rankHits(hits, opts = {}) {
|
|
|
1068
1335
|
}
|
|
1069
1336
|
|
|
1070
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
|
+
}
|
|
1071
1363
|
async function runHybridQuery(store, projectId, query, opts) {
|
|
1072
1364
|
const bm25Hits = store.search(projectId, query, opts.candidates);
|
|
1073
1365
|
let vectorHits = [];
|
|
@@ -1078,10 +1370,55 @@ async function runHybridQuery(store, projectId, query, opts) {
|
|
|
1078
1370
|
const hits = vectorHits.length > 0 ? mergeSearchAndVectorHits(bm25Hits, vectorHits) : bm25Hits;
|
|
1079
1371
|
const relevanceScores = vectorHits.length > 0 ? reciprocalRankFusion([bm25Hits, vectorHits]) : void 0;
|
|
1080
1372
|
const ranked = rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores });
|
|
1081
|
-
const packed = packContext(ranked, opts.budget);
|
|
1373
|
+
const packed = packContext(ranked, opts.budget, { query });
|
|
1082
1374
|
return { bm25Count: bm25Hits.length, vectorCount: vectorHits.length, hits, packed };
|
|
1083
1375
|
}
|
|
1084
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
|
+
|
|
1085
1422
|
// src/vector/embed.ts
|
|
1086
1423
|
var DEFAULT_BASE_URL = "http://127.0.0.1:11434";
|
|
1087
1424
|
var DEFAULT_MODEL = "nomic-embed-text";
|
|
@@ -1121,7 +1458,7 @@ var OllamaEmbeddingProvider = class {
|
|
|
1121
1458
|
};
|
|
1122
1459
|
|
|
1123
1460
|
// src/cli/commands/sync.ts
|
|
1124
|
-
import
|
|
1461
|
+
import pc4 from "picocolors";
|
|
1125
1462
|
|
|
1126
1463
|
// src/conversation/chunk.ts
|
|
1127
1464
|
var HEADING_LINE = /^#{1,6}\s+(.+)$/;
|
|
@@ -1161,21 +1498,31 @@ function chunkAssistantText(text, maxChars) {
|
|
|
1161
1498
|
|
|
1162
1499
|
// src/conversation/redact.ts
|
|
1163
1500
|
var RULES = [
|
|
1164
|
-
{
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
+
},
|
|
1169
1514
|
// key/token/secret/password = "value" or : value, in code, JSON, env-file or prose form.
|
|
1170
1515
|
{
|
|
1171
1516
|
name: "key-value-secret",
|
|
1172
|
-
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
|
|
1173
1519
|
}
|
|
1174
1520
|
];
|
|
1175
|
-
function redact(text) {
|
|
1521
|
+
function redact(text, profile = "all") {
|
|
1176
1522
|
let redactedCount = 0;
|
|
1177
1523
|
let out = text;
|
|
1178
1524
|
for (const rule of RULES) {
|
|
1525
|
+
if (profile === "high-confidence" && !rule.highConfidence) continue;
|
|
1179
1526
|
out = out.replace(rule.pattern, (_match, ...rest) => {
|
|
1180
1527
|
redactedCount += 1;
|
|
1181
1528
|
const key = typeof rest[0] === "string" ? rest[0] : null;
|
|
@@ -1190,8 +1537,8 @@ var DEFAULT_MAX_BODY_CHARS = 2500;
|
|
|
1190
1537
|
var DEFAULT_MAX_CHUNK_CHARS = 900;
|
|
1191
1538
|
var MAX_TITLE_CHARS = 200;
|
|
1192
1539
|
var MAX_FILES_PER_NODE = 20;
|
|
1193
|
-
var EXPLANATION_MARKERS = /\b(because|the reason|design decision|trade-?off|instead of|rationale|so that)\b
|
|
1194
|
-
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;
|
|
1195
1542
|
function scoreConversationTurn(userText, replyText) {
|
|
1196
1543
|
const text = `${userText}
|
|
1197
1544
|
${replyText}`;
|
|
@@ -1265,63 +1612,6 @@ function collectConversationTurns(turns, projectId, opts = {}) {
|
|
|
1265
1612
|
return turns.filter((t) => t.assistantText.length > 0).flatMap((turn) => toMemoryNodes(turn, projectId, opts));
|
|
1266
1613
|
}
|
|
1267
1614
|
|
|
1268
|
-
// src/collectors/docs.ts
|
|
1269
|
-
var DEFAULT_MAX_BODY_CHARS2 = 2e3;
|
|
1270
|
-
var DEFAULT_MAX_CHUNK_CHARS2 = 1200;
|
|
1271
|
-
var MAX_TITLE_CHARS2 = 200;
|
|
1272
|
-
var EXPLANATION_MARKERS2 = /\b(because|the reason|design decision|trade-?off|instead of|rationale|why)\b/i;
|
|
1273
|
-
function scoreDocSection(path, heading, text) {
|
|
1274
|
-
let score = 0.45;
|
|
1275
|
-
if (EXPLANATION_MARKERS2.test(text)) score += 0.25;
|
|
1276
|
-
if (/(^|\/)readme\.md$/i.test(path)) score += 0.1;
|
|
1277
|
-
if (heading === null) score -= 0.1;
|
|
1278
|
-
if (text.length < 80) score -= 0.15;
|
|
1279
|
-
return Number(Math.min(1, Math.max(0.05, score)).toFixed(3));
|
|
1280
|
-
}
|
|
1281
|
-
function slugify(heading, index) {
|
|
1282
|
-
if (heading === null) return `_preamble-${index}`;
|
|
1283
|
-
const slug = heading.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1284
|
-
return slug || `_section-${index}`;
|
|
1285
|
-
}
|
|
1286
|
-
function sectionTitle(path, heading, index, count) {
|
|
1287
|
-
if (heading) return truncate(`${path} \u2014 ${heading}`, MAX_TITLE_CHARS2);
|
|
1288
|
-
if (count > 1) return truncate(`${path} (part ${index + 1}/${count})`, MAX_TITLE_CHARS2);
|
|
1289
|
-
return truncate(path, MAX_TITLE_CHARS2);
|
|
1290
|
-
}
|
|
1291
|
-
function toMemoryNodes2(file, projectId, opts = {}) {
|
|
1292
|
-
const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS2;
|
|
1293
|
-
const maxChunk = opts.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS2;
|
|
1294
|
-
const chunks = chunkAssistantText(file.content, maxChunk);
|
|
1295
|
-
if (chunks.length === 0) return [];
|
|
1296
|
-
const seenSlugs = /* @__PURE__ */ new Map();
|
|
1297
|
-
return chunks.map((chunk, index) => {
|
|
1298
|
-
const baseSlug = slugify(chunk.heading, index);
|
|
1299
|
-
const occurrence = seenSlugs.get(baseSlug) ?? 0;
|
|
1300
|
-
seenSlugs.set(baseSlug, occurrence + 1);
|
|
1301
|
-
const naturalKey = occurrence === 0 ? `${file.path}#${baseSlug}` : `${file.path}#${baseSlug}:${occurrence}`;
|
|
1302
|
-
return {
|
|
1303
|
-
id: makeNodeId(projectId, "doc_section", naturalKey),
|
|
1304
|
-
kind: "doc_section",
|
|
1305
|
-
projectId,
|
|
1306
|
-
ts: file.ts,
|
|
1307
|
-
source: "docs",
|
|
1308
|
-
title: sectionTitle(file.path, chunk.heading, index, chunks.length),
|
|
1309
|
-
body: truncate(chunk.text, maxBody),
|
|
1310
|
-
files: [{ path: file.path, insertions: null, deletions: null, binary: false }],
|
|
1311
|
-
signal: scoreDocSection(file.path, chunk.heading, chunk.text),
|
|
1312
|
-
meta: {
|
|
1313
|
-
path: file.path,
|
|
1314
|
-
heading: chunk.heading,
|
|
1315
|
-
chunkIndex: index,
|
|
1316
|
-
chunkCount: chunks.length
|
|
1317
|
-
}
|
|
1318
|
-
};
|
|
1319
|
-
});
|
|
1320
|
-
}
|
|
1321
|
-
function collectDocFiles(files, projectId, opts = {}) {
|
|
1322
|
-
return files.flatMap((file) => toMemoryNodes2(file, projectId, opts));
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
1615
|
// src/git/parse.ts
|
|
1326
1616
|
var RECORD_SEP = "";
|
|
1327
1617
|
var UNIT_SEP = "";
|
|
@@ -1431,8 +1721,182 @@ function unquoteGitPath(p) {
|
|
|
1431
1721
|
return Buffer.from(bytes).toString("utf8");
|
|
1432
1722
|
}
|
|
1433
1723
|
|
|
1434
|
-
// 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;
|
|
1435
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;
|
|
1436
1900
|
function buildLogArgs(opts = {}) {
|
|
1437
1901
|
const { rev = "HEAD", afterCommit, since, maxCount, includeMerges = true, paths } = opts;
|
|
1438
1902
|
const args = ["log", `--format=${GIT_LOG_FORMAT}`, "--numstat", "--no-color"];
|
|
@@ -1457,7 +1921,7 @@ async function* readCommits(cwd, opts = {}) {
|
|
|
1457
1921
|
}
|
|
1458
1922
|
}
|
|
1459
1923
|
} catch (err) {
|
|
1460
|
-
if (err instanceof GitError &&
|
|
1924
|
+
if (err instanceof GitError && EMPTY_HISTORY2.test(err.stderr)) return;
|
|
1461
1925
|
throw err;
|
|
1462
1926
|
}
|
|
1463
1927
|
for (const record of splitRecords(buffer, true).records) {
|
|
@@ -1468,7 +1932,7 @@ async function* readCommits(cwd, opts = {}) {
|
|
|
1468
1932
|
|
|
1469
1933
|
// src/collectors/git-commits.ts
|
|
1470
1934
|
var DEFAULTS = { maxFilesPerNode: 40, maxBodyChars: 4e3 };
|
|
1471
|
-
var
|
|
1935
|
+
var MAX_TITLE_CHARS2 = 200;
|
|
1472
1936
|
var CONVENTIONAL = /^([a-z]+)(?:\(([^)]*)\))?(!)?:\s*(.+)$/i;
|
|
1473
1937
|
function parseConventionalHeader(subject) {
|
|
1474
1938
|
const m = CONVENTIONAL.exec(subject.trim());
|
|
@@ -1537,7 +2001,7 @@ function toMemoryNode(commit, projectId, opts = {}) {
|
|
|
1537
2001
|
projectId,
|
|
1538
2002
|
ts: commit.authoredAt,
|
|
1539
2003
|
source: "git",
|
|
1540
|
-
title: truncate(commit.subject || `(no subject) ${commit.shortSha}`,
|
|
2004
|
+
title: truncate(commit.subject || `(no subject) ${commit.shortSha}`, MAX_TITLE_CHARS2),
|
|
1541
2005
|
body: truncate(bodyParts.join("\n"), maxBody),
|
|
1542
2006
|
files: keptFiles,
|
|
1543
2007
|
signal: scoreCommit(commit),
|
|
@@ -1564,9 +2028,156 @@ async function* collectGitCommits(cwd, projectId, opts = {}) {
|
|
|
1564
2028
|
}
|
|
1565
2029
|
}
|
|
1566
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
|
+
|
|
1567
2178
|
// src/collectors/shell-history.ts
|
|
1568
2179
|
var DEFAULT_MAX_BODY_CHARS3 = 1e3;
|
|
1569
|
-
var
|
|
2180
|
+
var MAX_TITLE_CHARS5 = 200;
|
|
1570
2181
|
var NOISE = /^(cd|ls|dir|pwd|clear|cls|exit|history|whoami|date|type|cat|more|less|ll|la)\b/i;
|
|
1571
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;
|
|
1572
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;
|
|
@@ -1607,7 +2218,7 @@ function toMemoryNode2(entry, projectId, opts = {}) {
|
|
|
1607
2218
|
projectId,
|
|
1608
2219
|
ts: entry.ts,
|
|
1609
2220
|
source: `shell:${entry.shell}`,
|
|
1610
|
-
title: truncate(titleLine,
|
|
2221
|
+
title: truncate(titleLine, MAX_TITLE_CHARS5),
|
|
1611
2222
|
body: renderBody(entry, maxBody),
|
|
1612
2223
|
files: [],
|
|
1613
2224
|
signal: scoreShellCommand(entry),
|
|
@@ -1626,24 +2237,24 @@ function collectShellHistory(entries, projectId, opts = {}) {
|
|
|
1626
2237
|
}
|
|
1627
2238
|
|
|
1628
2239
|
// src/conversation/claude-code-reader.ts
|
|
1629
|
-
import { readFile as
|
|
2240
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1630
2241
|
|
|
1631
2242
|
// src/conversation/paths.ts
|
|
1632
|
-
import { existsSync as
|
|
2243
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1633
2244
|
import { readdir } from "fs/promises";
|
|
1634
|
-
import { homedir as
|
|
1635
|
-
import { join as
|
|
2245
|
+
import { homedir as homedir3 } from "os";
|
|
2246
|
+
import { join as join5 } from "path";
|
|
1636
2247
|
function claudeProjectSlug(repoRoot) {
|
|
1637
2248
|
return repoRoot.replace(/[\\/:]/g, "-");
|
|
1638
2249
|
}
|
|
1639
2250
|
function claudeProjectTranscriptDir(repoRoot) {
|
|
1640
|
-
return
|
|
2251
|
+
return join5(homedir3(), ".claude", "projects", claudeProjectSlug(repoRoot));
|
|
1641
2252
|
}
|
|
1642
2253
|
async function listTranscriptFiles(repoRoot) {
|
|
1643
2254
|
const dir = claudeProjectTranscriptDir(repoRoot);
|
|
1644
|
-
if (!
|
|
2255
|
+
if (!existsSync3(dir)) return [];
|
|
1645
2256
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
1646
|
-
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));
|
|
1647
2258
|
}
|
|
1648
2259
|
|
|
1649
2260
|
// src/conversation/claude-code-reader.ts
|
|
@@ -1716,15 +2327,15 @@ async function collectClaudeCodeTranscripts(repoRoot) {
|
|
|
1716
2327
|
const files = await listTranscriptFiles(repoRoot);
|
|
1717
2328
|
const turns = [];
|
|
1718
2329
|
for (const file of files) {
|
|
1719
|
-
const raw = await
|
|
2330
|
+
const raw = await readFile4(file, "utf8");
|
|
1720
2331
|
turns.push(...parseClaudeCodeTranscript(raw));
|
|
1721
2332
|
}
|
|
1722
2333
|
return turns;
|
|
1723
2334
|
}
|
|
1724
2335
|
|
|
1725
2336
|
// src/docs/read.ts
|
|
1726
|
-
import { readFile as
|
|
1727
|
-
import { join as
|
|
2337
|
+
import { readFile as readFile5, stat } from "fs/promises";
|
|
2338
|
+
import { join as join6 } from "path";
|
|
1728
2339
|
var DEFAULT_PATHSPECS = ["*.md"];
|
|
1729
2340
|
async function listDocFiles(repoRoot, opts = {}) {
|
|
1730
2341
|
const pathspecs = opts.include ?? DEFAULT_PATHSPECS;
|
|
@@ -1737,11 +2348,11 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
1737
2348
|
const unreadable = [];
|
|
1738
2349
|
for (const relPath of paths) {
|
|
1739
2350
|
const path = relPath.replace(/\\/g, "/");
|
|
1740
|
-
const absPath =
|
|
2351
|
+
const absPath = join6(repoRoot, relPath);
|
|
1741
2352
|
let content;
|
|
1742
2353
|
let mtime;
|
|
1743
2354
|
try {
|
|
1744
|
-
[content, { mtime }] = await Promise.all([
|
|
2355
|
+
[content, { mtime }] = await Promise.all([readFile5(absPath, "utf8"), stat(absPath)]);
|
|
1745
2356
|
} catch {
|
|
1746
2357
|
unreadable.push(path);
|
|
1747
2358
|
continue;
|
|
@@ -1752,11 +2363,11 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
1752
2363
|
}
|
|
1753
2364
|
|
|
1754
2365
|
// src/shell/detect.ts
|
|
1755
|
-
import { existsSync as
|
|
1756
|
-
import { readFile as
|
|
2366
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2367
|
+
import { readFile as readFile7, stat as stat2 } from "fs/promises";
|
|
1757
2368
|
|
|
1758
2369
|
// src/shell/hook-log.ts
|
|
1759
|
-
import { appendFile, mkdir as
|
|
2370
|
+
import { appendFile, mkdir as mkdir4, readFile as readFile6 } from "fs/promises";
|
|
1760
2371
|
import { dirname as dirname3 } from "path";
|
|
1761
2372
|
function parseHookLogLine(line) {
|
|
1762
2373
|
const trimmed = line.trim();
|
|
@@ -1781,7 +2392,7 @@ function parseHookLogLine(line) {
|
|
|
1781
2392
|
async function readHookLog(path, fromLine) {
|
|
1782
2393
|
let raw;
|
|
1783
2394
|
try {
|
|
1784
|
-
raw = await
|
|
2395
|
+
raw = await readFile6(path, "utf8");
|
|
1785
2396
|
} catch {
|
|
1786
2397
|
return { entries: [], totalLines: fromLine };
|
|
1787
2398
|
}
|
|
@@ -1914,8 +2525,8 @@ function hookEntryToRaw(e) {
|
|
|
1914
2525
|
};
|
|
1915
2526
|
}
|
|
1916
2527
|
async function tryReadScrapeSource(path, parse, tailLines) {
|
|
1917
|
-
if (!
|
|
1918
|
-
const [raw, stats] = await Promise.all([
|
|
2528
|
+
if (!existsSync4(path)) return null;
|
|
2529
|
+
const [raw, stats] = await Promise.all([readFile7(path, "utf8"), stat2(path)]);
|
|
1919
2530
|
return parse(raw, stats.mtimeMs, { tailLines });
|
|
1920
2531
|
}
|
|
1921
2532
|
async function collectAvailableShellHistory(opts = {}) {
|
|
@@ -1923,7 +2534,7 @@ async function collectAvailableShellHistory(opts = {}) {
|
|
|
1923
2534
|
const tailLines = opts.tailLines ?? 300;
|
|
1924
2535
|
const preferHook = opts.preferHook ?? true;
|
|
1925
2536
|
const hookPath = hookLogPath();
|
|
1926
|
-
const hookExists =
|
|
2537
|
+
const hookExists = existsSync4(hookPath);
|
|
1927
2538
|
if (hookExists) {
|
|
1928
2539
|
const fromLine = Number(opts.hookCursor ?? "0") || 0;
|
|
1929
2540
|
const { entries, totalLines } = await readHookLog(hookPath, fromLine);
|
|
@@ -1979,25 +2590,25 @@ function addStats(into, from) {
|
|
|
1979
2590
|
async function syncGit(store, projectId, opts, repo, config, log) {
|
|
1980
2591
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
1981
2592
|
if (!repo.head) {
|
|
1982
|
-
log(`${
|
|
2593
|
+
log(`${pc4.yellow("git")} skipped -- repository has no commits yet`);
|
|
1983
2594
|
return { totals, seen: 0 };
|
|
1984
2595
|
}
|
|
1985
2596
|
if (!config.sources.git.enabled) {
|
|
1986
|
-
log(`${
|
|
2597
|
+
log(`${pc4.dim("git")} disabled in config`);
|
|
1987
2598
|
return { totals, seen: 0 };
|
|
1988
2599
|
}
|
|
1989
2600
|
let cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, GIT_SOURCE);
|
|
1990
2601
|
if (cursor && !await isAncestor(repo.root, cursor, repo.head)) {
|
|
1991
|
-
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`);
|
|
1992
2603
|
cursor = null;
|
|
1993
2604
|
}
|
|
1994
2605
|
if (cursor === repo.head) {
|
|
1995
|
-
log(`${
|
|
2606
|
+
log(`${pc4.green("git up to date")} at ${repo.head.slice(0, 7)}`);
|
|
1996
2607
|
store.setSyncCursor(projectId, GIT_SOURCE, repo.head);
|
|
1997
2608
|
return { totals, seen: 0 };
|
|
1998
2609
|
}
|
|
1999
2610
|
log(
|
|
2000
|
-
`${
|
|
2611
|
+
`${pc4.dim("git syncing")} ${repo.branch ?? "HEAD"} ${cursor ? `${cursor.slice(0, 7)}..${repo.head.slice(0, 7)}` : "(full history)"}`
|
|
2001
2612
|
);
|
|
2002
2613
|
let batch = [];
|
|
2003
2614
|
let seen = 0;
|
|
@@ -2005,7 +2616,7 @@ async function syncGit(store, projectId, opts, repo, config, log) {
|
|
|
2005
2616
|
if (batch.length === 0) return;
|
|
2006
2617
|
addStats(totals, store.upsertNodes(batch));
|
|
2007
2618
|
batch = [];
|
|
2008
|
-
log(` ${
|
|
2619
|
+
log(` ${pc4.dim(`${seen} commits read, ${totals.inserted} new`)}`);
|
|
2009
2620
|
};
|
|
2010
2621
|
const nodes = collectGitCommits(repo.root, projectId, {
|
|
2011
2622
|
afterCommit: cursor,
|
|
@@ -2023,10 +2634,51 @@ async function syncGit(store, projectId, opts, repo, config, log) {
|
|
|
2023
2634
|
store.setSyncCursor(projectId, GIT_SOURCE, repo.head);
|
|
2024
2635
|
return { totals, seen };
|
|
2025
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
|
+
}
|
|
2026
2678
|
async function syncShell(store, projectId, opts, repoRoot, config, log) {
|
|
2027
2679
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
2028
2680
|
if (!config.sources.shell.enabled) {
|
|
2029
|
-
log(`${
|
|
2681
|
+
log(`${pc4.dim("shell")} disabled in config`);
|
|
2030
2682
|
return { totals, seen: 0 };
|
|
2031
2683
|
}
|
|
2032
2684
|
const results = await collectAvailableShellHistory({
|
|
@@ -2035,7 +2687,7 @@ async function syncShell(store, projectId, opts, repoRoot, config, log) {
|
|
|
2035
2687
|
hookCursor: store.getSyncCursor(projectId, "shell:pwsh-hook")
|
|
2036
2688
|
});
|
|
2037
2689
|
if (results.length === 0) {
|
|
2038
|
-
log(`${
|
|
2690
|
+
log(`${pc4.dim("shell")} no history source found on this machine`);
|
|
2039
2691
|
return { totals, seen: 0 };
|
|
2040
2692
|
}
|
|
2041
2693
|
let seen = 0;
|
|
@@ -2047,7 +2699,7 @@ async function syncShell(store, projectId, opts, repoRoot, config, log) {
|
|
|
2047
2699
|
addStats(totals, store.upsertNodes(nodes));
|
|
2048
2700
|
}
|
|
2049
2701
|
store.setSyncCursor(projectId, sourceKey, result.cursorAfter ?? `scanned:${result.entries.length}`);
|
|
2050
|
-
log(` ${
|
|
2702
|
+
log(` ${pc4.dim(`${sourceKey}: ${nodes.length} entr${nodes.length === 1 ? "y" : "ies"} read`)}`);
|
|
2051
2703
|
}
|
|
2052
2704
|
return { totals, seen };
|
|
2053
2705
|
}
|
|
@@ -2060,20 +2712,20 @@ async function syncConversation(store, projectId, repoRoot, config, log, forceEn
|
|
|
2060
2712
|
}
|
|
2061
2713
|
const turns = await collectClaudeCodeTranscripts(repoRoot);
|
|
2062
2714
|
if (turns.length === 0) {
|
|
2063
|
-
log(`${
|
|
2715
|
+
log(`${pc4.dim("conversation")} no transcripts found`);
|
|
2064
2716
|
return { totals, seen: 0 };
|
|
2065
2717
|
}
|
|
2066
2718
|
const nodes = collectConversationTurns(turns, projectId, { maxBodyChars: config.limits.maxBodyChars });
|
|
2067
2719
|
if (nodes.length > 0) addStats(totals, store.upsertNodes(nodes));
|
|
2068
2720
|
store.setSyncCursor(projectId, CONVERSATION_SOURCE, `scanned:${nodes.length}`);
|
|
2069
|
-
log(` ${
|
|
2721
|
+
log(` ${pc4.dim(`${CONVERSATION_SOURCE}: ${nodes.length} of ${turns.length} exchange(s) kept`)}`);
|
|
2070
2722
|
return { totals, seen: nodes.length };
|
|
2071
2723
|
}
|
|
2072
2724
|
var DOCS_SOURCE = "docs";
|
|
2073
2725
|
async function syncDocs(store, projectId, repoRoot, config, log) {
|
|
2074
2726
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
2075
2727
|
if (!config.sources.docs.enabled) {
|
|
2076
|
-
log(`${
|
|
2728
|
+
log(`${pc4.dim("docs")} disabled in config`);
|
|
2077
2729
|
return { totals, seen: 0 };
|
|
2078
2730
|
}
|
|
2079
2731
|
const { files, unreadable } = await readDocFiles(repoRoot, { include: config.sources.docs.include });
|
|
@@ -2087,11 +2739,11 @@ async function syncDocs(store, projectId, repoRoot, config, log) {
|
|
|
2087
2739
|
);
|
|
2088
2740
|
store.setSyncCursor(projectId, DOCS_SOURCE, `scanned:${nodes.length}`);
|
|
2089
2741
|
if (files.length === 0 && unreadable.length === 0) {
|
|
2090
|
-
log(`${
|
|
2742
|
+
log(`${pc4.dim("docs")} no tracked .md files found`);
|
|
2091
2743
|
} else {
|
|
2092
|
-
const prunedPart = pruned > 0 ? `, ${
|
|
2744
|
+
const prunedPart = pruned > 0 ? `, ${pc4.yellow(`${pruned} stale removed`)}` : "";
|
|
2093
2745
|
const skippedPart = unreadable.length > 0 ? `, ${unreadable.length} unreadable (kept)` : "";
|
|
2094
|
-
log(` ${
|
|
2746
|
+
log(` ${pc4.dim(`${DOCS_SOURCE}: ${nodes.length} section(s) from ${files.length} file(s)`)}${prunedPart}${pc4.dim(skippedPart)}`);
|
|
2095
2747
|
}
|
|
2096
2748
|
return { totals, seen: nodes.length };
|
|
2097
2749
|
}
|
|
@@ -2106,11 +2758,13 @@ async function runSync(opts) {
|
|
|
2106
2758
|
const started = Date.now();
|
|
2107
2759
|
try {
|
|
2108
2760
|
store.upsertProject({ id: projectId, root: repo.root, originUrl: repo.originUrl });
|
|
2761
|
+
await recordProject({ projectId, root: repo.root, dbPath: ws.dbPath, originUrl: repo.originUrl });
|
|
2109
2762
|
if (opts.rebuild) {
|
|
2110
2763
|
const removed = store.clearProject(projectId);
|
|
2111
|
-
log(`${
|
|
2764
|
+
log(`${pc4.dim("rebuild")} dropped ${removed} existing node(s)`);
|
|
2112
2765
|
}
|
|
2113
2766
|
const git2 = await syncGit(store, projectId, opts, repo, config, log);
|
|
2767
|
+
const diffs = await syncDiffs(store, projectId, opts, repo, config, log);
|
|
2114
2768
|
const shell = await syncShell(store, projectId, opts, repo.root, config, log);
|
|
2115
2769
|
const conversation = await syncConversation(store, projectId, repo.root, config, log, opts.conversationOverride);
|
|
2116
2770
|
const docs = await syncDocs(store, projectId, repo.root, config, log);
|
|
@@ -2118,15 +2772,16 @@ async function runSync(opts) {
|
|
|
2118
2772
|
if (!opts.noEmbed) {
|
|
2119
2773
|
const result = await embedPendingNodes(store, new OllamaEmbeddingProvider(), projectId);
|
|
2120
2774
|
if (result.embedded > 0) {
|
|
2121
|
-
embedLine = ` ${
|
|
2775
|
+
embedLine = ` ${pc4.dim(`vector: ${result.embedded} node(s) embedded`)}${result.skipped > 0 ? pc4.dim(`, ${result.skipped} skipped`) : ""}
|
|
2122
2776
|
`;
|
|
2123
2777
|
} else if (result.providerUnavailable) {
|
|
2124
|
-
log(`${
|
|
2778
|
+
log(`${pc4.dim("vector")} embedding provider unavailable (is Ollama running with nomic-embed-text pulled?) -- BM25-only for now`);
|
|
2125
2779
|
}
|
|
2126
2780
|
}
|
|
2127
2781
|
store.markSynced(projectId);
|
|
2128
2782
|
const totals = { inserted: 0, updated: 0, unchanged: 0 };
|
|
2129
2783
|
addStats(totals, git2.totals);
|
|
2784
|
+
addStats(totals, diffs.totals);
|
|
2130
2785
|
addStats(totals, shell.totals);
|
|
2131
2786
|
addStats(totals, conversation.totals);
|
|
2132
2787
|
addStats(totals, docs.totals);
|
|
@@ -2135,11 +2790,12 @@ async function runSync(opts) {
|
|
|
2135
2790
|
const conversationEnabled = opts.conversationOverride ?? config.sources.conversation.enabled;
|
|
2136
2791
|
const conversationPart = conversationEnabled ? `, ${conversation.seen} conversation exchange(s)` : "";
|
|
2137
2792
|
const docsPart = config.sources.docs.enabled ? `, ${docs.seen} doc section(s)` : "";
|
|
2793
|
+
const diffPart = config.sources.diff.enabled ? `, ${diffs.seen} file diff(s)` : "";
|
|
2138
2794
|
out(
|
|
2139
2795
|
[
|
|
2140
|
-
`${
|
|
2141
|
-
` ${
|
|
2142
|
-
` ${
|
|
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)`)}`,
|
|
2143
2799
|
""
|
|
2144
2800
|
].join("\n") + embedLine
|
|
2145
2801
|
);
|
|
@@ -2156,20 +2812,39 @@ async function searchMemory(input) {
|
|
|
2156
2812
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
2157
2813
|
const budget = input.budget ?? 2e3;
|
|
2158
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
|
+
}
|
|
2159
2837
|
const store = MemoryStore.open(ws.dbPath);
|
|
2160
2838
|
try {
|
|
2161
|
-
const { bm25Count, vectorCount, hits, packed } = await runHybridQuery(store, projectId, input.query,
|
|
2162
|
-
budget,
|
|
2163
|
-
candidates,
|
|
2164
|
-
embeddingProvider: input.noVector ? null : new OllamaEmbeddingProvider()
|
|
2165
|
-
});
|
|
2839
|
+
const { bm25Count, vectorCount, hits, packed } = await runHybridQuery(store, projectId, input.query, queryOpts);
|
|
2166
2840
|
return {
|
|
2167
2841
|
text: renderContextBlock(input.query, packed),
|
|
2168
2842
|
matched: hits.length,
|
|
2169
2843
|
bm25Matched: bm25Count,
|
|
2170
2844
|
vectorMatched: vectorCount,
|
|
2171
2845
|
tokensUsed: packed.tokensUsed,
|
|
2172
|
-
tokensBudget: packed.tokensBudget
|
|
2846
|
+
tokensBudget: packed.tokensBudget,
|
|
2847
|
+
projectsSearched: [basename2(repo.root) || repo.root]
|
|
2173
2848
|
};
|
|
2174
2849
|
} finally {
|
|
2175
2850
|
store.close();
|
|
@@ -2207,20 +2882,23 @@ async function getStatus(input) {
|
|
|
2207
2882
|
|
|
2208
2883
|
// src/mcp/server.ts
|
|
2209
2884
|
function createServer() {
|
|
2210
|
-
const server = new McpServer({ name: "nexusmem", version:
|
|
2885
|
+
const server = new McpServer({ name: "nexusmem", version: readOwnVersion() });
|
|
2211
2886
|
server.registerTool(
|
|
2212
2887
|
"search_memory",
|
|
2213
2888
|
{
|
|
2214
2889
|
title: "Search remembered project history",
|
|
2215
|
-
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.",
|
|
2216
2891
|
inputSchema: {
|
|
2217
|
-
projectRoot:
|
|
2218
|
-
query:
|
|
2219
|
-
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
|
+
)
|
|
2220
2898
|
}
|
|
2221
2899
|
},
|
|
2222
|
-
async ({ projectRoot, query, budget }) => {
|
|
2223
|
-
const result = await searchMemory({ projectRoot, query, budget });
|
|
2900
|
+
async ({ projectRoot, query, budget, allProjects }) => {
|
|
2901
|
+
const result = await searchMemory({ projectRoot, query, budget, allProjects });
|
|
2224
2902
|
return {
|
|
2225
2903
|
content: [{ type: "text", text: result.text }],
|
|
2226
2904
|
structuredContent: {
|
|
@@ -2229,7 +2907,8 @@ function createServer() {
|
|
|
2229
2907
|
bm25Matched: result.bm25Matched,
|
|
2230
2908
|
vectorMatched: result.vectorMatched,
|
|
2231
2909
|
tokensUsed: result.tokensUsed,
|
|
2232
|
-
tokensBudget: result.tokensBudget
|
|
2910
|
+
tokensBudget: result.tokensBudget,
|
|
2911
|
+
projectsSearched: result.projectsSearched
|
|
2233
2912
|
}
|
|
2234
2913
|
};
|
|
2235
2914
|
}
|
|
@@ -2238,9 +2917,9 @@ function createServer() {
|
|
|
2238
2917
|
"sync_project",
|
|
2239
2918
|
{
|
|
2240
2919
|
title: "Sync remembered history",
|
|
2241
|
-
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.",
|
|
2242
2921
|
inputSchema: {
|
|
2243
|
-
projectRoot:
|
|
2922
|
+
projectRoot: z3.string().describe("Absolute path to the repository root")
|
|
2244
2923
|
}
|
|
2245
2924
|
},
|
|
2246
2925
|
async ({ projectRoot }) => {
|
|
@@ -2254,7 +2933,7 @@ function createServer() {
|
|
|
2254
2933
|
title: "Show what is remembered",
|
|
2255
2934
|
description: "Report how many nodes NexusMem currently remembers for a repository, broken down by kind and source.",
|
|
2256
2935
|
inputSchema: {
|
|
2257
|
-
projectRoot:
|
|
2936
|
+
projectRoot: z3.string().describe("Absolute path to the repository root")
|
|
2258
2937
|
}
|
|
2259
2938
|
},
|
|
2260
2939
|
async ({ projectRoot }) => {
|
|
@@ -2274,17 +2953,41 @@ async function runMcpServer() {
|
|
|
2274
2953
|
}
|
|
2275
2954
|
|
|
2276
2955
|
// src/cli/commands/query.ts
|
|
2277
|
-
import
|
|
2956
|
+
import pc5 from "picocolors";
|
|
2278
2957
|
async function runQuery(opts) {
|
|
2279
|
-
const { ws, projectId } = await loadContext(opts.cwd);
|
|
2280
|
-
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;
|
|
2281
2961
|
try {
|
|
2282
|
-
const
|
|
2962
|
+
const queryOpts = {
|
|
2283
2963
|
budget: opts.budget,
|
|
2284
2964
|
candidates: opts.candidates,
|
|
2285
2965
|
halfLifeDays: opts.halfLifeDays,
|
|
2286
2966
|
embeddingProvider: opts.noVector ? null : new OllamaEmbeddingProvider()
|
|
2287
|
-
}
|
|
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
|
+
}
|
|
2288
2991
|
const matched = hits.length;
|
|
2289
2992
|
const rawTokens = hits.reduce((n, h) => n + approxTokens(h.body), 0);
|
|
2290
2993
|
const packerEfficiency = rawTokens > 0 ? 1 - packed.tokensUsed / rawTokens : 0;
|
|
@@ -2309,15 +3012,15 @@ async function runQuery(opts) {
|
|
|
2309
3012
|
return 0;
|
|
2310
3013
|
}
|
|
2311
3014
|
if (matched === 0) {
|
|
2312
|
-
process.stderr.write(`${
|
|
3015
|
+
process.stderr.write(`${pc5.yellow("no matches")} for "${opts.query}"
|
|
2313
3016
|
`);
|
|
2314
3017
|
return 0;
|
|
2315
3018
|
}
|
|
2316
3019
|
process.stderr.write(
|
|
2317
3020
|
[
|
|
2318
|
-
`${
|
|
2319
|
-
`${
|
|
2320
|
-
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)`)}` : "",
|
|
2321
3024
|
""
|
|
2322
3025
|
].filter(Boolean).join("\n")
|
|
2323
3026
|
);
|
|
@@ -2325,28 +3028,30 @@ async function runQuery(opts) {
|
|
|
2325
3028
|
`);
|
|
2326
3029
|
return 0;
|
|
2327
3030
|
} finally {
|
|
2328
|
-
|
|
3031
|
+
opened?.close();
|
|
3032
|
+
store?.close();
|
|
2329
3033
|
}
|
|
2330
3034
|
}
|
|
2331
3035
|
|
|
2332
3036
|
// src/cli/commands/scan-conversation.ts
|
|
2333
|
-
import
|
|
3037
|
+
import pc7 from "picocolors";
|
|
2334
3038
|
|
|
2335
3039
|
// src/cli/format.ts
|
|
2336
|
-
import
|
|
3040
|
+
import pc6 from "picocolors";
|
|
2337
3041
|
var GIT_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
2338
3042
|
var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
2339
3043
|
var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
2340
3044
|
var DOCS_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
3045
|
+
var DIFF_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
2341
3046
|
function signalBand(signal, bands) {
|
|
2342
3047
|
if (signal >= bands.high) return "high";
|
|
2343
3048
|
if (signal >= bands.medium) return "medium";
|
|
2344
3049
|
return "low";
|
|
2345
3050
|
}
|
|
2346
3051
|
var BAND_COLOR = {
|
|
2347
|
-
high:
|
|
2348
|
-
medium:
|
|
2349
|
-
low:
|
|
3052
|
+
high: pc6.green,
|
|
3053
|
+
medium: pc6.yellow,
|
|
3054
|
+
low: pc6.dim
|
|
2350
3055
|
};
|
|
2351
3056
|
function formatSignal(signal, bands) {
|
|
2352
3057
|
return BAND_COLOR[signalBand(signal, bands)](signal.toFixed(2));
|
|
@@ -2359,9 +3064,9 @@ async function runScanConversation(opts) {
|
|
|
2359
3064
|
const files = await listTranscriptFiles(repo.root);
|
|
2360
3065
|
if (!opts.json) {
|
|
2361
3066
|
process.stderr.write(
|
|
2362
|
-
files.length ? `${
|
|
3067
|
+
files.length ? `${pc7.dim("transcripts")} ${files.length} file(s) in ${claudeProjectTranscriptDir(repo.root)}
|
|
2363
3068
|
|
|
2364
|
-
` : `${
|
|
3069
|
+
` : `${pc7.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
2365
3070
|
`
|
|
2366
3071
|
);
|
|
2367
3072
|
}
|
|
@@ -2378,7 +3083,7 @@ async function runScanConversation(opts) {
|
|
|
2378
3083
|
const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
2379
3084
|
process.stderr.write(
|
|
2380
3085
|
`
|
|
2381
|
-
${
|
|
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"
|
|
2382
3087
|
);
|
|
2383
3088
|
return 0;
|
|
2384
3089
|
}
|
|
@@ -2386,44 +3091,8 @@ function formatNode(node) {
|
|
|
2386
3091
|
return [formatSignal(node.signal, CONVERSATION_SIGNAL_BANDS), node.ts.slice(0, 16).replace("T", " "), node.title].join(" ");
|
|
2387
3092
|
}
|
|
2388
3093
|
|
|
2389
|
-
// src/cli/commands/scan-
|
|
2390
|
-
import
|
|
2391
|
-
async function runScanDocs(opts) {
|
|
2392
|
-
const repo = await readRepoInfo(opts.cwd);
|
|
2393
|
-
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
2394
|
-
const { files, unreadable } = await readDocFiles(repo.root);
|
|
2395
|
-
if (!opts.json) {
|
|
2396
|
-
process.stderr.write(
|
|
2397
|
-
files.length ? `${pc7.dim("tracked .md files")} ${files.map((f) => f.path).join(", ")}
|
|
2398
|
-
|
|
2399
|
-
` : `${pc7.yellow("no tracked .md files found")}
|
|
2400
|
-
`
|
|
2401
|
-
);
|
|
2402
|
-
if (unreadable.length > 0) {
|
|
2403
|
-
process.stderr.write(`${pc7.yellow("unreadable")} ${unreadable.join(", ")}
|
|
2404
|
-
|
|
2405
|
-
`);
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
const nodes = collectDocFiles(files, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
2409
|
-
if (opts.json) {
|
|
2410
|
-
process.stdout.write(`${JSON.stringify(nodes, null, 2)}
|
|
2411
|
-
`);
|
|
2412
|
-
return 0;
|
|
2413
|
-
}
|
|
2414
|
-
for (const node of nodes) process.stdout.write(`${formatNode2(node)}
|
|
2415
|
-
`);
|
|
2416
|
-
const approxTotal = nodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
2417
|
-
process.stderr.write(
|
|
2418
|
-
`
|
|
2419
|
-
${pc7.bold(String(nodes.length))} section(s) from ${files.length} file(s) above threshold ${pc7.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
2420
|
-
`
|
|
2421
|
-
);
|
|
2422
|
-
return 0;
|
|
2423
|
-
}
|
|
2424
|
-
function formatNode2(node) {
|
|
2425
|
-
return [formatSignal(node.signal, DOCS_SIGNAL_BANDS), node.title].join(" ");
|
|
2426
|
-
}
|
|
3094
|
+
// src/cli/commands/scan-diff.ts
|
|
3095
|
+
import pc9 from "picocolors";
|
|
2427
3096
|
|
|
2428
3097
|
// src/cli/commands/scan-git.ts
|
|
2429
3098
|
import pc8 from "picocolors";
|
|
@@ -2450,7 +3119,7 @@ async function runScanGit(opts) {
|
|
|
2450
3119
|
for await (const node of collectGitCommits(repo.root, projectId, collectOpts)) {
|
|
2451
3120
|
if (node.signal < opts.minSignal) continue;
|
|
2452
3121
|
nodes.push(node);
|
|
2453
|
-
if (!opts.json) process.stdout.write(`${
|
|
3122
|
+
if (!opts.json) process.stdout.write(`${formatNode2(node)}
|
|
2454
3123
|
`);
|
|
2455
3124
|
}
|
|
2456
3125
|
if (opts.json) {
|
|
@@ -2463,7 +3132,7 @@ ${summarize2(nodes)}
|
|
|
2463
3132
|
`);
|
|
2464
3133
|
return 0;
|
|
2465
3134
|
}
|
|
2466
|
-
function
|
|
3135
|
+
function formatNode2(node) {
|
|
2467
3136
|
const sha = String(node.meta.shortSha ?? "").padEnd(9);
|
|
2468
3137
|
const date = node.ts.slice(0, 10);
|
|
2469
3138
|
const files = Number(node.meta.filesChanged ?? 0);
|
|
@@ -2494,17 +3163,103 @@ ${hottest.join("\n")}` : ""
|
|
|
2494
3163
|
].filter(Boolean).join("\n");
|
|
2495
3164
|
}
|
|
2496
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
|
+
|
|
2497
3252
|
// src/cli/commands/scan-shell.ts
|
|
2498
|
-
import
|
|
3253
|
+
import pc11 from "picocolors";
|
|
2499
3254
|
async function runScanShell(opts) {
|
|
2500
3255
|
const repo = await readRepoInfo(opts.cwd);
|
|
2501
3256
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
2502
3257
|
const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
|
|
2503
3258
|
if (!opts.json) {
|
|
2504
3259
|
process.stderr.write(
|
|
2505
|
-
results.length ? `${
|
|
3260
|
+
results.length ? `${pc11.dim("sources found")} ${results.map((r) => r.name).join(", ")}
|
|
2506
3261
|
|
|
2507
|
-
` : `${
|
|
3262
|
+
` : `${pc11.yellow("no shell history source found on this machine")}
|
|
2508
3263
|
`
|
|
2509
3264
|
);
|
|
2510
3265
|
}
|
|
@@ -2513,9 +3268,9 @@ async function runScanShell(opts) {
|
|
|
2513
3268
|
const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
2514
3269
|
allNodes.push(...nodes);
|
|
2515
3270
|
if (!opts.json) {
|
|
2516
|
-
process.stdout.write(`${
|
|
3271
|
+
process.stdout.write(`${pc11.bold(`shell:${result.name}`)} ${pc11.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
|
|
2517
3272
|
`);
|
|
2518
|
-
for (const node of nodes) process.stdout.write(`${
|
|
3273
|
+
for (const node of nodes) process.stdout.write(`${formatNode5(node)}
|
|
2519
3274
|
`);
|
|
2520
3275
|
process.stdout.write("\n");
|
|
2521
3276
|
}
|
|
@@ -2526,20 +3281,20 @@ async function runScanShell(opts) {
|
|
|
2526
3281
|
return 0;
|
|
2527
3282
|
}
|
|
2528
3283
|
const approxTotal = allNodes.reduce((n, x) => n + approxTokens(x.body), 0);
|
|
2529
|
-
process.stderr.write(`${
|
|
3284
|
+
process.stderr.write(`${pc11.bold(String(allNodes.length))} node(s) total ${pc11.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
2530
3285
|
`);
|
|
2531
3286
|
return 0;
|
|
2532
3287
|
}
|
|
2533
|
-
function
|
|
2534
|
-
const approx = node.meta.tsApprox ?
|
|
3288
|
+
function formatNode5(node) {
|
|
3289
|
+
const approx = node.meta.tsApprox ? pc11.dim("~") : " ";
|
|
2535
3290
|
const exit = node.meta.exitCode;
|
|
2536
|
-
const exitLabel = typeof exit === "number" && exit !== 0 ?
|
|
3291
|
+
const exitLabel = typeof exit === "number" && exit !== 0 ? pc11.red(`exit ${exit}`) : "";
|
|
2537
3292
|
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
|
|
2538
3293
|
}
|
|
2539
3294
|
|
|
2540
3295
|
// src/cli/commands/status.ts
|
|
2541
3296
|
import { statSync } from "fs";
|
|
2542
|
-
import
|
|
3297
|
+
import pc12 from "picocolors";
|
|
2543
3298
|
function humanBytes(bytes) {
|
|
2544
3299
|
if (bytes < 1024) return `${bytes} B`;
|
|
2545
3300
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -2564,23 +3319,23 @@ async function runStatus(opts) {
|
|
|
2564
3319
|
const kinds = Object.entries(stats.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
2565
3320
|
process.stdout.write(
|
|
2566
3321
|
[
|
|
2567
|
-
`${
|
|
2568
|
-
`${
|
|
2569
|
-
`${
|
|
2570
|
-
`${
|
|
2571
|
-
`${
|
|
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)})`)}`,
|
|
2572
3327
|
"",
|
|
2573
|
-
`${
|
|
3328
|
+
`${pc12.bold(String(stats.total))} node(s)${stats.total ? ` ${pc12.dim(`${stats.oldest?.slice(0, 10)} .. ${stats.newest?.slice(0, 10)}`)}` : ""}`,
|
|
2574
3329
|
...kinds,
|
|
2575
|
-
stats.total ? ` ${
|
|
3330
|
+
stats.total ? ` ${pc12.dim(`${stats.distinctFiles} distinct file path(s)`)}` : "",
|
|
2576
3331
|
"",
|
|
2577
|
-
sources.length ?
|
|
3332
|
+
sources.length ? pc12.dim("sources") : pc12.yellow("no sources synced yet"),
|
|
2578
3333
|
...sources.map((s) => {
|
|
2579
3334
|
const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
2580
3335
|
const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
|
|
2581
|
-
return ` ${s.source.padEnd(14)} ${
|
|
3336
|
+
return ` ${s.source.padEnd(14)} ${pc12.dim(`last run ${when}`)} ${pc12.dim(`cursor ${cursorLabel}`)}`;
|
|
2582
3337
|
}),
|
|
2583
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
3338
|
+
gitCursor && gitCursor !== repo.head ? `${pc12.yellow("git behind HEAD")} \u2014 run ${pc12.bold("nexusmem sync")}` : "",
|
|
2584
3339
|
""
|
|
2585
3340
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
2586
3341
|
);
|
|
@@ -2604,7 +3359,7 @@ function guard(run) {
|
|
|
2604
3359
|
process.exitCode = await run();
|
|
2605
3360
|
} catch (err) {
|
|
2606
3361
|
if (isExpected(err)) {
|
|
2607
|
-
process.stderr.write(`${
|
|
3362
|
+
process.stderr.write(`${pc13.red("error")} ${err.message}
|
|
2608
3363
|
`);
|
|
2609
3364
|
process.exitCode = 1;
|
|
2610
3365
|
return;
|
|
@@ -2614,7 +3369,7 @@ function guard(run) {
|
|
|
2614
3369
|
};
|
|
2615
3370
|
}
|
|
2616
3371
|
var program = new Command();
|
|
2617
|
-
program.name("nexusmem").description("NexusMem \u2014 local-first persistent memory for AI coding agents").version(
|
|
3372
|
+
program.name("nexusmem").description("NexusMem \u2014 local-first persistent memory for AI coding agents").version(readOwnVersion());
|
|
2618
3373
|
program.command("init").description("Create the .nexusmem workspace and database for this repository").option("-C, --cwd <path>", "repository path", process.cwd()).option("--force", "overwrite an existing config (the database is kept)", false).option("--hook", "also install the opt-in PowerShell hook (cwd + exit code + timestamp)", false).option("--enable-conversation", "opt in to the conversation-transcript source (off by default -- see docs/phase-2-spec.md)", false).action(
|
|
2619
3374
|
(options) => guard(
|
|
2620
3375
|
() => runInit({ cwd: options.cwd, force: options.force, hook: options.hook, enableConversation: options.enableConversation })
|
|
@@ -2642,7 +3397,7 @@ program.command("hook").description("Manage the opt-in PowerShell hook that logs
|
|
|
2642
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 }))())
|
|
2643
3398
|
);
|
|
2644
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 }))());
|
|
2645
|
-
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(
|
|
2646
3401
|
(text, options) => guard(
|
|
2647
3402
|
() => runQuery({
|
|
2648
3403
|
cwd: options.cwd,
|
|
@@ -2651,10 +3406,12 @@ program.command("query").description("Search remembered history and print a toke
|
|
|
2651
3406
|
candidates: options.candidates,
|
|
2652
3407
|
halfLifeDays: options.halfLife,
|
|
2653
3408
|
noVector: !options.vector,
|
|
3409
|
+
allProjects: options.allProjects,
|
|
2654
3410
|
json: options.json
|
|
2655
3411
|
})
|
|
2656
3412
|
)()
|
|
2657
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 }))());
|
|
2658
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(
|
|
2659
3416
|
(options) => guard(
|
|
2660
3417
|
() => runScanGit({
|
|
@@ -2667,6 +3424,17 @@ program.command("scan-git").description("Preview the MemoryNodes git history wou
|
|
|
2667
3424
|
})
|
|
2668
3425
|
)()
|
|
2669
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
|
+
);
|
|
2670
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(
|
|
2671
3439
|
(options) => guard(
|
|
2672
3440
|
() => runScanShell({ cwd: options.cwd, tailLines: options.tailLines, minSignal: options.minSignal, json: options.json })
|
|
@@ -2679,7 +3447,7 @@ program.command("scan-docs").description("Preview the MemoryNodes tracked .md fi
|
|
|
2679
3447
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
2680
3448
|
program.parseAsync(process.argv).catch((err) => {
|
|
2681
3449
|
const message = err instanceof Error ? err.message : String(err);
|
|
2682
|
-
process.stderr.write(`${
|
|
3450
|
+
process.stderr.write(`${pc13.red("error")} ${message}
|
|
2683
3451
|
`);
|
|
2684
3452
|
process.exitCode = 1;
|
|
2685
3453
|
});
|