indelible-mcp 2.2.0 → 2.2.1
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/README.md +35 -56
- package/package.json +21 -17
- package/src/index.js +366 -0
- package/src/lib/api-client.js +102 -0
- package/{lib → src/lib}/config.js +15 -36
- package/{lib → src/lib}/crypto.js +3 -31
- package/src/lib/spv.js +144 -0
- package/src/lib/transcript.js +111 -0
- package/{tools → src/tools}/load_context.js +76 -124
- package/src/tools/load_file.js +110 -0
- package/src/tools/load_project.js +107 -0
- package/{tools → src/tools}/save_file.js +33 -58
- package/{tools → src/tools}/save_project.js +53 -95
- package/src/tools/save_session.js +207 -0
- package/{tools → src/tools}/setup_wallet.js +34 -21
- package/index.js +0 -211
- package/lib/api.js +0 -230
- package/lib/spv.js +0 -308
- package/tools/load_file.js +0 -178
- package/tools/load_project.js +0 -153
- package/tools/load_style.js +0 -107
- package/tools/save_session.js +0 -415
- package/tools/save_style.js +0 -105
package/README.md
CHANGED
|
@@ -6,41 +6,27 @@ Blockchain-backed memory for Claude Code. Save your AI conversations permanently
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install -g indelible-mcp
|
|
9
|
-
indelible-mcp
|
|
9
|
+
indelible-mcp setup
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
Copy the config output and paste it into your Claude Code settings, then restart Claude Code.
|
|
13
|
-
|
|
14
12
|
## Setup
|
|
15
13
|
|
|
16
|
-
### 1.
|
|
17
|
-
|
|
18
|
-
Sign in with Google at [indelible.one](https://indelible.one). Your BSV private key (WIF) is generated automatically and shown in Settings.
|
|
19
|
-
|
|
20
|
-
### 2. Install
|
|
14
|
+
### 1. Install & Create Wallet
|
|
21
15
|
|
|
22
16
|
```bash
|
|
23
17
|
npm install -g indelible-mcp
|
|
18
|
+
indelible-mcp setup
|
|
24
19
|
```
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
indelible-mcp setup --wif=YOUR_KEY
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Paste the WIF from your indelible.one Settings page. This saves it to `~/.indelible/config.json`.
|
|
21
|
+
This generates a BSV keypair locally (your private key never leaves your machine) and registers with the Indelible server.
|
|
33
22
|
|
|
34
|
-
|
|
23
|
+
### 2. Fund Your Wallet
|
|
35
24
|
|
|
36
|
-
|
|
25
|
+
Send a small amount of BSV to the address shown after setup. Any BSV wallet works (HandCash, RelayX, etc.).
|
|
37
26
|
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
indelible-mcp --show-config
|
|
41
|
-
```
|
|
27
|
+
### 3. Add MCP Config to Claude Code
|
|
42
28
|
|
|
43
|
-
|
|
29
|
+
Run `indelible-mcp --show-config` to get your config, or add this to your Claude Code `settings.json`:
|
|
44
30
|
|
|
45
31
|
```json
|
|
46
32
|
{
|
|
@@ -52,52 +38,45 @@ Add the output to your Claude Code settings:
|
|
|
52
38
|
}
|
|
53
39
|
```
|
|
54
40
|
|
|
55
|
-
|
|
41
|
+
## Usage
|
|
56
42
|
|
|
57
|
-
|
|
43
|
+
Once configured, just talk to Claude Code naturally:
|
|
58
44
|
|
|
59
|
-
|
|
45
|
+
- **"Save this session to blockchain"** - saves your conversation
|
|
46
|
+
- **"Load my previous context"** - restores past sessions
|
|
47
|
+
- **"Delta save"** - saves only new messages (cheaper, faster)
|
|
60
48
|
|
|
61
|
-
|
|
49
|
+
### Auto-Save & Auto-Restore
|
|
62
50
|
|
|
63
|
-
|
|
51
|
+
Indelible automatically saves before Claude Code compacts your context, and restores your sessions after compaction. Nothing is lost.
|
|
64
52
|
|
|
65
|
-
###
|
|
66
|
-
Import your BSV wallet from indelible.one. Only needed once.
|
|
53
|
+
### CLI Commands
|
|
67
54
|
|
|
68
55
|
```
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
### `load_context`
|
|
80
|
-
Load previous sessions from the blockchain.
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
"Load my previous context"
|
|
56
|
+
indelible-mcp setup Generate wallet & register
|
|
57
|
+
indelible-mcp setup --migrate Derive API key from existing wallet
|
|
58
|
+
indelible-mcp save Save current session
|
|
59
|
+
indelible-mcp save --summary XYZ Save with custom summary
|
|
60
|
+
indelible-mcp load Load context from blockchain
|
|
61
|
+
indelible-mcp load --sessions=5 Load N sessions
|
|
62
|
+
indelible-mcp status Show account status
|
|
63
|
+
indelible-mcp hook pre-compact Pre-compaction save hook
|
|
64
|
+
indelible-mcp hook post-compact Post-compaction restore hook
|
|
84
65
|
```
|
|
85
66
|
|
|
86
67
|
## How It Works
|
|
87
68
|
|
|
88
|
-
1. Your
|
|
89
|
-
2.
|
|
90
|
-
3.
|
|
91
|
-
4.
|
|
92
|
-
|
|
93
|
-
## View Your Data
|
|
94
|
-
|
|
95
|
-
Visit [indelible.one](https://indelible.one) and sign in with Google to see all your saved sessions and code.
|
|
69
|
+
1. Your conversation is encrypted locally with your WIF key
|
|
70
|
+
2. An OP_RETURN transaction is built and signed locally using `@bsv/sdk`
|
|
71
|
+
3. The signed transaction is broadcast via Indelible's SPV bridge
|
|
72
|
+
4. Your private key **never** leaves your machine
|
|
96
73
|
|
|
97
|
-
##
|
|
74
|
+
## Security
|
|
98
75
|
|
|
99
|
-
|
|
76
|
+
- **Zero-knowledge encryption** - your WIF-derived AES-256-GCM key encrypts all data before it touches the network
|
|
77
|
+
- **Self-sovereign keys** - generated locally with `@bsv/sdk`, never transmitted
|
|
78
|
+
- **Immutable storage** - once on BSV, your data cannot be altered or deleted
|
|
100
79
|
|
|
101
|
-
##
|
|
80
|
+
## Learn More
|
|
102
81
|
|
|
103
|
-
|
|
82
|
+
[indelible.one](https://indelible.one)
|
package/package.json
CHANGED
|
@@ -1,35 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "indelible-mcp",
|
|
3
|
-
"version": "2.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.2.1",
|
|
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
|
-
"main": "index.js",
|
|
6
|
+
"main": "src/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"indelible-mcp": "index.js"
|
|
8
|
+
"indelible-mcp": "src/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"
|
|
12
|
-
"lib/",
|
|
13
|
-
"tools/",
|
|
14
|
-
"README.md"
|
|
11
|
+
"src/"
|
|
15
12
|
],
|
|
16
13
|
"scripts": {
|
|
17
|
-
"start": "node index.js"
|
|
14
|
+
"start": "node src/index.js",
|
|
15
|
+
"build": "bun build --compile src/index.js --outfile dist/indelible.exe",
|
|
16
|
+
"build:all": "bun build --compile --target=bun-windows-x64 src/index.js --outfile dist/indelible-win-x64.exe && bun build --compile --target=bun-linux-x64 src/index.js --outfile dist/indelible-linux-x64 && bun build --compile --target=bun-darwin-arm64 src/index.js --outfile dist/indelible-darwin-arm64"
|
|
18
17
|
},
|
|
19
18
|
"keywords": [
|
|
19
|
+
"claude-code",
|
|
20
20
|
"mcp",
|
|
21
|
-
"claude",
|
|
22
|
-
"indelible",
|
|
23
21
|
"blockchain",
|
|
24
22
|
"bsv",
|
|
25
|
-
"
|
|
23
|
+
"memory",
|
|
24
|
+
"context",
|
|
25
|
+
"indelible",
|
|
26
|
+
"code-vault",
|
|
27
|
+
"encrypted-storage"
|
|
26
28
|
],
|
|
27
|
-
"author": "",
|
|
28
29
|
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/indelibleai/indelible-mcp"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://indelible.one",
|
|
29
35
|
"dependencies": {
|
|
30
|
-
"@
|
|
31
|
-
"
|
|
32
|
-
"cross-keychain": "^1.1.0",
|
|
33
|
-
"node-fetch": "^3.3.2"
|
|
36
|
+
"@bsv/sdk": "1.10.1",
|
|
37
|
+
"cross-keychain": "^1.1.0"
|
|
34
38
|
}
|
|
35
39
|
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Indelible CLI — Blockchain memory for Claude Code
|
|
4
|
+
*
|
|
5
|
+
* Modes:
|
|
6
|
+
* indelible-mcp setup --wif=KEY Import private key from indelible.one
|
|
7
|
+
* indelible-mcp save [--summary] Save current session to blockchain
|
|
8
|
+
* indelible-mcp load [--sessions] Load context from blockchain
|
|
9
|
+
* indelible-mcp status Show account status
|
|
10
|
+
* indelible-mcp hook pre-compact Pre-compaction save hook
|
|
11
|
+
* indelible-mcp hook post-compact Post-compaction restore hook
|
|
12
|
+
* (stdin pipe, no TTY) MCP stdio JSON-RPC server mode
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createInterface } from 'node:readline'
|
|
16
|
+
import { homedir } from 'node:os'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
import { loadConfig } from './lib/config.js'
|
|
19
|
+
import { setupWallet } from './tools/setup_wallet.js'
|
|
20
|
+
import { saveSession } from './tools/save_session.js'
|
|
21
|
+
import { loadContext } from './tools/load_context.js'
|
|
22
|
+
import { saveFile } from './tools/save_file.js'
|
|
23
|
+
import { saveProject } from './tools/save_project.js'
|
|
24
|
+
import { loadFile } from './tools/load_file.js'
|
|
25
|
+
import { loadProject } from './tools/load_project.js'
|
|
26
|
+
|
|
27
|
+
const CONTEXT_FILE = join(homedir(), '.indelible', 'indelible-context.jsonl')
|
|
28
|
+
|
|
29
|
+
// ============ CLI MODE ============
|
|
30
|
+
|
|
31
|
+
async function runCli(args) {
|
|
32
|
+
const command = args[0]
|
|
33
|
+
|
|
34
|
+
switch (command) {
|
|
35
|
+
case 'setup': {
|
|
36
|
+
const apiUrl = args.find(a => a.startsWith('--api='))?.split('=')[1]
|
|
37
|
+
const importWif = args.find(a => a.startsWith('--wif='))?.split('=')[1]
|
|
38
|
+
const pin = args.find(a => a.startsWith('--pin='))?.split('=')[1]
|
|
39
|
+
const result = await setupWallet(apiUrl || undefined, importWif, pin)
|
|
40
|
+
console.log(JSON.stringify(result, null, 2))
|
|
41
|
+
break
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case 'save': {
|
|
45
|
+
const summaryIdx = args.indexOf('--summary')
|
|
46
|
+
const summary = summaryIdx !== -1 ? args.slice(summaryIdx + 1).join(' ') : undefined
|
|
47
|
+
const result = await saveSession(CONTEXT_FILE, summary)
|
|
48
|
+
console.log(JSON.stringify(result, null, 2))
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
case 'load': {
|
|
53
|
+
const sessionsArg = args.find(a => a.startsWith('--sessions='))
|
|
54
|
+
const numSessions = sessionsArg ? parseInt(sessionsArg.split('=')[1]) : 5
|
|
55
|
+
const result = await loadContext(numSessions)
|
|
56
|
+
if (result.context) {
|
|
57
|
+
// Output context to stdout (for piping)
|
|
58
|
+
console.log(result.context)
|
|
59
|
+
} else {
|
|
60
|
+
console.log(JSON.stringify(result, null, 2))
|
|
61
|
+
}
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case 'status': {
|
|
66
|
+
const config = loadConfig()
|
|
67
|
+
if (!config?.wif_encrypted && !config?.wif) {
|
|
68
|
+
console.log(JSON.stringify({ error: 'Wallet not configured. Run: indelible-mcp setup' }))
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
console.log(JSON.stringify({
|
|
72
|
+
address: config.address,
|
|
73
|
+
encrypted: !!config.wif_encrypted,
|
|
74
|
+
api_key: config.api_key ? '***' + config.api_key.slice(-8) : 'not set',
|
|
75
|
+
api_url: config.api_url,
|
|
76
|
+
last_session_id: config.last_session_id || null,
|
|
77
|
+
last_saved_message_count: config.last_saved_message_count || 0
|
|
78
|
+
}, null, 2))
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'vault': {
|
|
83
|
+
const subCmd = args[1]
|
|
84
|
+
if (subCmd === 'save-file') {
|
|
85
|
+
const filePath = args[2]
|
|
86
|
+
if (!filePath) { console.error('Usage: indelible-mcp vault save-file <path>'); break }
|
|
87
|
+
const result = await saveFile(filePath)
|
|
88
|
+
console.log(JSON.stringify(result, null, 2))
|
|
89
|
+
} else if (subCmd === 'save-project') {
|
|
90
|
+
const dirPath = args[2]
|
|
91
|
+
if (!dirPath) { console.error('Usage: indelible-mcp vault save-project <dir>'); break }
|
|
92
|
+
const nameArg = args.find(a => a.startsWith('--name='))
|
|
93
|
+
const name = nameArg ? nameArg.split('=')[1] : undefined
|
|
94
|
+
const result = await saveProject(dirPath, { name })
|
|
95
|
+
console.log(JSON.stringify(result, null, 2))
|
|
96
|
+
} else if (subCmd === 'load-file') {
|
|
97
|
+
const txId = args[2]
|
|
98
|
+
if (!txId) { console.error('Usage: indelible-mcp vault load-file <txid> [--output=path]'); break }
|
|
99
|
+
const outArg = args.find(a => a.startsWith('--output='))
|
|
100
|
+
const outputPath = outArg ? outArg.split('=')[1] : undefined
|
|
101
|
+
const result = await loadFile(txId, { outputPath })
|
|
102
|
+
console.log(JSON.stringify(result, null, 2))
|
|
103
|
+
} else if (subCmd === 'load-project') {
|
|
104
|
+
const txId = args[2]
|
|
105
|
+
if (!txId) { console.error('Usage: indelible-mcp vault load-project <txid> [--output-dir=path]'); break }
|
|
106
|
+
const outArg = args.find(a => a.startsWith('--output-dir='))
|
|
107
|
+
const outputDir = outArg ? outArg.split('=')[1] : undefined
|
|
108
|
+
const result = await loadProject(txId, { outputDir })
|
|
109
|
+
console.log(JSON.stringify(result, null, 2))
|
|
110
|
+
} else {
|
|
111
|
+
console.log(`
|
|
112
|
+
Code Vault — Encrypted code storage on BSV blockchain
|
|
113
|
+
|
|
114
|
+
Commands:
|
|
115
|
+
vault save-file <path> Save a file to blockchain
|
|
116
|
+
vault save-project <dir> [--name=NAME] Save a project directory to blockchain
|
|
117
|
+
vault load-file <txid> [--output=path] Load a file from blockchain
|
|
118
|
+
vault load-project <txid> [--output-dir=path] Load a project from blockchain
|
|
119
|
+
`)
|
|
120
|
+
}
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
default:
|
|
125
|
+
printHelp()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function printHelp() {
|
|
130
|
+
console.log(`
|
|
131
|
+
Indelible MCP — Blockchain memory for Claude Code
|
|
132
|
+
|
|
133
|
+
Usage:
|
|
134
|
+
indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
|
|
135
|
+
indelible-mcp save Save current session to blockchain
|
|
136
|
+
indelible-mcp save --summary XYZ Save with custom summary
|
|
137
|
+
indelible-mcp load Load context from blockchain
|
|
138
|
+
indelible-mcp load --sessions=5 Load N sessions
|
|
139
|
+
indelible-mcp status Show account status
|
|
140
|
+
indelible-mcp hook pre-compact Pre-compaction save hook
|
|
141
|
+
indelible-mcp hook post-compact Post-compaction restore hook
|
|
142
|
+
|
|
143
|
+
Code Vault:
|
|
144
|
+
indelible-mcp vault save-file <path> Save a file to blockchain
|
|
145
|
+
indelible-mcp vault save-project <dir> Save a project to blockchain
|
|
146
|
+
indelible-mcp vault load-file <txid> Load a file from blockchain
|
|
147
|
+
indelible-mcp vault load-project <txid> Load a project from blockchain
|
|
148
|
+
|
|
149
|
+
Get your key: Sign in at indelible.one with Google → Settings → Private Key
|
|
150
|
+
Learn more: https://indelible.one
|
|
151
|
+
`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============ HOOK MODE ============
|
|
155
|
+
|
|
156
|
+
async function runPreCompactSave() {
|
|
157
|
+
// Read stdin for hook metadata (Claude Code sends JSON)
|
|
158
|
+
let trigger = 'auto'
|
|
159
|
+
try {
|
|
160
|
+
const stdin = await readStdin()
|
|
161
|
+
if (stdin) {
|
|
162
|
+
const meta = JSON.parse(stdin)
|
|
163
|
+
trigger = meta.trigger || 'auto'
|
|
164
|
+
}
|
|
165
|
+
} catch { /* no stdin or not JSON */ }
|
|
166
|
+
|
|
167
|
+
const config = loadConfig()
|
|
168
|
+
if (!config?.mcp_enabled && config?.mcp_enabled !== undefined) {
|
|
169
|
+
process.stderr.write('Indelible: MCP disabled, skipping save\n')
|
|
170
|
+
process.exit(0)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = await saveSession(CONTEXT_FILE, `Auto-save before ${trigger} compaction`)
|
|
174
|
+
|
|
175
|
+
if (result.success) {
|
|
176
|
+
process.stderr.write(`Indelible: Saved ${result.newMessages} messages (${result.saveType}) tx:${result.txId?.slice(0, 12)}...\n`)
|
|
177
|
+
} else {
|
|
178
|
+
process.stderr.write(`Indelible: Save failed: ${result.error}\n`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
process.exit(0)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function runPostCompactRestore() {
|
|
185
|
+
const config = loadConfig()
|
|
186
|
+
if (!config?.mcp_enabled && config?.mcp_enabled !== undefined) {
|
|
187
|
+
process.stderr.write('Indelible: MCP disabled, skipping restore\n')
|
|
188
|
+
process.exit(0)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const result = await loadContext(5)
|
|
192
|
+
|
|
193
|
+
if (result.success && result.context) {
|
|
194
|
+
// Write to stdout — Claude Code injects this into context
|
|
195
|
+
process.stdout.write(result.context)
|
|
196
|
+
process.stderr.write(`Indelible: Restored ${result.sessionCount} session(s)\n`)
|
|
197
|
+
} else if (result.error) {
|
|
198
|
+
process.stderr.write(`Indelible: Restore failed: ${result.error}\n`)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
process.exit(0)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function readStdin() {
|
|
205
|
+
return new Promise((resolve) => {
|
|
206
|
+
let data = ''
|
|
207
|
+
const timeout = setTimeout(() => resolve(data), 1000)
|
|
208
|
+
process.stdin.on('data', chunk => { data += chunk })
|
|
209
|
+
process.stdin.on('end', () => { clearTimeout(timeout); resolve(data) })
|
|
210
|
+
process.stdin.on('error', () => { clearTimeout(timeout); resolve('') })
|
|
211
|
+
})
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============ MCP SERVER MODE ============
|
|
215
|
+
|
|
216
|
+
const SERVER_INFO = {
|
|
217
|
+
name: 'indelible',
|
|
218
|
+
version: '2.0.0',
|
|
219
|
+
description: 'Blockchain-backed memory for Claude Code'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const TOOLS = [
|
|
223
|
+
{
|
|
224
|
+
name: 'setup_wallet',
|
|
225
|
+
description: 'Import your BSV private key from indelible.one. Get your key by signing in with Google at indelible.one.',
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
api_url: { type: 'string', description: 'Indelible API URL (default: https://indelible.one)' },
|
|
230
|
+
import_wif: { type: 'string', description: 'Your private key (WIF) from indelible.one' },
|
|
231
|
+
pin: { type: 'string', description: 'PIN to encrypt your private key (minimum 4 characters)' }
|
|
232
|
+
},
|
|
233
|
+
required: ['import_wif', 'pin']
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'save_session',
|
|
238
|
+
description: 'Save the current Claude Code session to the blockchain. Encrypts locally, broadcasts via SPV bridge.',
|
|
239
|
+
inputSchema: {
|
|
240
|
+
type: 'object',
|
|
241
|
+
properties: {
|
|
242
|
+
transcript_path: { type: 'string', description: 'Path to the JSONL transcript file' },
|
|
243
|
+
summary: { type: 'string', description: 'Brief summary of this session' }
|
|
244
|
+
},
|
|
245
|
+
required: ['transcript_path']
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'load_context',
|
|
250
|
+
description: 'Load previous session context from the blockchain. Returns summary of past sessions.',
|
|
251
|
+
inputSchema: {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
num_sessions: { type: 'number', description: 'Number of recent sessions to load (default: 5)' }
|
|
255
|
+
},
|
|
256
|
+
required: []
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
async function handleMcpRequest(request) {
|
|
262
|
+
const { method, params, id } = request
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
switch (method) {
|
|
266
|
+
case 'initialize':
|
|
267
|
+
return {
|
|
268
|
+
jsonrpc: '2.0', id,
|
|
269
|
+
result: {
|
|
270
|
+
protocolVersion: '2024-11-05',
|
|
271
|
+
serverInfo: SERVER_INFO,
|
|
272
|
+
capabilities: { tools: {} }
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
case 'tools/list':
|
|
277
|
+
return { jsonrpc: '2.0', id, result: { tools: TOOLS } }
|
|
278
|
+
|
|
279
|
+
case 'tools/call': {
|
|
280
|
+
const { name, arguments: args } = params
|
|
281
|
+
let result
|
|
282
|
+
|
|
283
|
+
switch (name) {
|
|
284
|
+
case 'setup_wallet':
|
|
285
|
+
result = await setupWallet(args?.api_url, args?.import_wif, args?.pin)
|
|
286
|
+
break
|
|
287
|
+
case 'save_session':
|
|
288
|
+
result = await saveSession(args?.transcript_path, args?.summary)
|
|
289
|
+
break
|
|
290
|
+
case 'load_context':
|
|
291
|
+
result = await loadContext(args?.num_sessions)
|
|
292
|
+
break
|
|
293
|
+
default:
|
|
294
|
+
throw new Error(`Unknown tool: ${name}`)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
jsonrpc: '2.0', id,
|
|
299
|
+
result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
case 'notifications/initialized':
|
|
304
|
+
return null
|
|
305
|
+
|
|
306
|
+
default:
|
|
307
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } }
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return { jsonrpc: '2.0', id, error: { code: -32000, message: error.message } }
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function runMcpServer() {
|
|
315
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false })
|
|
316
|
+
|
|
317
|
+
for await (const line of rl) {
|
|
318
|
+
if (!line.trim()) continue
|
|
319
|
+
try {
|
|
320
|
+
const request = JSON.parse(line)
|
|
321
|
+
const response = await handleMcpRequest(request)
|
|
322
|
+
if (response) console.log(JSON.stringify(response))
|
|
323
|
+
} catch {
|
|
324
|
+
console.log(JSON.stringify({
|
|
325
|
+
jsonrpc: '2.0', id: null,
|
|
326
|
+
error: { code: -32700, message: 'Parse error' }
|
|
327
|
+
}))
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ============ MAIN ============
|
|
333
|
+
|
|
334
|
+
const args = process.argv.slice(2)
|
|
335
|
+
|
|
336
|
+
if (args[0] === 'hook') {
|
|
337
|
+
if (args[1] === 'pre-compact') {
|
|
338
|
+
runPreCompactSave().catch(e => { process.stderr.write(`Indelible error: ${e.message}\n`); process.exit(0) })
|
|
339
|
+
} else if (args[1] === 'post-compact') {
|
|
340
|
+
runPostCompactRestore().catch(e => { process.stderr.write(`Indelible error: ${e.message}\n`); process.exit(0) })
|
|
341
|
+
} else {
|
|
342
|
+
console.error('Unknown hook:', args[1])
|
|
343
|
+
process.exit(1)
|
|
344
|
+
}
|
|
345
|
+
} else if (args[0] === '--help' || args[0] === '-h') {
|
|
346
|
+
printHelp()
|
|
347
|
+
} else if (args[0] === '--show-config') {
|
|
348
|
+
console.log(`
|
|
349
|
+
Add this to your Claude Code settings.json:
|
|
350
|
+
|
|
351
|
+
{
|
|
352
|
+
"mcpServers": {
|
|
353
|
+
"indelible": {
|
|
354
|
+
"command": "indelible-mcp"
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
`)
|
|
359
|
+
} else if (args.length > 0) {
|
|
360
|
+
runCli(args).catch(e => { console.error('Error:', e.message); process.exit(1) })
|
|
361
|
+
} else if (!process.stdin.isTTY) {
|
|
362
|
+
// No args, piped stdin = MCP server mode
|
|
363
|
+
runMcpServer().catch(console.error)
|
|
364
|
+
} else {
|
|
365
|
+
printHelp()
|
|
366
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API client for Indelible CLI
|
|
3
|
+
* Handles registration, session indexing, and status checks.
|
|
4
|
+
* Blockchain operations go through SPV bridge (see spv.js).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { loadConfig } from './config.js'
|
|
8
|
+
|
|
9
|
+
const DEFAULT_API_URL = 'https://indelible.one'
|
|
10
|
+
|
|
11
|
+
function getApiUrl() {
|
|
12
|
+
const config = loadConfig()
|
|
13
|
+
return config?.api_url || DEFAULT_API_URL
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getHeaders(config) {
|
|
17
|
+
const headers = { 'Content-Type': 'application/json' }
|
|
18
|
+
if (config?.api_key) {
|
|
19
|
+
headers['Authorization'] = `Bearer ${config.api_key}`
|
|
20
|
+
}
|
|
21
|
+
return headers
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register API key with server (one-time, for SPV bridge access + web app)
|
|
26
|
+
*/
|
|
27
|
+
export async function register(address, apiKey) {
|
|
28
|
+
const res = await fetch(`${getApiUrl()}/api/cli/register`, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({ address, api_key: apiKey }),
|
|
32
|
+
signal: AbortSignal.timeout(10000)
|
|
33
|
+
})
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
36
|
+
throw new Error(err.error || `Registration failed: ${res.status}`)
|
|
37
|
+
}
|
|
38
|
+
return res.json()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Index a session on server for web app retrieval.
|
|
43
|
+
* Called after successful broadcast — best effort, doesn't block save.
|
|
44
|
+
*/
|
|
45
|
+
export async function indexSession(sessionData, config) {
|
|
46
|
+
const res = await fetch(`${getApiUrl()}/api/cli/index-session`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: getHeaders(config),
|
|
49
|
+
body: JSON.stringify(sessionData),
|
|
50
|
+
signal: AbortSignal.timeout(10000)
|
|
51
|
+
})
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
54
|
+
throw new Error(err.error || `Indexing failed: ${res.status}`)
|
|
55
|
+
}
|
|
56
|
+
return res.json()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Fetch session index from server (faster than scanning blockchain)
|
|
61
|
+
*/
|
|
62
|
+
export async function getSessions(address, limit, config) {
|
|
63
|
+
const res = await fetch(`${getApiUrl()}/api/cli/sessions`, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: getHeaders(config),
|
|
66
|
+
body: JSON.stringify({ address, limit }),
|
|
67
|
+
signal: AbortSignal.timeout(10000)
|
|
68
|
+
})
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
71
|
+
throw new Error(err.error || `Fetch sessions failed: ${res.status}`)
|
|
72
|
+
}
|
|
73
|
+
return res.json()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get account status
|
|
78
|
+
*/
|
|
79
|
+
export async function getStatus(config) {
|
|
80
|
+
const res = await fetch(`${getApiUrl()}/api/cli/status`, {
|
|
81
|
+
method: 'GET',
|
|
82
|
+
headers: getHeaders(config),
|
|
83
|
+
signal: AbortSignal.timeout(10000)
|
|
84
|
+
})
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }))
|
|
87
|
+
throw new Error(err.error || `Status check failed: ${res.status}`)
|
|
88
|
+
}
|
|
89
|
+
return res.json()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if server is reachable
|
|
94
|
+
*/
|
|
95
|
+
export async function checkConnection() {
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(`${getApiUrl()}/api/health`, { signal: AbortSignal.timeout(5000) })
|
|
98
|
+
return res.ok
|
|
99
|
+
} catch {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
}
|