indelible-mcp 2.3.0 → 2.4.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.3.0",
3
+ "version": "2.4.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
@@ -24,6 +24,9 @@ import { saveFile } from './tools/save_file.js'
24
24
  import { saveProject } from './tools/save_project.js'
25
25
  import { loadFile } from './tools/load_file.js'
26
26
  import { loadProject } from './tools/load_project.js'
27
+ import { saveStyle, saveStyleFromFile } from './tools/save_style.js'
28
+ import { loadStyle } from './tools/load_style.js'
29
+ import { updateVaultIndex } from './tools/update_vault_index.js'
27
30
 
28
31
  const CONTEXT_FILE = join(homedir(), '.indelible', 'indelible-context.jsonl')
29
32
 
@@ -162,15 +165,39 @@ async function runCli(args) {
162
165
  const outputDir = outArg ? outArg.split('=')[1] : undefined
163
166
  const result = await loadProject(txId, { outputDir })
164
167
  console.log(JSON.stringify(result, null, 2))
168
+ } else if (subCmd === 'save-style') {
169
+ const filePath = args[2]
170
+ const nameArg = args.find(a => a.startsWith('--name='))
171
+ const descArg = args.find(a => a.startsWith('--desc='))
172
+ const styleName = nameArg ? nameArg.split('=')[1] : 'default'
173
+ const description = descArg ? descArg.split('=')[1] : ''
174
+ let result
175
+ if (filePath) {
176
+ result = await saveStyleFromFile(filePath, styleName, description)
177
+ } else {
178
+ console.error('Usage: indelible-mcp vault save-style <file> [--name=NAME] [--desc=DESC]')
179
+ break
180
+ }
181
+ console.log(JSON.stringify(result, null, 2))
182
+ } else if (subCmd === 'load-style') {
183
+ const txId = args[2]
184
+ const result = await loadStyle(txId)
185
+ console.log(JSON.stringify(result, null, 2))
186
+ } else if (subCmd === 'update-index') {
187
+ const result = await updateVaultIndex()
188
+ console.log(JSON.stringify(result, null, 2))
165
189
  } else {
166
190
  console.log(`
167
191
  Code Vault — Encrypted code storage on BSV blockchain
168
192
 
169
193
  Commands:
170
- vault save-file <path> Save a file to blockchain
171
- vault save-project <dir> [--name=NAME] Save a project directory to blockchain
172
- vault load-file <txid> [--output=path] Load a file from blockchain
173
- vault load-project <txid> [--output-dir=path] Load a project from blockchain
194
+ vault save-file <path> Save a file to blockchain
195
+ vault save-project <dir> [--name=NAME] Save a project directory
196
+ vault load-file <txid> [--output=path] Load a file from blockchain
197
+ vault load-project <txid> [--output-dir=path] Load a project from blockchain
198
+ vault save-style <file> [--name=N] [--desc=D] Save an AI style to blockchain
199
+ vault load-style [txid] Load an AI style from blockchain
200
+ vault update-index Update on-chain vault index
174
201
  `)
175
202
  }
176
203
  break
@@ -193,26 +220,34 @@ Commands:
193
220
 
