indelible-mcp 3.8.0 → 4.0.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Indelible MCP Server
2
2
 
3
- Blockchain-backed memory for Claude Code. Save your AI conversations permanently on BSV.
3
+ Blockchain-backed memory for Claude Code. Save your AI conversations permanently on BSV via a federated mesh of SPV bridges.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -67,13 +67,23 @@ indelible-mcp hook post-compact Post-compaction restore hook
67
67
  ## How It Works
68
68
 
69
69
  1. Your conversation is encrypted locally with your WIF key
70
- 2. An OP_RETURN transaction is built and signed locally using `@bsv/sdk`
71
- 3. The signed transaction is broadcast via Indelible's SPV bridge
72
- 4. Your private key **never** leaves your machine
70
+ 2. A minimal OP_RETURN transaction is built containing only `{protocol, encrypted}` — no metadata leaks
71
+ 3. The signed transaction is broadcast via Indelible's federation bridges
72
+ 4. Session metadata is automatically indexed across all federation bridges for fast retrieval
73
+ 5. Your private key **never** leaves your machine
74
+
75
+ ## Federation
76
+
77
+ Indelible uses a multi-seed architecture — your saves and loads automatically try multiple federation bridges. If one bridge is down, the next picks up. No single point of failure.
78
+
79
+ Bridges sync session metadata via SessionRelay (WebSocket peer-to-peer), so all your sessions are available from any bridge in the mesh. The web app and CLI read from bridges first, with blockchain fallback.
80
+
81
+ The federation mesh is powered by [Relay Federation](https://github.com/zcoolz/relay-federation).
73
82
 
74
83
  ## Security
75
84
 
76
85
  - **Zero-knowledge encryption** - your WIF-derived AES-256-GCM key encrypts all data before it touches the network
86
+ - **Privacy-hardened transactions** - OP_RETURN contains only `{protocol, encrypted}` — no plaintext metadata on-chain
77
87
  - **Self-sovereign keys** - generated locally with `@bsv/sdk`, never transmitted
78
88
  - **Immutable storage** - once on BSV, your data cannot be altered or deleted
79
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "3.8.0",
3
+ "version": "4.0.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
@@ -254,7 +254,7 @@ Commands:
254
254
 
255
255
  function printHelp() {
256
256
  console.log(`
257
- Indelible MCP — Blockchain memory for Claude Code (v3.8.0)
257
+ Indelible MCP — Blockchain memory for Claude Code (v4.0.0)
258
258
 
259
259
  Setup:
260
260
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -466,7 +466,7 @@ function readStdin() {
466
466
 
467
467
  const SERVER_INFO = {
468
468
  name: 'indelible',
469
- version: '3.8.0',
469
+ version: '4.0.0',
470
470
  description: 'Blockchain-backed memory and code storage for Claude Code'
471
471
  }
472
472
 
package/src/lib/spv.js CHANGED
@@ -246,6 +246,24 @@ export async function broadcastTx(rawTxHex) {
246
246
  throw new Error(`Broadcast failed on all bridges: ${errors.join(', ')}`)
247
247
  }
248
248
 
249
+ /**
250
+ * Index session metadata on all federation bridges (best-effort).
251
+ * @param {object} sessionData — { txId, address, session_id, summary, message_count, save_type, timestamp, ... }
252
+ */
253
+ export async function indexSessionOnBridge(sessionData) {
254
+ const bridges = await getBridges()
255
+ for (const bridge of bridges) {
256
+ try {
257
+ await fetch(`${bridge.url}/api/sessions/index`, {
258
+ method: 'POST',
259
+ headers: { 'Content-Type': 'application/json' },
260
+ body: JSON.stringify(sessionData),
261
+ signal: AbortSignal.timeout(5000)
262
+ })
263
+ } catch {} // best-effort
264
+ }
265
+ }
266
+
249
267
  function pushDataChunk(data) {
250
268
  const len = data.length
251
269
  let op
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { readFileSync, existsSync } from 'node:fs'
12
+ import { gunzipSync } from 'node:zlib'
12
13
  import { join } from 'node:path'
13
14
  import { homedir } from 'node:os'
14
15
  import { Transaction } from '@bsv/sdk'
@@ -21,6 +22,16 @@ import { loadStyle } from './load_style.js'
21
22
  const RECENT_MESSAGES_FULL = 25
22
23
  const OLDER_MESSAGES_SUMMARIZED = 15
23
24
 
25
+ /** Decrypt and optionally gunzip a session payload (handles gz: prefix) */
26
+ function decryptSession(encrypted, wif) {
27
+ const text = decrypt(encrypted, wif)
28
+ if (text.startsWith('gz:')) {
29
+ const buf = gunzipSync(Buffer.from(text.slice(3), 'base64'))
30
+ return JSON.parse(buf.toString('utf8'))
31
+ }
32
+ return JSON.parse(text)
33
+ }
34
+
24
35
  function mergeDeltaSessions(sessions) {
25
36
  if (sessions.length === 0) return sessions
26
37
  const ordered = [...sessions].reverse()
@@ -195,7 +206,7 @@ export async function loadContext(numSessions = 5) {
195
206
  for (const entry of recent) {
196
207
  try {
197
208
  if (entry.encrypted) {
198
- const data = JSON.parse(decrypt(entry.encrypted, wif))
209
+ const data = decryptSession(entry.encrypted, wif)
199
210
  decrypted.push(data)
200
211
  }
201
212
  } catch { /* skip sessions that fail to decrypt */ }
@@ -240,12 +251,12 @@ export async function loadContext(numSessions = 5) {
240
251
  for (const session of sessions) {
241
252
  try {
242
253
  if (session.encrypted) {
243
- const data = JSON.parse(decrypt(session.encrypted, wif))
254
+ const data = decryptSession(session.encrypted, wif)
244
255
  decrypted.push(data)
245
256
  } else if (session.txId) {
246
257
  const encrypted = await fetchEncryptedFromChain(session.txId)
247
258
  if (encrypted) {
248
- const data = JSON.parse(decrypt(encrypted, wif))
259
+ const data = decryptSession(encrypted, wif)
249
260
  decrypted.push(data)
250
261
  }
251
262
  }
@@ -4,6 +4,7 @@
4
4
  * Supports v1 (plaintext metadata) and v2 (encrypted metadata).
5
5
  */
6
6
 
7
+ import { gunzipSync } from 'node:zlib'
7
8
  import { writeFileSync } from 'node:fs'
8
9
  import { dirname } from 'node:path'
9
10
  import { mkdirSync, existsSync } from 'node:fs'
@@ -72,8 +73,10 @@ export async function loadFile(txId, options = {}) {
72
73
  }
73
74
  }
74
75
  content = decrypt(encryptedFull, wif)
76
+ if (content.startsWith('gz:')) content = gunzipSync(Buffer.from(content.slice(3), 'base64')).toString('utf8')
75
77
  } else {
76
78
  content = decrypt(payload.encrypted, wif)
79
+ if (content.startsWith('gz:')) content = gunzipSync(Buffer.from(content.slice(3), 'base64')).toString('utf8')
77
80
  }
78
81
 
79
82
  // Decrypt metadata (v2) or use plaintext (v1)
@@ -4,6 +4,7 @@
4
4
  * On-chain: only encrypted data, no plaintext filenames.
5
5
  */
6
6
 
7
+ import { gzipSync } from 'node:zlib'
7
8
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
8
9
  import { basename, join } from 'node:path'
9
10
  import { homedir } from 'node:os'
@@ -40,8 +41,9 @@ export async function saveFile(filePath, options = {}) {
40
41
  const filename = basename(filePath)
41
42
  const relativePath = options.relativePath || filePath
42
43
 
43
- // Encrypt content and metadata
44
- const encrypted = encrypt(content, wif)
44
+ // Gzip compress then encrypt content
45
+ const compressed = gzipSync(Buffer.from(content))
46
+ const encrypted = encrypt('gz:' + compressed.toString('base64'), wif)
45
47
  const filenameEnc = encrypt(filename, wif)
46
48
  const pathEnc = encrypt(relativePath, wif)
47
49
 
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync, readdirSync } from 'node:fs'
17
+ import { gzipSync } from 'node:zlib'
17
18
  import { dirname, join } from 'node:path'
18
19
  import { homedir } from 'node:os'
19
20
  import { execSync } from 'node:child_process'
@@ -534,19 +535,13 @@ export async function saveSession(transcriptPath, summary) {
534
535
  if (estimatedFee > COST_WARNING_SATS) {
535
536
  process.stderr.write(`[indelible] WARNING: Estimated cost ~${estimatedFee} sats (~$${(estimatedFee / 1e8 * 15).toFixed(2)}). Rich mode is ${richMode ? 'ON' : 'OFF'}. Set "rich_saves": false in config to reduce cost.\n`)
536
537
  }
537
- const encrypted = encrypt(sessionJson, wif)
538
+ const compressed = gzipSync(Buffer.from(sessionJson))
539
+ const encrypted = encrypt('gz:' + compressed.toString('base64'), wif)
538
540
 
539
541
  // Build the blockchain payload
540
542
  const payload = {
541
543
  protocol: 'indelible.claude-code',
542
- version: 2,
543
- address: config.address,
544
- session_id: sessionId,
545
- prev_session_id: prevSessionId,
546
- summary: session.summary,
547
- message_count: session.message_count,
548
- encrypted,
549
- timestamp: session.created_at
544
+ encrypted
550
545
  }
551
546
 
552
547
  // Build + sign transaction LOCALLY with @bsv/sdk
@@ -574,6 +569,19 @@ export async function saveSession(transcriptPath, summary) {
574
569
  }, config)
575
570
  } catch { /* indexing failure doesn't block save */ }
576
571
 
572
+ // Index session on federation bridges (LevelDB)
573
+ try {
574
+ await spv.indexSessionOnBridge({
575
+ txId: finalTxId, address: config.address,
576
+ session_id: sessionId,
577
+ prev_session_id: prevSessionId,
578
+ summary: session.summary,
579
+ message_count: session.message_count,
580
+ save_type: isDelta ? 'delta' : 'full',
581
+ timestamp: session.created_at
582
+ })
583
+ } catch { /* bridge indexing failure doesn't block save */ }
584
+
577
585
  // Update session history with txId now that we have it
578
586
  const structuredCtx = session.structured_context || {}
579
587
  updateSessionHistory(