nexusmem 0.9.0 → 0.10.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 +40 -1
- package/README.md +550 -529
- package/dist/cli/index.js +368 -80
- package/dist/cli/index.js.map +1 -1
- package/package.json +2 -1
package/dist/cli/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import 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),
|
|
@@ -995,6 +1015,24 @@ var V10 = `
|
|
|
995
1015
|
ALTER TABLE nodes ADD COLUMN trust_state TEXT NOT NULL DEFAULT 'candidate';
|
|
996
1016
|
CREATE INDEX idx_nodes_trust_state ON nodes (project_id, trust_state) WHERE trust_state != 'candidate';
|
|
997
1017
|
`;
|
|
1018
|
+
var V11 = `
|
|
1019
|
+
CREATE TEMP TABLE nodes_vec_stage AS
|
|
1020
|
+
SELECT v.rowid AS rowid, n.project_id AS project_id, v.embedding AS embedding
|
|
1021
|
+
FROM nodes_vec v
|
|
1022
|
+
JOIN nodes n ON n.rowid = v.rowid;
|
|
1023
|
+
|
|
1024
|
+
DROP TABLE nodes_vec;
|
|
1025
|
+
|
|
1026
|
+
CREATE VIRTUAL TABLE nodes_vec USING vec0 (
|
|
1027
|
+
project_id TEXT PARTITION KEY,
|
|
1028
|
+
embedding float[${EMBEDDING_DIM}]
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
INSERT INTO nodes_vec (rowid, project_id, embedding)
|
|
1032
|
+
SELECT rowid, project_id, embedding FROM nodes_vec_stage;
|
|
1033
|
+
|
|
1034
|
+
DROP TABLE nodes_vec_stage;
|
|
1035
|
+
`;
|
|
998
1036
|
var MIGRATIONS = [
|
|
999
1037
|
{ version: 1, up: (db) => db.exec(V1) },
|
|
1000
1038
|
{ version: 2, up: (db) => db.exec(V2) },
|
|
@@ -1005,7 +1043,8 @@ var MIGRATIONS = [
|
|
|
1005
1043
|
{ version: 7, up: (db) => db.exec(V7) },
|
|
1006
1044
|
{ version: 8, up: (db) => db.exec(V8) },
|
|
1007
1045
|
{ version: 9, up: (db) => db.exec(V9) },
|
|
1008
|
-
{ version: 10, up: (db) => db.exec(V10) }
|
|
1046
|
+
{ version: 10, up: (db) => db.exec(V10) },
|
|
1047
|
+
{ version: 11, up: (db) => db.exec(V11) }
|
|
1009
1048
|
];
|
|
1010
1049
|
var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
1011
1050
|
function currentSchemaVersion(db) {
|
|
@@ -1104,6 +1143,8 @@ function defaultProvenanceForKind(kind) {
|
|
|
1104
1143
|
return "recorded";
|
|
1105
1144
|
case "session_summary":
|
|
1106
1145
|
return "derived";
|
|
1146
|
+
case "github_thread":
|
|
1147
|
+
return "recorded";
|
|
1107
1148
|
}
|
|
1108
1149
|
}
|
|
1109
1150
|
|
|
@@ -1467,24 +1508,24 @@ function getEmbedding(db, nodeId) {
|
|
|
1467
1508
|
if (!row) return null;
|
|
1468
1509
|
return new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
|
|
1469
1510
|
}
|
|
1470
|
-
function upsertEmbedding(db, rowid, embedding) {
|
|
1471
|
-
db.prepare("INSERT OR REPLACE INTO nodes_vec (rowid, embedding) VALUES (?, ?)").run(BigInt(rowid), embedding);
|
|
1511
|
+
function upsertEmbedding(db, rowid, projectId, embedding) {
|
|
1512
|
+
db.prepare("INSERT OR REPLACE INTO nodes_vec (rowid, project_id, embedding) VALUES (?, ?, ?)").run(BigInt(rowid), projectId, embedding);
|
|
1472
1513
|
}
|
|
1473
1514
|
function dropAllEmbeddings(db) {
|
|
1474
1515
|
return db.prepare("DELETE FROM nodes_vec").run().changes;
|
|
1475
1516
|
}
|
|
1476
1517
|
function vectorSearch(db, projectId, embedding, limit = 20, opts = {}) {
|
|
1477
|
-
const overfetch = Math.max(limit * 8, 50);
|
|
1478
1518
|
const asOfEpoch = opts.asOfEpoch ?? null;
|
|
1519
|
+
const k = asOfEpoch === null ? limit : Math.max(limit * 8, 50);
|
|
1479
1520
|
return db.prepare(
|
|
1480
1521
|
`SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, n.trust_state AS trustState, v.distance AS distance
|
|
1481
1522
|
FROM nodes_vec v
|
|
1482
1523
|
JOIN nodes n ON n.rowid = v.rowid
|
|
1483
|
-
WHERE v.embedding MATCH ? AND k = ? AND
|
|
1524
|
+
WHERE v.embedding MATCH ? AND k = ? AND v.project_id = ?
|
|
1484
1525
|
AND (? IS NULL OR n.created_at <= ?)
|
|
1485
1526
|
ORDER BY v.distance
|
|
1486
1527
|
LIMIT ?`
|
|
1487
|
-
).all(embedding,
|
|
1528
|
+
).all(embedding, k, projectId, asOfEpoch, asOfEpoch, limit);
|
|
1488
1529
|
}
|
|
1489
1530
|
|
|
1490
1531
|
// src/store/fts.ts
|
|
@@ -1755,8 +1796,8 @@ var MemoryStore = class _MemoryStore {
|
|
|
1755
1796
|
countNodesNeedingEmbedding(projectId) {
|
|
1756
1797
|
return countNodesNeedingEmbedding(this.db, projectId);
|
|
1757
1798
|
}
|
|
1758
|
-
upsertEmbedding(rowid, embedding) {
|
|
1759
|
-
upsertEmbedding(this.db, rowid, embedding);
|
|
1799
|
+
upsertEmbedding(rowid, projectId, embedding) {
|
|
1800
|
+
upsertEmbedding(this.db, rowid, projectId, embedding);
|
|
1760
1801
|
}
|
|
1761
1802
|
/** The stored vector for one node, or null if it has not been embedded yet. */
|
|
1762
1803
|
getEmbedding(nodeId) {
|
|
@@ -2336,7 +2377,7 @@ var DEFAULT_SUMMARY_CHARS = 320;
|
|
|
2336
2377
|
var NODE_OVERHEAD_TOKENS = 8;
|
|
2337
2378
|
var CONVERSATION_ANSWER_MARKER = "\n\nA: ";
|
|
2338
2379
|
var MAX_PER_FAMILY = 2;
|
|
2339
|
-
var CHUNKED_KINDS = /* @__PURE__ */ new Set(["conversation_turn", "doc_section"]);
|
|
2380
|
+
var CHUNKED_KINDS = /* @__PURE__ */ new Set(["conversation_turn", "doc_section", "code_diff"]);
|
|
2340
2381
|
var HUNK_BOUNDARY = "\n@@ ";
|
|
2341
2382
|
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
2342
2383
|
"the",
|
|
@@ -2653,7 +2694,7 @@ var HALF_LIFE_RATIO = {
|
|
|
2653
2694
|
};
|
|
2654
2695
|
var SUPERSEDED_PENALTY = 0.5;
|
|
2655
2696
|
var REJECTED_TRUST_PENALTY = 0.3;
|
|
2656
|
-
var MAX_PRIOR_OVERTURN = 2;
|
|
2697
|
+
var MAX_PRIOR_OVERTURN = 2.4;
|
|
2657
2698
|
var PRIOR_COUNT = 2;
|
|
2658
2699
|
var PER_PRIOR_OVERTURN = MAX_PRIOR_OVERTURN ** (1 / PRIOR_COUNT);
|
|
2659
2700
|
var SIGNAL_EXPONENT = Math.log(PER_PRIOR_OVERTURN) / Math.log(1 / SIGNAL_FLOOR);
|
|
@@ -3476,11 +3517,13 @@ function isGeneratedPath(path) {
|
|
|
3476
3517
|
return GENERATED_PATHS.some((re) => re.test(path));
|
|
3477
3518
|
}
|
|
3478
3519
|
var TEST_PATHS = /(^|\/)(tests?|__tests__|spec|e2e)\/|\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
3520
|
+
var MANIFEST_PATHS = /(^|\/)(package\.json|tsconfig(\..+)?\.json)$/;
|
|
3479
3521
|
function scoreFileDiff(subject, file) {
|
|
3480
3522
|
const header = parseConventionalHeader(subject);
|
|
3481
3523
|
let score = header.type ? TYPE_WEIGHTS[header.type] ?? 0.5 : 0.5;
|
|
3482
3524
|
if (header.breaking) score += 0.1;
|
|
3483
3525
|
if (TEST_PATHS.test(file.path)) score -= 0.1;
|
|
3526
|
+
if (MANIFEST_PATHS.test(file.path)) score -= 0.1;
|
|
3484
3527
|
if (file.status === "added") score += 0.05;
|
|
3485
3528
|
if (file.status === "deleted") score -= 0.1;
|
|
3486
3529
|
const churn = file.insertions + file.deletions;
|
|
@@ -3613,6 +3656,63 @@ function collectDocFiles(files, projectId, opts = {}) {
|
|
|
3613
3656
|
return files.flatMap((file) => toMemoryNodes3(file, projectId, opts));
|
|
3614
3657
|
}
|
|
3615
3658
|
|
|
3659
|
+
// src/collectors/github.ts
|
|
3660
|
+
var NOTABLE_LABELS = /\b(bug|help wanted|good first issue)\b/i;
|
|
3661
|
+
function scoreGithubThread(thread) {
|
|
3662
|
+
let score = 0.4;
|
|
3663
|
+
if (thread.merged) score += 0.2;
|
|
3664
|
+
else if (thread.state === "closed" && thread.comments.length === 0) score -= 0.15;
|
|
3665
|
+
if (thread.comments.length >= 3) score += 0.15;
|
|
3666
|
+
else if (thread.comments.length >= 1) score += 0.05;
|
|
3667
|
+
if (thread.labels.some((l) => NOTABLE_LABELS.test(l))) score += 0.1;
|
|
3668
|
+
return Number(Math.min(1, Math.max(0.05, score)).toFixed(3));
|
|
3669
|
+
}
|
|
3670
|
+
var MAX_TITLE_CHARS5 = 200;
|
|
3671
|
+
function threadTitle(thread) {
|
|
3672
|
+
const prefix = thread.type === "pull_request" ? "PR" : "Issue";
|
|
3673
|
+
return truncate(`${prefix} #${thread.number}: ${thread.title}`, MAX_TITLE_CHARS5);
|
|
3674
|
+
}
|
|
3675
|
+
function renderBody(thread) {
|
|
3676
|
+
const status = thread.merged ? "merged" : thread.state;
|
|
3677
|
+
const header = `${thread.title}
|
|
3678
|
+
opened by @${thread.author}, ${status}`;
|
|
3679
|
+
const opening = thread.body.trim();
|
|
3680
|
+
const commentsText = thread.comments.map((c) => `--- @${c.author} (${c.createdAt.slice(0, 10)}) ---
|
|
3681
|
+
${c.body.trim()}`).join("\n\n");
|
|
3682
|
+
return [header, opening, commentsText].filter(Boolean).join("\n\n");
|
|
3683
|
+
}
|
|
3684
|
+
var DEFAULT_MAX_BODY_CHARS3 = 4e3;
|
|
3685
|
+
function toMemoryNode2(thread, projectId, opts = {}) {
|
|
3686
|
+
const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS3;
|
|
3687
|
+
const naturalKey = `${thread.type}:${thread.number}`;
|
|
3688
|
+
const source = thread.type === "pull_request" ? "github:pr" : "github:issue";
|
|
3689
|
+
return {
|
|
3690
|
+
id: makeNodeId(projectId, "github_thread", naturalKey),
|
|
3691
|
+
kind: "github_thread",
|
|
3692
|
+
projectId,
|
|
3693
|
+
ts: thread.updatedAt,
|
|
3694
|
+
source,
|
|
3695
|
+
title: threadTitle(thread),
|
|
3696
|
+
body: truncate(renderBody(thread), maxBody),
|
|
3697
|
+
files: [],
|
|
3698
|
+
signal: scoreGithubThread(thread),
|
|
3699
|
+
provenance: "recorded",
|
|
3700
|
+
// verbatim discourse, same tier as conversation_turn
|
|
3701
|
+
meta: {
|
|
3702
|
+
number: thread.number,
|
|
3703
|
+
type: thread.type,
|
|
3704
|
+
state: thread.state,
|
|
3705
|
+
merged: thread.merged,
|
|
3706
|
+
labels: thread.labels,
|
|
3707
|
+
url: thread.url,
|
|
3708
|
+
commentCount: thread.comments.length
|
|
3709
|
+
}
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
function collectGithubThreads(threads, projectId, opts = {}) {
|
|
3713
|
+
return threads.map((thread) => toMemoryNode2(thread, projectId, opts));
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3616
3716
|
// src/slm/summarize.ts
|
|
3617
3717
|
var MAX_USER_CHARS = 400;
|
|
3618
3718
|
var MAX_REPLY_CHARS = 700;
|
|
@@ -3704,7 +3804,7 @@ function sessionFallbackTitle(session) {
|
|
|
3704
3804
|
const line = redact(opening.userText).text.split(/\r?\n/)[0]?.trim();
|
|
3705
3805
|
return line && line.length > 0 ? line : "Working session";
|
|
3706
3806
|
}
|
|
3707
|
-
var
|
|
3807
|
+
var MAX_TITLE_CHARS6 = 200;
|
|
3708
3808
|
var GENERIC_TITLE = /^(a |the )?(session |conversation |project |work )?(summary|update|overview|recap|status)\b/i;
|
|
3709
3809
|
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;
|
|
3710
3810
|
function cleanTitle(line) {
|
|
@@ -3718,12 +3818,12 @@ function parseSummary(raw, fallbackTitle) {
|
|
|
3718
3818
|
if (firstIndex === -1) return null;
|
|
3719
3819
|
const first = lines[firstIndex].trim();
|
|
3720
3820
|
const labelled = /^TITLE:\s*(.+)$/i.exec(first);
|
|
3721
|
-
const fallback = truncate(cleanTitle(fallbackTitle) || "Working session",
|
|
3821
|
+
const fallback = truncate(cleanTitle(fallbackTitle) || "Working session", MAX_TITLE_CHARS6);
|
|
3722
3822
|
if (!labelled) return { title: fallback, body: text };
|
|
3723
3823
|
const candidate = cleanTitle(labelled[1]);
|
|
3724
3824
|
const rest = lines.slice(firstIndex + 1).join("\n").trim();
|
|
3725
3825
|
return {
|
|
3726
|
-
title: candidate.length > 0 && !GENERIC_TITLE.test(candidate) && !ROLE_PREAMBLE_TITLE.test(candidate) ? truncate(candidate,
|
|
3826
|
+
title: candidate.length > 0 && !GENERIC_TITLE.test(candidate) && !ROLE_PREAMBLE_TITLE.test(candidate) ? truncate(candidate, MAX_TITLE_CHARS6) : fallback,
|
|
3727
3827
|
// A model that emitted only a title still gets a usable node: the title
|
|
3728
3828
|
// doubles as the body rather than storing an empty one.
|
|
3729
3829
|
body: rest.length > 0 ? rest : candidate
|
|
@@ -3733,7 +3833,7 @@ function parseSummary(raw, fallbackTitle) {
|
|
|
3733
3833
|
// src/collectors/sessions.ts
|
|
3734
3834
|
var SESSION_SOURCE_PREFIX = "session";
|
|
3735
3835
|
var DEFAULT_SETTLE_MINUTES = 30;
|
|
3736
|
-
var
|
|
3836
|
+
var DEFAULT_MAX_BODY_CHARS4 = 2500;
|
|
3737
3837
|
var DEFAULT_MAX_SESSIONS = 10;
|
|
3738
3838
|
function scoreSession(turnCount) {
|
|
3739
3839
|
const score = 0.6 + Math.min(0.25, turnCount / 100);
|
|
@@ -3777,7 +3877,7 @@ ${t.assistantText}`).join("\n")),
|
|
|
3777
3877
|
};
|
|
3778
3878
|
}
|
|
3779
3879
|
async function collectSessionSummaries(turns, projectId, provider, opts = {}) {
|
|
3780
|
-
const maxBodyChars = opts.maxBodyChars ??
|
|
3880
|
+
const maxBodyChars = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS4;
|
|
3781
3881
|
const maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
3782
3882
|
const all = groupTurnsIntoSessions(turns);
|
|
3783
3883
|
const settled = selectSettledSessions(all, opts.settleMinutes ?? DEFAULT_SETTLE_MINUTES, opts.now);
|
|
@@ -3825,8 +3925,8 @@ async function collectSessionSummaries(turns, projectId, provider, opts = {}) {
|
|
|
3825
3925
|
}
|
|
3826
3926
|
|
|
3827
3927
|
// src/collectors/shell-history.ts
|
|
3828
|
-
var
|
|
3829
|
-
var
|
|
3928
|
+
var DEFAULT_MAX_BODY_CHARS5 = 1e3;
|
|
3929
|
+
var MAX_TITLE_CHARS7 = 200;
|
|
3830
3930
|
var NOISE = /^(cd|ls|dir|pwd|clear|cls|exit|history|whoami|date|type|cat|more|less|ll|la)\b/i;
|
|
3831
3931
|
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;
|
|
3832
3932
|
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;
|
|
@@ -3849,7 +3949,7 @@ function scoreShellCommand(entry) {
|
|
|
3849
3949
|
else if (cmd.length <= 3) score -= 0.05;
|
|
3850
3950
|
return Number(Math.min(1, Math.max(0.05, score)).toFixed(3));
|
|
3851
3951
|
}
|
|
3852
|
-
function
|
|
3952
|
+
function renderBody2(entry, maxChars) {
|
|
3853
3953
|
const parts = [`$ ${entry.command}`];
|
|
3854
3954
|
const metaLine = [];
|
|
3855
3955
|
if (entry.cwd) metaLine.push(`cwd: ${entry.cwd}`);
|
|
@@ -3858,8 +3958,8 @@ function renderBody(entry, maxChars) {
|
|
|
3858
3958
|
if (metaLine.length) parts.push("", metaLine.join(" "));
|
|
3859
3959
|
return truncate(parts.join("\n"), maxChars);
|
|
3860
3960
|
}
|
|
3861
|
-
function
|
|
3862
|
-
const maxBody = opts.maxBodyChars ??
|
|
3961
|
+
function toMemoryNode3(entry, projectId, opts = {}) {
|
|
3962
|
+
const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS5;
|
|
3863
3963
|
const titleLine = entry.command.split(/\r?\n/)[0] ?? entry.command;
|
|
3864
3964
|
return {
|
|
3865
3965
|
id: makeNodeId(projectId, "shell_command", entry.naturalKey),
|
|
@@ -3867,8 +3967,8 @@ function toMemoryNode2(entry, projectId, opts = {}) {
|
|
|
3867
3967
|
projectId,
|
|
3868
3968
|
ts: entry.ts,
|
|
3869
3969
|
source: `shell:${entry.shell}`,
|
|
3870
|
-
title: truncate(titleLine,
|
|
3871
|
-
body:
|
|
3970
|
+
title: truncate(titleLine, MAX_TITLE_CHARS7),
|
|
3971
|
+
body: renderBody2(entry, maxBody),
|
|
3872
3972
|
files: [],
|
|
3873
3973
|
signal: scoreShellCommand(entry),
|
|
3874
3974
|
provenance: "observed",
|
|
@@ -3883,7 +3983,7 @@ function toMemoryNode2(entry, projectId, opts = {}) {
|
|
|
3883
3983
|
};
|
|
3884
3984
|
}
|
|
3885
3985
|
function collectShellHistory(entries, projectId, opts = {}) {
|
|
3886
|
-
return entries.map((entry) =>
|
|
3986
|
+
return entries.map((entry) => toMemoryNode3(entry, projectId, opts));
|
|
3887
3987
|
}
|
|
3888
3988
|
|
|
3889
3989
|
// src/conversation/claude-code-reader.ts
|
|
@@ -4015,6 +4115,112 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
4015
4115
|
return { files, unreadable };
|
|
4016
4116
|
}
|
|
4017
4117
|
|
|
4118
|
+
// src/github/read.ts
|
|
4119
|
+
import { execFile as execFile2 } from "child_process";
|
|
4120
|
+
import { promisify as promisify2 } from "util";
|
|
4121
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
4122
|
+
var GithubUnavailableError = class extends Error {
|
|
4123
|
+
constructor(message, cause) {
|
|
4124
|
+
super(message);
|
|
4125
|
+
this.cause = cause;
|
|
4126
|
+
this.name = "GithubUnavailableError";
|
|
4127
|
+
}
|
|
4128
|
+
cause;
|
|
4129
|
+
};
|
|
4130
|
+
function toGithubError(err) {
|
|
4131
|
+
const code = err?.code;
|
|
4132
|
+
if (code === "ENOENT") {
|
|
4133
|
+
return new GithubUnavailableError(
|
|
4134
|
+
"gh CLI not found on PATH -- install it from https://cli.github.com and run `gh auth login`",
|
|
4135
|
+
err
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
4138
|
+
const stderr = String(err?.stderr ?? "");
|
|
4139
|
+
if (/auth login|not logged into|401/i.test(stderr)) {
|
|
4140
|
+
return new GithubUnavailableError("gh CLI is not authenticated -- run `gh auth login`", err);
|
|
4141
|
+
}
|
|
4142
|
+
const detail = stderr.trim().split("\n")[0] || (err instanceof Error ? err.message : String(err));
|
|
4143
|
+
return new GithubUnavailableError(`gh api call failed: ${detail}`, err);
|
|
4144
|
+
}
|
|
4145
|
+
function parseGithubSlug(originUrl) {
|
|
4146
|
+
if (!originUrl) return null;
|
|
4147
|
+
const match = originUrl.match(/github\.com[:/]([^/]+)\/(.+?)(?:\.git)?\/?$/i);
|
|
4148
|
+
if (!match) return null;
|
|
4149
|
+
return `${match[1]}/${match[2]}`;
|
|
4150
|
+
}
|
|
4151
|
+
var GhCliProvider = class {
|
|
4152
|
+
constructor(repoSlug, opts = {}) {
|
|
4153
|
+
this.repoSlug = repoSlug;
|
|
4154
|
+
this.opts = opts;
|
|
4155
|
+
}
|
|
4156
|
+
repoSlug;
|
|
4157
|
+
opts;
|
|
4158
|
+
async listThreads(since) {
|
|
4159
|
+
const maxThreads = this.opts.maxThreads ?? 100;
|
|
4160
|
+
const perPage = Math.min(maxThreads, 100);
|
|
4161
|
+
const params = new URLSearchParams({
|
|
4162
|
+
state: "all",
|
|
4163
|
+
sort: "updated",
|
|
4164
|
+
direction: "desc",
|
|
4165
|
+
per_page: String(perPage)
|
|
4166
|
+
});
|
|
4167
|
+
if (since) params.set("since", since);
|
|
4168
|
+
let items;
|
|
4169
|
+
try {
|
|
4170
|
+
const { stdout } = await execFileAsync2("gh", ["api", `repos/${this.repoSlug}/issues?${params}`], {
|
|
4171
|
+
maxBuffer: 16 * 1024 * 1024
|
|
4172
|
+
});
|
|
4173
|
+
items = JSON.parse(stdout);
|
|
4174
|
+
} catch (err) {
|
|
4175
|
+
throw toGithubError(err);
|
|
4176
|
+
}
|
|
4177
|
+
const bounded = items.slice(0, maxThreads);
|
|
4178
|
+
const threads = [];
|
|
4179
|
+
for (const item of bounded) {
|
|
4180
|
+
const comments = item.comments > 0 ? await this.fetchComments(item.number) : [];
|
|
4181
|
+
threads.push({
|
|
4182
|
+
number: item.number,
|
|
4183
|
+
type: item.pull_request ? "pull_request" : "issue",
|
|
4184
|
+
title: item.title,
|
|
4185
|
+
body: item.body ?? "",
|
|
4186
|
+
author: item.user?.login ?? "unknown",
|
|
4187
|
+
state: item.state,
|
|
4188
|
+
merged: item.pull_request?.merged_at != null,
|
|
4189
|
+
labels: item.labels.map((l) => typeof l === "string" ? l : l.name),
|
|
4190
|
+
createdAt: item.created_at,
|
|
4191
|
+
updatedAt: item.updated_at,
|
|
4192
|
+
url: item.html_url,
|
|
4193
|
+
comments
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4196
|
+
return threads;
|
|
4197
|
+
}
|
|
4198
|
+
/**
|
|
4199
|
+
* Fails soft per thread, unlike `listThreads` itself: one comment fetch
|
|
4200
|
+
* failing (a deleted comment, a transient network blip) shouldn't drop the
|
|
4201
|
+
* whole sync the way an unreachable `gh`/no auth does. The thread still
|
|
4202
|
+
* gets ingested, just without that page of discussion this run.
|
|
4203
|
+
*/
|
|
4204
|
+
async fetchComments(number) {
|
|
4205
|
+
const maxComments = this.opts.maxCommentsPerThread ?? 100;
|
|
4206
|
+
try {
|
|
4207
|
+
const { stdout } = await execFileAsync2(
|
|
4208
|
+
"gh",
|
|
4209
|
+
["api", `repos/${this.repoSlug}/issues/${number}/comments?per_page=${Math.min(maxComments, 100)}`],
|
|
4210
|
+
{ maxBuffer: 16 * 1024 * 1024 }
|
|
4211
|
+
);
|
|
4212
|
+
const items = JSON.parse(stdout);
|
|
4213
|
+
return items.slice(0, maxComments).map((c) => ({
|
|
4214
|
+
author: c.user?.login ?? "unknown",
|
|
4215
|
+
body: c.body ?? "",
|
|
4216
|
+
createdAt: c.created_at
|
|
4217
|
+
}));
|
|
4218
|
+
} catch {
|
|
4219
|
+
return [];
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
};
|
|
4223
|
+
|
|
4018
4224
|
// src/slm/contradiction.ts
|
|
4019
4225
|
var MAX_BODY_CHARS = 1500;
|
|
4020
4226
|
var MAX_REASON_CHARS = 200;
|
|
@@ -4939,7 +5145,7 @@ ${node.body}`)
|
|
|
4939
5145
|
for (const [index, node] of group.entries()) {
|
|
4940
5146
|
const vector = vectors[index];
|
|
4941
5147
|
if (vector && vector.length === provider.dimension) {
|
|
4942
|
-
store.upsertEmbedding(node.rowid, vector);
|
|
5148
|
+
store.upsertEmbedding(node.rowid, projectId, vector);
|
|
4943
5149
|
embedded += 1;
|
|
4944
5150
|
embeddedHere += 1;
|
|
4945
5151
|
} else {
|
|
@@ -5169,6 +5375,38 @@ async function syncDocs(store, projectId, repoRoot, config, log) {
|
|
|
5169
5375
|
}
|
|
5170
5376
|
return { totals, seen: nodes.length };
|
|
5171
5377
|
}
|
|
5378
|
+
var GITHUB_SOURCE = "github";
|
|
5379
|
+
async function syncGithub(store, projectId, opts, repo, config, log) {
|
|
5380
|
+
const totals = { inserted: 0, updated: 0, unchanged: 0, denied: 0 };
|
|
5381
|
+
const enabled = opts.githubOverride ?? config.sources.github.enabled;
|
|
5382
|
+
if (!enabled) return { totals, seen: 0 };
|
|
5383
|
+
const slug = parseGithubSlug(repo.originUrl);
|
|
5384
|
+
if (!slug) {
|
|
5385
|
+
log(`${pc7.dim("github")} no github.com remote found -- skipped`);
|
|
5386
|
+
return { totals, seen: 0 };
|
|
5387
|
+
}
|
|
5388
|
+
const provider = opts.githubProvider ?? new GhCliProvider(slug, {
|
|
5389
|
+
maxThreads: config.sources.github.maxThreads,
|
|
5390
|
+
maxCommentsPerThread: config.sources.github.maxCommentsPerThread
|
|
5391
|
+
});
|
|
5392
|
+
const cursor = opts.full || opts.rebuild ? null : store.getSyncCursor(projectId, GITHUB_SOURCE);
|
|
5393
|
+
const requestedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5394
|
+
let threads;
|
|
5395
|
+
try {
|
|
5396
|
+
threads = await provider.listThreads(cursor);
|
|
5397
|
+
} catch (err) {
|
|
5398
|
+
if (err instanceof GithubUnavailableError) {
|
|
5399
|
+
log(`${pc7.dim("github")} ${err.message} -- skipped`);
|
|
5400
|
+
return { totals, seen: 0 };
|
|
5401
|
+
}
|
|
5402
|
+
throw err;
|
|
5403
|
+
}
|
|
5404
|
+
const nodes = collectGithubThreads(threads, projectId, { maxBodyChars: config.limits.maxBodyChars });
|
|
5405
|
+
if (nodes.length > 0) addStats(totals, store.upsertNodes(nodes));
|
|
5406
|
+
store.setSyncCursor(projectId, GITHUB_SOURCE, requestedAt);
|
|
5407
|
+
log(` ${pc7.dim(`${GITHUB_SOURCE}: ${nodes.length} thread(s) read from ${slug}`)}`);
|
|
5408
|
+
return { totals, seen: nodes.length };
|
|
5409
|
+
}
|
|
5172
5410
|
async function syncStructure(store, projectId, repoRoot, config, log) {
|
|
5173
5411
|
if (!config.sources.structure.enabled) {
|
|
5174
5412
|
log(`${pc7.dim("structure")} disabled in config`);
|
|
@@ -5291,6 +5529,7 @@ async function runSync(opts) {
|
|
|
5291
5529
|
const conversation = syncConversation(store, projectId, turns, config, log, opts.conversationOverride);
|
|
5292
5530
|
const sessions = await syncSessions(store, projectId, turns, config, log);
|
|
5293
5531
|
const docs = await syncDocs(store, projectId, repo.root, config, log);
|
|
5532
|
+
const github = await syncGithub(store, projectId, opts, repo, config, log);
|
|
5294
5533
|
const structure = await syncStructure(store, projectId, repo.root, config, log);
|
|
5295
5534
|
let embedLine = "";
|
|
5296
5535
|
let embeddingAvailable = false;
|
|
@@ -5333,6 +5572,7 @@ async function runSync(opts) {
|
|
|
5333
5572
|
addStats(totals, conversation.totals);
|
|
5334
5573
|
addStats(totals, sessions.totals);
|
|
5335
5574
|
addStats(totals, docs.totals);
|
|
5575
|
+
addStats(totals, github.totals);
|
|
5336
5576
|
const stats2 = store.stats(projectId);
|
|
5337
5577
|
const elapsed = ((Date.now() - started) / 1e3).toFixed(2);
|
|
5338
5578
|
const conversationPart = conversationEnabled ? `, ${conversation.seen} conversation exchange(s)` : "";
|
|
@@ -5340,10 +5580,12 @@ async function runSync(opts) {
|
|
|
5340
5580
|
const docsPart = config.sources.docs.enabled ? `, ${docs.seen} doc section(s)` : "";
|
|
5341
5581
|
const diffPart = config.sources.diff.enabled ? `, ${diffs.seen} file diff(s)` : "";
|
|
5342
5582
|
const structurePart = config.sources.structure.enabled ? `, ${structure.edges} import edge(s)` : "";
|
|
5583
|
+
const githubEnabled = opts.githubOverride ?? config.sources.github.enabled;
|
|
5584
|
+
const githubPart = githubEnabled ? `, ${github.seen} github thread(s)` : "";
|
|
5343
5585
|
const deniedPart = totals.denied > 0 ? ` ${pc7.red(`-${totals.denied} denied`)}` : "";
|
|
5344
5586
|
out(
|
|
5345
5587
|
[
|
|
5346
|
-
`${pc7.green("synced")} ${git2.seen} commit(s)${diffPart}, ${shell.seen} shell entr${shell.seen === 1 ? "y" : "ies"}${conversationPart}${sessionPart}${docsPart}${structurePart} in ${elapsed}s`,
|
|
5588
|
+
`${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`,
|
|
5347
5589
|
` ${pc7.green(`+${totals.inserted} new`)} ${pc7.yellow(`~${totals.updated} updated`)} ${pc7.dim(`=${totals.unchanged} unchanged`)}${deniedPart}`,
|
|
5348
5590
|
` ${pc7.dim(`${stats2.total} node(s) total across ${stats2.distinctFiles} file path(s)`)}`,
|
|
5349
5591
|
""
|
|
@@ -5461,7 +5703,7 @@ function createServer() {
|
|
|
5461
5703
|
"search_memory",
|
|
5462
5704
|
{
|
|
5463
5705
|
title: "Search remembered project history",
|
|
5464
|
-
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
|
|
5706
|
+
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.",
|
|
5465
5707
|
inputSchema: {
|
|
5466
5708
|
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5467
5709
|
query: z4.string().describe("Free-text question or search terms"),
|
|
@@ -5494,7 +5736,7 @@ function createServer() {
|
|
|
5494
5736
|
"sync_project",
|
|
5495
5737
|
{
|
|
5496
5738
|
title: "Sync remembered history",
|
|
5497
|
-
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.",
|
|
5739
|
+
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.",
|
|
5498
5740
|
inputSchema: {
|
|
5499
5741
|
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5500
5742
|
pruneSource: z4.string().optional().describe('Delete every node from this exact source (e.g. "shell:pwsh") instead of syncing'),
|
|
@@ -5528,7 +5770,7 @@ function createServer() {
|
|
|
5528
5770
|
"list_recent_memory",
|
|
5529
5771
|
{
|
|
5530
5772
|
title: "List recently remembered items",
|
|
5531
|
-
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
|
|
5773
|
+
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.",
|
|
5532
5774
|
inputSchema: {
|
|
5533
5775
|
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5534
5776
|
limit: z4.number().int().positive().optional().describe("Max items to return, newest first. Default 20.")
|
|
@@ -5801,6 +6043,7 @@ var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
|
5801
6043
|
var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
5802
6044
|
var DOCS_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
5803
6045
|
var DIFF_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
6046
|
+
var GITHUB_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
5804
6047
|
function signalBand(signal, bands) {
|
|
5805
6048
|
if (signal >= bands.high) return "high";
|
|
5806
6049
|
if (signal >= bands.medium) return "medium";
|
|
@@ -6008,14 +6251,57 @@ function formatNode4(node) {
|
|
|
6008
6251
|
].join(" ");
|
|
6009
6252
|
}
|
|
6010
6253
|
|
|
6011
|
-
// src/cli/commands/scan-
|
|
6254
|
+
// src/cli/commands/scan-github.ts
|
|
6012
6255
|
import pc16 from "picocolors";
|
|
6256
|
+
async function runScanGithub(opts) {
|
|
6257
|
+
const repo = await readRepoInfo(opts.cwd);
|
|
6258
|
+
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
6259
|
+
const slug = parseGithubSlug(repo.originUrl);
|
|
6260
|
+
if (!slug) {
|
|
6261
|
+
process.stderr.write(`${pc16.yellow("no github.com remote found for this repository")}
|
|
6262
|
+
`);
|
|
6263
|
+
return 0;
|
|
6264
|
+
}
|
|
6265
|
+
const provider = opts.provider ?? new GhCliProvider(slug);
|
|
6266
|
+
let threads;
|
|
6267
|
+
try {
|
|
6268
|
+
threads = await provider.listThreads(null);
|
|
6269
|
+
} catch (err) {
|
|
6270
|
+
if (err instanceof GithubUnavailableError) {
|
|
6271
|
+
process.stderr.write(`${pc16.yellow(err.message)}
|
|
6272
|
+
`);
|
|
6273
|
+
return 0;
|
|
6274
|
+
}
|
|
6275
|
+
throw err;
|
|
6276
|
+
}
|
|
6277
|
+
const nodes = collectGithubThreads(threads, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
6278
|
+
if (opts.json) {
|
|
6279
|
+
process.stdout.write(`${JSON.stringify(nodes, null, 2)}
|
|
6280
|
+
`);
|
|
6281
|
+
return 0;
|
|
6282
|
+
}
|
|
6283
|
+
for (const node of nodes) process.stdout.write(`${formatNode5(node)}
|
|
6284
|
+
`);
|
|
6285
|
+
const approxTotal = approxTotalTokens(nodes);
|
|
6286
|
+
process.stderr.write(
|
|
6287
|
+
`
|
|
6288
|
+
${pc16.bold(String(nodes.length))} thread(s) from ${pc16.dim(slug)} above threshold ${pc16.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
6289
|
+
`
|
|
6290
|
+
);
|
|
6291
|
+
return 0;
|
|
6292
|
+
}
|
|
6293
|
+
function formatNode5(node) {
|
|
6294
|
+
return [formatSignal(node.signal, GITHUB_SIGNAL_BANDS), node.title].join(" ");
|
|
6295
|
+
}
|
|
6296
|
+
|
|
6297
|
+
// src/cli/commands/scan-session.ts
|
|
6298
|
+
import pc17 from "picocolors";
|
|
6013
6299
|
async function runScanSession(opts) {
|
|
6014
6300
|
const repo = await readRepoInfo(opts.cwd);
|
|
6015
6301
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
6016
6302
|
const turns = await collectClaudeCodeTranscripts(repo.root);
|
|
6017
6303
|
if (turns.length === 0) {
|
|
6018
|
-
process.stderr.write(`${
|
|
6304
|
+
process.stderr.write(`${pc17.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
6019
6305
|
`);
|
|
6020
6306
|
return 0;
|
|
6021
6307
|
}
|
|
@@ -6023,7 +6309,7 @@ async function runScanSession(opts) {
|
|
|
6023
6309
|
const settled = selectSettledSessions(sessions, opts.settleMinutes);
|
|
6024
6310
|
if (!opts.json) {
|
|
6025
6311
|
process.stderr.write(
|
|
6026
|
-
`${
|
|
6312
|
+
`${pc17.dim("sessions")} ${sessions.length} found, ${settled.length} settled (quiet ${opts.settleMinutes}m+)
|
|
6027
6313
|
|
|
6028
6314
|
`
|
|
6029
6315
|
);
|
|
@@ -6049,7 +6335,7 @@ async function runScanSession(opts) {
|
|
|
6049
6335
|
}
|
|
6050
6336
|
for (const preview of previews) {
|
|
6051
6337
|
process.stdout.write(
|
|
6052
|
-
`${
|
|
6338
|
+
`${pc17.bold(preview.sessionKey)} ${preview.startedAt.slice(0, 16).replace("T", " ")} ${preview.includedTurns}/${preview.turns} turn(s), ${preview.promptChars} chars
|
|
6053
6339
|
${preview.prompt}
|
|
6054
6340
|
|
|
6055
6341
|
`
|
|
@@ -6061,7 +6347,7 @@ ${preview.prompt}
|
|
|
6061
6347
|
settleMinutes: opts.settleMinutes,
|
|
6062
6348
|
maxSessions: opts.maxSessions,
|
|
6063
6349
|
onProgress: (done, total) => {
|
|
6064
|
-
if (!opts.json) process.stderr.write(` ${
|
|
6350
|
+
if (!opts.json) process.stderr.write(` ${pc17.dim(`summarizing ${done}/${total}`)}
|
|
6065
6351
|
`);
|
|
6066
6352
|
}
|
|
6067
6353
|
});
|
|
@@ -6071,21 +6357,21 @@ ${preview.prompt}
|
|
|
6071
6357
|
return 0;
|
|
6072
6358
|
}
|
|
6073
6359
|
for (const node of result.nodes) {
|
|
6074
|
-
process.stdout.write(`${
|
|
6075
|
-
${
|
|
6360
|
+
process.stdout.write(`${pc17.bold(node.title)}
|
|
6361
|
+
${pc17.dim(node.ts.slice(0, 16).replace("T", " "))}
|
|
6076
6362
|
${node.body}
|
|
6077
6363
|
|
|
6078
6364
|
`);
|
|
6079
6365
|
}
|
|
6080
6366
|
if (result.providerUnavailable) {
|
|
6081
6367
|
process.stderr.write(
|
|
6082
|
-
`${
|
|
6368
|
+
`${pc17.yellow("model unavailable")} -- is Ollama running with \`${opts.model}\` pulled? (\`ollama pull ${opts.model}\`)
|
|
6083
6369
|
`
|
|
6084
6370
|
);
|
|
6085
6371
|
return 0;
|
|
6086
6372
|
}
|
|
6087
6373
|
process.stderr.write(
|
|
6088
|
-
`${
|
|
6374
|
+
`${pc17.bold(String(result.nodes.length))} summarized` + (result.failed > 0 ? `, ${pc17.yellow(`${result.failed} failed`)}` : "") + ` ${pc17.dim(`(model ${opts.model})`)}
|
|
6089
6375
|
`
|
|
6090
6376
|
);
|
|
6091
6377
|
return 0;
|
|
@@ -6093,16 +6379,16 @@ ${node.body}
|
|
|
6093
6379
|
var SCAN_SESSION_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
6094
6380
|
|
|
6095
6381
|
// src/cli/commands/scan-shell.ts
|
|
6096
|
-
import
|
|
6382
|
+
import pc18 from "picocolors";
|
|
6097
6383
|
async function runScanShell(opts) {
|
|
6098
6384
|
const repo = await readRepoInfo(opts.cwd);
|
|
6099
6385
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
6100
6386
|
const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
|
|
6101
6387
|
if (!opts.json) {
|
|
6102
6388
|
process.stderr.write(
|
|
6103
|
-
results.length ? `${
|
|
6389
|
+
results.length ? `${pc18.dim("sources found")} ${results.map((r) => r.name).join(", ")}
|
|
6104
6390
|
|
|
6105
|
-
` : `${
|
|
6391
|
+
` : `${pc18.yellow("no shell history source found on this machine")}
|
|
6106
6392
|
`
|
|
6107
6393
|
);
|
|
6108
6394
|
}
|
|
@@ -6111,9 +6397,9 @@ async function runScanShell(opts) {
|
|
|
6111
6397
|
const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
6112
6398
|
allNodes.push(...nodes);
|
|
6113
6399
|
if (!opts.json) {
|
|
6114
|
-
process.stdout.write(`${
|
|
6400
|
+
process.stdout.write(`${pc18.bold(`shell:${result.name}`)} ${pc18.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
|
|
6115
6401
|
`);
|
|
6116
|
-
for (const node of nodes) process.stdout.write(`${
|
|
6402
|
+
for (const node of nodes) process.stdout.write(`${formatNode6(node)}
|
|
6117
6403
|
`);
|
|
6118
6404
|
process.stdout.write("\n");
|
|
6119
6405
|
}
|
|
@@ -6124,19 +6410,19 @@ async function runScanShell(opts) {
|
|
|
6124
6410
|
return 0;
|
|
6125
6411
|
}
|
|
6126
6412
|
const approxTotal = approxTotalTokens(allNodes);
|
|
6127
|
-
process.stderr.write(`${
|
|
6413
|
+
process.stderr.write(`${pc18.bold(String(allNodes.length))} node(s) total ${pc18.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
6128
6414
|
`);
|
|
6129
6415
|
return 0;
|
|
6130
6416
|
}
|
|
6131
|
-
function
|
|
6132
|
-
const approx = node.meta.tsApprox ?
|
|
6417
|
+
function formatNode6(node) {
|
|
6418
|
+
const approx = node.meta.tsApprox ? pc18.dim("~") : " ";
|
|
6133
6419
|
const exit = node.meta.exitCode;
|
|
6134
|
-
const exitLabel = typeof exit === "number" && exit !== 0 ?
|
|
6420
|
+
const exitLabel = typeof exit === "number" && exit !== 0 ? pc18.red(`exit ${exit}`) : "";
|
|
6135
6421
|
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
|
|
6136
6422
|
}
|
|
6137
6423
|
|
|
6138
6424
|
// src/cli/commands/scan-structure.ts
|
|
6139
|
-
import
|
|
6425
|
+
import pc19 from "picocolors";
|
|
6140
6426
|
var TRACKED_EXTENSIONS = TRACKED_PATHSPECS.map((p) => p.replace("*", "")).join("/");
|
|
6141
6427
|
async function runScanStructure(opts) {
|
|
6142
6428
|
const repo = await readRepoInfo(opts.cwd);
|
|
@@ -6147,24 +6433,24 @@ async function runScanStructure(opts) {
|
|
|
6147
6433
|
return 0;
|
|
6148
6434
|
}
|
|
6149
6435
|
if (unreadable.length > 0) {
|
|
6150
|
-
process.stderr.write(`${
|
|
6436
|
+
process.stderr.write(`${pc19.yellow("unreadable")} ${unreadable.join(", ")}
|
|
6151
6437
|
|
|
6152
6438
|
`);
|
|
6153
6439
|
}
|
|
6154
6440
|
for (const edge of edges) {
|
|
6155
|
-
process.stdout.write(`${edge.fromPath} ${
|
|
6441
|
+
process.stdout.write(`${edge.fromPath} ${pc19.dim("->")} ${edge.toPath}
|
|
6156
6442
|
`);
|
|
6157
6443
|
}
|
|
6158
6444
|
process.stderr.write(
|
|
6159
6445
|
`
|
|
6160
|
-
${
|
|
6446
|
+
${pc19.bold(String(edges.length))} edge(s) from ${filesScanned} tracked ${TRACKED_EXTENSIONS} file(s)
|
|
6161
6447
|
`
|
|
6162
6448
|
);
|
|
6163
6449
|
return 0;
|
|
6164
6450
|
}
|
|
6165
6451
|
|
|
6166
6452
|
// src/cli/commands/stale.ts
|
|
6167
|
-
import
|
|
6453
|
+
import pc20 from "picocolors";
|
|
6168
6454
|
var STALE_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
6169
6455
|
async function runStale(opts) {
|
|
6170
6456
|
const { projectId, ws } = await loadContext(opts.cwd);
|
|
@@ -6174,15 +6460,15 @@ async function runStale(opts) {
|
|
|
6174
6460
|
if (opts.dismiss) {
|
|
6175
6461
|
const dismissed = store.dismissContradictionSuggestion(projectId, opts.dismiss);
|
|
6176
6462
|
out(
|
|
6177
|
-
dismissed > 0 ? `${
|
|
6178
|
-
` : `${
|
|
6463
|
+
dismissed > 0 ? `${pc20.green("dismissed")} the contradiction suggestion for ${opts.dismiss} -- it will not resurface
|
|
6464
|
+
` : `${pc20.dim("no open contradiction suggestion")} for ${opts.dismiss} -- nothing to dismiss
|
|
6179
6465
|
`
|
|
6180
6466
|
);
|
|
6181
6467
|
return 0;
|
|
6182
6468
|
}
|
|
6183
6469
|
const candidates = store.listStaleCandidates(projectId, { minAgeDays: opts.minAgeDays, limit: opts.limit });
|
|
6184
6470
|
if (candidates.length === 0) {
|
|
6185
|
-
out(`${
|
|
6471
|
+
out(`${pc20.dim("no stale candidates")} -- no unconfirmed node older than the threshold lacks a successor
|
|
6186
6472
|
`);
|
|
6187
6473
|
return 0;
|
|
6188
6474
|
}
|
|
@@ -6204,15 +6490,15 @@ async function runStale(opts) {
|
|
|
6204
6490
|
for (const s of suggestions) byCandidateId.set(s.candidateId, s);
|
|
6205
6491
|
out(
|
|
6206
6492
|
[
|
|
6207
|
-
`${
|
|
6493
|
+
`${pc20.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
|
|
6208
6494
|
...candidates.map((c) => {
|
|
6209
|
-
const line = ` ${
|
|
6495
|
+
const line = ` ${pc20.dim(c.id)} ${pc20.yellow(`${c.ageDays}d old`)} [${c.kind}] ${c.title}`;
|
|
6210
6496
|
const hit = byCandidateId.get(c.id);
|
|
6211
6497
|
return hit ? `${line}
|
|
6212
|
-
${
|
|
6498
|
+
${pc20.red("likely superseded by")} ${pc20.dim(hit.againstId)} ${hit.againstTitle} -- ${hit.reason}` : line;
|
|
6213
6499
|
}),
|
|
6214
6500
|
"",
|
|
6215
|
-
`run ${
|
|
6501
|
+
`run ${pc20.bold("nexusmem mark-stale <id> --supersedes <newId>")} on any that are actually wrong`
|
|
6216
6502
|
].join("\n").concat("\n")
|
|
6217
6503
|
);
|
|
6218
6504
|
return 0;
|
|
@@ -6224,7 +6510,7 @@ async function runStale(opts) {
|
|
|
6224
6510
|
// src/cli/commands/status.ts
|
|
6225
6511
|
import { basename as basename4 } from "path";
|
|
6226
6512
|
import { statSync } from "fs";
|
|
6227
|
-
import
|
|
6513
|
+
import pc21 from "picocolors";
|
|
6228
6514
|
function daySpan(oldest, newest) {
|
|
6229
6515
|
const oldestDay = Date.parse(oldest.slice(0, 10));
|
|
6230
6516
|
const newestDay = Date.parse(newest.slice(0, 10));
|
|
@@ -6284,34 +6570,34 @@ async function runStatus(opts) {
|
|
|
6284
6570
|
const flaggedCount = store.countContradictionSuggestions(projectId);
|
|
6285
6571
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
6286
6572
|
const kinds = Object.entries(stats2.byKind).sort((a, b) => b[1] - a[1]).map(([kind, n]) => ` ${String(n).padStart(6)} ${kind}`);
|
|
6287
|
-
const staleProjectWarning = otherProjectIds.length ? `${
|
|
6573
|
+
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(
|
|
6288
6574
|
"nexusmem sync --prune-source <name>"
|
|
6289
6575
|
)} to remove stale source data` : "";
|
|
6290
6576
|
out(
|
|
6291
6577
|
[
|
|
6292
|
-
`${
|
|
6293
|
-
`${
|
|
6294
|
-
`${
|
|
6295
|
-
`${
|
|
6296
|
-
`${
|
|
6578
|
+
`${pc21.dim("repo ")} ${repo.root}`,
|
|
6579
|
+
`${pc21.dim("branch ")} ${repo.branch ?? pc21.yellow("(detached)")}`,
|
|
6580
|
+
`${pc21.dim("project ")} ${pc21.cyan(projectId)}`,
|
|
6581
|
+
`${pc21.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc21.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
|
|
6582
|
+
`${pc21.dim("database")} ${ws.dbPath} ${pc21.dim(`(${humanBytes(dbBytes)})`)}`,
|
|
6297
6583
|
staleProjectWarning,
|
|
6298
6584
|
"",
|
|
6299
|
-
`${
|
|
6585
|
+
`${pc21.bold(String(stats2.total))} node(s)${stats2.total ? ` ${pc21.dim(`${stats2.oldest?.slice(0, 10)} .. ${stats2.newest?.slice(0, 10)}`)}` : ""}`,
|
|
6300
6586
|
...kinds,
|
|
6301
|
-
stats2.total ? ` ${
|
|
6587
|
+
stats2.total ? ` ${pc21.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
|
|
6302
6588
|
"",
|
|
6303
|
-
sources.length ?
|
|
6589
|
+
sources.length ? pc21.dim("sources") : pc21.yellow("no sources synced yet"),
|
|
6304
6590
|
...sources.map((s) => {
|
|
6305
6591
|
const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
6306
6592
|
const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
|
|
6307
|
-
return ` ${s.source.padEnd(14)} ${
|
|
6593
|
+
return ` ${s.source.padEnd(14)} ${pc21.dim(`last run ${when}`)} ${pc21.dim(`cursor ${cursorLabel}`)}`;
|
|
6308
6594
|
}),
|
|
6309
6595
|
"",
|
|
6310
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
6311
|
-
chains.failuresTotal ? `${
|
|
6312
|
-
structure.edges ? `${
|
|
6313
|
-
staleCount ? `${
|
|
6314
|
-
flaggedCount ? `${
|
|
6596
|
+
gitCursor && gitCursor !== repo.head ? `${pc21.yellow("git behind HEAD")} \u2014 run ${pc21.bold("nexusmem sync")}` : "",
|
|
6597
|
+
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` : ""}` : "",
|
|
6598
|
+
structure.edges ? `${pc21.dim("structure")} ${pc21.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : "",
|
|
6599
|
+
staleCount ? `${pc21.dim("aging ")} ${pc21.bold(String(staleCount))} unconfirmed node(s) worth a look \u2014 run ${pc21.bold("nexusmem stale")}` : "",
|
|
6600
|
+
flaggedCount ? `${pc21.dim("flagged ")} ${pc21.bold(String(flaggedCount))} likely-superseded node(s) awaiting review \u2014 run ${pc21.bold("nexusmem stale")} for detail` : ""
|
|
6315
6601
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
6316
6602
|
);
|
|
6317
6603
|
return 0;
|
|
@@ -6334,7 +6620,7 @@ function guard(run) {
|
|
|
6334
6620
|
process.exitCode = await run();
|
|
6335
6621
|
} catch (err) {
|
|
6336
6622
|
if (isExpected(err)) {
|
|
6337
|
-
process.stderr.write(`${
|
|
6623
|
+
process.stderr.write(`${pc22.red("error")} ${err.message}
|
|
6338
6624
|
`);
|
|
6339
6625
|
process.exitCode = 1;
|
|
6340
6626
|
return;
|
|
@@ -6350,7 +6636,7 @@ program.command("init").description("Create the .nexusmem workspace and database
|
|
|
6350
6636
|
() => runInit({ cwd: options.cwd, force: options.force, hook: options.hook, enableConversation: options.enableConversation })
|
|
6351
6637
|
)()
|
|
6352
6638
|
);
|
|
6353
|
-
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(
|
|
6639
|
+
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(
|
|
6354
6640
|
"--embed-limit <count>",
|
|
6355
6641
|
"stop embedding after this many nodes (default: embed everything pending)",
|
|
6356
6642
|
(v) => Number.parseInt(v, 10)
|
|
@@ -6371,6 +6657,7 @@ program.command("sync").description("Ingest new history into the local database"
|
|
|
6371
6657
|
since: options.since,
|
|
6372
6658
|
shellTailLines: options.shellLines,
|
|
6373
6659
|
conversationOverride: options.conversation ? true : void 0,
|
|
6660
|
+
githubOverride: options.github ? true : void 0,
|
|
6374
6661
|
noEmbed: !options.embed,
|
|
6375
6662
|
embedLimit: options.embedLimit,
|
|
6376
6663
|
pruneSource: options.pruneSource,
|
|
@@ -6502,6 +6789,7 @@ program.command("scan-session").description("Preview the session summaries a loc
|
|
|
6502
6789
|
)()
|
|
6503
6790
|
);
|
|
6504
6791
|
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 }))());
|
|
6792
|
+
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 }))());
|
|
6505
6793
|
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(
|
|
6506
6794
|
(options) => guard(
|
|
6507
6795
|
() => runPrecheck({
|
|
@@ -6517,7 +6805,7 @@ program.command("scan-structure").description("Preview the JS/TS/Python/Go/Rust/
|
|
|
6517
6805
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
6518
6806
|
program.parseAsync(process.argv).catch((err) => {
|
|
6519
6807
|
const message = err instanceof Error ? err.message : String(err);
|
|
6520
|
-
process.stderr.write(`${
|
|
6808
|
+
process.stderr.write(`${pc22.red("error")} ${message}
|
|
6521
6809
|
`);
|
|
6522
6810
|
process.exitCode = 1;
|
|
6523
6811
|
});
|