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 +1 -1
- package/src/index.js +61 -2
- package/src/lib/api-client.js +27 -0
- package/src/tools/diary_chat.js +130 -0
- package/src/tools/diary_connect.js +62 -0
- package/src/tools/diary_save.js +137 -0
- package/src/tools/load_context.js +17 -0
- package/src/tools/save_session.js +48 -2
package/package.json
CHANGED
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.
|
|
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.
|
|
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
|
}
|
package/src/lib/api-client.js
CHANGED
|
@@ -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)
|