indelible-mcp 3.0.0 → 3.1.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.0.0",
3
+ "version": "3.1.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",
package/src/index.js CHANGED
@@ -223,7 +223,7 @@ Commands:
223
223
 
224
224
  function printHelp() {
225
225
  console.log(`
226
- Indelible MCP — Blockchain memory for Claude Code (v3.0.0)
226
+ Indelible MCP — Blockchain memory for Claude Code (v3.1.0)
227
227
 
228
228
  Setup:
229
229
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -324,7 +324,7 @@ function readStdin() {
324
324
 
325
325
  const SERVER_INFO = {
326
326
  name: 'indelible',
327
- version: '3.0.0',
327
+ version: '3.1.0',
328
328
  description: 'Blockchain-backed memory and code storage for Claude Code'
329
329
  }
330
330
 
package/src/lib/spv.js CHANGED
@@ -62,7 +62,21 @@ async function getBridges() {
62
62
  }))
63
63
  }
64
64
 
65
- // No config — use gateway default
65
+ // No config or empty — use gateway default, auto-generate relay key if missing
66
+ if (!migrationDone && config?.address && (!apiKey || !apiKey.startsWith('relay_sk_'))) {
67
+ try {
68
+ const { ensureRelayKey } = await import('./api-client.js')
69
+ const result = await ensureRelayKey(config.address)
70
+ if (result) {
71
+ apiKey = result.key
72
+ config.spv_bridges = [GATEWAY_BRIDGE]
73
+ config.spv_api_key = apiKey
74
+ saveConfig(config)
75
+ }
76
+ } catch { /* backend unreachable */ }
77
+ migrationDone = true
78
+ }
79
+
66
80
  return [{ ...GATEWAY_BRIDGE, apiKey }]
67
81
  }
68
82
 
@@ -8,6 +8,9 @@
8
8
  * 4. Format smart context (LOCAL)
9
9
  */
10
10
 
11
+ import { readFileSync, existsSync } from 'node:fs'
12
+ import { join } from 'node:path'
13
+ import { homedir } from 'node:os'
11
14
  import { loadConfig, getWif } from '../lib/config.js'
12
15
  import { decrypt } from '../lib/crypto.js'
13
16
  import { getSessions } from '../lib/api-client.js'
@@ -159,75 +162,92 @@ export async function loadContext(numSessions = 5) {
159
162
  }
160
163
 
161
164
  try {
162
- // Try server index first (fast), fall back to SPV address scan
163
- let sessions = []
164
-
165
- try {
166
- const result = await getSessions(config.address, numSessions, config)
167
- sessions = result.sessions || []
168
- } catch {
169
- // Server unavailable — scan blockchain directly via SPV bridge
170
- // Only check last 15 txs and stop once we have enough sessions
171
- try {
172
- const history = await spv.getAddressHistory(config.address)
173
- const recent = history.slice(-15).reverse()
174
- for (const tx of recent) {
175
- if (sessions.length >= numSessions) break
176
- try {
177
- const txData = await spv.getTx(tx.tx_hash)
178
- const encrypted = spv.extractEncryptedFromTx(txData)
179
- if (encrypted) {
180
- sessions.push({ txId: tx.tx_hash, encrypted })
181
- }
182
- } catch { continue }
183
- }
184
- } catch { /* SPV scan failed too */ }
185
- }
186
-
187
- if (sessions.length === 0) {
188
- return {
189
- success: true,
190
- context: null,
191
- message: 'No previous sessions found. This is a fresh start!',
192
- sessionCount: 0
165
+ // LOCAL-FIRST: read from hard drive session index (zero network calls)
166
+ const indexPath = join(homedir(), '.indelible', 'session-index.json')
167
+ let decrypted = []
168
+ let source = 'local'
169
+
170
+ if (existsSync(indexPath)) {
171
+ const index = JSON.parse(readFileSync(indexPath, 'utf-8'))
172
+ const recent = index.slice(-numSessions)
173
+ for (const entry of recent) {
174
+ try {
175
+ if (entry.encrypted) {
176
+ const data = JSON.parse(decrypt(entry.encrypted, wif))
177
+ decrypted.push(data)
178
+ }
179
+ } catch { /* skip sessions that fail to decrypt */ }
193
180
  }
194
181
  }
195
182
 
196
- // Decrypt locally WIF never leaves
197
- const decrypted = []
198
- for (const session of sessions) {
183
+ // FALLBACK: if local index empty/missing, try server then SPV (disaster recovery)
184
+ if (decrypted.length === 0) {
185
+ source = 'blockchain'
186
+ let sessions = []
187
+
199
188
  try {
200
- if (session.encrypted) {
201
- const data = JSON.parse(decrypt(session.encrypted, wif))
202
- decrypted.push(data)
203
- } else if (session.txId) {
204
- const encrypted = await fetchEncryptedFromChain(session.txId)
205
- if (encrypted) {
206
- const data = JSON.parse(decrypt(encrypted, wif))
207
- decrypted.push(data)
189
+ const result = await getSessions(config.address, numSessions, config)
190
+ sessions = result.sessions || []
191
+ } catch {
192
+ try {
193
+ const history = await spv.getAddressHistory(config.address)
194
+ const recent = history.slice(-15).reverse()
195
+ for (const tx of recent) {
196
+ if (sessions.length >= numSessions) break
197
+ try {
198
+ const txData = await spv.getTx(tx.tx_hash)
199
+ const encrypted = spv.extractEncryptedFromTx(txData)
200
+ if (encrypted) {
201
+ sessions.push({ txId: tx.tx_hash, encrypted })
202
+ }
203
+ } catch { continue }
208
204
  }
205
+ } catch { /* SPV scan failed too */ }
206
+ }
207
+
208
+ if (sessions.length === 0) {
209
+ return {
210
+ success: true,
211
+ context: null,
212
+ message: 'No previous sessions found. This is a fresh start!',
213
+ sessionCount: 0
209
214
  }
210
- } catch { /* skip sessions that fail to decrypt */ }
211
- }
215
+ }
212
216
 
213
- if (decrypted.length === 0) {
214
- const contextParts = [
215
- '# Restored from Blockchain Memory',
216
- `Found ${sessions.length} session(s) but could not decrypt. Showing metadata only.`,
217
- ''
218
- ]
219
217
  for (const session of sessions) {
220
- const date = session.timestamp ? new Date(session.timestamp).toLocaleDateString() : 'Unknown'
221
- contextParts.push(`## Session: ${date}`)
222
- contextParts.push(`Summary: ${session.summary || 'N/A'}`)
223
- contextParts.push(`Messages: ${session.message_count || 'N/A'}`)
224
- contextParts.push('')
218
+ try {
219
+ if (session.encrypted) {
220
+ const data = JSON.parse(decrypt(session.encrypted, wif))
221
+ decrypted.push(data)
222
+ } else if (session.txId) {
223
+ const encrypted = await fetchEncryptedFromChain(session.txId)
224
+ if (encrypted) {
225
+ const data = JSON.parse(decrypt(encrypted, wif))
226
+ decrypted.push(data)
227
+ }
228
+ }
229
+ } catch { /* skip sessions that fail to decrypt */ }
225
230
  }
226
- return {
227
- success: true,
228
- context: contextParts.join('\n'),
229
- sessionCount: sessions.length,
230
- message: `Loaded ${sessions.length} session metadata from blockchain.`
231
+
232
+ if (decrypted.length === 0) {
233
+ const contextParts = [
234
+ '# Restored from Blockchain Memory',
235
+ `Found ${sessions.length} session(s) but could not decrypt. Showing metadata only.`,
236
+ ''
237
+ ]
238
+ for (const session of sessions) {
239
+ const date = session.timestamp ? new Date(session.timestamp).toLocaleDateString() : 'Unknown'
240
+ contextParts.push(`## Session: ${date}`)
241
+ contextParts.push(`Summary: ${session.summary || 'N/A'}`)
242
+ contextParts.push(`Messages: ${session.message_count || 'N/A'}`)
243
+ contextParts.push('')
244
+ }
245
+ return {
246
+ success: true,
247
+ context: contextParts.join('\n'),
248
+ sessionCount: sessions.length,
249
+ message: `Loaded ${sessions.length} session metadata from blockchain.`
250
+ }
231
251
  }
232
252
  }
233
253
 
@@ -268,7 +288,7 @@ export async function loadContext(numSessions = 5) {
268
288
  date: s.created_at,
269
289
  messageCount: s.message_count
270
290
  })),
