indelible-mcp 2.5.0 → 2.6.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.5.0",
3
+ "version": "2.6.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
@@ -220,7 +220,7 @@ Commands:
220
220
 
221
221
  function printHelp() {
222
222
  console.log(`
223
- Indelible MCP — Blockchain memory for Claude Code (v2.5.0)
223
+ Indelible MCP — Blockchain memory for Claude Code (v2.6.0)
224
224
 
225
225
  Setup:
226
226
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -316,7 +316,7 @@ function readStdin() {
316
316
 
317
317
  const SERVER_INFO = {
318
318
  name: 'indelible',
319
- version: '2.5.0',
319
+ version: '2.6.0',
320
320
  description: 'Blockchain-backed memory and code storage for Claude Code'
321
321
  }
322
322
 
@@ -34,7 +34,7 @@ export async function saveFile(filePath, options = {}) {
34
34
  const filenameEnc = encrypt(filename, wif)
35
35
  const pathEnc = encrypt(relativePath, wif)
36
36
 
37
- let utxos = await spv.getUtxos(config.address)
37
+ let utxos = options.utxos || await spv.getUtxos(config.address)
38
38
  if (!utxos || utxos.length === 0) {
39
39
  return { success: false, error: 'No UTXOs available. Fund your wallet first.' }
40
40
  }
@@ -43,6 +43,7 @@ export async function saveFile(filePath, options = {}) {
43
43
 
44
44
  try {
45
45
  let masterTxId
46
+ let finalChangeUtxos = null
46
47
 
47
48
  if (encrypted.length <= MAX_CHUNK_SIZE) {
48
49
  const payload = {
@@ -57,11 +58,12 @@ export async function saveFile(filePath, options = {}) {
57
58
  timestamp
58
59
  }
59
60
 
60
- const { txHex, txId } = await spv.buildOpReturnTxWithChange(
61
+ const { txHex, txId, changeUtxos } = await spv.buildOpReturnTxWithChange(
61
62
  wif, utxos, JSON.stringify(payload)
62
63
  )
63
64
  const result = await spv.broadcastTx(txHex)
64
65
  masterTxId = result.txid || txId
66
+ finalChangeUtxos = changeUtxos
65
67
  } else {
66
68
  // Chunked upload
67
69
  const chunks = []
@@ -118,11 +120,12 @@ export async function saveFile(filePath, options = {}) {
118
120
  await new Promise(r => setTimeout(r, 500))
119
121
  utxos = await spv.getUtxos(config.address)
120
122
  }
121
- const { txHex, txId } = await spv.buildOpReturnTxWithChange(
123
+ const { txHex, txId, changeUtxos: masterChangeUtxos } = await spv.buildOpReturnTxWithChange(
122
124
  wif, utxos, JSON.stringify(masterPayload)
123
125
  )
124
126
  const result = await spv.broadcastTx(txHex)
125
127
  masterTxId = result.txid || txId
128
+ finalChangeUtxos = masterChangeUtxos
126
129
  }
127
130
 
128
131
  // Track in config
@@ -147,6 +150,7 @@ export async function saveFile(filePath, options = {}) {
147
150
  size: fileSize,
148
151
  contentHash: `sha256:${contentHash}`,
149
152
  chunks: chunkCount,
153
+ changeUtxos: finalChangeUtxos,
150
154
  message: `File "${filename}" saved to blockchain. txId: ${masterTxId}${chunkCount > 1 ? ` (${chunkCount} chunks)` : ''}`
151
155
  }
152
156
  } catch (error) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * save_project tool — Save a directory to BSV blockchain
3
- * Walks directory (respecting .gitignore), saves each file,
4
- * creates encrypted manifest linking all file txids.
3
+ * Walks directory (respecting .gitignore), encrypts all files client-side,
4
+ * bundles into a single OP_RETURN transaction.
5
5
  */
6
6
 
7
7
  import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'
@@ -9,7 +9,6 @@ import { join, relative, basename } from 'node:path'
9
9
  import { loadConfig, saveConfig, getWif } from '../lib/config.js'
10
10
  import { encrypt, sha256 } from '../lib/crypto.js'
11
11
  import * as spv from '../lib/spv.js'
12
- import { saveFile } from './save_file.js'
13
12
 
14
13
  const ALWAYS_IGNORE = [
15
14
  'node_modules', '.git', '.next', 'dist', 'build',
@@ -93,96 +92,90 @@ export async function saveProject(dirPath, options = {}) {
93
92
  }
94
93
 
95
94
  const timestamp = new Date().toISOString()
96
- const savedFiles = []
97
95
  let totalSize = 0
98
96
 
99
97
  try {
100
- for (let i = 0; i < allFiles.length; i++) {
101
- const file = allFiles[i]
102
- const result = await saveFile(file.absolutePath, {
103
- relativePath: `${projectName}/${file.relativePath}`
98
+ // Read and encrypt all files into a single bundle
99
+ const bundleFiles = []
100
+ for (const file of allFiles) {
101
+ const content = readFileSync(file.absolutePath, 'utf-8')
102
+ const fileSize = Buffer.byteLength(content, 'utf-8')
103
+ const contentHash = `sha256:${sha256(content)}`
104
+ const encrypted = encrypt(content, wif)
105
+ const filename = file.relativePath.split(/[/\\]/).pop()
106
+
107
+ bundleFiles.push({
108
+ path_enc: encrypt(file.relativePath, wif),
109
+ filename_enc: encrypt(filename, wif),
110
+ size: fileSize,
111
+ content_hash: contentHash,
112
+ encrypted
104
113
  })
105
-
106
- if (!result.success) {
107
- return {
108
- success: false,
109
- error: `Failed to save file ${file.relativePath}: ${result.error}`,
110
- savedSoFar: savedFiles.length,
111
- savedFiles
112
- }
113
- }
114
-
115
- savedFiles.push({
116
- path: file.relativePath,
117
- txid: result.txId,
118
- size: result.size
119
- })
120
- totalSize += result.size
121
-
122
- if (i < allFiles.length - 1) {
123
- await new Promise(r => setTimeout(r, 200))
124
- }
114
+ totalSize += fileSize
125
115
  }
126
116
 
127
- // Create encrypted manifest
128
- const manifestFiles = savedFiles.map(f => ({
129
- path_enc: encrypt(f.path, wif),
130
- txid: f.txid,
131
- size: f.size
132
- }))
133
-
134
- const manifest = {
135
- protocol: 'indelible.project',
136
- version: 2,
117
+ const payload = {
118
+ protocol: 'indelible.project-bundle',
119
+ version: 1,
137
120
  name_enc: encrypt(projectName, wif),
138
- file_count: savedFiles.length,
121
+ file_count: allFiles.length,
139
122
  total_size: totalSize,
140
- files: manifestFiles,
123
+ files: bundleFiles,
141
124
  owner: config.address,
142
125
  timestamp
143
126
  }
144
127
 
145
- let utxos = await spv.getUtxos(config.address)
128
+ const utxos = await spv.getUtxos(config.address)
146
129
  if (!utxos || utxos.length === 0) {
147
- return {
148
- success: false,
149
- error: 'No UTXOs for manifest tx. Files were saved but manifest was not.',
150
- savedFiles
151
- }
130
+ return { success: false, error: 'No UTXOs available. Fund your wallet first.' }
152
131
  }
153
132
 
154
- const { txHex, txId } = await spv.buildOpReturnTx(
155
- wif, utxos, JSON.stringify(manifest)
133
+ const { txHex, txId } = await spv.buildOpReturnTxWithChange(
134
+ wif, utxos, JSON.stringify(payload), 'INDELIBLE_PROJECT_BUNDLE'
156
135
  )
157
- const result = await spv.broadcastTx(txHex)
158
- const manifestTxId = result.txid || txId
136
+ await spv.broadcastTx(txHex)
159
137
 
160
138
  // Track in config
161
139
  const projects = config.project_txids || []
162
140
  projects.push({
163
- txId: manifestTxId,
141
+ txId,
164
142
  name: projectName,
165
- fileCount: savedFiles.length,
143
+ fileCount: allFiles.length,
166
144
  totalSize,
167
145
  timestamp
168
146
  })
169
147
  saveConfig({ ...config, project_txids: projects })
170
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
+
171
167
  return {
172
168
  success: true,
173
- txId: manifestTxId,
169
+ txId,
174
170
  name: projectName,
175
- fileCount: savedFiles.length,
171
+ fileCount: allFiles.length,
176
172
  totalSize,
177
- files: savedFiles,
178
- message: `Project "${projectName}" saved to blockchain. ${savedFiles.length} files, ${totalSize} bytes total. Manifest txId: ${manifestTxId}`
173
+ message: `Project "${projectName}" saved as single tx bundle. ${allFiles.length} files, ${totalSize} bytes. txId: ${txId}`
179
174
  }
180
175
  } catch (error) {
181
176
  return {
182
177
  success: false,
183
- error: `Failed to save project: ${error.message}`,
184
- savedSoFar: savedFiles.length,
185
- savedFiles
178
+ error: `Failed to save project: ${error.message}`
186
179
  }
187
180
  }
188
181
  }