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 +1 -1
- package/src/index.js +2 -2
- package/src/lib/spv.js +15 -1
- package/src/tools/load_context.js +81 -61
- package/src/tools/save_project.js +0 -18
- package/src/tools/save_session.js +21 -0
package/package.json
CHANGED
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.
|
|
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.
|
|
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
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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}
|
|
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
|