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 +1 -1
- package/src/index.js +2 -2
- package/src/lib/api-client.js +12 -2
- package/src/lib/spv.js +11 -14
- package/src/tools/load_context.js +16 -20
- package/src/tools/load_file.js +25 -26
- package/src/tools/load_project.js +14 -7
- package/src/tools/load_style.js +22 -35
- package/src/tools/save_session.js +7 -0
package/package.json
CHANGED
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.
|
|
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.
|
|
469
|
+
version: '3.8.0',
|
|
470
470
|
description: 'Blockchain-backed memory and code storage for Claude Code'
|
|
471
471
|
}
|
|
472
472
|
|
package/src/lib/api-client.js
CHANGED
|
@@ -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
|
|
189
|
-
const
|
|
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
|
|
14
|
-
|
|
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 = [
|
|
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 = [
|
|
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
|
|
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
|
-
|
|
171
|
-
|
|
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
|
|
156
|
-
*
|
|
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
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
}
|
package/src/tools/load_file.js
CHANGED
|
@@ -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
|
|
22
|
-
if (!
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
61
|
-
const
|
|
62
|
-
|
|
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
|
|
22
|
-
if (!
|
|
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
|
-
//
|
|
27
|
-
const
|
|
28
|
-
|
|
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' }
|
package/src/tools/load_style.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|