indelible-mcp 2.8.2 → 2.9.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/tools/save_session.js +174 -6
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 (v2.
|
|
226
|
+
Indelible MCP — Blockchain memory for Claude Code (v2.9.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: '2.
|
|
327
|
+
version: '2.9.0',
|
|
328
328
|
description: 'Blockchain-backed memory and code storage for Claude Code'
|
|
329
329
|
}
|
|
330
330
|
|
|
@@ -16,11 +16,22 @@
|
|
|
16
16
|
import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync, readdirSync } from 'node:fs'
|
|
17
17
|
import { dirname, join } from 'node:path'
|
|
18
18
|
import { homedir } from 'node:os'
|
|
19
|
+
import { execSync } from 'node:child_process'
|
|
19
20
|
import { loadConfig, saveConfig, getWif } from '../lib/config.js'
|
|
20
21
|
import { encrypt, sha256 } from '../lib/crypto.js'
|
|
21
22
|
import { findCurrentTranscript, parseTranscript } from '../lib/transcript.js'
|
|
22
23
|
import * as spv from '../lib/spv.js'
|
|
23
24
|
import { indexSession } from '../lib/api-client.js'
|
|
25
|
+
import { saveFile } from './save_file.js'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Derive memory directory from transcript path.
|
|
29
|
+
* Transcript lives at ~/.claude/projects/<slug>/UUID.jsonl
|
|
30
|
+
* Memory dir is ~/.claude/projects/<slug>/memory/
|
|
31
|
+
*/
|
|
32
|
+
function getMemoryDir(transcriptPath) {
|
|
33
|
+
return join(dirname(transcriptPath), 'memory')
|
|
34
|
+
}
|
|
24
35
|
|
|
25
36
|
function createSummary(messages, userSummary) {
|
|
26
37
|
if (userSummary) return userSummary
|
|
@@ -34,9 +45,21 @@ function createSummary(messages, userSummary) {
|
|
|
34
45
|
return 'Claude Code session'
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Append session entry to diary file (local backup).
|
|
50
|
+
* Searches for *DIARY*.md in the project working directory.
|
|
51
|
+
* If no diary file exists, skips silently.
|
|
52
|
+
*/
|
|
53
|
+
function updateDiary(summary, timestamp, transcriptPath) {
|
|
38
54
|
try {
|
|
39
|
-
|
|
55
|
+
// Find diary file dynamically — look in project root (2 levels up from transcript)
|
|
56
|
+
const projectDir = dirname(dirname(transcriptPath))
|
|
57
|
+
let files
|
|
58
|
+
try { files = readdirSync(projectDir) } catch { return }
|
|
59
|
+
const diaryFile = files.find(f => /diary/i.test(f) && f.endsWith('.md'))
|
|
60
|
+
if (!diaryFile) return
|
|
61
|
+
const diaryPath = join(projectDir, diaryFile)
|
|
62
|
+
if (!existsSync(diaryPath)) return
|
|
40
63
|
const diary = readFileSync(diaryPath, 'utf-8')
|
|
41
64
|
const date = new Date(timestamp)
|
|
42
65
|
const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
@@ -53,6 +76,99 @@ function updateDiary(diaryPath, summary, timestamp) {
|
|
|
53
76
|
} catch { /* don't block */ }
|
|
54
77
|
}
|
|
55
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Update MEMORY.md with latest session info (local backup).
|
|
81
|
+
* Derives memory path from transcript path — works for any user.
|
|
82
|
+
* Also scans recent git commits to auto-update feature statuses.
|
|
83
|
+
*/
|
|
84
|
+
function updateMemory(summary, timestamp, messageCount, transcriptPath) {
|
|
85
|
+
try {
|
|
86
|
+
const memoryDir = getMemoryDir(transcriptPath)
|
|
87
|
+
const memoryPath = join(memoryDir, 'MEMORY.md')
|
|
88
|
+
if (!existsSync(memoryPath)) return
|
|
89
|
+
let memory = readFileSync(memoryPath, 'utf-8')
|
|
90
|
+
const date = new Date(timestamp)
|
|
91
|
+
const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
92
|
+
const timeStr = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })
|
|
93
|
+
// Update or insert last session tracking block
|
|
94
|
+
const sessionBlock = `## Last Session\n- **Date:** ${dateStr} @ ${timeStr}\n- **Summary:** ${summary}\n- **Messages:** ${messageCount}`
|
|
95
|
+
const marker = '## Last Session'
|
|
96
|
+
const idx = memory.indexOf(marker)
|
|
97
|
+
if (idx !== -1) {
|
|
98
|
+
const nextHeading = memory.indexOf('\n## ', idx + marker.length)
|
|
99
|
+
const end = nextHeading !== -1 ? nextHeading : memory.length
|
|
100
|
+
memory = memory.slice(0, idx) + sessionBlock + memory.slice(end)
|
|
101
|
+
} else {
|
|
102
|
+
const firstNewline = memory.indexOf('\n')
|
|
103
|
+
memory = memory.slice(0, firstNewline + 1) + '\n' + sessionBlock + '\n' + memory.slice(firstNewline + 1)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Git commit scanning — auto-update feature statuses
|
|
107
|
+
try {
|
|
108
|
+
const cwd = process.cwd()
|
|
109
|
+
if (existsSync(join(cwd, '.git'))) {
|
|
110
|
+
const gitLog = execSync('git log --oneline -10', { cwd, encoding: 'utf-8', timeout: 5000 })
|
|
111
|
+
const commits = gitLog.trim().split('\n').map(l => l.toLowerCase())
|
|
112
|
+
|
|
113
|
+
const doneKeywords = ['complete', 'done', 'finish', 'fix', 'deploy', 'ship', 'release']
|
|
114
|
+
const sectionRegex = /^## (.+?)(?:\s*\(.*?IN PROGRESS.*?\))/gm
|
|
115
|
+
let match
|
|
116
|
+
while ((match = sectionRegex.exec(memory)) !== null) {
|
|
117
|
+
const sectionName = match[1].toLowerCase().trim()
|
|
118
|
+
const sectionWords = sectionName.split(/[\s—\-:]+/).filter(w => w.length > 2)
|
|
119
|
+
const hasMatch = commits.some(commit =>
|
|
120
|
+
sectionWords.some(word => commit.includes(word)) &&
|
|
121
|
+
doneKeywords.some(word => commit.includes(word))
|
|
122
|
+
)
|
|
123
|
+
if (hasMatch) {
|
|
124
|
+
memory = memory.replace(match[0], match[0].replace('IN PROGRESS', 'COMPLETE'))
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch { /* git scan is best-effort */ }
|
|
129
|
+
|
|
130
|
+
// Line count warning
|
|
131
|
+
const lineCount = memory.split('\n').length
|
|
132
|
+
if (lineCount > 195) {
|
|
133
|
+
process.stderr.write(`[MCP] Warning: MEMORY.md is ${lineCount} lines (limit: 200). Consider trimming.\n`)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
writeFileSync(memoryPath, memory)
|
|
137
|
+
} catch { /* don't block save on memory failure */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Append a timestamped session recap to session-history.md.
|
|
142
|
+
* Full rolling log — no line limit. Creates file if it doesn't exist.
|
|
143
|
+
*/
|
|
144
|
+
function updateSessionHistory(transcriptPath, summary, timestamp, messageCount, saveType, newMessages, txId, structuredCtx) {
|
|
145
|
+
try {
|
|
146
|
+
const memoryDir = getMemoryDir(transcriptPath)
|
|
147
|
+
if (!existsSync(memoryDir)) mkdirSync(memoryDir, { recursive: true })
|
|
148
|
+
const historyPath = join(memoryDir, 'session-history.md')
|
|
149
|
+
|
|
150
|
+
const date = new Date(timestamp)
|
|
151
|
+
const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
152
|
+
const timeStr = date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })
|
|
153
|
+
|
|
154
|
+
const lines = [`### ${dateStr} @ ${timeStr}`]
|
|
155
|
+
lines.push(`- **Summary:** ${summary}`)
|
|
156
|
+
lines.push(`- **Messages:** ${messageCount}${newMessages ? ` (${saveType}: ${newMessages} new)` : ''}`)
|
|
157
|
+
if (structuredCtx?.git_branch) lines.push(`- **Git branch:** ${structuredCtx.git_branch}`)
|
|
158
|
+
if (txId) lines.push(`- **Tx:** ${txId.slice(0, 12)}...`)
|
|
159
|
+
lines.push('')
|
|
160
|
+
|
|
161
|
+
let existing = ''
|
|
162
|
+
if (existsSync(historyPath)) {
|
|
163
|
+
existing = readFileSync(historyPath, 'utf-8')
|
|
164
|
+
} else {
|
|
165
|
+
existing = '# Session History\n\nTimestamped session recaps — full rolling log.\n\n'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
writeFileSync(historyPath, existing + lines.join('\n'))
|
|
169
|
+
} catch { /* don't block save on history failure */ }
|
|
170
|
+
}
|
|
171
|
+
|
|
56
172
|
function ensureJsonlBackup(path, messages) {
|
|
57
173
|
try {
|
|
58
174
|
const dir = dirname(path)
|
|
@@ -180,7 +296,8 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
180
296
|
} catch { /* don't block save on structured context failure */ }
|
|
181
297
|
|
|
182
298
|
// Local backups (fire-and-forget)
|
|
183
|
-
|
|
299
|
+
updateDiary(session.summary, session.created_at, actualPath)
|
|
300
|
+
updateMemory(session.summary, session.created_at, allMessages.length, actualPath)
|
|
184
301
|
if (transcriptPath) ensureJsonlBackup(transcriptPath, allMessages)
|
|
185
302
|
|
|
186
303
|
// Encrypt locally — WIF never leaves
|
|
@@ -202,7 +319,7 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
202
319
|
// Build + sign transaction LOCALLY with @bsv/sdk
|
|
203
320
|
try {
|
|
204
321
|
const utxos = await spv.getUtxos(config.address)
|
|
205
|
-
const { txHex, txId } = await spv.
|
|
322
|
+
const { txHex, txId, changeUtxos } = await spv.buildOpReturnTxWithChange(wif, utxos, JSON.stringify(payload))
|
|
206
323
|
|
|
207
324
|
// Broadcast via SPV bridge (gated by API key)
|
|
208
325
|
const broadcastResult = await spv.broadcastTx(txHex)
|
|
@@ -223,18 +340,67 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
223
340
|
}, config)
|
|
224
341
|
} catch { /* indexing failure doesn't block save */ }
|
|
225
342
|
|
|
343
|
+
// Update session history with txId now that we have it
|
|
344
|
+
const structuredCtx = session.structured_context || {}
|
|
345
|
+
updateSessionHistory(
|
|
346
|
+
actualPath, session.summary, session.created_at,
|
|
347
|
+
allMessages.length, isDelta ? 'delta' : 'full',
|
|
348
|
+
messagesToCommit.length, finalTxId, structuredCtx
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
// Save memory files to blockchain via vault pipeline (UTXO chaining)
|
|
352
|
+
let memoryTxId = config.memory_file_txid || null
|
|
353
|
+
let historyTxId = config.session_history_txid || null
|
|
354
|
+
let memChangeUtxos = changeUtxos || []
|
|
355
|
+
|
|
356
|
+
if (config.memory_auto_save !== false && memChangeUtxos.length > 0) {
|
|
357
|
+
try {
|
|
358
|
+
const memoryDir = getMemoryDir(actualPath)
|
|
359
|
+
const memoryPath = join(memoryDir, 'MEMORY.md')
|
|
360
|
+
const historyPath = join(memoryDir, 'session-history.md')
|
|
361
|
+
|
|
362
|
+
// Save MEMORY.md to chain
|
|
363
|
+
if (existsSync(memoryPath)) {
|
|
364
|
+
const memResult = await saveFile(memoryPath, {
|
|
365
|
+
relativePath: 'memory/MEMORY.md',
|
|
366
|
+
utxos: memChangeUtxos
|
|
367
|
+
})
|
|
368
|
+
if (memResult.success) {
|
|
369
|
+
memoryTxId = memResult.txId
|
|
370
|
+
memChangeUtxos = memResult.changeUtxos || []
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Save session-history.md to chain (using change from MEMORY.md tx)
|
|
375
|
+
if (existsSync(historyPath) && memChangeUtxos.length > 0) {
|
|
376
|
+
const histResult = await saveFile(historyPath, {
|
|
377
|
+
relativePath: 'memory/session-history.md',
|
|
378
|
+
utxos: memChangeUtxos
|
|
379
|
+
})
|
|
380
|
+
if (histResult.success) {
|
|
381
|
+
historyTxId = histResult.txId
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
// Memory file saves are best-effort — session save already succeeded
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
226
389
|
// Update config for next delta
|
|
227
390
|
saveConfig({
|
|
228
391
|
...config,
|
|
229
392
|
last_session_id: sessionId,
|
|
230
393
|
last_tx_id: finalTxId,
|
|
231
394
|
last_saved_message_count: allMessages.length,
|
|
232
|
-
last_saved_transcript: actualPath
|
|
395
|
+
last_saved_transcript: actualPath,
|
|
396
|
+
memory_file_txid: memoryTxId,
|
|
397
|
+
session_history_txid: historyTxId
|
|
233
398
|
})
|
|
234
399
|
|
|
235
400
|
const deltaInfo = isDelta
|
|
236
401
|
? ` (delta: ${messagesToCommit.length} new, ${allMessages.length} total)`
|
|
237
402
|
: ` (full: ${allMessages.length} messages)`
|
|
403
|
+
const memInfo = memoryTxId ? ' Memory files saved.' : ''
|
|
238
404
|
|
|
239
405
|
return {
|
|
240
406
|
success: true,
|
|
@@ -245,7 +411,9 @@ export async function saveSession(transcriptPath, summary) {
|
|
|
245
411
|
newMessages: messagesToCommit.length,
|
|
246
412
|
saveType: isDelta ? 'delta' : 'full',
|
|
247
413
|
summary: session.summary,
|
|
248
|
-
|
|
414
|
+
memoryTxId,
|
|
415
|
+
historyTxId,
|
|
416
|
+
message: `Session saved!${deltaInfo} committed to blockchain.${prevSessionId ? ' Linked to previous session.' : ' (First session)'}${memInfo}`
|
|
249
417
|
}
|
|
250
418
|
} catch (error) {
|
|
251
419
|
return { success: false, error: `Failed to commit: ${error.message}` }
|