indelible-mcp 2.6.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "2.6.1",
3
+ "version": "2.8.0",
4
4
  "description": "Blockchain-backed memory and code storage for Claude Code. Save AI conversations and source code permanently on BSV.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -27,6 +27,9 @@ import { loadProject } from './tools/load_project.js'
27
27
  import { saveStyle, saveStyleFromFile } from './tools/save_style.js'
28
28
  import { loadStyle } from './tools/load_style.js'
29
29
  import { updateVaultIndex } from './tools/update_vault_index.js'
30
+ import { diaryConnect } from './tools/diary_connect.js'
31
+ import { diaryChat } from './tools/diary_chat.js'
32
+ import { diarySave } from './tools/diary_save.js'
30
33
 
31
34
  const CONTEXT_FILE = join(homedir(), '.indelible', 'indelible-context.jsonl')
32
35
 
@@ -220,7 +223,7 @@ Commands:
220
223
 
221
224
  function printHelp() {
222
225
  console.log(`
223
- Indelible MCP — Blockchain memory for Claude Code (v2.6.1)
226
+ Indelible MCP — Blockchain memory for Claude Code (v2.8.0)
224
227
 
225
228
  Setup:
226
229
  indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
@@ -243,6 +246,11 @@ Code Vault:
243
246
  indelible-mcp vault load-style [txid] Load an AI style
244
247
  indelible-mcp vault update-index Update vault index
245
248
 
249
+ Diary AI (Codex/ChatGPT companion):
250
+ indelible-mcp diary connect --key=SK --model=MODEL Connect OpenAI companion
251
+ indelible-mcp diary chat "message" Ask the AI companion
252
+ indelible-mcp diary save Save exchange to blockchain
253
+
246
254
  Hooks (auto-called by Claude Code):
247
255
  indelible-mcp hook pre-compact Auto-save before compaction
248
256
  indelible-mcp hook post-compact Auto-restore after compaction
@@ -316,7 +324,7 @@ function readStdin() {
316
324
 
317
325
  const SERVER_INFO = {
318
326
  name: 'indelible',
319
- version: '2.6.1',
327
+ version: '2.8.0',
320
328
  description: 'Blockchain-backed memory and code storage for Claude Code'
321
329
  }
322
330
 
@@ -436,6 +444,48 @@ const TOOLS = [
436
444
  properties: {},
437
445
  required: []
438
446
  }
447
+ },
448
+ {
449
+ name: 'diary_connect',
450
+ description: 'Connect Diary AI companion (Codex/ChatGPT). Stores your OpenAI API key and model preference.',
451
+ inputSchema: {
452
+ type: 'object',
453
+ properties: {
454
+ api_key: { type: 'string', description: 'OpenAI API key (sk-...)' },
455
+ model: { type: 'string', description: 'Model ID (default: gpt-4o). Options: gpt-5.2, gpt-5, gpt-5-nano, gpt-4o, gpt-4o-mini, o1' },
456
+ name: { type: 'string', description: 'Display name for the AI companion (default: Codex)' }
457
+ },
458
+ required: ['api_key']
459
+ }
460
+ },
461
+ {
462
+ name: 'diary_chat',
463
+ description: 'Send a message to Diary AI (Codex/ChatGPT). Returns the response with a name badge.',
464
+ inputSchema: {
465
+ type: 'object',
466
+ properties: {
467
+ message: { type: 'string', description: 'The message to send to the AI companion' },
468
+ context: { type: 'string', description: 'Optional context about current work' },
469
+ system_prompt: { type: 'string', description: 'Optional system prompt override' }
470
+ },
471
+ required: ['message']
472
+ }
473
+ },
474
+ {
475
+ name: 'diary_save',
476
+ description: 'Save a Diary AI (Codex) exchange to the blockchain. Same WIF, tagged with source.',
477
+ inputSchema: {
478
+ type: 'object',
479
+ properties: {
480
+ messages: {
481
+ type: 'array',
482
+ description: 'Array of message objects: [{ role: "user"|"assistant", content: "..." }]',
483
+ items: { type: 'object', properties: { role: { type: 'string' }, content: { type: 'string' } } }
484
+ },
485
+ summary: { type: 'string', description: 'Brief summary of the exchange' }
486
+ },
487
+ required: ['messages']
488
+ }
439
489
  }
440
490
  ]
441
491
 
@@ -492,6 +542,15 @@ async function handleMcpRequest(request) {
492
542
  case 'update_vault_index':
493
543
  result = await updateVaultIndex()
494
544
  break
545
+ case 'diary_connect':
546
+ result = await diaryConnect({ apiKey: args?.api_key, model: args?.model, name: args?.name })
547
+ break
548
+ case 'diary_chat':
549
+ result = await diaryChat({ message: args?.message, context: args?.context, systemPrompt: args?.system_prompt })
550
+ break
551
+ case 'diary_save':
552
+ result = await diarySave({ messages: args?.messages, summary: args?.summary })
553
+ break
495
554
  default:
496
555
  throw new Error(`Unknown tool: ${name}`)
497
556
  }
@@ -89,6 +89,33 @@ export async function getStatus(config) {
89
89
  return res.json()
90
90
  }
91
91
 
92
+ /**
93
+ * Check if user has Pro or Developer tier (required for Diary AI tools)
94
+ * @param {string} wif - User's WIF
95
+ * @returns {Promise<{ok: boolean, plan: string, error?: string}>}
96
+ */
97
+ export async function checkProTier(wif) {
98
+ try {
99
+ const res = await fetch(`${getApiUrl()}/api/stripe/subscription-status-wif`, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify({ wif }),
103
+ signal: AbortSignal.timeout(5000)
104
+ })
105
+ if (res.ok) {
106
+ const data = await res.json()
107
+ if (data.plan === 'admin' || data.plan === 'pro' || data.plan === 'developer') {
108
+ return { ok: true, plan: data.plan }
109
+ }
110
+ return { ok: false, plan: data.plan || 'free', error: 'Pro tier required for Diary AI companion. Upgrade at indelible.one/pricing' }
111
+ }
112
+ } catch {
113
+ // Server unreachable — allow (offline-first)
114
+ return { ok: true, plan: 'unknown' }
115
+ }
116
+ return { ok: true, plan: 'unknown' }
117
+ }
118
+
92
119
  /**
93
120
  * Check if server is reachable
94
121
  */
@@ -0,0 +1,130 @@
1
+ /**
2
+ * diary_chat — Send a message to Diary AI (Codex/ChatGPT)
3
+ *
4
+ * Calls OpenAI chat completions API directly using the config
5
+ * set by diary_connect. Returns the response formatted with
6
+ * a name badge for display in Claude Code chat.
7
+ *
8
+ * Usage:
9
+ * diary_chat({ message: 'How should we architect this?', context: 'Working on SPV bridge...' })
10
+ */
11
+
12
+ import { loadConfig, getWif } from '../lib/config.js'
13
+ import { checkProTier } from '../lib/api-client.js'
14
+ // Bun has native fetch — no import needed
15
+
16
+ /**
17
+ * @param {Object} options
18
+ * @param {string} options.message - The question/message to send to Codex
19
+ * @param {string} [options.context] - Optional context about current work
20
+ * @param {string} [options.systemPrompt] - Optional system prompt override
21
+ * @returns {Promise<Object>} - { success, response, formatted, model, name }
22
+ */
23
+ export async function diaryChat({ message, context, systemPrompt }) {
24
+ if (!message) {
25
+ return {
26
+ success: false,
27
+ error: 'No message provided'
28
+ }
29
+ }
30
+
31
+ const config = loadConfig()
32
+ if (!config) {
33
+ return {
34
+ success: false,
35
+ error: 'No config found. Run setup first.'
36
+ }
37
+ }
38
+
39
+ // Check Pro tier
40
+ const wif = await getWif()
41
+ if (wif) {
42
+ const tier = await checkProTier(wif)
43
+ if (!tier.ok) return { success: false, error: tier.error }
44
+ }
45
+
46
+ if (!config.diary || !config.diary.apiKey) {
47
+ return {
48
+ success: false,
49
+ error: 'Diary AI not configured. Run diary_connect first with your OpenAI API key.'
50
+ }
51
+ }
52
+
53
+ const { apiKey, model, name } = config.diary
54
+ const displayName = name || 'Codex'
55
+
56
+ // Build messages array
57
+ const messages = []
58
+
59
+ // System prompt — use provided, or default
60
+ const sysPrompt = systemPrompt || `You are ${displayName}, an AI companion working alongside Claude Code in a VS Code session. You are a planner and architect. Keep responses focused and practical. The user and Claude can see your responses — speak directly to both.`
61
+ messages.push({ role: 'system', content: sysPrompt })
62
+
63
+ // Add context if provided
64
+ if (context) {
65
+ messages.push({ role: 'system', content: `Current context:\n${context}` })
66
+ }
67
+
68
+ // Add the user's message
69
+ messages.push({ role: 'user', content: message })
70
+
71
+ try {
72
+ const response = await fetch('https://api.openai.com/v1/chat/completions', {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Authorization': `Bearer ${apiKey}`,
76
+ 'Content-Type': 'application/json'
77
+ },
78
+ body: JSON.stringify({
79
+ model: model || 'gpt-4o',
80
+ messages,
81
+ max_tokens: 4096,
82
+ temperature: 0.7
83
+ })
84
+ })
85
+
86
+ if (!response.ok) {
87
+ const errorText = await response.text()
88
+ if (response.status === 401) {
89
+ return {
90
+ success: false,
91
+ error: 'Invalid OpenAI API key. Run diary_connect with a valid key.'
92
+ }
93
+ }
94
+ if (response.status === 429) {
95
+ return {
96
+ success: false,
97
+ error: 'OpenAI rate limit exceeded. Try again in a moment.'
98
+ }
99
+ }
100
+ return {
101
+ success: false,
102
+ error: `OpenAI API error ${response.status}: ${errorText}`
103
+ }
104
+ }
105
+
106
+ const data = await response.json()
107
+ const content = data.choices?.[0]?.message?.content || ''
108
+
109
+ // Format with name badge for display in Claude Code chat
110
+ const formatted = `**🤖 ${displayName}:**\n> ${content.split('\n').join('\n> ')}`
111
+
112
+ return {
113
+ success: true,
114
+ response: content,
115
+ formatted,
116
+ model: data.model || model,
117
+ name: displayName,
118
+ usage: {
119
+ inputTokens: data.usage?.prompt_tokens || 0,
120
+ outputTokens: data.usage?.completion_tokens || 0
121
+ }
122
+ }
123
+
124
+ } catch (error) {
125
+ return {
126
+ success: false,
127
+ error: `Failed to reach OpenAI: ${error.message}`
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * diary_connect — Configure Diary AI companion (Codex/ChatGPT)
3
+ *
4
+ * Stores OpenAI API key, model preference, and display name
5
+ * in the MCP config under a "diary" section.
6
+ *
7
+ * Usage:
8
+ * diary_connect({ apiKey: 'sk-...', model: 'gpt-4o', name: 'Codex' })
9
+ */
10
+
11
+ import { loadConfig, saveConfig, getWif } from '../lib/config.js'
12
+ import { checkProTier } from '../lib/api-client.js'
13
+
14
+ /**
15
+ * @param {Object} options
16
+ * @param {string} options.apiKey - OpenAI API key (sk-...)
17
+ * @param {string} [options.model] - Model ID (default: gpt-4o)
18
+ * @param {string} [options.name] - Display name (default: Codex)
19
+ * @returns {Promise<Object>} - { success, message, diary }
20
+ */
21
+ export async function diaryConnect({ apiKey, model, name }) {
22
+ if (!apiKey || !apiKey.startsWith('sk-')) {
23
+ return {
24
+ success: false,
25
+ error: 'Invalid API key. Must start with sk-'
26
+ }
27
+ }
28
+
29
+ const config = loadConfig()
30
+ if (!config) {
31
+ return {
32
+ success: false,
33
+ error: 'No config found. Run setup first.'
34
+ }
35
+ }
36
+
37
+ // Check Pro tier
38
+ const wif = await getWif()
39
+ if (wif) {
40
+ const tier = await checkProTier(wif)
41
+ if (!tier.ok) return { success: false, error: tier.error }
42
+ }
43
+
44
+ // Store diary settings in config
45
+ config.diary = {
46
+ apiKey,
47
+ model: model || 'gpt-4o',
48
+ name: name || 'Codex'
49
+ }
50
+
51
+ saveConfig(config)
52
+
53
+ return {
54
+ success: true,
55
+ message: `Diary AI connected! Using ${config.diary.model} as "${config.diary.name}"`,
56
+ diary: {
57
+ model: config.diary.model,
58
+ name: config.diary.name,
59
+ hasKey: true
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * diary_save — Save Codex exchanges to blockchain
3
+ *
4
+ * Saves Diary AI (Codex) conversations to the same blockchain
5
+ * as Claude sessions, using the same WIF. Tagged with source: 'codex'
6
+ * so load_context can tell who said what.
7
+ *
8
+ * Usage:
9
+ * diary_save({ messages: [...], summary: 'Architecture discussion' })
10
+ */
11
+
12
+ import { loadConfig, saveConfig, getWif } from '../lib/config.js'
13
+ import { encrypt, sha256 } from '../lib/crypto.js'
14
+ import * as spv from '../lib/spv.js'
15
+ import { indexSession, checkProTier } from '../lib/api-client.js'
16
+
17
+ /**
18
+ * @param {Object} options
19
+ * @param {Array} options.messages - Array of { role: 'user'|'assistant', content: string }
20
+ * @param {string} [options.summary] - Brief summary of the exchange
21
+ * @returns {Promise<Object>} - { success, txId, sessionId, messageCount }
22
+ */
23
+ export async function diarySave({ messages, summary }) {
24
+ if (!messages || !Array.isArray(messages) || messages.length === 0) {
25
+ return {
26
+ success: false,
27
+ error: 'No messages provided. Pass an array of { role, content } objects.'
28
+ }
29
+ }
30
+
31
+ const config = loadConfig()
32
+ if (!config) {
33
+ return {
34
+ success: false,
35
+ error: 'No config found. Run setup first.'
36
+ }
37
+ }
38
+
39
+ const wif = await getWif()
40
+ if (!wif) {
41
+ return {
42
+ success: false,
43
+ error: 'No WIF found. Run setup first.'
44
+ }
45
+ }
46
+
47
+ // Check Pro tier
48
+ const tier = await checkProTier(wif)
49
+ if (!tier.ok) return { success: false, error: tier.error }
50
+
51
+ const diaryName = config.diary?.name || 'Codex'
52
+
53
+ // Add timestamps to messages if missing
54
+ const timestampedMessages = messages.map(m => ({
55
+ role: m.role,
56
+ content: m.content,
57
+ timestamp: m.timestamp || new Date().toISOString()
58
+ }))
59
+
60
+ // Build session object — same structure as save_session but tagged as codex
61
+ const sessionId = sha256(JSON.stringify(messages) + Date.now())
62
+ const prevSessionId = config.last_session_id || null
63
+
64
+ const session = {
65
+ id: sessionId,
66
+ type: 'diary',
67
+ source: 'codex',
68
+ source_name: diaryName,
69
+ prev_session_id: prevSessionId,
70
+ summary: summary || `${diaryName} exchange (${messages.length} messages)`,
71
+ message_count: messages.length,
72
+ created_at: new Date().toISOString(),
73
+ messages: timestampedMessages
74
+ }
75
+
76
+ // Encrypt with same WIF as Claude sessions
77
+ const encrypted = encrypt(JSON.stringify(session), wif)
78
+
79
+ // Build blockchain payload
80
+ const payload = {
81
+ protocol: 'indelible.claude-code',
82
+ version: 2,
83
+ address: config.address,
84
+ session_id: sessionId,
85
+ prev_session_id: prevSessionId,
86
+ summary: session.summary,
87
+ message_count: session.message_count,
88
+ encrypted,
89
+ timestamp: session.created_at
90
+ }
91
+
92
+ // Build + sign transaction locally, broadcast via SPV bridge
93
+ try {
94
+ const utxos = await spv.getUtxos(config.address)
95
+ const { txHex, txId } = await spv.buildOpReturnTx(wif, utxos, JSON.stringify(payload))
96
+ const broadcastResult = await spv.broadcastTx(txHex)
97
+ const finalTxId = broadcastResult.txid || txId
98
+
99
+ // Index on server for web app retrieval (best effort)
100
+ try {
101
+ await indexSession({
102
+ txId: finalTxId,
103
+ address: config.address,
104
+ session_id: sessionId,
105
+ prev_session_id: prevSessionId,
106
+ summary: session.summary,
107
+ message_count: session.message_count,
108
+ save_type: 'diary',
109
+ encrypted,
110
+ timestamp: session.created_at
111
+ }, config)
112
+ } catch { /* indexing failure doesn't block save */ }
113
+
114
+ // Update config with new session chain
115
+ saveConfig({
116
+ ...config,
117
+ last_session_id: sessionId,
118
+ last_tx_id: finalTxId
119
+ })
120
+
121
+ return {
122
+ success: true,
123
+ txId: finalTxId,
124
+ sessionId,
125
+ prevSessionId,
126
+ messageCount: messages.length,
127
+ source: diaryName,
128
+ message: `${diaryName} exchange saved! ${messages.length} messages committed to blockchain.`
129
+ }
130
+
131
+ } catch (error) {
132
+ return {
133
+ success: false,
134
+ error: `Failed to save: ${error.message}`
135
+ }
136
+ }
137
+ }
@@ -30,6 +30,7 @@ function mergeDeltaSessions(sessions) {
30
30
  parent.summary = session.summary
31
31
  parent.created_at = session.created_at
32
32
  parent.id = session.id
33
+ parent.structured_context = session.structured_context || parent.structured_context
33
34
  } else {
34
35
  merged.push({ ...session })
35
36
  }
@@ -56,6 +57,22 @@ function formatCurrentConversation(session) {
56
57
  lines.push(`Last saved: ${session.created_at ? new Date(session.created_at).toLocaleString() : 'Unknown'}`)
57
58
  lines.push('')
58
59
 
60
+ // Render structured context if present (todos, plan, git branch)
61
+ if (session.structured_context) {
62
+ const sc = session.structured_context
63
+ if (sc.git_branch) lines.push(`Git branch: \`${sc.git_branch}\``)
64
+ if (sc.plan_slug) lines.push(`Active plan: \`${sc.plan_slug}\``)
65
+ if (sc.todos && sc.todos.length > 0) {
66
+ lines.push('')
67
+ lines.push('### Active Tasks (at time of save):')
68
+ for (const t of sc.todos) {
69
+ const icon = t.status === 'completed' ? '[x]' : t.status === 'in_progress' ? '[~]' : '[ ]'
70
+ lines.push(`- ${icon} ${t.content}`)
71
+ }
72
+ }
73
+ lines.push('')
74
+ }
75
+
59
76
  if (total === 0) return lines.join('\n')
