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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "3.4.0",
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.4.0)
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.4.0',
469
+ version: '3.6.0',
470
470
  description: 'Blockchain-backed memory and code storage for Claude Code'
471
471
  }
472
472
 
@@ -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 — extracts user/assistant text messages only.
51
- * Filters out tool_use, tool_result, system entries, IDE selection context.
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 (text) {
80
- messages.push({
81
- role: 'user',
82
- content: text,
83
- timestamp: entry.timestamp || new Date().toISOString()
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 (text) {
100
- messages.push({
101
- role: 'assistant',
102
- content: text,
103
- timestamp: entry.timestamp || new Date().toISOString()
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 */ }
@@ -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
- max_tokens: 4096,
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 = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
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 = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.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
- const preview = typeof firstUser.content === 'string'
44
- ? firstUser.content.slice(0, 200)
45
- : JSON.stringify(firstUser.content).slice(0, 200)
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 => JSON.stringify({
316
- type: m.role === 'user' ? 'user' : 'assistant',
317
- message: { content: [{ type: 'text', text: m.content }] },
318
- timestamp: m.timestamp
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 encrypted = encrypt(JSON.stringify(session), wif)
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 = {