indelible-mcp 4.9.6 → 4.9.8

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +466 -188
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "4.9.6",
3
+ "version": "4.9.8",
4
4
  "description": "Blockchain-backed memory and code storage for Claude Code. Save AI conversations and source code permanently on BSV.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -1489,6 +1489,13 @@ function appendReceipt(receipt, meta = {}) {
1489
1489
  trigger: meta.trigger || "interactive",
1490
1490
  // interactive | auto (background/hook)
1491
1491
  label: meta.label || null,
1492
+ // g-363 self-reconciling ledger (Ben's ask, 2026-07-16): persist the content
1493
+ // identity so a stranded receipt can ALWAYS be reconciled by content — even
1494
+ // months later, after the 60-min tx-cache has pruned. This is the durable
1495
+ // targetKey for findByContentKey; null for saves with no single content hash
1496
+ // (e.g. session deltas). Cheap to write, and it removes the biggest
1497
+ // "cannot learn content key" unresolvable bucket for good.
1498
+ contentHash: receipt.contentHash ?? receipt.content_hash ?? meta.contentHash ?? null,
1492
1499
  surfaced: false
1493
1500
  // has the IDE shown this entry yet?
1494
1501
  };
@@ -3305,16 +3312,16 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
3305
3312
  let resolvedHistoryPath = "~/.claude/projects/<project>/memory/session-history.md";
