indelible-mcp 3.5.0 → 3.7.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 +2 -2
- package/src/index.js +2 -2
- package/src/lib/spv.js +1 -1
- package/src/lib/transcript.js +105 -38
- package/src/tools/load_context.js +27 -2
- package/src/tools/save_file.js +6 -3
- package/src/tools/save_project.js +2 -1
- package/src/tools/save_session.js +62 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "indelible-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"license": "MIT",
|
|
30
30
|
"repository": {
|
|
31
31
|
"type": "git",
|
|
32
|
-
"url": "https://github.com/indelibleai/indelible-mcp"
|
|
32
|
+
"url": "git+https://github.com/indelibleai/indelible-mcp.git"
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://indelible.one",
|
|
35
35
|
"dependencies": {
|
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.7.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.7.0',
|
|
470
470
|
description: 'Blockchain-backed memory and code storage for Claude Code'
|
|
471
471
|
}
|
|
472
472
|
|
package/src/lib/spv.js
CHANGED
package/src/lib/transcript.js
CHANGED
|
@@ -46,11 +46,14 @@ export function findCurrentTranscript() {
|
|
|
46
46
|
return newest
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
const MAX_BLOCK_BYTES = 65536 // 64KB per tool_result block
|
|
50
|
+
|
|
49
51
|
/**
|
|
50
|
-
* Parse JSONL transcript file
|
|
51
|
-
*
|
|
52
|
+
* Parse JSONL transcript file.
|
|
53
|
+
* richMode=false (default): extracts user/assistant text messages only (current behavior).
|
|
54
|
+
* richMode=true: preserves tool_use, tool_result, and thinking blocks as array content.
|
|
52
55
|
*/
|
|
53
|
-
export function parseTranscript(filePath) {
|
|
56
|
+
export function parseTranscript(filePath, richMode = false) {
|
|
54
57
|
const content = readFileSync(filePath, 'utf-8')
|
|
55
58
|
const lines = content.trim().split('\n')
|
|
56
59
|
const messages = []
|
|
@@ -62,46 +65,110 @@ export function parseTranscript(filePath) {
|
|
|
62
65
|
|
|
63
66
|
if (entry.type === 'user') {
|
|
64
67
|
const contentArr = entry.message?.content
|
|
65
|
-
if (Array.isArray(contentArr) && contentArr.some(c => c.type === 'tool_result')) continue
|
|
66
|
-
|
|
67
|
-
let text = ''
|
|
68
|
-
if (Array.isArray(contentArr)) {
|
|
69
|
-
text = contentArr
|
|
70
|
-
.filter(c => c.type === 'text')
|
|
71
|
-
.map(c => c.text)
|
|
72
|
-
.join('\n')
|
|
73
|
-
.replace(/<ide_selection>[\s\S]*?<\/ide_selection>\s*/g, '')
|
|
74
|
-
.trim()
|
|
75
|
-
} else if (typeof entry.message?.content === 'string') {
|
|
76
|
-
text = entry.message.content
|
|
77
|
-
}
|
|
78
68
|
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
69
|
+
if (!richMode) {
|
|
70
|
+
// Text-only mode (current behavior)
|
|
71
|
+
if (Array.isArray(contentArr) && contentArr.some(c => c.type === 'tool_result')) continue
|
|
72
|
+
let text = ''
|
|
73
|
+
if (Array.isArray(contentArr)) {
|
|
74
|
+
text = contentArr
|
|
75
|
+
.filter(c => c.type === 'text')
|
|
76
|
+
.map(c => c.text)
|
|
77
|
+
.join('\n')
|
|
78
|
+
.replace(/<ide_selection>[\s\S]*?<\/ide_selection>\s*/g, '')
|
|
79
|
+
.trim()
|
|
80
|
+
} else if (typeof entry.message?.content === 'string') {
|
|
81
|
+
text = entry.message.content
|
|
82
|
+
}
|
|
83
|
+
if (text) {
|
|
84
|
+
messages.push({
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: text,
|
|
87
|
+
timestamp: entry.timestamp || new Date().toISOString()
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
// Rich mode — keep text + tool_result blocks
|
|
92
|
+
const blocks = []
|
|
93
|
+
if (Array.isArray(contentArr)) {
|
|
94
|
+
for (const block of contentArr) {
|
|
95
|
+
if (block.type === 'text') {
|
|
96
|
+
const cleaned = (block.text || '').replace(/<ide_selection>[\s\S]*?<\/ide_selection>\s*/g, '').trim()
|
|
97
|
+
if (cleaned) blocks.push({ type: 'text', text: cleaned })
|
|
98
|
+
} else if (block.type === 'tool_result') {
|
|
99
|
+
const resultText = typeof block.content === 'string' ? block.content : JSON.stringify(block.content || '')
|
|
100
|
+
const truncated = resultText.length > MAX_BLOCK_BYTES
|
|
101
|
+
blocks.push({
|
|
102
|
+
type: 'tool_result',
|
|
103
|
+
tool_use_id: block.tool_use_id,
|
|
104
|
+
content: truncated ? resultText.slice(0, MAX_BLOCK_BYTES - 100) + `\n...[truncated, original ${resultText.length} bytes]` : resultText,
|
|
105
|
+
is_error: block.is_error || false,
|
|
106
|
+
...(truncated ? { truncated: true } : {})
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} else if (typeof entry.message?.content === 'string') {
|
|
111
|
+
blocks.push({ type: 'text', text: entry.message.content })
|
|
112
|
+
}
|
|
113
|
+
if (blocks.length > 0) {
|
|
114
|
+
const textOnly = blocks.filter(b => b.type === 'text').map(b => b.text).join('\n')
|
|
115
|
+
messages.push({
|
|
116
|
+
role: 'user',
|
|
117
|
+
content: blocks,
|
|
118
|
+
content_text: textOnly,
|
|
119
|
+
timestamp: entry.timestamp || new Date().toISOString()
|
|
120
|
+
})
|
|
121
|
+
}
|
|
85
122
|
}
|
|
86
123
|
} else if (entry.type === 'assistant') {
|
|
87
124
|
const contentArr = entry.message?.content
|
|
88
|
-
let text = ''
|
|
89
|
-
if (Array.isArray(contentArr)) {
|
|
90
|
-
text = contentArr
|
|
91
|
-
.filter(c => c.type === 'text')
|
|
92
|
-
.map(c => c.text)
|
|
93
|
-
.join('\n')
|
|
94
|
-
.trim()
|
|
95
|
-
} else if (typeof entry.message?.content === 'string') {
|
|
96
|
-
text = entry.message.content
|
|
97
|
-
}
|
|
98
125
|
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
126
|
+
if (!richMode) {
|
|
127
|
+
// Text-only mode (current behavior)
|
|
128
|
+
let text = ''
|
|
129
|
+
if (Array.isArray(contentArr)) {
|
|
130
|
+
text = contentArr
|
|
131
|
+
.filter(c => c.type === 'text')
|
|
132
|
+
.map(c => c.text)
|
|
133
|
+
.join('\n')
|
|
134
|
+
.trim()
|
|
135
|
+
} else if (typeof entry.message?.content === 'string') {
|
|
136
|
+
text = entry.message.content
|
|
137
|
+
}
|
|
138
|
+
if (text) {
|
|
139
|
+
messages.push({
|
|
140
|
+
role: 'assistant',
|
|
141
|
+
content: text,
|
|
142
|
+
timestamp: entry.timestamp || new Date().toISOString()
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
} else {
|
|
146
|
+
// Rich mode — keep text + tool_use + thinking blocks
|
|
147
|
+
const blocks = []
|
|
148
|
+
if (Array.isArray(contentArr)) {
|
|
149
|
+
for (const block of contentArr) {
|
|
150
|
+
if (block.type === 'text') {
|
|
151
|
+
if (block.text?.trim()) blocks.push({ type: 'text', text: block.text.trim() })
|
|
152
|
+
} else if (block.type === 'tool_use') {
|
|
153
|
+
blocks.push({ type: 'tool_use', name: block.name, input: block.input })
|
|
154
|
+
} else if (block.type === 'thinking') {
|
|
155
|
+
// Strip signature (large base64, wastes space)
|
|
156
|
+
const { signature, ...rest } = block
|
|
157
|
+
blocks.push({ type: 'thinking', thinking: rest.thinking || '' })
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else if (typeof entry.message?.content === 'string') {
|
|
161
|
+
blocks.push({ type: 'text', text: entry.message.content })
|
|
162
|
+
}
|
|
163
|
+
if (blocks.length > 0) {
|
|
164
|
+
const textOnly = blocks.filter(b => b.type === 'text').map(b => b.text).join('\n')
|
|
165
|
+
messages.push({
|
|
166
|
+
role: 'assistant',
|
|
167
|
+
content: blocks,
|
|
168
|
+
content_text: textOnly,
|
|
169
|
+
timestamp: entry.timestamp || new Date().toISOString()
|
|
170
|
+
})
|
|
171
|
+
}
|
|
105
172
|
}
|
|
106
173
|
}
|
|
107
174
|
} catch { /* skip malformed lines */ }
|
|
@@ -42,9 +42,34 @@ function mergeDeltaSessions(sessions) {
|
|
|
42
42
|
return merged.reverse()
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Convert rich content (block arrays) to readable text.
|
|
47
|
+
* Handles both string content (pass-through) and array content (render blocks).
|
|
48
|
+
*/
|
|
49
|
+
function renderRichContent(msg) {
|
|
50
|
+
// Use content_text shim if available (fastest path)
|
|
51
|
+
if (msg.content_text) return msg.content_text
|
|
52
|
+
if (typeof msg.content === 'string') return msg.content
|
|
53
|
+
if (!Array.isArray(msg.content)) return ''
|
|
54
|
+
return msg.content.map(block => {
|
|
55
|
+
if (block.type === 'text') return block.text
|
|
56
|
+
if (block.type === 'tool_use') {
|
|
57
|
+
const desc = block.input?.description || ''
|
|
58
|
+
const summary = block.input?.command || block.input?.pattern || block.input?.file_path || JSON.stringify(block.input).slice(0, 100)
|
|
59
|
+
return desc ? `[${block.name}] ${summary} — ${desc}` : `[${block.name}] ${summary}`
|
|
60
|
+
}
|
|
61
|
+
if (block.type === 'thinking') return `[Thinking] ${(block.thinking || '').slice(0, 200)}`
|
|
62
|
+
if (block.type === 'tool_result') {
|
|
63
|
+
const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content)
|
|
64
|
+
return `[Result] ${text.slice(0, 200)}`
|
|
65
|
+
}
|
|
66
|
+
return ''
|
|
67
|
+
}).filter(Boolean).join('\n')
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
function summarizeMessage(msg) {
|
|
46
71
|
const role = msg.role === 'user' ? 'User' : 'Assistant'
|
|
47
|
-
const text =
|
|
72
|
+
const text = renderRichContent(msg)
|
|
48
73
|
const oneLine = text.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim()
|
|
49
74
|
const truncated = oneLine.length > 150 ? oneLine.slice(0, 150) + '...' : oneLine
|
|
50
75
|
return `- **${role}:** ${truncated}`
|
|
@@ -95,7 +120,7 @@ function formatCurrentConversation(session) {
|
|
|
95
120
|
lines.push(`### Recent messages (last ${recentMessages.length}):`)
|
|
96
121
|
for (const msg of recentMessages) {
|
|
97
122
|
const role = msg.role === 'user' ? 'User' : 'Assistant'
|
|
98
|
-
const content =
|
|
123
|
+
const content = renderRichContent(msg)
|
|
99
124
|
const truncated = content.length > 2000 ? content.slice(0, 2000) + '\n...(truncated)' : content
|
|
100
125
|
lines.push(`**${role}:**`)
|
|
101
126
|
lines.push(truncated)
|
package/src/tools/save_file.js
CHANGED
|
@@ -69,9 +69,10 @@ export async function saveFile(filePath, options = {}) {
|
|
|
69
69
|
timestamp
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
const { txHex, txId, changeUtxos } = await spv.buildOpReturnTxWithChange(
|
|
72
|
+
const { txHex, txId, changeUtxos, fee, txSize } = await spv.buildOpReturnTxWithChange(
|
|
73
73
|
wif, utxos, JSON.stringify(payload)
|
|
74
74
|
)
|
|
75
|
+
process.stderr.write(`[indelible] save_file: ${fee} sats (${txSize} bytes)\n`)
|
|
75
76
|
const result = await spv.broadcastTx(txHex)
|
|
76
77
|
cacheTx(txId, payload)
|
|
77
78
|
masterTxId = result.txid || txId
|
|
@@ -92,9 +93,10 @@ export async function saveFile(filePath, options = {}) {
|
|
|
92
93
|
data: chunks[i]
|
|
93
94
|
})
|
|
94
95
|
|
|
95
|
-
const { txHex, txId, changeUtxos } = await spv.buildOpReturnTxWithChange(
|
|
96
|
+
const { txHex, txId, changeUtxos, fee, txSize } = await spv.buildOpReturnTxWithChange(
|
|
96
97
|
wif, utxos, chunkPayload
|
|
97
98
|
)
|
|
99
|
+
process.stderr.write(`[indelible] save_file chunk ${i + 1}/${chunks.length}: ${fee} sats (${txSize} bytes)\n`)
|
|
98
100
|
const result = await spv.broadcastTx(txHex)
|
|
99
101
|
cacheTx(txId, JSON.parse(chunkPayload))
|
|
100
102
|
chunkTxIds.push(result.txid || txId)
|
|
@@ -133,9 +135,10 @@ export async function saveFile(filePath, options = {}) {
|
|
|
133
135
|
await new Promise(r => setTimeout(r, 500))
|
|
134
136
|
utxos = await spv.getUtxos(config.address)
|
|
135
137
|
}
|
|
136
|
-
const { txHex, txId, changeUtxos: masterChangeUtxos } = await spv.buildOpReturnTxWithChange(
|
|
138
|
+
const { txHex, txId, changeUtxos: masterChangeUtxos, fee: masterFee, txSize: masterTxSize } = await spv.buildOpReturnTxWithChange(
|
|
137
139
|
wif, utxos, JSON.stringify(masterPayload)
|
|
138
140
|
)
|
|
141
|
+
process.stderr.write(`[indelible] save_file master index: ${masterFee} sats (${masterTxSize} bytes)\n`)
|
|
139
142
|
const result = await spv.broadcastTx(txHex)
|
|
140
143
|
cacheTx(txId, masterPayload)
|
|
141
144
|
masterTxId = result.txid || txId
|
|
@@ -141,9 +141,10 @@ export async function saveProject(dirPath, options = {}) {
|
|
|
141
141
|
return { success: false, error: 'No UTXOs available. Fund your wallet first.' }
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
const { txHex, txId } = await spv.buildOpReturnTxWithChange(
|
|
144
|
+
const { txHex, txId, fee, txSize } = await spv.buildOpReturnTxWithChange(
|
|
145
145
|
wif, utxos, JSON.stringify(payload), 'INDELIBLE_PROJECT_BUNDLE'
|
|
146
146
|
)
|
|
147
|
+
process.stderr.write(`[indelible] save_project "${projectName}": ${fee} sats (${txSize} bytes)\n`)
|
|
147
148
|
await spv.broadcastTx(txHex)
|
|
148
149
|
cacheTx(txId, payload)
|
|
149
150
|
|
|
@@ -21,7 +21,7 @@ import { loadConfig, saveConfig, getWif } from '../lib/config.js'
|
|
|
21
21
|
import { encrypt, sha256 } from '../lib/crypto.js'
|
|
22
22
|
import { findCurrentTranscript, parseTranscript } from '../lib/transcript.js'
|
|
23
23
|
import * as spv from '../lib/spv.js'
|
|
24
|
-
import { indexSession } from '../lib/api-client.js'
|
|
24
|
+
import { indexSession, checkProTier } from '../lib/api-client.js'
|
|
25
25
|
import { saveFile } from './save_file.js'
|
|
26
26
|
|
|
27
27
|
const CONTEXT_FILE = join(homedir(), '.indelible', 'indelible-context.jsonl')
|
|
@@ -40,9 +40,11 @@ function createSummary(messages, userSummary) {
|
|
|
40
40
|
if (userSummary) return userSummary
|
|
41
41
|
const firstUser = messages.find(m => m.role === 'user')
|
|
42
42
|
if (firstUser) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// Use content_text (rich mode fallback) or extract text from content
|
|
44
|
+
const text = firstUser.content_text
|
|
45
|
+
|| (typeof firstUser.content === 'string' ? firstUser.content : null)
|
|
46
|
+
|| (Array.isArray(firstUser.content) ? (firstUser.content.find(b => b.type === 'text')?.text || '') : '')
|
|
47
|
+
const preview = text.slice(0, 200)
|
|
46
48
|
return preview + (preview.length >= 200 ? '...' : '')
|
|
47
49
|
}
|
|
48
50
|
return 'Claude Code session'
|
|
@@ -312,11 +314,15 @@ function ensureJsonlBackup(path, messages) {
|
|
|
312
314
|
try {
|
|
313
315
|
const dir = dirname(path)
|
|
314
316
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
315
|
-
const lines = messages.map(m =>
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
317
|
+
const lines = messages.map(m => {
|
|
318
|
+
// If content is already an array (rich mode), pass through. Otherwise wrap as text block.
|
|
319
|
+
const content = Array.isArray(m.content) ? m.content : [{ type: 'text', text: m.content }]
|
|
320
|
+
return JSON.stringify({
|
|
321
|
+
type: m.role === 'user' ? 'user' : 'assistant',
|
|
322
|
+
message: { content },
|
|
323
|
+
timestamp: m.timestamp
|
|
324
|
+
})
|
|
325
|
+
})
|
|
320
326
|
writeFileSync(path, lines.join('\n') + '\n')
|
|
321
327
|
} catch { /* don't block */ }
|
|
322
328
|
}
|
|
@@ -422,12 +428,46 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
422
428
|
return { success: false, error: `Transcript not found: ${transcriptPath}` }
|
|
423
429
|
}
|
|
424
430
|
|
|
431
|
+
// Check tier for rich mode
|
|
432
|
+
let richMode = false
|
|
433
|
+
const richSetting = config.rich_saves
|
|
434
|
+
if (richSetting === false || richSetting === 'disabled') {
|
|
435
|
+
richMode = false
|
|
436
|
+
} else {
|
|
437
|
+
try {
|
|
438
|
+
const tierResult = await checkProTier(config.address)
|
|
439
|
+
richMode = tierResult.ok === true
|
|
440
|
+
} catch { /* offline or error — stay text-only */ }
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// First-time rich mode warning
|
|
444
|
+
if (richMode && !config.rich_saves_acknowledged) {
|
|
445
|
+
process.stderr.write(
|
|
446
|
+
`[indelible] Rich mode active — saves capture tool calls, results, and thinking blocks (~6-8x larger). ` +
|
|
447
|
+
`To disable: set "rich_saves": false in ~/.indelible/config.json\n`
|
|
448
|
+
)
|
|
449
|
+
config.rich_saves_acknowledged = true
|
|
450
|
+
saveConfig({ ...config })
|
|
451
|
+
}
|
|
452
|
+
|
|
425
453
|
// Parse transcript
|
|
426
|
-
const allMessages = parseTranscript(actualPath)
|
|
454
|
+
const allMessages = parseTranscript(actualPath, richMode)
|
|
427
455
|
if (allMessages.length === 0) {
|
|
428
456
|
return { success: false, error: 'No messages found in transcript' }
|
|
429
457
|
}
|
|
430
458
|
|
|
459
|
+
// Session size cap — if rich mode produces >8MB, fall back to text-only
|
|
460
|
+
const MAX_SESSION_BYTES = 8 * 1024 * 1024
|
|
461
|
+
if (richMode) {
|
|
462
|
+
const estimatedSize = JSON.stringify(allMessages).length
|
|
463
|
+
if (estimatedSize > MAX_SESSION_BYTES) {
|
|
464
|
+
process.stderr.write(`[indelible] Rich session too large (${(estimatedSize / 1024 / 1024).toFixed(1)} MB) — falling back to text-only\n`)
|
|
465
|
+
richMode = false
|
|
466
|
+
allMessages.length = 0
|
|
467
|
+
allMessages.push(...parseTranscript(actualPath, false))
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
431
471
|
// Append recent plan files (modified in last 24h) — plans are context worth saving
|
|
432
472
|
const plans = getRecentPlans()
|
|
433
473
|
if (plans.length > 0) {
|
|
@@ -460,6 +500,8 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
460
500
|
const session = {
|
|
461
501
|
id: sessionId,
|
|
462
502
|
type: isDelta ? 'delta' : 'full',
|
|
503
|
+
schema_version: richMode ? 2 : 1,
|
|
504
|
+
rich_mode: richMode,
|
|
463
505
|
prev_session_id: prevSessionId,
|
|
464
506
|
summary: createSummary(allMessages, summary),
|
|
465
507
|
message_count: allMessages.length,
|
|
@@ -483,7 +525,9 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
483
525
|
if (transcriptPath) ensureJsonlBackup(transcriptPath, allMessages)
|
|
484
526
|
|
|
485
527
|
// Encrypt locally — WIF never leaves
|
|
486
|
-
const
|
|
528
|
+
const sessionJson = JSON.stringify(session)
|
|
529
|
+
process.stderr.write(`[indelible] Saving session (${(sessionJson.length / 1024).toFixed(0)} KB, rich_mode: ${richMode})\n`)
|
|
530
|
+
const encrypted = encrypt(sessionJson, wif)
|
|
487
531
|
|
|
488
532
|
// Build the blockchain payload
|
|
489
533
|
const payload = {
|
|
@@ -501,7 +545,8 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
501
545
|
// Build + sign transaction LOCALLY with @bsv/sdk
|
|
502
546
|
try {
|
|
503
547
|
const utxos = await spv.getUtxos(config.address)
|
|
504
|
-
const { txHex, txId, changeUtxos } = await spv.buildOpReturnTxWithChange(wif, utxos, JSON.stringify(payload))
|
|
548
|
+
const { txHex, txId, changeUtxos, fee, txSize } = await spv.buildOpReturnTxWithChange(wif, utxos, JSON.stringify(payload))
|
|
549
|
+
process.stderr.write(`[indelible] save_session: ${fee} sats (${txSize} bytes)\n`)
|
|
505
550
|
|
|
506
551
|
// Broadcast via SPV bridge (gated by API key)
|
|
507
552
|
const broadcastResult = await spv.broadcastTx(txHex)
|
|
@@ -626,9 +671,13 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
626
671
|
: ` (full: ${allMessages.length} messages)`
|
|
627
672
|
const memInfo = memoryTxId ? ' Memory files saved.' : ''
|
|
628
673
|
|
|
674
|
+
const costInfo = fee ? ` Cost: ${fee} sats.` : ''
|
|
675
|
+
|
|
629
676
|
return {
|
|
630
677
|
success: true,
|
|
631
678
|
txId: finalTxId,
|
|
679
|
+
fee,
|
|
680
|
+
txSize,
|
|
632
681
|
sessionId,
|
|
633
682
|
prevSessionId,
|
|
634
683
|
messageCount: allMessages.length,
|
|
@@ -637,7 +686,7 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
637
686
|
summary: session.summary,
|
|
638
687
|
memoryTxId,
|
|
639
688
|
historyTxId,
|
|
640
|
-
message: `Session saved!${deltaInfo} committed to blockchain.${prevSessionId ? ' Linked to previous session.' : ' (First session)'}${memInfo}`
|
|
689
|
+
message: `Session saved!${deltaInfo} committed to blockchain.${costInfo}${prevSessionId ? ' Linked to previous session.' : ' (First session)'}${memInfo}`
|
|
641
690
|
}
|
|
642
691
|
} catch (error) {
|
|
643
692
|
return { success: false, error: `Failed to commit: ${error.message}` }
|