nexusmem 0.9.1 → 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 +13 -1
- package/README.md +550 -529
- package/dist/cli/index.js +336 -69
- 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),
|
|
@@ -1123,6 +1143,8 @@ function defaultProvenanceForKind(kind) {
|
|
|
1123
1143
|
return "recorded";
|
|
1124
1144
|
case "session_summary":
|
|
1125
1145
|
return "derived";
|
|
1146
|
+
case "github_thread":
|
|
1147
|
+
return "recorded";
|
|
1126
1148
|
}
|
|
1127
1149
|
}
|
|
1128
1150
|
|
|
@@ -3634,6 +3656,63 @@ function collectDocFiles(files, projectId, opts = {}) {
|
|
|
3634
3656
|
return files.flatMap((file) => toMemoryNodes3(file, projectId, opts));
|
|
3635
3657
|
}
|
|
3636
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
|
+
|
|
3637
3716
|
// src/slm/summarize.ts
|
|
3638
3717
|
var MAX_USER_CHARS = 400;
|
|
3639
3718
|
var MAX_REPLY_CHARS = 700;
|
|
@@ -3725,7 +3804,7 @@ function sessionFallbackTitle(session) {
|
|
|
3725
3804
|
const line = redact(opening.userText).text.split(/\r?\n/)[0]?.trim();
|
|
3726
3805
|
return line && line.length > 0 ? line : "Working session";
|
|
3727
3806
|
}
|
|
3728
|
-
var
|
|
3807
|
+
var MAX_TITLE_CHARS6 = 200;
|
|
3729
3808
|
var GENERIC_TITLE = /^(a |the )?(session |conversation |project |work )?(summary|update|overview|recap|status)\b/i;
|
|
3730
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;
|
|
3731
3810
|
function cleanTitle(line) {
|
|
@@ -3739,12 +3818,12 @@ function parseSummary(raw, fallbackTitle) {
|
|
|
3739
3818
|
if (firstIndex === -1) return null;
|
|
3740
3819
|
const first = lines[firstIndex].trim();
|
|
3741
3820
|
const labelled = /^TITLE:\s*(.+)$/i.exec(first);
|
|
3742
|
-
const fallback = truncate(cleanTitle(fallbackTitle) || "Working session",
|
|
3821
|
+
const fallback = truncate(cleanTitle(fallbackTitle) || "Working session", MAX_TITLE_CHARS6);
|
|
3743
3822
|
if (!labelled) return { title: fallback, body: text };
|
|
3744
3823
|
const candidate = cleanTitle(labelled[1]);
|
|
3745
3824
|
const rest = lines.slice(firstIndex + 1).join("\n").trim();
|
|
3746
3825
|
return {
|
|
3747
|
-
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,
|
|
3748
3827
|
// A model that emitted only a title still gets a usable node: the title
|
|
3749
3828
|
// doubles as the body rather than storing an empty one.
|
|
3750
3829
|
body: rest.length > 0 ? rest : candidate
|
|
@@ -3754,7 +3833,7 @@ function parseSummary(raw, fallbackTitle) {
|
|
|
3754
3833
|
// src/collectors/sessions.ts
|
|
3755
3834
|
var SESSION_SOURCE_PREFIX = "session";
|
|
3756
3835
|
var DEFAULT_SETTLE_MINUTES = 30;
|
|
3757
|
-
var
|
|
3836
|
+
var DEFAULT_MAX_BODY_CHARS4 = 2500;
|
|
3758
3837
|
var DEFAULT_MAX_SESSIONS = 10;
|
|
3759
3838
|
function scoreSession(turnCount) {
|
|
3760
3839
|
const score = 0.6 + Math.min(0.25, turnCount / 100);
|
|
@@ -3798,7 +3877,7 @@ ${t.assistantText}`).join("\n")),
|
|
|
3798
3877
|
};
|
|
3799
3878
|
}
|
|
3800
3879
|
async function collectSessionSummaries(turns, projectId, provider, opts = {}) {
|
|
3801
|
-
const maxBodyChars = opts.maxBodyChars ??
|
|
3880
|
+
const maxBodyChars = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS4;
|
|
3802
3881
|
const maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
|
|
3803
3882
|
const all = groupTurnsIntoSessions(turns);
|
|
3804
3883
|
const settled = selectSettledSessions(all, opts.settleMinutes ?? DEFAULT_SETTLE_MINUTES, opts.now);
|
|
@@ -3846,8 +3925,8 @@ async function collectSessionSummaries(turns, projectId, provider, opts = {}) {
|
|
|
3846
3925
|
}
|
|
3847
3926
|
|
|
3848
3927
|
// src/collectors/shell-history.ts
|
|
3849
|
-
var
|
|
3850
|
-
var
|
|
3928
|
+
var DEFAULT_MAX_BODY_CHARS5 = 1e3;
|
|
3929
|
+
var MAX_TITLE_CHARS7 = 200;
|
|
3851
3930
|
var NOISE = /^(cd|ls|dir|pwd|clear|cls|exit|history|whoami|date|type|cat|more|less|ll|la)\b/i;
|
|
3852
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;
|
|
3853
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;
|
|
@@ -3870,7 +3949,7 @@ function scoreShellCommand(entry) {
|
|
|
3870
3949
|
else if (cmd.length <= 3) score -= 0.05;
|
|
3871
3950
|
return Number(Math.min(1, Math.max(0.05, score)).toFixed(3));
|
|
3872
3951
|
}
|
|
3873
|
-
function
|
|
3952
|
+
function renderBody2(entry, maxChars) {
|
|
3874
3953
|
const parts = [`$ ${entry.command}`];
|
|
3875
3954
|
const metaLine = [];
|
|
3876
3955
|
if (entry.cwd) metaLine.push(`cwd: ${entry.cwd}`);
|
|
@@ -3879,8 +3958,8 @@ function renderBody(entry, maxChars) {
|
|
|
3879
3958
|
if (metaLine.length) parts.push("", metaLine.join(" "));
|
|
3880
3959
|
return truncate(parts.join("\n"), maxChars);
|
|
3881
3960
|
}
|
|
3882
|
-
function
|
|
3883
|
-
const maxBody = opts.maxBodyChars ??
|
|
3961
|
+
function toMemoryNode3(entry, projectId, opts = {}) {
|
|
3962
|
+
const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS5;
|
|
3884
3963
|
const titleLine = entry.command.split(/\r?\n/)[0] ?? entry.command;
|
|
3885
3964
|
return {
|
|
3886
3965
|
id: makeNodeId(projectId, "shell_command", entry.naturalKey),
|
|
@@ -3888,8 +3967,8 @@ function toMemoryNode2(entry, projectId, opts = {}) {
|
|
|
3888
3967
|
projectId,
|
|
3889
3968
|
ts: entry.ts,
|
|
3890
3969
|
source: `shell:${entry.shell}`,
|
|
3891
|
-
title: truncate(titleLine,
|
|
3892
|
-
body:
|
|
3970
|
+
title: truncate(titleLine, MAX_TITLE_CHARS7),
|
|
3971
|
+
body: renderBody2(entry, maxBody),
|
|
3893
3972
|
files: [],
|
|
3894
3973
|
signal: scoreShellCommand(entry),
|
|
3895
3974
|
provenance: "observed",
|
|
@@ -3904,7 +3983,7 @@ function toMemoryNode2(entry, projectId, opts = {}) {
|
|
|
3904
3983
|
};
|
|
3905
3984
|
}
|
|
3906
3985
|
function collectShellHistory(entries, projectId, opts = {}) {
|
|
3907
|
-
return entries.map((entry) =>
|
|
3986
|
+
return entries.map((entry) => toMemoryNode3(entry, projectId, opts));
|
|
3908
3987
|
}
|
|
3909
3988
|
|
|
3910
3989
|
// src/conversation/claude-code-reader.ts
|
|
@@ -4036,6 +4115,112 @@ async function readDocFiles(repoRoot, opts = {}) {
|
|
|
4036
4115
|
return { files, unreadable };
|
|
4037
4116
|
}
|
|
4038
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
|
+
|
|
4039
4224
|
// src/slm/contradiction.ts
|
|
4040
4225
|
var MAX_BODY_CHARS = 1500;
|
|
4041
4226
|
var MAX_REASON_CHARS = 200;
|
|
@@ -5190,6 +5375,38 @@ async function syncDocs(store, projectId, repoRoot, config, log) {
|
|
|
5190
5375
|
}
|
|
5191
5376
|
return { totals, seen: nodes.length };
|
|
5192
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
|
+
}
|
|
5193
5410
|
async function syncStructure(store, projectId, repoRoot, config, log) {
|
|
5194
5411
|
if (!config.sources.structure.enabled) {
|
|
5195
5412
|
log(`${pc7.dim("structure")} disabled in config`);
|
|
@@ -5312,6 +5529,7 @@ async function runSync(opts) {
|
|
|
5312
5529
|
const conversation = syncConversation(store, projectId, turns, config, log, opts.conversationOverride);
|
|
5313
5530
|
const sessions = await syncSessions(store, projectId, turns, config, log);
|
|
5314
5531
|
const docs = await syncDocs(store, projectId, repo.root, config, log);
|
|
5532
|
+
const github = await syncGithub(store, projectId, opts, repo, config, log);
|
|
5315
5533
|
const structure = await syncStructure(store, projectId, repo.root, config, log);
|
|
5316
5534
|
let embedLine = "";
|
|
5317
5535
|
let embeddingAvailable = false;
|
|
@@ -5354,6 +5572,7 @@ async function runSync(opts) {
|
|
|
5354
5572
|
addStats(totals, conversation.totals);
|
|
5355
5573
|
addStats(totals, sessions.totals);
|
|
5356
5574
|
addStats(totals, docs.totals);
|
|
5575
|
+
addStats(totals, github.totals);
|
|
5357
5576
|
const stats2 = store.stats(projectId);
|
|
5358
5577
|
const elapsed = ((Date.now() - started) / 1e3).toFixed(2);
|
|
5359
5578
|
const conversationPart = conversationEnabled ? `, ${conversation.seen} conversation exchange(s)` : "";
|
|
@@ -5361,10 +5580,12 @@ async function runSync(opts) {
|
|
|
5361
5580
|
const docsPart = config.sources.docs.enabled ? `, ${docs.seen} doc section(s)` : "";
|
|
5362
5581
|
const diffPart = config.sources.diff.enabled ? `, ${diffs.seen} file diff(s)` : "";
|
|
5363
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)` : "";
|
|
5364
5585
|
const deniedPart = totals.denied > 0 ? ` ${pc7.red(`-${totals.denied} denied`)}` : "";
|
|
5365
5586
|
out(
|
|
5366
5587
|
[
|
|
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`,
|
|
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`,
|
|
5368
5589
|
` ${pc7.green(`+${totals.inserted} new`)} ${pc7.yellow(`~${totals.updated} updated`)} ${pc7.dim(`=${totals.unchanged} unchanged`)}${deniedPart}`,
|
|
5369
5590
|
` ${pc7.dim(`${stats2.total} node(s) total across ${stats2.distinctFiles} file path(s)`)}`,
|
|
5370
5591
|
""
|
|
@@ -5482,7 +5703,7 @@ function createServer() {
|
|
|
5482
5703
|
"search_memory",
|
|
5483
5704
|
{
|
|
5484
5705
|
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
|
|
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.",
|
|
5486
5707
|
inputSchema: {
|
|
5487
5708
|
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5488
5709
|
query: z4.string().describe("Free-text question or search terms"),
|
|
@@ -5515,7 +5736,7 @@ function createServer() {
|
|
|
5515
5736
|
"sync_project",
|
|
5516
5737
|
{
|
|
5517
5738
|
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.",
|
|
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.",
|
|
5519
5740
|
inputSchema: {
|
|
5520
5741
|
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5521
5742
|
pruneSource: z4.string().optional().describe('Delete every node from this exact source (e.g. "shell:pwsh") instead of syncing'),
|
|
@@ -5549,7 +5770,7 @@ function createServer() {
|
|
|
5549
5770
|
"list_recent_memory",
|
|
5550
5771
|
{
|
|
5551
5772
|
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
|
|
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.",
|
|
5553
5774
|
inputSchema: {
|
|
5554
5775
|
projectRoot: z4.string().describe("Absolute path to the repository root"),
|
|
5555
5776
|
limit: z4.number().int().positive().optional().describe("Max items to return, newest first. Default 20.")
|
|
@@ -5822,6 +6043,7 @@ var SHELL_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
|
5822
6043
|
var CONVERSATION_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
5823
6044
|
var DOCS_SIGNAL_BANDS = { high: 0.55, medium: 0.35 };
|
|
5824
6045
|
var DIFF_SIGNAL_BANDS = { high: 0.7, medium: 0.45 };
|
|
6046
|
+
var GITHUB_SIGNAL_BANDS = { high: 0.6, medium: 0.4 };
|
|
5825
6047
|
function signalBand(signal, bands) {
|
|
5826
6048
|
if (signal >= bands.high) return "high";
|
|
5827
6049
|
if (signal >= bands.medium) return "medium";
|
|
@@ -6029,14 +6251,57 @@ function formatNode4(node) {
|
|
|
6029
6251
|
].join(" ");
|
|
6030
6252
|
}
|
|
6031
6253
|
|
|
6032
|
-
// src/cli/commands/scan-
|
|
6254
|
+
// src/cli/commands/scan-github.ts
|
|
6033
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";
|
|
6034
6299
|
async function runScanSession(opts) {
|
|
6035
6300
|
const repo = await readRepoInfo(opts.cwd);
|
|
6036
6301
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
6037
6302
|
const turns = await collectClaudeCodeTranscripts(repo.root);
|
|
6038
6303
|
if (turns.length === 0) {
|
|
6039
|
-
process.stderr.write(`${
|
|
6304
|
+
process.stderr.write(`${pc17.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
|
|
6040
6305
|
`);
|
|
6041
6306
|
return 0;
|
|
6042
6307
|
}
|
|
@@ -6044,7 +6309,7 @@ async function runScanSession(opts) {
|
|
|
6044
6309
|
const settled = selectSettledSessions(sessions, opts.settleMinutes);
|
|
6045
6310
|
if (!opts.json) {
|
|
6046
6311
|
process.stderr.write(
|
|
6047
|
-
`${
|
|
6312
|
+
`${pc17.dim("sessions")} ${sessions.length} found, ${settled.length} settled (quiet ${opts.settleMinutes}m+)
|
|
6048
6313
|
|
|
6049
6314
|
`
|
|
6050
6315
|
);
|
|
@@ -6070,7 +6335,7 @@ async function runScanSession(opts) {
|
|
|
6070
6335
|
}
|
|
6071
6336
|
for (const preview of previews) {
|
|
6072
6337
|
process.stdout.write(
|
|
6073
|
-
`${
|
|
6338
|
+
`${pc17.bold(preview.sessionKey)} ${preview.startedAt.slice(0, 16).replace("T", " ")} ${preview.includedTurns}/${preview.turns} turn(s), ${preview.promptChars} chars
|
|
6074
6339
|
${preview.prompt}
|
|
6075
6340
|
|
|
6076
6341
|
`
|
|
@@ -6082,7 +6347,7 @@ ${preview.prompt}
|
|
|
6082
6347
|
settleMinutes: opts.settleMinutes,
|
|
6083
6348
|
maxSessions: opts.maxSessions,
|
|
6084
6349
|
onProgress: (done, total) => {
|
|
6085
|
-
if (!opts.json) process.stderr.write(` ${
|
|
6350
|
+
if (!opts.json) process.stderr.write(` ${pc17.dim(`summarizing ${done}/${total}`)}
|
|
6086
6351
|
`);
|
|
6087
6352
|
}
|
|
6088
6353
|
});
|
|
@@ -6092,21 +6357,21 @@ ${preview.prompt}
|
|
|
6092
6357
|
return 0;
|
|
6093
6358
|
}
|
|
6094
6359
|
for (const node of result.nodes) {
|
|
6095
|
-
process.stdout.write(`${
|
|
6096
|
-
${
|
|
6360
|
+
process.stdout.write(`${pc17.bold(node.title)}
|
|
6361
|
+
${pc17.dim(node.ts.slice(0, 16).replace("T", " "))}
|
|
6097
6362
|
${node.body}
|
|
6098
6363
|
|
|
6099
6364
|
`);
|
|
6100
6365
|
}
|
|
6101
6366
|
if (result.providerUnavailable) {
|
|
6102
6367
|
process.stderr.write(
|
|
6103
|
-
`${
|
|
6368
|
+
`${pc17.yellow("model unavailable")} -- is Ollama running with \`${opts.model}\` pulled? (\`ollama pull ${opts.model}\`)
|
|
6104
6369
|
`
|
|
6105
6370
|
);
|
|
6106
6371
|
return 0;
|
|
6107
6372
|
}
|
|
6108
6373
|
process.stderr.write(
|
|
6109
|
-
`${
|
|
6374
|
+
`${pc17.bold(String(result.nodes.length))} summarized` + (result.failed > 0 ? `, ${pc17.yellow(`${result.failed} failed`)}` : "") + ` ${pc17.dim(`(model ${opts.model})`)}
|
|
6110
6375
|
`
|
|
6111
6376
|
);
|
|
6112
6377
|
return 0;
|
|
@@ -6114,16 +6379,16 @@ ${node.body}
|
|
|
6114
6379
|
var SCAN_SESSION_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
6115
6380
|
|
|
6116
6381
|
// src/cli/commands/scan-shell.ts
|
|
6117
|
-
import
|
|
6382
|
+
import pc18 from "picocolors";
|
|
6118
6383
|
async function runScanShell(opts) {
|
|
6119
6384
|
const repo = await readRepoInfo(opts.cwd);
|
|
6120
6385
|
const projectId = makeProjectId({ root: repo.root, originUrl: repo.originUrl });
|
|
6121
6386
|
const results = await collectAvailableShellHistory({ tailLines: opts.tailLines, repoRoot: repo.root });
|
|
6122
6387
|
if (!opts.json) {
|
|
6123
6388
|
process.stderr.write(
|
|
6124
|
-
results.length ? `${
|
|
6389
|
+
results.length ? `${pc18.dim("sources found")} ${results.map((r) => r.name).join(", ")}
|
|
6125
6390
|
|
|
6126
|
-
` : `${
|
|
6391
|
+
` : `${pc18.yellow("no shell history source found on this machine")}
|
|
6127
6392
|
`
|
|
6128
6393
|
);
|
|
6129
6394
|
}
|
|
@@ -6132,9 +6397,9 @@ async function runScanShell(opts) {
|
|
|
6132
6397
|
const nodes = collectShellHistory(result.entries, projectId).filter((n) => n.signal >= opts.minSignal);
|
|
6133
6398
|
allNodes.push(...nodes);
|
|
6134
6399
|
if (!opts.json) {
|
|
6135
|
-
process.stdout.write(`${
|
|
6400
|
+
process.stdout.write(`${pc18.bold(`shell:${result.name}`)} ${pc18.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
|
|
6136
6401
|
`);
|
|
6137
|
-
for (const node of nodes) process.stdout.write(`${
|
|
6402
|
+
for (const node of nodes) process.stdout.write(`${formatNode6(node)}
|
|
6138
6403
|
`);
|
|
6139
6404
|
process.stdout.write("\n");
|
|
6140
6405
|
}
|
|
@@ -6145,19 +6410,19 @@ async function runScanShell(opts) {
|
|
|
6145
6410
|
return 0;
|
|
6146
6411
|
}
|
|
6147
6412
|
const approxTotal = approxTotalTokens(allNodes);
|
|
6148
|
-
process.stderr.write(`${
|
|
6413
|
+
process.stderr.write(`${pc18.bold(String(allNodes.length))} node(s) total ${pc18.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
|
|
6149
6414
|
`);
|
|
6150
6415
|
return 0;
|
|
6151
6416
|
}
|
|
6152
|
-
function
|
|
6153
|
-
const approx = node.meta.tsApprox ?
|
|
6417
|
+
function formatNode6(node) {
|
|
6418
|
+
const approx = node.meta.tsApprox ? pc18.dim("~") : " ";
|
|
6154
6419
|
const exit = node.meta.exitCode;
|
|
6155
|
-
const exitLabel = typeof exit === "number" && exit !== 0 ?
|
|
6420
|
+
const exitLabel = typeof exit === "number" && exit !== 0 ? pc18.red(`exit ${exit}`) : "";
|
|
6156
6421
|
return [formatSignal(node.signal, SHELL_SIGNAL_BANDS), approx + node.ts.slice(0, 16).replace("T", " "), node.title, exitLabel].filter(Boolean).join(" ");
|
|
6157
6422
|
}
|
|
6158
6423
|
|
|
6159
6424
|
// src/cli/commands/scan-structure.ts
|
|
6160
|
-
import
|
|
6425
|
+
import pc19 from "picocolors";
|
|
6161
6426
|
var TRACKED_EXTENSIONS = TRACKED_PATHSPECS.map((p) => p.replace("*", "")).join("/");
|
|
6162
6427
|
async function runScanStructure(opts) {
|
|
6163
6428
|
const repo = await readRepoInfo(opts.cwd);
|
|
@@ -6168,24 +6433,24 @@ async function runScanStructure(opts) {
|
|
|
6168
6433
|
return 0;
|
|
6169
6434
|
}
|
|
6170
6435
|
if (unreadable.length > 0) {
|
|
6171
|
-
process.stderr.write(`${
|
|
6436
|
+
process.stderr.write(`${pc19.yellow("unreadable")} ${unreadable.join(", ")}
|
|
6172
6437
|
|
|
6173
6438
|
`);
|
|
6174
6439
|
}
|
|
6175
6440
|
for (const edge of edges) {
|
|
6176
|
-
process.stdout.write(`${edge.fromPath} ${
|
|
6441
|
+
process.stdout.write(`${edge.fromPath} ${pc19.dim("->")} ${edge.toPath}
|
|
6177
6442
|
`);
|
|
6178
6443
|
}
|
|
6179
6444
|
process.stderr.write(
|
|
6180
6445
|
`
|
|
6181
|
-
${
|
|
6446
|
+
${pc19.bold(String(edges.length))} edge(s) from ${filesScanned} tracked ${TRACKED_EXTENSIONS} file(s)
|
|
6182
6447
|
`
|
|
6183
6448
|
);
|
|
6184
6449
|
return 0;
|
|
6185
6450
|
}
|
|
6186
6451
|
|
|
6187
6452
|
// src/cli/commands/stale.ts
|
|
6188
|
-
import
|
|
6453
|
+
import pc20 from "picocolors";
|
|
6189
6454
|
var STALE_DEFAULT_MODEL = DEFAULT_SLM_MODEL;
|
|
6190
6455
|
async function runStale(opts) {
|
|
6191
6456
|
const { projectId, ws } = await loadContext(opts.cwd);
|
|
@@ -6195,15 +6460,15 @@ async function runStale(opts) {
|
|
|
6195
6460
|
if (opts.dismiss) {
|
|
6196
6461
|
const dismissed = store.dismissContradictionSuggestion(projectId, opts.dismiss);
|
|
6197
6462
|
out(
|
|
6198
|
-
dismissed > 0 ? `${
|
|
6199
|
-
` : `${
|
|
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
|
|
6200
6465
|
`
|
|
6201
6466
|
);
|
|
6202
6467
|
return 0;
|
|
6203
6468
|
}
|
|
6204
6469
|
const candidates = store.listStaleCandidates(projectId, { minAgeDays: opts.minAgeDays, limit: opts.limit });
|
|
6205
6470
|
if (candidates.length === 0) {
|
|
6206
|
-
out(`${
|
|
6471
|
+
out(`${pc20.dim("no stale candidates")} -- no unconfirmed node older than the threshold lacks a successor
|
|
6207
6472
|
`);
|
|
6208
6473
|
return 0;
|
|
6209
6474
|
}
|
|
@@ -6225,15 +6490,15 @@ async function runStale(opts) {
|
|
|
6225
6490
|
for (const s of suggestions) byCandidateId.set(s.candidateId, s);
|
|
6226
6491
|
out(
|
|
6227
6492
|
[
|
|
6228
|
-
`${
|
|
6493
|
+
`${pc20.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
|
|
6229
6494
|
...candidates.map((c) => {
|
|
6230
|
-
const line = ` ${
|
|
6495
|
+
const line = ` ${pc20.dim(c.id)} ${pc20.yellow(`${c.ageDays}d old`)} [${c.kind}] ${c.title}`;
|
|
6231
6496
|
const hit = byCandidateId.get(c.id);
|
|
6232
6497
|
return hit ? `${line}
|
|
6233
|
-
${
|
|
6498
|
+
${pc20.red("likely superseded by")} ${pc20.dim(hit.againstId)} ${hit.againstTitle} -- ${hit.reason}` : line;
|
|
6234
6499
|
}),
|
|
6235
6500
|
"",
|
|
6236
|
-
`run ${
|
|
6501
|
+
`run ${pc20.bold("nexusmem mark-stale <id> --supersedes <newId>")} on any that are actually wrong`
|
|
6237
6502
|
].join("\n").concat("\n")
|
|
6238
6503
|
);
|
|
6239
6504
|
return 0;
|
|
@@ -6245,7 +6510,7 @@ async function runStale(opts) {
|
|
|
6245
6510
|
// src/cli/commands/status.ts
|
|
6246
6511
|
import { basename as basename4 } from "path";
|
|
6247
6512
|
import { statSync } from "fs";
|
|
6248
|
-
import
|
|
6513
|
+
import pc21 from "picocolors";
|
|
6249
6514
|
function daySpan(oldest, newest) {
|
|
6250
6515
|
const oldestDay = Date.parse(oldest.slice(0, 10));
|
|
6251
6516
|
const newestDay = Date.parse(newest.slice(0, 10));
|
|
@@ -6305,34 +6570,34 @@ async function runStatus(opts) {
|
|
|
6305
6570
|
const flaggedCount = store.countContradictionSuggestions(projectId);
|
|
6306
6571
|
const dbBytes = fileSize(ws.dbPath) + fileSize(`${ws.dbPath}-wal`);
|
|
6307
6572
|
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 ? `${
|
|
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(
|
|
6309
6574
|
"nexusmem sync --prune-source <name>"
|
|
6310
6575
|
)} to remove stale source data` : "";
|
|
6311
6576
|
out(
|
|
6312
6577
|
[
|
|
6313
|
-
`${
|
|
6314
|
-
`${
|
|
6315
|
-
`${
|
|
6316
|
-
`${
|
|
6317
|
-
`${
|
|
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)})`)}`,
|
|
6318
6583
|
staleProjectWarning,
|
|
6319
6584
|
"",
|
|
6320
|
-
`${
|
|
6585
|
+
`${pc21.bold(String(stats2.total))} node(s)${stats2.total ? ` ${pc21.dim(`${stats2.oldest?.slice(0, 10)} .. ${stats2.newest?.slice(0, 10)}`)}` : ""}`,
|
|
6321
6586
|
...kinds,
|
|
6322
|
-
stats2.total ? ` ${
|
|
6587
|
+
stats2.total ? ` ${pc21.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
|
|
6323
6588
|
"",
|
|
6324
|
-
sources.length ?
|
|
6589
|
+
sources.length ? pc21.dim("sources") : pc21.yellow("no sources synced yet"),
|
|
6325
6590
|
...sources.map((s) => {
|
|
6326
6591
|
const when = s.lastRunAt ? new Date(s.lastRunAt).toISOString().slice(0, 16).replace("T", " ") : "never";
|
|
6327
6592
|
const cursorLabel = s.source === "git" ? s.cursor?.slice(0, 7) ?? "-" : s.cursor ?? "-";
|
|
6328
|
-
return ` ${s.source.padEnd(14)} ${
|
|
6593
|
+
return ` ${s.source.padEnd(14)} ${pc21.dim(`last run ${when}`)} ${pc21.dim(`cursor ${cursorLabel}`)}`;
|
|
6329
6594
|
}),
|
|
6330
6595
|
"",
|
|
6331
|
-
gitCursor && gitCursor !== repo.head ? `${
|
|
6332
|
-
chains.failuresTotal ? `${
|
|
6333
|
-
structure.edges ? `${
|
|
6334
|
-
staleCount ? `${
|
|
6335
|
-
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` : ""
|
|
6336
6601
|
].filter((line) => line !== "").join("\n").concat("\n")
|
|
6337
6602
|
);
|
|
6338
6603
|
return 0;
|
|
@@ -6355,7 +6620,7 @@ function guard(run) {
|
|
|
6355
6620
|
process.exitCode = await run();
|
|
6356
6621
|
} catch (err) {
|
|
6357
6622
|
if (isExpected(err)) {
|
|
6358
|
-
process.stderr.write(`${
|
|
6623
|
+
process.stderr.write(`${pc22.red("error")} ${err.message}
|
|
6359
6624
|
`);
|
|
6360
6625
|
process.exitCode = 1;
|
|
6361
6626
|
return;
|
|
@@ -6371,7 +6636,7 @@ program.command("init").description("Create the .nexusmem workspace and database
|
|
|
6371
6636
|
() => runInit({ cwd: options.cwd, force: options.force, hook: options.hook, enableConversation: options.enableConversation })
|
|
6372
6637
|
)()
|
|
6373
6638
|
);
|
|
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(
|
|
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(
|
|
6375
6640
|
"--embed-limit <count>",
|
|
6376
6641
|
"stop embedding after this many nodes (default: embed everything pending)",
|
|
6377
6642
|
(v) => Number.parseInt(v, 10)
|
|
@@ -6392,6 +6657,7 @@ program.command("sync").description("Ingest new history into the local database"
|
|
|
6392
6657
|
since: options.since,
|
|
6393
6658
|
shellTailLines: options.shellLines,
|
|
6394
6659
|
conversationOverride: options.conversation ? true : void 0,
|
|
6660
|
+
githubOverride: options.github ? true : void 0,
|
|
6395
6661
|
noEmbed: !options.embed,
|
|
6396
6662
|
embedLimit: options.embedLimit,
|
|
6397
6663
|
pruneSource: options.pruneSource,
|
|
@@ -6523,6 +6789,7 @@ program.command("scan-session").description("Preview the session summaries a loc
|
|
|
6523
6789
|
)()
|
|
6524
6790
|
);
|
|
6525
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 }))());
|
|
6526
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(
|
|
6527
6794
|
(options) => guard(
|
|
6528
6795
|
() => runPrecheck({
|
|
@@ -6538,7 +6805,7 @@ program.command("scan-structure").description("Preview the JS/TS/Python/Go/Rust/
|
|
|
6538
6805
|
program.command("mcp").description("Start the MCP server (stdio transport) for Claude Desktop, Cursor, Windsurf, etc.").action(() => guard(() => runMcpServer().then(() => 0))());
|
|
6539
6806
|
program.parseAsync(process.argv).catch((err) => {
|
|
6540
6807
|
const message = err instanceof Error ? err.message : String(err);
|
|
6541
|
-
process.stderr.write(`${
|
|
6808
|
+
process.stderr.write(`${pc22.red("error")} ${message}
|
|
6542
6809
|
`);
|
|
6543
6810
|
process.exitCode = 1;
|
|
6544
6811
|
});
|