indelible-mcp 4.8.0 → 4.8.1

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 +317 -62
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "4.8.0",
3
+ "version": "4.8.1",
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
@@ -314,15 +314,17 @@ async function getRawTx(txid) {
314
314
  `);
315
315
  return text;
316
316
  }
317
+ function bcastTimeoutMs(rawTxHex) {
318
+ const txMB = (rawTxHex?.length || 0) / 2 / 1048576;
319
+ return Math.min(18e4, Math.round(15e3 + txMB * 12e3));
320
+ }
317
321
  async function broadcastViaBridges(rawTxHex) {
318
322
  const bridges = await getBridges();
319
- process.stderr.write(`[MCP] Bridge fallback \u2014 broadcasting to ${bridges.length} bridges (parallel)...
320
- `);
321
323
  const t0 = Date.now();
322
- const attempts = bridges.map(async (bridge) => {
324
+ const tryBridge = async (bridge) => {
323
325
  const bt0 = Date.now();
324
326
  const controller = new AbortController();
325
- const timeout = setTimeout(() => controller.abort(), 1e4);
327
+ const timeout = setTimeout(() => controller.abort(), bcastTimeoutMs(rawTxHex));
326
328
  try {
327
329
  const res = await fetch(`${bridge.url}/api/broadcast`, {
328
330
  method: "POST",
@@ -349,16 +351,34 @@ async function broadcastViaBridges(rawTxHex) {
349
351
  `);
350
352
  throw err;
351
353
  }
