indelible-mcp 4.9.7 → 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 +138 -28
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "4.9.7",
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
  };
@@ -4326,17 +4333,37 @@ function computeMapLayout(dated) {
4326
4333
  };
4327
4334
  const globalTF = /* @__PURE__ */ new Map();
4328
4335
  for (const s of dated) for (const w of grams(s.title)) globalTF.set(w, (globalTF.get(w) || 0) + 1);
4329
- function nameOf(members, nTerms) {
4336
+ function nameOf(members, nTerms, allowFallback = false) {
4330
4337
  const tf = /* @__PURE__ */ new Map();
4331
4338
  for (const s of members) for (const w of grams(s.title)) tf.set(w, (tf.get(w) || 0) + 1);
4332
- 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]);
4333
4341
  const picked = [];
4334
4342
  for (const [w] of scored) {
4335
4343
  if (picked.some((p) => p.includes(w) || w.includes(p))) continue;
4336
4344
  picked.push(w);
4337
4345
  if (picked.length >= nTerms) break;
4338
4346
  }
4339
- 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)";
4340
4367
  }
4341
4368
  const centroidOf = (members) => ({
4342
4369
  cx: members.reduce((a, s) => a + s.x, 0) / (members.length || 1),
@@ -4344,7 +4371,7 @@ function computeMapLayout(dated) {
4344
4371
  cz: members.reduce((a, s) => a + s.z, 0) / (members.length || 1)
4345
4372
  });
4346
4373
  function kmeansLocal(members, k) {
4347
- if (members.length < k * 25) return null;
4374
+ if (members.length < k * 3) return null;
4348
4375
  const c0 = [members[Math.floor(rnd() * members.length)].v.slice()];
4349
4376
  while (c0.length < k) {
4350
4377
  const d = members.map((s) => Math.min(...c0.map((c) => dist2(s.v, c))));
@@ -4386,7 +4413,12 @@ function computeMapLayout(dated) {
4386
4413
  const subLabels = [];
4387
4414
  for (let c = 0; c < K; c++) {
4388
4415
  const members = dated.filter((s) => s.c === c);
4389
- const label = nameOf(members, 3);
4416
+ const label = nameOf(
4417
+ members,
4418
+ 3,
4419
+ /* allowFallback */
4420
+ true
4421
+ );
4390
4422
  const mDates = members.map((s) => s.date).sort();
4391
4423
  clusterInfo.push({
4392
4424
  c,
@@ -4397,11 +4429,13 @@ function computeMapLayout(dated) {
4397
4429
  last_touched: mDates.length ? mDates[mDates.length - 1].slice(0, 10) : null,
4398
4430
  ...centroidOf(members)
4399
4431
  });
4400
- const sub = kmeansLocal(members, 3);
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;
4401
4435
  if (sub) {
4402
- for (let sc = 0; sc < 3; sc++) {
4436
+ for (let sc = 0; sc < subK; sc++) {
4403
4437
  const m = members.filter((_, i) => sub[i] === sc);
4404
- if (m.length < 25) continue;
4438
+ if (m.length < subFloor) continue;
4405
4439
  const sl = nameOf(m, 2);
4406
4440
  if (sl === "(misc)" || sl === label) continue;
4407
4441
  subLabels.push({ label: sl, count: m.length, ...centroidOf(m) });
@@ -4648,8 +4682,8 @@ function draw(){
4648
4682
  const vg=g.createRadialGradient(W/2,H/2,H*0.2,W/2,H/2,Math.max(W,H)*0.75);
4649
4683
  vg.addColorStop(0,'#0e0a07');vg.addColorStop(1,'#070503');g.fillStyle=vg;g.fillRect(0,0,W,H);
4650
4684
  projected=P.map(p=>({p,q:proj(p)}));
4651
- // time thread
4652
- 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();
4653
4687
  projected.forEach((e,i)=>{i?g.lineTo(e.q.X,e.q.Y):g.moveTo(e.q.X,e.q.Y)});g.stroke();
4654
4688
  // points: crisp everywhere. Heat = the DOT'S OWN color from the density ramp
4655
4689
  // (no additive accumulation \u2014 that both lagged and blew the core out to mush).
@@ -5596,7 +5630,27 @@ async function _run({ dir, key, address, contentKey, build, broadcast, checkConf
5596
5630
  });
5597
5631
  prune(dir, now()).catch((e) => process.stderr.write(`[build-cache] prune failed (non-fatal): ${e?.message}
5598
5632
  `));
5599
- 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
+ }
5600
5654
  if (write?.txid && fresh.txId && write.txid.toLowerCase() !== fresh.txId.toLowerCase()) {
5601
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
5602
5656
  `);
@@ -5853,7 +5907,7 @@ async function saveFile(filePath, options = {}) {
5853
5907
  }
5854
5908
  const chunkCount = encrypted.length <= MAX_CHUNK_SIZE ? 1 : Math.ceil(encrypted.length / MAX_CHUNK_SIZE);
5855
5909
  const receipt = buildReceipt(_finalWrite, { txId: masterTxId, fee: _saveFee, txSize: _finalSize });
5856
- appendReceipt(receipt, { trigger: "interactive", saveType: "file" });
5910
+ appendReceipt(receipt, { trigger: "interactive", saveType: "file", contentHash: `sha256:${contentHash}` });
5857
5911
  return {
5858
5912
  success: true,
5859
5913
  txId: masterTxId,
@@ -7117,22 +7171,25 @@ async function findByContentKey({
7117
7171
  excludeTxids = [],
7118
7172
  maxScan = 400,
7119
7173
  confirmedOnly = false,
7120
- stopAtFirst = true
7174
+ stopAtFirst = true,
7175
+ matchCap = 10
7121
7176
  }) {
7122
- if (!address || !targetKey) return { matches: [], scanned: 0, truncated: false };
7177
+ if (!address || !targetKey) return { matches: [], scanned: 0, truncated: false, fetchFailures: 0, classification: "absent" };
7123
7178
  const exclude = new Set(excludeTxids.map((t) => String(t).toLowerCase()));
7124
7179
  let history = [];
7125
7180
  try {
7126
7181
  history = await getHistory(address);
7127
7182
  } catch {
7128
- 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" };
7129
7184
  }
7130
- 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" };
7131
7186
  const ordered = [...history].sort((a, b) => (b.height || 0) - (a.height || 0));
7132
7187
  const truncated = ordered.length > maxScan;
7133
7188
  const candidates = ordered.slice(0, maxScan);
7134
7189
  const matches = [];
7135
7190
  let scanned = 0;
7191
+ let fetchFailures = 0;
7192
+ let confirmedFound = false;
7136
7193
  for (const h of candidates) {
7137
7194
  const txid = (h.tx_hash || h.txid || "").toString();
7138
7195
  if (!txid || exclude.has(txid.toLowerCase())) continue;
@@ -7142,23 +7199,62 @@ async function findByContentKey({
7142
7199
  try {
7143
7200
  payload = await fetchPayload(txid);
7144
7201
  } catch {
7202
+ fetchFailures++;
7145
7203
  continue;
7146
7204
  }
7147
7205
  if (payloadContentKey(payload) === targetKey) {
7148
- matches.push({ txid, height: h.height || null });
7149
- 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;
7150
7211
  }
7151
7212
  }
7152
- 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 };
7153
7225
  }
7154
7226
 
7155
7227
  // mcp-server/tools/load_file.js
7156
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
+ }
7157
7247
  async function healStrandedTxid(strandedTxId, config) {
7158
7248
  try {
7249
+ let targetKey = null;
7159
7250
  const cachedRaw = await readFile6(join12(TX_CACHE_DIR3, `${strandedTxId.trim()}.json`), "utf-8").catch(() => null);
7160
- if (!cachedRaw) return null;
7161
- 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());
7162
7258
  const address = config?.address;
7163
7259
  if (!targetKey || !address) return null;
7164
7260
  const { matches } = await findByContentKey({
@@ -8502,7 +8598,7 @@ function redactSecrets2(s) {
8502
8598
  return redactCredentials(String(s));
8503
8599
  }
8504
8600
  async function reportBug(args2 = {}, mcpVersion) {
8505
- 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;
8506
8602
  if (!summary || !description) {
8507
8603
  return { success: false, error: "summary and description are required (you supply the narrative; the tool supplies the facts)." };
8508
8604
  }
@@ -8514,6 +8610,10 @@ async function reportBug(args2 = {}, mcpVersion) {
8514
8610
  severity: ["low", "medium", "high"].includes(severity) ? severity : "medium",
8515
8611
  tool_name: tool_name ? String(tool_name).slice(0, 80) : null,
8516
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,
8517
8617
  // Machine-pulled facts (the AI cannot get these wrong)
8518
8618
  mcp_version: getMcpVersion(mcpVersion),
8519
8619
  wallet_address: getWalletAddress(config),
@@ -8533,9 +8633,17 @@ async function reportBug(args2 = {}, mcpVersion) {
8533
8633
  }
8534
8634
  const data = await resp.json().catch(() => ({}));
8535
8635
  if (data.known_issue) {
8536
- 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
+ };
8537
8645
  }
8538
- 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." };
8539
8647
  } catch (err) {
8540
8648
  return { success: false, error: `could not reach intake (${err.message}). Fallback: save_file the report on-chain and share the txid.` };
8541
8649
  }
@@ -9121,7 +9229,7 @@ Commands:
9121
9229
  }
9122
9230
  function printHelp() {
9123
9231
  console.log(`
9124
- Indelible MCP \u2014 Blockchain memory for Claude Code (v4.9.7)
9232
+ Indelible MCP \u2014 Blockchain memory for Claude Code (v4.9.8)
9125
9233
 
9126
9234
  Setup:
9127
9235
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -9428,7 +9536,7 @@ function readStdin() {
9428
9536
  }
9429
9537
  var SERVER_INFO = {
9430
9538
  name: "indelible",
9431
- version: "4.9.7",
9539
+ version: "4.9.8",
9432
9540
  description: "Blockchain-backed memory and code storage for Claude Code"
9433
9541
  };
9434
9542
  var TOOLS = [
@@ -9754,7 +9862,8 @@ var TOOLS = [
9754
9862
  description: { type: "string", description: "What you were doing, what went wrong, and exact repro steps." },
9755
9863
  severity: { type: "string", enum: ["low", "medium", "high"], description: "User-facing impact." },
9756
9864
  tool_name: { type: "string", description: "Which Indelible tool/command failed (optional)." },
9757
- 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." }
9758
9867
  },
9759
9868
  required: ["summary", "description"]
9760
9869
  }
@@ -9909,7 +10018,8 @@ async function handleMcpRequest(request) {
9909
10018
  description: args2?.description,
9910
10019
  severity: args2?.severity || "medium",
9911
10020
  tool_name: args2?.tool_name || null,
9912
- last_error: args2?.last_error || null
10021
+ last_error: args2?.last_error || null,
10022
+ still_file: args2?.still_file === true
9913
10023
  });
9914
10024
  break;
9915
10025
  case "share_session":