indelible-mcp 3.4.0 → 3.6.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/transcript.js +105 -38
- package/src/tools/diary_chat.js +18 -2
- package/src/tools/diary_connect.js +15 -0
- package/src/tools/load_context.js +27 -2
- package/src/tools/save_session.js +40 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "indelible-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.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.6.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.6.0',
|
|
470
470
|
description: 'Blockchain-backed memory and code storage for Claude Code'
|
|
471
471
|
}
|
|
472
472
|
|
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 */ }
|
package/src/tools/diary_chat.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* diary_chat({ message: 'How should we architect this?', context: 'Working on SPV bridge...' })
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { loadConfig, getWif } from '../lib/config.js'
|
|
12
|
+
import { loadConfig, saveConfig, getWif } from '../lib/config.js'
|
|
13
13
|
import { checkProTier, getLatestSessions } from '../lib/api-client.js'
|
|
14
14
|
import { decrypt } from '../lib/crypto.js'
|
|
15
15
|
|
|
@@ -43,6 +43,20 @@ export async function diaryChat({ message, context, systemPrompt }) {
|
|
|
43
43
|
if (!tier.ok) return { success: false, error: tier.error }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// If no local diary config or model, try pulling from server
|
|
47
|
+
if (!config.diary || !config.diary.model) {
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`https://indelible.one/api/mcp/settings?address=${config.address}`)
|
|
50
|
+
const server = await res.json()
|
|
51
|
+
if (server.diary_model) {
|
|
52
|
+
config.diary = config.diary || {}
|
|
53
|
+
config.diary.model = server.diary_model
|
|
54
|
+
config.diary.name = server.diary_name || 'Codex'
|
|
55
|
+
saveConfig(config)
|
|
56
|
+
}
|
|
57
|
+
} catch { /* server fetch is best-effort */ }
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
if (!config.diary || !config.diary.apiKey) {
|
|
47
61
|
return {
|
|
48
62
|
success: false,
|
|
@@ -108,7 +122,9 @@ export async function diaryChat({ message, context, systemPrompt }) {
|
|
|
108
122
|
body: JSON.stringify({
|
|
109
123
|
model: model || 'gpt-4o',
|
|
110
124
|
messages,
|
|
111
|
-
|
|
125
|
+
...(model?.startsWith('gpt-5') || model?.startsWith('o1') || model?.startsWith('o3')
|
|
126
|
+
? { max_completion_tokens: 4096 }
|
|
127
|
+
: { max_tokens: 4096 }),
|
|
112
128
|
temperature: 0.7
|
|
113
129
|
})
|
|
114
130
|
})
|
|
@@ -50,6 +50,21 @@ export async function diaryConnect({ apiKey, model, name }) {
|
|
|
50
50
|
|
|
51
51
|
saveConfig(config)
|
|
52
52
|
|
|
53
|
+
// Push to server so dashboard stays in sync
|
|
54
|
+
try {
|
|
55
|
+
await fetch('https://indelible.one/api/mcp/settings', {
|
|
56
|
+
method: 'PATCH',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
address: config.address,
|
|
60
|
+
diary_model: config.diary.model,
|
|
61
|
+
diary_name: config.diary.name,
|
|
62
|
+
diary_connected: true,
|
|
63
|
+
diary_enabled: true
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
} catch { /* server sync is best-effort */ }
|
|
67
|
+
|
|
53
68
|
return {
|
|
54
69
|
success: true,
|
|
55
70
|
message: `Diary AI connected! Using ${config.diary.model} as "${config.diary.name}"`,
|
|
@@ -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)
|
|
@@ -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,31 @@ 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
|
+
try {
|
|
434
|
+
const tierResult = await checkProTier(config.address)
|
|
435
|
+
richMode = tierResult.ok === true
|
|
436
|
+
} catch { /* offline or error — stay text-only */ }
|
|
437
|
+
|
|
425
438
|
// Parse transcript
|
|
426
|
-
const allMessages = parseTranscript(actualPath)
|
|
439
|
+
const allMessages = parseTranscript(actualPath, richMode)
|
|
427
440
|
if (allMessages.length === 0) {
|
|
428
441
|
return { success: false, error: 'No messages found in transcript' }
|
|
429
442
|
}
|
|
430
443
|
|
|
444
|
+
// Session size cap — if rich mode produces >8MB, fall back to text-only
|
|
445
|
+
const MAX_SESSION_BYTES = 8 * 1024 * 1024
|
|
446
|
+
if (richMode) {
|
|
447
|
+
const estimatedSize = JSON.stringify(allMessages).length
|
|
448
|
+
if (estimatedSize > MAX_SESSION_BYTES) {
|
|
449
|
+
process.stderr.write(`[indelible] Rich session too large (${(estimatedSize / 1024 / 1024).toFixed(1)} MB) — falling back to text-only\n`)
|
|
450
|
+
richMode = false
|
|
451
|
+
allMessages.length = 0
|
|
452
|
+
allMessages.push(...parseTranscript(actualPath, false))
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
431
456
|
// Append recent plan files (modified in last 24h) — plans are context worth saving
|
|
432
457
|
const plans = getRecentPlans()
|
|
433
458
|
if (plans.length > 0) {
|
|
@@ -460,6 +485,8 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
460
485
|
const session = {
|
|
461
486
|
id: sessionId,
|
|
462
487
|
type: isDelta ? 'delta' : 'full',
|
|
488
|
+
schema_version: richMode ? 2 : 1,
|
|
489
|
+
rich_mode: richMode,
|
|
463
490
|
prev_session_id: prevSessionId,
|
|
464
491
|
summary: createSummary(allMessages, summary),
|
|
465
492
|
message_count: allMessages.length,
|
|
@@ -483,7 +510,9 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
483
510
|
if (transcriptPath) ensureJsonlBackup(transcriptPath, allMessages)
|
|
484
511
|
|
|
485
512
|
// Encrypt locally — WIF never leaves
|
|
486
|
-
const
|
|
513
|
+
const sessionJson = JSON.stringify(session)
|
|
514
|
+
process.stderr.write(`[indelible] Saving session (${(sessionJson.length / 1024).toFixed(0)} KB, rich_mode: ${richMode})\n`)
|
|
515
|
+
const encrypted = encrypt(sessionJson, wif)
|
|
487
516
|
|
|
488
517
|
// Build the blockchain payload
|
|
489
518
|
const payload = {
|