271
- message: `Smart restore: ${merged.length} conversation(s) from ${decrypted.length} blockchain chunks.`
291
+ message: `Smart restore (${source}): ${merged.length} conversation(s) from ${decrypted.length} chunks.`
272
292
  }
273
293
  } catch (error) {
274
294
  return { success: false, error: `Failed to load context: ${error.message}`, context: null }
@@ -146,24 +146,6 @@ export async function saveProject(dirPath, options = {}) {
146
146
  })
147
147
  saveConfig({ ...config, project_txids: projects })
148
148
 
149
- // Index in server Redis so Vault UI can see it
150
- try {
151
- const apiUrl = config.api_url || 'https://indelible.one'
152
- await fetch(`${apiUrl}/api/files/project/index`, {
153
- method: 'POST',
154
- headers: { 'Content-Type': 'application/json' },
155
- body: JSON.stringify({
156
- address: config.address,
157
- txId,
158
- name: projectName,
159
- fileCount: allFiles.length,
160
- totalSize,
161
- timestamp,
162
- bundle: true
163
- })
164
- })
165
- } catch { /* don't block save on index failure */ }
166
-
167
149
  return {
168
150
  success: true,
169
151
  txId,
@@ -348,6 +348,27 @@ export async function saveSession(transcriptPath, summary) {
348
348
  messagesToCommit.length, finalTxId, structuredCtx
349
349
  )
350
350
 
351
+ // Append to local session index (hard drive = read cache)
352
+ try {
353
+ const indexPath = join(homedir(), '.indelible', 'session-index.json')
354
+ let index = []
355
+ if (existsSync(indexPath)) {
356
+ index = JSON.parse(readFileSync(indexPath, 'utf-8'))
357
+ }
358
+ index.push({
359
+ txId: finalTxId,
360
+ sessionId,
361
+ prevTxId: config.last_tx_id || null,
362
+ summary: session.summary,
363
+ messageCount: allMessages.length,
364
+ saveType: isDelta ? 'delta' : 'full',
365
+ newMessages: messagesToCommit.length,
366
+ encrypted,
367
+ timestamp: session.created_at
368
+ })
369
+ writeFileSync(indexPath, JSON.stringify(index, null, 2))
370
+ } catch { /* don't block on index failure */ }
371
+
351
372
  // Save memory files to blockchain via vault pipeline (UTXO chaining)
352
373
  let memoryTxId = config.memory_file_txid || null
353
374
  let historyTxId = config.session_history_txid || null