indelible-mcp 2.5.1 → 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.1",
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.1)
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.1',
319
+ version: '2.6.0',
320
320
  description: 'Blockchain-backed memory and code storage for Claude Code'
321
321
  }
322
322
 
@@ -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,110 +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
- let lastChangeUtxos = null
101
-
102
- for (let i = 0; i < allFiles.length; i++) {
103
- const file = allFiles[i]
104
- const fileOpts = {
105
- relativePath: `${projectName}/${file.relativePath}`
106
- }
107
- // Chain: pass previous file's change UTXOs so we don't fight over the same UTXO
108
- if (lastChangeUtxos && lastChangeUtxos.length > 0) {
109
- fileOpts.utxos = lastChangeUtxos
110
- }
111
-
112
- const result = await saveFile(file.absolutePath, fileOpts)
113
-
114
- if (!result.success) {
115
- return {
116
- success: false,
117
- error: `Failed to save file ${file.relativePath}: ${result.error}`,
118
- savedSoFar: savedFiles.length,
119
- savedFiles
120
- }
121
- }
122
-
123
- // Capture change UTXOs for next file
124
- lastChangeUtxos = result.changeUtxos
125
-
126
- savedFiles.push({
127
- path: file.relativePath,
128
- txid: result.txId,
129
- size: result.size
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
130
113
  })
131
- totalSize += result.size
132
-
133
- if (i < allFiles.length - 1) {
134
- await new Promise(r => setTimeout(r, 200))
135
- }
114
+ totalSize += fileSize
136
115
  }
137
116
 
138
- // Create encrypted manifest
139
- const manifestFiles = savedFiles.map(f => ({
140
- path_enc: encrypt(f.path, wif),
141
- txid: f.txid,
142
- size: f.size
143
- }))
144
-
145
- const manifest = {
146
- protocol: 'indelible.project',
147
- version: 2,
117
+ const payload = {
118
+ protocol: 'indelible.project-bundle',
119
+ version: 1,
148
120
  name_enc: encrypt(projectName, wif),
149
- file_count: savedFiles.length,
121
+ file_count: allFiles.length,
150
122
  total_size: totalSize,
151
- files: manifestFiles,
123
+ files: bundleFiles,
152
124
  owner: config.address,
153
125
  timestamp
154
126
  }
155
127
 
156
- // Use last file's change UTXOs for manifest (chain continues)
157
- let utxos = (lastChangeUtxos && lastChangeUtxos.length > 0)
158
- ? lastChangeUtxos
159
- : await spv.getUtxos(config.address)
128
+ const utxos = await spv.getUtxos(config.address)
160
129
  if (!utxos || utxos.length === 0) {
161
- return {
162
- success: false,
163
- error: 'No UTXOs for manifest tx. Files were saved but manifest was not.',
164
- savedFiles
165
- }
130
+ return { success: false, error: 'No UTXOs available. Fund your wallet first.' }
166
131
  }
167
132
 
168
133
  const { txHex, txId } = await spv.buildOpReturnTxWithChange(
169
- wif, utxos, JSON.stringify(manifest)
134
+ wif, utxos, JSON.stringify(payload), 'INDELIBLE_PROJECT_BUNDLE'
170
135
  )
171
- const result = await spv.broadcastTx(txHex)
172
- const manifestTxId = result.txid || txId
136
+ await spv.broadcastTx(txHex)
173
137
 
174
138
  // Track in config
175
139
  const projects = config.project_txids || []
176
140
  projects.push({
177
- txId: manifestTxId,
141
+ txId,
178
142
  name: projectName,
179
- fileCount: savedFiles.length,
143
+ fileCount: allFiles.length,
180
144
  totalSize,
181
145
  timestamp
182
146
  })
183
147
  saveConfig({ ...config, project_txids: projects })
184
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
+
185
167
  return {
186
168
  success: true,
187
- txId: manifestTxId,
169
+ txId,
188
170
  name: projectName,
189
- fileCount: savedFiles.length,
171
+ fileCount: allFiles.length,
190
172
  totalSize,
191
- files: savedFiles,
192
- 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}`
193
174
  }
194
175
  } catch (error) {
195
176
  return {
196
177
  success: false,
197
- error: `Failed to save project: ${error.message}`,
198
- savedSoFar: savedFiles.length,
199
- savedFiles
178
+ error: `Failed to save project: ${error.message}`
200
179
  }
201
180
  }
202
181
  }