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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "2.8.2",
3
+ "version": "2.9.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 (v2.8.2)
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.8.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
- function updateDiary(diaryPath, summary, timestamp) {
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
- if (!diaryPath || !existsSync(diaryPath)) return
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
- if (config.diary_path) updateDiary(config.diary_path, session.summary, session.created_at)
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.buildOpReturnTx(wif, utxos, JSON.stringify(payload))
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
- message: `Session saved!${deltaInfo} committed to blockchain.${prevSessionId ? ' Linked to previous session.' : ' (First session)'}`
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}` }