194
221
  function printHelp() {
195
222
  console.log(`
196
- Indelible MCP — Blockchain memory for Claude Code
197
-
198
- Usage:
199
- indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
200
- indelible-mcp save Save current session to blockchain
201
- indelible-mcp save --summary XYZ Save with custom summary
202
- indelible-mcp load Load context from blockchain
203
- indelible-mcp load --sessions=5 Load N sessions
204
- indelible-mcp status Show account status
205
- indelible-mcp install-hooks Install auto-save/restore hooks into Claude Code
206
- indelible-mcp hook pre-compact Pre-compaction save hook
207
- indelible-mcp hook post-compact Post-compaction restore hook
223
+ Indelible MCP — Blockchain memory for Claude Code (v2.4.0)
208
224
 
209
- Code Vault:
210
- indelible-mcp vault save-file <path> Save a file to blockchain
211
- indelible-mcp vault save-project <dir> Save a project to blockchain
212
- indelible-mcp vault load-file <txid> Load a file from blockchain
213
- indelible-mcp vault load-project <txid> Load a project from blockchain
225
+ Setup:
226
+ indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
227
+ indelible-mcp install-hooks Install auto-save/restore hooks
228
+ indelible-mcp status Show account status
229
+ indelible-mcp --show-config Show MCP config JSON
214
230
 
215
- Get your key: Sign in at indelible.one with Google → Settings → Private Key
231
+ Sessions:
232
+ indelible-mcp save Save current session to blockchain
233
+ indelible-mcp save --summary "my note" Save with custom summary
234
+ indelible-mcp load Load context from blockchain
235
+ indelible-mcp load --sessions=10 Load N sessions
236
+
237
+ Code Vault:
238
+ indelible-mcp vault save-file <path> Save a file
239
+ indelible-mcp vault save-project <dir> [--name=NAME] Save a project
240
+ indelible-mcp vault load-file <txid> [--output=path] Load a file
241
+ indelible-mcp vault load-project <txid> [--output-dir=dir] Load a project
242
+ indelible-mcp vault save-style <file> [--name=N] [--desc=D] Save an AI style
243
+ indelible-mcp vault load-style [txid] Load an AI style
244
+ indelible-mcp vault update-index Update vault index
245
+
246
+ Hooks (auto-called by Claude Code):
247
+ indelible-mcp hook pre-compact Auto-save before compaction
248
+ indelible-mcp hook post-compact Auto-restore after compaction
249
+
250
+ Get your key: Sign in at indelible.one → Settings → Private Key
216
251
  Learn more: https://indelible.one
217
252
  `)
218
253
  }
@@ -281,8 +316,8 @@ function readStdin() {
281
316
 
282
317
  const SERVER_INFO = {
283
318
  name: 'indelible',
284
- version: '2.0.0',
285
- description: 'Blockchain-backed memory for Claude Code'
319
+ version: '2.4.0',
320
+ description: 'Blockchain-backed memory and code storage for Claude Code'
286
321
  }
287
322
 
288
323
  const TOOLS = [
@@ -321,6 +356,30 @@ const TOOLS = [
321
356
  },
322
357
  required: []
323
358
  }
359
+ },
360
+ {
361
+ name: 'save_style',
362
+ description: 'Save an AI interaction style (rules/preferences) to blockchain. Encrypted with your key.',
363
+ inputSchema: {
364
+ type: 'object',
365
+ properties: {
366
+ rules_text: { type: 'string', description: 'The full rules/style text (markdown)' },
367
+ style_name: { type: 'string', description: 'Short name for this style' },
368
+ description: { type: 'string', description: 'One-line description' }
369
+ },
370
+ required: ['rules_text', 'style_name']
371
+ }
372
+ },
373
+ {
374
+ name: 'load_style',
375
+ description: 'Load an AI interaction style from blockchain. Returns rules text and metadata.',
376
+ inputSchema: {
377
+ type: 'object',
378
+ properties: {
379
+ txid: { type: 'string', description: 'Transaction ID of the style. If omitted, loads last saved style.' }
380
+ },
381
+ required: []
382
+ }
324
383
  }
325
384
  ]
326
385
 
@@ -356,6 +415,12 @@ async function handleMcpRequest(request) {
356
415
  case 'load_context':
357
416
  result = await loadContext(args?.num_sessions)
358
417
  break
418
+ case 'save_style':
419
+ result = await saveStyle(args?.rules_text, args?.style_name, args?.description)
420
+ break
421
+ case 'load_style':
422
+ result = await loadStyle(args?.txid)
423
+ break
359
424
  default:
360
425
  throw new Error(`Unknown tool: ${name}`)
361
426
  }
package/src/lib/spv.js CHANGED
@@ -10,7 +10,12 @@
10
10
  import { Transaction, P2PKH, PrivateKey, SatoshisPerKilobyte, LockingScript, OP } from '@bsv/sdk'
11
11
  import { loadConfig } from './config.js'
12
12
 
