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/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 pc21 from "picocolors";
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 MAX_TITLE_CHARS5 = 200;
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", MAX_TITLE_CHARS5);
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, MAX_TITLE_CHARS5) : fallback,
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 DEFAULT_MAX_BODY_CHARS3 = 2500;
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 ?? DEFAULT_MAX_BODY_CHARS3;
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 DEFAULT_MAX_BODY_CHARS4 = 1e3;
3850
- var MAX_TITLE_CHARS6 = 200;
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 renderBody(entry, maxChars) {
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 toMemoryNode2(entry, projectId, opts = {}) {
3883
- const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS4;
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, MAX_TITLE_CHARS6),
3892
- body: renderBody(entry, maxBody),
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) => toMemoryNode2(entry, projectId, opts));
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 and per-session summaries. Returns a token-budgeted, ranked context block -- not raw search results.",
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 and session summaries -- newest first. Chronological, not relevance-ranked: use search_memory instead for a specific question.",
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-session.ts
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(`${pc16.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
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
- `${pc16.dim("sessions")} ${sessions.length} found, ${settled.length} settled (quiet ${opts.settleMinutes}m+)
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
- `${pc16.bold(preview.sessionKey)} ${preview.startedAt.slice(0, 16).replace("T", " ")} ${preview.includedTurns}/${preview.turns} turn(s), ${preview.promptChars} chars
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(` ${pc16.dim(`summarizing ${done}/${total}`)}
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(`${pc16.bold(node.title)}
6096
- ${pc16.dim(node.ts.slice(0, 16).replace("T", " "))}
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
- `${pc16.yellow("model unavailable")} -- is Ollama running with \`${opts.model}\` pulled? (\`ollama pull ${opts.model}\`)
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
- `${pc16.bold(String(result.nodes.length))} summarized` + (result.failed > 0 ? `, ${pc16.yellow(`${result.failed} failed`)}` : "") + ` ${pc16.dim(`(model ${opts.model})`)}
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 pc17 from "picocolors";
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 ? `${pc17.dim("sources found")} ${results.map((r) => r.name).join(", ")}
6389
+ results.length ? `${pc18.dim("sources found")} ${results.map((r) => r.name).join(", ")}
6125
6390
 
6126
- ` : `${pc17.yellow("no shell history source found on this machine")}
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(`${pc17.bold(`shell:${result.name}`)} ${pc17.dim(`(${nodes.length} of ${result.entries.length} above threshold)`)}
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(`${formatNode5(node)}
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(`${pc17.bold(String(allNodes.length))} node(s) total ${pc17.dim(`~${approxTotal.toLocaleString()} tokens if sent raw`)}
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 formatNode5(node) {
6153
- const approx = node.meta.tsApprox ? pc17.dim("~") : " ";
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 ? pc17.red(`exit ${exit}`) : "";
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 pc18 from "picocolors";
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(`${pc18.yellow("unreadable")} ${unreadable.join(", ")}
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} ${pc18.dim("->")} ${edge.toPath}
6441
+ process.stdout.write(`${edge.fromPath} ${pc19.dim("->")} ${edge.toPath}
6177
6442
  `);
6178
6443
  }
6179
6444
  process.stderr.write(
6180
6445
  `
6181
- ${pc18.bold(String(edges.length))} edge(s) from ${filesScanned} tracked ${TRACKED_EXTENSIONS} file(s)
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 pc19 from "picocolors";
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 ? `${pc19.green("dismissed")} the contradiction suggestion for ${opts.dismiss} -- it will not resurface
6199
- ` : `${pc19.dim("no open contradiction suggestion")} for ${opts.dismiss} -- nothing to dismiss
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(`${pc19.dim("no stale candidates")} -- no unconfirmed node older than the threshold lacks a successor
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
- `${pc19.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
6493
+ `${pc20.bold(String(candidates.length))} stale candidate(s) -- oldest first, none of these were changed:`,
6229
6494
  ...candidates.map((c) => {
6230
- const line = ` ${pc19.dim(c.id)} ${pc19.yellow(`${c.ageDays}d old`)} [${c.kind}] ${c.title}`;
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
- ${pc19.red("likely superseded by")} ${pc19.dim(hit.againstId)} ${hit.againstTitle} -- ${hit.reason}` : line;
6498
+ ${pc20.red("likely superseded by")} ${pc20.dim(hit.againstId)} ${hit.againstTitle} -- ${hit.reason}` : line;
6234
6499
  }),
6235
6500
  "",
6236
- `run ${pc19.bold("nexusmem mark-stale <id> --supersedes <newId>")} on any that are actually wrong`
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 pc20 from "picocolors";
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 ? `${pc20.yellow("stale ")} ${otherProjectIds.length} prior project ${otherProjectIds.length === 1 ? "identity holds" : "identities hold"} ${otherProjectNodes} node(s) \u2014 run ${pc20.bold(
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
- `${pc20.dim("repo ")} ${repo.root}`,
6314
- `${pc20.dim("branch ")} ${repo.branch ?? pc20.yellow("(detached)")}`,
6315
- `${pc20.dim("project ")} ${pc20.cyan(projectId)}`,
6316
- `${pc20.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc20.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
6317
- `${pc20.dim("database")} ${ws.dbPath} ${pc20.dim(`(${humanBytes(dbBytes)})`)}`,
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
- `${pc20.bold(String(stats2.total))} node(s)${stats2.total ? ` ${pc20.dim(`${stats2.oldest?.slice(0, 10)} .. ${stats2.newest?.slice(0, 10)}`)}` : ""}`,
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 ? ` ${pc20.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
6587
+ stats2.total ? ` ${pc21.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
6323
6588
  "",
6324
- sources.length ? pc20.dim("sources") : pc20.yellow("no sources synced yet"),
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)} ${pc20.dim(`last run ${when}`)} ${pc20.dim(`cursor ${cursorLabel}`)}`;
6593
+ return ` ${s.source.padEnd(14)} ${pc21.dim(`last run ${when}`)} ${pc21.dim(`cursor ${cursorLabel}`)}`;
6329
6594
  }),
6330
6595
  "",
6331
- gitCursor && gitCursor !== repo.head ? `${pc20.yellow("git behind HEAD")} \u2014 run ${pc20.bold("nexusmem sync")}` : "",
6332
- chains.failuresTotal ? `${pc20.dim("chains ")} ${pc20.bold(String(chains.resolvedTotal))}/${chains.failuresTotal} failure(s) resolved ${pc20.dim(`(${chains.resolvedByRetry} retry, ${chains.resolvedByDiscussion} discussion)`)}${chains.resolvedTotal < chains.failuresTotal ? ` \u2014 run ${pc20.bold("nexusmem sync --link-failures")} to link more` : ""}` : "",
6333
- structure.edges ? `${pc20.dim("structure")} ${pc20.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : "",
6334
- staleCount ? `${pc20.dim("aging ")} ${pc20.bold(String(staleCount))} unconfirmed node(s) worth a look \u2014 run ${pc20.bold("nexusmem stale")}` : "",
6335
- flaggedCount ? `${pc20.dim("flagged ")} ${pc20.bold(String(flaggedCount))} likely-superseded node(s) awaiting review \u2014 run ${pc20.bold("nexusmem stale")} for detail` : ""
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(`${pc21.red("error")} ${err.message}
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(`${pc21.red("error")} ${message}
6808
+ process.stderr.write(`${pc22.red("error")} ${message}
6542
6809
  `);
6543
6810
  process.exitCode = 1;
6544
6811
  });