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 +1 -1
- package/src/index.js +89 -24
- package/src/lib/spv.js +7 -3
- package/src/tools/load_style.js +106 -0
- package/src/tools/save_style.js +105 -0
- package/src/tools/update_vault_index.js +71 -0
package/package.json
CHANGED
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>
|
|
171
|
-
vault save-project <dir> [--name=NAME]
|
|
172
|
-
vault load-file <txid> [--output=path]
|
|
173
|
-
vault load-project <txid> [--output-dir=path]
|
|
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
|
-
|
|
210
|
-
indelible-mcp
|
|
211
|
-
indelible-mcp
|
|
212
|
-
indelible-mcp
|
|
213
|
-
indelible-mcp
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|