60
77
 
61
78
  const recentStart = Math.max(0, total - RECENT_MESSAGES_FULL)
@@ -13,8 +13,9 @@
13
13
  * 9. Local backups (LOCAL)
14
14
  */
15
15
 
16
- import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync } from 'node:fs'
17
- import { dirname } from 'node:path'
16
+ import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync, readdirSync } from 'node:fs'
17
+ import { dirname, join } from 'node:path'
18
+ import { homedir } from 'node:os'
18
19
  import { loadConfig, saveConfig, getWif } from '../lib/config.js'
19
20
  import { encrypt, sha256 } from '../lib/crypto.js'
20
21
  import { findCurrentTranscript, parseTranscript } from '../lib/transcript.js'
@@ -65,6 +66,43 @@ function ensureJsonlBackup(path, messages) {
65
66
  } catch { /* don't block */ }
66
67
  }
67
68
 
69
+ /**
70
+ * Extract structured context (todos, plan slug, git branch) from transcript.
71
+ */
72
+ function getStructuredContext(transcriptPath) {
73
+ const ctx = { todos: null, plan_slug: null, git_branch: null, session_id: null }
74
+ try {
75
+ const content = readFileSync(transcriptPath, 'utf-8')
76
+ const lines = content.trim().split('\n')
77
+ for (let i = lines.length - 1; i >= 0; i--) {
78
+ try {
79
+ const entry = JSON.parse(lines[i])
80
+ if (entry.sessionId) {
81
+ ctx.session_id = entry.sessionId
82
+ ctx.git_branch = entry.gitBranch || null
83
+ ctx.plan_slug = entry.slug || null
84
+ break
85
+ }
86
+ } catch {}
87
+ }
88
+ } catch {}
89
+
90
+ const todosDir = join(homedir(), '.claude', 'todos')
91
+ if (ctx.session_id && existsSync(todosDir)) {
92
+ try {
93
+ const files = readdirSync(todosDir)
94
+ const match = files.find(f => f.startsWith(ctx.session_id))
95
+ if (match) {
96
+ const todos = JSON.parse(readFileSync(join(todosDir, match), 'utf-8'))
97
+ if (Array.isArray(todos) && todos.length > 0) {
98
+ ctx.todos = todos
99
+ }
100
+ }
101
+ } catch {}
102
+ }
103
+ return ctx
104
+ }
105
+
68
106
  /**
69
107
  * Save current session to blockchain.
70
108
  * Builds + signs transaction locally, broadcasts via SPV bridge.
@@ -133,6 +171,14 @@ export async function saveSession(transcriptPath, summary) {
133
171
  messages: messagesToCommit
134
172
  }
135
173
 
174
+ // Attach structured context (todos, active plan, git branch)
175
+ try {
176
+ const structuredCtx = getStructuredContext(actualPath)
177
+ if (structuredCtx.todos || structuredCtx.plan_slug) {
178
+ session.structured_context = structuredCtx
179
+ }
180
+ } catch { /* don't block save on structured context failure */ }
181
+
136
182
  // Local backups (fire-and-forget)
137
183
  if (config.diary_path) updateDiary(config.diary_path, session.summary, session.created_at)
138
184
  if (transcriptPath) ensureJsonlBackup(transcriptPath, allMessages)