indelible-mcp 4.9.6 → 4.9.7

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 +330 -162
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "4.9.6",
3
+ "version": "4.9.7",
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
@@ -3305,16 +3305,16 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
3305
3305
  let resolvedHistoryPath = "~/.claude/projects/<project>/memory/session-history.md";
3306
3306
  try {
3307
3307
  const projectsDir = join10(homedir10(), ".claude", "projects");
3308
- const { readdirSync: readdirSync3, statSync: statSync6 } = await import("fs");
3308
+ const { readdirSync: readdirSync3, statSync: statSync7 } = await import("fs");
3309
3309
  let newestProject = null;
3310
3310
  let newestTime = 0;
3311
3311
  for (const project of readdirSync3(projectsDir)) {
3312
3312
  const projectPath = join10(projectsDir, project);
3313
3313
  try {
3314
- if (!statSync6(projectPath).isDirectory()) continue;
3314
+ if (!statSync7(projectPath).isDirectory()) continue;
3315
3315
  for (const file of readdirSync3(projectPath)) {
3316
3316
  if (!file.endsWith(".jsonl")) continue;
3317
- const fStat = statSync6(join10(projectPath, file));
3317
+ const fStat = statSync7(join10(projectPath, file));
3318
3318
  if (fStat.mtimeMs > newestTime) {
3319
3319
  newestTime = fStat.mtimeMs;
3320
3320
  newestProject = projectPath;
@@ -3887,11 +3887,12 @@ __export(recall_index_exports, {
3887
3887
  readGaps: () => readGaps,
3888
3888
  readIndex: () => readIndex,
3889
3889
  redactSecrets: () => redactSecrets,
3890
+ resetGaps: () => resetGaps,
3890
3891
  resolveSaveType: () => resolveSaveType,
3891
3892
  searchIndex: () => searchIndex,
3892
3893
  searchIndexSemantic: () => searchIndexSemantic
3893
3894
  });
3894
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync12, appendFileSync as appendFileSync3 } from "fs";
3895
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync8, appendFileSync as appendFileSync3 } from "fs";
3895
3896
  import { join as join18 } from "path";
3896
3897
  import { homedir as homedir15 } from "os";
3897
3898
  function ensureDirs() {
@@ -3961,6 +3962,33 @@ function readIndex(address) {
3961
3962
  function readGaps(address) {
3962
3963
  return readJsonlMap(gapsPath(address));
3963
3964
  }
3965
+ async function resetGaps(address, reasons = []) {
3966
+ const set = new Set(reasons);
3967
+ let removed = 0;
3968
+ await withFileLock(gapsLock(address), () => {
3969
+ let raw;
3970
+ try {
3971
+ raw = readFileSync12(gapsPath(address), "utf8");
3972
+ } catch {
3973
+ return;
3974
+ }
3975
+ const kept = [];
3976
+ for (const line of raw.split("\n")) {
3977
+ if (!line.trim()) continue;
3978
+ let r;
3979
+ try {
3980
+ r = JSON.parse(line);
3981
+ } catch {
3982
+ kept.push(line);
3983
+ continue;
3984
+ }
3985
+ if (set.has(r.reason)) removed++;
3986
+ else kept.push(line);
3987
+ }
3988
+ if (removed) writeFileSync8(gapsPath(address), kept.length ? kept.join("\n") + "\n" : "");
3989
+ });
3990
+ return removed;
3991
+ }
3964
3992
  async function appendJsonl(path, lockPath, records) {
3965
3993
  if (!records.length) return;
3966
3994
  ensureDirs();
@@ -4186,81 +4214,6 @@ var init_recall_index = __esm({
4186
4214
  }
4187
4215
  });
4188
4216
 
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
4217
  // mcp-server/lib/semantic/map-model.js
4265
4218
  function computeMapLayout(dated) {
4266
4219
  const n = dated.length;
@@ -4434,7 +4387,16 @@ function computeMapLayout(dated) {
4434
4387
  for (let c = 0; c < K; c++) {
4435
4388
  const members = dated.filter((s) => s.c === c);
4436
4389
  const label = nameOf(members, 3);
4437
- clusterInfo.push({ c, label, count: members.length, ...centroidOf(members) });
4390
+ const mDates = members.map((s) => s.date).sort();
4391
+ clusterInfo.push({
4392
+ c,
4393
+ label,
4394
+ count: members.length,
4395
+ member_txIds: members.map((s) => s.txId),
4396
+ first_touched: mDates.length ? mDates[0].slice(0, 10) : null,
4397
+ last_touched: mDates.length ? mDates[mDates.length - 1].slice(0, 10) : null,
4398
+ ...centroidOf(members)
4399
+ });
4438
4400
  const sub = kmeansLocal(members, 3);
4439
4401
  if (sub) {
4440
4402
  for (let sc = 0; sc < 3; sc++) {
@@ -4475,8 +4437,15 @@ function computeMapLayout(dated) {
4475
4437
  }
4476
4438
  const hMax = Math.max(...heat);
4477
4439
  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) });
4440
+ 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 }));
4441
+ const nc = (c) => ({
4442
+ label: c.label,
4443
+ count: c.count,
4444
+ x: +NORM(c.cx, "x").toFixed(4),
4445
+ y: +NORM(c.cy, "y").toFixed(4),
4446
+ z: +NORM(c.cz, "z").toFixed(4),
4447
+ ...c.member_txIds ? { member_txIds: c.member_txIds, first_touched: c.first_touched, last_touched: c.last_touched } : {}
4448
+ });
4480
4449
  const cls = clusterInfo.map(nc);
4481
4450
  const subs = subLabels.map(nc);
4482
4451
  return {
@@ -4504,13 +4473,20 @@ var build_map_exports = {};
4504
4473
  __export(build_map_exports, {
4505
4474
  buildSemanticMap: () => buildSemanticMap
4506
4475
  });
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";
4476
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync9, readdirSync, statSync as statSync3, renameSync as renameSync3 } from "node:fs";
4477
+ import { join as join19, dirname as dirname4 } from "node:path";
4478
+ import { homedir as homedir16 } from "node:os";
4479
+ function persistModel(outPath, model) {
4480
+ const modelPath = join19(dirname4(outPath), "SEMANTIC_MAP.json");
4481
+ const tmp = `${modelPath}.tmp-${process.pid}`;
4482
+ writeFileSync9(tmp, JSON.stringify({ ...model, generated_at: (/* @__PURE__ */ new Date()).toISOString() }));
4483
+ renameSync3(tmp, modelPath);
4484
+ return modelPath;
4485
+ }
4510
4486
  function buildSemanticMap({
4511
4487
  addr,
4512
- dir = join21(homedir17(), ".indelible", "recall-index"),
4513
- outPath = join21(homedir17(), ".indelible", "SEMANTIC_MAP.html"),
4488
+ dir = join19(homedir16(), ".indelible", "recall-index"),
4489
+ outPath = join19(homedir16(), ".indelible", "SEMANTIC_MAP.html"),
4514
4490
  log = () => {
4515
4491
  }
4516
4492
  } = {}) {
@@ -4521,7 +4497,7 @@ function buildSemanticMap({
4521
4497
  } catch {
4522
4498
  cands = [];
4523
4499
  }
4524
- const sized = cands.map((f) => ({ f, s: statSync4(join21(dir, f)).size })).sort((a, b) => b.s - a.s);
4500
+ const sized = cands.map((f) => ({ f, s: statSync3(join19(dir, f)).size })).sort((a, b) => b.s - a.s);
4525
4501
  if (!sized.length) {
4526
4502
  const e = new Error("no .embeds.jsonl found \u2014 run `indelible-mcp semantic-fetch` first");
4527
4503
  e.code = "NO_INDEX";
@@ -4532,7 +4508,7 @@ function buildSemanticMap({
4532
4508
  log("address: " + addr);
4533
4509
  let embedsRaw;
4534
4510
  try {
4535
- embedsRaw = readFileSync15(join21(dir, addr + ".embeds.jsonl"), "utf8");
4511
+ embedsRaw = readFileSync14(join19(dir, addr + ".embeds.jsonl"), "utf8");
4536
4512
  } catch {
4537
4513
  const e = new Error(`no semantic index for ${addr} \u2014 run \`indelible-mcp semantic-fetch\` first`);
4538
4514
  e.code = "NO_INDEX";
@@ -4574,7 +4550,7 @@ function buildSemanticMap({
4574
4550
  log("sessions with vectors: " + sessions.length);
4575
4551
  const meta = /* @__PURE__ */ new Map();
4576
4552
  try {
4577
- for (const line of readFileSync15(join21(dir, addr + ".jsonl"), "utf8").split("\n")) {
4553
+ for (const line of readFileSync14(join19(dir, addr + ".jsonl"), "utf8").split("\n")) {
4578
4554
  if (!line.trim()) continue;
4579
4555
  let r;
4580
4556
  try {
@@ -4597,16 +4573,18 @@ function buildSemanticMap({
4597
4573
  log(`dated: ${dated.length} | span: ${span.from} \u2192 ${span.to}`);
4598
4574
  if (dated.length < MIN_FOR_MAP) {
4599
4575
  const html2 = sparsePage(addr, sessions.length, dated.length);
4600
- writeFileSync8(outPath, html2);
4576
+ writeFileSync9(outPath, html2);
4577
+ persistModel(outPath, { sparse: true, count: dated.length, span });
4601
4578
  log(`wrote ${outPath} (sparse floor, ${dated.length}/${MIN_FOR_MAP})`);
4602
4579
  return { path: outPath, sessions: sessions.length, dated: dated.length, K: 0, districts: 0, sparse: true, span };
4603
4580
  }
4604
4581
  log("PCA + k-means + density (shared compute)...");
4605
4582
  const model = computeMapLayout(dated);
4606
4583
  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 };
4584
+ writeFileSync9(outPath, html);
4585
+ const modelPath = persistModel(outPath, model);
4586
+ log(`wrote ${outPath} (${Math.round(html.length / 1024)} KB) + ${modelPath}`);
4587
+ return { path: outPath, modelPath, sessions: sessions.length, dated: dated.length, K: model.K, districts: model.subs.length, sparse: false, span };
4610
4588
  }
4611
4589
  function sparsePage(addr, total, dated) {
4612
4590
  return `<!doctype html><meta charset="utf-8"><title>Indelible \u2014 The Shape of the Work</title>
@@ -4731,6 +4709,81 @@ var init_build_map = __esm({
4731
4709
  }
4732
4710
  });
4733
4711
 
4712
+ // mcp-server/lib/semantic/fetch-pack.js
4713
+ var fetch_pack_exports = {};
4714
+ __export(fetch_pack_exports, {
4715
+ fetchPack: () => fetchPack
4716
+ });
4717
+ import { createHash as createHash5 } from "crypto";
4718
+ import { createWriteStream, existsSync as existsSync19, mkdirSync as mkdirSync8, renameSync as renameSync4, rmSync, statSync as statSync5 } from "fs";
4719
+ import { join as join22 } from "path";
4720
+ async function sha256File(path) {
4721
+ const { createReadStream: createReadStream3 } = await import("fs");
4722
+ return new Promise((resolve3, reject) => {
4723
+ const h = createHash5("sha256");
4724
+ createReadStream3(path).on("data", (d) => h.update(d)).on("end", () => resolve3(h.digest("hex"))).on("error", reject);
4725
+ });
4726
+ }
4727
+ async function fetchToTmp(url, tmpPath, timeoutMs) {
4728
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs), redirect: "follow" });
4729
+ if (!res.ok || !res.body) throw new PackError("FETCH_FAIL", `HTTP ${res.status} for ${url}`);
4730
+ const { Readable } = await import("stream");
4731
+ const { pipeline } = await import("stream/promises");
4732
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(tmpPath));
4733
+ }
4734
+ async function fetchPack({ profile = PROFILE, timeoutMs = 3e5, onProgress = null } = {}) {
4735
+ const dir = packDir(profile);
4736
+ mkdirSync8(dir, { recursive: true, mode: 448 });
4737
+ const fetched = [];
4738
+ const skipped = [];
4739
+ for (const f of PACK_FILES) {
4740
+ const finalPath = join22(dir, f.name);
4741
+ if (existsSync19(finalPath) && statSync5(finalPath).size === f.bytes) {
4742
+ skipped.push(f.name);
4743
+ continue;
4744
+ }
4745
+ const tmpPath = `${finalPath}.tmp-${process.pid}`;
4746
+ try {
4747
+ if (onProgress) onProgress({ file: f.name, bytes: f.bytes, state: "fetching" });
4748
+ await fetchToTmp(f.url, tmpPath, timeoutMs);
4749
+ const gotHash = await sha256File(tmpPath);
4750
+ if (gotHash !== f.sha256) {
4751
+ throw new PackError(
4752
+ "HASH_MISMATCH",
4753
+ `${f.name}: expected ${f.sha256.slice(0, 12)}\u2026, got ${gotHash.slice(0, 12)}\u2026 \u2014 refusing (upstream drifted or tampered)`
4754
+ );
4755
+ }
4756
+ const gotSize = statSync5(tmpPath).size;
4757
+ if (gotSize !== f.bytes) {
4758
+ throw new PackError("HASH_MISMATCH", `${f.name}: expected ${f.bytes} bytes, got ${gotSize}`);
4759
+ }
4760
+ renameSync4(tmpPath, finalPath);
4761
+ fetched.push(f.name);
4762
+ if (onProgress) onProgress({ file: f.name, bytes: f.bytes, state: "verified" });
4763
+ } catch (err) {
4764
+ try {
4765
+ rmSync(tmpPath, { force: true });
4766
+ } catch {
4767
+ }
4768
+ if (err instanceof PackError) throw err;
4769
+ throw new PackError("FETCH_FAIL", `${f.name}: ${err.message}`);
4770
+ }
4771
+ }
4772
+ return { ok: true, fetched, skipped, dir };
4773
+ }
4774
+ var PackError;
4775
+ var init_fetch_pack = __esm({
4776
+ "mcp-server/lib/semantic/fetch-pack.js"() {
4777
+ init_manifest();
4778
+ PackError = class extends Error {
4779
+ constructor(code, message) {
4780
+ super(message);
4781
+ this.code = code;
4782
+ }
4783
+ };
4784
+ }
4785
+ });
4786
+
4734
4787
  // mcp-server/lib/timecard.js
4735
4788
  var timecard_exports = {};
4736
4789
  __export(timecard_exports, {
@@ -4787,10 +4840,10 @@ init_config_customer();
4787
4840
  init_api_client();
4788
4841
  import { createInterface as createInterface3 } from "node:readline";
4789
4842
  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";
4843
+ import { homedir as homedir19 } from "node:os";
4844
+ import { join as join23, dirname as dirname6, resolve as resolve2 } from "node:path";
4792
4845
  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";
4846
+ import { readFileSync as readFileSync17, writeFileSync as writeFileSync10, existsSync as existsSync20, mkdirSync as mkdirSync9, readdirSync as readdirSync2, statSync as statSync6 } from "node:fs";
4794
4847
 
4795
4848
  // mcp-server/lib/guardrails-run.js
4796
4849
  function contextFromInput(input) {
@@ -8302,32 +8355,108 @@ async function recallContext({ from_date = null, to_date = null, query = null, d
8302
8355
  };
8303
8356
  }
8304
8357
 
8358
+ // mcp-server/tools/map_themes.js
8359
+ init_config_customer();
8360
+ import { readFileSync as readFileSync15, existsSync as existsSync17, statSync as statSync4 } from "fs";
8361
+ import { join as join20 } from "path";
8362
+ import { homedir as homedir17 } from "os";
8363
+ var MODEL_PATH = join20(homedir17(), ".indelible", "SEMANTIC_MAP.json");
8364
+ var TXIDS_OVERVIEW = 5;
8365
+ var TXIDS_DEEP = 60;
8366
+ async function mapThemes({ sample_titles = 5, sort = "size", theme = null } = {}) {
8367
+ const config = await loadConfig();
8368
+ const addr = config?.address;
8369
+ if (!addr) return { success: false, error: "No wallet configured \u2014 run setup_wallet first." };
8370
+ const indexPath2 = join20(homedir17(), ".indelible", "recall-index", `${addr}.jsonl`);
8371
+ let fresh = false;
8372
+ try {
8373
+ fresh = existsSync17(MODEL_PATH) && (!existsSync17(indexPath2) || statSync4(MODEL_PATH).mtimeMs >= statSync4(indexPath2).mtimeMs);
8374
+ } catch {
8375
+ }
8376
+ if (!fresh) {
8377
+ try {
8378
+ const { buildSemanticMap: buildSemanticMap2 } = await Promise.resolve().then(() => (init_build_map(), build_map_exports));
8379
+ buildSemanticMap2({ addr, log: () => {
8380
+ } });
8381
+ } catch (e) {
8382
+ if (e.code === "NO_INDEX") {
8383
+ 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." };
8384
+ }
8385
+ return { success: false, error: `Could not build the map model: ${e.message}` };
8386
+ }
8387
+ }
8388
+ let model;
8389
+ try {
8390
+ model = JSON.parse(readFileSync15(MODEL_PATH, "utf8"));
8391
+ } catch (e) {
8392
+ return { success: false, error: `Could not read the map model (${e.message}) \u2014 run \`indelible-mcp map\` to regenerate it.` };
8393
+ }
8394
+ if (model.sparse) {
8395
+ 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.` };
8396
+ }
8397
+ const now = Date.now();
8398
+ const nTitles = Math.max(0, Math.min(20, Number(sample_titles) || 5));
8399
+ const themeFilter = theme ? String(theme).toLowerCase() : null;
8400
+ const txidCap = themeFilter ? TXIDS_DEEP : TXIDS_OVERVIEW;
8401
+ let themes = model.cls.map((c, i) => {
8402
+ const members = model.pts.filter((p) => p.c === i).sort((a, b) => a.d < b.d ? 1 : -1);
8403
+ const last = c.last_touched || members[0]?.d || null;
8404
+ const txids = c.member_txIds ? [...c.member_txIds].reverse() : members.map((p) => p.id);
8405
+ return {
8406
+ theme: c.label,
8407
+ sessions: c.count,
8408
+ first_touched: c.first_touched || members[members.length - 1]?.d || null,
8409
+ last_touched: last,
8410
+ days_since_touched: last ? Math.max(0, Math.floor((now - Date.parse(last)) / 864e5)) : null,
8411
+ sample_titles: members.slice(0, nTitles).map((p) => `${p.d} \xB7 ${p.t || "(untitled)"}`),
8412
+ member_txids: txids.slice(0, txidCap),
8413
+ member_txids_truncated: txids.length > txidCap ? txids.length : false
8414
+ };
8415
+ });
8416
+ if (themeFilter) {
8417
+ const matched = themes.filter((t) => t.theme.toLowerCase().includes(themeFilter));
8418
+ if (!matched.length) {
8419
+ return { success: false, error: `No theme matches "${theme}". Themes: ${themes.map((t) => t.theme).join(" | ")}` };
8420
+ }
8421
+ themes = matched;
8422
+ }
8423
+ themes.sort(sort === "cold" ? (a, b) => (b.days_since_touched ?? -1) - (a.days_since_touched ?? -1) : (a, b) => b.sessions - a.sessions);
8424
+ return {
8425
+ success: true,
8426
+ generated_at: model.generated_at || null,
8427
+ mapped_sessions: model.count,
8428
+ span: model.span || null,
8429
+ themes,
8430
+ 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).`
8431
+ };
8432
+ }
8433
+
8305
8434
  // 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";
8435
+ import { readFileSync as readFileSync16, existsSync as existsSync18 } from "fs";
8436
+ import { join as join21, dirname as dirname5 } from "path";
8437
+ import { homedir as homedir18 } from "os";
8309
8438
  import { fileURLToPath } from "url";
8310
8439
  import os from "os";
8311
8440
  var INTAKE_URL = process.env.INDELIBLE_BUG_INTAKE || "https://indelible.one/api/bug-report";
8312
- var INDELIBLE_DIR = join19(homedir16(), ".indelible");
8441
+ var INDELIBLE_DIR = join21(homedir18(), ".indelible");
8313
8442
  function getMcpVersion(injected) {
8314
8443
  if (injected) return injected;
8315
8444
  const candidates = [];
8316
8445
  try {
8317
- candidates.push(join19(dirname4(fileURLToPath(import.meta.url)), "..", "..", "package.json"));
8446
+ candidates.push(join21(dirname5(fileURLToPath(import.meta.url)), "..", "..", "package.json"));
8318
8447
  } catch {
8319
8448
  }
8320
8449
  try {
8321
- candidates.push(join19(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"));
8450
+ candidates.push(join21(dirname5(fileURLToPath(import.meta.url)), "..", "package.json"));
8322
8451
  } catch {
8323
8452
  }
8324
- candidates.push(join19(homedir16(), "AppData", "Roaming", "npm", "node_modules", "indelible-mcp", "package.json"));
8453
+ candidates.push(join21(homedir18(), "AppData", "Roaming", "npm", "node_modules", "indelible-mcp", "package.json"));
8325
8454
  candidates.push("/usr/local/lib/node_modules/indelible-mcp/package.json");
8326
8455
  candidates.push("/usr/lib/node_modules/indelible-mcp/package.json");
8327
8456
  for (const p of candidates) {
8328
8457
  try {
8329
- if (!existsSync17(p)) continue;
8330
- const pkg = JSON.parse(readFileSync14(p, "utf8"));
8458
+ if (!existsSync18(p)) continue;
8459
+ const pkg = JSON.parse(readFileSync16(p, "utf8"));
8331
8460
  if (pkg && pkg.name === "indelible-mcp" && pkg.version) return pkg.version;
8332
8461
  } catch {
8333
8462
  }
@@ -8336,7 +8465,7 @@ function getMcpVersion(injected) {
8336
8465
  }
8337
8466
  function readJson(path) {
8338
8467
  try {
8339
- return existsSync17(path) ? JSON.parse(readFileSync14(path, "utf8")) : null;
8468
+ return existsSync18(path) ? JSON.parse(readFileSync16(path, "utf8")) : null;
8340
8469
  } catch {
8341
8470
  return null;
8342
8471
  }
@@ -8346,9 +8475,9 @@ function getWalletAddress(config) {
8346
8475
  }
8347
8476
  function getRecentReceipts(n = 5) {
8348
8477
  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);
8478
+ const p = join21(INDELIBLE_DIR, "save-log.jsonl");
8479
+ if (!existsSync18(p)) return [];
8480
+ const lines = readFileSync16(p, "utf8").trim().split("\n").filter(Boolean).slice(-n);
8352
8481
  return lines.map((l) => {
8353
8482
  try {
8354
8483
  const e = JSON.parse(l);
@@ -8377,7 +8506,7 @@ async function reportBug(args2 = {}, mcpVersion) {
8377
8506
  if (!summary || !description) {
8378
8507
  return { success: false, error: "summary and description are required (you supply the narrative; the tool supplies the facts)." };
8379
8508
  }
8380
- const config = readJson(join19(INDELIBLE_DIR, "config.json")) || {};
8509
+ const config = readJson(join21(INDELIBLE_DIR, "config.json")) || {};
8381
8510
  const payload = {
8382
8511
  // AI-supplied narrative (defensively redacted + length-capped)
8383
8512
  summary: redactSecrets2(summary).slice(0, 200),
@@ -8647,16 +8776,16 @@ init_wall_stub();
8647
8776
  init_wall_stub();
8648
8777
  init_wall_stub();
8649
8778
  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");
8779
+ var PROJECT_ROOT_FOR_CLONE_CHECK = resolve2(dirname6(fileURLToPath2(import.meta.url)), "../..");
8780
+ var CONTEXT_FILE2 = join23(homedir19(), ".indelible", "indelible-context.jsonl");
8652
8781
  function installHooks() {
8653
- const claudeDir = join22(homedir18(), ".claude");
8654
- const settingsPath = join22(claudeDir, "settings.local.json");
8655
- if (!existsSync19(claudeDir)) mkdirSync9(claudeDir, { recursive: true });
8782
+ const claudeDir = join23(homedir19(), ".claude");
8783
+ const settingsPath = join23(claudeDir, "settings.local.json");
8784
+ if (!existsSync20(claudeDir)) mkdirSync9(claudeDir, { recursive: true });
8656
8785
  let settings = {};
8657
- if (existsSync19(settingsPath)) {
8786
+ if (existsSync20(settingsPath)) {
8658
8787
  try {
8659
- settings = JSON.parse(readFileSync16(settingsPath, "utf8"));
8788
+ settings = JSON.parse(readFileSync17(settingsPath, "utf8"));
8660
8789
  } catch {
8661
8790
  settings = {};
8662
8791
  }
@@ -8707,13 +8836,13 @@ function installHooks() {
8707
8836
  });
8708
8837
  installed.push("PreToolUse:guardrails");
8709
8838
  }
8710
- writeFileSync9(settingsPath, JSON.stringify(settings, null, 2));
8839
+ writeFileSync10(settingsPath, JSON.stringify(settings, null, 2));
8711
8840
  return { settingsPath, installed, alreadyInstalled: installed.length === 0 };
8712
8841
  }
8713
8842
  function runPreToolUseGuard() {
8714
8843
  let code = 0;
8715
8844
  try {
8716
- const raw = readFileSync16(0, "utf8").trim();
8845
+ const raw = readFileSync17(0, "utf8").trim();
8717
8846
  if (!raw) process.exit(0);
8718
8847
  const hit = evaluate(JSON.parse(raw));
8719
8848
  if (hit) {
@@ -8913,6 +9042,7 @@ Commands:
8913
9042
  const { packPresent: packPresent2 } = await Promise.resolve().then(() => (init_manifest(), manifest_exports));
8914
9043
  const { fetchPack: fetchPack2 } = await Promise.resolve().then(() => (init_fetch_pack(), fetch_pack_exports));
8915
9044
  const { rebuildVectorSidecar: rebuildVectorSidecar2 } = await Promise.resolve().then(() => (init_vec_store(), vec_store_exports));
9045
+ const { buildIndexWindow: buildIndexWindow2, resetGaps: resetGaps2 } = await Promise.resolve().then(() => (init_recall_index(), recall_index_exports));
8916
9046
  try {
8917
9047
  if (!packPresent2()) {
8918
9048
  console.log("Fetching the semantic model pack (~50MB, one time, hash-verified)...");
@@ -8926,15 +9056,37 @@ Commands:
8926
9056
  console.log(JSON.stringify({ success: false, error: "No wallet configured \u2014 run setup first." }));
8927
9057
  break;
8928
9058
  }
9059
+ const wif = await getWif();
9060
+ if (!wif) {
9061
+ 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.)");
9062
+ } else {
9063
+ const retried = await resetGaps2(cfg.address, ["FETCH_TIMEOUT", "NOT_ON_CHAIN", "WALLET_LOCKED"]);
9064
+ if (retried) console.log(` re-attempting ${retried} previously-skipped sessions...`);
9065
+ console.log(`Reading your on-chain history for ${cfg.address} (one-time, resumable, all local)...`);
9066
+ let indexed = 0;
9067
+ for (; ; ) {
9068
+ const r = await buildIndexWindow2(cfg.address, wif, { maxDecrypt: 150 });
9069
+ indexed += r.indexed;
9070
+ console.log(` indexed ${indexed} sessions; ${r.remaining} to go`);
9071
+ if (r.remaining === 0 || r.indexed === 0 && r.gapped === 0) break;
9072
+ }
9073
+ }
8929
9074
  console.log(`Backfilling meaning-vectors for ${cfg.address} (resumable \u2014 re-run to continue)...`);
8930
- let total = 0;
9075
+ let total = 0, cov = { vecs: 0, indexed: 0 };
8931
9076
  for (; ; ) {
8932
9077
  const r = await rebuildVectorSidecar2(cfg.address, { maxEmbed: 100 });
8933
9078
  total += r.embedded;
8934
- console.log(` embedded ${total} sessions; remaining ${r.remaining}; coverage ${r.coverage.vecs}/${r.coverage.indexed}`);
9079
+ cov = r.coverage || cov;
9080
+ console.log(` embedded ${total} sessions; remaining ${r.remaining}; coverage ${cov.vecs}/${cov.indexed}`);
8935
9081
  if (r.remaining === 0 || r.embedded === 0) break;
8936
9082
  }
8937
- console.log(JSON.stringify({ success: true, message: "Semantic recall enabled \u2014 meaning-based search is live in recall_context.", embedded: total }));
9083
+ if (cov.vecs > 0) {
9084
+ 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 }));
9085
+ } else if (!wif) {
9086
+ 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." }));
9087
+ } else {
9088
+ 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 }));
9089
+ }
8938
9090
  } catch (e) {
8939
9091
  console.log(JSON.stringify({ success: false, error: e.code === "HASH_MISMATCH" ? `Integrity check failed: ${e.message}` : e.message }));
8940
9092
  }
@@ -8942,9 +9094,9 @@ Commands:
8942
9094
  }
8943
9095
  case "map": {
8944
9096
  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");
9097
+ const { join: join24 } = await import("node:path");
9098
+ const { homedir: homedir20 } = await import("node:os");
9099
+ const out = join24(homedir20(), ".indelible", "SEMANTIC_MAP.html");
8948
9100
  try {
8949
9101
  const cfg = await loadConfig();
8950
9102
  const r = buildSemanticMap2({ addr: cfg?.address, outPath: out, log: (m) => process.stderr.write(`[map] ${m}
@@ -8958,7 +9110,7 @@ Commands:
8958
9110
  ${r.path}`);
8959
9111
  }
8960
9112
  } 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`.");
9113
+ 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
9114
  else console.log(JSON.stringify({ success: false, error: e.message }));
8963
9115
  }
8964
9116
  break;
@@ -8969,7 +9121,7 @@ Commands:
8969
9121
  }
8970
9122
  function printHelp() {
8971
9123
  console.log(`
8972
- Indelible MCP \u2014 Blockchain memory for Claude Code (v4.9.6)
9124
+ Indelible MCP \u2014 Blockchain memory for Claude Code (v4.9.7)
8973
9125
 
8974
9126
  Setup:
8975
9127
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -9018,19 +9170,19 @@ Learn more: https://indelible.one
9018
9170
  `);
9019
9171
  }
9020
9172
  function findNewestTranscript() {
9021
- const projectsDir = join22(homedir18(), ".claude", "projects");
9022
- if (!existsSync19(projectsDir)) return null;
9173
+ const projectsDir = join23(homedir19(), ".claude", "projects");
9174
+ if (!existsSync20(projectsDir)) return null;
9023
9175
  let newestTime = 0;
9024
9176
  let newest = null;
9025
9177
  try {
9026
9178
  for (const project of readdirSync2(projectsDir)) {
9027
- const projectPath = join22(projectsDir, project);
9179
+ const projectPath = join23(projectsDir, project);
9028
9180
  try {
9029
- if (!statSync5(projectPath).isDirectory()) continue;
9181
+ if (!statSync6(projectPath).isDirectory()) continue;
9030
9182
  for (const file of readdirSync2(projectPath)) {
9031
9183
  if (!file.endsWith(".jsonl")) continue;
9032
- const p = join22(projectPath, file);
9033
- const t = statSync5(p).mtimeMs;
9184
+ const p = join23(projectPath, file);
9185
+ const t = statSync6(p).mtimeMs;
9034
9186
  if (t > newestTime) {
9035
9187
  newestTime = t;
9036
9188
  newest = p;
@@ -9053,13 +9205,13 @@ async function printTimeCard() {
9053
9205
  };
9054
9206
  try {
9055
9207
  const t = findNewestTranscript();
9056
- if (t) f.lastSeen = phraseDelta2(Date.now() - statSync5(t).mtimeMs);
9208
+ if (t) f.lastSeen = phraseDelta2(Date.now() - statSync6(t).mtimeMs);
9057
9209
  } catch {
9058
9210
  }
9059
9211
  try {
9060
- const gPath = join22(homedir18(), ".indelible", "goals.json");
9061
- if (existsSync19(gPath)) {
9062
- const g = JSON.parse(readFileSync16(gPath, "utf8"));
9212
+ const gPath = join23(homedir19(), ".indelible", "goals.json");
9213
+ if (existsSync20(gPath)) {
9214
+ const g = JSON.parse(readFileSync17(gPath, "utf8"));
9063
9215
  const active = (g.goals || []).filter((x) => x.status === "active");
9064
9216
  if (active.length) {
9065
9217
  const oldest = active.reduce((a, b) => new Date(a.created_at) < new Date(b.created_at) ? a : b);
@@ -9095,21 +9247,21 @@ async function runPreCompactSave() {
9095
9247
  process.stderr.write("Indelible: MCP disabled, skipping save\n");
9096
9248
  process.exit(0);
9097
9249
  }
9098
- const target = transcriptPath && existsSync19(transcriptPath) ? transcriptPath : findNewestTranscript() || CONTEXT_FILE2;
9250
+ const target = transcriptPath && existsSync20(transcriptPath) ? transcriptPath : findNewestTranscript() || CONTEXT_FILE2;
9099
9251
  const result = await saveSession(target, `Auto-save before ${trigger} compaction`);
9100
9252
  if (result.success) {
9101
9253
  process.stderr.write(`Indelible: Saved ${result.newMessages} messages (${result.saveType}) tx:${result.txId?.slice(0, 12)}...
9102
9254
  `);
9103
9255
  } else if (result.gated && result.upsell) {
9104
- const upsellMarker = join22(homedir18(), ".indelible", ".upsell-shown");
9256
+ const upsellMarker = join23(homedir19(), ".indelible", ".upsell-shown");
9105
9257
  let shownRecently = false;
9106
9258
  try {
9107
- shownRecently = Date.now() - statSync5(upsellMarker).mtimeMs < 24 * 60 * 60 * 1e3;
9259
+ shownRecently = Date.now() - statSync6(upsellMarker).mtimeMs < 24 * 60 * 60 * 1e3;
9108
9260
  } catch {
9109
9261
  }
9110
9262
  if (!shownRecently) {
9111
9263
  try {
9112
- writeFileSync9(upsellMarker, (/* @__PURE__ */ new Date()).toISOString());
9264
+ writeFileSync10(upsellMarker, (/* @__PURE__ */ new Date()).toISOString());
9113
9265
  } catch {
9114
9266
  }
9115
9267
  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 +9283,20 @@ async function runPreCompactSave() {
9131
9283
  process.exit(0);
9132
9284
  }
9133
9285
  function findMemoryDir() {
9134
- const projectsDir = join22(homedir18(), ".claude", "projects");
9135
- if (!existsSync19(projectsDir)) return null;
9286
+ const projectsDir = join23(homedir19(), ".claude", "projects");
9287
+ if (!existsSync20(projectsDir)) return null;
9136
9288
  let newestTime = 0;
9137
9289
  let newestProject = null;
9138
9290
  const projects = readdirSync2(projectsDir);
9139
9291
  for (const project of projects) {
9140
- const projectPath = join22(projectsDir, project);
9292
+ const projectPath = join23(projectsDir, project);
9141
9293
  try {
9142
- const pStat = statSync5(projectPath);
9294
+ const pStat = statSync6(projectPath);
9143
9295
  if (!pStat.isDirectory()) continue;
9144
9296
  const files = readdirSync2(projectPath);
9145
9297
  for (const file of files) {
9146
9298
  if (!file.endsWith(".jsonl")) continue;
9147
- const fStat = statSync5(join22(projectPath, file));
9299
+ const fStat = statSync6(join23(projectPath, file));
9148
9300
  if (fStat.mtimeMs > newestTime) {
9149
9301
  newestTime = fStat.mtimeMs;
9150
9302
  newestProject = projectPath;
@@ -9153,7 +9305,7 @@ function findMemoryDir() {
9153
9305
  } catch {
9154
9306
  }
9155
9307
  }
9156
- return newestProject ? join22(newestProject, "memory") : null;
9308
+ return newestProject ? join23(newestProject, "memory") : null;
9157
9309
  }
9158
9310
  async function runPostCompactRestore() {
9159
9311
  await printTimeCard();
@@ -9203,10 +9355,10 @@ async function runPostCompactRestore() {
9203
9355
  try {
9204
9356
  const memoryDir = findMemoryDir();
9205
9357
  if (memoryDir && (config.memory_file_txid || config.session_history_txid)) {
9206
- if (!existsSync19(memoryDir)) mkdirSync9(memoryDir, { recursive: true });
9358
+ if (!existsSync20(memoryDir)) mkdirSync9(memoryDir, { recursive: true });
9207
9359
  if (config.memory_file_txid) {
9208
- const memPath = join22(memoryDir, "MEMORY.md");
9209
- if (!existsSync19(memPath)) {
9360
+ const memPath = join23(memoryDir, "MEMORY.md");
9361
+ if (!existsSync20(memPath)) {
9210
9362
  const memResult = await loadFile(config.memory_file_txid, { outputPath: memPath });
9211
9363
  if (memResult.success) {
9212
9364
  process.stderr.write(`Indelible: MEMORY.md restored from chain (file was missing)
@@ -9215,8 +9367,8 @@ async function runPostCompactRestore() {
9215
9367
  }
9216
9368
  }
9217
9369
  if (config.session_history_txid) {
9218
- const histPath = join22(memoryDir, "session-history.md");
9219
- if (!existsSync19(histPath)) {
9370
+ const histPath = join23(memoryDir, "session-history.md");
9371
+ if (!existsSync20(histPath)) {
9220
9372
  const histResult = await loadFile(config.session_history_txid, { outputPath: histPath });
9221
9373
  if (histResult.success) {
9222
9374
  process.stderr.write(`Indelible: session-history.md restored from chain (file was missing)
@@ -9232,9 +9384,9 @@ async function runPostCompactRestore() {
9232
9384
  try {
9233
9385
  const histDir = findMemoryDir();
9234
9386
  if (histDir) {
9235
- const histPath = join22(histDir, "session-history.md");
9236
- if (existsSync19(histPath)) {
9237
- const histContent = readFileSync16(histPath, "utf-8");
9387
+ const histPath = join23(histDir, "session-history.md");
9388
+ if (existsSync20(histPath)) {
9389
+ const histContent = readFileSync17(histPath, "utf-8");
9238
9390
  const lines = histContent.split("\n");
9239
9391
  const entryStarts = [];
9240
9392
  for (let i = 0; i < lines.length; i++) {
@@ -9276,7 +9428,7 @@ function readStdin() {
9276
9428
  }
9277
9429
  var SERVER_INFO = {
9278
9430
  name: "indelible",
9279
- version: "4.9.6",
9431
+ version: "4.9.7",
9280
9432
  description: "Blockchain-backed memory and code storage for Claude Code"
9281
9433
  };
9282
9434
  var TOOLS = [
@@ -9579,6 +9731,19 @@ var TOOLS = [
9579
9731
  required: []
9580
9732
  }
9581
9733
  },
9734
+ {
9735
+ name: "map_themes",
9736
+ 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).',
9737
+ inputSchema: {
9738
+ type: "object",
9739
+ properties: {
9740
+ sample_titles: { type: "number", description: "How many recent titles to include per theme (default 5, max 20)." },
9741
+ sort: { type: "string", enum: ["size", "cold"], description: '"size" (default) = biggest themes first; "cold" = longest-untouched first.' },
9742
+ 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)." }
9743
+ },
9744
+ required: []
9745
+ }
9746
+ },
9582
9747
  {
9583
9748
  name: "report_bug",
9584
9749
  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).`,
@@ -9735,6 +9900,9 @@ async function handleMcpRequest(request) {
9735
9900
  ranking: args2?.ranking || "blend"
9736
9901
  });
9737
9902
  break;
9903
+ case "map_themes":
9904
+ result = await mapThemes({ sample_titles: args2?.sample_titles, sort: args2?.sort || "size", theme: args2?.theme || null });
9905
+ break;
9738
9906
  case "report_bug":
9739
9907
  result = await reportBug({
9740
9908
  summary: args2?.summary,
@@ -9838,11 +10006,11 @@ async function runWizard() {
9838
10006
  }
9839
10007
  let mcpOk = false;
9840
10008
  for (const cfgPath of [
9841
- join22(homedir18(), ".claude.json"),
9842
- join22(homedir18(), ".claude", "settings.json")
10009
+ join23(homedir19(), ".claude.json"),
10010
+ join23(homedir19(), ".claude", "settings.json")
9843
10011
  ]) {
9844
10012
  try {
9845
- const s = JSON.parse(readFileSync16(cfgPath, "utf8"));
10013
+ const s = JSON.parse(readFileSync17(cfgPath, "utf8"));
9846
10014
  if (s?.mcpServers?.indelible) {
9847
10015
  mcpOk = true;
9848
10016
  break;
@@ -9852,10 +10020,10 @@ async function runWizard() {
9852
10020
  }
9853
10021
  console.log(mcpOk ? " \u2713 MCP registered with Claude Code" : " \u2717 MCP not registered");
9854
10022
  if (!mcpOk) allGood = false;
9855
- const hooksPath = join22(homedir18(), ".claude", "settings.local.json");
10023
+ const hooksPath = join23(homedir19(), ".claude", "settings.local.json");
9856
10024
  let hooksOk = false;
9857
10025
  try {
9858
- const s = JSON.parse(readFileSync16(hooksPath, "utf8"));
10026
+ const s = JSON.parse(readFileSync17(hooksPath, "utf8"));
9859
10027
  hooksOk = s?.hooks?.PreCompact?.some(
9860
10028
  (h) => h.hooks?.some((hh) => hh.command?.includes("indelible-mcp")) || h.command?.includes("indelible-mcp")
9861
10029
  ) && s?.hooks?.SessionStart?.some(