nexusmem 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -1
- package/README.md +550 -529
- package/dist/cli/index.js +379 -72
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc22 from "picocolors";
|
|
6
6
|
|
|
7
7
|
// src/config/workspace.ts
|
|
8
8
|
import { existsSync } from "fs";
|
|
@@ -155,7 +155,26 @@ var ConfigSchema = z.object({
|
|
|
155
155
|
*/
|
|
156
156
|
structure: z.object({
|
|
157
157
|
enabled: z.boolean().default(true)
|
|
158
|
-
}).default({ enabled: true })
|
|
158
|
+
}).default({ enabled: true }),
|
|
159
|
+
/**
|
|
160
|
+
* Issue and PR threads (title, body, comments) for this repo's
|
|
161
|
+
* github.com remote, read via the `gh` CLI.
|
|
162
|
+
*
|
|
163
|
+
* Opt-in, unlike git/shell/docs/structure -- not for a sensitivity
|
|
164
|
+
* reason (issue/PR text is usually no more sensitive than a doc file),
|
|
165
|
+
* but because this is the first source with a real external dependency:
|
|
166
|
+
* it needs `gh` installed and authenticated, and makes live network
|
|
167
|
+
* calls instead of reading only what's already on disk. A repo with no
|
|
168
|
+
* github.com remote at all, or an unauthenticated `gh`, must stay a
|
|
169
|
+
* silent no-op either way -- see `syncGithub` in sync.ts.
|
|
170
|
+
*/
|
|
171
|
+
github: z.object({
|
|
172
|
+
enabled: z.boolean().default(false),
|
|
173
|
+
/** Threads read per sync, newest-updated first. */
|
|
174
|
+
maxThreads: z.number().int().positive().default(100),
|
|
175
|
+
/** Comments read per thread. */
|
|
176
|
+
maxCommentsPerThread: z.number().int().positive().default(100)
|
|
177
|
+
}).default({ enabled: false, maxThreads: 100, maxCommentsPerThread: 100 })
|
|
159
178
|
}).default({
|
|
160
179
|
git: { enabled: true, since: null, includeMerges: true },
|
|
161
180
|
shell: { enabled: true, tailLines: 300 },
|
|
@@ -169,7 +188,8 @@ var ConfigSchema = z.object({
|
|
|
169
188
|
},
|
|
170
189
|
docs: { enabled: true, include: ["*.md"] },
|
|
171
190
|
diff: { enabled: true, maxCommits: 200, maxFilesPerCommit: 20, contextLines: 3 },
|
|
172
|
-
structure: { enabled: true }
|
|
191
|
+
structure: { enabled: true },
|
|
192
|
+
github: { enabled: false, maxThreads: 100, maxCommentsPerThread: 100 }
|
|
173
193
|
}),
|
|
174
194
|
limits: z.object({
|
|
175
195
|
maxFilesPerNode: z.number().int().positive().default(40),
|
|
@@ -1013,6 +1033,10 @@ INSERT INTO nodes_vec (rowid, project_id, embedding)
|
|
|
1013
1033
|
|
|
1014
1034
|
DROP TABLE nodes_vec_stage;
|
|
1015
1035
|
`;
|
|
1036
|
+
var V12 = `
|
|
1037
|
+
ALTER TABLE nodes ADD COLUMN retrieved_count INTEGER NOT NULL DEFAULT 0;
|
|
1038
|
+
ALTER TABLE nodes ADD COLUMN last_retrieved_at INTEGER;
|
|
1039
|
+
`;
|
|
1016
1040
|
var MIGRATIONS = [
|
|
1017
1041
|
{ version: 1, up: (db) => db.exec(V1) },
|
|
1018
1042
|
{ version: 2, up: (db) => db.exec(V2) },
|
|
@@ -1024,7 +1048,8 @@ var MIGRATIONS = [
|
|
|
1024
1048
|
{ version: 8, up: (db) => db.exec(V8) },
|
|
1025
1049
|
{ version: 9, up: (db) => db.exec(V9) },
|
|
1026
1050
|
{ version: 10, up: (db) => db.exec(V10) },
|
|
1027
|
-
{ version: 11, up: (db) => db.exec(V11) }
|
|
1051
|
+
{ version: 11, up: (db) => db.exec(V11) },
|
|
1052
|
+
{ version: 12, up: (db) => db.exec(V12) }
|
|
1028
1053
|
];
|
|
1029
1054
|
var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
1030
1055
|
function currentSchemaVersion(db) {
|
|
@@ -1123,6 +1148,8 @@ function defaultProvenanceForKind(kind) {
|
|
|
1123
1148
|
return "recorded";
|
|
1124
1149
|
case "session_summary":
|
|
1125
1150
|
return "derived";
|
|
1151
|
+
case "github_thread":
|
|
1152
|
+
return "recorded";
|
|
1126
1153
|
}
|
|
1127
1154
|
}
|
|
1128
1155
|
|
|
@@ -1294,6 +1321,19 @@ function listStaleCandidates(db, projectId, opts = {}) {
|
|
|
1294
1321
|
ageDays: Math.round((now.getTime() - r.tsEpoch) / 864e5)
|
|
1295
1322
|
}));
|
|
1296
1323
|
}
|
|
1324
|
+
function recordRetrievals(db, ids) {
|
|
1325
|
+
if (ids.length === 0) return;
|
|
1326
|
+
db.prepare(
|
|
1327
|
+
`UPDATE nodes SET retrieved_count = retrieved_count + 1, last_retrieved_at = @now
|
|
1328
|
+
WHERE id IN (SELECT value FROM json_each(@ids))`
|
|
1329
|
+
).run({ now: Date.now(), ids: JSON.stringify(ids) });
|
|
1330
|
+
}
|
|
1331
|
+
function getRetrievalStats(db, id) {
|
|
1332
|
+
const row = db.prepare("SELECT retrieved_count AS retrievedCount, last_retrieved_at AS lastRetrievedAt FROM nodes WHERE id = ?").get(
|
|
1333
|
+
id
|
|
1334
|
+
);
|
|
1335
|
+
return row ?? null;
|
|
1336
|
+
}
|
|
1297
1337
|
function countStaleCandidates(db, projectId, opts = {}) {
|
|
1298
1338
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
1299
1339
|
const minAgeDays = opts.minAgeDays ?? 45;
|
|
@@ -1823,6 +1863,14 @@ var MemoryStore = class _MemoryStore {
|
|
|
1823
1863
|
countStaleCandidates(projectId, opts = {}) {
|
|
1824
1864
|
return countStaleCandidates(this.db, projectId, opts);
|
|
1825
1865
|
}
|
|
1866
|
+
/** Bump retrieval bookkeeping for nodes actually packed into a query result. See `nodes.ts`'s `recordRetrievals` for why this doesn't touch ranking. */
|
|
1867
|
+
recordRetrievals(ids) {
|
|
1868
|
+
recordRetrievals(this.db, ids);
|
|
1869
|
+
}
|
|
1870
|
+
/** Retrieval bookkeeping for one node, or null if it doesn't exist. Mainly for tests. */
|
|
1871
|
+
getRetrievalStats(id) {
|
|
1872
|
+
return getRetrievalStats(this.db, id);
|
|
1873
|
+
}
|
|
1826
1874
|
/** Memoize one SLM contradiction judgment (either verdict). Suggest-only: never writes `supersedes`. */
|
|
1827
1875
|
recordContradictionCheck(input) {
|
|
1828
1876
|
recordContradictionCheck(this.db, input);
|
|
@@ -2790,8 +2838,21 @@ async function runCrossProjectQuery(sources, query, opts) {
|
|
|
2790
2838
|
rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores, supersededIds })
|
|
2791
2839
|
);
|
|
2792
2840
|
const packed = packContext(ranked, opts.budget, { query });
|
|
2841
|
+
recordRetrievalsByProject(storeByLabel, packed.nodes);
|
|
2793
2842
|
return { bm25Count, vectorCount, hits, packed, perProject };
|
|
2794
2843
|
}
|
|
2844
|
+
function recordRetrievalsByProject(storeByLabel, nodes) {
|
|
2845
|
+
const idsByLabel = /* @__PURE__ */ new Map();
|
|
2846
|
+
for (const node of nodes) {
|
|
2847
|
+
if (!node.project) continue;
|
|
2848
|
+
const ids = idsByLabel.get(node.project) ?? [];
|
|
2849
|
+
ids.push(node.id);
|
|
2850
|
+
idsByLabel.set(node.project, ids);
|
|
2851
|
+
}
|
|
2852
|
+
for (const [label, ids] of idsByLabel) {
|
|
2853
|
+
storeByLabel.get(label)?.recordRetrievals(ids);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2795
2856
|
async function runHybridQuery(store, projectId, query, opts) {
|
|
2796
2857
|
const bm25Hits = store.search(projectId, query, opts.candidates, { asOfEpoch: opts.asOfEpoch });
|
|
2797
2858
|
let vectorHits = [];
|
|
@@ -2807,6 +2868,7 @@ async function runHybridQuery(store, projectId, query, opts) {
|
|
|
2807
2868
|
rankHits(hits, { halfLifeDays: opts.halfLifeDays, relevanceScores, supersededIds })
|
|
2808
2869
|
);
|
|
2809
2870
|
const packed = packContext(ranked, opts.budget, { query });
|
|
2871
|
+
store.recordRetrievals(packed.nodes.map((n) => n.id));
|
|
2810
2872
|
return { bm25Count: bm25Hits.length, vectorCount: vectorHits.length, hits, packed };
|
|
2811
2873
|
}
|
|
2812
2874
|
|
|
@@ -3634,6 +3696,63 @@ function collectDocFiles(files, projectId, opts = {}) {
|
|
|
3634
3696
|
return files.flatMap((file) => toMemoryNodes3(file, projectId, opts));
|
|
3635
3697
|
}
|
|
3636
3698
|
|
|
3699
|
+
// src/collectors/github.ts
|
|
3700
|
+
var NOTABLE_LABELS = /\b(bug|help wanted|good first issue)\b/i;
|
|
3701
|
+
function scoreGithubThread(thread) {
|
|
3702
|
+
let score = 0.4;
|
|
3703
|
+
if (thread.merged) score += 0.2;
|
|
3704
|
+
else if (thread.state === "closed" && thread.comments.length === 0) score -= 0.15;
|
|
3705
|
+
if (thread.comments.length >= 3) score += 0.15;
|
|
3706
|
+
else if (thread.comments.length >= 1) score += 0.05;
|
|
3707
|
+
if (thread.labels.some((l) => NOTABLE_LABELS.test(l))) score += 0.1;
|
|
3708
|
+
return Number(Math.min(1, Math.max(0.05, score)).toFixed(3));
|
|
3709
|
+
}
|
|
3710
|
+
var MAX_TITLE_CHARS5 = 200;
|
|
3711
|
+
function threadTitle(thread) {
|
|
3712
|
+
const prefix = thread.type === "pull_request" ? "PR" : "Issue";
|
|
3713
|
+
return truncate(`${prefix} #${thread.number}: ${thread.title}`, MAX_TITLE_CHARS5);
|
|
3714
|
+
}
|
|
3715
|
+
function renderBody(thread) {
|
|
3716
|
+
const status = thread.merged ? "merged" : thread.state;
|
|
3717
|
+
const header = `${thread.title}
|
|
3718
|
+
opened by @${thread.author}, ${status}`;
|
|
3719
|
+
const opening = thread.body.trim();
|
|
3720
|
+
const commentsText = thread.comments.map((c) => `--- @${c.author} (${c.createdAt.slice(0, 10)}) ---
|
|
3721
|
+
${c.body.trim()}`).join("\n\n");
|
|
3722
|
+
return [header, opening, commentsText].filter(Boolean).join("\n\n");
|
|
3723
|
+
}
|
|
3724
|
+
var DEFAULT_MAX_BODY_CHARS3 = 4e3;
|
|
3725
|
+
function toMemoryNode2(thread, projectId, opts = {}) {
|
|
3726
|
+
const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS3;
|
|
3727
|
+
const naturalKey = `${thread.type}:${thread.number}`;
|
|
3728
|
+
const source = thread.type === "pull_request" ? "github:pr" : "github:issue";
|
|
3729
|
+
return {
|
|
3730
|
+
id: makeNodeId(projectId, "github_thread", naturalKey),
|
|
3731
|
+
kind: "github_thread",
|
|
3732
|
+
projectId,
|
|
3733
|
+
ts: thread.updatedAt,
|
|
3734
|
+
source,
|
|
3735
|
+
title: threadTitle(thread),
|
|
3736
|
+
body: truncate(renderBody(thread), maxBody),
|
|
3737
|
+
files: [],
|
|
3738
|
+
signal: scoreGithubThread(thread),
|
|
3739
|
+
provenance: "recorded",
|
|
3740
|
+
// verbatim discourse, same tier as conversation_turn
|
|
3741
|
+
meta: {
|
|
3742
|
+
number: thread.number,
|
|
3743
|
+
type: thread.type,
|
|
3744
|
+
state: thread.state,
|
|
3745
|
+
merged: thread.merged,
|
|
3746
|
+
labels: thread.labels,
|
|
3747
|
+
url: thread.url,
|
|
3748
|
+
commentCount: thread.comments.length
|
|
3749
|
+
}
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
3752
|
+
function collectGithubThreads(threads, projectId, opts = {}) {
|
|
3753
|
+
return threads.map((thread) => toMemoryNode2(thread, projectId, opts));
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3637
3756
|
// src/slm/summarize.ts
|
|
3638
3757
|
var MAX_USER_CHARS = 400;
|
|
3639
3758
|
var MAX_REPLY_CHARS = 700;
|
|
@@ -3725,7 +3844,7 @@ function sessionFallbackTitle(session) {
|
|
|
3725
3844
|
const line = redact(opening.userText).text.split(/\r?\n/)[0]?.trim();
|
|
3726
3845
|
return line && line.length > 0 ? line : "Working session";
|
|
3727
3846
|
}
|
|
3728
|
-
var
|
|
3847
|
+
var MAX_TITLE_CHARS6 = 200;
|
|
3729
3848
|
var GENERIC_TITLE = /^(a |the )?(session |conversation |project |work )?(summary|update|overview|recap|status)\b/i;
|
|
3730
3849
|
var ROLE_PREAMBLE_TITLE = /^(role\s*[::]|you\s*(?:'re|are)\s+(acting as|serving as|playing the role of|a\b)|i\s*(?:'m|am)\s+(acting as|a\b)|acting as\b|บทบาท\s*[::]|ในฐานะ|คุณ(กำลัง)?(ทำหน้าที่เป็น|เป็น|รับบทบาทเป็น)|(ผม|ฉัน)(กำลัง)?(ทำหน้าที่เป็น|เป็น))/i;
|
|
3731
3850
|
function cleanTitle(line) {
|
|
@@ -3739,12 +3858,12 @@ function parseSummary(raw, fallbackTitle) {
|
|
|
3739
3858
|
if (firstIndex === -1) return null;
|
|
3740
3859
|
const first = lines[firstIndex].trim();
|
|
3741
3860
|
const labelled = /^TITLE:\s*(.+)$/i.exec(first);
|
|
3742
|
-
const fallback = truncate(cleanTitle(fallbackTitle) || "Working session",
|
|
3861
|
+
const fallback = truncate(cleanTitle(fallbackTitle) || "Working session", MAX_TITLE_CHARS6);
|
|
3743
3862
|
if (!labelled) return { title: fallback, body: text };
|
|
3744
3863
|
const candidate = cleanTitle(labelled[1]);
|
|
3745
3864
|
const rest = lines.slice(firstIndex + 1).join("\n").trim();
|
|
3746
3865
|
return {
|
|
3747
|
-
title: candidate.length > 0 && !GENERIC_TITLE.test(candidate) && !ROLE_PREAMBLE_TITLE.test(candidate) ? truncate(candidate,
|
|
3866
|
+
title: candidate.length > 0 && !GENERIC_TITLE.test(candidate) && !ROLE_PREAMBLE_TITLE.test(candidate) ? truncate(candidate, MAX_TITLE_CHARS6) : fallback,
|
|
3748
3867
|
// A model that emitted only a title still gets a usable node: the title
|
|
3749
3868
|
// doubles as the body rather than storing an empty one.
|
|
3750
3869
|
body: rest.length > 0 ? rest : candidate
|
|
@@ -3754,7 +3873,7 @@ function parseSummary(raw, fallbackTitle) {
|
|
|
3754
3873
|
// src/collectors/sessions.ts
|
|
3755
3874
|
var SESSION_SOURCE_PREFIX = "session";
|
|
3756
3875
|
var DEFAULT_SETTLE_MINUTES = 30;
|
|
3757
|
-
var
|
|
3876
|
+
var DEFAULT_MAX_BODY_CHARS4 = 2500;
|
|
3758
3877
|
var DEFAULT_MAX_SESSIONS = 10;
|
|
3759
3878
|
function scoreSession(turnCount) {
|
|
3760
3879
|
const score = 0.6 + Math.min(0.25, turnCount / 100);
|
|
@@ -3798,7 +3917,7 @@ ${t.assistantText}`).join("\n")),
|
|
|
3798
3917
|
};
|
|
3799
3918
|
}
|
|
3800
3919
|
async function collectSessionSummaries(turns, projectId, provider, opts = {}) {
|
|
3801
|
-
const maxBodyChars = opts.maxBodyChars ??
|
|
3920
|
+
const maxBodyChars = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS4;
|
|
3802
3921
|
const maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
3803
3922
|
const all = groupTurnsIntoSessions(turns);
|
|
3804
3923
|
const settled = selectSettledSessions(all, opts.settleMinutes ?? DEFAULT_SETTLE_MINUTES, opts.now);
|
|
@@ -3846,8 +3965,8 @@ async function collectSessionSummaries(turns, projectId, provider, opts = {}) {
|
|
|
3846
3965
|
}
|
|
3847
3966
|
|
|
3848
3967
|
// src/collectors/shell-history.ts
|
|
3849
|
-
var
|
|
3850
|
-
var
|
|
3968
|
+
var DEFAULT_MAX_BODY_CHARS5 = 1e3;
|
|
3969
|
+
var MAX_TITLE_CHARS7 = 200;
|
|
3851
3970
|
var NOISE = /^(cd|ls|dir|pwd|clear|cls|exit|history|whoami|date|type|cat|more|less|ll|la)\b/i;
|
|
3852
3971
|
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;
|
|
3853
3972
|
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;
|
|
@@ -3870,7 +3989,7 @@ function scoreShellCommand(entry) {
|
|
|
3870
3989
|
else if (cmd.length <= 3) score -= 0.05;
|
|
3871
3990
|
return Number(Math.min(1, Math.max(0.05, score)).toFixed(3));
|
|
3872
3991
|
}
|
|
3873
|
-
function
|
|
3992
|
+
function renderBody2(entry, maxChars) {
|
|
3874
3993
|
const parts = [`$ ${entry.command}`];
|
|
3875
3994
|
const metaLine = [];
|
|
3876
3995
|
if (entry.cwd) metaLine.push(`cwd: ${entry.cwd}`);
|
|
@@ -3879,8 +3998,8 @@ function renderBody(entry, maxChars) {
|
|
|
3879
3998
|
if (metaLine.length) parts.push("", metaLine.join(" "));
|
|
3880
3999
|
return truncate(parts.join("\n"), maxChars);
|
|
3881
4000
|
}
|
|
3882
|
-
function
|
|
3883
|
-
const maxBody = opts.maxBodyChars ??
|
|
4001
|
+
function toMemoryNode3(entry, projectId, opts = {}) {
|
|
4002
|
+
const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS5;
|
|
3884
4003
|
const titleLine = entry.command.split(/\r?\n/)[0] ?? entry.command;
|
|
3885
4004
|
return {
|
|
3886
4005
|
id: makeNodeId(projectId, "shell_command", entry.naturalKey),
|
|
@@ -3888,8 +4007,8 @@ function toMemoryNode2(entry, projectId, opts = {}) {
|
|
|
3888
4007
|
projectId,
|
|
3889
4008
|
ts: entry.ts,
|
|
3890
4009
|
source: `shell:${entry.shell}`,
|
|
3891
|
-
title: truncate(titleLine,
|
|
3892
|
-
body:
|
|
4010
|
+
title: truncate(titleLine, MAX_TITLE_CHARS7),
|
|
4011
|
+
body: renderBody2(entry, maxBody),
|
|
3893
4012
|
files: [],
|
|
3894
4013
|
signal: scoreShellCommand(entry),
|
|
3895
4014
|
provenance: "observed",
|
|
@@ -3904,7 +4023,7 @@ function toMemoryNode2(entry, projectId, opts = {}) {
|
|
|
3904
4023
|
};
|
|
3905
4024
|
}
|
|
3906
4025
|
function collectShellHistory(entries, projectId, opts = {}) {
|
|
3907
|
-
return entries.map((entry) =>
|
|
4026
|
+
return entries.map((entry) => toMemoryNode3(entry, projectId, opts));
|
|
3908
4027
|
}
|
|
3909
4028
|
|
|
3910
4029
|
// src/conversation/claude-code-reader.ts
|
|
@@ -4036,6 +4155,112 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
4036
4155
|
return { files, unreadable };
|
|
4037
4156
|
}
|
|
4038
4157
|
|
|
4158
|
+
// src/github/read.ts
|
|
4159
|
+
import { execFile as execFile2 } from "child_process";
|
|
4160
|
+
import { promisify as promisify2 } from "util";
|
|
4161
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
4162
|
+
var GithubUnavailableError = class extends Error {
|
|
4163
|
+
constructor(message, cause) {
|
|
4164
|
+
super(message);
|
|
4165
|
+
this.cause = cause;
|
|
4166
|
+
this.name = "GithubUnavailableError";
|
|
4167
|
+
}
|
|
4168
|
+
cause;
|
|
4169
|
+
};
|
|
4170
|
+
function toGithubError(err) {
|
|
4171
|
+
const code = err?.code;
|
|
4172
|
+
if (code === "ENOENT") {
|
|
4173
|
+
return new GithubUnavailableError(
|
|
4174
|
+
"gh CLI not found on PATH -- install it from https://cli.github.com and run `gh auth login`",
|
|
4175
|
+
err
|
|
4176
|
+
);
|
|
4177
|
+
}
|
|
4178
|
+
const stderr = String(err?.stderr ?? "");
|
|
4179
|
+
if (/auth login|not logged into|401/i.test(stderr)) {
|
|
4180
|
+
return new GithubUnavailableError("gh CLI is not authenticated -- run `gh auth login`", err);
|
|
4181
|
+
}
|
|
4182
|
+
const detail = stderr.trim().split("\n")[0] || (err instanceof Error ? err.message : String(err));
|
|
4183
|
+
return new GithubUnavailableError(`gh api call failed: ${detail}`, err);
|
|
4184
|
+
}
|
|
4185
|
+
function parseGithubSlug(originUrl) {
|
|
4186
|
+
if (!originUrl) return null;
|
|
4187
|
+
const match = originUrl.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/i);
|
|
4188
|
+
if (!match) return null;
|
|
4189
|
+
return `${match[1]}/${match[2]}`;
|
|
4190
|
+
}
|
|
4191
|
+
var GhCliProvider = class {
|
|
4192
|
+
constructor(repoSlug, opts = {}) {
|
|
4193
|
+
this.repoSlug = repoSlug;
|
|
4194
|
+
this.opts = opts;
|
|
4195
|
+
}
|
|
4196
|
+
repoSlug;
|
|
4197
|
+
opts;
|
|
4198
|
+
async listThreads(since) {
|
|
4199
|
+
const maxThreads = this.opts.maxThreads ?? 100;
|
|
4200
|
+
const perPage = Math.min(maxThreads, 100);
|
|
4201
|
+
const params = new URLSearchParams({
|
|
4202
|
+
state: "all",
|
|
4203
|
+
sort: "updated",
|
|
4204
|
+
direction: "desc",
|
|
4205
|
+
per_page: String(perPage)
|
|
4206
|
+
});
|
|
4207
|
+
if (since) params.set("since", since);
|
|
4208
|
+
let items;
|
|
4209
|
+
try {
|
|
4210
|
+
const { stdout } = await execFileAsync2("gh", ["api", `repos/${this.repoSlug}/issues?${params}`], {
|
|
4211
|
+
maxBuffer: 16 * 1024 * 1024
|
|
4212
|
+
});
|
|
4213
|
+
items = JSON.parse(stdout);
|
|
4214
|
+
} catch (err) {
|
|
4215
|
+
throw toGithubError(err);
|
|
4216
|
+
}
|
|
4217
|
+
const bounded = items.slice(0, maxThreads);
|
|
4218
|
+
const threads = [];
|
|
4219
|
+
for (const item of bounded) {
|
|
4220
|
+
const comments = item.comments > 0 ? await this.fetchComments(item.number) : [];
|
|
4221
|
+
threads.push({
|
|
4222
|
+
number: item.number,
|
|
4223
|
+
type: item.pull_request ? "pull_request" : "issue",
|
|
4224
|
+
title: item.title,
|
|
4225
|
+
body: item.body ?? "",
|
|
4226
|
+
author: item.user?.login ?? "unknown",
|
|
4227
|
+
state: item.state,
|
|
4228
|
+
merged: item.pull_request?.merged_at != null,
|
|
4229
|
+
labels: item.labels.map((l) => typeof l === "string" ? l : l.name),
|
|
4230
|
+
createdAt: item.created_at,
|
|
4231
|
+
updatedAt: item.updated_at,
|
|
4232
|
+
url: item.html_url,
|
|
4233
|
+
comments
|
|
4234
|
+
});
|
|
4235
|
+
}
|
|
4236
|
+
return threads;
|
|
4237
|
+
}
|
|
4238
|
+
/**
|
|
4239
|
+
* Fails soft per thread, unlike `listThreads` itself: one comment fetch
|
|
4240
|
+
* failing (a deleted comment, a transient network blip) shouldn't drop the
|
|
4241
|
+
* whole sync the way an unreachable `gh`/no auth does. The thread still
|
|
4242
|
+
* gets ingested, just without that page of discussion this run.
|
|
4243
|
+
*/
|
|
4244
|
+
async fetchComments(number) {
|
|
4245
|
+
const maxComments = this.opts.maxCommentsPerThread ?? 100;
|
|
4246
|
+
try {
|
|
4247
|
+
const { stdout } = await execFileAsync2(
|
|
4248
|
+
"gh",
|
|
4249
|
+
["api", `repos/${this.repoSlug}/issues/${number}/comments?per_page=${Math.min(maxComments, 100)}`],
|
|
4250
|
+
{ maxBuffer: 16 * 1024 * 1024 }
|
|
4251
|
+
);
|
|
4252
|
+
const items = JSON.parse(stdout);
|
|
4253
|
+
return items.slice(0, maxComments).map((c) => ({
|
|
4254
|
+
author: c.user?.login ?? "unknown",
|
|
4255
|
+
body: c.body ?? "",
|
|
4256
|
+
createdAt: c.created_at
|
|
4257
|
+
}));
|
|
4258
|
+
} catch {
|
|
4259
|
+
return [];
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
};
|
|
4263
|
+
|
|
4039
4264
|
// src/slm/contradiction.ts
|
|
4040
4265
|
var MAX_BODY_CHARS = 1500;
|
|
4041
4266
|
var MAX_REASON_CHARS = 200;
|
|
@@ -5190,6 +5415,38 @@ async function syncDocs(store, projectId, repoRoot, config, log) {
|
|
|
5190
5415
|
}
|
|
5191
5416
|
return { totals, seen: nodes.length };
|
|
5192
5417
|
}
|
|
5418
|
+
var GITHUB_SOURCE = "github";
|
|
5419
|
+
async function syncGithub(store, projectId, opts, repo, config, log) {
|
|
5420
|
+
const totals = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
|
|
5421
|
+
const enabled = opts.githubOverride ?? config.sources.github.enabled;
|
|
5422
|
+
if (!enabled) return { totals, seen: 0 };
|
|
5423
|
+
const slug = parseGithubSlug(repo.originUrl);
|
|
5424
|
+
if (!slug) {
|
|
5425
|
+
log(`${pc7.dim("github")} no github.com remote found -- skipped`);
|
|
5426
|
+
return { totals, seen: 0 };
|
|
5427
|
+
}
|
|
5428
|
+
const provider = opts.githubProvider ?? new GhCliProvider(slug, {
|
|
5429
|
+
maxThreads: config.sources.github.maxThreads,
|
|
5430
|
+
maxCommentsPerThread: config.sources.github.maxCommentsPerThread
|
|
5431
|
+
});
|
|
5432
|
+
const cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, GITHUB_SOURCE);
|
|
5433
|
+
const requestedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5434
|
+
let threads;
|
|
5435
|
+
try {
|
|
5436
|
+
threads = await provider.listThreads(cursor);
|
|
5437
|
+
} catch (err) {
|
|
5438
|
+
if (err instanceof GithubUnavailableError) {
|
|
5439
|
+
log(`${pc7.dim("github")} ${err.message} -- skipped`);
|
|
5440
|
+
return { totals, seen: 0 };
|
|
5441
|
+
}
|
|
5442
|
+
throw err;
|
|
5443
|
+
}
|
|
5444
|
+
const nodes = collectGithubThreads(threads, projectId, { maxBodyChars: config.limits.maxBodyChars });
|
|
5445
|
+
if (nodes.length > 0) addStats(totals, store.upsertNodes(nodes));
|
|
5446
|
+
store.setSyncCursor(projectId, GITHUB_SOURCE, requestedAt);
|
|
5447
|
+
log(` ${pc7.dim(`${GITHUB_SOURCE}: ${nodes.length} thread(s) read from ${slug}`)}`);
|
|
5448
|
+
return { totals, seen: nodes.length };
|
|
5449
|
+
}
|
|
5193
5450
|
async function syncStructure(store, projectId, repoRoot, config, log) {
|
|
5194
5451
|
if (!config.sources.structure.enabled) {
|
|
5195
5452
|
log(`${pc7.dim("structure")} disabled in config`);
|
|
@@ -5312,6 +5569,7 @@ async function runSync(opts) {
|
|
|
5312
5569
|
const conversation = syncConversation(store, projectId, turns, config, log, opts.conversationOverride);
|
|
5313
5570
|
const sessions = await syncSessions(store, projectId, turns, config, log);
|
|
5314
5571
|
const docs = await syncDocs(store, projectId, repo.root, config, log);
|
|
5572
|
+
const github = await syncGithub(store, projectId, opts, repo, config, log);
|
|
5315
5573
|
const structure = await syncStructure(store, projectId, repo.root, config, log);
|
|
5316
5574
|
let embedLine = "";
|
|
5317
5575
|
let embeddingAvailable = false;
|
|
@@ -5354,6 +5612,7 @@ async function runSync(opts) {
|
|
|
5354
5612
|
addStats(totals, conversation.totals);
|
|
5355
5613
|
addStats(totals, sessions.totals);
|
|
5356
5614
|
addStats(totals, docs.totals);
|
|
5615
|
+
addStats(totals, github.totals);
|
|
5357
5616
|
const stats2 = store.stats(projectId);
|
|
5358
5617
|
const elapsed = ((Date.now() - started) / 1e3).toFixed(2);
|
|
5359
5618
|
const conversationPart = conversationEnabled ? `, ${conversation.seen} conversation exchange(s)` : "";
|
|
@@ -5361,10 +5620,12 @@ async function runSync(opts) {
|
|
|
5361
5620
|
const docsPart = config.sources.docs.enabled ? `, ${docs.seen} doc section(s)` : "";
|
|
5362
5621
|
const diffPart = config.sources.diff.enabled ? `, ${diffs.seen} file diff(s)` : "";
|
|
5363
5622
|
const structurePart = config.sources.structure.enabled ? `, ${structure.edges} import edge(s)` : "";
|
|
5623
|
+
const githubEnabled = opts.githubOverride ?? config.sources.github.enabled;
|
|
5624
|
+
const githubPart = githubEnabled ? `, ${github.seen} github thread(s)` : "";
|
|
5364
5625
|
const deniedPart = totals.denied > 0 ? ` ${pc7.red(`-${totals.denied} denied`)}` : "";
|
|
5365
5626
|
out(
|
|
5366
5627
|
[
|
|
5367
|
-
`${pc7.green("synced")} ${git2.seen} commit(s)${diffPart}, ${shell.seen} shell entr${shell.seen === 1 ? "y" : "ies"}${conversationPart}${sessionPart}${docsPart}${structurePart} in ${elapsed}s`,
|
|
5628
|
+
`${pc7.green("synced")} ${git2.seen} commit(s)${diffPart}, ${shell.seen} shell entr${shell.seen === 1 ? "y" : "ies"}${conversationPart}${sessionPart}${docsPart}${githubPart}${structurePart} in ${elapsed}s`,
|
|
5368
5629
|
` ${pc7.green(`+${totals.inserted} new`)} ${pc7.yellow(`~${totals.updated} updated`)} ${pc7.dim(`=${totals.unchanged} unchanged`)}${deniedPart}`,
|
|
5369
5630
|
` ${pc7.dim(`${stats2.total} node(s) total across ${stats2.distinctFiles} file path(s)`)}`,
|
|
5370
5631
|
""
|
|
@@ -5482,7 +5743,7 @@ function createServer() {
|
|
|
5482
5743
|
"search_memory",
|
|
5483
5744
|
{
|
|
5484
5745
|
title: "Search remembered project history",
|
|
5485
|
-
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
|
|
5746
|
+
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, per-session summaries, and github.com issue/PR threads. Returns a token-budgeted, ranked context block -- not raw search results.",
|
|
5486
5747
|
inputSchema: {
|
|
5487
5748
|
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5488
5749
|
query: z4.string().describe("Free-text question or search terms"),
|
|
@@ -5515,7 +5776,7 @@ function createServer() {
|
|
|
5515
5776
|
"sync_project",
|
|
5516
5777
|
{
|
|
5517
5778
|
title: "Sync remembered history",
|
|
5518
|
-
description: "Ingest new git, diff, shell, docs and (if enabled) conversation history for a NexusMem-tracked repository into its local database. Pass pruneSource or pruneStaleShell instead to delete a dead source's nodes (e.g. the pre-hook shell scrape) rather than syncing -- dry-run unless yes is also true, since this is an irreversible full wipe of that source.",
|
|
5779
|
+
description: "Ingest new git, diff, shell, docs and (if enabled) conversation and github.com issue/PR history for a NexusMem-tracked repository into its local database. Pass pruneSource or pruneStaleShell instead to delete a dead source's nodes (e.g. the pre-hook shell scrape) rather than syncing -- dry-run unless yes is also true, since this is an irreversible full wipe of that source.",
|
|
5519
5780
|
inputSchema: {
|
|
5520
5781
|
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5521
5782
|
pruneSource: z4.string().optional().describe('Delete every node from this exact source (e.g. "shell:pwsh") instead of syncing'),
|
|
@@ -5549,7 +5810,7 @@ function createServer() {
|
|
|
5549
5810
|
"list_recent_memory",
|
|
5550
5811
|
{
|
|
5551
5812
|
title: "List recently remembered items",
|
|
5552
|
-
description: "List the most recently remembered items for a NexusMem-tracked repository -- git commits, code diffs, shell commands, tracked docs, and (if enabled) conversation transcripts
|
|
5813
|
+
description: "List the most recently remembered items for a NexusMem-tracked repository -- git commits, code diffs, shell commands, tracked docs, and (if enabled) conversation transcripts, session summaries, and github.com issue/PR threads -- newest first. Chronological, not relevance-ranked: use search_memory instead for a specific question.",
|
|
5553
5814
|
inputSchema: {
|
|
5554
5815
|
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5555
5816
|
limit: z4.number().int().positive().optional().describe("Max items to return, newest first. Default 20.")
|
|
@@ -5655,7 +5916,7 @@ ${pc8.dim("-".repeat(40))}
|
|
|
5655
5916
|
out(` ${pc8.bold(risk.path)}
|
|
5656
5917
|
`);
|
|
5657
5918
|
if (risk.unresolvedFailures.length > 0) {
|
|
5658
|
-
out(` ${pc8.yellow("WARN")}
|
|
5919
|
+
out(` ${pc8.yellow("WARN")} commands naming this file failed, still unresolved (${risk.unresolvedFailures.length}):
|
|
5659
5920
|
`);
|
|
5660
5921
|
for (const f of risk.unresolvedFailures.slice(0, 3)) {
|
|
5661
5922
|
out(` ${pc8.dim(`${f.ts.slice(0, 10)} ${f.command.slice(0, 90)}`)}
|
|
@@ -5667,7 +5928,7 @@ ${pc8.dim("-".repeat(40))}
|
|
|
5667
5928
|
}
|
|
5668
5929
|
}
|
|
5669
5930
|
if (risk.commitsRecent >= HIGH_CHURN_THRESHOLD) {
|
|
5670
|
-
out(` ${pc8.
|
|
5931
|
+
out(` ${pc8.dim("note")} high churn (untuned heuristic): ${risk.commitsRecent} commits touched this file recently
|
|
5671
5932
|
`);
|
|
5672
5933
|
}
|
|
5673
5934
|
out("\n");
|
|
@@ -5822,6 +6083,7 @@ var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
|
5822
6083
|
var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
5823
6084
|
var DOCS_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
5824
6085
|
var DIFF_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
6086
|
+
var GITHUB_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
5825
6087
|
function signalBand(signal, bands) {
|
|
5826
6088
|
if (signal >= bands.high) return "high";
|
|
5827
6089
|
if (signal >= bands.medium) return "medium";
|
|
@@ -6029,14 +6291,57 @@ function formatNode4(node) {
|
|
|
6029
6291
|
].join(" ");
|
|
6030
6292
|
}
|
|
6031
6293
|
|
|
6032
|
-
// src/cli/commands/scan-
|
|
6294
|
+
// src/cli/commands/scan-github.ts
|
|
6033
6295
|
import pc16 from "picocolors";
|
|
6296
|
+
async function runScanGithub(opts) {
|
|
6297
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
6298
|
+
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
6299
|
+
const slug = parseGithubSlug(repo.originUrl);
|
|
6300
|
+
if (!slug) {
|
|
6301
|
+
process.stderr.write(`${pc16.yellow("no github.com remote found for this repository")}
|
|
6302
|
+
`);
|
|
6303
|
+
return 0;
|
|
6304
|
+
}
|
|
6305
|
+
const provider = opts.provider ?? new GhCliProvider(slug);
|
|
6306
|
+
let threads;
|
|
6307
|
+
try {
|
|
6308
|
+
threads = await provider.listThreads(null);
|
|
6309
|
+
} catch (err) {
|
|
6310
|
+
if (err instanceof GithubUnavailableError) {
|
|
6311
|
+
process.stderr.write(`${pc16.yellow(err.message)}
|
|
6312
|
+
`);
|
|
6313
|
+
return 0;
|
|
6314
|
+
}
|
|
6315
|
+
throw err;
|
|
6316
|
+
}
|
|
6317
|
+
const nodes = collectGithubThreads(threads, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
6318
|
+
if (opts.json) {
|
|
6319
|
+
process.stdout.write(`${JSON.stringify(nodes, null, 2)}
|
|
6320
|
+
`);
|
|
6321
|
+
return 0;
|
|
6322
|
+
}
|
|
6323
|
+
for (const node of nodes) process.stdout.write(`${formatNode5(node)}
|
|
6324
|
+
`);
|
|
6325
|
+
const approxTotal = approxTotalTokens(nodes);
|
|
6326
|
+
process.stderr.write(
|
|
6327
|
+
`
|
|
6328
|
+
${pc16.bold(String(nodes.length))} thread(s) from ${pc16.dim(slug)} above threshold ${pc16.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
6329
|
+
`
|
|
6330
|
+
);
|
|
6331
|
+
return 0;
|
|
6332
|
+
}
|
|
6333
|
+
function formatNode5(node) {
|
|
6334
|
+
return [formatSignal(node.signal, GITHUB_SIGNAL_BANDS), node.title].join(" ");
|
|
6335
|
+
}
|
|
6336
|
+
|
|
6337
|
+
// src/cli/commands/scan-session.ts
|
|
6338
|
+
import pc17 from "picocolors";
|
|
6034
6339
|
async function runScanSession(opts) {
|
|
6035
6340
|
const repo = await readRepoInfo(opts.cwd);
|
|
6036
6341
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
6037
6342
|
const turns = await collectClaudeCodeTranscripts(repo.root);
|
|
6038
6343
|
if (turns.length === 0) {
|
|
6039
|
-
process.stderr.write(`${
|
|
6344
|
+
process.stderr.write(`${pc17.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
6040
6345
|
`);
|
|
6041
6346
|
return 0;
|
|
6042
6347
|
}
|
|
@@ -6044,7 +6349,7 @@ async function runScanSession(opts) {
|
|
|
6044
6349
|
const settled = selectSettledSessions(sessions, opts.settleMinutes);
|
|
6045
6350
|
if (!opts.json) {
|
|
6046
6351
|
process.stderr.write(
|
|
6047
|
-
`${
|
|
6352
|
+
`${pc17.dim("sessions")} ${sessions.length} found, ${settled.length} settled (quiet ${opts.settleMinutes}m+)
|
|
6048
6353
|
|
|
6049
6354
|
`
|
|
6050
6355
|
);
|
|
@@ -6070,7 +6375,7 @@ async function runScanSession(opts) {
|
|
|
6070
6375
|
}
|
|
6071
6376
|
for (const preview of previews) {
|
|
6072
6377
|
process.stdout.write(
|
|
6073
|
-
`${
|
|
6378
|
+
`${pc17.bold(preview.sessionKey)} ${preview.startedAt.slice(0, 16).replace("T", " ")} ${preview.includedTurns}/${preview.turns} turn(s), ${preview.promptChars} chars
|
|
6074
6379
|
${preview.prompt}
|
|
6075
6380
|
|
|
6076
6381
|
`
|
|
@@ -6082,7 +6387,7 @@ ${preview.prompt}
|
|
|
6082
6387
|
settleMinutes: opts.settleMinutes,
|
|
6083
6388
|
maxSessions: opts.maxSessions,
|
|
6084
6389
|
onProgress: (done, total) => {
|
|
6085
|
-
if (!opts.json) process.stderr.write(` ${
|
|
6390
|
+
if (!opts.json) process.stderr.write(` ${pc17.dim(`summarizing ${done}/${total}`)}
|
|
6086
6391
|
`);
|
|
6087
6392
|
}
|
|
6088
6393
|
});
|
|
@@ -6092,21 +6397,21 @@ ${preview.prompt}
|
|
|
6092
6397
|
return 0;
|
|
6093
6398
|
}
|
|
6094
6399
|
for (const node of result.nodes) {
|
|
6095
|
-
process.stdout.write(`${
|
|
6096
|
-
${
|
|
6400
|
+
process.stdout.write(`${pc17.bold(node.title)}
|
|
6401
|
+
${pc17.dim(node.ts.slice(0, 16).replace("T", " "))}
|
|
6097
6402
|
${node.body}
|
|
6098
6403
|
|
|
6099
6404
|
`);
|
|
6100
6405
|
}
|
|
6101
6406
|
if (result.providerUnavailable) {
|
|
6102
6407
|
process.stderr.write(
|
|
6103
|
-
`${
|
|
6408
|
+
`${pc17.yellow("model unavailable")} -- is Ollama running with \`${opts.model}\` pulled? (\`ollama pull ${opts.model}\`)
|
|
6104
6409
|
`
|
|
6105
6410
|
);
|
|
6106
6411
|
return 0;
|
|
6107
6412
|
}
|
|
6108
6413
|
process.stderr.write(
|
|
6109
|
-
`${
|
|
6414
|
+
`${pc17.bold(String(result.nodes.length))} summarized` + (result.failed > 0 ? `, ${pc17.yellow(`${result.failed} failed`)}` : "") + ` ${pc17.dim(`(model ${opts.model})`)}
|
|
6110
6415
|
`
|
|
6111
6416
|
);
|
|
6112
6417
|
return 0;
|
|
@@ -6114,16 +6419,16 @@ ${node.body}
|
|
|
6114
6419
|
var SCAN_SESSION_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
6115
6420
|
|
|
6116
6421
|
// src/cli/commands/scan-shell.ts
|
|
6117
|
-
import
|
|
6422
|
+
import pc18 from "picocolors";
|
|
6118
6423
|
async function runScanShell(opts) {
|
|
6119
6424
|
const repo = await readRepoInfo(opts.cwd);
|
|
6120
6425
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
6121
6426
|
const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
|
|
6122
6427
|
if (!opts.json) {
|
|
6123
6428
|
process.stderr.write(
|
|
6124
|
-
results.length ? `${
|
|
6429
|
+
results.length ? `${pc18.dim("sources found")} ${results.map((r) => r.name).join(", ")}
|
|
6125
6430
|
|
|
6126
|
-
` : `${
|
|
6431
|
+
` : `${pc18.yellow("no shell history source found on this machine")}
|
|
6127
6432
|
`
|
|
6128
6433
|
);
|
|
6129
6434
|
}
|
|
@@ -6132,9 +6437,9 @@ async function runScanShell(opts) {
|
|
|
6132
6437
|
const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
6133
6438
|
allNodes.push(...nodes);
|
|
6134
6439
|
if (!opts.json) {
|
|
6135
|
-
process.stdout.write(`${
|
|
6440
|
+
process.stdout.write(`${pc18.bold(`shell:${result.name}`)} ${pc18.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
|
|
6136
6441
|
`);
|
|
6137
|
-
for (const node of nodes) process.stdout.write(`${
|
|
6442
|
+
for (const node of nodes) process.stdout.write(`${formatNode6(node)}
|
|
6138
6443
|
`);
|
|
6139
6444
|
process.stdout.write("\n");
|
|
6140
6445
|
}
|
|
@@ -6145,19 +6450,19 @@ async function runScanShell(opts) {
|
|
|
6145
6450
|
return 0;
|
|
6146
6451
|
}
|
|
6147
6452
|
const approxTotal = approxTotalTokens(allNodes);
|
|
6148
|
-
process.stderr.write(`${
|
|
6453
|
+
process.stderr.write(`${pc18.bold(String(allNodes.length))} node(s) total ${pc18.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
6149
6454
|
`);
|
|
6150
6455
|
return 0;
|
|
6151
6456
|
}
|
|
6152
|
-
function
|
|
6153
|
-
const approx = node.meta.tsApprox ?
|
|
6457
|
+
function formatNode6(node) {
|
|
6458
|
+
const approx = node.meta.tsApprox ? pc18.dim("~") : " ";
|
|
6154
6459
|
const exit = node.meta.exitCode;
|
|
6155
|
-
const exitLabel = typeof exit === "number" && exit !== 0 ?
|
|
6460
|
+
const exitLabel = typeof exit === "number" && exit !== 0 ? pc18.red(`exit ${exit}`) : "";
|
|
6156
6461
|
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
|
|
6157
6462
|
}
|
|
6158
6463
|
|
|
6159
6464
|
// src/cli/commands/scan-structure.ts
|
|
6160
|
-
import
|
|
6465
|
+
import pc19 from "picocolors";
|
|
6161
6466
|
var TRACKED_EXTENSIONS = TRACKED_PATHSPECS.map((p) => p.replace("*", "")).join("/");
|
|
6162
6467
|
async function runScanStructure(opts) {
|
|
6163
6468
|
const repo = await readRepoInfo(opts.cwd);
|
|
@@ -6168,24 +6473,24 @@ async function runScanStructure(opts) {
|
|
|
6168
6473
|
return 0;
|
|
6169
6474
|
}
|
|
6170
6475
|
if (unreadable.length > 0) {
|
|
6171
|
-
process.stderr.write(`${
|
|
6476
|
+
process.stderr.write(`${pc19.yellow("unreadable")} ${unreadable.join(", ")}
|
|
6172
6477
|
|
|
6173
6478
|
`);
|
|
6174
6479
|
}
|
|
6175
6480
|
for (const edge of edges) {
|
|
6176
|
-
process.stdout.write(`${edge.fromPath} ${
|
|
6481
|
+
process.stdout.write(`${edge.fromPath} ${pc19.dim("->")} ${edge.toPath}
|
|
6177
6482
|
`);
|
|
6178
6483
|
}
|
|
6179
6484
|
process.stderr.write(
|
|
6180
6485
|
`
|
|
6181
|
-
${
|
|
6486
|
+
${pc19.bold(String(edges.length))} edge(s) from ${filesScanned} tracked ${TRACKED_EXTENSIONS} file(s)
|
|
6182
6487
|
`
|
|
6183
6488
|
);
|
|
6184
6489
|
return 0;
|
|
6185
6490
|
}
|
|
6186
6491
|
|
|
6187
6492
|
// src/cli/commands/stale.ts
|
|
6188
|
-
import
|
|
6493
|
+
import pc20 from "picocolors";
|
|
6189
6494
|
var STALE_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
6190
6495
|
async function runStale(opts) {
|
|
6191
6496
|
const { projectId, ws } = await loadContext(opts.cwd);
|
|
@@ -6195,15 +6500,15 @@ async function runStale(opts) {
|
|
|
6195
6500
|
if (opts.dismiss) {
|
|
6196
6501
|
const dismissed = store.dismissContradictionSuggestion(projectId, opts.dismiss);
|
|
6197
6502
|
out(
|
|
6198
|
-
dismissed > 0 ? `${
|
|
6199
|
-
` : `${
|
|
6503
|
+
dismissed > 0 ? `${pc20.green("dismissed")} the contradiction suggestion for ${opts.dismiss} -- it will not resurface
|
|
6504
|
+
` : `${pc20.dim("no open contradiction suggestion")} for ${opts.dismiss} -- nothing to dismiss
|
|
6200
6505
|
`
|
|
6201
6506
|
);
|
|
6202
6507
|
return 0;
|
|
6203
6508
|
}
|
|
6204
6509
|
const candidates = store.listStaleCandidates(projectId, { minAgeDays: opts.minAgeDays, limit: opts.limit });
|
|
6205
6510
|
if (candidates.length === 0) {
|
|
6206
|
-
out(`${
|
|
6511
|
+
out(`${pc20.dim("no stale candidates")} -- no unconfirmed node older than the threshold lacks a successor
|
|
6207
6512
|
`);
|
|
6208
6513
|
return 0;
|
|
6209
6514
|
}
|
|
@@ -6225,15 +6530,15 @@ async function runStale(opts) {
|
|
|
6225
6530
|
for (const s of suggestions) byCandidateId.set(s.candidateId, s);
|
|
6226
6531
|
out(
|
|
6227
6532
|
[
|
|
6228
|
-
`${
|
|
6533
|
+
`${pc20.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
|
|
6229
6534
|
...candidates.map((c) => {
|
|
6230
|
-
const line = ` ${
|
|
6535
|
+
const line = ` ${pc20.dim(c.id)} ${pc20.yellow(`${c.ageDays}d old`)} [${c.kind}] ${c.title}`;
|
|
6231
6536
|
const hit = byCandidateId.get(c.id);
|
|
6232
6537
|
return hit ? `${line}
|
|
6233
|
-
${
|
|
6538
|
+
${pc20.red("likely superseded by")} ${pc20.dim(hit.againstId)} ${hit.againstTitle} -- ${hit.reason}` : line;
|
|
6234
6539
|
}),
|
|
6235
6540
|
"",
|
|
6236
|
-
`run ${
|
|
6541
|
+
`run ${pc20.bold("nexusmem mark-stale <id> --supersedes <newId>")} on any that are actually wrong`
|
|
6237
6542
|
].join("\n").concat("\n")
|
|
6238
6543
|
);
|
|
6239
6544
|
return 0;
|
|
@@ -6245,7 +6550,7 @@ async function runStale(opts) {
|
|
|
6245
6550
|
// src/cli/commands/status.ts
|
|
6246
6551
|
import { basename as basename4 } from "path";
|
|
6247
6552
|
import { statSync } from "fs";
|
|
6248
|
-
import
|
|
6553
|
+
import pc21 from "picocolors";
|
|
6249
6554
|
function daySpan(oldest, newest) {
|
|
6250
6555
|
const oldestDay = Date.parse(oldest.slice(0, 10));
|
|
6251
6556
|
const newestDay = Date.parse(newest.slice(0, 10));
|
|
@@ -6305,34 +6610,34 @@ async function runStatus(opts) {
|
|
|
6305
6610
|
const flaggedCount = store.countContradictionSuggestions(projectId);
|
|
6306
6611
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
6307
6612
|
const kinds = Object.entries(stats2.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
6308
|
-
const staleProjectWarning = otherProjectIds.length ? `${
|
|
6613
|
+
const staleProjectWarning = otherProjectIds.length ? `${pc21.yellow("stale ")} ${otherProjectIds.length} prior project ${otherProjectIds.length === 1 ? "identity holds" : "identities hold"} ${otherProjectNodes} node(s) \u2014 run ${pc21.bold(
|
|
6309
6614
|
"nexusmem sync --prune-source <name>"
|
|
6310
6615
|
)} to remove stale source data` : "";
|
|
6311
6616
|
out(
|
|
6312
6617
|
[
|
|
6313
|
-
`${
|
|
6314
|
-
`${
|
|
6315
|
-
`${
|
|
6316
|
-
`${
|
|
6317
|
-
`${
|
|
6618
|
+
`${pc21.dim("repo ")} ${repo.root}`,
|
|
6619
|
+
`${pc21.dim("branch ")} ${repo.branch ?? pc21.yellow("(detached)")}`,
|
|
6620
|
+
`${pc21.dim("project ")} ${pc21.cyan(projectId)}`,
|
|
6621
|
+
`${pc21.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc21.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
|
|
6622
|
+
`${pc21.dim("database")} ${ws.dbPath} ${pc21.dim(`(${humanBytes(dbBytes)})`)}`,
|
|
6318
6623
|
staleProjectWarning,
|
|
6319
6624
|
"",
|
|
6320
|
-
`${
|
|
6625
|
+
`${pc21.bold(String(stats2.total))} node(s)${stats2.total ? ` ${pc21.dim(`${stats2.oldest?.slice(0, 10)} .. ${stats2.newest?.slice(0, 10)}`)}` : ""}`,
|
|
6321
6626
|
...kinds,
|
|
6322
|
-
stats2.total ? ` ${
|
|
6627
|
+
stats2.total ? ` ${pc21.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
|
|
6323
6628
|
"",
|
|
6324
|
-
sources.length ?
|
|
6629
|
+
sources.length ? pc21.dim("sources") : pc21.yellow("no sources synced yet"),
|
|
6325
6630
|
...sources.map((s) => {
|
|
6326
6631
|
const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
6327
6632
|
const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
|
|
6328
|
-
return ` ${s.source.padEnd(14)} ${
|
|
6633
|
+
return ` ${s.source.padEnd(14)} ${pc21.dim(`last run ${when}`)} ${pc21.dim(`cursor ${cursorLabel}`)}`;
|
|
6329
6634
|
}),
|
|
6330
6635
|
"",
|
|
6331
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
6332
|
-
chains.failuresTotal ? `${
|
|
6333
|
-
structure.edges ? `${
|
|
6334
|
-
staleCount ? `${
|
|
6335
|
-
flaggedCount ? `${
|
|
6636
|
+
gitCursor && gitCursor !== repo.head ? `${pc21.yellow("git behind HEAD")} \u2014 run ${pc21.bold("nexusmem sync")}` : "",
|
|
6637
|
+
chains.failuresTotal ? `${pc21.dim("chains ")} ${pc21.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc21.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc21.bold("nexusmem sync --link-failures")} to link more` : ""}` : "",
|
|
6638
|
+
structure.edges ? `${pc21.dim("structure")} ${pc21.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : "",
|
|
6639
|
+
staleCount ? `${pc21.dim("aging ")} ${pc21.bold(String(staleCount))} unconfirmed node(s) worth a look \u2014 run ${pc21.bold("nexusmem stale")}` : "",
|
|
6640
|
+
flaggedCount ? `${pc21.dim("flagged ")} ${pc21.bold(String(flaggedCount))} likely-superseded node(s) awaiting review \u2014 run ${pc21.bold("nexusmem stale")} for detail` : ""
|
|
6336
6641
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
6337
6642
|
);
|
|
6338
6643
|
return 0;
|
|
@@ -6355,7 +6660,7 @@ function guard(run) {
|
|
|
6355
6660
|
process.exitCode = await run();
|
|
6356
6661
|
} catch (err) {
|
|
6357
6662
|
if (isExpected(err)) {
|
|
6358
|
-
process.stderr.write(`${
|
|
6663
|
+
process.stderr.write(`${pc22.red("error")} ${err.message}
|
|
6359
6664
|
`);
|
|
6360
6665
|
process.exitCode = 1;
|
|
6361
6666
|
return;
|
|
@@ -6371,7 +6676,7 @@ program.command("init").description("Create the .nexusmem workspace and database
|
|
|
6371
6676
|
() => runInit({ cwd: options.cwd, force: options.force, hook: options.hook, enableConversation: options.enableConversation })
|
|
6372
6677
|
)()
|
|
6373
6678
|
);
|
|
6374
|
-
program.command("sync").description("Ingest new history into the local database").option("-C, --cwd <path>", "repository path", process.cwd()).option("--full", "ignore the stored cursor and re-walk all history", false).option("--rebuild", "drop this project's nodes and re-ingest from scratch", false).option("--since <date>", "override the configured git cutoff, e.g. 1.year.ago").option("--shell-lines <count>", "override the configured shell tail-window size", (v) => Number.parseInt(v, 10)).option("--conversation", "force the conversation source on for this run, without persisting it to config", false).option("--no-embed", "skip the vector-embedding pass for this run").option(
|
|
6679
|
+
program.command("sync").description("Ingest new history into the local database").option("-C, --cwd <path>", "repository path", process.cwd()).option("--full", "ignore the stored cursor and re-walk all history", false).option("--rebuild", "drop this project's nodes and re-ingest from scratch", false).option("--since <date>", "override the configured git cutoff, e.g. 1.year.ago").option("--shell-lines <count>", "override the configured shell tail-window size", (v) => Number.parseInt(v, 10)).option("--conversation", "force the conversation source on for this run, without persisting it to config", false).option("--github", "force the github issue/PR source on for this run, without persisting it to config", false).option("--no-embed", "skip the vector-embedding pass for this run").option(
|
|
6375
6680
|
"--embed-limit <count>",
|
|
6376
6681
|
"stop embedding after this many nodes (default: embed everything pending)",
|
|
6377
6682
|
(v) => Number.parseInt(v, 10)
|
|
@@ -6392,6 +6697,7 @@ program.command("sync").description("Ingest new history into the local database"
|
|
|
6392
6697
|
since: options.since,
|
|
6393
6698
|
shellTailLines: options.shellLines,
|
|
6394
6699
|
conversationOverride: options.conversation ? true : void 0,
|
|
6700
|
+
githubOverride: options.github ? true : void 0,
|
|
6395
6701
|
noEmbed: !options.embed,
|
|
6396
6702
|
embedLimit: options.embedLimit,
|
|
6397
6703
|
pruneSource: options.pruneSource,
|
|
@@ -6523,6 +6829,7 @@ program.command("scan-session").description("Preview the session summaries a loc
|
|
|
6523
6829
|
)()
|
|
6524
6830
|
);
|
|
6525
6831
|
program.command("scan-docs").description("Preview the MemoryNodes tracked .md files would produce (writes nothing)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--min-signal <score>", "drop nodes below this signal", (v) => Number.parseFloat(v), 0).option("--json", "emit MemoryNodes as JSON on stdout", false).action((options) => guard(() => runScanDocs({ cwd: options.cwd, minSignal: options.minSignal, json: options.json }))());
|
|
6832
|
+
program.command("scan-github").description("Preview the MemoryNodes github.com issues/PRs would produce (writes nothing, needs `gh` installed and authenticated)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--min-signal <score>", "drop nodes below this signal", (v) => Number.parseFloat(v), 0).option("--json", "emit MemoryNodes as JSON on stdout", false).action((options) => guard(() => runScanGithub({ cwd: options.cwd, minSignal: options.minSignal, json: options.json }))());
|
|
6526
6833
|
program.command("precheck").description("Warn about staged files with unresolved past failures or high recent churn (advisory, exits 0 unless --strict)").option("-C, --cwd <path>", "repository path", process.cwd()).option("--files <paths...>", "check exactly these repo-relative paths instead of what is staged").option("--working", "check the working tree (unstaged changes) instead of what is staged for commit", false).option("--strict", "exit 1 when any file has an unresolved failure", false).option("-q, --quiet", "only print output when there is something to warn about", false).action(
|
|
6527
6834
|
(options) => guard(
|
|
6528
6835
|
() => runPrecheck({
|
|
@@ -6538,7 +6845,7 @@ program.command("scan-structure").description("Preview the JS/TS/Python/Go/Rust/
|
|
|
6538
6845
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
6539
6846
|
program.parseAsync(process.argv).catch((err) => {
|
|
6540
6847
|
const message = err instanceof Error ? err.message : String(err);
|
|
6541
|
-
process.stderr.write(`${
|
|
6848
|
+
process.stderr.write(`${pc22.red("error")} ${message}
|
|
6542
6849
|
`);
|
|
6543
6850
|
process.exitCode = 1;
|
|
6544
6851
|
});
|