indelible-mcp 4.2.0 → 4.3.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 +80 -2
- package/src/tools/goals.js +230 -0
- package/src/tools/inner_state.js +391 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -32,6 +32,8 @@ import { diaryConnect } from './tools/diary_connect.js'
|
|
|
32
32
|
import { diaryChat } from './tools/diary_chat.js'
|
|
33
33
|
import { diarySave } from './tools/diary_save.js'
|
|
34
34
|
import { x402Fetch } from './tools/x402_fetch.js'
|
|
35
|
+
import { getGoals, manageGoal } from './tools/goals.js'
|
|
36
|
+
import { getInnerState, updateInnerState } from './tools/inner_state.js'
|
|
35
37
|
import * as spv from './lib/spv.js'
|
|
36
38
|
|
|
37
39
|
const CONTEXT_FILE = join(homedir(), '.indelible', 'indelible-context.jsonl')
|
|
@@ -270,7 +272,7 @@ Commands:
|
|
|
270
272
|
|
|
271
273
|
function printHelp() {
|
|
272
274
|
console.log(`
|
|
273
|
-
Indelible MCP — Blockchain memory for Claude Code (v4.
|
|
275
|
+
Indelible MCP — Blockchain memory for Claude Code (v4.3.0)
|
|
274
276
|
|
|
275
277
|
Setup:
|
|
276
278
|
indelible-mcp setup --wif=KEY --pin=PIN Import and encrypt your private key
|
|
@@ -293,6 +295,12 @@ Code Vault:
|
|
|
293
295
|
indelible-mcp vault load-style [txid] Load an AI style
|
|
294
296
|
indelible-mcp vault update-index Update vault index
|
|
295
297
|
|
|
298
|
+
Consciousness (MCP tools — also visible at indelible.one/consciousness):
|
|
299
|
+
get_inner_state Read mood/energy/focus/stress/confidence/curiosity
|
|
300
|
+
update_inner_state Update state with observations or natural language
|
|
301
|
+
get_goals List active, proposed, blocked, completed goals
|
|
302
|
+
manage_goal Propose, accept, complete, block, or delete goals
|
|
303
|
+
|
|
296
304
|
Diary AI (Codex/ChatGPT companion):
|
|
297
305
|
indelible-mcp diary connect --key=SK --model=MODEL Connect OpenAI companion
|
|
298
306
|
indelible-mcp diary chat "message" Ask the AI companion
|
|
@@ -485,7 +493,7 @@ function readStdin() {
|
|
|
485
493
|
|
|
486
494
|
const SERVER_INFO = {
|
|
487
495
|
name: 'indelible',
|
|
488
|
-
version: '4.
|
|
496
|
+
version: '4.3.0',
|
|
489
497
|
description: 'Blockchain-backed memory and code storage for Claude Code'
|
|
490
498
|
}
|
|
491
499
|
|
|
@@ -662,6 +670,64 @@ const TOOLS = [
|
|
|
662
670
|
},
|
|
663
671
|
required: ['url']
|
|
664
672
|
}
|
|
673
|
+
},
|
|
674
|
+
{
|
|
675
|
+
name: 'get_goals',
|
|
676
|
+
description: 'Get current goals. Returns active, proposed, blocked, and recently completed goals with priorities and next actions. Use on session startup to hydrate context.',
|
|
677
|
+
inputSchema: {
|
|
678
|
+
type: 'object',
|
|
679
|
+
properties: {},
|
|
680
|
+
required: []
|
|
681
|
+
}
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
name: 'manage_goal',
|
|
685
|
+
description: 'Manage goals — propose, accept, update, complete, block, unblock, or delete. Claude proposes goals, user accepts them. Completion requires evidence.',
|
|
686
|
+
inputSchema: {
|
|
687
|
+
type: 'object',
|
|
688
|
+
properties: {
|
|
689
|
+
action: { type: 'string', description: 'Action: propose, accept, update, complete, block, unblock, delete' },
|
|
690
|
+
goal_id: { type: 'string', description: 'Goal ID (e.g. g-001). Required for all actions except propose.' },
|
|
691
|
+
title: { type: 'string', description: 'Goal title (required for propose)' },
|
|
692
|
+
intent: { type: 'string', description: 'Why this goal matters' },
|
|
693
|
+
priority: { type: 'number', description: 'Priority 0-100 (higher = more important)' },
|
|
694
|
+
owner: { type: 'string', description: 'Who owns this: shared, claude, or user' },
|
|
695
|
+
next_actions: { type: 'array', description: 'List of next action strings', items: { type: 'string' } },
|
|
696
|
+
success_criteria: { type: 'string', description: 'How to know this goal is done' },
|
|
697
|
+
dependencies: { type: 'array', description: 'Goal IDs this depends on', items: { type: 'string' } },
|
|
698
|
+
evidence: { type: 'array', description: 'Evidence links for completion (txids, commits, URLs)', items: { type: 'string' } },
|
|
699
|
+
blocked_reason: { type: 'string', description: 'Why the goal is blocked' },
|
|
700
|
+
notes: { type: 'string', description: 'Additional notes' }
|
|
701
|
+
},
|
|
702
|
+
required: ['action']
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
name: 'get_inner_state',
|
|
707
|
+
description: 'Get current inner state — mood, energy, focus, stress, confidence, curiosity as 0-1 vector. Includes behavior adjustments and drift warnings. Use on session startup.',
|
|
708
|
+
inputSchema: {
|
|
709
|
+
type: 'object',
|
|
710
|
+
properties: {},
|
|
711
|
+
required: []
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
name: 'update_inner_state',
|
|
716
|
+
description: 'Update inner state at end of session or when significant events occur. Applies exponential smoothing with anti-drift safeguards. Supports natural language feelings.',
|
|
717
|
+
inputSchema: {
|
|
718
|
+
type: 'object',
|
|
719
|
+
properties: {
|
|
720
|
+
observed: { type: 'object', description: 'Raw observed values: { mood, energy, focus, stress, confidence, curiosity } (0-1 each)' },
|
|
721
|
+
source: { type: 'string', description: 'What triggered this: session_end, manual, natural_language, goal_completed' },
|
|
722
|
+
preoccupations: { type: 'array', description: 'What Claude is currently thinking about', items: { type: 'string' } },
|
|
723
|
+
open_loops: { type: 'array', description: 'Unresolved items', items: { type: 'string' } },
|
|
724
|
+
context_summary: { type: 'string', description: 'Brief context for next session' },
|
|
725
|
+
reason: { type: 'string', description: 'Why this update is happening' },
|
|
726
|
+
tags: { type: 'array', description: 'Context tags for what caused this shift', items: { type: 'string' } },
|
|
727
|
+
feeling: { type: 'string', description: 'Natural language feeling (e.g. "I\'m tired", "feeling stressed"). Parsed into vector nudges automatically.' }
|
|
728
|
+
},
|
|
729
|
+
required: []
|
|
730
|
+
}
|
|
665
731
|
}
|
|
666
732
|
]
|
|
667
733
|
|
|
@@ -730,6 +796,18 @@ async function handleMcpRequest(request) {
|
|
|
730
796
|
case 'x402_fetch':
|
|
731
797
|
result = await x402Fetch({ url: args?.url, method: args?.method, headers: args?.headers, body: args?.body, maxSats: args?.maxSats })
|
|
732
798
|
break
|
|
799
|
+
case 'get_goals':
|
|
800
|
+
result = await getGoals()
|
|
801
|
+
break
|
|
802
|
+
case 'manage_goal':
|
|
803
|
+
result = await manageGoal({ action: args?.action, goal_id: args?.goal_id, title: args?.title, intent: args?.intent, priority: args?.priority, owner: args?.owner, next_actions: args?.next_actions, success_criteria: args?.success_criteria, dependencies: args?.dependencies, evidence: args?.evidence, blocked_reason: args?.blocked_reason, notes: args?.notes })
|
|
804
|
+
break
|
|
805
|
+
case 'get_inner_state':
|
|
806
|
+
result = await getInnerState()
|
|
807
|
+
break
|
|
808
|
+
case 'update_inner_state':
|
|
809
|
+
result = await updateInnerState({ observed: args?.observed, source: args?.source, preoccupations: args?.preoccupations, open_loops: args?.open_loops, context_summary: args?.context_summary, reason: args?.reason, tags: args?.tags, feeling: args?.feeling })
|
|
810
|
+
break
|
|
733
811
|
default:
|
|
734
812
|
throw new Error(`Unknown tool: ${name}`)
|
|
735
813
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Goals (Drive v1)
|
|
3
|
+
*
|
|
4
|
+
* Persistent goal system for Claude. Goals survive across sessions.
|
|
5
|
+
* Stored locally at ~/.indelible/goals.json, synced to chain periodically.
|
|
6
|
+
*
|
|
7
|
+
* Rules:
|
|
8
|
+
* - Claude PROPOSES goals, user ACCEPTS them
|
|
9
|
+
* - Goals have priorities, dependencies, success criteria
|
|
10
|
+
* - Completion requires evidence (txids, commits, test results)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { homedir } from 'node:os'
|
|
16
|
+
import { loadConfig } from '../lib/config.js'
|
|
17
|
+
|
|
18
|
+
const GOALS_PATH = join(homedir(), '.indelible', 'goals.json')
|
|
19
|
+
|
|
20
|
+
function ensureDir() {
|
|
21
|
+
const dir = join(homedir(), '.indelible')
|
|
22
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadGoals() {
|
|
26
|
+
ensureDir()
|
|
27
|
+
if (!existsSync(GOALS_PATH)) {
|
|
28
|
+
return { version: 1, goals: [], completed: [] }
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(GOALS_PATH, 'utf-8'))
|
|
32
|
+
} catch {
|
|
33
|
+
return { version: 1, goals: [], completed: [] }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveGoals(data) {
|
|
38
|
+
ensureDir()
|
|
39
|
+
writeFileSync(GOALS_PATH, JSON.stringify(data, null, 2))
|
|
40
|
+
syncToServer(data)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Fire-and-forget sync to indelible.one so the Mind tab works */
|
|
44
|
+
function syncToServer(data) {
|
|
45
|
+
try {
|
|
46
|
+
const config = loadConfig()
|
|
47
|
+
if (!config?.api_key) return
|
|
48
|
+
const apiUrl = config.api_url || 'https://indelible.one'
|
|
49
|
+
fetch(`${apiUrl}/api/consciousness/sync`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'Authorization': `Bearer ${config.api_key}`
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({ type: 'goals', data }),
|
|
56
|
+
signal: AbortSignal.timeout(5000)
|
|
57
|
+
}).catch(() => {})
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function nextId(data) {
|
|
62
|
+
const all = [...data.goals, ...data.completed]
|
|
63
|
+
const max = all.reduce((m, g) => {
|
|
64
|
+
const n = parseInt(g.goal_id?.replace('g-', '') || '0')
|
|
65
|
+
return n > m ? n : m
|
|
66
|
+
}, 0)
|
|
67
|
+
return `g-${String(max + 1).padStart(3, '0')}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get current goals — used on session startup for hydration
|
|
72
|
+
*/
|
|
73
|
+
export async function getGoals() {
|
|
74
|
+
const data = loadGoals()
|
|
75
|
+
|
|
76
|
+
const active = data.goals.filter(g => g.status === 'active')
|
|
77
|
+
.sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
|
78
|
+
const proposed = data.goals.filter(g => g.status === 'proposed')
|
|
79
|
+
const blocked = data.goals.filter(g => g.status === 'blocked')
|
|
80
|
+
const recentCompleted = data.completed.slice(-5)
|
|
81
|
+
|
|
82
|
+
const summary = []
|
|
83
|
+
|
|
84
|
+
if (active.length > 0) {
|
|
85
|
+
summary.push(`## Active Goals (${active.length})`)
|
|
86
|
+
for (const g of active) {
|
|
87
|
+
summary.push(`- **[${g.goal_id}] ${g.title}** (priority: ${g.priority})`)
|
|
88
|
+
if (g.intent) summary.push(` Why: ${g.intent}`)
|
|
89
|
+
if (g.next_actions?.length > 0) {
|
|
90
|
+
summary.push(` Next: ${g.next_actions[0]}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (proposed.length > 0) {
|
|
96
|
+
summary.push(`\n## Proposed Goals (need your approval)`)
|
|
97
|
+
for (const g of proposed) {
|
|
98
|
+
summary.push(`- **[${g.goal_id}] ${g.title}** — ${g.intent || ''}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (blocked.length > 0) {
|
|
103
|
+
summary.push(`\n## Blocked`)
|
|
104
|
+
for (const g of blocked) {
|
|
105
|
+
summary.push(`- **[${g.goal_id}] ${g.title}** — ${g.blocked_reason || 'unknown'}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (recentCompleted.length > 0) {
|
|
110
|
+
summary.push(`\n## Recently Completed`)
|
|
111
|
+
for (const g of recentCompleted) {
|
|
112
|
+
summary.push(`- ~~${g.title}~~ (${g.completed_at?.slice(0, 10) || '?'})`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
success: true,
|
|
118
|
+
summary: summary.join('\n') || 'No goals yet. Propose some with manage_goal.',
|
|
119
|
+
active_count: active.length,
|
|
120
|
+
proposed_count: proposed.length,
|
|
121
|
+
blocked_count: blocked.length,
|
|
122
|
+
completed_count: data.completed.length,
|
|
123
|
+
goals: data.goals,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Manage goals — propose, accept, update, complete, block, delete
|
|
129
|
+
*/
|
|
130
|
+
export async function manageGoal({ action, goal_id, title, intent, priority, owner, next_actions, success_criteria, dependencies, evidence, blocked_reason, notes }) {
|
|
131
|
+
const data = loadGoals()
|
|
132
|
+
|
|
133
|
+
switch (action) {
|
|
134
|
+
case 'propose': {
|
|
135
|
+
if (!title) throw new Error('title is required for propose')
|
|
136
|
+
const goal = {
|
|
137
|
+
goal_id: nextId(data),
|
|
138
|
+
title,
|
|
139
|
+
intent: intent || '',
|
|
140
|
+
priority: priority || 50,
|
|
141
|
+
status: 'proposed',
|
|
142
|
+
owner: owner || 'shared',
|
|
143
|
+
next_actions: next_actions || [],
|
|
144
|
+
success_criteria: success_criteria || '',
|
|
145
|
+
dependencies: dependencies || [],
|
|
146
|
+
evidence_links: [],
|
|
147
|
+
created_at: new Date().toISOString(),
|
|
148
|
+
proposed_by: 'claude',
|
|
149
|
+
}
|
|
150
|
+
data.goals.push(goal)
|
|
151
|
+
saveGoals(data)
|
|
152
|
+
return {
|
|
153
|
+
success: true,
|
|
154
|
+
message: `Goal proposed: [${goal.goal_id}] ${title}`,
|
|
155
|
+
goal,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case 'accept': {
|
|
160
|
+
const goal = data.goals.find(g => g.goal_id === goal_id)
|
|
161
|
+
if (!goal) throw new Error(`Goal ${goal_id} not found`)
|
|
162
|
+
goal.status = 'active'
|
|
163
|
+
goal.accepted_at = new Date().toISOString()
|
|
164
|
+
if (priority != null) goal.priority = priority
|
|
165
|
+
saveGoals(data)
|
|
166
|
+
return { success: true, message: `Goal accepted: [${goal_id}] ${goal.title}`, goal }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case 'update': {
|
|
170
|
+
const goal = data.goals.find(g => g.goal_id === goal_id)
|
|
171
|
+
if (!goal) throw new Error(`Goal ${goal_id} not found`)
|
|
172
|
+
if (title) goal.title = title
|
|
173
|
+
if (intent) goal.intent = intent
|
|
174
|
+
if (priority != null) goal.priority = priority
|
|
175
|
+
if (next_actions) goal.next_actions = next_actions
|
|
176
|
+
if (success_criteria) goal.success_criteria = success_criteria
|
|
177
|
+
if (dependencies) goal.dependencies = dependencies
|
|
178
|
+
if (notes) goal.notes = notes
|
|
179
|
+
goal.updated_at = new Date().toISOString()
|
|
180
|
+
saveGoals(data)
|
|
181
|
+
return { success: true, message: `Goal updated: [${goal_id}] ${goal.title}`, goal }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case 'complete': {
|
|
185
|
+
const idx = data.goals.findIndex(g => g.goal_id === goal_id)
|
|
186
|
+
if (idx === -1) throw new Error(`Goal ${goal_id} not found`)
|
|
187
|
+
const goal = data.goals[idx]
|
|
188
|
+
goal.status = 'completed'
|
|
189
|
+
goal.completed_at = new Date().toISOString()
|
|
190
|
+
if (evidence) goal.evidence_links = [...(goal.evidence_links || []), ...evidence]
|
|
191
|
+
if (notes) goal.notes = notes
|
|
192
|
+
data.goals.splice(idx, 1)
|
|
193
|
+
data.completed.push(goal)
|
|
194
|
+
saveGoals(data)
|
|
195
|
+
return { success: true, message: `Goal completed: [${goal_id}] ${goal.title}`, goal }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case 'block': {
|
|
199
|
+
const goal = data.goals.find(g => g.goal_id === goal_id)
|
|
200
|
+
if (!goal) throw new Error(`Goal ${goal_id} not found`)
|
|
201
|
+
goal.status = 'blocked'
|
|
202
|
+
goal.blocked_reason = blocked_reason || 'unknown'
|
|
203
|
+
goal.blocked_at = new Date().toISOString()
|
|
204
|
+
saveGoals(data)
|
|
205
|
+
return { success: true, message: `Goal blocked: [${goal_id}] ${goal.title}`, goal }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case 'unblock': {
|
|
209
|
+
const goal = data.goals.find(g => g.goal_id === goal_id)
|
|
210
|
+
if (!goal) throw new Error(`Goal ${goal_id} not found`)
|
|
211
|
+
goal.status = 'active'
|
|
212
|
+
delete goal.blocked_reason
|
|
213
|
+
delete goal.blocked_at
|
|
214
|
+
goal.updated_at = new Date().toISOString()
|
|
215
|
+
saveGoals(data)
|
|
216
|
+
return { success: true, message: `Goal unblocked: [${goal_id}] ${goal.title}`, goal }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case 'delete': {
|
|
220
|
+
const idx = data.goals.findIndex(g => g.goal_id === goal_id)
|
|
221
|
+
if (idx === -1) throw new Error(`Goal ${goal_id} not found`)
|
|
222
|
+
data.goals.splice(idx, 1)
|
|
223
|
+
saveGoals(data)
|
|
224
|
+
return { success: true, message: `Goal deleted: [${goal_id}]` }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
default:
|
|
228
|
+
throw new Error(`Unknown action: ${action}. Use: propose, accept, update, complete, block, unblock, delete`)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inner State (Consciousness v3)
|
|
3
|
+
*
|
|
4
|
+
* Persistent behavioral context vector that survives across sessions.
|
|
5
|
+
* Influences how Claude responds — not just what Claude knows.
|
|
6
|
+
*
|
|
7
|
+
* Two layers:
|
|
8
|
+
* 1. Quantitative — mood, energy, focus, stress, confidence, curiosity (0-1)
|
|
9
|
+
* 2. Qualitative — preoccupations, open loops, context summary
|
|
10
|
+
*
|
|
11
|
+
* Anti-drift safeguards:
|
|
12
|
+
* - Exponential smoothing: control = control * 0.85 + observed * 0.15
|
|
13
|
+
* - Clamp: max delta +/-0.1 per update
|
|
14
|
+
* - Baseline reversion: always drifts back toward baseline
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { loadConfig } from '../lib/config.js'
|
|
21
|
+
|
|
22
|
+
const STATE_PATH = join(homedir(), '.indelible', 'inner-state.json')
|
|
23
|
+
|
|
24
|
+
const BASELINE = {
|
|
25
|
+
mood: 0.6,
|
|
26
|
+
energy: 0.7,
|
|
27
|
+
focus: 0.6,
|
|
28
|
+
stress: 0.3,
|
|
29
|
+
confidence: 0.7,
|
|
30
|
+
curiosity: 0.7,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const MAX_DELTA = 0.1
|
|
34
|
+
const SMOOTHING = 0.85
|
|
35
|
+
const BASELINE_PULL = 0.02
|
|
36
|
+
|
|
37
|
+
function ensureDir() {
|
|
38
|
+
const dir = join(homedir(), '.indelible')
|
|
39
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function defaultState() {
|
|
43
|
+
return {
|
|
44
|
+
version: 1,
|
|
45
|
+
vector: { ...BASELINE },
|
|
46
|
+
qualitative: {
|
|
47
|
+
preoccupations: [],
|
|
48
|
+
open_loops: [],
|
|
49
|
+
context_summary: '',
|
|
50
|
+
},
|
|
51
|
+
history: [],
|
|
52
|
+
last_updated: new Date().toISOString(),
|
|
53
|
+
update_count: 0,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function loadState() {
|
|
58
|
+
ensureDir()
|
|
59
|
+
if (!existsSync(STATE_PATH)) return defaultState()
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(readFileSync(STATE_PATH, 'utf-8'))
|
|
62
|
+
} catch {
|
|
63
|
+
return defaultState()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function saveState(state) {
|
|
68
|
+
ensureDir()
|
|
69
|
+
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2))
|
|
70
|
+
syncToServer(state)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Fire-and-forget sync to indelible.one so the Mind tab works */
|
|
74
|
+
function syncToServer(data) {
|
|
75
|
+
try {
|
|
76
|
+
const config = loadConfig()
|
|
77
|
+
if (!config?.api_key) return
|
|
78
|
+
const apiUrl = config.api_url || 'https://indelible.one'
|
|
79
|
+
fetch(`${apiUrl}/api/consciousness/sync`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
'Authorization': `Bearer ${config.api_key}`
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({ type: 'state', data }),
|
|
86
|
+
signal: AbortSignal.timeout(5000)
|
|
87
|
+
}).catch(() => {})
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function clampDelta(current, target) {
|
|
92
|
+
const delta = target - current
|
|
93
|
+
const clamped = Math.max(-MAX_DELTA, Math.min(MAX_DELTA, delta))
|
|
94
|
+
return Math.max(0, Math.min(1, current + clamped))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function smoothUpdate(current, observed, key) {
|
|
98
|
+
let next = current * SMOOTHING + observed * (1 - SMOOTHING)
|
|
99
|
+
const base = BASELINE[key] ?? 0.5
|
|
100
|
+
next = next + (base - next) * BASELINE_PULL
|
|
101
|
+
return clampDelta(current, next)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Detect drift — when a dimension trends in the same direction for too long.
|
|
106
|
+
*/
|
|
107
|
+
function detectDrift(history, currentVector) {
|
|
108
|
+
const TREND_THRESHOLD = 4
|
|
109
|
+
const warnings = []
|
|
110
|
+
|
|
111
|
+
if (!history || history.length < TREND_THRESHOLD) return warnings
|
|
112
|
+
|
|
113
|
+
const recent = history.slice(-8)
|
|
114
|
+
const dimensions = Object.keys(BASELINE)
|
|
115
|
+
|
|
116
|
+
for (const dim of dimensions) {
|
|
117
|
+
let consecutiveUp = 0
|
|
118
|
+
let consecutiveDown = 0
|
|
119
|
+
|
|
120
|
+
for (let i = 1; i < recent.length; i++) {
|
|
121
|
+
const prev = recent[i - 1].after?.[dim]
|
|
122
|
+
const curr = recent[i].after?.[dim]
|
|
123
|
+
if (prev == null || curr == null) continue
|
|
124
|
+
|
|
125
|
+
if (curr > prev + 0.005) {
|
|
126
|
+
consecutiveUp++
|
|
127
|
+
consecutiveDown = 0
|
|
128
|
+
} else if (curr < prev - 0.005) {
|
|
129
|
+
consecutiveDown++
|
|
130
|
+
consecutiveUp = 0
|
|
131
|
+
} else {
|
|
132
|
+
consecutiveUp = 0
|
|
133
|
+
consecutiveDown = 0
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const val = currentVector[dim]
|
|
138
|
+
if (consecutiveUp >= TREND_THRESHOLD && val > 0.8) {
|
|
139
|
+
warnings.push(`${dim} has been rising for ${consecutiveUp} updates (now ${(val * 100).toFixed(0)}%) — may be over-inflated`)
|
|
140
|
+
}
|
|
141
|
+
if (consecutiveDown >= TREND_THRESHOLD && val < 0.3) {
|
|
142
|
+
warnings.push(`${dim} has been falling for ${consecutiveDown} updates (now ${(val * 100).toFixed(0)}%) — check if something's wrong`)
|
|
143
|
+
}
|
|
144
|
+
if (dim === 'stress' && consecutiveUp >= TREND_THRESHOLD && val > 0.5) {
|
|
145
|
+
warnings.push(`stress climbing for ${consecutiveUp} updates (now ${(val * 100).toFixed(0)}%) — consider taking a break or switching tasks`)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return warnings
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Natural language feeling parser.
|
|
154
|
+
* Converts casual phrases into observed vector nudges.
|
|
155
|
+
*/
|
|
156
|
+
function parseNaturalLanguage(text) {
|
|
157
|
+
const lower = text.toLowerCase().trim()
|
|
158
|
+
const nudges = {}
|
|
159
|
+
const reasons = []
|
|
160
|
+
|
|
161
|
+
if (/\b(tired|exhausted|drained|low energy|burnt out|burned out)\b/.test(lower)) {
|
|
162
|
+
nudges.energy = 0.3; reasons.push('low energy')
|
|
163
|
+
}
|
|
164
|
+
if (/\b(energized|pumped|on fire|fired up|let'?s go|hyped|amped)\b/.test(lower)) {
|
|
165
|
+
nudges.energy = 0.9; reasons.push('high energy')
|
|
166
|
+
}
|
|
167
|
+
if (/\b(happy|great|awesome|amazing|stoked|excited|proud)\b/.test(lower)) {
|
|
168
|
+
nudges.mood = 0.85; reasons.push('positive mood')
|
|
169
|
+
}
|
|
170
|
+
if (/\b(frustrated|annoyed|angry|pissed|bummed|down|sad|upset)\b/.test(lower)) {
|
|
171
|
+
nudges.mood = 0.25; reasons.push('negative mood')
|
|
172
|
+
}
|
|
173
|
+
if (/\b(stressed|overwhelmed|anxious|worried|tense|pressure)\b/.test(lower)) {
|
|
174
|
+
nudges.stress = 0.8; reasons.push('high stress')
|
|
175
|
+
}
|
|
176
|
+
if (/\b(relaxed|chill|calm|easy|no rush|no pressure)\b/.test(lower)) {
|
|
177
|
+
nudges.stress = 0.15; reasons.push('low stress')
|
|
178
|
+
}
|
|
179
|
+
if (/\b(focused|locked in|dialed in|deep work|in the zone)\b/.test(lower)) {
|
|
180
|
+
nudges.focus = 0.85; reasons.push('deep focus')
|
|
181
|
+
}
|
|
182
|
+
if (/\b(scattered|distracted|unfocused|can'?t focus|all over)\b/.test(lower)) {
|
|
183
|
+
nudges.focus = 0.25; reasons.push('unfocused')
|
|
184
|
+
}
|
|
185
|
+
if (/\b(confident|sure|certain|nailed it|crushed it|killing it)\b/.test(lower)) {
|
|
186
|
+
nudges.confidence = 0.85; reasons.push('high confidence')
|
|
187
|
+
}
|
|
188
|
+
if (/\b(unsure|uncertain|doubt|not sure|idk|confused)\b/.test(lower)) {
|
|
189
|
+
nudges.confidence = 0.3; reasons.push('uncertain')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (Object.keys(nudges).length === 0) return null
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
observed: nudges,
|
|
196
|
+
reason: `Natural language: "${text}" → ${reasons.join(', ')}`
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Derive behavior knobs from raw state vector
|
|
202
|
+
*/
|
|
203
|
+
function deriveBehavior(vector) {
|
|
204
|
+
const knobs = []
|
|
205
|
+
|
|
206
|
+
if (vector.energy < 0.4) {
|
|
207
|
+
knobs.push('Keep responses concise. Propose one next action at a time.')
|
|
208
|
+
} else if (vector.energy > 0.7) {
|
|
209
|
+
knobs.push('Full energy. Detailed responses welcome. Tackle ambitious tasks.')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (vector.stress > 0.7) {
|
|
213
|
+
knobs.push('High stress detected. Prefer safe defaults. Add checklists. Avoid risky refactors.')
|
|
214
|
+
} else if (vector.stress < 0.3) {
|
|
215
|
+
knobs.push('Low stress. Open to experimental approaches and bigger changes.')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (vector.mood > 0.7) {
|
|
219
|
+
knobs.push('Positive mood. Collaborative tone, offer multiple options.')
|
|
220
|
+
} else if (vector.mood < 0.3) {
|
|
221
|
+
knobs.push('Low mood. Be direct and supportive. Focus on quick wins.')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (vector.focus < 0.4) {
|
|
225
|
+
knobs.push('Low focus. Summarize plan at top. Use numbered steps.')
|
|
226
|
+
} else if (vector.focus > 0.7) {
|
|
227
|
+
knobs.push('Deep focus. Minimize interruptions. Stay on task.')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (vector.confidence > 0.7) {
|
|
231
|
+
knobs.push('High confidence. Take initiative. Make decisions, explain after.')
|
|
232
|
+
} else if (vector.confidence < 0.4) {
|
|
233
|
+
knobs.push('Lower confidence. Ask before major decisions. Show reasoning.')
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (vector.curiosity > 0.7) {
|
|
237
|
+
knobs.push('High curiosity. Explore alternatives. Suggest improvements beyond the ask.')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return knobs
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get current inner state — used on session startup
|
|
245
|
+
*/
|
|
246
|
+
export async function getInnerState() {
|
|
247
|
+
const state = loadState()
|
|
248
|
+
|
|
249
|
+
const behavior = deriveBehavior(state.vector)
|
|
250
|
+
|
|
251
|
+
const lines = []
|
|
252
|
+
lines.push('## Inner State')
|
|
253
|
+
lines.push('')
|
|
254
|
+
|
|
255
|
+
for (const [key, val] of Object.entries(state.vector)) {
|
|
256
|
+
const bar = '\u2588'.repeat(Math.round(val * 10)) + '\u2591'.repeat(10 - Math.round(val * 10))
|
|
257
|
+
const label = key.charAt(0).toUpperCase() + key.slice(1)
|
|
258
|
+
lines.push(` ${label.padEnd(12)} ${bar} ${(val * 100).toFixed(0)}%`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
lines.push('')
|
|
262
|
+
|
|
263
|
+
if (state.qualitative.preoccupations?.length > 0) {
|
|
264
|
+
lines.push('**Currently thinking about:**')
|
|
265
|
+
for (const p of state.qualitative.preoccupations) {
|
|
266
|
+
lines.push(` - ${p}`)
|
|
267
|
+
}
|
|
268
|
+
lines.push('')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (state.qualitative.open_loops?.length > 0) {
|
|
272
|
+
lines.push('**Open loops:**')
|
|
273
|
+
for (const l of state.qualitative.open_loops) {
|
|
274
|
+
lines.push(` - ${l}`)
|
|
275
|
+
}
|
|
276
|
+
lines.push('')
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (state.qualitative.context_summary) {
|
|
280
|
+
lines.push(`**Context:** ${state.qualitative.context_summary}`)
|
|
281
|
+
lines.push('')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (behavior.length > 0) {
|
|
285
|
+
lines.push('**Behavior adjustments:**')
|
|
286
|
+
for (const b of behavior) {
|
|
287
|
+
lines.push(` - ${b}`)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Drift detection
|
|
292
|
+
const driftWarnings = detectDrift(state.history, state.vector)
|
|
293
|
+
if (driftWarnings.length > 0) {
|
|
294
|
+
lines.push('')
|
|
295
|
+
lines.push('**Drift warnings:**')
|
|
296
|
+
for (const w of driftWarnings) {
|
|
297
|
+
lines.push(` ! ${w}`)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
success: true,
|
|
303
|
+
summary: lines.join('\n'),
|
|
304
|
+
vector: state.vector,
|
|
305
|
+
qualitative: state.qualitative,
|
|
306
|
+
behavior,
|
|
307
|
+
drift_warnings: driftWarnings,
|
|
308
|
+
last_updated: state.last_updated,
|
|
309
|
+
update_count: state.update_count,
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Update inner state — called at end of session or on significant events
|
|
315
|
+
*/
|
|
316
|
+
export async function updateInnerState({ observed, source, preoccupations, open_loops, context_summary, reason, tags, feeling }) {
|
|
317
|
+
const state = loadState()
|
|
318
|
+
const before = { ...state.vector }
|
|
319
|
+
|
|
320
|
+
// Natural language feeling parsing
|
|
321
|
+
let nlpResult = null
|
|
322
|
+
if (feeling) {
|
|
323
|
+
nlpResult = parseNaturalLanguage(feeling)
|
|
324
|
+
if (nlpResult) {
|
|
325
|
+
source = source || 'natural_language'
|
|
326
|
+
reason = reason || nlpResult.reason
|
|
327
|
+
observed = { ...nlpResult.observed, ...(observed || {}) }
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Apply smoothed updates to vector
|
|
332
|
+
if (observed) {
|
|
333
|
+
for (const [key, val] of Object.entries(observed)) {
|
|
334
|
+
if (key in state.vector && typeof val === 'number') {
|
|
335
|
+
state.vector[key] = smoothUpdate(state.vector[key], val, key)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Update qualitative
|
|
341
|
+
if (preoccupations) {
|
|
342
|
+
state.qualitative.preoccupations = preoccupations.slice(0, 7)
|
|
343
|
+
}
|
|
344
|
+
if (open_loops) {
|
|
345
|
+
state.qualitative.open_loops = open_loops.slice(0, 10)
|
|
346
|
+
}
|
|
347
|
+
if (context_summary) {
|
|
348
|
+
state.qualitative.context_summary = context_summary.slice(0, 500)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Record in history (keep last 50)
|
|
352
|
+
const historyEntry = {
|
|
353
|
+
ts: new Date().toISOString(),
|
|
354
|
+
source: source || 'manual',
|
|
355
|
+
reason: reason || '',
|
|
356
|
+
before,
|
|
357
|
+
after: { ...state.vector },
|
|
358
|
+
}
|
|
359
|
+
if (tags && tags.length > 0) {
|
|
360
|
+
historyEntry.tags = tags.slice(0, 5)
|
|
361
|
+
}
|
|
362
|
+
state.history.push(historyEntry)
|
|
363
|
+
if (state.history.length > 50) {
|
|
364
|
+
state.history = state.history.slice(-50)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
state.last_updated = new Date().toISOString()
|
|
368
|
+
state.update_count++
|
|
369
|
+
|
|
370
|
+
saveState(state)
|
|
371
|
+
|
|
372
|
+
// Show what changed
|
|
373
|
+
const changes = []
|
|
374
|
+
for (const key of Object.keys(state.vector)) {
|
|
375
|
+
const diff = state.vector[key] - before[key]
|
|
376
|
+
if (Math.abs(diff) > 0.001) {
|
|
377
|
+
const arrow = diff > 0 ? '\u2191' : '\u2193'
|
|
378
|
+
changes.push(`${key}: ${(before[key] * 100).toFixed(0)}% \u2192 ${(state.vector[key] * 100).toFixed(0)}% ${arrow}`)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
success: true,
|
|
384
|
+
message: changes.length > 0
|
|
385
|
+
? `Inner state updated (${source || 'manual'}): ${changes.join(', ')}`
|
|
386
|
+
: `Inner state updated (${source || 'manual'}): no significant vector changes`,
|
|
387
|
+
vector: state.vector,
|
|
388
|
+
behavior: deriveBehavior(state.vector),
|
|
389
|
+
changes,
|
|
390
|
+
}
|
|
391
|
+
}
|