352
- });
353
- try {
354
- const result = await Promise.any(attempts);
355
- process.stderr.write(`[MCP] Bridge broadcast accepted in ${Date.now() - t0}ms
354
+ };
355
+ let ordered = bridges;
356
+ if (bridges.length > 1) {
357
+ try {
358
+ const winner = await Promise.any(bridges.map(async (b) => {
359
+ const r = await fetch(`${b.url}/health`, { signal: AbortSignal.timeout(5e3) });
360
+ if (!r.ok) throw new Error("unhealthy");
361
+ return b;
362
+ }));
363
+ ordered = [winner, ...bridges.filter((b) => b.url !== winner.url)];
364
+ process.stderr.write(`[MCP] Bridge broadcast \u2014 ${winner.name} answered first; sending the tx to it (single bridge; mesh gossips the rest)
365
+ `);
366
+ } catch {
367
+ }
368
+ }
369
+ let lastErr = null;
370
+ for (const bridge of ordered) {
371
+ try {
372
+ const r = await tryBridge(bridge);
373
+ setStickyBridge(bridge);
374
+ process.stderr.write(`[MCP] Bridge broadcast accepted via ${bridge.name} in ${Date.now() - t0}ms
356
375
  `);
357
- return result;
358
- } catch (agg) {
359
- const msgs = agg.errors ? agg.errors.map((e) => e.message).join(" | ") : agg.message;
360
- throw new Error(`Bridge broadcast failed \u2014 all ${bridges.length} bridges down: ${msgs}`);
376
+ return r;
377
+ } catch (e) {
378
+ lastErr = e;
379
+ }
361
380
  }
381
+ throw new Error(`Bridge broadcast failed \u2014 all ${bridges.length} bridges down: ${lastErr && lastErr.message}`);
362
382
  }
363
383
  function setStickyBridge(bridge) {
364
384
  _stickyBridge = { url: bridge.url, name: bridge.name, until: Date.now() + STICKY_WINDOW_MS };
@@ -438,8 +458,8 @@ async function buildOpReturnTxWithChange(wif, utxos, dataStr) {
438
458
  });
439
459
  totalInput += utxo.value;
440
460
  }
441
- const prefixBytes = Array.from(Buffer.from("BSVWRAPPER_MERKLE_E", "utf8"));
442
- const dataBytes = Array.from(Buffer.from(dataStr, "utf8"));
461
+ const prefixBytes = Buffer.from("BSVWRAPPER_MERKLE_E", "utf8");
462
+ const dataBytes = Buffer.from(dataStr, "utf8");
443
463
  const opReturnScript = new LockingScript([
444
464
  { op: OP.OP_FALSE },
445
465
  { op: OP.OP_RETURN },
@@ -454,7 +474,7 @@ async function buildOpReturnTxWithChange(wif, utxos, dataStr) {
454
474
  lockingScript: p2pkh.lock(address),
455
475
  change: true
456
476
  });
457
- await tx.fee(new SatoshisPerKilobyte(1e3));
477
+ await tx.fee(new SatoshisPerKilobyte(150));
458
478
  await tx.sign();
459
479
  const txId = tx.id("hex");
460
480
  const txHex = tx.toHex();
@@ -504,7 +524,7 @@ async function buildPaymentTx(wif, utxos, payToAddress, satoshis) {
504
524
  lockingScript: p2pkh.lock(address),
505
525
  change: true
506
526
  });
507
- await tx.fee(new SatoshisPerKilobyte(1e3));
527
+ await tx.fee(new SatoshisPerKilobyte(150));
508
528
  await tx.sign();
509
529
  const txId = tx.id("hex");
510
530
  const txHex = tx.toHex();
@@ -1561,6 +1581,11 @@ function verifyShare(envelope, ownerPubkey) {
1561
1581
  }
1562
1582
  }
1563
1583
  function detectSaveFormat(payload) {
1584
+ const hasV3Fields = payload != null && (payload.wrap_self !== void 0 || payload.wrap_version !== void 0);
1585
+ if (hasV3Fields) {
1586
+ if (payload.wrap_version === 3 && typeof payload.wrap_self === "string" && payload.wrap_self.length > 0) return "v3-wallet";
1587
+ return "v3-malformed";
1588
+ }
1564
1589
  if (payload && payload.version === 2) return "v2-brc100";
1565
1590
  if (payload && typeof payload.wrap_owner === "string" && payload.wrap_owner.length > 0) return "v2-wrapped";
1566
1591
  return "legacy";
@@ -1579,6 +1604,49 @@ function unwrapForOwner(wrapStr, ownerPrivkey) {
1579
1604
  const aad = buildAAD(ownerAddr, ownerAddr, SELF_WRAP_TXID);
1580
1605
  return eciesUnwrap(wrapStr, ownerPrivkey, aad);
1581
1606
  }
1607
+ function isValidHex(s) {
1608
+ return typeof s === "string" && s.length > 0 && s.length % 2 === 0 && /^[0-9a-f]+$/i.test(s);
1609
+ }
1610
+ async function unwrapForSelfV3(wrapHex, wallet, keyID) {
1611
+ if (!wallet) return { ok: false, reason: "wallet_unavailable" };
1612
+ if (!isValidHex(wrapHex)) return { ok: false, reason: "corrupt" };
1613
+ if (typeof keyID !== "string" || keyID.length === 0) return { ok: false, reason: "corrupt" };
1614
+ try {
1615
+ const { plaintext } = await wallet.decrypt({
1616
+ protocolID: ENCRYPT_PROTOCOL_ID_V2,
1617
+ keyID,
1618
+ counterparty: "self",
1619
+ ciphertext: Array.from(Buffer.from(wrapHex, "hex"))
1620
+ });
1621
+ const contentKey = Buffer.from(plaintext);
1622
+ if (contentKey.length !== 32) return { ok: false, reason: "corrupt" };
1623
+ return { ok: true, contentKey };
1624
+ } catch {
1625
+ return { ok: false, reason: "corrupt" };
1626
+ }
1627
+ }
1628
+ async function unwrapRecoveryV3(wrapHex, wallet, keyID, ownerPubkeyHex) {
1629
+ if (!wallet) return { ok: false, reason: "wallet_unavailable" };
1630
+ if (!isValidHex(wrapHex)) return { ok: false, reason: "corrupt" };
1631
+ if (typeof keyID !== "string" || keyID.length === 0) return { ok: false, reason: "corrupt" };
1632
+ if (typeof ownerPubkeyHex !== "string" || !/^[0-9a-f]{66}$/i.test(ownerPubkeyHex)) {
1633
+ return { ok: false, reason: "corrupt" };
1634
+ }
1635
+ try {
1636
+ const { plaintext } = await wallet.decrypt({
1637
+ protocolID: ENCRYPT_PROTOCOL_ID_V2,
1638
+ keyID,
1639
+ counterparty: ownerPubkeyHex,
1640
+ ciphertext: Array.from(Buffer.from(wrapHex, "hex"))
1641
+ });
1642
+ const contentKey = Buffer.from(plaintext);
1643
+ if (contentKey.length !== 32) return { ok: false, reason: "corrupt" };
1644
+ return { ok: true, contentKey };
1645
+ } catch {
1646
+ return { ok: false, reason: "corrupt" };
1647
+ }
1648
+ }
1649
+ var ENCRYPT_PROTOCOL_ID_V2 = [2, "indelible share"];
1582
1650
 
1583
1651
  // mcp-server/tools/save_session.js
1584
1652
  init_api_client();
@@ -1668,6 +1736,36 @@ async function pollInfraEmotions() {
1668
1736
  }
1669
1737
 
1670
1738
  // mcp-server/lib/redact.js
1739
+ import { createHash as createHash3 } from "node:crypto";
1740
+ var B58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
1741
+ function base58Decode(str) {
1742
+ const bytes = [0];
1743
+ for (let i = 0; i < str.length; i++) {
1744
+ const c = B58_ALPHABET.indexOf(str[i]);
1745
+ if (c < 0) return null;
1746
+ let carry = c;
1747
+ for (let j = 0; j < bytes.length; j++) {
1748
+ carry += bytes[j] * 58;
1749
+ bytes[j] = carry & 255;
1750
+ carry >>= 8;
1751
+ }
1752
+ while (carry > 0) {
1753
+ bytes.push(carry & 255);
1754
+ carry >>= 8;
1755
+ }
1756
+ }
1757
+ for (let i = 0; i < str.length && str[i] === "1"; i++) bytes.push(0);
1758
+ return Buffer.from(bytes.reverse());
1759
+ }
1760
+ function isValidWif(candidate) {
1761
+ const decoded = base58Decode(candidate);
1762
+ if (!decoded || decoded.length < 5) return false;
1763
+ const payload = decoded.subarray(0, decoded.length - 4);
1764
+ const checksum = decoded.subarray(decoded.length - 4);
1765
+ const h = createHash3("sha256").update(createHash3("sha256").update(payload).digest()).digest();
1766
+ return h.subarray(0, 4).equals(checksum);
1767
+ }
1768
+ var MAX_VALIDATED_CANDIDATES = 2e4;
1671
1769
  var CREDENTIAL_PATTERNS = [
1672
1770
  { name: "anthropic_key", pattern: /\bsk-ant-api03-[A-Za-z0-9_\-]{80,}/g },
1673
1771
  // Catches legacy `sk-XXXX...` (51 chars) and modern `sk-proj-XXXX...` (longer,
@@ -1675,8 +1773,8 @@ var CREDENTIAL_PATTERNS = [
1675
1773
  { name: "openai_key", pattern: /\bsk-(?!ant-)(?:proj-)?[A-Za-z0-9_\-]{40,}/g },
1676
1774
  { name: "stripe_key", pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
1677
1775
  { name: "aws_access_key", pattern: /\bAKIA[A-Z0-9]{16}\b/g },
1678
- { name: "bsv_wif", pattern: /\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\b/g },
1679
- // true base58 alphabet (no 0/O/I/l) — fewer false positives
1776
+ { name: "bsv_wif", pattern: /\b[5KL][1-9A-HJ-NP-Za-km-z]{50,51}\b/g, validate: isValidWif },
1777
+ // base58 shape + Base58Check checksum (no base64 false positives)
1680
1778
  { name: "telegram_token", pattern: /\b\d{8,12}:[A-Za-z0-9_-]{30,40}\b/g },
1681
1779
  { name: "github_pat", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
1682
1780
  { name: "github_pat_v2", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
@@ -1685,10 +1783,17 @@ var CREDENTIAL_PATTERNS = [
1685
1783
  function scanContentForCredentials(text) {
1686
1784
  if (typeof text !== "string" || text.length === 0) return [];
1687
1785
  const found = [];
1688
- for (const { name, pattern } of CREDENTIAL_PATTERNS) {
1786
+ for (const { name, pattern, validate } of CREDENTIAL_PATTERNS) {
1689
1787
  pattern.lastIndex = 0;
1690
1788
  let m;
1789
+ let validated = 0;
1691
1790
  while ((m = pattern.exec(text)) !== null) {
1791
+ if (validate) {
1792
+ if (validated < MAX_VALIDATED_CANDIDATES) {
1793
+ validated++;
1794
+ if (!validate(m[0])) continue;
1795
+ }
1796
+ }
1692
1797
  found.push({
1693
1798
  pattern_name: name,
1694
1799
  preview: m[0].slice(0, 12) + "..."
@@ -2060,7 +2165,7 @@ import { fileURLToPath as _fileURLToPath } from "url";
2060
2165
  var PROJECT_ROOT_FOR_SAVE_FILE_CLONE_CHECK = _pathResolve(_dirname(_fileURLToPath(import.meta.url)), "..");
2061
2166
  var CODE_EXTS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"]);
2062
2167
  var CLONE_CHECK_TIMEOUT_MS = 5e3;
2063
- var MAX_CHUNK_SIZE = 5e4;
2168
+ var MAX_CHUNK_SIZE = 1e7;
2064
2169
  var TX_CACHE_DIR = join4(homedir4(), ".indelible", "tx-cache");
2065
2170
  async function cacheTx(txId, payload) {
2066
2171
  try {
@@ -2080,9 +2185,13 @@ async function saveFile(filePath, options = {}) {
2080
2185
  if (!existsSync4(filePath)) {
2081
2186
  return { success: false, error: `File not found: ${filePath}` };
2082
2187
  }
2083
- const content = await readFile(filePath, "utf-8");
2084
- const contentHash = sha256(content);
2085
- const fileSize = Buffer.byteLength(content, "utf-8");
2188
+ const buf = await readFile(filePath);
2189
+ const asText = buf.toString("utf-8");
2190
+ const isText = Buffer.from(asText, "utf-8").equals(buf);
2191
+ const fileEnc = isText ? "utf8" : "b64";
2192
+ const content = isText ? asText : "";
2193
+ const contentHash = sha256(buf);
2194
+ const fileSize = buf.length;
2086
2195
  const filename = basename(filePath);
2087
2196
  const relativePath = options.relativePath || filePath;
2088
2197
  const fileVerdict = reviewFile({ filename, relativePath, contentHash, fileSize, content });
@@ -2157,7 +2266,7 @@ async function saveFile(filePath, options = {}) {
2157
2266
  `);
2158
2267
  }
