indelible-mcp 3.7.0 → 3.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "3.7.0",
3
+ "version": "3.8.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.7.0)
257
+ Indelible MCP — Blockchain memory for Claude Code (v3.8.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.7.0',
469
+ version: '3.8.0',
470
470
  description: 'Blockchain-backed memory and code storage for Claude Code'
471
471
  }
472
472
 
@@ -4,6 +4,7 @@
4
4
  * Blockchain operations go through SPV bridge (see spv.js).
5
5
  */
6
6
 
7
+ import { Transaction } from '@bsv/sdk'
7
8
  import { loadConfig } from './config.js'
8
9
  import * as spv from './spv.js'
9
10
 
@@ -185,8 +186,17 @@ export async function getLatestSessions(address, wif, limit = 3) {
185
186
  for (const txInfo of history) {
186
187
  if (sessions.length >= limit) break
187
188
  try {
188
- const tx = await spv.getTx(txInfo.tx_hash)
189
- const encrypted = spv.extractEncryptedFromTx(tx)
189
+ const rawHex = await spv.getRawTx(txInfo.tx_hash)
190
+ const tx = Transaction.fromHex(rawHex)
191
+ let encrypted = null
192
+ for (const output of tx.outputs) {
193
+ if (output.satoshis === 0) {
194
+ const hex = output.lockingScript.toHex()
195
+ const str = Buffer.from(hex, 'hex').toString('utf8')
196
+ const match = str.match(/([A-Za-z0-9+/=]{12,}):([A-Za-z0-9+/=]{20,}):([A-Za-z0-9+/=]{20,})/)
197
+ if (match) { encrypted = match[0]; break }
198
+ }
199
+ }
190
200
  if (encrypted) {
191
201
  sessions.push({
192
202
  txId: txInfo.tx_hash,
package/src/lib/spv.js CHANGED
@@ -10,8 +10,12 @@
10
10
  import { Transaction, P2PKH, PrivateKey, SatoshisPerKilobyte, LockingScript, OP } from '@bsv/sdk'
11
11
  import { loadConfig, saveConfig } from './config.js'
12
12
 
13
- const GATEWAY_URL = 'http://155.138.238.167:8080'
14
- const GATEWAY_BRIDGE = { url: GATEWAY_URL, name: 'relay-gateway' }
13
+ const SEED_BRIDGES = [
14
+ { url: 'http://149.28.243.56:9333', name: 'bridge-delta' },
15
+ { url: 'http://144.202.48.217:9333', name: 'bridge-alpha' },
16
+ { url: 'http://45.63.77.31:9333', name: 'bridge-beta' },
17
+ { url: 'http://45.63.70.235:9333', name: 'bridge-gamma' },
18
+ ]
15
19
 
16
20
  // Old individual bridge IPs — used to detect configs that need migration
17
21
  const OLD_BRIDGE_IPS = [
@@ -67,7 +71,7 @@ async function getBridges() {
67
71
  return OLD_BRIDGE_IPS.some(ip => url.includes(ip))
68
72
  })
69
73
  if (hasOldIps) {
70
- bridges = [GATEWAY_BRIDGE]
74
+ bridges = [...SEED_BRIDGES]
71
75
  config.spv_bridges = bridges
72
76
  // Auto-generate relay key if missing
73
77
  if (!apiKey || !apiKey.startsWith('relay_sk_')) {
@@ -103,7 +107,7 @@ async function getBridges() {
103
107
  const result = await ensureRelayKey(config.address)
104
108
  if (result) {
105
109
  apiKey = result.key
106
- config.spv_bridges = [GATEWAY_BRIDGE]
110
+ config.spv_bridges = [...SEED_BRIDGES]
107
111
  config.spv_api_key = apiKey
108
112
  saveConfig(config)
109
113
  }
@@ -111,7 +115,7 @@ async function getBridges() {
111
115
  migrationDone = true
112
116
  }
113
117
 
114
- return [{ ...GATEWAY_BRIDGE, apiKey }]
118
+ return SEED_BRIDGES.map(b => ({ ...b, apiKey }))
115
119
  }
116
120
 
117
121
  async function spvFetch(path, options = {}) {
@@ -167,15 +171,8 @@ export async function checkHealth() {
167
171
  }
168
172
 
169
173
  export async function getUtxos(address) {
170
- try {
171
- const res = await spvFetch(`/api/address/${address}/unspent`)
172
- if (res.ok) return res.json()
173
- } catch { /* bridge unavailable */ }
174
-
175
- // WoC lookup for address-based indexing
176
- const wocRes = await fetch(`https://api.whatsonchain.com/v1/bsv/main/address/${address}/unspent`)
177
- if (wocRes.ok) return wocRes.json()
178
-
174
+ const res = await spvFetch(`/api/address/${address}/unspent`)
175
+ if (res.ok) return res.json()
179
176
  throw new Error(`Failed to get UTXOs for ${address}`)
180
177
  }
181
178
 
@@ -11,6 +11,7 @@
11
11
  import { readFileSync, existsSync } from 'node:fs'
12
12
  import { join } from 'node:path'
13
13
  import { homedir } from 'node:os'
14
+ import { Transaction } from '@bsv/sdk'
14
15
  import { loadConfig, getWif } from '../lib/config.js'
15
16
  import { decrypt } from '../lib/crypto.js'
16
17
  import { getSessions } from '../lib/api-client.js'
@@ -152,28 +153,24 @@ function formatPreviousConversation(session) {
152
153
  }
153
154
 
154
155
  /**
155
- * Fetch encrypted data directly from a tx's OP_RETURN
156
- * Tries SPV bridge first, falls back to WhatsOnChain
156
+ * Fetch encrypted data directly from blockchain tx
157
+ * Uses getRawTx (federation bridge) no WoC dependency
157
158
  */
158
159
  async function fetchEncryptedFromChain(txId) {
159
160
  const cleanTxId = txId.trim()
160
-
161
- // Try SPV bridge
162
- try {
163
- const tx = await spv.getTx(cleanTxId)
164
- const encrypted = spv.extractEncryptedFromTx(tx)
165
- if (encrypted) return encrypted
166
- } catch { /* fall through */ }
167
-
168
- // Fallback: WhatsOnChain
169
161
  try {
170
- const res = await fetch(`https://api.whatsonchain.com/v1/bsv/main/tx/${cleanTxId}`)
171
- if (!res.ok) return null
172
- const tx = await res.json()
173
- return spv.extractEncryptedFromTx(tx)
174
- } catch {
175
- return null
176
- }
162
+ const rawHex = await spv.getRawTx(cleanTxId)
163
+ const tx = Transaction.fromHex(rawHex)
164
+ for (const output of tx.outputs) {
165
+ if (output.satoshis === 0) {
166
+ const hex = output.lockingScript.toHex()
167
+ const str = Buffer.from(hex, 'hex').toString('utf8')
168
+ const match = str.match(/([A-Za-z0-9+/=]{12,}):([A-Za-z0-9+/=]{20,}):([A-Za-z0-9+/=]{20,})/)
169
+ if (match) return match[0]
170
+ }
171
+ }
172
+ } catch { /* failed */ }
173
+ return null
177
174
  }
178
175
 
179
176
  /**
@@ -222,8 +219,7 @@ export async function loadContext(numSessions = 5) {
222
219
  for (const tx of recent) {
223
220
  if (sessions.length >= numSessions) break
224
221
  try {
225
- const txData = await spv.getTx(tx.tx_hash)
226
- const encrypted = spv.extractEncryptedFromTx(txData)
222
+ const encrypted = await fetchEncryptedFromChain(tx.tx_hash)
227
223
  if (encrypted) {
228
224
  sessions.push({ txId: tx.tx_hash, encrypted })
229
225
  }
@@ -7,10 +7,28 @@
7
7
  import { writeFileSync } from 'node:fs'
8
8
  import { dirname } from 'node:path'
9
9
  import { mkdirSync, existsSync } from 'node:fs'
10
+ import { Transaction } from '@bsv/sdk'
10
11
  import { getWif } from '../lib/config.js'
11
12
  import { decrypt, sha256 } from '../lib/crypto.js'
12
13
  import * as spv from '../lib/spv.js'
13
14
 
15
+ /**
16
+ * Parse OP_RETURN JSON from raw tx hex — no WoC/getTx dependency
17
+ */
18
+ function parseOpReturnJson(rawHex) {
19
+ const tx = Transaction.fromHex(rawHex)
20
+ for (const output of tx.outputs) {
21
+ if (output.satoshis === 0) {
22
+ const hex = output.lockingScript.toHex()
23
+ const str = Buffer.from(hex, 'hex').toString('utf8')
24
+ const jsonStart = str.indexOf('{')
25
+ if (jsonStart === -1) continue
26
+ try { return JSON.parse(str.slice(jsonStart)) } catch { continue }
27
+ }
28
+ }
29
+ return null
30
+ }
31
+
14
32
  export async function loadFile(txId, options = {}) {
15
33
  const wif = await getWif()
16
34
  if (!wif) {
@@ -18,31 +36,16 @@ export async function loadFile(txId, options = {}) {
18
36
  }
19
37
 
20
38
  try {
21
- const txData = await spv.getTx(txId)
22
- if (!txData) {
39
+ const rawHex = await spv.getRawTx(txId)
40
+ if (!rawHex) {
23
41
  return { success: false, error: `Transaction not found: ${txId}` }
24
42
  }
25
43
 
26
- // Extract OP_RETURN payload
27
- const opReturn = txData.vout?.find(out => out.value === 0)
28
- if (!opReturn) {
29
- return { success: false, error: 'No OP_RETURN output found' }
30
- }
31
-
32
- const hex = opReturn.scriptPubKey?.hex
33
- if (!hex) {
34
- return { success: false, error: 'Empty script' }
35
- }
36
-
37
- // Parse JSON from OP_RETURN
38
- const raw = Buffer.from(hex, 'hex').toString('utf8')
39
- const jsonStart = raw.indexOf('{')
40
- if (jsonStart === -1) {
44
+ const payload = parseOpReturnJson(rawHex)
45
+ if (!payload) {
41
46
  return { success: false, error: 'No JSON payload found in OP_RETURN' }
42
47
  }
43
48
 
44
- const payload = JSON.parse(raw.slice(jsonStart))
45
-
46
49
  if (payload.protocol !== 'indelible.file') {
47
50
  return { success: false, error: `Not an indelible.file tx (got: ${payload.protocol || 'unknown'})` }
48
51
  }
@@ -57,15 +60,11 @@ export async function loadFile(txId, options = {}) {
57
60
  if (!chunkTxId) {
58
61
  return { success: false, error: `Missing chunk_${i} reference` }
59
62
  }
60
- const chunkTx = await spv.getTx(chunkTxId)
61
- const chunkOp = chunkTx?.vout?.find(out => out.value === 0)
62
- const chunkHex = chunkOp?.scriptPubKey?.hex
63
- if (!chunkHex) {
63
+ const chunkRawHex = await spv.getRawTx(chunkTxId)
64
+ const chunkData = parseOpReturnJson(chunkRawHex)
65
+ if (!chunkData || !chunkData.data) {
64
66
  return { success: false, error: `Chunk ${i} has no data` }
65
67
  }
66
- const chunkRaw = Buffer.from(chunkHex, 'hex').toString('utf8')
67
- const chunkJsonStart = chunkRaw.indexOf('{')
68
- const chunkData = JSON.parse(chunkRaw.slice(chunkJsonStart))
69
68
  encryptedFull += chunkData.data
70
69
 
71
70
  if (i < payload._chunks) {
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { writeFileSync, mkdirSync, existsSync } from 'node:fs'
8
8
  import { join, dirname } from 'node:path'
9
+ import { Transaction } from '@bsv/sdk'
9
10
  import { getWif } from '../lib/config.js'
10
11
  import { decrypt, sha256 } from '../lib/crypto.js'
11
12
  import * as spv from '../lib/spv.js'
@@ -18,19 +19,25 @@ export async function loadProject(txId, options = {}) {
18
19
  }
19
20
 
20
21
  try {
21
- const txData = await spv.getTx(txId)
22
- if (!txData) {
22
+ const rawHex = await spv.getRawTx(txId)
23
+ if (!rawHex) {
23
24
  return { success: false, error: `Manifest transaction not found: ${txId}` }
24
25
  }
25
26
 
26
- // Extract OP_RETURN payload
27
- const opReturn = txData.vout?.find(out => out.value === 0)
28
- if (!opReturn) {
27
+ // Parse raw tx and extract OP_RETURN JSON payload
28
+ const txData = Transaction.fromHex(rawHex)
29
+ let raw = null
30
+ for (const output of txData.outputs) {
31
+ if (output.satoshis === 0) {
32
+ const hex = output.lockingScript.toHex()
33
+ raw = Buffer.from(hex, 'hex').toString('utf8')
34
+ break
35
+ }
36
+ }
37
+ if (!raw) {
29
38
  return { success: false, error: 'No OP_RETURN output found' }
30
39
  }
31
40
 
32
- const hex = opReturn.scriptPubKey?.hex
33
- const raw = Buffer.from(hex, 'hex').toString('utf8')
34
41
  const jsonStart = raw.indexOf('{')
35
42
  if (jsonStart === -1) {
36
43
  return { success: false, error: 'No JSON payload found in OP_RETURN' }
@@ -7,58 +7,45 @@
7
7
  * Used by post-compact hook to reload rules after context compaction.
8
8
  */
9
9
 
10
+ import { Transaction } from '@bsv/sdk'
10
11
  import { loadConfig, getWif } from '../lib/config.js'
11
12
  import { decrypt } from '../lib/crypto.js'
12
13
  import * as spv from '../lib/spv.js'
13
14
 
14
15
  /**
15
16
  * Fetch and extract encrypted style data from a blockchain tx.
16
- * Tries SPV bridge first, falls back to WhatsOnChain.
17
+ * Uses getRawTx (federation bridge) no WoC dependency.
17
18
  *
18
19
  * @param {string} txId - Transaction ID
19
20
  * @returns {Promise<string|null>} Encrypted data string or null
20
21
  */
21
22
  async function fetchStyleFromChain(txId) {
22
23
  const cleanTxId = txId.trim()
23
-
24
- // Try SPV bridge first
25
24
  try {
26
- const tx = await spv.getTx(cleanTxId)
27
- const encrypted = spv.extractEncryptedFromTx(tx)
28
- if (encrypted) return encrypted
29
- } catch {
30
- // SPV bridge failed, try WhatsOnChain
31
- }
25
+ const rawHex = await spv.getRawTx(cleanTxId)
26
+ const tx = Transaction.fromHex(rawHex)
27
+ for (const output of tx.outputs) {
28
+ if (output.satoshis === 0) {
29
+ const hex = output.lockingScript.toHex()
30
+ const str = Buffer.from(hex, 'hex').toString('utf8')
32
31
 
33
- // Fallback: WhatsOnChain parse OP_RETURN for JSON payload
34
- try {
35
- const res = await fetch(`https://api.whatsonchain.com/v1/bsv/main/tx/${cleanTxId}`)
36
- if (!res.ok) return null
37
- const tx = await res.json()
38
- const opReturn = tx.vout?.find(out => out.value === 0)
39
- if (!opReturn) return null
40
- const hex = opReturn.scriptPubKey?.hex
41
- if (!hex) return null
42
- const str = Buffer.from(hex, 'hex').toString('utf8')
32
+ // Try JSON payload first (style format has encrypted field inside JSON)
33
+ try {
34
+ const jsonStart = str.indexOf('{')
35
+ if (jsonStart !== -1) {
36
+ const json = JSON.parse(str.slice(jsonStart))
37
+ if (json.protocol === 'indelible.style' && json.encrypted) {
38
+ return json.encrypted
39
+ }
40
+ }
41
+ } catch { /* not JSON, try raw */ }
43
42
 
44
- // Try to parse as JSON payload (style format has encrypted field inside JSON)
45
- try {
46
- // Find the JSON object in the OP_RETURN data
47
- const jsonStart = str.indexOf('{')
48
- if (jsonStart === -1) return null
49
- const json = JSON.parse(str.slice(jsonStart))
50
- if (json.protocol === 'indelible.style' && json.encrypted) {
51
- return json.encrypted
43
+ // Raw encrypted format (iv:tag:ciphertext)
44
+ const match = str.match(/([A-Za-z0-9+/=]{12,}):([A-Za-z0-9+/=]{20,}):([A-Za-z0-9+/=]{20,})/)
45
+ if (match) return match[0]
52
46
  }
53
- } catch {
54
- // Not JSON format, try raw encrypted format (iv:tag:ciphertext)
55
- const match = str.match(/([A-Za-z0-9+/=]{12,}):([A-Za-z0-9+/=]{20,}):([A-Za-z0-9+/=]{20,})/)
56
- return match ? match[0] : null
57
47
  }
58
- } catch {
59
- return null
60
- }
61
-
48
+ } catch { /* failed */ }
62
49
  return null
63
50
  }
64
51
 
@@ -527,6 +527,13 @@ export async function saveSession(transcriptPath, summary) {
527
527
  // Encrypt locally — WIF never leaves
528
528
  const sessionJson = JSON.stringify(session)
529
529
  process.stderr.write(`[indelible] Saving session (${(sessionJson.length / 1024).toFixed(0)} KB, rich_mode: ${richMode})\n`)
530
+
531
+ // Cost warning — estimate fee from JSON size (base64 + tx overhead ≈ 1.4x)
532
+ const COST_WARNING_SATS = 333_333 // ~$0.05 at $15/BSV
533
+ const estimatedFee = Math.ceil(sessionJson.length * 1.4)
534
+ if (estimatedFee > COST_WARNING_SATS) {
535
+ 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
+ }
530
537
  const encrypted = encrypt(sessionJson, wif)
531
538
 
532
539
  // Build the blockchain payload