indelible-mcp 4.5.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +351 -56
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "4.5.0",
3
+ "version": "4.6.0",
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
@@ -257,11 +257,42 @@ async function checkHealth() {
257
257
  }
258
258
  }
259
259
  async function getUtxos(address) {
260
- const res = await spvFetch(`/api/address/${address}/unspent`);
261
- const utxos = await res.json();
262
- process.stderr.write(`[MCP] UTXOs from bridge: ${Array.isArray(utxos) ? utxos.length : 0}
260
+ const path = `/api/address/${address}/unspent`;
261
+ const res = await spvFetch(path);
262
+ const first = await res.json();
263
+ if (Array.isArray(first) && first.length > 0) {
264
+ process.stderr.write(`[MCP] UTXOs from bridge: ${first.length}
263
265
  `);
264
- return utxos;
266
+ return first;
267
+ }
268
+ let reached = 0;
269
+ try {
270
+ const bridges = await getBridges();
271
+ const settled = await Promise.allSettled(bridges.map(async (bridge) => {
272
+ const r = await fetch(`${bridge.url}${path}`, {
273
+ signal: AbortSignal.timeout(6e3),
274
+ headers: { "X-API-Key": bridge.apiKey }
275
+ });
276
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
277
+ return { name: bridge.name, utxos: await r.json() };
278
+ }));
279
+ for (const s of settled) {
280
+ if (s.status !== "fulfilled") continue;
281
+ reached++;
282
+ const u = s.value.utxos;
283
+ if (Array.isArray(u) && u.length > 0) {
284
+ process.stderr.write(`[MCP] UTXOs recovered via fan-out from ${s.value.name}: ${u.length}
285
+ `);
286
+ return u;
287
+ }
288
+ }
289
+ } catch (e) {
290
+ process.stderr.write(`[MCP] UTXO fan-out error: ${e.message}
291
+ `);
292
+ }
293
+ process.stderr.write(`[MCP] UTXOs: 0 across ${reached} bridge(s)
294
+ `);
295
+ return Array.isArray(first) ? first : [];
265
296
  }
266
297
  async function getAddressHistory(address) {
267
298
  const res = await spvFetch(`/api/address/${address}/history`);
@@ -838,7 +869,8 @@ async function commitSession(session, wif) {
838
869
  if (utxos && utxos.length > 0) {
839
870
  const payload = {
840
871
  protocol: "indelible.claude-code",
841
- encrypted: session.encrypted
872
+ encrypted: session.encrypted,
873
+ wrap_owner: session.wrap_owner || null
842
874
  };
843
875
  const { txHex, txId, changeUtxos, fee, txSize } = await buildOpReturnTxWithChange(wif, utxos, JSON.stringify(payload));
844
876
  const writeReceipt = await broadcastTx(txHex);
@@ -849,17 +881,22 @@ async function commitSession(session, wif) {
849
881
  } catch {
850
882
  }
851
883
  }
852
- await indexSession({
853
- txId,
854
- address: session.address,
855
- encrypted: session.encrypted,
856
- session_id: session.session_id,
857
- prev_session_id: session.prev_session_id,
858
- summary_enc: summaryEnc,
859
- message_count: session.message_count,
860
- save_type: session.type || "full",
861
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
862
- }, config, wif);
884
+ try {
885
+ await indexSession({
886
+ txId,
887
+ address: session.address,
888
+ encrypted: session.encrypted,
889
+ session_id: session.session_id,
890
+ prev_session_id: session.prev_session_id,
891
+ summary_enc: summaryEnc,
892
+ message_count: session.message_count,
893
+ save_type: session.type || "full",
894
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
895
+ }, config, wif);
896
+ } catch (idxErr) {
897
+ process.stderr.write(`[MCP] WARNING \u2014 indexSession failed (non-fatal, tx is on chain): ${idxErr.message}
898
+ `);
899
+ }
863
900
  const receipt = buildReceipt(writeReceipt, { txId, fee, txSize });
864
901
  return { success: true, txId, changeUtxos, receipt };
865
902
  }
@@ -1075,10 +1112,78 @@ var init_wall_stub = __esm({
1075
1112
  init_config_customer();
1076
1113
  import { createInterface as createInterface3 } from "node:readline";
1077
1114
  import { execSync as execSync2 } from "node:child_process";
1078
- import { homedir as homedir12 } from "node:os";
1079
- import { join as join13, dirname as dirname4, resolve } from "node:path";
1080
- import { fileURLToPath } from "node:url";
1081
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync14, mkdirSync as mkdirSync6, readdirSync, statSync } from "node:fs";
1115
+ import { homedir as homedir13 } from "node:os";
1116
+ import { join as join14, dirname as dirname5, resolve } from "node:path";
1117
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
1118
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync15, mkdirSync as mkdirSync6, readdirSync, statSync } from "node:fs";
1119
+
1120
+ // mcp-server/lib/guardrails-run.js
1121
+ function contextFromInput(input) {
1122
+ const tool = input?.tool_name;
1123
+ const ti = input?.tool_input || {};
1124
+ return {
1125
+ tool,
1126
+ filePath: ti.file_path || "",
1127
+ // For Write the new content is `content`; for Edit it's `new_string`.
1128
+ added: tool === "Write" ? ti.content || "" : ti.new_string || "",
1129
+ command: ti.command || ""
1130
+ };
1131
+ }
1132
+ function runRules(input, rules) {
1133
+ const ctx = contextFromInput(input);
1134
+ for (const rule of rules) {
1135
+ let reason = null;
1136
+ try {
1137
+ reason = rule.test(ctx);
1138
+ } catch {
1139
+ reason = null;
1140
+ }
1141
+ if (reason) return { id: rule.id, severity: rule.severity, reason };
1142
+ }
1143
+ return null;
1144
+ }
1145
+
1146
+ // mcp-server/lib/guardrails.customer.js
1147
+ var RULES = [
1148
+ {
1149
+ // Reading a private key or seed into the chat sends it to the model and into
1150
+ // the saved transcript. The key controls real funds — this is the one worth
1151
+ // hard-blocking. If the agent needs a non-secret field, it can read just that.
1152
+ id: "credential-pull",
1153
+ severity: "BLOCK",
1154
+ test: ({ command }) => {
1155
+ if (!command) return null;
1156
+ const READ = /\b(cat|less|more|head|tail|type|Get-Content|gc)\b/i;
1157
+ const SECRET = /(config\.json|\.env\b|\bwif\b|\bseed\b|private[-_ ]?key|\.indelible[\\/])/i;
1158
+ return READ.test(command) && SECRET.test(command) ? "That command would print your wallet key or seed into the conversation \u2014 which hands it to the model and writes it into the saved transcript. Your private key controls your funds; never read it into the chat. If you only need a non-secret value, read just that field." : null;
1159
+ }
1160
+ },
1161
+ {
1162
+ // The irreversible-command guard. Advisory, never blocked — your repo, your
1163
+ // machine, your call — but a force-push / recursive delete / hard reset has no
1164
+ // undo, so make the blast radius visible before it runs.
1165
+ id: "destructive-op",
1166
+ severity: "WARN",
1167
+ test: ({ command }) => {
1168
+ if (!command) return null;
1169
+ const hits = [
1170
+ /git\s+push\b[^\n]*(--force\b|--force-with-lease\b|-f\b)/i,
1171
+ // rewrites remote history
1172
+ /git\s+reset\s+--hard\b/i,
1173
+ // discards local work
1174
+ /\brm\s+-[a-z]*r[a-z]*f\b/i,
1175
+ // rm -rf / -fr / -Rf
1176
+ /\brm\s+-[a-z]*f[a-z]*r\b/i,
1177
+ /\bRemove-Item\b[^\n]*-Recurse\b[^\n]*-Force\b/i
1178
+ // PowerShell recursive force-delete
1179
+ ];
1180
+ return hits.some((re) => re.test(command)) ? "This command is irreversible \u2014 a force-push rewrites remote history, and rm -rf / reset --hard / Remove-Item -Recurse -Force cannot be undone. Confirm you have a backup and you are aiming at the right target before you run it." : null;
1181
+ }
1182
+ }
1183
+ ];
1184
+ function evaluate(input) {
1185
+ return runRules(input, RULES);
1186
+ }
1082
1187
 
1083
1188
  // mcp-server/tools/setup_wallet.js
1084
1189
  init_config_customer();
@@ -2922,7 +3027,9 @@ async function _saveSessionInner(transcriptPath, summary, mode) {
2922
3027
  timestamp: session.created_at
2923
3028
  };
2924
3029
  await appendFile(indexPath2, JSON.stringify(entry) + "\n");
2925
- } catch {
3030
+ } catch (e) {
3031
+ process.stderr.write(`[save_session] WARNING \u2014 session-index append failed; wrap NOT cached locally (on-chain copy is primary): ${e?.message || e}
3032
+ `);
2926
3033
  }
2927
3034
  let memoryTxId = config.memory_file_txid || null;
2928
3035
  let historyTxId = config.session_history_txid || null;
@@ -3194,20 +3301,25 @@ async function saveStyle(rulesText, styleName, description) {
3194
3301
  return { success: false, error: "No UTXOs available. Fund your wallet first." };
3195
3302
  }
3196
3303
  const { txHex, txId } = await buildOpReturnTx(wif, utxos, JSON.stringify(payload));
3197
- await broadcastTx(txHex);
3304
+ const writeReceipt = await broadcastTx(txHex);
3198
3305
  await saveConfig({
3199
3306
  ...config,
3200
3307
  style_txid: txId,
3201
3308
  style_name: styleName,
3202
3309
  style_id: styleId
3203
3310
  });
3311
+ const receipt = buildReceipt(writeReceipt, { txId, fee: null, txSize: txHex.length / 2 });
3312
+ appendReceipt(receipt, { trigger: "interactive", saveType: "style" });
3204
3313
  return {
3205
3314
  success: true,
3206
3315
  txId,
3207
3316
  styleId,
3208
3317
  styleName,
3209
3318
  rulesLength: rulesText.length,
3210
- message: `Style "${styleName}" saved to blockchain. txId: ${txId}`
3319
+ receipt,
3320
+ message: `Style "${styleName}" saved to blockchain.
3321
+
3322
+ ${formatSaveReceipt(receipt)}`
3211
3323
  };
3212
3324
  } catch (error) {
3213
3325
  return { success: false, error: `Failed to broadcast style: ${error.message}` };
@@ -5073,12 +5185,17 @@ async function updateVaultIndex() {
5073
5185
  const result = await broadcastTx(txHex);
5074
5186
  const indexTxId = result.txid || txId;
5075
5187
  await saveConfig({ ...config, vault_index_txid: indexTxId });
5188
+ const receipt = buildReceipt(result, { txId: indexTxId, fee: null, txSize: txHex.length / 2 });
5189
+ appendReceipt(receipt, { trigger: "interactive", saveType: "vault-index" });
5076
5190
  return {
5077
5191
  success: true,
5078
5192
  txId: indexTxId,
5079
5193
  files: files.length,
5080
5194
  projects: projects.length,
5081
- message: `Vault index updated: ${files.length} files, ${projects.length} projects. txId: ${indexTxId}`
5195
+ receipt,
5196
+ message: `Vault index updated: ${files.length} files, ${projects.length} projects.
5197
+
5198
+ ${formatSaveReceipt(receipt)}`
5082
5199
  };
5083
5200
  } catch (error) {
5084
5201
  return { success: false, error: `Failed to update vault index: ${error.message}` };
@@ -5523,6 +5640,13 @@ async function saveGoalsToChain() {
5523
5640
  content: [{ type: "text", text: `Failed to parse ${GOALS_PATH2}: ${err.message}` }]
5524
5641
  };
5525
5642
  }
5643
+ const contentHash = crypto.createHash("sha256").update(JSON.stringify(goalsData)).digest("hex");
5644
+ const config = await loadConfig();
5645
+ if (config.goals_snapshot_hash === contentHash) {
5646
+ return {
5647
+ content: [{ type: "text", text: `Goals unchanged since the last snapshot${config.goals_snapshot_txid ? ` (txid ${config.goals_snapshot_txid})` : ""} \u2014 skipped broadcast, no cost.` }]
5648
+ };
5649
+ }
5526
5650
  const wif = await getWif();
5527
5651
  if (!wif) {
5528
5652
  return {
@@ -5559,19 +5683,21 @@ async function saveGoalsToChain() {
5559
5683
  utxos,
5560
5684
  JSON.stringify(payload)
5561
5685
  );
5562
- await broadcastTx(txHex);
5686
+ const writeReceipt = await broadcastTx(txHex);
5687
+ const receipt = buildReceipt(writeReceipt, { txId, fee, txSize });
5688
+ appendReceipt(receipt, { trigger: "interactive", saveType: "goals" });
5689
+ await saveConfig({ ...config, goals_snapshot_hash: contentHash, goals_snapshot_txid: txId });
5563
5690
  return {
5564
5691
  content: [{
5565
5692
  type: "text",
5566
5693
  text: `\u2713 Goals snapshot broadcast.
5567
5694
 
5568
- txid: ${txId}
5569
5695
  address: ${address}
5570
5696
  goals: ${goals.length} active, ${completed.length} completed
5571
- fee: ${fee} sats (${txSize} bytes)
5572
5697
 
5573
- Web Goals tab should now reflect these on next refresh (chain scan).
5574
- https://whatsonchain.com/tx/${txId}`
5698
+ ${formatSaveReceipt(receipt)}
5699
+
5700
+ Web Goals tab should reflect these on next refresh (chain scan).`
5575
5701
  }]
5576
5702
  };
5577
5703
  }
@@ -5876,6 +6002,116 @@ async function recallContext({ from_date = null, to_date = null, query = null, d
5876
6002
  };
5877
6003
  }
5878
6004
 
6005
+ // mcp-server/tools/report_bug.js
6006
+ import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
6007
+ import { join as join13, dirname as dirname4 } from "path";
6008
+ import { homedir as homedir12 } from "os";
6009
+ import { fileURLToPath } from "url";
6010
+ import os from "os";
6011
+ var INTAKE_URL = process.env.INDELIBLE_BUG_INTAKE || "https://indelible.one/api/bug-report";
6012
+ var INDELIBLE_DIR = join13(homedir12(), ".indelible");
6013
+ function getMcpVersion(injected) {
6014
+ if (injected) return injected;
6015
+ const candidates = [];
6016
+ try {
6017
+ candidates.push(join13(dirname4(fileURLToPath(import.meta.url)), "..", "..", "package.json"));
6018
+ } catch {
6019
+ }
6020
+ try {
6021
+ candidates.push(join13(dirname4(fileURLToPath(import.meta.url)), "..", "package.json"));
6022
+ } catch {
6023
+ }
6024
+ candidates.push(join13(homedir12(), "AppData", "Roaming", "npm", "node_modules", "indelible-mcp", "package.json"));
6025
+ candidates.push("/usr/local/lib/node_modules/indelible-mcp/package.json");
6026
+ candidates.push("/usr/lib/node_modules/indelible-mcp/package.json");
6027
+ for (const p of candidates) {
6028
+ try {
6029
+ if (!existsSync14(p)) continue;
6030
+ const pkg = JSON.parse(readFileSync10(p, "utf8"));
6031
+ if (pkg && pkg.name === "indelible-mcp" && pkg.version) return pkg.version;
6032
+ } catch {
6033
+ }
6034
+ }
6035
+ return "unknown";
6036
+ }
6037
+ function readJson(path) {
6038
+ try {
6039
+ return existsSync14(path) ? JSON.parse(readFileSync10(path, "utf8")) : null;
6040
+ } catch {
6041
+ return null;
6042
+ }
6043
+ }
6044
+ function getWalletAddress(config) {
6045
+ return config?.address || config?.wallet_address || config?.wallet?.address || null;
6046
+ }
6047
+ function getRecentReceipts(n = 5) {
6048
+ try {
6049
+ const p = join13(INDELIBLE_DIR, "save-log.jsonl");
6050
+ if (!existsSync14(p)) return [];
6051
+ const lines = readFileSync10(p, "utf8").trim().split("\n").filter(Boolean).slice(-n);
6052
+ return lines.map((l) => {
6053
+ try {
6054
+ const e = JSON.parse(l);
6055
+ return {
6056
+ txid: e.txid || e.txId || null,
6057
+ source: e.source || null,
6058
+ bridge: e.bridge || null,
6059
+ confirmed: e.confirmed ?? null,
6060
+ error: e.error || null,
6061
+ ts: e.ts || e.timestamp || null
6062
+ };
6063
+ } catch {
6064
+ return null;
6065
+ }
6066
+ }).filter(Boolean);
6067
+ } catch {
6068
+ return [];
6069
+ }
6070
+ }
6071
+ function redactSecrets2(s) {
6072
+ if (s == null) return s;
6073
+ return String(s).replace(/\b[KL5][1-9A-HJ-NP-Za-km-z]{50,51}\b/g, "[REDACTED-WIF]");
6074
+ }
6075
+ async function reportBug(args2 = {}, mcpVersion) {
6076
+ const { summary, description, severity = "medium", tool_name = null, last_error = null } = args2;
6077
+ if (!summary || !description) {
6078
+ return { success: false, error: "summary and description are required (you supply the narrative; the tool supplies the facts)." };
6079
+ }
6080
+ const config = readJson(join13(INDELIBLE_DIR, "config.json")) || {};
6081
+ const payload = {
6082
+ // AI-supplied narrative (defensively redacted + length-capped)
6083
+ summary: redactSecrets2(summary).slice(0, 200),
6084
+ description: redactSecrets2(description).slice(0, 5e3),
6085
+ severity: ["low", "medium", "high"].includes(severity) ? severity : "medium",
6086
+ tool_name: tool_name ? String(tool_name).slice(0, 80) : null,
6087
+ last_error: redactSecrets2(last_error),
6088
+ // Machine-pulled facts (the AI cannot get these wrong)
6089
+ mcp_version: getMcpVersion(mcpVersion),
6090
+ wallet_address: getWalletAddress(config),
6091
+ recent_receipts: getRecentReceipts(5),
6092
+ environment: { os: `${os.platform()} ${os.release()}`, arch: os.arch(), node: process.version },
6093
+ reported_at: (/* @__PURE__ */ new Date()).toISOString(),
6094
+ client: "indelible-mcp"
6095
+ };
6096
+ try {
6097
+ const resp = await fetch(INTAKE_URL, {
6098
+ method: "POST",
6099
+ headers: { "Content-Type": "application/json" },
6100
+ body: JSON.stringify(payload)
6101
+ });
6102
+ if (!resp.ok) {
6103
+ return { success: false, error: `intake responded ${resp.status}`, sent: { summary: payload.summary, mcp_version: payload.mcp_version } };
6104
+ }
6105
+ const data = await resp.json().catch(() => ({}));
6106
+ if (data.known_issue) {
6107
+ return { success: true, status: "known_issue", issue_id: data.issue_id || null, message: data.message || "Matched a known, already-fixed issue." };
6108
+ }
6109
+ return { success: true, status: "filed", ticket_id: data.ticket_id || null, message: data.message || "Bug report sent to the Indelible team." };
6110
+ } catch (err) {
6111
+ return { success: false, error: `could not reach intake (${err.message}). Fallback: save_file the report on-chain and share the txid.` };
6112
+ }
6113
+ }
6114
+
5879
6115
  // mcp-server/tools/share_session.js
5880
6116
  init_config_customer();
5881
6117
  init_spv();
@@ -6059,16 +6295,16 @@ init_wall_stub();
6059
6295
  init_wall_stub();
6060
6296
  init_wall_stub();
6061
6297
  init_wall_stub();
6062
- var PROJECT_ROOT_FOR_CLONE_CHECK = resolve(dirname4(fileURLToPath(import.meta.url)), "../..");
6063
- var CONTEXT_FILE2 = join13(homedir12(), ".indelible", "indelible-context.jsonl");
6298
+ var PROJECT_ROOT_FOR_CLONE_CHECK = resolve(dirname5(fileURLToPath2(import.meta.url)), "../..");
6299
+ var CONTEXT_FILE2 = join14(homedir13(), ".indelible", "indelible-context.jsonl");
6064
6300
  function installHooks() {
6065
- const claudeDir = join13(homedir12(), ".claude");
6066
- const settingsPath = join13(claudeDir, "settings.local.json");
6067
- if (!existsSync14(claudeDir)) mkdirSync6(claudeDir, { recursive: true });
6301
+ const claudeDir = join14(homedir13(), ".claude");
6302
+ const settingsPath = join14(claudeDir, "settings.local.json");
6303
+ if (!existsSync15(claudeDir)) mkdirSync6(claudeDir, { recursive: true });
6068
6304
  let settings = {};
6069
- if (existsSync14(settingsPath)) {
6305
+ if (existsSync15(settingsPath)) {
6070
6306
  try {
6071
- settings = JSON.parse(readFileSync10(settingsPath, "utf8"));
6307
+ settings = JSON.parse(readFileSync11(settingsPath, "utf8"));
6072
6308
  } catch {
6073
6309
  settings = {};
6074
6310
  }
@@ -6083,6 +6319,9 @@ function installHooks() {
6083
6319
  const hasSessionStart = settings.hooks.SessionStart?.some(
6084
6320
  (h) => h.hooks?.some((hh) => hh.command?.includes("indelible-mcp hook session-start")) || h.command?.includes("indelible-mcp hook session-start")
6085
6321
  );
6322
+ const hasPreToolUse = settings.hooks.PreToolUse?.some(
6323
+ (h) => h.hooks?.some((hh) => hh.command?.includes("indelible-mcp hook pre-tool-use")) || h.command?.includes("indelible-mcp hook pre-tool-use")
6324
+ );
6086
6325
  const installed = [];
6087
6326
  if (!hasPreCompact) {
6088
6327
  if (!settings.hooks.PreCompact) settings.hooks.PreCompact = [];
@@ -6108,9 +6347,38 @@ function installHooks() {
6108
6347
  });
6109
6348
  installed.push("SessionStart:style");
6110
6349
  }
6350
+ if (!hasPreToolUse) {
6351
+ if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
6352
+ settings.hooks.PreToolUse.push({
6353
+ matcher: "Bash",
6354
+ hooks: [{ type: "command", command: "indelible-mcp hook pre-tool-use" }]
6355
+ });
6356
+ installed.push("PreToolUse:guardrails");
6357
+ }
6111
6358
  writeFileSync7(settingsPath, JSON.stringify(settings, null, 2));
6112
6359
  return { settingsPath, installed, alreadyInstalled: installed.length === 0 };
6113
6360
  }
6361
+ function runPreToolUseGuard() {
6362
+ let code = 0;
6363
+ try {
6364
+ const raw = readFileSync11(0, "utf8").trim();
6365
+ if (!raw) process.exit(0);
6366
+ const hit = evaluate(JSON.parse(raw));
6367
+ if (hit) {
6368
+ if (hit.severity === "BLOCK") {
6369
+ process.stderr.write(`[indelible:${hit.id}] BLOCKED: ${hit.reason}
6370
+ `);
6371
+ code = 2;
6372
+ } else {
6373
+ process.stdout.write(`[indelible:${hit.id}] advisory: ${hit.reason} (not blocked)
6374
+ `);
6375
+ }
6376
+ }
6377
+ } catch {
6378
+ code = 0;
6379
+ }
6380
+ process.exit(code);
6381
+ }
6114
6382
  async function runCli(args2) {
6115
6383
  const command = args2[0];
6116
6384
  switch (command) {
@@ -6298,7 +6566,7 @@ Commands:
6298
6566
  }
6299
6567
  function printHelp() {
6300
6568
  console.log(`
6301
- Indelible MCP \u2014 Blockchain memory for Claude Code (v4.5.0)
6569
+ Indelible MCP \u2014 Blockchain memory for Claude Code (v4.6.0)
6302
6570
 
6303
6571
  Setup:
6304
6572
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -6338,6 +6606,7 @@ Payments:
6338
6606
  Hooks (auto-called by Claude Code):
6339
6607
  indelible-mcp hook pre-compact Auto-save before compaction
6340
6608
  indelible-mcp hook post-compact Auto-restore after compaction
6609
+ indelible-mcp hook pre-tool-use Guardrails: block a key leak / flag an irreversible command
6341
6610
 
6342
6611
  Get your key: Sign in at indelible.one \u2192 Settings \u2192 Private Key
6343
6612
  Learn more: https://indelible.one
@@ -6372,20 +6641,20 @@ async function runPreCompactSave() {
6372
6641
  process.exit(0);
6373
6642
  }
6374
6643
  function findMemoryDir() {
6375
- const projectsDir = join13(homedir12(), ".claude", "projects");
6376
- if (!existsSync14(projectsDir)) return null;
6644
+ const projectsDir = join14(homedir13(), ".claude", "projects");
6645
+ if (!existsSync15(projectsDir)) return null;
6377
6646
  let newestTime = 0;
6378
6647
  let newestProject = null;
6379
6648
  const projects = readdirSync(projectsDir);
6380
6649
  for (const project of projects) {
6381
- const projectPath = join13(projectsDir, project);
6650
+ const projectPath = join14(projectsDir, project);
6382
6651
  try {
6383
6652
  const pStat = statSync(projectPath);
6384
6653
  if (!pStat.isDirectory()) continue;
6385
6654
  const files = readdirSync(projectPath);
6386
6655
  for (const file of files) {
6387
6656
  if (!file.endsWith(".jsonl")) continue;
6388
- const fStat = statSync(join13(projectPath, file));
6657
+ const fStat = statSync(join14(projectPath, file));
6389
6658
  if (fStat.mtimeMs > newestTime) {
6390
6659
  newestTime = fStat.mtimeMs;
6391
6660
  newestProject = projectPath;
@@ -6394,7 +6663,7 @@ function findMemoryDir() {
6394
6663
  } catch {
6395
6664
  }
6396
6665
  }
6397
- return newestProject ? join13(newestProject, "memory") : null;
6666
+ return newestProject ? join14(newestProject, "memory") : null;
6398
6667
  }
6399
6668
  async function runPostCompactRestore() {
6400
6669
  const config = await loadConfig();
@@ -6443,10 +6712,10 @@ async function runPostCompactRestore() {
6443
6712
  try {
6444
6713
  const memoryDir = findMemoryDir();
6445
6714
  if (memoryDir && (config.memory_file_txid || config.session_history_txid)) {
6446
- if (!existsSync14(memoryDir)) mkdirSync6(memoryDir, { recursive: true });
6715
+ if (!existsSync15(memoryDir)) mkdirSync6(memoryDir, { recursive: true });
6447
6716
  if (config.memory_file_txid) {
6448
- const memPath = join13(memoryDir, "MEMORY.md");
6449
- if (!existsSync14(memPath)) {
6717
+ const memPath = join14(memoryDir, "MEMORY.md");
6718
+ if (!existsSync15(memPath)) {
6450
6719
  const memResult = await loadFile(config.memory_file_txid, { outputPath: memPath });
6451
6720
  if (memResult.success) {
6452
6721
  process.stderr.write(`Indelible: MEMORY.md restored from chain (file was missing)
@@ -6455,8 +6724,8 @@ async function runPostCompactRestore() {
6455
6724
  }
6456
6725
  }
6457
6726
  if (config.session_history_txid) {
6458
- const histPath = join13(memoryDir, "session-history.md");
6459
- if (!existsSync14(histPath)) {
6727
+ const histPath = join14(memoryDir, "session-history.md");
6728
+ if (!existsSync15(histPath)) {
6460
6729
  const histResult = await loadFile(config.session_history_txid, { outputPath: histPath });
6461
6730
  if (histResult.success) {
6462
6731
  process.stderr.write(`Indelible: session-history.md restored from chain (file was missing)
@@ -6472,9 +6741,9 @@ async function runPostCompactRestore() {
6472
6741
  try {
6473
6742
  const histDir = findMemoryDir();
6474
6743
  if (histDir) {
6475
- const histPath = join13(histDir, "session-history.md");
6476
- if (existsSync14(histPath)) {
6477
- const histContent = readFileSync10(histPath, "utf-8");
6744
+ const histPath = join14(histDir, "session-history.md");
6745
+ if (existsSync15(histPath)) {
6746
+ const histContent = readFileSync11(histPath, "utf-8");
6478
6747
  const lines = histContent.split("\n");
6479
6748
  const entryStarts = [];
6480
6749
  for (let i = 0; i < lines.length; i++) {
@@ -6516,7 +6785,7 @@ function readStdin() {
6516
6785
  }
6517
6786
  var SERVER_INFO = {
6518
6787
  name: "indelible",
6519
- version: "4.5.0",
6788
+ version: "4.6.0",
6520
6789
  description: "Blockchain-backed memory and code storage for Claude Code"
6521
6790
  };
6522
6791
  var TOOLS = [
@@ -6800,6 +7069,21 @@ var TOOLS = [
6800
7069
  required: []
6801
7070
  }
6802
7071
  },
7072
+ {
7073
+ name: "report_bug",
7074
+ 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).`,
7075
+ inputSchema: {
7076
+ type: "object",
7077
+ properties: {
7078
+ summary: { type: "string", description: "One-line summary of the bug." },
7079
+ description: { type: "string", description: "What you were doing, what went wrong, and exact repro steps." },
7080
+ severity: { type: "string", enum: ["low", "medium", "high"], description: "User-facing impact." },
7081
+ tool_name: { type: "string", description: "Which Indelible tool/command failed (optional)." },
7082
+ last_error: { type: "string", description: "The exact error string you saw (optional)." }
7083
+ },
7084
+ required: ["summary", "description"]
7085
+ }
7086
+ },
6803
7087
  {
6804
7088
  name: "share_session",
6805
7089
  description: "Issue a viewing key for a saved session, granting selective access to a recipient (g-170). Recipient gets an ECIES-wrapped content key bound to their wallet.",
@@ -6920,6 +7204,15 @@ async function handleMcpRequest(request) {
6920
7204
  max_index: args2?.max_index || 150
6921
7205
  });
6922
7206
  break;
7207
+ case "report_bug":
7208
+ result = await reportBug({
7209
+ summary: args2?.summary,
7210
+ description: args2?.description,
7211
+ severity: args2?.severity || "medium",
7212
+ tool_name: args2?.tool_name || null,
7213
+ last_error: args2?.last_error || null
7214
+ });
7215
+ break;
6923
7216
  case "share_session":
6924
7217
  result = await shareSession({
6925
7218
  session_txid: args2?.session_txid,
@@ -6995,11 +7288,11 @@ async function runWizard() {
6995
7288
  }
6996
7289
  let mcpOk = false;
6997
7290
  for (const cfgPath of [
6998
- join13(homedir12(), ".claude.json"),
6999
- join13(homedir12(), ".claude", "settings.json")
7291
+ join14(homedir13(), ".claude.json"),
7292
+ join14(homedir13(), ".claude", "settings.json")
7000
7293
  ]) {
7001
7294
  try {
7002
- const s = JSON.parse(readFileSync10(cfgPath, "utf8"));
7295
+ const s = JSON.parse(readFileSync11(cfgPath, "utf8"));
7003
7296
  if (s?.mcpServers?.indelible) {
7004
7297
  mcpOk = true;
7005
7298
  break;
@@ -7009,10 +7302,10 @@ async function runWizard() {
7009
7302
  }
7010
7303
  console.log(mcpOk ? " \u2713 MCP registered with Claude Code" : " \u2717 MCP not registered");
7011
7304
  if (!mcpOk) allGood = false;
7012
- const hooksPath = join13(homedir12(), ".claude", "settings.local.json");
7305
+ const hooksPath = join14(homedir13(), ".claude", "settings.local.json");
7013
7306
  let hooksOk = false;
7014
7307
  try {
7015
- const s = JSON.parse(readFileSync10(hooksPath, "utf8"));
7308
+ const s = JSON.parse(readFileSync11(hooksPath, "utf8"));
7016
7309
  hooksOk = s?.hooks?.PreCompact?.some(
7017
7310
  (h) => h.hooks?.some((hh) => hh.command?.includes("indelible-mcp")) || h.command?.includes("indelible-mcp")
7018
7311
  ) && s?.hooks?.SessionStart?.some(
@@ -7246,6 +7539,8 @@ if (args[0] === "hook") {
7246
7539
  }
7247
7540
  process.exit(0);
7248
7541
  })();
7542
+ } else if (args[1] === "pre-tool-use") {
7543
+ runPreToolUseGuard();
7249
7544
  } else {
7250
7545
  console.error("Unknown hook:", args[1]);
7251
7546
  process.exit(1);