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/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),
@@ -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 n.project_id = ?
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, overfetch, projectId, asOfEpoch, asOfEpoch, limit);
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 MAX_TITLE_CHARS5 = 200;
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", MAX_TITLE_CHARS5);
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, MAX_TITLE_CHARS5) : fallback,
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 DEFAULT_MAX_BODY_CHARS3 = 2500;
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 ?? DEFAULT_MAX_BODY_CHARS3;
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 DEFAULT_MAX_BODY_CHARS4 = 1e3;
3829
- var MAX_TITLE_CHARS6 = 200;
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 renderBody(entry, maxChars) {
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 toMemoryNode2(entry, projectId, opts = {}) {
3862
- const maxBody = opts.maxBodyChars ?? DEFAULT_MAX_BODY_CHARS4;
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, MAX_TITLE_CHARS6),
3871
- body: renderBody(entry, maxBody),
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) => toMemoryNode2(entry, projectId, opts));
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 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.",
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 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.",
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-session.ts
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(`${pc16.yellow("no transcripts found")} at ${claudeProjectTranscriptDir(repo.root)}
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
- `${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+)
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
- `${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
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(` ${pc16.dim(`summarizing ${done}/${total}`)}
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(`${pc16.bold(node.title)}
6075
- ${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", " "))}
6076
6362
  ${node.body}
6077
6363
 
6078
6364
  `);
6079
6365
  }
6080
6366
  if (result.providerUnavailable) {
6081
6367
  process.stderr.write(
6082
- `${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}\`)
6083
6369
  `
6084
6370
  );
6085
6371
  return 0;
6086
6372
  }
6087
6373
  process.stderr.write(
6088
- `${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})`)}
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 pc17 from "picocolors";
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 ? `${pc17.dim("sources found")} ${results.map((r) => r.name).join(", ")}
6389
+ results.length ? `${pc18.dim("sources found")} ${results.map((r) => r.name).join(", ")}
6104
6390
 
6105
- ` : `${pc17.yellow("no shell history source found on this machine")}
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(`${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)`)}
6115
6401
  `);
6116
- for (const node of nodes) process.stdout.write(`${formatNode5(node)}
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(`${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`)}
6128
6414
  `);
6129
6415
  return 0;
6130
6416
  }
6131
- function formatNode5(node) {
6132
- const approx = node.meta.tsApprox ? pc17.dim("~") : " ";
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 ? pc17.red(`exit ${exit}`) : "";
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 pc18 from "picocolors";
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(`${pc18.yellow("unreadable")} ${unreadable.join(", ")}
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} ${pc18.dim("->")} ${edge.toPath}
6441
+ process.stdout.write(`${edge.fromPath} ${pc19.dim("->")} ${edge.toPath}
6156
6442
  `);
6157
6443
  }
6158
6444
  process.stderr.write(
6159
6445
  `
6160
- ${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)
6161
6447
  `
6162
6448
  );
6163
6449
  return 0;
6164
6450
  }
6165
6451
 
6166
6452
  // src/cli/commands/stale.ts
6167
- import pc19 from "picocolors";
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 ? `${pc19.green("dismissed")} the contradiction suggestion for ${opts.dismiss} -- it will not resurface
6178
- ` : `${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
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(`${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
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
- `${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:`,
6208
6494
  ...candidates.map((c) => {
6209
- 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}`;
6210
6496
  const hit = byCandidateId.get(c.id);
6211
6497
  return hit ? `${line}
6212
- ${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;
6213
6499
  }),
6214
6500
  "",
6215
- `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`
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 pc20 from "picocolors";
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 ? `${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(
6288
6574
  "nexusmem sync --prune-source <name>"
6289
6575
  )} to remove stale source data` : "";
6290
6576
  out(
6291
6577
  [
6292
- `${pc20.dim("repo ")} ${repo.root}`,
6293
- `${pc20.dim("branch ")} ${repo.branch ?? pc20.yellow("(detached)")}`,
6294
- `${pc20.dim("project ")} ${pc20.cyan(projectId)}`,
6295
- `${pc20.dim("schema ")} v${schema}${schema === LATEST_SCHEMA_VERSION ? "" : pc20.yellow(` (latest is v${LATEST_SCHEMA_VERSION})`)}`,
6296
- `${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)})`)}`,
6297
6583
  staleProjectWarning,
6298
6584
  "",
6299
- `${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)}`)}` : ""}`,
6300
6586
  ...kinds,
6301
- stats2.total ? ` ${pc20.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
6587
+ stats2.total ? ` ${pc21.dim(`${stats2.distinctFiles} distinct file path(s)`)}` : "",
6302
6588
  "",
6303
- sources.length ? pc20.dim("sources") : pc20.yellow("no sources synced yet"),
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)} ${pc20.dim(`last run ${when}`)} ${pc20.dim(`cursor ${cursorLabel}`)}`;
6593
+ return ` ${s.source.padEnd(14)} ${pc21.dim(`last run ${when}`)} ${pc21.dim(`cursor ${cursorLabel}`)}`;
6308
6594
  }),
6309
6595
  "",
6310
- gitCursor && gitCursor !== repo.head ? `${pc20.yellow("git behind HEAD")} \u2014 run ${pc20.bold("nexusmem sync")}` : "",
6311
- 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` : ""}` : "",
6312
- structure.edges ? `${pc20.dim("structure")} ${pc20.bold(String(structure.edges))} import edge(s) across ${structure.files} file(s)` : "",
6313
- staleCount ? `${pc20.dim("aging ")} ${pc20.bold(String(staleCount))} unconfirmed node(s) worth a look \u2014 run ${pc20.bold("nexusmem stale")}` : "",
6314
- 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` : ""
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(`${pc21.red("error")} ${err.message}
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(`${pc21.red("error")} ${message}
6808
+ process.stderr.write(`${pc22.red("error")} ${message}
6521
6809
  `);
6522
6810
  process.exitCode = 1;
6523
6811
  });