indelible-mcp 1.0.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/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # Indelible MCP Server
2
+
3
+ Blockchain-backed memory for Claude Code. Save your AI conversations permanently on BSV.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npm install -g @indelible/mcp-server
9
+ indelible-mcp --show-config
10
+ ```
11
+
12
+ Copy the config output and paste it into your Claude Code settings, then restart Claude Code.
13
+
14
+ ## Setup
15
+
16
+ ### 1. Install
17
+
18
+ ```bash
19
+ npm install -g @indelible/mcp-server
20
+ ```
21
+
22
+ ### 2. Configure Claude Code
23
+
24
+ Run:
25
+ ```bash
26
+ indelible-mcp --show-config
27
+ ```
28
+
29
+ This outputs the config to add to your Claude Code settings:
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "indelible": {
35
+ "command": "indelible-mcp"
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ### 3. Restart Claude Code
42
+
43
+ Close and reopen VS Code (or your Claude Code client).
44
+
45
+ ### 4. Set Up Your Wallet
46
+
47
+ In Claude Code, say:
48
+
49
+ > "Set up my wallet"
50
+
51
+ This generates a new BSV keypair and saves it to `~/.indelible/config.json`.
52
+
53
+ **Important:** Save your WIF (private key) somewhere safe. This is your identity.
54
+
55
+ ### 5. Fund Your Wallet
56
+
57
+ Send a small amount of BSV (~$0.01) to your address for transaction fees.
58
+
59
+ ## Tools
60
+
61
+ ### `setup_wallet`
62
+ Generate a new BSV wallet. Only needed once.
63
+
64
+ ```
65
+ "Set up my wallet"
66
+ ```
67
+
68
+ ### `save_session`
69
+ Save the current conversation to the blockchain.
70
+
71
+ ```
72
+ "Save this session to blockchain"
73
+ ```
74
+
75
+ ### `load_context`
76
+ Load previous sessions from the blockchain.
77
+
78
+ ```
79
+ "Load my previous context"
80
+ ```
81
+
82
+ ## How It Works
83
+
84
+ 1. Your conversations are encrypted with AES-256-GCM using your WIF-derived key
85
+ 2. Encrypted data is stored on the BSV blockchain via OP_RETURN
86
+ 3. Only you can decrypt your data (using your WIF)
87
+ 4. Sessions are chained together for continuity
88
+
89
+ ## View Your Data
90
+
91
+ Visit [indelible.one](https://indelible.one) and log in with your WIF to see all your saved sessions and code.
92
+
93
+ ## Cost
94
+
95
+ ~1 satoshi per byte. A typical session costs less than $0.01.
96
+
97
+ ## License
98
+
99
+ MIT
package/index.js ADDED
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Indelible MCP Server
4
+ *
5
+ * Provides blockchain-backed memory for Claude Code sessions.
6
+ * Communicates via stdio using JSON-RPC (MCP protocol).
7
+ *
8
+ * Tools:
9
+ * - setup_wallet: Generate BSV keypair, save to ~/.indelible/config.json
10
+ * - save_session: Read transcript, encrypt, commit to chain
11
+ * - load_context: Fetch latest checkpoint, decrypt, return summary
12
+ */
13
+
14
+ // Handle CLI flags before loading MCP
15
+ if (process.argv.includes('--show-config')) {
16
+ console.log(`
17
+ Add this to your Claude Code settings.json:
18
+
19
+ {
20
+ "mcpServers": {
21
+ "indelible": {
22
+ "command": "indelible-mcp"
23
+ }
24
+ }
25
+ }
26
+
27
+ Then restart Claude Code and say "set up my wallet" to get started.
28
+ `)
29
+ process.exit(0)
30
+ }
31
+
32
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
33
+ console.log(`
34
+ Indelible MCP Server - Blockchain memory for Claude Code
35
+
36
+ Usage:
37
+ indelible-mcp Start MCP server (used by Claude Code)
38
+ indelible-mcp --show-config Show Claude Code configuration
39
+ indelible-mcp --help Show this help message
40
+
41
+ After installing, run 'indelible-mcp --show-config' and paste
42
+ the output into your Claude Code settings.
43
+
44
+ Learn more: https://indelible.one
45
+ `)
46
+ process.exit(0)
47
+ }
48
+
49
+ import { createInterface } from 'readline'
50
+ import { setupWallet } from './tools/setup_wallet.js'
51
+ import { saveSession } from './tools/save_session.js'
52
+ import { loadContext } from './tools/load_context.js'
53
+
54
+ // MCP Server metadata
55
+ const SERVER_INFO = {
56
+ name: 'indelible-mcp',
57
+ version: '1.0.0',
58
+ description: 'Blockchain-backed memory for Claude Code'
59
+ }
60
+
61
+ // Available tools
62
+ const TOOLS = [
63
+ {
64
+ name: 'setup_wallet',
65
+ description: 'Generate a new BSV wallet for Indelible AI. Creates keypair and saves to ~/.indelible/config.json. Only run once per machine.',
66
+ inputSchema: {
67
+ type: 'object',
68
+ properties: {
69
+ api_url: {
70
+ type: 'string',
71
+ description: 'Indelible API URL (default: https://indelible.one)'
72
+ }
73
+ },
74
+ required: []
75
+ }
76
+ },
77
+ {
78
+ name: 'save_session',
79
+ description: 'Save the current Claude Code session to the blockchain. Encrypts transcript and commits to BSV chain.',
80
+ inputSchema: {
81
+ type: 'object',
82
+ properties: {
83
+ transcript_path: {
84
+ type: 'string',
85
+ description: 'Path to the JSONL transcript file'
86
+ },
87
+ summary: {
88
+ type: 'string',
89
+ description: 'Brief summary of what was accomplished in this session'
90
+ }
91
+ },
92
+ required: ['transcript_path']
93
+ }
94
+ },
95
+ {
96
+ name: 'load_context',
97
+ description: 'Load previous session context from the blockchain. Returns summary of past sessions to restore memory.',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ num_sessions: {
102
+ type: 'number',
103
+ description: 'Number of recent sessions to load (default: 3)'
104
+ }
105
+ },
106
+ required: []
107
+ }
108
+ }
109
+ ]
110
+
111
+ // Handle MCP requests
112
+ async function handleRequest(request) {
113
+ const { method, params, id } = request
114
+
115
+ try {
116
+ switch (method) {
117
+ case 'initialize':
118
+ return {
119
+ jsonrpc: '2.0',
120
+ id,
121
+ result: {
122
+ protocolVersion: '2024-11-05',
123
+ serverInfo: SERVER_INFO,
124
+ capabilities: {
125
+ tools: {}
126
+ }
127
+ }
128
+ }
129
+
130
+ case 'tools/list':
131
+ return {
132
+ jsonrpc: '2.0',
133
+ id,
134
+ result: { tools: TOOLS }
135
+ }
136
+
137
+ case 'tools/call':
138
+ const { name, arguments: args } = params
139
+ let result
140
+
141
+ switch (name) {
142
+ case 'setup_wallet':
143
+ result = await setupWallet(args?.api_url)
144
+ break
145
+ case 'save_session':
146
+ result = await saveSession(args?.transcript_path, args?.summary)
147
+ break
148
+ case 'load_context':
149
+ result = await loadContext(args?.num_sessions)
150
+ break
151
+ default:
152
+ throw new Error(`Unknown tool: ${name}`)
153
+ }
154
+
155
+ return {
156
+ jsonrpc: '2.0',
157
+ id,
158
+ result: {
159
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
160
+ }
161
+ }
162
+
163
+ case 'notifications/initialized':
164
+ // Client acknowledged initialization - no response needed
165
+ return null
166
+
167
+ default:
168
+ return {
169
+ jsonrpc: '2.0',
170
+ id,
171
+ error: { code: -32601, message: `Method not found: ${method}` }
172
+ }
173
+ }
174
+ } catch (error) {
175
+ return {
176
+ jsonrpc: '2.0',
177
+ id,
178
+ error: { code: -32000, message: error.message }
179
+ }
180
+ }
181
+ }
182
+
183
+ // Main: stdio JSON-RPC loop
184
+ async function main() {
185
+ const rl = createInterface({
186
+ input: process.stdin,
187
+ output: process.stdout,
188
+ terminal: false
189
+ })
190
+
191
+ for await (const line of rl) {
192
+ if (!line.trim()) continue
193
+
194
+ try {
195
+ const request = JSON.parse(line)
196
+ const response = await handleRequest(request)
197
+
198
+ if (response) {
199
+ console.log(JSON.stringify(response))
200
+ }
201
+ } catch (error) {
202
+ console.log(JSON.stringify({
203
+ jsonrpc: '2.0',
204
+ id: null,
205
+ error: { code: -32700, message: 'Parse error' }
206
+ }))
207
+ }
208
+ }
209
+ }
210
+
211
+ main().catch(console.error)
package/lib/api.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Indelible API client for MCP Server
3
+ */
4
+
5
+ import fetch from 'node-fetch'
6
+ import { loadConfig } from './config.js'
7
+
8
+ const DEFAULT_API_URL = 'https://indelible.one'
9
+
10
+ /**
11
+ * Get API base URL from config
12
+ */
13
+ async function getApiUrl() {
14
+ const config = await loadConfig()
15
+ return config?.api_url || DEFAULT_API_URL
16
+ }
17
+
18
+ /**
19
+ * Commit a session to the blockchain
20
+ * @param {Object} session - Session data to commit
21
+ * @param {string} wif - WIF for signing/encryption
22
+ * @returns {Promise<{success: boolean, txId?: string, error?: string}>}
23
+ */
24
+ export async function commitSession(session, wif) {
25
+ const apiUrl = await getApiUrl()
26
+
27
+ const response = await fetch(`${apiUrl}/api/claude-code/commit`, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json'
31
+ },
32
+ body: JSON.stringify({ session, wif })
33
+ })
34
+
35
+ if (!response.ok) {
36
+ const error = await response.text()
37
+ throw new Error(error || `Commit failed: ${response.status}`)
38
+ }
39
+
40
+ return response.json()
41
+ }
42
+
43
+ /**
44
+ * Get latest sessions for an address
45
+ * @param {string} address - BSV address
46
+ * @param {string} wif - WIF for decryption
47
+ * @param {number} limit - Number of sessions to fetch
48
+ * @returns {Promise<{sessions: Array}>}
49
+ */
50
+ export async function getLatestSessions(address, wif, limit = 3) {
51
+ const apiUrl = await getApiUrl()
52
+
53
+ const response = await fetch(`${apiUrl}/api/claude-code/latest`, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json'
57
+ },
58
+ body: JSON.stringify({ address, wif, limit })
59
+ })
60
+
61
+ if (!response.ok) {
62
+ if (response.status === 404) {
63
+ return { sessions: [] }
64
+ }
65
+ throw new Error(`Failed to fetch sessions: ${response.status}`)
66
+ }
67
+
68
+ return response.json()
69
+ }
70
+
71
+ /**
72
+ * Check if API is reachable
73
+ * @returns {Promise<boolean>}
74
+ */
75
+ export async function checkConnection() {
76
+ try {
77
+ const apiUrl = await getApiUrl()
78
+ const response = await fetch(`${apiUrl}/api/health`, { method: 'GET' })
79
+ return response.ok
80
+ } catch {
81
+ return false
82
+ }
83
+ }
package/lib/config.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Config management for Indelible MCP Server
3
+ * Stores WIF and settings in ~/.indelible/config.json
4
+ */
5
+
6
+ import { readFile, writeFile, mkdir } from 'fs/promises'
7
+ import { existsSync } from 'fs'
8
+ import { homedir } from 'os'
9
+ import { join } from 'path'
10
+
11
+ const CONFIG_DIR = join(homedir(), '.indelible')
12
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
13
+
14
+ /**
15
+ * Get config file path
16
+ */
17
+ export function getConfigPath() {
18
+ return CONFIG_FILE
19
+ }
20
+
21
+ /**
22
+ * Load config from disk
23
+ * @returns {Promise<Object|null>} Config object or null if not found
24
+ */
25
+ export async function loadConfig() {
26
+ try {
27
+ if (!existsSync(CONFIG_FILE)) {
28
+ return null
29
+ }
30
+ const data = await readFile(CONFIG_FILE, 'utf-8')
31
+ return JSON.parse(data)
32
+ } catch (error) {
33
+ return null
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Save config to disk
39
+ * @param {Object} config - Config object to save
40
+ */
41
+ export async function saveConfig(config) {
42
+ // Ensure directory exists
43
+ if (!existsSync(CONFIG_DIR)) {
44
+ await mkdir(CONFIG_DIR, { recursive: true })
45
+ }
46
+
47
+ await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8')
48
+ }
49
+
50
+ /**
51
+ * Check if wallet is configured
52
+ * @returns {Promise<boolean>}
53
+ */
54
+ export async function hasWallet() {
55
+ const config = await loadConfig()
56
+ return config?.wif != null
57
+ }
package/lib/crypto.js ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Crypto utilities for Indelible MCP Server
3
+ * WIF-derived AES-256-GCM encryption
4
+ */
5
+
6
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto'
7
+
8
+ /**
9
+ * Derive encryption key from WIF
10
+ * @param {string} wif - Wallet Import Format private key
11
+ * @returns {Buffer} 32-byte key
12
+ */
13
+ export function deriveKey(wif) {
14
+ return createHash('sha256').update(wif).digest()
15
+ }
16
+
17
+ /**
18
+ * Encrypt data with WIF-derived key
19
+ * @param {string} plaintext - Data to encrypt
20
+ * @param {string} wif - WIF for key derivation
21
+ * @returns {string} Base64 encoded encrypted data (iv:tag:ciphertext)
22
+ */
23
+ export function encrypt(plaintext, wif) {
24
+ const key = deriveKey(wif)
25
+ const iv = randomBytes(12)
26
+ const cipher = createCipheriv('aes-256-gcm', key, iv)
27
+
28
+ let encrypted = cipher.update(plaintext, 'utf8', 'base64')
29
+ encrypted += cipher.final('base64')
30
+ const tag = cipher.getAuthTag()
31
+
32
+ // Format: iv:tag:ciphertext (all base64)
33
+ return `${iv.toString('base64')}:${tag.toString('base64')}:${encrypted}`
34
+ }
35
+
36
+ /**
37
+ * Decrypt data with WIF-derived key
38
+ * @param {string} encrypted - Base64 encoded encrypted data (iv:tag:ciphertext)
39
+ * @param {string} wif - WIF for key derivation
40
+ * @returns {string} Decrypted plaintext
41
+ */
42
+ export function decrypt(encrypted, wif) {
43
+ const [ivB64, tagB64, ciphertext] = encrypted.split(':')
44
+ const key = deriveKey(wif)
45
+ const iv = Buffer.from(ivB64, 'base64')
46
+ const tag = Buffer.from(tagB64, 'base64')
47
+
48
+ const decipher = createDecipheriv('aes-256-gcm', key, iv)
49
+ decipher.setAuthTag(tag)
50
+
51
+ let decrypted = decipher.update(ciphertext, 'base64', 'utf8')
52
+ decrypted += decipher.final('utf8')
53
+
54
+ return decrypted
55
+ }
56
+
57
+ /**
58
+ * Hash data with SHA256
59
+ * @param {string} data - Data to hash
60
+ * @returns {string} Hex-encoded hash
61
+ */
62
+ export function sha256(data) {
63
+ return createHash('sha256').update(data).digest('hex')
64
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "indelible-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Indelible AI - blockchain-backed memory for Claude Code",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "indelible-mcp": "index.js"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "lib/",
13
+ "tools/",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "start": "node index.js"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "claude",
22
+ "indelible",
23
+ "blockchain",
24
+ "bsv",
25
+ "ai-memory"
26
+ ],
27
+ "author": "",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "@anthropic-ai/sdk": "^0.52.0",
31
+ "@bsv/sdk": "^1.1.23",
32
+ "node-fetch": "^3.3.2"
33
+ }
34
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * load_context tool
3
+ * Fetch latest checkpoints from blockchain, decrypt, and return context summary
4
+ */
5
+
6
+ import { loadConfig } from '../lib/config.js'
7
+ import { decrypt } from '../lib/crypto.js'
8
+ import { getLatestSessions } from '../lib/api.js'
9
+
10
+ /**
11
+ * Format session for context injection
12
+ * @param {Object} session - Decrypted session object
13
+ * @returns {string} Formatted session summary
14
+ */
15
+ function formatSession(session) {
16
+ const date = new Date(session.created_at).toLocaleDateString()
17
+ const lines = [
18
+ `## Session: ${date}`,
19
+ `Summary: ${session.summary}`,
20
+ `Messages: ${session.message_count}`,
21
+ ''
22
+ ]
23
+
24
+ // Include key exchanges (first 3 user/assistant pairs)
25
+ if (session.messages?.length > 0) {
26
+ lines.push('### Key points:')
27
+ let count = 0
28
+ for (const msg of session.messages) {
29
+ if (count >= 6) break // 3 pairs max
30
+ const role = msg.role === 'user' ? 'User' : 'Assistant'
31
+ const content = typeof msg.content === 'string'
32
+ ? msg.content.slice(0, 150)
33
+ : JSON.stringify(msg.content).slice(0, 150)
34
+ lines.push(`- ${role}: ${content}${content.length >= 150 ? '...' : ''}`)
35
+ count++
36
+ }
37
+ lines.push('')
38
+ }
39
+
40
+ return lines.join('\n')
41
+ }
42
+
43
+ /**
44
+ * Load previous session context from blockchain
45
+ * @param {number} numSessions - Number of recent sessions to load (default: 3)
46
+ * @returns {Promise<Object>} Result with context and status
47
+ */
48
+ export async function loadContext(numSessions = 3) {
49
+ // Load config
50
+ const config = await loadConfig()
51
+ if (!config?.wif) {
52
+ return {
53
+ success: false,
54
+ error: 'Wallet not configured. Run setup_wallet first.',
55
+ context: null
56
+ }
57
+ }
58
+
59
+ try {
60
+ // Fetch sessions from API
61
+ const { sessions } = await getLatestSessions(config.address, config.wif, numSessions)
62
+
63
+ if (!sessions || sessions.length === 0) {
64
+ return {
65
+ success: true,
66
+ context: null,
67
+ message: 'No previous sessions found. This is a fresh start!',
68
+ sessionCount: 0
69
+ }
70
+ }
71
+
72
+ // Decrypt and format each session
73
+ const decrypted = []
74
+ for (const session of sessions) {
75
+ try {
76
+ const data = JSON.parse(decrypt(session.encrypted, config.wif))
77
+ decrypted.push(data)
78
+ } catch {
79
+ // Skip sessions that fail to decrypt
80
+ }
81
+ }
82
+
83
+ if (decrypted.length === 0) {
84
+ return {
85
+ success: true,
86
+ context: null,
87
+ message: 'Found sessions but could not decrypt. Key mismatch?',
88
+ sessionCount: sessions.length
89
+ }
90
+ }
91
+
92
+ // Build context string
93
+ const contextParts = [
94
+ '# Previous Session Context',
95
+ `Loaded ${decrypted.length} recent session(s) from blockchain.`,
96
+ ''
97
+ ]
98
+
99
+ for (const session of decrypted) {
100
+ contextParts.push(formatSession(session))
101
+ }
102
+
103
+ const context = contextParts.join('\n')
104
+
105
+ return {
106
+ success: true,
107
+ context,
108
+ sessionCount: decrypted.length,
109
+ sessions: decrypted.map(s => ({
110
+ id: s.id,
111
+ summary: s.summary,
112
+ date: s.created_at,
113
+ messageCount: s.message_count
114
+ })),
115
+ message: `Loaded ${decrypted.length} session(s) from blockchain memory.`
116
+ }
117
+ } catch (error) {
118
+ return {
119
+ success: false,
120
+ error: `Failed to load context: ${error.message}`,
121
+ context: null
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * save_session tool
3
+ * Read transcript JSONL, encrypt, and commit to blockchain
4
+ * Supports session chaining via prev_session_id
5
+ */
6
+
7
+ import { readFile, writeFile } from 'fs/promises'
8
+ import { existsSync } from 'fs'
9
+ import { loadConfig, saveConfig } from '../lib/config.js'
10
+ import { encrypt, sha256 } from '../lib/crypto.js'
11
+ import { commitSession } from '../lib/api.js'
12
+
13
+ /**
14
+ * Parse JSONL transcript file
15
+ * @param {string} path - Path to JSONL file
16
+ * @returns {Promise<Array>} Array of message objects
17
+ */
18
+ async function parseTranscript(path) {
19
+ const content = await readFile(path, 'utf-8')
20
+ const lines = content.trim().split('\n')
21
+ const messages = []
22
+
23
+ for (const line of lines) {
24
+ if (!line.trim()) continue
25
+ try {
26
+ const entry = JSON.parse(line)
27
+ // Extract relevant fields from Claude Code transcript format
28
+ // Claude Code uses type: "user" and type: "assistant"
29
+ if (entry.type === 'user' || entry.type === 'assistant') {
30
+ // Extract content from message object
31
+ let content = ''
32
+ if (entry.message?.content) {
33
+ // Content can be array of content blocks or string
34
+ if (Array.isArray(entry.message.content)) {
35
+ content = entry.message.content
36
+ .filter(c => c.type === 'text')
37
+ .map(c => c.text)
38
+ .join('\n')
39
+ } else {
40
+ content = entry.message.content
41
+ }
42
+ }
43
+
44
+ if (content) {
45
+ messages.push({
46
+ role: entry.type === 'user' ? 'user' : 'assistant',
47
+ content,
48
+ timestamp: entry.timestamp || new Date().toISOString()
49
+ })
50
+ }
51
+ }
52
+ } catch {
53
+ // Skip malformed lines
54
+ }
55
+ }
56
+
57
+ return messages
58
+ }
59
+
60
+ /**
61
+ * Create session summary from messages
62
+ * @param {Array} messages - Array of messages
63
+ * @param {string} userSummary - Optional user-provided summary
64
+ * @returns {string} Session summary
65
+ */
66
+ function createSummary(messages, userSummary) {
67
+ if (userSummary) return userSummary
68
+
69
+ // Auto-generate summary from first user message
70
+ const firstUser = messages.find(m => m.role === 'user')
71
+ if (firstUser) {
72
+ const preview = typeof firstUser.content === 'string'
73
+ ? firstUser.content.slice(0, 200)
74
+ : JSON.stringify(firstUser.content).slice(0, 200)
75
+ return preview + (preview.length >= 200 ? '...' : '')
76
+ }
77
+
78
+ return 'Claude Code session'
79
+ }
80
+
81
+ /**
82
+ * Save current session to blockchain
83
+ * @param {string} transcriptPath - Path to JSONL transcript
84
+ * @param {string} summary - Optional session summary
85
+ * @returns {Promise<Object>} Result with txId and status
86
+ */
87
+ export async function saveSession(transcriptPath, summary) {
88
+ // Load config
89
+ const config = await loadConfig()
90
+ if (!config?.wif) {
91
+ return {
92
+ success: false,
93
+ error: 'Wallet not configured. Run setup_wallet first.'
94
+ }
95
+ }
96
+
97
+ // Check transcript exists
98
+ if (!transcriptPath || !existsSync(transcriptPath)) {
99
+ return {
100
+ success: false,
101
+ error: `Transcript not found: ${transcriptPath}`
102
+ }
103
+ }
104
+
105
+ // Parse transcript
106
+ const messages = await parseTranscript(transcriptPath)
107
+ if (messages.length === 0) {
108
+ return {
109
+ success: false,
110
+ error: 'No messages found in transcript'
111
+ }
112
+ }
113
+
114
+ // Get previous session ID for chaining
115
+ const prevSessionId = config.last_session_id || null
116
+
117
+ // Create session object
118
+ const sessionId = sha256(transcriptPath + Date.now())
119
+ const session = {
120
+ id: sessionId,
121
+ type: 'claude-code',
122
+ prev_session_id: prevSessionId,
123
+ summary: createSummary(messages, summary),
124
+ message_count: messages.length,
125
+ created_at: new Date().toISOString(),
126
+ messages: messages
127
+ }
128
+
129
+ // Encrypt session data
130
+ const encrypted = encrypt(JSON.stringify(session), config.wif)
131
+
132
+ // Commit to chain
133
+ try {
134
+ const result = await commitSession({
135
+ address: config.address,
136
+ encrypted,
137
+ summary: session.summary,
138
+ message_count: session.message_count,
139
+ session_id: sessionId,
140
+ prev_session_id: prevSessionId
141
+ }, config.wif)
142
+
143
+ // Update config with new session ID for chaining
144
+ await saveConfig({
145
+ ...config,
146
+ last_session_id: sessionId,
147
+ last_tx_id: result.txId
148
+ })
149
+
150
+ return {
151
+ success: true,
152
+ txId: result.txId,
153
+ sessionId: sessionId,
154
+ prevSessionId: prevSessionId,
155
+ messageCount: messages.length,
156
+ summary: session.summary,
157
+ message: `Session saved! ${messages.length} messages committed to blockchain.${prevSessionId ? ' Linked to previous session.' : ' (First session)'}`
158
+ }
159
+ } catch (error) {
160
+ return {
161
+ success: false,
162
+ error: `Failed to commit: ${error.message}`
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * setup_wallet tool
3
+ * Generate BSV keypair and save to ~/.indelible/config.json
4
+ */
5
+
6
+ import { PrivateKey } from '@bsv/sdk'
7
+ import { loadConfig, saveConfig, getConfigPath } from '../lib/config.js'
8
+ import { checkConnection } from '../lib/api.js'
9
+
10
+ const DEFAULT_API_URL = 'https://indelible.one'
11
+
12
+ /**
13
+ * Generate a new BSV wallet for Indelible AI
14
+ * @param {string} apiUrl - Optional API URL override
15
+ * @returns {Promise<Object>} Result with address and status
16
+ */
17
+ export async function setupWallet(apiUrl = DEFAULT_API_URL) {
18
+ // Check if wallet already exists
19
+ const existing = await loadConfig()
20
+ if (existing?.wif) {
21
+ return {
22
+ success: false,
23
+ error: 'Wallet already configured',
24
+ address: existing.address,
25
+ configPath: getConfigPath(),
26
+ hint: 'Delete ~/.indelible/config.json to reset'
27
+ }
28
+ }
29
+
30
+ // Generate new keypair
31
+ const privateKey = PrivateKey.fromRandom()
32
+ const wif = privateKey.toWif()
33
+ const address = privateKey.toPublicKey().toAddress()
34
+
35
+ // Save config
36
+ const config = {
37
+ wif,
38
+ address,
39
+ api_url: apiUrl,
40
+ created_at: new Date().toISOString()
41
+ }
42
+
43
+ await saveConfig(config)
44
+
45
+ // Check API connection
46
+ const connected = await checkConnection()
47
+
48
+ return {
49
+ success: true,
50
+ address,
51
+ configPath: getConfigPath(),
52
+ apiConnected: connected,
53
+ message: connected
54
+ ? `Wallet created! Address: ${address}. Connected to ${apiUrl}`
55
+ : `Wallet created! Address: ${address}. Warning: Could not connect to ${apiUrl}`
56
+ }
57
+ }