indelible-mcp 4.2.0 → 4.3.1

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/LICENSE ADDED
@@ -0,0 +1,57 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: Indelible Federation (zcooL)
6
+ Licensed Work: Indelible CLI / MCP
7
+ The Licensed Work is (c) 2026 Indelible Federation
8
+ Additional Use Grant: You may make use of the Licensed Work for personal,
9
+ non-commercial, educational, and evaluation purposes.
10
+ You may NOT use the Licensed Work to offer a competing
11
+ commercial blockchain storage, session saving, or
12
+ encrypted vault service without written permission
13
+ from the Licensor.
14
+ Change Date: April 30, 2030
15
+ Change License: MIT License
16
+
17
+ Terms
18
+
19
+ The Licensor hereby grants you the right to copy, modify, create derivative
20
+ works, redistribute, and make non-production use of the Licensed Work. The
21
+ Licensor may make an Additional Use Grant, above, permitting limited
22
+ production use.
23
+
24
+ Effective on the Change Date, or the fourth anniversary of the first publicly
25
+ available distribution of a specific version of the Licensed Work under this
26
+ License, whichever comes first, the Licensor hereby grants you rights under
27
+ the terms of the Change License, and the rights granted in the paragraph
28
+ above terminate.
29
+
30
+ If your use of the Licensed Work does not comply with the requirements
31
+ currently in effect as described in this License, you must purchase a
32
+ commercial license from the Licensor, its affiliated entities, or authorized
33
+ resellers, or you must refrain from using the Licensed Work.
34
+
35
+ All copies of the original and modified Licensed Work, and derivative works
36
+ of the Licensed Work, are subject to this License. This License applies
37
+ separately for each version of the Licensed Work and the Change Date may
38
+ vary for each version of the Licensed Work released by Licensor.
39
+
40
+ You must conspicuously display this License on each original or modified copy
41
+ of the Licensed Work. If you receive the Licensed Work in original or
42
+ modified form from a third party, the terms and conditions set forth in this
43
+ License apply to your use of that work.
44
+
45
+ Any use of the Licensed Work in violation of this License will automatically
46
+ terminate your rights under this License for the current and all other
47
+ versions of the Licensed Work.
48
+
49
+ This License does not grant you any right in any trademark or logo of
50
+ Licensor or its affiliates (provided that you may use a trademark or logo of
51
+ Licensor as expressly required by this License).
52
+
53
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
54
+ AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
55
+ EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
56
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
57
+ TITLE.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "4.2.0",
3
+ "version": "4.3.1",
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",
@@ -26,7 +26,7 @@
26
26
  "code-vault",
27
27
  "encrypted-storage"
28
28
  ],
29
- "license": "MIT",
29
+ "license": "BSL-1.1",
30
30
  "repository": {
31
31
  "type": "git",
32
32
  "url": "git+https://github.com/indelibleai/indelible-mcp.git"
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.1)
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.1',
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
  }
@@ -237,7 +237,7 @@ export async function checkProTier(address) {
237
237
  })
238
238
  if (res.ok) {
239
239
  const data = await res.json()
240
- if (data.plan === 'admin' || data.plan === 'pro' || data.plan === 'developer') {
240
+ if (data.active !== false && (data.plan === 'admin' || data.plan === 'pro' || data.plan === 'developer')) {
241
241
  return { ok: true, plan: data.plan }
242
242
  }
243
243
  return { ok: false, plan: data.plan || 'free', error: 'Pro tier required for Diary AI companion. Upgrade at indelible.one/pricing' }
@@ -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
+ }