2159
2268
  }
2160
- const compressed = gzipSync(Buffer.from(content));
2269
+ const compressed = gzipSync(buf);
2161
2270
  const encrypted = encrypt("gz:" + compressed.toString("base64"), wif);
2162
2271
  let utxos = options.utxos || await getSpendableUtxos(config.address, getUtxos);
2163
2272
  if (!utxos || utxos.length === 0) {
@@ -2180,6 +2289,7 @@ async function saveFile(filePath, options = {}) {
2180
2289
  path_enc: pathEnc,
2181
2290
  size: fileSize,
2182
2291
  content_hash: `sha256:${contentHash}`,
2292
+ enc: fileEnc,
2183
2293
  encrypted,
2184
2294
  owner: config.address,
2185
2295
  timestamp
@@ -2236,6 +2346,7 @@ async function saveFile(filePath, options = {}) {
2236
2346
  path_enc: pathEnc,
2237
2347
  size: fileSize,
2238
2348
  content_hash: `sha256:${contentHash}`,
2349
+ enc: fileEnc,
2239
2350
  _chunks: chunks.length,
2240
2351
  owner: config.address,
2241
2352
  timestamp
@@ -2868,6 +2979,13 @@ async function _saveSessionInner(transcriptPath, summary, mode) {
2868
2979
  const config = await loadConfig();
2869
2980
  const wif = await getWif();
2870
2981
  if (!wif) {
2982
+ if (config?.external_identity) {
2983
+ return {
2984
+ success: false,
2985
+ wallet_native: true,
2986
+ error: "Wallet-native account: this box holds no signing key (it lives in your external wallet). Save from the web app, or link a recovery key to enable box saves."
2987
+ };
2988
+ }
2871
2989
  return {
2872
2990
  success: false,
2873
2991
  error: "Wallet not configured or PIN incorrect. Run setup_wallet first."
@@ -3305,7 +3423,7 @@ import { join as join9 } from "path";
3305
3423
  import { homedir as homedir9 } from "os";
3306
3424
  init_api_client();
3307
3425
  init_spv();
3308
- import { PrivateKey as PrivateKey7 } from "@bsv/sdk";
3426
+ import { PrivateKey as PrivateKey7, CompletedProtoWallet as CompletedProtoWallet2 } from "@bsv/sdk";
3309
3427
  import { Transaction as Transaction5 } from "@bsv/sdk";
3310
3428
 
3311
3429
  // mcp-server/tools/load_style.js
@@ -4397,22 +4515,80 @@ async function updateInnerState({ observed, source, preoccupations, open_loops,
4397
4515
 
4398
4516
  // mcp-server/tools/load_context.js
4399
4517
  import { readFileSync as readFileSync7 } from "fs";
4400
- function decryptSession(payloadOrEncrypted, wif) {
4401
- let encrypted, wrap_owner;
4518
+ async function decryptSession(payloadOrEncrypted, wif, wallet = null) {
4519
+ let payload;
4402
4520
  if (typeof payloadOrEncrypted === "string") {
4403
- encrypted = payloadOrEncrypted;
4404
- wrap_owner = null;
4521
+ payload = { encrypted: payloadOrEncrypted, wrap_owner: null };
4405
4522
  } else {
4406
- encrypted = payloadOrEncrypted.encrypted;
4407
- wrap_owner = payloadOrEncrypted.wrap_owner;
4523
+ payload = payloadOrEncrypted;
4408
4524
  }
4525
+ const encrypted = payload.encrypted;
4526
+ const wrap_owner = payload.wrap_owner;
4409
4527
  let text;
4410
- const fmt = detectSaveFormat({ wrap_owner });
4411
- if (fmt === "v2-wrapped") {
4412
- const r = unwrapForOwner(wrap_owner, PrivateKey7.fromWif(wif));
4528
+ const fmt = detectSaveFormat({ wrap_owner, wrap_self: payload.wrap_self, wrap_version: payload.wrap_version });
4529
+ if (fmt === "v3-wallet") {
4530
+ let w = wallet;
4531
+ if (!w && wif) {
4532
+ try {
4533
+ w = new CompletedProtoWallet2(PrivateKey7.fromWif(wif));
4534
+ } catch {
4535
+ w = null;
4536
+ }
4537
+ }
4538
+ let r = null;
4539
+ if (payload.owner_pubkey && w) {
4540
+ let idPub = null;
4541
+ try {
4542
+ idPub = (await w.getPublicKey({ identityKey: true }))?.publicKey || null;
4543
+ } catch {
4544
+ }
4545
+ if (idPub && idPub !== payload.owner_pubkey) {
4546
+ if (payload.wrap_recovery) {
4547
+ const rr = await unwrapRecoveryV3(payload.wrap_recovery, w, payload.key_id, payload.owner_pubkey);
4548
+ if (rr.ok) r = rr;
4549
+ }
4550
+ if (!r) r = { ok: false, reason: "wallet_unavailable" };
4551
+ }
4552
+ }
4553
+ if (!r) {
4554
+ r = await unwrapForSelfV3(payload.wrap_self, w, payload.key_id);
4555
+ if (!r.ok && r.reason === "corrupt" && payload.wrap_recovery && payload.owner_pubkey) {
4556
+ r = await unwrapRecoveryV3(payload.wrap_recovery, w, payload.key_id, payload.owner_pubkey);
4557
+ }
4558
+ }
4559
+ if (!r.ok) {
4560
+ const err = new Error(r.reason === "wallet_unavailable" ? "wallet-native (v3) save: no wallet available to unwrap \u2014 connect the wallet that made this save" : `wallet-native (v3) save: unwrap failed (${r.reason})`);
4561
+ err.code = r.reason === "wallet_unavailable" ? "WALLET_UNAVAILABLE" : "V3_CORRUPT";
4562
+ throw err;
4563
+ }
4564
+ text = decryptWithKey(encrypted, r.contentKey);
4565
+ } else if (fmt === "v3-malformed") {
4566
+ const err = new Error("v3 fields present but malformed \u2014 failing closed");
4567
+ err.code = "V3_CORRUPT";
4568
+ throw err;
4569
+ } else if (fmt === "v2-wrapped") {
4570
+ let ownerPriv = null;
4571
+ if (wif) {
4572
+ try {
4573
+ ownerPriv = PrivateKey7.fromWif(wif);
4574
+ } catch {
4575
+ ownerPriv = null;
4576
+ }
4577
+ }
4578
+ if (!ownerPriv) {
4579
+ const err = new Error("v2-wrapped save: no valid key available to unwrap (wif missing or invalid)");
4580
+ err.code = "WALLET_UNAVAILABLE";
4581
+ throw err;
4582
+ }
4583
+ const r = unwrapForOwner(wrap_owner, ownerPriv);
4413
4584
  if (!r.ok) throw new Error(`wrap_owner unwrap failed: ${r.reason}`);
4414
4585
  text = decryptWithKey(encrypted, r.contentKey);
4415
4586
  } else {
4587
+ if (!wif) {
4588
+ const err = new Error("legacy save: no key available to decrypt (wif missing)");
4589
+ err.code = "WALLET_UNAVAILABLE";
4590
+ throw err;
4591
+ }
4416
4592
  text = decrypt(encrypted, wif);
4417
4593
  }
4418
4594
  if (text.startsWith("gz:")) {
@@ -4462,7 +4638,15 @@ async function fetchEncryptedFromChain(txId) {
4462
4638
  if (end > 0) {
4463
4639
  try {
4464
4640
  const payload = JSON.parse(str.slice(jsonStart, end + 1));
4465
- if (payload.encrypted) return { encrypted: payload.encrypted, wrap_owner: payload.wrap_owner || null };
4641
+ if (payload.encrypted) {
4642
+ const out = { encrypted: payload.encrypted, wrap_owner: payload.wrap_owner || null };
4643
+ if (payload.wrap_version !== void 0) out.wrap_version = payload.wrap_version;
4644
+ if (payload.wrap_self !== void 0) out.wrap_self = payload.wrap_self;
4645
+ if (payload.key_id !== void 0) out.key_id = payload.key_id;
4646
+ if (payload.wrap_recovery !== void 0) out.wrap_recovery = payload.wrap_recovery;
4647
+ if (payload.owner_pubkey !== void 0) out.owner_pubkey = payload.owner_pubkey;
4648
+ return out;
4649
+ }
4466
4650
  } catch {
4467
4651
  }
4468
4652
  }
@@ -4647,14 +4831,19 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4647
4831
  process.stderr.write(`[indelible] Scavenging ${recent.length} sessions from JSONL index (streamed tail)
4648
4832
  `);
4649
4833
  }
4834
+ let v3Locked = 0;
4650
4835
  if (recent.length) {
4651
4836
  for (const entry of recent) {
4652
4837
  try {
4653
4838
  if (entry.encrypted) {
4654
- const data = decryptSession(entry, wif);
4839
+ const data = await decryptSession(entry, wif);
4655
4840
  decrypted.push(data);
4656
4841
  }
4657
- } catch {
4842
+ } catch (e) {
4843
+ if (e?.code === "WALLET_UNAVAILABLE") {
4844
+ v3Locked++;
4845
+ continue;
4846
+ }
4658
4847
  }
4659
4848
  }
4660
4849
  decrypted.reverse();
@@ -4668,7 +4857,7 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4668
4857
  try {
4669
4858
  const enc = await fetchEncryptedFromChain(cursorTx);
4670
4859
  if (!enc) break;
4671
- const data = decryptSession(enc, wif);
4860
+ const data = await decryptSession(enc, wif);
4672
4861
  if (data.type === "diary" || data.type === "duo") {
4673
4862
  cursorTx = data.prev_tx_id;
4674
4863
  continue;
@@ -4676,7 +4865,8 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4676
4865
  decrypted.push(data);
4677
4866
  if (data.type !== "delta") break;
4678
4867
  cursorTx = data.prev_tx_id;
4679
- } catch {
4868
+ } catch (e) {
4869
+ if (e?.code === "WALLET_UNAVAILABLE") v3Locked++;
4680
4870
  break;
4681
4871
  }
4682
4872
  }
@@ -4698,16 +4888,20 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4698
4888
  for (const session of sessions) {
4699
4889
  try {
4700
4890
  if (session.encrypted) {
4701
- const data = decryptSession(session, wif);
4891
+ const data = await decryptSession(session, wif);
4702
4892
  decrypted.push(data);
4703
4893
  } else if (session.txId) {
4704
4894
  const fromChain = await fetchEncryptedFromChain(session.txId);
4705
4895
  if (fromChain) {
4706
- const data = decryptSession(fromChain, wif);
4896
+ const data = await decryptSession(fromChain, wif);
4707
4897
  decrypted.push(data);
4708
4898
  }
4709
4899
  }
4710
- } catch {
4900
+ } catch (e) {
4901
+ if (e?.code === "WALLET_UNAVAILABLE") {
4902
+ v3Locked++;
4903
+ continue;
4904
+ }
4711
4905
  }
4712
4906
  }
4713
4907
  if (decrypted.length === 0) {
@@ -4738,6 +4932,10 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4738
4932
  "# Restored from Blockchain Memory (Indelible)",
4739
4933
  ""
4740
4934
  ];
4935
+ if (v3Locked > 0) {
4936
+ contextParts.push(`\u26A0\uFE0F PARTIAL RESTORE \u2014 ${v3Locked} wallet-native (v3) save(s) could NOT be decrypted because the wallet that made them is not available here. Connect that wallet (e.g. MetaNet Desktop via the web app) to restore them. Nothing was skipped silently \u2014 this notice is the record.`);
4937
+ contextParts.push("");
4938
+ }
4741
4939
  if (merged.length > 0) {
4742
4940
  contextParts.push(formatCurrentConversation(merged[0], pagination));
4743
4941
  for (let i = 1; i < merged.length; i++) {
@@ -4863,7 +5061,8 @@ async function loadContext(numSessions = 5, pagination = null, opts = {}) {
4863
5061
  total_preserved: driftResult.total_preserved,
4864
5062
  warning_emitted: !!driftWarning
4865
5063
  } : null,
4866
- message: `Smart restore (${source}): ${merged.length} conversation(s) from ${decrypted.length} chunks. Current conversation shown in full.`
5064
+ wallet_native_locked: v3Locked,
5065
+ message: `Smart restore (${source}): ${merged.length} conversation(s) from ${decrypted.length} chunks. Current conversation shown in full.` + (v3Locked > 0 ? ` \u26A0\uFE0F ${v3Locked} wallet-native save(s) locked \u2014 wallet not available (PARTIAL restore).` : "")
4867
5066
  };
4868
5067
  } catch (error) {
4869
5068
  return {
@@ -5121,16 +5320,18 @@ async function loadFile(txId, options = {}) {
5121
5320
  if (!encrypted) {
5122
5321
  return { success: false, error: "No encrypted content found in tx" };
5123
5322
  }
5124
- let content;
5323
+ let outBuf;
5125
5324
  try {
5126
- content = decrypt(encrypted, wif);
5127
- if (content.startsWith("gz:")) {
5128
- content = gunzipSync2(Buffer.from(content.slice(3), "base64")).toString("utf8");
5325
+ const dec = decrypt(encrypted, wif);
5326
+ if (dec.startsWith("gz:")) {
5327
+ outBuf = gunzipSync2(Buffer.from(dec.slice(3), "base64"));
5328
+ } else {
5329
+ outBuf = Buffer.from(dec, "utf8");
5129
5330
  }
5130
5331
  } catch (err) {
5131
5332
  return { success: false, error: `Decryption failed: ${err.message}` };
5132
5333
  }
5133
- const contentHash = sha256(content);
5334
+ const contentHash = sha256(outBuf);
5134
5335
  const expectedHash = payload.content_hash?.replace("sha256:", "") || null;
5135
5336
  const verified = expectedHash ? contentHash === expectedHash : null;
5136
5337
  if (options.outputPath) {
@@ -5138,8 +5339,10 @@ async function loadFile(txId, options = {}) {
5138
5339
  if (!existsSync11(dir)) {
5139
5340
  await mkdir4(dir, { recursive: true });
5140
5341
  }
5141
- await writeFile4(options.outputPath, content, "utf-8");
5342
+ await writeFile4(options.outputPath, outBuf);
5142
5343
  }
5344
+ const isBinary = payload.enc === "b64";
5345
+ const content = isBinary ? `[binary file: ${outBuf.length} bytes${options.outputPath ? ", written to disk" : ""}]` : outBuf.toString("utf8");
5143
5346
  const filename = payload.filename_enc ? decrypt(payload.filename_enc, wif) : payload.filename;
5144
5347
  const filePath = payload.path_enc ? decrypt(payload.path_enc, wif) : payload.path;
5145
5348
  return {
@@ -5148,6 +5351,7 @@ async function loadFile(txId, options = {}) {
5148
5351
  filename,
5149
5352
  path: filePath,
5150
5353
  size: payload.size,
5354
+ enc: payload.enc || "utf8",
5151
5355
  content,
5152
5356
  contentHash: `sha256:${contentHash}`,
5153
5357
  verified,
@@ -5676,7 +5880,7 @@ async function buildIndexWindow(address, wif, { fromDate = null, toDate = null,
5676
5880
  if (!fromChain || !fromChain.encrypted) {
5677
5881
  newGaps.push({ txId: s.txId, date: s.timestamp || null, save_type: s.save_type || null, reason: "NOT_ON_CHAIN", on_chain: false, logged_at: nowISO });
5678
5882
  } else {
5679
- const session = decryptSession(fromChain, wif);
5883
+ const session = await decryptSession(fromChain, wif);
5680
5884
  const { title, searchText, message_count } = deriveTitleSearch(session);
5681
5885
  newRecords.push({
5682
5886
  txId: s.txId,
@@ -5693,7 +5897,7 @@ async function buildIndexWindow(address, wif, { fromDate = null, toDate = null,
5693
5897
  }
5694
5898
  } catch (e) {
5695
5899
  const msg = String(e.message || e);
5696
- const reason = /timeout/i.test(msg) ? "FETCH_TIMEOUT" : "DECRYPT_FAIL";
5900
+ const reason = e?.code === "WALLET_UNAVAILABLE" ? "WALLET_LOCKED" : /timeout/i.test(msg) ? "FETCH_TIMEOUT" : "DECRYPT_FAIL";
5697
5901
  newGaps.push({ txId: s.txId, date: s.timestamp || null, save_type: s.save_type || null, reason, detail: redactSecrets(msg).slice(0, 120), on_chain: reason === "DECRYPT_FAIL", logged_at: nowISO });
5698
5902
  }
5699
5903
  done++;
@@ -5769,7 +5973,7 @@ async function diaryRecall({ from_date = null, to_date = null, query = null, dep
5769
5973
  entries.push({ txId, error: "not_on_chain" });
5770
5974
  continue;
5771
5975
  }
5772
- const s = decryptSession(fromChain, wif);
5976
+ const s = await decryptSession(fromChain, wif);
5773
5977
  if (!saveTypes.includes(s?.type)) {
5774
5978
  entries.push({ txId, error: "wrong_scope", type: s?.type || null });
5775
5979
  continue;
@@ -6223,7 +6427,7 @@ async function recallContext({ from_date = null, to_date = null, query = null, d
6223
6427
  sessions.push({ txId, error: "not_on_chain" });
6224
6428
  continue;
6225
6429
  }
6226
- const s = decryptSession(fromChain, wif);
6430
+ const s = await decryptSession(fromChain, wif);
6227
6431
  const msgs = Array.isArray(s?.messages) ? s.messages : [];
6228
6432
  sessions.push({
6229
6433
  txId,
@@ -6384,7 +6588,9 @@ import {
6384
6588
  P2PKH as P2PKH3,
6385
6589
  SatoshisPerKilobyte as SatoshisPerKilobyte3,
6386
6590
  LockingScript as LockingScript3,
6387
- OP as OP3
6591
+ OP as OP3,
6592
+ CompletedProtoWallet as CompletedProtoWallet3,
6593
+ AuthFetch as AuthFetch2
6388
6594
  } from "@bsv/sdk";
6389
6595
  var SHARE_DUST_SATS = 546;
6390
6596
  async function findRecipientPubkey(recipientAddr, scanLimit = 20) {
@@ -6462,12 +6668,23 @@ async function shareSession({ session_txid, recipient_addr, expires_at = null })
6462
6668
  const ownerAddr = ownerPriv.toPublicKey().toAddress();
6463
6669
  const payload = await fetchScopePayload(session_txid);
6464
6670
  if (!payload) return { success: false, error: `could not fetch scope tx ${session_txid}` };
6465
- if (!payload.wrap_owner) {
6671
+ const fmt = detectSaveFormat({ wrap_owner: payload.wrap_owner, wrap_self: payload.wrap_self, wrap_version: payload.wrap_version });
6672
+ let contentKey;
6673
+ if (fmt === "v3-wallet") {
6674
+ const rv3 = await unwrapForSelfV3(payload.wrap_self, new CompletedProtoWallet3(ownerPriv), payload.key_id);
6675
+ if (!rv3.ok) {
6676
+ return { success: false, error: rv3.reason === "wallet_unavailable" ? "failed to unwrap own v3 content_key: no wallet available" : `failed to unwrap own v3 content_key: ${rv3.reason} (wallet-native saves made by an external wallet can only be shared from the web app)` };
6677
+ }
6678
+ contentKey = rv3.contentKey;
6679
+ } else if (fmt === "v2-wrapped") {
6680
+ const r = unwrapForOwner(payload.wrap_owner, ownerPriv);
6681
+ if (!r.ok) return { success: false, error: `failed to unwrap own content_key: ${r.reason}` };
6682
+ contentKey = r.contentKey;
6683
+ } else if (fmt === "v3-malformed") {
6684
+ return { success: false, error: "scope carries malformed v3 fields \u2014 failing closed, not shareable" };
6685
+ } else {
6466
6686
  return { success: false, error: "scope is legacy format (no wrap_owner) \u2014 only post-g-170 saves are shareable. Re-save the session to make it shareable." };
6467
6687
  }
6468
- const r = unwrapForOwner(payload.wrap_owner, ownerPriv);
6469
- if (!r.ok) return { success: false, error: `failed to unwrap own content_key: ${r.reason}` };
6470
- const contentKey = r.contentKey;
6471
6688
  const recipientPubkey = await findRecipientPubkey(recipient_addr);
6472
6689
  if (!recipientPubkey) {
6473
6690
  return { success: false, error: `recipient ${recipient_addr} has no signed tx in their history \u2014 they must broadcast at least one tx for their pubkey to be discoverable` };
@@ -6522,7 +6739,7 @@ async function shareSession({ session_txid, recipient_addr, expires_at = null })
6522
6739
  tx.addOutput({ lockingScript: opReturnScript, satoshis: 0 });
6523
6740
  tx.addOutput({ lockingScript: p2pkh.lock(recipient_addr), satoshis: SHARE_DUST_SATS });
6524
6741
  tx.addOutput({ lockingScript: ownerLock, change: true });
6525
- await tx.fee(new SatoshisPerKilobyte3(1e3));
6742
+ await tx.fee(new SatoshisPerKilobyte3(150));
6526
6743
  await tx.sign();
6527
6744
  txHex = tx.toHex();
6528
6745
  txId = tx.id("hex");
@@ -6530,7 +6747,45 @@ async function shareSession({ session_txid, recipient_addr, expires_at = null })
6530
6747
  } catch (e) {
6531
6748
  return { success: false, error: `broadcast failed: ${e.message}` };
6532
6749
  }
6533
- return { success: true, share_txid: txId, envelope };
6750
+ const index_status = await indexShareOnBridge(txId, envelope, ownerPriv);
6751
+ return { success: true, share_txid: txId, envelope, index_status };
6752
+ }
6753
+ async function indexShareOnBridge(txId, envelope, ownerPriv) {
6754
+ try {
6755
+ const bridges = await getBridges();
6756
+ const record = {
6757
+ txId,
6758
+ recipient: envelope.recipient_addr,
6759
+ sender: envelope.owner_addr,
6760
+ identityKey: ownerPriv.toPublicKey().toString(),
6761
+ protocol: envelope.protocol,
6762
+ payload: JSON.stringify(envelope),
6763
+ // decryptShare recovers the full envelope from here
6764
+ // the envelope stamps ISO; the bridge index demands ms-epoch (putShare validation)
6765
+ issued_at: typeof envelope.issued_at === "number" ? envelope.issued_at : Date.parse(envelope.issued_at)
6766
+ };
6767
+ const af = new AuthFetch2(new CompletedProtoWallet3(ownerPriv));
6768
+ let lastErr = null;
6769
+ for (const b of bridges.slice(0, 3)) {
6770
+ try {
6771
+ const res = await af.fetch(`${b.url}/api/shares/index`, {
6772
+ method: "POST",
6773
+ headers: { "Content-Type": "application/json" },
6774
+ body: JSON.stringify(record)
6775
+ });
6776
+ if (res.ok) {
6777
+ const body = await res.json().catch(() => ({}));
6778
+ return { ok: true, bridge: b.url, gossip: body.gossip_status || "unknown" };
6779
+ }
6780
+ lastErr = `${b.url}: HTTP ${res.status}`;
6781
+ } catch (e) {
6782
+ lastErr = `${b.url}: ${e.message}`;
6783
+ }
6784
+ }
6785
+ return { ok: false, error: lastErr || "no bridge accepted the index record" };
6786
+ } catch (e) {
6787
+ return { ok: false, error: e.message };
6788
+ }
6534
6789
  }
6535
6790
  function pushDataChunk2(data) {
6536
6791
  const len = data.length;
@@ -6828,7 +7083,7 @@ Commands:
6828
7083
  }
6829
7084
  function printHelp() {
6830
7085
  console.log(`
6831
- Indelible MCP \u2014 Blockchain memory for Claude Code (v4.8.0)
7086
+ Indelible MCP \u2014 Blockchain memory for Claude Code (v4.8.1)
6832
7087
 
6833
7088
  Setup:
6834
7089
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -7047,7 +7302,7 @@ function readStdin() {
7047
7302
  }
7048
7303
  var SERVER_INFO = {
7049
7304
  name: "indelible",
7050
- version: "4.8.0",
7305
+ version: "4.8.1",
7051
7306
  description: "Blockchain-backed memory and code storage for Claude Code"
7052
7307
  };
7053
7308
  var TOOLS = [