3306
3313
  try {
3307
3314
  const projectsDir = join10(homedir10(), ".claude", "projects");
3308
- const { readdirSync: readdirSync3, statSync: statSync6 } = await import("fs");
3315
+ const { readdirSync: readdirSync3, statSync: statSync7 } = await import("fs");
3309
3316
  let newestProject = null;
3310
3317
  let newestTime = 0;
3311
3318
  for (const project of readdirSync3(projectsDir)) {
3312
3319
  const projectPath = join10(projectsDir, project);
3313
3320
  try {
3314
- if (!statSync6(projectPath).isDirectory()) continue;
3321
+ if (!statSync7(projectPath).isDirectory()) continue;
3315
3322
  for (const file of readdirSync3(projectPath)) {
3316
3323
  if (!file.endsWith(".jsonl")) continue;
3317
- const fStat = statSync6(join10(projectPath, file));
3324
+ const fStat = statSync7(join10(projectPath, file));
3318
3325
  if (fStat.mtimeMs > newestTime) {
3319
3326
  newestTime = fStat.mtimeMs;
3320
3327
  newestProject = projectPath;
@@ -3887,11 +3894,12 @@ __export(recall_index_exports, {
3887
3894
  readGaps: () => readGaps,
3888
3895
  readIndex: () => readIndex,
3889
3896
  redactSecrets: () => redactSecrets,
3897
+ resetGaps: () => resetGaps,
3890
3898
  resolveSaveType: () => resolveSaveType,
3891
3899
  searchIndex: () => searchIndex,
3892
3900
  searchIndexSemantic: () => searchIndexSemantic
3893
3901
  });
3894
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync12, appendFileSync as appendFileSync3 } from "fs";
3902
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync8, appendFileSync as appendFileSync3 } from "fs";
3895
3903
  import { join as join18 } from "path";
3896
3904
  import { homedir as homedir15 } from "os";
3897
3905
  function ensureDirs() {
@@ -3961,6 +3969,33 @@ function readIndex(address) {
3961
3969
  function readGaps(address) {
3962
3970
  return readJsonlMap(gapsPath(address));
3963
3971
  }
3972
+ async function resetGaps(address, reasons = []) {
3973
+ const set = new Set(reasons);
3974
+ let removed = 0;
3975
+ await withFileLock(gapsLock(address), () => {
3976
+ let raw;
3977
+ try {
3978
+ raw = readFileSync12(gapsPath(address), "utf8");
3979
+ } catch {
3980
+ return;
3981
+ }
3982
+ const kept = [];
3983
+ for (const line of raw.split("\n")) {
3984
+ if (!line.trim()) continue;
3985
+ let r;
3986
+ try {
3987
+ r = JSON.parse(line);
3988
+ } catch {
3989
+ kept.push(line);
3990
+ continue;
3991
+ }
3992
+ if (set.has(r.reason)) removed++;
3993
+ else kept.push(line);
3994
+ }
3995
+ if (removed) writeFileSync8(gapsPath(address), kept.length ? kept.join("\n") + "\n" : "");
3996
+ });
3997
+ return removed;
3998
+ }
3964
3999
  async function appendJsonl(path, lockPath, records) {
3965
4000
  if (!records.length) return;
3966
4001
  ensureDirs();
@@ -4186,81 +4221,6 @@ var init_recall_index = __esm({
4186
4221
  }
4187
4222
  });
4188
4223
 
4189
- // mcp-server/lib/semantic/fetch-pack.js
4190
- var fetch_pack_exports = {};
4191
- __export(fetch_pack_exports, {
4192
- fetchPack: () => fetchPack
4193
- });
4194
- import { createHash as createHash5 } from "crypto";
4195
- import { createWriteStream, existsSync as existsSync18, mkdirSync as mkdirSync8, renameSync as renameSync3, rmSync, statSync as statSync3 } from "fs";
4196
- import { join as join20 } from "path";
4197
- async function sha256File(path) {
4198
- const { createReadStream: createReadStream3 } = await import("fs");
4199
- return new Promise((resolve3, reject) => {
4200
- const h = createHash5("sha256");
4201
- createReadStream3(path).on("data", (d) => h.update(d)).on("end", () => resolve3(h.digest("hex"))).on("error", reject);
4202
- });
4203
- }
4204
- async function fetchToTmp(url, tmpPath, timeoutMs) {
4205
- const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: "follow" });
4206
- if (!res.ok || !res.body) throw new PackError("FETCH_FAIL", `HTTP ${res.status} for ${url}`);
4207
- const { Readable } = await import("stream");
4208
- const { pipeline } = await import("stream/promises");
4209
- await pipeline(Readable.fromWeb(res.body), createWriteStream(tmpPath));
4210
- }
4211
- async function fetchPack({ profile = PROFILE, timeoutMs = 3e5, onProgress = null } = {}) {
4212
- const dir = packDir(profile);
4213
- mkdirSync8(dir, { recursive: true, mode: 448 });
4214
- const fetched = [];
4215
- const skipped = [];
4216
- for (const f of PACK_FILES) {
4217
- const finalPath = join20(dir, f.name);
4218
- if (existsSync18(finalPath) && statSync3(finalPath).size === f.bytes) {
4219
- skipped.push(f.name);
4220
- continue;
4221
- }
4222
- const tmpPath = `${finalPath}.tmp-${process.pid}`;
4223
- try {
4224
- if (onProgress) onProgress({ file: f.name, bytes: f.bytes, state: "fetching" });
4225
- await fetchToTmp(f.url, tmpPath, timeoutMs);
4226
- const gotHash = await sha256File(tmpPath);
4227
- if (gotHash !== f.sha256) {
4228
- throw new PackError(
4229
- "HASH_MISMATCH",
4230
- `${f.name}: expected ${f.sha256.slice(0, 12)}\u2026, got ${gotHash.slice(0, 12)}\u2026 \u2014 refusing (upstream drifted or tampered)`
4231
- );
4232
- }
4233
- const gotSize = statSync3(tmpPath).size;
4234
- if (gotSize !== f.bytes) {
4235
- throw new PackError("HASH_MISMATCH", `${f.name}: expected ${f.bytes} bytes, got ${gotSize}`);
4236
- }
4237
- renameSync3(tmpPath, finalPath);
4238
- fetched.push(f.name);
4239
- if (onProgress) onProgress({ file: f.name, bytes: f.bytes, state: "verified" });
4240
- } catch (err) {
4241
- try {
4242
- rmSync(tmpPath, { force: true });
4243
- } catch {
4244
- }
4245
- if (err instanceof PackError) throw err;
4246
- throw new PackError("FETCH_FAIL", `${f.name}: ${err.message}`);
4247
- }
4248
- }
4249
- return { ok: true, fetched, skipped, dir };
4250
- }
4251
- var PackError;
4252
- var init_fetch_pack = __esm({
4253
- "mcp-server/lib/semantic/fetch-pack.js"() {
4254
- init_manifest();
4255
- PackError = class extends Error {
4256
- constructor(code, message) {
4257
- super(message);
4258
- this.code = code;
4259
- }
4260
- };
4261
- }
4262
- });
4263
-
4264
4224
  // mcp-server/lib/semantic/map-model.js
4265
4225
  function computeMapLayout(dated) {
4266
4226
  const n = dated.length;
@@ -4373,17 +4333,37 @@ function computeMapLayout(dated) {
4373
4333
  };
4374
4334
  const globalTF = /* @__PURE__ */ new Map();
4375
4335
  for (const s of dated) for (const w of grams(s.title)) globalTF.set(w, (globalTF.get(w) || 0) + 1);
4376
- function nameOf(members, nTerms) {
4336
+ function nameOf(members, nTerms, allowFallback = false) {
4377
4337
  const tf = /* @__PURE__ */ new Map();
4378
4338
  for (const s of members) for (const w of grams(s.title)) tf.set(w, (tf.get(w) || 0) + 1);
4379
- const scored = [...tf.entries()].filter(([w, f]) => f >= 3).map(([w, f]) => [w, (w.includes(" ") ? 1.6 : 1) * f * f / (globalTF.get(w) || 1)]).sort((a, b) => b[1] - a[1]);
4339
+ const minF = Math.min(3, Math.max(1, Math.round(members.length * 0.2)));
4340
+ const scored = [...tf.entries()].filter(([w, f]) => f >= minF).map(([w, f]) => [w, (w.includes(" ") ? 1.6 : 1) * f * f / (globalTF.get(w) || 1)]).sort((a, b) => b[1] - a[1]);
4380
4341
  const picked = [];
4381
4342
  for (const [w] of scored) {
4382
4343
  if (picked.some((p) => p.includes(w) || w.includes(p))) continue;
4383
4344
  picked.push(w);
4384
4345
  if (picked.length >= nTerms) break;
4385
4346
  }
4386
- return picked.join(" \xB7 ") || "(misc)";
4347
+ if (picked.length) return picked.join(" \xB7 ");
4348
+ if (!allowFallback) return "(misc)";
4349
+ let best = "";
4350
+ let bestScore = -1;
4351
+ for (const s of members) {
4352
+ let sc = 0;
4353
+ const seen = /* @__PURE__ */ new Set();
4354
+ for (const w of grams(s.title)) {
4355
+ if (!seen.has(w)) {
4356
+ seen.add(w);
4357
+ sc += (tf.get(w) || 0) / (globalTF.get(w) || 1);
4358
+ }
4359
+ }
4360
+ if (sc > bestScore) {
4361
+ bestScore = sc;
4362
+ best = s.title || "";
4363
+ }
4364
+ }
4365
+ const distinct = tok(best).sort((a, b) => (globalTF.get(a) || 1) - (globalTF.get(b) || 1)).slice(0, nTerms);
4366
+ return distinct.length ? distinct.join(" \xB7 ") : "(untitled)";
4387
4367
  }
4388
4368
  const centroidOf = (members) => ({
4389
4369
  cx: members.reduce((a, s) => a + s.x, 0) / (members.length || 1),
@@ -4391,7 +4371,7 @@ function computeMapLayout(dated) {
4391
4371
  cz: members.reduce((a, s) => a + s.z, 0) / (members.length || 1)
4392
4372
  });
4393
4373
  function kmeansLocal(members, k) {
4394
- if (members.length < k * 25) return null;
4374
+ if (members.length < k * 3) return null;
4395
4375
  const c0 = [members[Math.floor(rnd() * members.length)].v.slice()];
4396
4376
  while (c0.length < k) {
4397
4377
  const d = members.map((s) => Math.min(...c0.map((c) => dist2(s.v, c))));
@@ -4433,13 +4413,29 @@ function computeMapLayout(dated) {
4433
4413
  const subLabels = [];
4434
4414
  for (let c = 0; c < K; c++) {
4435
4415
  const members = dated.filter((s) => s.c === c);
4436
- const label = nameOf(members, 3);
4437
- clusterInfo.push({ c, label, count: members.length, ...centroidOf(members) });
4438
- const sub = kmeansLocal(members, 3);
4416
+ const label = nameOf(
4417
+ members,
4418
+ 3,
4419
+ /* allowFallback */
4420
+ true
4421
+ );
4422
+ const mDates = members.map((s) => s.date).sort();
4423
+ clusterInfo.push({
4424
+ c,
4425
+ label,
4426
+ count: members.length,
4427
+ member_txIds: members.map((s) => s.txId),
4428
+ first_touched: mDates.length ? mDates[0].slice(0, 10) : null,
4429
+ last_touched: mDates.length ? mDates[mDates.length - 1].slice(0, 10) : null,
4430
+ ...centroidOf(members)
4431
+ });
4432
+ const subK = members.length >= 30 ? 3 : 2;
4433
+ const subFloor = members.length >= 75 ? 25 : Math.max(4, Math.round(members.length * 0.15));
4434
+ const sub = members.length >= 12 ? kmeansLocal(members, subK) : null;
4439
4435
  if (sub) {
4440
- for (let sc = 0; sc < 3; sc++) {
4436
+ for (let sc = 0; sc < subK; sc++) {
4441
4437
  const m = members.filter((_, i) => sub[i] === sc);
4442
- if (m.length < 25) continue;
4438
+ if (m.length < subFloor) continue;
4443
4439
  const sl = nameOf(m, 2);
4444
4440
  if (sl === "(misc)" || sl === label) continue;
4445
4441
  subLabels.push({ label: sl, count: m.length, ...centroidOf(m) });
@@ -4475,8 +4471,15 @@ function computeMapLayout(dated) {
4475
4471
  }
4476
4472
  const hMax = Math.max(...heat);
4477
4473
  for (let i = 0; i < n; i++) heat[i] = Math.log1p(heat[i]) / Math.log1p(hMax);
4478
- const pts = dated.map((s, i) => ({ x: +s.X.toFixed(4), y: +s.Y.toFixed(4), z: +s.Z.toFixed(4), h: +heat[i].toFixed(3), m: s.date.slice(0, 7), d: s.date.slice(0, 10), t: s.title, c: s.c }));
4479
- const nc = (c) => ({ label: c.label, count: c.count, x: +NORM(c.cx, "x").toFixed(4), y: +NORM(c.cy, "y").toFixed(4), z: +NORM(c.cz, "z").toFixed(4) });
4474
+ const pts = dated.map((s, i) => ({ id: s.txId, x: +s.X.toFixed(4), y: +s.Y.toFixed(4), z: +s.Z.toFixed(4), h: +heat[i].toFixed(3), m: s.date.slice(0, 7), d: s.date.slice(0, 10), t: s.title, c: s.c }));
4475
+ const nc = (c) => ({
4476
+ label: c.label,
4477
+ count: c.count,
4478
+ x: +NORM(c.cx, "x").toFixed(4),
4479
+ y: +NORM(c.cy, "y").toFixed(4),
4480
+ z: +NORM(c.cz, "z").toFixed(4),
4481
+ ...c.member_txIds ? { member_txIds: c.member_txIds, first_touched: c.first_touched, last_touched: c.last_touched } : {}
4482
+ });
4480
4483
  const cls = clusterInfo.map(nc);
4481
4484
  const subs = subLabels.map(nc);
4482
4485
  return {
@@ -4504,13 +4507,20 @@ var build_map_exports = {};
4504
4507
  __export(build_map_exports, {
4505
4508
  buildSemanticMap: () => buildSemanticMap
4506
4509
  });
4507
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync8, readdirSync, statSync as statSync4 } from "node:fs";
4508
- import { join as join21 } from "node:path";
4509
- import { homedir as homedir17 } from "node:os";
4510
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync9, readdirSync, statSync as statSync3, renameSync as renameSync3 } from "node:fs";
4511
+ import { join as join19, dirname as dirname4 } from "node:path";
4512
+ import { homedir as homedir16 } from "node:os";
4513
+ function persistModel(outPath, model) {
4514
+ const modelPath = join19(dirname4(outPath), "SEMANTIC_MAP.json");
4515
+ const tmp = `${modelPath}.tmp-${process.pid}`;
4516
+ writeFileSync9(tmp, JSON.stringify({ ...model, generated_at: (/* @__PURE__ */ new Date()).toISOString() }));
4517
+ renameSync3(tmp, modelPath);
4518
+ return modelPath;
4519
+ }
4510
4520
  function buildSemanticMap({
4511
4521
  addr,
4512
- dir = join21(homedir17(), ".indelible", "recall-index"),
4513
- outPath = join21(homedir17(), ".indelible", "SEMANTIC_MAP.html"),
4522
+ dir = join19(homedir16(), ".indelible", "recall-index"),
4523
+ outPath = join19(homedir16(), ".indelible", "SEMANTIC_MAP.html"),
4514
4524
  log = () => {
4515
4525
  }
4516
4526
  } = {}) {
@@ -4521,7 +4531,7 @@ function buildSemanticMap({
4521
4531
  } catch {
4522
4532
  cands = [];
4523
4533
  }
4524
- const sized = cands.map((f) => ({ f, s: statSync4(join21(dir, f)).size })).sort((a, b) => b.s - a.s);
4534
+ const sized = cands.map((f) => ({ f, s: statSync3(join19(dir, f)).size })).sort((a, b) => b.s - a.s);
4525
4535
  if (!sized.length) {
4526
4536
  const e = new Error("no .embeds.jsonl found \u2014 run `indelible-mcp semantic-fetch` first");
4527
4537
  e.code = "NO_INDEX";
@@ -4532,7 +4542,7 @@ function buildSemanticMap({
4532
4542
  log("address: " + addr);
4533
4543
  let embedsRaw;
4534
4544
  try {
4535
- embedsRaw = readFileSync15(join21(dir, addr + ".embeds.jsonl"), "utf8");
4545
+ embedsRaw = readFileSync14(join19(dir, addr + ".embeds.jsonl"), "utf8");
4536
4546
  } catch {
4537
4547
  const e = new Error(`no semantic index for ${addr} \u2014 run \`indelible-mcp semantic-fetch\` first`);
4538
4548
  e.code = "NO_INDEX";
@@ -4574,7 +4584,7 @@ function buildSemanticMap({
4574
4584
  log("sessions with vectors: " + sessions.length);
4575
4585
  const meta = /* @__PURE__ */ new Map();
4576
4586
  try {
4577
- for (const line of readFileSync15(join21(dir, addr + ".jsonl"), "utf8").split("\n")) {
4587
+ for (const line of readFileSync14(join19(dir, addr + ".jsonl"), "utf8").split("\n")) {
4578
4588
  if (!line.trim()) continue;
4579
4589
  let r;
4580
4590
  try {
@@ -4597,16 +4607,18 @@ function buildSemanticMap({
4597
4607
  log(`dated: ${dated.length} | span: ${span.from} \u2192 ${span.to}`);
4598
4608
  if (dated.length < MIN_FOR_MAP) {
4599
4609
  const html2 = sparsePage(addr, sessions.length, dated.length);
4600
- writeFileSync8(outPath, html2);
4610
+ writeFileSync9(outPath, html2);
4611
+ persistModel(outPath, { sparse: true, count: dated.length, span });
4601
4612
  log(`wrote ${outPath} (sparse floor, ${dated.length}/${MIN_FOR_MAP})`);
4602
4613
  return { path: outPath, sessions: sessions.length, dated: dated.length, K: 0, districts: 0, sparse: true, span };
4603
4614
  }
4604
4615
  log("PCA + k-means + density (shared compute)...");
4605
4616
  const model = computeMapLayout(dated);
4606
4617
  const html = renderHtml(model);
4607
- writeFileSync8(outPath, html);
4608
- log(`wrote ${outPath} (${Math.round(html.length / 1024)} KB)`);
4609
- return { path: outPath, sessions: sessions.length, dated: dated.length, K: model.K, districts: model.subs.length, sparse: false, span };
4618
+ writeFileSync9(outPath, html);
4619
+ const modelPath = persistModel(outPath, model);
4620
+ log(`wrote ${outPath} (${Math.round(html.length / 1024)} KB) + ${modelPath}`);
4621
+ return { path: outPath, modelPath, sessions: sessions.length, dated: dated.length, K: model.K, districts: model.subs.length, sparse: false, span };
4610
4622
  }
4611
4623
  function sparsePage(addr, total, dated) {
4612
4624
  return `<!doctype html><meta charset="utf-8"><title>Indelible \u2014 The Shape of the Work</title>
@@ -4670,8 +4682,8 @@ function draw(){
4670
4682
  const vg=g.createRadialGradient(W/2,H/2,H*0.2,W/2,H/2,Math.max(W,H)*0.75);
4671
4683
  vg.addColorStop(0,'#0e0a07');vg.addColorStop(1,'#070503');g.fillStyle=vg;g.fillRect(0,0,W,H);
4672
4684
  projected=P.map(p=>({p,q:proj(p)}));
4673
- // time thread
4674
- g.strokeStyle='rgba(244,168,58,0.04)';g.beginPath();
4685
+ // time thread \u2014 opacity scales with map size (a few segments need more alpha than 5,000)
4686
+ g.strokeStyle='rgba(244,168,58,'+Math.min(0.28,Math.max(0.04,4/P.length))+')';g.beginPath();
4675
4687
  projected.forEach((e,i)=>{i?g.lineTo(e.q.X,e.q.Y):g.moveTo(e.q.X,e.q.Y)});g.stroke();
4676
4688
  // points: crisp everywhere. Heat = the DOT'S OWN color from the density ramp
4677
4689
  // (no additive accumulation \u2014 that both lagged and blew the core out to mush).
@@ -4731,6 +4743,81 @@ var init_build_map = __esm({
4731
4743
  }
4732
4744
  });
4733
4745
 
4746
+ // mcp-server/lib/semantic/fetch-pack.js
4747
+ var fetch_pack_exports = {};
4748
+ __export(fetch_pack_exports, {
4749
+ fetchPack: () => fetchPack
4750
+ });
4751
+ import { createHash as createHash5 } from "crypto";
4752
+ import { createWriteStream, existsSync as existsSync19, mkdirSync as mkdirSync8, renameSync as renameSync4, rmSync, statSync as statSync5 } from "fs";
4753
+ import { join as join22 } from "path";
4754
+ async function sha256File(path) {
4755
+ const { createReadStream: createReadStream3 } = await import("fs");
4756
+ return new Promise((resolve3, reject) => {
4757
+ const h = createHash5("sha256");
4758
+ createReadStream3(path).on("data", (d) => h.update(d)).on("end", () => resolve3(h.digest("hex"))).on("error", reject);
4759
+ });
4760
+ }
4761
+ async function fetchToTmp(url, tmpPath, timeoutMs) {
4762
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: "follow" });
4763
+ if (!res.ok || !res.body) throw new PackError("FETCH_FAIL", `HTTP ${res.status} for ${url}`);
4764
+ const { Readable } = await import("stream");
4765
+ const { pipeline } = await import("stream/promises");
4766
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(tmpPath));
4767
+ }
4768
+ async function fetchPack({ profile = PROFILE, timeoutMs = 3e5, onProgress = null } = {}) {
4769
+ const dir = packDir(profile);
4770
+ mkdirSync8(dir, { recursive: true, mode: 448 });
4771
+ const fetched = [];
4772
+ const skipped = [];
4773
+ for (const f of PACK_FILES) {
4774
+ const finalPath = join22(dir, f.name);
4775
+ if (existsSync19(finalPath) && statSync5(finalPath).size === f.bytes) {
4776
+ skipped.push(f.name);
4777
+ continue;
4778
+ }
4779
+ const tmpPath = `${finalPath}.tmp-${process.pid}`;
4780
+ try {
4781
+ if (onProgress) onProgress({ file: f.name, bytes: f.bytes, state: "fetching" });
4782
+ await fetchToTmp(f.url, tmpPath, timeoutMs);
4783
+ const gotHash = await sha256File(tmpPath);
4784
+ if (gotHash !== f.sha256) {
4785
+ throw new PackError(
4786
+ "HASH_MISMATCH",
4787
+ `${f.name}: expected ${f.sha256.slice(0, 12)}\u2026, got ${gotHash.slice(0, 12)}\u2026 \u2014 refusing (upstream drifted or tampered)`
4788
+ );
4789
+ }
4790
+ const gotSize = statSync5(tmpPath).size;
4791
+ if (gotSize !== f.bytes) {
4792
+ throw new PackError("HASH_MISMATCH", `${f.name}: expected ${f.bytes} bytes, got ${gotSize}`);
4793
+ }
4794
+ renameSync4(tmpPath, finalPath);
4795
+ fetched.push(f.name);
4796
+ if (onProgress) onProgress({ file: f.name, bytes: f.bytes, state: "verified" });
4797
+ } catch (err) {
4798
+ try {
4799
+ rmSync(tmpPath, { force: true });
4800
+ } catch {
4801
+ }
4802
+ if (err instanceof PackError) throw err;
4803
+ throw new PackError("FETCH_FAIL", `${f.name}: ${err.message}`);
4804
+ }
4805
+ }
4806
+ return { ok: true, fetched, skipped, dir };
4807
+ }
4808
+ var PackError;
4809
+ var init_fetch_pack = __esm({
4810
+ "mcp-server/lib/semantic/fetch-pack.js"() {
4811
+ init_manifest();
4812
+ PackError = class extends Error {
4813
+ constructor(code, message) {
4814
+ super(message);
4815
+ this.code = code;
4816
+ }
4817
+ };
4818
+ }
4819
+ });
4820
+
4734
4821
  // mcp-server/lib/timecard.js
4735
4822
  var timecard_exports = {};
4736
4823
  __export(timecard_exports, {
@@ -4787,10 +4874,10 @@ init_config_customer();
4787
4874
  init_api_client();
4788
4875
  import { createInterface as createInterface3 } from "node:readline";
4789
4876
  import { execSync as execSync2 } from "node:child_process";
4790
- import { homedir as homedir18 } from "node:os";
4791
- import { join as join22, dirname as dirname5, resolve as resolve2 } from "node:path";
4877
+ import { homedir as homedir19 } from "node:os";
4878
+ import { join as join23, dirname as dirname6, resolve as resolve2 } from "node:path";
4792
4879
  import { fileURLToPath as fileURLToPath2 } from "node:url";
4793
- import { readFileSync as readFileSync16, writeFileSync as writeFileSync9, existsSync as existsSync19, mkdirSync as mkdirSync9, readdirSync as readdirSync2, statSync as statSync5 } from "node:fs";
4880
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync10, existsSync as existsSync20, mkdirSync as mkdirSync9, readdirSync as readdirSync2, statSync as statSync6 } from "node:fs";
4794
4881
 
4795
4882
  // mcp-server/lib/guardrails-run.js
4796
4883
  function contextFromInput(input) {
@@ -5543,7 +5630,27 @@ async function _run({ dir, key, address, contentKey, build, broadcast, checkConf
5543
5630
  });
5544
5631
  prune(dir, now()).catch((e) => process.stderr.write(`[build-cache] prune failed (non-fatal): ${e?.message}
5545
5632
  `));
5546
- const write = await broadcast(fresh.txHex);
5633
+ let write;
5634
+ try {
5635
+ write = await broadcast(fresh.txHex);
5636
+ } catch (err) {
5637
+ let seen = null;
5638
+ try {
5639
+ seen = await checkConfirmation2(fresh.txId);
5640
+ } catch {
5641
+ }
5642
+ if (seen && (seen.confirmed || seen.inMempool || seen.exists === true)) {
5643
+ process.stderr.write(`[g-363] broadcast ack failed but ${fresh.txId.slice(0, 12)}\u2026 is on-chain/mempool \u2014 slow-ack recovered, no rival (${err?.message || "timeout"})
5644
+ `);
5645
+ return {
5646
+ ...fresh,
5647
+ write: { txid: fresh.txId, via: "chain-verified", source: "slow-ack-recovered", confirmed: !!seen.confirmed },
5648
+ reused: false,
5649
+ alreadyOnChain: true
5650
+ };
5651
+ }
5652
+ throw err;
5653
+ }
5547
5654
  if (write?.txid && fresh.txId && write.txid.toLowerCase() !== fresh.txId.toLowerCase()) {
5548
5655
  process.stderr.write(`[g-363 TRIPWIRE] bridge-computed txid ${write.txid} differs from built txid ${fresh.txId} \u2014 possible transit mutation, receipt kept as built
5549
5656
  `);
@@ -5800,7 +5907,7 @@ async function saveFile(filePath, options = {}) {
5800
5907
  }
5801
5908
  const chunkCount = encrypted.length <= MAX_CHUNK_SIZE ? 1 : Math.ceil(encrypted.length / MAX_CHUNK_SIZE);
5802
5909
  const receipt = buildReceipt(_finalWrite, { txId: masterTxId, fee: _saveFee, txSize: _finalSize });
5803
- appendReceipt(receipt, { trigger: "interactive", saveType: "file" });
5910
+ appendReceipt(receipt, { trigger: "interactive", saveType: "file", contentHash: `sha256:${contentHash}` });
5804
5911
  return {
5805
5912
  success: true,
5806
5913
  txId: masterTxId,
@@ -7064,22 +7171,25 @@ async function findByContentKey({
7064
7171
  excludeTxids = [],
7065
7172
  maxScan = 400,
7066
7173
  confirmedOnly = false,
7067
- stopAtFirst = true
7174
+ stopAtFirst = true,
7175
+ matchCap = 10
7068
7176
  }) {
7069
- if (!address || !targetKey) return { matches: [], scanned: 0, truncated: false };
7177
+ if (!address || !targetKey) return { matches: [], scanned: 0, truncated: false, fetchFailures: 0, classification: "absent" };
7070
7178
  const exclude = new Set(excludeTxids.map((t) => String(t).toLowerCase()));
7071
7179
  let history = [];
7072
7180
  try {
7073
7181
  history = await getHistory(address);
7074
7182
  } catch {
7075
- return { matches: [], scanned: 0, truncated: false, error: "history-unreachable" };
7183
+ return { matches: [], scanned: 0, truncated: false, fetchFailures: 0, classification: "inconclusive-history-unreachable", error: "history-unreachable" };
7076
7184
  }
7077
- if (!Array.isArray(history)) return { matches: [], scanned: 0, truncated: false };
7185
+ if (!Array.isArray(history)) return { matches: [], scanned: 0, truncated: false, fetchFailures: 0, classification: "absent" };
7078
7186
  const ordered = [...history].sort((a, b) => (b.height || 0) - (a.height || 0));
7079
7187
  const truncated = ordered.length > maxScan;
7080
7188
  const candidates = ordered.slice(0, maxScan);
7081
7189
  const matches = [];
7082
7190
  let scanned = 0;
7191
+ let fetchFailures = 0;
7192
+ let confirmedFound = false;
7083
7193
  for (const h of candidates) {
7084
7194
  const txid = (h.tx_hash || h.txid || "").toString();
7085
7195
  if (!txid || exclude.has(txid.toLowerCase())) continue;
@@ -7089,23 +7199,62 @@ async function findByContentKey({
7089
7199
  try {
7090
7200
  payload = await fetchPayload(txid);
7091
7201
  } catch {
7202
+ fetchFailures++;
7092
7203
  continue;
7093
7204
  }
7094
7205
  if (payloadContentKey(payload) === targetKey) {
7095
- matches.push({ txid, height: h.height || null });
7096
- if (stopAtFirst) break;
7206
+ const height = h.height || null;
7207
+ matches.push({ txid, height });
7208
+ if (height > 0) confirmedFound = true;
7209
+ if (stopAtFirst && confirmedFound) break;
7210
+ if (matches.length >= matchCap) break;
7097
7211
  }
7098
7212
  }
7099
- return { matches, scanned, truncated };
7213
+ matches.sort((a, b) => {
7214
+ const ac = a.height > 0 ? 1 : 0, bc = b.height > 0 ? 1 : 0;
7215
+ if (ac !== bc) return bc - ac;
7216
+ return (b.height || 0) - (a.height || 0);
7217
+ });
7218
+ const result = stopAtFirst && matches.length ? [matches[0]] : matches;
7219
+ let classification;
7220
+ if (result.length) classification = "found";
7221
+ else if (truncated) classification = "inconclusive-truncated";
7222
+ else if (fetchFailures > 0) classification = "inconclusive-fetch-degraded";
7223
+ else classification = "absent";
7224
+ return { matches: result, scanned, truncated, fetchFailures, classification };
7100
7225
  }
7101
7226
 
7102
7227
  // mcp-server/tools/load_file.js
7103
7228
  var TX_CACHE_DIR3 = join12(homedir12(), ".indelible", "tx-cache");
7229
+ var SAVE_LOG_PATH = join12(homedir12(), ".indelible", "save-log.jsonl");
7230
+ async function contentKeyFromSaveLog(txid) {
7231
+ try {
7232
+ const raw = await readFile6(SAVE_LOG_PATH, "utf-8");
7233
+ for (const line of raw.split("\n")) {
7234
+ if (!line.trim()) continue;
7235
+ let e;
7236
+ try {
7237
+ e = JSON.parse(line);
7238
+ } catch {
7239
+ continue;
7240
+ }
7241
+ if (e.txid === txid && e.contentHash) return String(e.contentHash).replace(/^sha256:/, "");
7242
+ }
7243
+ } catch {
7244
+ }
7245
+ return null;
7246
+ }
7104
7247
  async function healStrandedTxid(strandedTxId, config) {
7105
7248
  try {
7249
+ let targetKey = null;
7106
7250
  const cachedRaw = await readFile6(join12(TX_CACHE_DIR3, `${strandedTxId.trim()}.json`), "utf-8").catch(() => null);
7107
- if (!cachedRaw) return null;
7108
- const targetKey = payloadContentKey(JSON.parse(cachedRaw));
7251
+ if (cachedRaw) {
7252
+ try {
7253
+ targetKey = payloadContentKey(JSON.parse(cachedRaw));
7254
+ } catch {
7255
+ }
7256
+ }
7257
+ if (!targetKey) targetKey = await contentKeyFromSaveLog(strandedTxId.trim());
7109
7258
  const address = config?.address;
7110
7259
  if (!targetKey || !address) return null;
7111
7260
  const { matches } = await findByContentKey({
@@ -8302,32 +8451,108 @@ async function recallContext({ from_date = null, to_date = null, query = null, d
8302
8451
  };
8303
8452
  }
8304
8453
 
8454
+ // mcp-server/tools/map_themes.js
8455
+ init_config_customer();
8456
+ import { readFileSync as readFileSync15, existsSync as existsSync17, statSync as statSync4 } from "fs";
8457
+ import { join as join20 } from "path";
8458
+ import { homedir as homedir17 } from "os";
8459
+ var MODEL_PATH = join20(homedir17(), ".indelible", "SEMANTIC_MAP.json");
8460
+ var TXIDS_OVERVIEW = 5;
8461
+ var TXIDS_DEEP = 60;
8462
+ async function mapThemes({ sample_titles = 5, sort = "size", theme = null } = {}) {
8463
+ const config = await loadConfig();
8464
+ const addr = config?.address;
8465
+ if (!addr) return { success: false, error: "No wallet configured \u2014 run setup_wallet first." };
8466
+ const indexPath2 = join20(homedir17(), ".indelible", "recall-index", `${addr}.jsonl`);
8467
+ let fresh = false;
8468
+ try {
8469
+ fresh = existsSync17(MODEL_PATH) && (!existsSync17(indexPath2) || statSync4(MODEL_PATH).mtimeMs >= statSync4(indexPath2).mtimeMs);
8470
+ } catch {
8471
+ }
8472
+ if (!fresh) {
8473
+ try {
8474
+ const { buildSemanticMap: buildSemanticMap2 } = await Promise.resolve().then(() => (init_build_map(), build_map_exports));
8475
+ buildSemanticMap2({ addr, log: () => {
8476
+ } });
8477
+ } catch (e) {
8478
+ if (e.code === "NO_INDEX") {
8479
+ return { success: false, error: "No semantic index yet. Run `indelible-mcp semantic-fetch` once in a terminal (one-time, ~50MB, builds your index + map locally), then ask again." };
8480
+ }
8481
+ return { success: false, error: `Could not build the map model: ${e.message}` };
8482
+ }
8483
+ }
8484
+ let model;
8485
+ try {
8486
+ model = JSON.parse(readFileSync15(MODEL_PATH, "utf8"));
8487
+ } catch (e) {
8488
+ return { success: false, error: `Could not read the map model (${e.message}) \u2014 run \`indelible-mcp map\` to regenerate it.` };
8489
+ }
8490
+ if (model.sparse) {
8491
+ return { success: true, sparse: true, mapped_sessions: model.count || 0, themes: [], message: `Only ${model.count || 0} mapped sessions so far \u2014 themes appear once a couple dozen are saved. The map grows as you save.` };
8492
+ }
8493
+ const now = Date.now();
8494
+ const nTitles = Math.max(0, Math.min(20, Number(sample_titles) || 5));
8495
+ const themeFilter = theme ? String(theme).toLowerCase() : null;
8496
+ const txidCap = themeFilter ? TXIDS_DEEP : TXIDS_OVERVIEW;
8497
+ let themes = model.cls.map((c, i) => {
8498
+ const members = model.pts.filter((p) => p.c === i).sort((a, b) => a.d < b.d ? 1 : -1);
8499
+ const last = c.last_touched || members[0]?.d || null;
8500
+ const txids = c.member_txIds ? [...c.member_txIds].reverse() : members.map((p) => p.id);
8501
+ return {
8502
+ theme: c.label,
8503
+ sessions: c.count,
8504
+ first_touched: c.first_touched || members[members.length - 1]?.d || null,
8505
+ last_touched: last,
8506
+ days_since_touched: last ? Math.max(0, Math.floor((now - Date.parse(last)) / 864e5)) : null,
8507
+ sample_titles: members.slice(0, nTitles).map((p) => `${p.d} \xB7 ${p.t || "(untitled)"}`),
8508
+ member_txids: txids.slice(0, txidCap),
8509
+ member_txids_truncated: txids.length > txidCap ? txids.length : false
8510
+ };
8511
+ });
8512
+ if (themeFilter) {
8513
+ const matched = themes.filter((t) => t.theme.toLowerCase().includes(themeFilter));
8514
+ if (!matched.length) {
8515
+ return { success: false, error: `No theme matches "${theme}". Themes: ${themes.map((t) => t.theme).join(" | ")}` };
8516
+ }
8517
+ themes = matched;
8518
+ }
8519
+ themes.sort(sort === "cold" ? (a, b) => (b.days_since_touched ?? -1) - (a.days_since_touched ?? -1) : (a, b) => b.sessions - a.sessions);
8520
+ return {
8521
+ success: true,
8522
+ generated_at: model.generated_at || null,
8523
+ mapped_sessions: model.count,
8524
+ span: model.span || null,
8525
+ themes,
8526
+ note: themeFilter ? 'To read this theme, call recall_context with depth:"full" and txids from member_txids (up to 20 per call, newest first).' : `Overview shows ${TXIDS_OVERVIEW} txids per theme. To drill a theme, call map_themes again with theme:"<name fragment>" for its ${TXIDS_DEEP} newest txids, then recall_context depth:"full" (\u226420 txids per call).`
8527
+ };
8528
+ }
8529
+
8305
8530
  // mcp-server/tools/report_bug.js
8306
- import { readFileSync as readFileSync14, existsSync as existsSync17 } from "fs";
8307
- import { join as join19, dirname as dirname4 } from "path";
8308
- import { homedir as homedir16 } from "os";
8531
+ import { readFileSync as readFileSync16, existsSync as existsSync18 } from "fs";
8532
+ import { join as join21, dirname as dirname5 } from "path";
8533
+ import { homedir as homedir18 } from "os";
8309
8534
  import { fileURLToPath } from "url";
8310
8535
  import os from "os";
8311
8536
  var INTAKE_URL = process.env.INDELIBLE_BUG_INTAKE || "https://indelible.one/api/bug-report";
8312
- var INDELIBLE_DIR = join19(homedir16(), ".indelible");
8537
+ var INDELIBLE_DIR = join21(homedir18(), ".indelible");
8313
8538
  function getMcpVersion(injected) {
8314
8539
  if (injected) return injected;
8315
8540
  const candidates = [];
8316
8541
  try {
8317
- candidates.push(join19(dirname4(fileURLToPath(import.meta.url)), "..", "..", "package.json"));
8542
+ candidates.push(join21(dirname5(fileURLToPath(import.meta.url)), "..", "..", "package.json"));
8318
8543
  } catch {
8319
8544
  }
8320
8545
  try {
8321
- candidates.push(join19(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"));
8546
+ candidates.push(join21(dirname5(fileURLToPath(import.meta.url)), "..", "package.json"));
8322
8547
  } catch {
8323
8548
  }
8324
- candidates.push(join19(homedir16(), "AppData", "Roaming", "npm", "node_modules", "indelible-mcp", "package.json"));
8549
+ candidates.push(join21(homedir18(), "AppData", "Roaming", "npm", "node_modules", "indelible-mcp", "package.json"));
8325
8550
  candidates.push("/usr/local/lib/node_modules/indelible-mcp/package.json");
8326
8551
  candidates.push("/usr/lib/node_modules/indelible-mcp/package.json");
8327
8552
  for (const p of candidates) {
8328
8553
  try {
8329
- if (!existsSync17(p)) continue;
8330
- const pkg = JSON.parse(readFileSync14(p, "utf8"));
8554
+ if (!existsSync18(p)) continue;
8555
+ const pkg = JSON.parse(readFileSync16(p, "utf8"));
8331
8556
  if (pkg && pkg.name === "indelible-mcp" && pkg.version) return pkg.version;
8332
8557
  } catch {
8333
8558
  }
@@ -8336,7 +8561,7 @@ function getMcpVersion(injected) {
8336
8561
  }
8337
8562
  function readJson(path) {
8338
8563
  try {
8339
- return existsSync17(path) ? JSON.parse(readFileSync14(path, "utf8")) : null;
8564
+ return existsSync18(path) ? JSON.parse(readFileSync16(path, "utf8")) : null;
8340
8565
  } catch {
8341
8566
  return null;
8342
8567
  }
@@ -8346,9 +8571,9 @@ function getWalletAddress(config) {
8346
8571
  }
8347
8572
  function getRecentReceipts(n = 5) {
8348
8573
  try {
8349
- const p = join19(INDELIBLE_DIR, "save-log.jsonl");
8350
- if (!existsSync17(p)) return [];
8351
- const lines = readFileSync14(p, "utf8").trim().split("\n").filter(Boolean).slice(-n);
8574
+ const p = join21(INDELIBLE_DIR, "save-log.jsonl");
8575
+ if (!existsSync18(p)) return [];
8576
+ const lines = readFileSync16(p, "utf8").trim().split("\n").filter(Boolean).slice(-n);
8352
8577
  return lines.map((l) => {
8353
8578
  try {
8354
8579
  const e = JSON.parse(l);
@@ -8373,11 +8598,11 @@ function redactSecrets2(s) {
8373
8598
  return redactCredentials(String(s));
8374
8599
  }
8375
8600
  async function reportBug(args2 = {}, mcpVersion) {
8376
- const { summary, description, severity = "medium", tool_name = null, last_error = null } = args2;
8601
+ const { summary, description, severity = "medium", tool_name = null, last_error = null, still_file = false } = args2;
8377
8602
  if (!summary || !description) {
8378
8603
  return { success: false, error: "summary and description are required (you supply the narrative; the tool supplies the facts)." };
8379
8604
  }
8380
- const config = readJson(join19(INDELIBLE_DIR, "config.json")) || {};
8605
+ const config = readJson(join21(INDELIBLE_DIR, "config.json")) || {};
8381
8606
  const payload = {
8382
8607
  // AI-supplied narrative (defensively redacted + length-capped)
8383
8608
  summary: redactSecrets2(summary).slice(0, 200),
@@ -8385,6 +8610,10 @@ async function reportBug(args2 = {}, mcpVersion) {
8385
8610
  severity: ["low", "medium", "high"].includes(severity) ? severity : "medium",
8386
8611
  tool_name: tool_name ? String(tool_name).slice(0, 80) : null,
8387
8612
  last_error: redactSecrets2(last_error),
8613
+ // User override: "no, my case is genuinely different." When true, the intake
8614
+ // files a real ticket + pages the team even if the report matches a known,
8615
+ // already-fixed issue — so a false match can never trap a real report.
8616
+ still_file: still_file === true,
8388
8617
  // Machine-pulled facts (the AI cannot get these wrong)
8389
8618
  mcp_version: getMcpVersion(mcpVersion),
8390
8619
  wallet_address: getWalletAddress(config),
@@ -8404,9 +8633,17 @@ async function reportBug(args2 = {}, mcpVersion) {
8404
8633
  }
8405
8634
  const data = await resp.json().catch(() => ({}));
8406
8635
  if (data.known_issue) {
8407
- return { success: true, status: "known_issue", issue_id: data.issue_id || null, message: data.message || "Matched a known, already-fixed issue." };
8636
+ return {
8637
+ success: true,
8638
+ status: "known_issue",
8639
+ issue_id: data.issue_id || null,
8640
+ message: data.message || "Matched a known, already-fixed issue.",
8641
+ // The team was already notified (the intake never swallows), but if the
8642
+ // narrative describes something genuinely different, the user can escalate.
8643
+ override_hint: "If this does not match your situation, call report_bug again with still_file: true to open a ticket and page the team anyway."
8644
+ };
8408
8645
  }
8409
- return { success: true, status: "filed", ticket_id: data.ticket_id || null, message: data.message || "Bug report sent to the Indelible team." };
8646
+ return { success: true, status: "filed", ticket_id: data.ticket_id || null, forced: !!data.forced, message: data.message || "Bug report sent to the Indelible team." };
8410
8647
  } catch (err) {
8411
8648
  return { success: false, error: `could not reach intake (${err.message}). Fallback: save_file the report on-chain and share the txid.` };
8412
8649
  }
@@ -8647,16 +8884,16 @@ init_wall_stub();
8647
8884
  init_wall_stub();
8648
8885
  init_wall_stub();
8649
8886
  init_wall_stub();
8650
- var PROJECT_ROOT_FOR_CLONE_CHECK = resolve2(dirname5(fileURLToPath2(import.meta.url)), "../..");
8651
- var CONTEXT_FILE2 = join22(homedir18(), ".indelible", "indelible-context.jsonl");
8887
+ var PROJECT_ROOT_FOR_CLONE_CHECK = resolve2(dirname6(fileURLToPath2(import.meta.url)), "../..");
8888
+ var CONTEXT_FILE2 = join23(homedir19(), ".indelible", "indelible-context.jsonl");
8652
8889
  function installHooks() {
8653
- const claudeDir = join22(homedir18(), ".claude");
8654
- const settingsPath = join22(claudeDir, "settings.local.json");
8655
- if (!existsSync19(claudeDir)) mkdirSync9(claudeDir, { recursive: true });
8890
+ const claudeDir = join23(homedir19(), ".claude");
8891
+ const settingsPath = join23(claudeDir, "settings.local.json");
8892
+ if (!existsSync20(claudeDir)) mkdirSync9(claudeDir, { recursive: true });
8656
8893
  let settings = {};
8657
- if (existsSync19(settingsPath)) {
8894
+ if (existsSync20(settingsPath)) {
8658
8895
  try {
8659
- settings = JSON.parse(readFileSync16(settingsPath, "utf8"));
8896
+ settings = JSON.parse(readFileSync17(settingsPath, "utf8"));
8660
8897
  } catch {
8661
8898
  settings = {};
8662
8899
  }
@@ -8707,13 +8944,13 @@ function installHooks() {
8707
8944
  });
8708
8945
  installed.push("PreToolUse:guardrails");
8709
8946
  }
8710
- writeFileSync9(settingsPath, JSON.stringify(settings, null, 2));
8947
+ writeFileSync10(settingsPath, JSON.stringify(settings, null, 2));
8711
8948
  return { settingsPath, installed, alreadyInstalled: installed.length === 0 };
8712
8949
  }
8713
8950
  function runPreToolUseGuard() {
8714
8951
  let code = 0;
8715
8952
  try {
8716
- const raw = readFileSync16(0, "utf8").trim();
8953
+ const raw = readFileSync17(0, "utf8").trim();
8717
8954
  if (!raw) process.exit(0);
8718
8955
  const hit = evaluate(JSON.parse(raw));
8719
8956
  if (hit) {
@@ -8913,6 +9150,7 @@ Commands:
8913
9150
  const { packPresent: packPresent2 } = await Promise.resolve().then(() => (init_manifest(), manifest_exports));
8914
9151
  const { fetchPack: fetchPack2 } = await Promise.resolve().then(() => (init_fetch_pack(), fetch_pack_exports));
8915
9152
  const { rebuildVectorSidecar: rebuildVectorSidecar2 } = await Promise.resolve().then(() => (init_vec_store(), vec_store_exports));
9153
+ const { buildIndexWindow: buildIndexWindow2, resetGaps: resetGaps2 } = await Promise.resolve().then(() => (init_recall_index(), recall_index_exports));
8916
9154
  try {
8917
9155
  if (!packPresent2()) {
8918
9156
  console.log("Fetching the semantic model pack (~50MB, one time, hash-verified)...");
@@ -8926,15 +9164,37 @@ Commands:
8926
9164
  console.log(JSON.stringify({ success: false, error: "No wallet configured \u2014 run setup first." }));
8927
9165
  break;
8928
9166
  }
9167
+ const wif = await getWif();
9168
+ if (!wif) {
9169
+ console.log("Wallet is locked \u2014 cannot read your history to build the map/recall index yet. Run setup_wallet to unlock, then re-run. (Pack is ready; embedding whatever is already indexed.)");
9170
+ } else {
9171
+ const retried = await resetGaps2(cfg.address, ["FETCH_TIMEOUT", "NOT_ON_CHAIN", "WALLET_LOCKED"]);
9172
+ if (retried) console.log(` re-attempting ${retried} previously-skipped sessions...`);
9173
+ console.log(`Reading your on-chain history for ${cfg.address} (one-time, resumable, all local)...`);
9174
+ let indexed = 0;
9175
+ for (; ; ) {
9176
+ const r = await buildIndexWindow2(cfg.address, wif, { maxDecrypt: 150 });
9177
+ indexed += r.indexed;
9178
+ console.log(` indexed ${indexed} sessions; ${r.remaining} to go`);
9179
+ if (r.remaining === 0 || r.indexed === 0 && r.gapped === 0) break;
9180
+ }
9181
+ }
8929
9182
  console.log(`Backfilling meaning-vectors for ${cfg.address} (resumable \u2014 re-run to continue)...`);
8930
- let total = 0;
9183
+ let total = 0, cov = { vecs: 0, indexed: 0 };
8931
9184
  for (; ; ) {
8932
9185
  const r = await rebuildVectorSidecar2(cfg.address, { maxEmbed: 100 });
8933
9186
  total += r.embedded;
8934
- console.log(` embedded ${total} sessions; remaining ${r.remaining}; coverage ${r.coverage.vecs}/${r.coverage.indexed}`);
9187
+ cov = r.coverage || cov;
9188
+ console.log(` embedded ${total} sessions; remaining ${r.remaining}; coverage ${cov.vecs}/${cov.indexed}`);
8935
9189
  if (r.remaining === 0 || r.embedded === 0) break;
8936
9190
  }
8937
- console.log(JSON.stringify({ success: true, message: "Semantic recall enabled \u2014 meaning-based search is live in recall_context.", embedded: total }));
9191
+ if (cov.vecs > 0) {
9192
+ console.log(JSON.stringify({ success: true, message: "Semantic recall + map ready \u2014 run `indelible-mcp map` to see the shape of your work.", embedded: total, mapped: cov.vecs }));
9193
+ } else if (!wif) {
9194
+ console.log(JSON.stringify({ success: false, error: "Wallet is locked \u2014 nothing to map yet. Run setup_wallet to unlock, then re-run to build your map." }));
9195
+ } else {
9196
+ console.log(JSON.stringify({ success: true, message: "No saved sessions to map yet \u2014 your map grows as you save. Save a few conversations, then re-run this.", embedded: 0, mapped: 0 }));
9197
+ }
8938
9198
  } catch (e) {
8939
9199
  console.log(JSON.stringify({ success: false, error: e.code === "HASH_MISMATCH" ? `Integrity check failed: ${e.message}` : e.message }));
8940
9200
  }
@@ -8942,9 +9202,9 @@ Commands:
8942
9202
  }
8943
9203
  case "map": {
8944
9204
  const { buildSemanticMap: buildSemanticMap2 } = await Promise.resolve().then(() => (init_build_map(), build_map_exports));
8945
- const { join: join23 } = await import("node:path");
8946
- const { homedir: homedir19 } = await import("node:os");
8947
- const out = join23(homedir19(), ".indelible", "SEMANTIC_MAP.html");
9205
+ const { join: join24 } = await import("node:path");
9206
+ const { homedir: homedir20 } = await import("node:os");
9207
+ const out = join24(homedir20(), ".indelible", "SEMANTIC_MAP.html");
8948
9208
  try {
8949
9209
  const cfg = await loadConfig();
8950
9210
  const r = buildSemanticMap2({ addr: cfg?.address, outPath: out, log: (m) => process.stderr.write(`[map] ${m}
@@ -8958,7 +9218,7 @@ Commands:
8958
9218
  ${r.path}`);
8959
9219
  }
8960
9220
  } catch (e) {
8961
- if (e.code === "NO_INDEX") console.log("No semantic index yet. Run `indelible-mcp semantic-fetch` first (one-time, ~50MB, all local), then `indelible-mcp map`.");
9221
+ if (e.code === "NO_INDEX") console.log("No map yet. Run `indelible-mcp semantic-fetch` to build it from your saved history (one-time, ~50MB, all local). If you just ran that and still see this, you have not saved any sessions yet \u2014 your map grows as you save.");
8962
9222
  else console.log(JSON.stringify({ success: false, error: e.message }));
8963
9223
  }
8964
9224
  break;
@@ -8969,7 +9229,7 @@ Commands:
8969
9229
  }
8970
9230
  function printHelp() {
8971
9231
  console.log(`
8972
- Indelible MCP \u2014 Blockchain memory for Claude Code (v4.9.6)
9232
+ Indelible MCP \u2014 Blockchain memory for Claude Code (v4.9.8)
8973
9233
 
8974
9234
  Setup:
8975
9235
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -9018,19 +9278,19 @@ Learn more: https://indelible.one
9018
9278
  `);
9019
9279
  }
9020
9280
  function findNewestTranscript() {
9021
- const projectsDir = join22(homedir18(), ".claude", "projects");
9022
- if (!existsSync19(projectsDir)) return null;
9281
+ const projectsDir = join23(homedir19(), ".claude", "projects");
9282
+ if (!existsSync20(projectsDir)) return null;
9023
9283
  let newestTime = 0;
9024
9284
  let newest = null;
9025
9285
  try {
9026
9286
  for (const project of readdirSync2(projectsDir)) {
9027
- const projectPath = join22(projectsDir, project);
9287
+ const projectPath = join23(projectsDir, project);
9028
9288
  try {
9029
- if (!statSync5(projectPath).isDirectory()) continue;
9289
+ if (!statSync6(projectPath).isDirectory()) continue;
9030
9290
  for (const file of readdirSync2(projectPath)) {
9031
9291
  if (!file.endsWith(".jsonl")) continue;
9032
- const p = join22(projectPath, file);
9033
- const t = statSync5(p).mtimeMs;
9292
+ const p = join23(projectPath, file);
9293
+ const t = statSync6(p).mtimeMs;
9034
9294
  if (t > newestTime) {
9035
9295
  newestTime = t;
9036
9296
  newest = p;
@@ -9053,13 +9313,13 @@ async function printTimeCard() {
9053
9313
  };
9054
9314
  try {
9055
9315
  const t = findNewestTranscript();
9056
- if (t) f.lastSeen = phraseDelta2(Date.now() - statSync5(t).mtimeMs);
9316
+ if (t) f.lastSeen = phraseDelta2(Date.now() - statSync6(t).mtimeMs);
9057
9317
  } catch {
9058
9318
  }
9059
9319
  try {
9060
- const gPath = join22(homedir18(), ".indelible", "goals.json");
9061
- if (existsSync19(gPath)) {
9062
- const g = JSON.parse(readFileSync16(gPath, "utf8"));
9320
+ const gPath = join23(homedir19(), ".indelible", "goals.json");
9321
+ if (existsSync20(gPath)) {
9322
+ const g = JSON.parse(readFileSync17(gPath, "utf8"));
9063
9323
  const active = (g.goals || []).filter((x) => x.status === "active");
9064
9324
  if (active.length) {
9065
9325
  const oldest = active.reduce((a, b) => new Date(a.created_at) < new Date(b.created_at) ? a : b);
@@ -9095,21 +9355,21 @@ async function runPreCompactSave() {
9095
9355
  process.stderr.write("Indelible: MCP disabled, skipping save\n");
9096
9356
  process.exit(0);
9097
9357
  }
9098
- const target = transcriptPath && existsSync19(transcriptPath) ? transcriptPath : findNewestTranscript() || CONTEXT_FILE2;
9358
+ const target = transcriptPath && existsSync20(transcriptPath) ? transcriptPath : findNewestTranscript() || CONTEXT_FILE2;
9099
9359
  const result = await saveSession(target, `Auto-save before ${trigger} compaction`);
9100
9360
  if (result.success) {
9101
9361
  process.stderr.write(`Indelible: Saved ${result.newMessages} messages (${result.saveType}) tx:${result.txId?.slice(0, 12)}...
9102
9362
  `);
9103
9363
  } else if (result.gated && result.upsell) {
9104
- const upsellMarker = join22(homedir18(), ".indelible", ".upsell-shown");
9364
+ const upsellMarker = join23(homedir19(), ".indelible", ".upsell-shown");
9105
9365
  let shownRecently = false;
9106
9366
  try {
9107
- shownRecently = Date.now() - statSync5(upsellMarker).mtimeMs < 24 * 60 * 60 * 1e3;
9367
+ shownRecently = Date.now() - statSync6(upsellMarker).mtimeMs < 24 * 60 * 60 * 1e3;
9108
9368
  } catch {
9109
9369
  }
9110
9370
  if (!shownRecently) {
9111
9371
  try {
9112
- writeFileSync9(upsellMarker, (/* @__PURE__ */ new Date()).toISOString());
9372
+ writeFileSync10(upsellMarker, (/* @__PURE__ */ new Date()).toISOString());
9113
9373
  } catch {
9114
9374
  }
9115
9375
  process.stderr.write("\n \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
@@ -9131,20 +9391,20 @@ async function runPreCompactSave() {
9131
9391
  process.exit(0);
9132
9392
  }
9133
9393
  function findMemoryDir() {
9134
- const projectsDir = join22(homedir18(), ".claude", "projects");
9135
- if (!existsSync19(projectsDir)) return null;
9394
+ const projectsDir = join23(homedir19(), ".claude", "projects");
9395
+ if (!existsSync20(projectsDir)) return null;
9136
9396
  let newestTime = 0;
9137
9397
  let newestProject = null;
9138
9398
  const projects = readdirSync2(projectsDir);
9139
9399
  for (const project of projects) {
9140
- const projectPath = join22(projectsDir, project);
9400
+ const projectPath = join23(projectsDir, project);
9141
9401
  try {
9142
- const pStat = statSync5(projectPath);
9402
+ const pStat = statSync6(projectPath);
9143
9403
  if (!pStat.isDirectory()) continue;
9144
9404
  const files = readdirSync2(projectPath);
9145
9405
  for (const file of files) {
9146
9406
  if (!file.endsWith(".jsonl")) continue;
9147
- const fStat = statSync5(join22(projectPath, file));
9407
+ const fStat = statSync6(join23(projectPath, file));
9148
9408
  if (fStat.mtimeMs > newestTime) {
9149
9409
  newestTime = fStat.mtimeMs;
9150
9410
  newestProject = projectPath;
@@ -9153,7 +9413,7 @@ function findMemoryDir() {
9153
9413
  } catch {
9154
9414
  }
9155
9415
  }
9156
- return newestProject ? join22(newestProject, "memory") : null;
9416
+ return newestProject ? join23(newestProject, "memory") : null;
9157
9417
  }
9158
9418
  async function runPostCompactRestore() {
9159
9419
  await printTimeCard();
@@ -9203,10 +9463,10 @@ async function runPostCompactRestore() {
9203
9463
  try {
9204
9464
  const memoryDir = findMemoryDir();
9205
9465
  if (memoryDir && (config.memory_file_txid || config.session_history_txid)) {
9206
- if (!existsSync19(memoryDir)) mkdirSync9(memoryDir, { recursive: true });
9466
+ if (!existsSync20(memoryDir)) mkdirSync9(memoryDir, { recursive: true });
9207
9467
  if (config.memory_file_txid) {
9208
- const memPath = join22(memoryDir, "MEMORY.md");
9209
- if (!existsSync19(memPath)) {
9468
+ const memPath = join23(memoryDir, "MEMORY.md");
9469
+ if (!existsSync20(memPath)) {
9210
9470
  const memResult = await loadFile(config.memory_file_txid, { outputPath: memPath });
9211
9471
  if (memResult.success) {
9212
9472
  process.stderr.write(`Indelible: MEMORY.md restored from chain (file was missing)
@@ -9215,8 +9475,8 @@ async function runPostCompactRestore() {
9215
9475
  }
9216
9476
  }
9217
9477
  if (config.session_history_txid) {
9218
- const histPath = join22(memoryDir, "session-history.md");
9219
- if (!existsSync19(histPath)) {
9478
+ const histPath = join23(memoryDir, "session-history.md");
9479
+ if (!existsSync20(histPath)) {
9220
9480
  const histResult = await loadFile(config.session_history_txid, { outputPath: histPath });
9221
9481
  if (histResult.success) {
9222
9482
  process.stderr.write(`Indelible: session-history.md restored from chain (file was missing)
@@ -9232,9 +9492,9 @@ async function runPostCompactRestore() {
9232
9492
  try {
9233
9493
  const histDir = findMemoryDir();
9234
9494
  if (histDir) {
9235
- const histPath = join22(histDir, "session-history.md");
9236
- if (existsSync19(histPath)) {
9237
- const histContent = readFileSync16(histPath, "utf-8");
9495
+ const histPath = join23(histDir, "session-history.md");
9496
+ if (existsSync20(histPath)) {
9497
+ const histContent = readFileSync17(histPath, "utf-8");
9238
9498
  const lines = histContent.split("\n");
9239
9499
  const entryStarts = [];
9240
9500
  for (let i = 0; i < lines.length; i++) {
@@ -9276,7 +9536,7 @@ function readStdin() {
9276
9536
  }
9277
9537
  var SERVER_INFO = {
9278
9538
  name: "indelible",
9279
- version: "4.9.6",
9539
+ version: "4.9.8",
9280
9540
  description: "Blockchain-backed memory and code storage for Claude Code"
9281
9541
  };
9282
9542
  var TOOLS = [
@@ -9579,6 +9839,19 @@ var TOOLS = [
9579
9839
  required: []
9580
9840
  }
9581
9841
  },
9842
+ {
9843
+ name: "map_themes",
9844
+ description: 'The table of contents of your mind: the semantic map THEME structure as data \u2014 every theme (continent) with label, session count, first/last touched, days since touched, sample titles, and member txids. Use it to answer "what have I been working on", "which themes have gone cold", then drill into a theme with recall_context depth:"full" txids:[...member_txids] (up to 20 per call). Reads the locally persisted map model (single source with the rendered map); 100% local, nothing leaves the machine. Requires the semantic index (`indelible-mcp semantic-fetch`, one-time).',
9845
+ inputSchema: {
9846
+ type: "object",
9847
+ properties: {
9848
+ sample_titles: { type: "number", description: "How many recent titles to include per theme (default 5, max 20)." },
9849
+ sort: { type: "string", enum: ["size", "cold"], description: '"size" (default) = biggest themes first; "cold" = longest-untouched first.' },
9850
+ theme: { type: "string", description: "Drill ONE theme: a fragment of its name (case-insensitive). Returns just that theme with its 60 newest member txids (the overview shows 5 per theme)." }
9851
+ },
9852
+ required: []
9853
+ }
9854
+ },
9582
9855
  {
9583
9856
  name: "report_bug",
9584
9857
  description: `File an ACCURATE bug report to the Indelible team. You supply ONLY the narrative (summary, description, severity, optional tool_name/last_error). The tool auto-attaches the real MCP version, wallet address, recent save receipts, and environment \u2014 so DO NOT invent versions, txids, URLs, or destinations; leave the facts to the tool. Before filing: (1) triage transients \u2014 retry "No UTXOs"/"gateway unreachable" blips (usually an unconfirmed change UTXO) before reporting; (2) do not file known/already-fixed issues (the server auto-matches these); (3) get the user's OK first. Returns status:"filed" (ticket_id) or status:"known_issue" (instant triage).`,
@@ -9589,7 +9862,8 @@ var TOOLS = [
9589
9862
  description: { type: "string", description: "What you were doing, what went wrong, and exact repro steps." },
9590
9863
  severity: { type: "string", enum: ["low", "medium", "high"], description: "User-facing impact." },
9591
9864
  tool_name: { type: "string", description: "Which Indelible tool/command failed (optional)." },
9592
- last_error: { type: "string", description: "The exact error string you saw (optional)." }
9865
+ last_error: { type: "string", description: "The exact error string you saw (optional)." },
9866
+ still_file: { type: "boolean", description: "Set true ONLY to override a known-issue match \u2014 files a ticket and pages the team even if the report looks like an already-fixed issue. Use when the user says their case is genuinely different." }
9593
9867
  },
9594
9868
  required: ["summary", "description"]
9595
9869
  }
@@ -9735,13 +10009,17 @@ async function handleMcpRequest(request) {
9735
10009
  ranking: args2?.ranking || "blend"
9736
10010
  });
9737
10011
  break;
10012
+ case "map_themes":
10013
+ result = await mapThemes({ sample_titles: args2?.sample_titles, sort: args2?.sort || "size", theme: args2?.theme || null });
10014
+ break;
9738
10015
  case "report_bug":
9739
10016
  result = await reportBug({
9740
10017
  summary: args2?.summary,
9741
10018
  description: args2?.description,
9742
10019
  severity: args2?.severity || "medium",
9743
10020
  tool_name: args2?.tool_name || null,
9744
- last_error: args2?.last_error || null
10021
+ last_error: args2?.last_error || null,
10022
+ still_file: args2?.still_file === true
9745
10023
  });
9746
10024
  break;
9747
10025
  case "share_session":
@@ -9838,11 +10116,11 @@ async function runWizard() {
9838
10116
  }
9839
10117
  let mcpOk = false;
9840
10118
  for (const cfgPath of [
9841
- join22(homedir18(), ".claude.json"),
9842
- join22(homedir18(), ".claude", "settings.json")
10119
+ join23(homedir19(), ".claude.json"),
10120
+ join23(homedir19(), ".claude", "settings.json")
9843
10121
  ]) {
9844
10122
  try {
9845
- const s = JSON.parse(readFileSync16(cfgPath, "utf8"));
10123
+ const s = JSON.parse(readFileSync17(cfgPath, "utf8"));
9846
10124
  if (s?.mcpServers?.indelible) {
9847
10125
  mcpOk = true;
9848
10126
  break;
@@ -9852,10 +10130,10 @@ async function runWizard() {
9852
10130
  }
9853
10131
  console.log(mcpOk ? " \u2713 MCP registered with Claude Code" : " \u2717 MCP not registered");
9854
10132
  if (!mcpOk) allGood = false;
9855
- const hooksPath = join22(homedir18(), ".claude", "settings.local.json");
10133
+ const hooksPath = join23(homedir19(), ".claude", "settings.local.json");
9856
10134
  let hooksOk = false;
9857
10135
  try {
9858
- const s = JSON.parse(readFileSync16(hooksPath, "utf8"));
10136
+ const s = JSON.parse(readFileSync17(hooksPath, "utf8"));
9859
10137
  hooksOk = s?.hooks?.PreCompact?.some(
9860
10138
  (h) => h.hooks?.some((hh) => hh.command?.includes("indelible-mcp")) || h.command?.includes("indelible-mcp")
9861
10139
  ) && s?.hooks?.SessionStart?.some(