indelible-mcp 3.7.0 → 3.9.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 +7 -1
- package/package.json +1 -1
- package/src/index.js +2 -2
- package/src/lib/api-client.js +12 -2
- package/src/lib/spv.js +29 -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 +21 -8
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
|
|
|
@@ -71,6 +71,12 @@ indelible-mcp hook post-compact Post-compaction restore hook
|
|
|
71
71
|
3. The signed transaction is broadcast via Indelible's SPV bridge
|
|
72
72
|
4. Your private key **never** leaves your machine
|
|
73
73
|
|
|
74
|
+
## Federation
|
|
75
|
+
|
|
76
|
+
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.
|
|
77
|
+
|
|
78
|
+
The federation mesh is powered by [Relay Federation](https://github.com/zcoolz/relay-federation).
|
|
79
|
+
|
|
74
80
|
## Security
|
|
75
81
|
|
|
76
82
|
- **Zero-knowledge encryption** - your WIF-derived AES-256-GCM key encrypts all data before it touches the network
|
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.9.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.9.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
|
|
|
@@ -249,6 +246,24 @@ export async function broadcastTx(rawTxHex) {
|
|
|
249
246
|
throw new Error(`Broadcast failed on all bridges: ${errors.join(', ')}`)
|
|
250
247
|
}
|
|
251
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
|
+
|
|
252
267
|
function pushDataChunk(data) {
|
|
253
268
|
const len = data.length
|
|
254
269
|
let op
|
|
@@ -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,19 +527,19 @@ 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
|
|
533
540
|
const payload = {
|
|
534
541
|
protocol: 'indelible.claude-code',
|
|
535
|
-
|
|
536
|
-
address: config.address,
|
|
537
|
-
session_id: sessionId,
|
|
538
|
-
prev_session_id: prevSessionId,
|
|
539
|
-
summary: session.summary,
|
|
540
|
-
message_count: session.message_count,
|
|
541
|
-
encrypted,
|
|
542
|
-
timestamp: session.created_at
|
|
542
|
+
encrypted
|
|
543
543
|
}
|
|
544
544
|
|
|
545
545
|
// Build + sign transaction LOCALLY with @bsv/sdk
|
|
@@ -567,6 +567,19 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
567
567
|
}, config)
|
|
568
568
|
} catch { /* indexing failure doesn't block save */ }
|
|
569
569
|
|
|
570
|
+
// Index session on federation bridges (LevelDB)
|
|
571
|
+
try {
|
|
572
|
+
await spv.indexSessionOnBridge({
|
|
573
|
+
txId: finalTxId, address: config.address,
|
|
574
|
+
session_id: sessionId,
|
|
575
|
+
prev_session_id: prevSessionId,
|
|
576
|
+
summary: session.summary,
|
|
577
|
+
message_count: session.message_count,
|
|
578
|
+
save_type: isDelta ? 'delta' : 'full',
|
|
579
|
+
timestamp: session.created_at
|
|
580
|
+
})
|
|
581
|
+
} catch { /* bridge indexing failure doesn't block save */ }
|
|
582
|
+
|
|
570
583
|
// Update session history with txId now that we have it
|
|
571
584
|
const structuredCtx = session.structured_context || {}
|
|
572
585
|
updateSessionHistory(
|