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 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 --show-config
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. Sign Up at indelible.one
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
- ### 3. Import Your Key
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
- **Your key never leaves your machine.**
23
+ ### 2. Fund Your Wallet
35
24
 
36
- ### 4. Configure Claude Code
25
+ Send a small amount of BSV to the address shown after setup. Any BSV wallet works (HandCash, RelayX, etc.).
37
26
 
38
- Run:
39
- ```bash
40
- indelible-mcp --show-config
41
- ```
27
+ ### 3. Add MCP Config to Claude Code
42
28
 
43
- Add the output to your Claude Code settings:
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
- ### 5. Restart Claude Code
41
+ ## Usage
56
42
 
57
- Close and reopen VS Code (or your Claude Code client).
43
+ Once configured, just talk to Claude Code naturally:
58
44
 
59
- ### 6. Fund Your Wallet
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
- Send a small amount of BSV (~$0.01) to your address for transaction fees.
49
+ ### Auto-Save & Auto-Restore
62
50
 
63
- ## Tools
51
+ Indelible automatically saves before Claude Code compacts your context, and restores your sessions after compaction. Nothing is lost.
64
52
 
65
- ### `setup_wallet`
66
- Import your BSV wallet from indelible.one. Only needed once.
53
+ ### CLI Commands
67
54
 
68
55
  ```
69
- "Import my wallet" or indelible-mcp setup --wif=YOUR_KEY
70
- ```
71
-
72
- ### `save_session`
73
- Save the current conversation to the blockchain.
74
-
75
- ```
76
- "Save this session to blockchain"
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 conversations are encrypted with AES-256-GCM using your WIF-derived key
89
- 2. Encrypted data is stored on the BSV blockchain via OP_RETURN
90
- 3. Only you can decrypt your data (using your WIF)
91
- 4. Sessions are chained together for continuity
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
- ## Cost
74
+ ## Security
98
75
 
99
- ~1 satoshi per byte. A typical session costs less than $0.01.
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
- ## License
80
+ ## Learn More
102
81
 
103
- MIT
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.0",
4
- "description": "MCP server for Indelible AI - blockchain-backed memory for Claude Code",
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
- "index.js",
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
- "ai-memory"
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
- "@anthropic-ai/sdk": "^0.52.0",
31
- "@bsv/sdk": "^1.1.23",
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
+ }