13
- const DEFAULT_SPV_URL = 'http://107.191.49.18:8080'
13
+ const DEFAULT_SPV_BRIDGES = [
14
+ { url: 'http://45.76.19.199:8080', name: 'SPV-NODE-THREE' },
15
+ { url: 'http://144.202.50.135:8080', name: 'SPV-RELAY-FOUR' },
16
+ { url: 'http://155.138.254.224:8080', name: 'BSV SPV Bridge' },
17
+ { url: 'http://107.191.49.18:8080', name: 'spv-bridge-2' },
18
+ ]
14
19
 
15
20
  function getBridges() {
16
21
  const config = loadConfig()
@@ -24,8 +29,7 @@ function getBridges() {
24
29
  }))
25
30
  }
26
31
 
27
- const url = config?.spv_url || DEFAULT_SPV_URL
28
- return [{ url, name: url, apiKey }]
32
+ return DEFAULT_SPV_BRIDGES.map(b => ({ ...b, apiKey }))
29
33
  }
30
34
 
31
35
  async function spvFetch(path, options = {}) {
@@ -0,0 +1,106 @@
1
+ /**
2
+ * load_style tool
3
+ * Load an AI interaction style from blockchain by txid.
4
+ * Protocol: INDELIBLE_STYLE_V1
5
+ *
6
+ * Fetches the tx, decrypts with the owner's WIF, returns the rules text.
7
+ * Used by post-compact hook to reload rules after context compaction.
8
+ */
9
+
10
+ import { loadConfig, getWif } from '../lib/config.js'
11
+ import { decrypt } from '../lib/crypto.js'
12
+ import * as spv from '../lib/spv.js'
13
+
14
+ /**
15
+ * Fetch and extract encrypted style data from a blockchain tx.
16
+ * Tries SPV bridge first, falls back to WhatsOnChain.
17
+ *
18
+ * @param {string} txId - Transaction ID
19
+ * @returns {Promise<string|null>} Encrypted data string or null
20
+ */
21
+ async function fetchStyleFromChain(txId) {
22
+ const cleanTxId = txId.trim()
23
+
24
+ // Try SPV bridge first
25
+ try {
26
+ const tx = await spv.getTx(cleanTxId)
27
+ const encrypted = spv.extractEncryptedFromTx(tx)
28
+ if (encrypted) return encrypted
29
+ } catch {
30
+ // SPV bridge failed, try WhatsOnChain
31
+ }
32
+
33
+ // Fallback: WhatsOnChain — parse OP_RETURN for JSON payload
34
+ try {
35
+ const res = await fetch(`https://api.whatsonchain.com/v1/bsv/main/tx/${cleanTxId}`)
36
+ if (!res.ok) return null
37
+ const tx = await res.json()
38
+ const opReturn = tx.vout?.find(out => out.value === 0)
39
+ if (!opReturn) return null
40
+ const hex = opReturn.scriptPubKey?.hex
41
+ if (!hex) return null
42
+ const str = Buffer.from(hex, 'hex').toString('utf8')
43
+
44
+ // Try to parse as JSON payload (style format has encrypted field inside JSON)
45
+ try {
46
+ // Find the JSON object in the OP_RETURN data
47
+ const jsonStart = str.indexOf('{')
48
+ if (jsonStart === -1) return null
49
+ const json = JSON.parse(str.slice(jsonStart))
50
+ if (json.protocol === 'indelible.style' && json.encrypted) {
51
+ return json.encrypted
52
+ }
53
+ } catch {
54
+ // Not JSON format, try raw encrypted format (iv:tag:ciphertext)
55
+ const match = str.match(/([A-Za-z0-9+/=]{12,}):([A-Za-z0-9+/=]{20,}):([A-Za-z0-9+/=]{20,})/)
56
+ return match ? match[0] : null
57
+ }
58
+ } catch {
59
+ return null
60
+ }
61
+
62
+ return null
63
+ }
64
+
65
+ /**
66
+ * Load a style from blockchain.
67
+ *
68
+ * @param {string} [txId] - Transaction ID. If omitted, reads from config.style_txid
69
+ * @returns {Promise<Object>} Result with rules text and metadata
70
+ */
71
+ export async function loadStyle(txId) {
72
+ const config = await loadConfig()
73
+ const wif = await getWif()
74
+ if (!wif) {
75
+ return { success: false, error: 'Wallet not configured or PIN incorrect.' }
76
+ }
77
+
78
+ // Use provided txId or fall back to config
79
+ const styleTxId = txId || config.style_txid
80
+ if (!styleTxId) {
81
+ return { success: false, error: 'No style txId provided and none saved in config.' }
82
+ }
83
+
84
+ try {
85
+ const encrypted = await fetchStyleFromChain(styleTxId)
86
+ if (!encrypted) {
87
+ return { success: false, error: `Could not fetch style from tx: ${styleTxId}` }
88
+ }
89
+
90
+ const decrypted = JSON.parse(decrypt(encrypted, wif))
91
+
92
+ return {
93
+ success: true,
94
+ txId: styleTxId,
95
+ styleId: decrypted.id,
96
+ name: decrypted.name,
97
+ description: decrypted.description,
98
+ owner: decrypted.owner,
99
+ createdAt: decrypted.created_at,
100
+ rules: decrypted.rules,
101
+ message: `Style "${decrypted.name}" loaded from blockchain.`
102
+ }
103
+ } catch (error) {
104
+ return { success: false, error: `Failed to load style: ${error.message}` }
105
+ }
106
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * save_style tool
3
+ * Save an AI interaction style (rules/preferences) to blockchain.
4
+ * Protocol: INDELIBLE_STYLE_V1
5
+ *
6
+ * Styles are separate from session data — they define HOW the AI behaves,
7
+ * not WHAT was discussed. Owned by the user's WIF, retrievable by txid.
8
+ *
9
+ * Uses same encryption (AES-256-GCM) and broadcast (SPV bridge) as sessions.
10
+ */
11
+
12
+ import { readFile } from 'fs/promises'
13
+ import { existsSync } from 'fs'
14
+ import { loadConfig, saveConfig, getWif } from '../lib/config.js'
15
+ import { encrypt, sha256 } from '../lib/crypto.js'
16
+ import * as spv from '../lib/spv.js'
17
+
18
+ /**
19
+ * Save a style ruleset to blockchain.
20
+ *
21
+ * @param {string} rulesText - The full rules text (markdown)
22
+ * @param {string} styleName - Short name for this style (e.g., "strict-control-flow")
23
+ * @param {string} description - One-line description
24
+ * @returns {Promise<Object>} Result with txId
25
+ */
26
+ export async function saveStyle(rulesText, styleName, description) {
27
+ const config = await loadConfig()
28
+ const wif = await getWif()
29
+ if (!wif) {
30
+ return { success: false, error: 'Wallet not configured or PIN incorrect. Run setup_wallet first.' }
31
+ }
32
+
33
+ if (!rulesText || rulesText.trim().length === 0) {
34
+ return { success: false, error: 'No rules text provided.' }
35
+ }
36
+
37
+ // Build style object
38
+ const styleId = sha256(styleName + Date.now())
39
+ const style = {
40
+ id: styleId,
41
+ protocol: 'INDELIBLE_STYLE_V1',
42
+ name: styleName,
43
+ description: description || '',
44
+ owner: config.address,
45
+ created_at: new Date().toISOString(),
46
+ rules: rulesText
47
+ }
48
+
49
+ // Encrypt
50
+ const encrypted = encrypt(JSON.stringify(style), wif)
51
+
52
+ // Build OP_RETURN payload (metadata is plaintext, rules are encrypted)
53
+ const payload = {
54
+ protocol: 'indelible.style',
55
+ version: 1,
56
+ name: styleName,
57
+ description: description || '',
58
+ owner: config.address,
59
+ style_id: styleId,
60
+ encrypted,
61
+ timestamp: style.created_at
62
+ }
63
+
64
+ // Broadcast via SPV
65
+ try {
66
+ const utxos = await spv.getUtxos(config.address)
67
+ if (!utxos || utxos.length === 0) {
68
+ return { success: false, error: 'No UTXOs available. Fund your wallet first.' }
69
+ }
70
+
71
+ const { txHex, txId } = await spv.buildOpReturnTx(wif, utxos, JSON.stringify(payload))
72
+ await spv.broadcastTx(txHex)
73
+
74
+ // Store style txid in config for quick access
75
+ await saveConfig({
76
+ ...config,
77
+ style_txid: txId,
78
+ style_name: styleName,
79
+ style_id: styleId
80
+ })
81
+
82
+ return {
83
+ success: true,
84
+ txId,
85
+ styleId,
86
+ styleName,
87
+ rulesLength: rulesText.length,
88
+ message: `Style "${styleName}" saved to blockchain. txId: ${txId}`
89
+ }
90
+ } catch (error) {
91
+ return { success: false, error: `Failed to broadcast style: ${error.message}` }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Save style from a file path (convenience wrapper).
97
+ * Reads the file and calls saveStyle().
98
+ */
99
+ export async function saveStyleFromFile(filePath, styleName, description) {
100
+ if (!existsSync(filePath)) {
101
+ return { success: false, error: `File not found: ${filePath}` }
102
+ }
103
+ const rulesText = await readFile(filePath, 'utf-8')
104
+ return saveStyle(rulesText, styleName, description)
105
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * update_vault_index — Broadcast a vault index tx to BSV blockchain.
3
+ * Contains encrypted metadata for all files + projects.
4
+ * Server can fetch this single tx to rebuild the entire file listing.
5
+ */
6
+
7
+ import { loadConfig, saveConfig, getWif } from '../lib/config.js'
8
+ import { encrypt } from '../lib/crypto.js'
9
+ import * as spv from '../lib/spv.js'
10
+
11
+ export async function updateVaultIndex() {
12
+ const config = await loadConfig()
13
+ const wif = await getWif()
14
+ if (!wif) return { success: false, error: 'Wallet not configured' }
15
+
16
+ const files = config.file_txids || []
17
+ const projects = config.project_txids || []
18
+
19
+ // Encrypt file metadata for on-chain storage
20
+ const encFiles = files.map(f => ({
21
+ txid: f.txId,
22
+ filename_enc: encrypt(f.filename || '', wif),
23
+ path_enc: encrypt(f.path || '', wif),
24
+ size: f.size || 0,
25
+ timestamp: f.timestamp || null
26
+ }))
27
+
28
+ const encProjects = projects.map(p => ({
29
+ txid: p.txId,
30
+ name_enc: encrypt(p.name || '', wif),
31
+ fileCount: p.fileCount || 0,
32
+ totalSize: p.totalSize || 0,
33
+ timestamp: p.timestamp || null
34
+ }))
35
+
36
+ const payload = {
37
+ protocol: 'indelible.vault-index',
38
+ version: 1,
39
+ files: encFiles,
40
+ projects: encProjects,
41
+ prev_index: config.vault_index_txid || null,
42
+ owner: config.address,
43
+ timestamp: new Date().toISOString()
44
+ }
45
+
46
+ try {
47
+ let utxos = await spv.getUtxos(config.address)
48
+ if (!utxos || utxos.length === 0) {
49
+ return { success: false, error: 'No UTXOs for vault index tx' }
50
+ }
51
+
52
+ const { txHex, txId } = await spv.buildOpReturnTx(
53
+ wif, utxos, JSON.stringify(payload)
54
+ )
55
+ const result = await spv.broadcastTx(txHex)
56
+ const indexTxId = result.txid || txId
57
+
58
+ // Save to config
59
+ await saveConfig({ ...config, vault_index_txid: indexTxId })
60
+
61
+ return {
62
+ success: true,
63
+ txId: indexTxId,
64
+ files: files.length,
65
+ projects: projects.length,
66
+ message: `Vault index updated: ${files.length} files, ${projects.length} projects. txId: ${indexTxId}`
67
+ }
68
+ } catch (error) {
69
+ return { success: false, error: `Failed to update vault index: ${error.message}` }
70
+ }
71
+ }