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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "4.2.0",
3
+ "version": "4.3.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
@@ -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.2.0)
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.2.0',
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
+ }