openfeelz 0.9.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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +341 -0
  3. package/dist/index.d.ts +21 -0
  4. package/dist/index.js +240 -0
  5. package/dist/src/analysis/analyzer.d.ts +51 -0
  6. package/dist/src/analysis/analyzer.js +206 -0
  7. package/dist/src/classify/classifier.d.ts +56 -0
  8. package/dist/src/classify/classifier.js +224 -0
  9. package/dist/src/cli/cli.d.ts +23 -0
  10. package/dist/src/cli/cli.js +234 -0
  11. package/dist/src/format/prompt-formatter.d.ts +34 -0
  12. package/dist/src/format/prompt-formatter.js +158 -0
  13. package/dist/src/format/status-markdown.d.ts +9 -0
  14. package/dist/src/format/status-markdown.js +86 -0
  15. package/dist/src/hook/hooks.d.ts +49 -0
  16. package/dist/src/hook/hooks.js +132 -0
  17. package/dist/src/http/dashboard-html.generated.d.ts +2 -0
  18. package/dist/src/http/dashboard-html.generated.js +2 -0
  19. package/dist/src/http/dashboard.d.ts +28 -0
  20. package/dist/src/http/dashboard.js +518 -0
  21. package/dist/src/mcp/mcp-server.d.ts +37 -0
  22. package/dist/src/mcp/mcp-server.js +278 -0
  23. package/dist/src/migration/migrate-v1.d.ts +34 -0
  24. package/dist/src/migration/migrate-v1.js +63 -0
  25. package/dist/src/model/custom-taxonomy.d.ts +39 -0
  26. package/dist/src/model/custom-taxonomy.js +78 -0
  27. package/dist/src/model/decay.d.ts +32 -0
  28. package/dist/src/model/decay.js +58 -0
  29. package/dist/src/model/emotion-model.d.ts +40 -0
  30. package/dist/src/model/emotion-model.js +121 -0
  31. package/dist/src/model/goal-modulation.d.ts +43 -0
  32. package/dist/src/model/goal-modulation.js +105 -0
  33. package/dist/src/model/mapping.d.ts +32 -0
  34. package/dist/src/model/mapping.js +212 -0
  35. package/dist/src/model/personality.d.ts +45 -0
  36. package/dist/src/model/personality.js +181 -0
  37. package/dist/src/model/rumination.d.ts +47 -0
  38. package/dist/src/model/rumination.js +118 -0
  39. package/dist/src/paths.d.ts +40 -0
  40. package/dist/src/paths.js +106 -0
  41. package/dist/src/state/multi-agent.d.ts +32 -0
  42. package/dist/src/state/multi-agent.js +83 -0
  43. package/dist/src/state/state-file.d.ts +32 -0
  44. package/dist/src/state/state-file.js +113 -0
  45. package/dist/src/state/state-manager.d.ts +50 -0
  46. package/dist/src/state/state-manager.js +231 -0
  47. package/dist/src/tool/emotion-tool.d.ts +40 -0
  48. package/dist/src/tool/emotion-tool.js +221 -0
  49. package/dist/src/types.d.ts +219 -0
  50. package/dist/src/types.js +97 -0
  51. package/openclaw.plugin.json +164 -0
  52. package/package.json +65 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenFeelz contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,341 @@
1
+ # OpenFeelz
2
+
3
+ > *"Let's build robots with Genuine People Personalities, they said. So they tried it out with me. I'm a personality prototype. You can tell can't you?"*
4
+ > -- Douglas Adams, *The Hitchhiker's Guide to the Galaxy*
5
+
6
+ [![CI](https://github.com/trianglegrrl/openfeelz/actions/workflows/ci.yml/badge.svg)](https://github.com/trianglegrrl/openfeelz/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org/)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
10
+ [![OpenClaw Plugin](https://img.shields.io/badge/OpenClaw-plugin-purple.svg)](https://openclaw.com)
11
+
12
+ An [OpenClaw](https://openclaw.com) plugin that gives AI agents a multidimensional emotional model with personality-influenced decay, rumination, and multi-agent awareness.
13
+
14
+ Most agents vibes-check each message independently and forget everything between turns. OpenFeelz gives them emotional short-term memory -- the agent knows you've been frustrated for the last three messages, and it carries that context forward. It's not sentience, it's just better interaction design. (But it's pretty cool.)
15
+
16
+ ## Features
17
+
18
+ - **PAD Dimensional Model** -- Pleasure, Arousal, Dominance + Connection, Curiosity, Energy, Trust
19
+ - **Ekman Basic Emotions** -- Happiness, Sadness, Anger, Fear, Disgust, Surprise
20
+ - **OCEAN Personality** -- Big Five traits influence baselines, decay rates, and response intensity
21
+ - **Exponential Decay** -- Emotions fade toward personality-influenced baselines over time
22
+ - **Rumination Engine** -- Intense emotions continue to influence state across interactions
23
+ - **Goal-Aware Modulation** -- Personality-inferred goals amplify relevant emotions
24
+ - **Multi-Agent Awareness** -- Agents see other agents' emotional states in the system prompt
25
+ - **Custom Taxonomy** -- Define your own emotion labels with dimension mappings
26
+ - **LLM Classification** -- Automatically classify user/agent emotions via OpenAI-compatible models
27
+ - **Web Dashboard** -- Glassmorphism UI at `/emotion-dashboard`
28
+ - **MCP Server** -- Expose emotional state to Cursor, Claude Desktop, etc.
29
+ - **CLI Tools** -- `openclaw emotion status`, `reset`, `personality`, `history`, `decay`
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ openclaw plugins install openfeelz
35
+ openclaw plugins enable openfeelz
36
+ ```
37
+
38
+ ## How It Works
39
+
40
+ Every agent turn, OpenFeelz hooks into the lifecycle:
41
+
42
+ ```
43
+ User sends a message
44
+ |
45
+ v
46
+ [before_agent_start hook]
47
+ |
48
+ v
49
+ 1. Load emotion state from disk
50
+ 2. Apply exponential decay based on elapsed time
51
+ 3. Advance any active rumination entries
52
+ 4. Format state into an <emotion_state> XML block
53
+ 5. Return as "prependContext" to OpenClaw
54
+ |
55
+ v
56
+ Agent sees emotional context in its system prompt
57
+ |
58
+ v
59
+ [Agent responds]
60
+ |
61
+ v
62
+ [agent_end hook]
63
+ |
64
+ v
65
+ 1. Classify emotions in user + agent messages via LLM
66
+ 2. Map to dimensional changes
67
+ 3. Start rumination if intensity exceeds threshold
68
+ 4. Save updated state to disk
69
+ ```
70
+
71
+ ### What the Agent Sees
72
+
73
+ The plugin prepends an `<emotion_state>` block to the system prompt:
74
+
75
+ ```xml
76
+ <emotion_state>
77
+ <dimensions>
78
+ pleasure: lowered (-0.12), arousal: elevated (0.18), curiosity: elevated (0.72)
79
+ </dimensions>
80
+ <user>
81
+ 2026-02-06 09:15: Felt strongly frustrated because deployment keeps failing.
82
+ 2026-02-06 08:40: Felt moderately anxious because tight deadline approaching.
83
+ Trend (last 24h): mostly frustrated.
84
+ </user>
85
+ <agent>
86
+ 2026-02-06 09:10: Felt moderately focused because working through error logs.
87
+ </agent>
88
+ <others>
89
+ research-agent — 2026-02-06 08:00: Felt mildly curious because investigating new library.
90
+ </others>
91
+ </emotion_state>
92
+ ```
93
+
94
+ - **`<dimensions>`** -- PAD dimensions that deviate >0.15 from personality baseline
95
+ - **`<user>`** -- Last 3 classified user emotions with timestamps, intensity, and triggers
96
+ - **`<agent>`** -- Last 2 agent emotions (continuity across turns)
97
+ - **`<others>`** -- Other agents' recent emotional states (up to `maxOtherAgents`)
98
+
99
+ The block only appears when there's something to show. Set `contextEnabled: false` to disable injection while keeping classification, decay, and the dashboard active.
100
+
101
+ ## Decay Model
102
+
103
+ Emotions return to personality-influenced baselines via exponential decay:
104
+
105
+ ```
106
+ newValue = baseline + (currentValue - baseline) * e^(-rate * elapsedHours)
107
+ halfLife = ln(2) / rate
108
+ ```
109
+
110
+ ### Default Rates
111
+
112
+ | Dimension / Emotion | Rate (per hour) | Half-Life | Notes |
113
+ |---------------------|-----------------|-----------|-------|
114
+ | Pleasure | 0.058 | ~12h | |
115
+ | Arousal | 0.087 | ~8h | Activation calms quickly |
116
+ | Dominance | 0.046 | ~15h | Sense of control shifts slowly |
117
+ | Connection | 0.035 | ~20h | Social bonds persist |
118
+ | Curiosity | 0.058 | ~12h | |
119
+ | Energy | 0.046 | ~15h | |
120
+ | Trust | 0.035 | ~20h | Hard-won, slow to fade |
121
+ | Happiness | 0.058 | ~12h | |
122
+ | Sadness | 0.046 | ~15h | Lingers longer than joy |
123
+ | Anger | 0.058 | ~12h | |
124
+ | Fear | 0.058 | ~12h | |
125
+ | Disgust | 0.046 | ~15h | |
126
+ | Surprise | 0.139 | ~5h | Fades the fastest |
127
+
128
+ ### Personality Modulation
129
+
130
+ OCEAN traits adjust decay rates:
131
+
132
+ - **High neuroticism** -- Negative emotions linger (~0.84-0.88x decay rate)
133
+ - **High extraversion** -- Sadness fades faster (~1.16x), arousal/pleasure recover quicker
134
+ - **High agreeableness** -- Anger fades faster (~1.12x), connection decays slower
135
+ - **High openness** -- Curiosity and surprise persist longer
136
+
137
+ ### When Decay Runs
138
+
139
+ Decay is computed on-demand, not on a timer:
140
+
141
+ 1. **`before_agent_start`** -- Primary mechanism. Applied based on elapsed time since last update.
142
+ 2. **Tool `query` action** -- Decay applied before reading, so values are accurate.
143
+ 3. **Optional background service** -- Set `decayServiceEnabled: true` for dashboard accuracy between interactions.
144
+
145
+ ### Configuring Decay
146
+
147
+ Three levels of control:
148
+
149
+ - **Global half-life** -- `halfLifeHours: 6` makes everything fade 2x faster
150
+ - **Per-dimension overrides** -- `"decayRates": { "pleasure": 0.1, "trust": 0.02 }`
151
+ - **Personality-driven** -- Change OCEAN traits and rates recalculate automatically
152
+
153
+ ## Configuration
154
+
155
+ In `~/.openclaw/openclaw.json` under `plugins.entries.openfeelz.config`:
156
+
157
+ ```json
158
+ {
159
+ "plugins": {
160
+ "entries": {
161
+ "openfeelz": {
162
+ "config": {
163
+ "apiKey": "${OPENAI_API_KEY}",
164
+ "model": "gpt-5-mini",
165
+ "halfLifeHours": 12,
166
+ "ruminationEnabled": true,
167
+ "personality": {
168
+ "openness": 0.7,
169
+ "conscientiousness": 0.6,
170
+ "extraversion": 0.5,
171
+ "agreeableness": 0.8,
172
+ "neuroticism": 0.3
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ Also configurable via the OpenClaw web UI.
182
+
183
+ ### Environment Variables
184
+
185
+ | Variable | Default | Description |
186
+ |----------|---------|-------------|
187
+ | `OPENAI_API_KEY` | _(required)_ | API key for LLM emotion classification |
188
+ | `OPENAI_BASE_URL` | `https://api.openai.com/v1` | Custom API base URL |
189
+ | `EMOTION_MODEL` | `gpt-5-mini` | Classification model (when OpenAI key present) |
190
+ | `EMOTION_CLASSIFIER_URL` | _(none)_ | External HTTP classifier (bypasses LLM) |
191
+ | `EMOTION_HALF_LIFE_HOURS` | `12` | Global decay half-life |
192
+ | `EMOTION_CONFIDENCE_MIN` | `0.35` | Min confidence threshold |
193
+ | `EMOTION_HISTORY_SIZE` | `100` | Max stored stimuli per agent |
194
+ | `EMOTION_TIMEZONE` | _(system)_ | IANA timezone for display |
195
+
196
+ ### Full Options Reference
197
+
198
+ | Option | Type | Default | Description |
199
+ |--------|------|---------|-------------|
200
+ | `apiKey` | string | `$OPENAI_API_KEY` | API key for LLM classification |
201
+ | `baseUrl` | string | OpenAI default | API base URL |
202
+ | `model` | string | `claude-sonnet-4-5` / `gpt-5-mini` | Classification model (auto-selected by available API key) |
203
+ | `classifierUrl` | string | _(none)_ | External classifier URL |
204
+ | `confidenceMin` | number | `0.35` | Min confidence threshold |
205
+ | `halfLifeHours` | number | `12` | Global decay half-life |
206
+ | `trendWindowHours` | number | `24` | Trend computation window |
207
+ | `maxHistory` | number | `100` | Max stored stimuli |
208
+ | `ruminationEnabled` | boolean | `true` | Enable rumination engine |
209
+ | `ruminationThreshold` | number | `0.7` | Intensity threshold for rumination |
210
+ | `ruminationMaxStages` | number | `4` | Max rumination stages |
211
+ | `realtimeClassification` | boolean | `false` | Classify on every message |
212
+ | `contextEnabled` | boolean | `true` | Prepend emotion context to prompt |
213
+ | `decayServiceEnabled` | boolean | `false` | Background decay service |
214
+ | `decayServiceIntervalMinutes` | number | `30` | Decay service interval |
215
+ | `dashboardEnabled` | boolean | `true` | Serve web dashboard |
216
+ | `timezone` | string | _(system)_ | IANA timezone |
217
+ | `maxOtherAgents` | number | `3` | Max other agents in prompt |
218
+ | `emotionLabels` | string[] | _(21 built-in)_ | Custom label taxonomy |
219
+ | `personality` | object | all `0.5` | OCEAN trait values |
220
+ | `decayRates` | object | _(see table)_ | Per-dimension rate overrides |
221
+ | `dimensionBaselines` | object | _(computed)_ | Per-dimension baseline overrides |
222
+
223
+ ## Agent Tool: `emotion_state`
224
+
225
+ The agent can inspect and modify its own emotional state:
226
+
227
+ | Action | Description | Parameters |
228
+ |--------|-------------|------------|
229
+ | `query` | Get current emotional state | `format?: "full" / "summary" / "dimensions" / "emotions"` |
230
+ | `modify` | Apply an emotional stimulus | `emotion, intensity?, trigger?` |
231
+ | `set_dimension` | Set or adjust a dimension | `dimension, value?` or `dimension, delta?` |
232
+ | `reset` | Reset to personality baseline | `dimensions?` (comma-separated, or all) |
233
+ | `set_personality` | Set an OCEAN trait | `trait, value` |
234
+ | `get_personality` | Get current OCEAN profile | _(none)_ |
235
+
236
+ ## CLI
237
+
238
+ ```bash
239
+ openclaw emotion status # Formatted state with bars
240
+ openclaw emotion status --json # Raw JSON
241
+ openclaw emotion personality # OCEAN profile
242
+ openclaw emotion personality set --trait openness --value 0.8
243
+ openclaw emotion reset # Reset all to baseline
244
+ openclaw emotion reset --dimensions pleasure,arousal
245
+ openclaw emotion history --limit 20 # Recent stimuli
246
+ openclaw emotion decay --dimension pleasure --rate 0.05
247
+ ```
248
+
249
+ ## Dashboard
250
+
251
+ `http://localhost:<gateway-port>/emotion-dashboard`
252
+
253
+ Real-time visualization of PAD dimensions, basic emotions, OCEAN profile, recent stimuli, and active rumination. Append `?format=json` for the raw API.
254
+
255
+ ## MCP Server
256
+
257
+ Works with any MCP-compatible client (Cursor, Claude Desktop, etc.):
258
+
259
+ ```json
260
+ {
261
+ "mcpServers": {
262
+ "openfeelz": {
263
+ "command": "npx",
264
+ "args": ["openfeelz/mcp"]
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ **Resources:** `emotion://state`, `emotion://personality`
271
+
272
+ **Tools:** `query_emotion`, `modify_emotion`, `set_personality`
273
+
274
+ ## Migration from v1
275
+
276
+ ```bash
277
+ openclaw hooks disable emotion-state
278
+ openclaw emotion migrate
279
+ ```
280
+
281
+ Converts v1 state files (flat labels + string intensities) to v2 format (dimensional model + numeric intensities). Uses a separate state file (`openfeelz.json`), so no risk of data loss.
282
+
283
+ ## Architecture
284
+
285
+ ```
286
+ index.ts Plugin entry: registers tool, hooks, service, CLI, dashboard
287
+ src/
288
+ types.ts All interfaces (DimensionalState, BasicEmotions, OCEANProfile, etc.)
289
+ model/
290
+ emotion-model.ts Core model: clamping, primary detection, intensity, deltas
291
+ personality.ts OCEAN: baselines, decay rates, rumination probability
292
+ decay.ts Exponential decay toward personality-influenced baselines
293
+ mapping.ts Emotion label -> dimension/emotion delta mapping (60+ labels)
294
+ rumination.ts Multi-stage internal processing for intense emotions
295
+ goal-modulation.ts Personality-inferred goals amplify relevant emotions
296
+ custom-taxonomy.ts User-defined emotion labels with custom mappings
297
+ state/
298
+ state-manager.ts Orchestrator: classify + map + decay + ruminate + persist
299
+ state-file.ts Atomic JSON I/O with file locking
300
+ multi-agent.ts Scan sibling agent states for awareness
301
+ classify/
302
+ classifier.ts Unified LLM + HTTP classifier with fallback
303
+ tool/
304
+ emotion-tool.ts OpenClaw tool: query/modify/reset/personality
305
+ hook/
306
+ hooks.ts before_agent_start + agent_end hooks
307
+ cli/
308
+ cli.ts Commander.js CLI commands
309
+ http/
310
+ dashboard.ts Glassmorphism HTML dashboard
311
+ mcp/
312
+ mcp-server.ts MCP server resources + tools
313
+ format/
314
+ prompt-formatter.ts System prompt <emotion_state> block builder
315
+ migration/
316
+ migrate-v1.ts v1 -> v2 converter
317
+ ```
318
+
319
+ ## Development
320
+
321
+ ```bash
322
+ npm install
323
+ npm test # Run all tests
324
+ npm run test:watch # Watch mode
325
+ npm run test:coverage # Coverage report
326
+ npm run typecheck # TypeScript strict mode
327
+ npm run lint # oxlint
328
+ npm run build # Compile to dist/
329
+ ```
330
+
331
+ ## Contributing
332
+
333
+ Issues, PRs, and questions are all welcome. If you want to poke around the model or improve it, please do -- I'd love to collaborate. :)
334
+
335
+ ## License
336
+
337
+ [MIT](LICENSE)
338
+
339
+ ---
340
+
341
+ Made with ❤️ by [@trianglegrrl](https://github.com/trianglegrrl) for the OpenClaw community 🦞
@@ -0,0 +1,21 @@
1
+ /**
2
+ * OpenFeelz - OpenClaw Plugin Entry Point
3
+ *
4
+ * Registers:
5
+ * - emotion_state tool (query/modify/reset/personality)
6
+ * - before_agent_start hook (inject emotional context)
7
+ * - agent_end hook (classify emotions from conversation)
8
+ * - background service (optional periodic decay)
9
+ * - CLI commands (openclaw emotion ...)
10
+ * - HTTP dashboard route (/emotion-dashboard)
11
+ *
12
+ * State is stored per-agent in each agent's workspace:
13
+ * {workspace}/openfeelz.json
14
+ */
15
+ declare const emotionEnginePlugin: {
16
+ id: string;
17
+ name: string;
18
+ description: string;
19
+ register(api: any): void;
20
+ };
21
+ export default emotionEnginePlugin;
package/dist/index.js ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * OpenFeelz - OpenClaw Plugin Entry Point
3
+ *
4
+ * Registers:
5
+ * - emotion_state tool (query/modify/reset/personality)
6
+ * - before_agent_start hook (inject emotional context)
7
+ * - agent_end hook (classify emotions from conversation)
8
+ * - background service (optional periodic decay)
9
+ * - CLI commands (openclaw emotion ...)
10
+ * - HTTP dashboard route (/emotion-dashboard)
11
+ *
12
+ * State is stored per-agent in each agent's workspace:
13
+ * {workspace}/openfeelz.json
14
+ */
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { DEFAULT_CONFIG } from "./src/types.js";
18
+ import { StateManager } from "./src/state/state-manager.js";
19
+ import { resolveAgentDir, resolveAgentStatePath, listAgentIds } from "./src/paths.js";
20
+ import { createEmotionTool } from "./src/tool/emotion-tool.js";
21
+ import { createBootstrapHook, createAgentEndHook } from "./src/hook/hooks.js";
22
+ import { registerEmotionCli } from "./src/cli/cli.js";
23
+ import { createDashboardHandler } from "./src/http/dashboard.js";
24
+ import { analyzePersonalityViaLLM, describeEmotionalStateViaLLM } from "./src/analysis/analyzer.js";
25
+ /**
26
+ * Resolve plugin configuration from raw pluginConfig + environment variables.
27
+ */
28
+ function resolveConfig(raw) {
29
+ const env = process.env;
30
+ const personality = (raw?.personality ?? {});
31
+ const apiKey = raw?.apiKey ?? env.ANTHROPIC_API_KEY ?? env.OPENAI_API_KEY ?? undefined;
32
+ const explicitModel = raw?.model ?? env.EMOTION_MODEL;
33
+ const hasAnthropicKey = !!(raw?.apiKey || env.ANTHROPIC_API_KEY);
34
+ const hasOpenAIKey = !!(raw?.apiKey || env.OPENAI_API_KEY);
35
+ let model = explicitModel ?? DEFAULT_CONFIG.model;
36
+ let baseUrl = raw?.baseUrl ?? env.OPENAI_BASE_URL ?? DEFAULT_CONFIG.baseUrl;
37
+ if (!explicitModel && hasOpenAIKey && !hasAnthropicKey) {
38
+ model = "gpt-5-mini";
39
+ baseUrl = baseUrl || "https://api.openai.com/v1";
40
+ }
41
+ return {
42
+ apiKey,
43
+ baseUrl,
44
+ model,
45
+ provider: raw?.provider ?? undefined,
46
+ classifierUrl: raw?.classifierUrl ?? env.EMOTION_CLASSIFIER_URL ?? undefined,
47
+ confidenceMin: raw?.confidenceMin ?? (Number(env.EMOTION_CONFIDENCE_MIN) || DEFAULT_CONFIG.confidenceMin),
48
+ halfLifeHours: raw?.halfLifeHours ?? (Number(env.EMOTION_HALF_LIFE_HOURS) || DEFAULT_CONFIG.halfLifeHours),
49
+ trendWindowHours: raw?.trendWindowHours ?? DEFAULT_CONFIG.trendWindowHours,
50
+ maxHistory: raw?.maxHistory ?? (Number(env.EMOTION_HISTORY_SIZE) || DEFAULT_CONFIG.maxHistory),
51
+ ruminationEnabled: raw?.ruminationEnabled ?? DEFAULT_CONFIG.ruminationEnabled,
52
+ ruminationThreshold: raw?.ruminationThreshold ?? DEFAULT_CONFIG.ruminationThreshold,
53
+ ruminationMaxStages: raw?.ruminationMaxStages ?? DEFAULT_CONFIG.ruminationMaxStages,
54
+ realtimeClassification: raw?.realtimeClassification ?? DEFAULT_CONFIG.realtimeClassification,
55
+ contextEnabled: raw?.contextEnabled ?? DEFAULT_CONFIG.contextEnabled,
56
+ decayServiceEnabled: raw?.decayServiceEnabled ?? DEFAULT_CONFIG.decayServiceEnabled,
57
+ decayServiceIntervalMinutes: raw?.decayServiceIntervalMinutes ?? DEFAULT_CONFIG.decayServiceIntervalMinutes,
58
+ dashboardEnabled: raw?.dashboardEnabled ?? DEFAULT_CONFIG.dashboardEnabled,
59
+ timezone: raw?.timezone ?? env.EMOTION_TIMEZONE ?? undefined,
60
+ maxOtherAgents: raw?.maxOtherAgents ?? DEFAULT_CONFIG.maxOtherAgents,
61
+ emotionLabels: raw?.emotionLabels ?? DEFAULT_CONFIG.emotionLabels,
62
+ personality: {
63
+ openness: personality.openness ?? DEFAULT_CONFIG.personality.openness,
64
+ conscientiousness: personality.conscientiousness ?? DEFAULT_CONFIG.personality.conscientiousness,
65
+ extraversion: personality.extraversion ?? DEFAULT_CONFIG.personality.extraversion,
66
+ agreeableness: personality.agreeableness ?? DEFAULT_CONFIG.personality.agreeableness,
67
+ neuroticism: personality.neuroticism ?? DEFAULT_CONFIG.personality.neuroticism,
68
+ },
69
+ decayRateOverrides: raw?.decayRates ?? {},
70
+ dimensionBaselineOverrides: raw?.dimensionBaselines ?? {},
71
+ };
72
+ }
73
+ /**
74
+ * Attempt to resolve an Anthropic API key from OpenClaw's auth-profiles.json.
75
+ * Falls back gracefully if the file doesn't exist or has no Anthropic profile.
76
+ */
77
+ function resolveApiKeyFromAuthProfiles(api, agentId = "main") {
78
+ try {
79
+ const agentDir = resolveAgentDir(api.config, agentId);
80
+ const authFile = path.join(agentDir, "auth-profiles.json");
81
+ if (!fs.existsSync(authFile))
82
+ return undefined;
83
+ const raw = JSON.parse(fs.readFileSync(authFile, "utf8"));
84
+ const profiles = raw?.profiles ?? {};
85
+ for (const profile of Object.values(profiles)) {
86
+ if (profile?.provider === "anthropic" && profile?.token) {
87
+ return profile.token;
88
+ }
89
+ }
90
+ }
91
+ catch {
92
+ // Not critical
93
+ }
94
+ return undefined;
95
+ }
96
+ const emotionEnginePlugin = {
97
+ id: "openfeelz",
98
+ name: "OpenFeelz",
99
+ description: "PAD + Ekman + OCEAN emotional model with personality-influenced decay, " +
100
+ "rumination, and multi-agent awareness",
101
+ register(api) {
102
+ const config = resolveConfig(api.pluginConfig);
103
+ // Resolve API key from OpenClaw auth profiles if not explicitly configured
104
+ if (!config.apiKey) {
105
+ const resolvedKey = resolveApiKeyFromAuthProfiles(api);
106
+ if (resolvedKey) {
107
+ config.apiKey = resolvedKey;
108
+ }
109
+ }
110
+ const cfg = api.config;
111
+ // Per-agent StateManager cache (state path = workspace/openfeelz.json)
112
+ const managerCache = new Map();
113
+ const getManager = (agentId) => {
114
+ const id = agentId?.trim() || "main";
115
+ let m = managerCache.get(id);
116
+ if (!m) {
117
+ const statePath = resolveAgentStatePath(cfg, id);
118
+ m = new StateManager(statePath, config);
119
+ managerCache.set(id, m);
120
+ }
121
+ return m;
122
+ };
123
+ const defaultStatePath = resolveAgentStatePath(cfg, "main");
124
+ api.logger?.info?.(`openfeelz: registered (state: ${defaultStatePath}, model: ${config.model}, provider: ${config.provider ?? "auto"})`);
125
+ // -- Tool -- (uses main agent when agent context not available)
126
+ api.registerTool(createEmotionTool(getManager("main")), { name: "emotion_state" });
127
+ // -- Hooks --
128
+ const bootstrapHandler = createBootstrapHook(getManager, config, cfg);
129
+ api.on("before_agent_start", async (event) => {
130
+ const agentId = event.agentId ?? "main";
131
+ const result = await bootstrapHandler({
132
+ prompt: event.prompt ?? "",
133
+ userKey: event.senderId ?? event.sessionKey ?? "unknown",
134
+ agentId,
135
+ });
136
+ return result;
137
+ });
138
+ const agentEndHandler = createAgentEndHook(getManager, config);
139
+ api.on("agent_end", async (event) => {
140
+ const agentId = event.agentId ?? "main";
141
+ await agentEndHandler({
142
+ success: event.success ?? true,
143
+ messages: event.messages ?? [],
144
+ userKey: event.senderId ?? event.sessionKey ?? "unknown",
145
+ agentId,
146
+ });
147
+ });
148
+ // -- Service (background analysis: startup + every 30m) --
149
+ if (config.apiKey) {
150
+ let analysisIntervalHandle = null;
151
+ const runAnalysis = async () => {
152
+ try {
153
+ for (const agentId of listAgentIds(cfg)) {
154
+ const manager = getManager(agentId);
155
+ const state = await manager.getState();
156
+ const opts = {
157
+ apiKey: config.apiKey,
158
+ model: config.model,
159
+ provider: config.provider,
160
+ baseUrl: config.baseUrl,
161
+ };
162
+ const [personality, emotionalState] = await Promise.all([
163
+ analyzePersonalityViaLLM(state, opts),
164
+ describeEmotionalStateViaLLM(state, opts),
165
+ ]);
166
+ const now = new Date().toISOString();
167
+ const updated = {
168
+ ...state,
169
+ cachedAnalysis: {
170
+ personality: { ...personality, generatedAt: now },
171
+ emotionalState: { ...emotionalState, generatedAt: now },
172
+ },
173
+ };
174
+ await manager.saveState(updated);
175
+ }
176
+ }
177
+ catch (err) {
178
+ api.logger?.error?.(`[openfeelz] Analysis service error: ${err}`);
179
+ }
180
+ };
181
+ api.registerService({
182
+ id: "openfeelz-analysis",
183
+ start: () => {
184
+ runAnalysis();
185
+ analysisIntervalHandle = setInterval(runAnalysis, 30 * 60_000);
186
+ api.logger?.info?.("openfeelz: analysis service started (interval: 30m)");
187
+ },
188
+ stop: () => {
189
+ if (analysisIntervalHandle) {
190
+ clearInterval(analysisIntervalHandle);
191
+ analysisIntervalHandle = null;
192
+ }
193
+ api.logger?.info?.("openfeelz: analysis service stopped");
194
+ },
195
+ });
196
+ }
197
+ // -- Service (optional background decay) --
198
+ if (config.decayServiceEnabled) {
199
+ let intervalHandle = null;
200
+ api.registerService({
201
+ id: "openfeelz-decay",
202
+ start: () => {
203
+ const ms = config.decayServiceIntervalMinutes * 60_000;
204
+ intervalHandle = setInterval(async () => {
205
+ try {
206
+ for (const agentId of listAgentIds(cfg)) {
207
+ const manager = getManager(agentId);
208
+ let state = await manager.getState();
209
+ state = manager.applyDecay(state);
210
+ state = manager.advanceRumination(state);
211
+ await manager.saveState(state);
212
+ }
213
+ }
214
+ catch (err) {
215
+ api.logger?.error?.(`[openfeelz] Decay service error: ${err}`);
216
+ }
217
+ }, ms);
218
+ api.logger?.info?.(`openfeelz: decay service started (interval: ${config.decayServiceIntervalMinutes}m)`);
219
+ },
220
+ stop: () => {
221
+ if (intervalHandle) {
222
+ clearInterval(intervalHandle);
223
+ intervalHandle = null;
224
+ }
225
+ api.logger?.info?.("openfeelz: decay service stopped");
226
+ },
227
+ });
228
+ }
229
+ // -- CLI --
230
+ api.registerCli(({ program }) => registerEmotionCli({ program, getManager, config }), { commands: ["emotion"] });
231
+ // -- HTTP Dashboard --
232
+ if (config.dashboardEnabled) {
233
+ api.registerHttpRoute({
234
+ path: "/emotion-dashboard",
235
+ handler: createDashboardHandler(getManager),
236
+ });
237
+ }
238
+ },
239
+ };
240
+ export default emotionEnginePlugin;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * LLM-backed personality and emotional state analysis.
3
+ *
4
+ * Uses direct HTTP calls with config.apiKey (same pattern as classifier).
5
+ * Supports Anthropic and OpenAI. Injectable fetchFn for testing.
6
+ */
7
+ import type { EmotionEngineState } from "../types.js";
8
+ export interface PersonalityAnalysisResult {
9
+ summary: string;
10
+ pad: {
11
+ pleasure: number;
12
+ arousal: number;
13
+ dominance: number;
14
+ };
15
+ extensions: {
16
+ connection: number;
17
+ curiosity: number;
18
+ energy: number;
19
+ trust: number;
20
+ };
21
+ ocean: {
22
+ openness: number;
23
+ conscientiousness: number;
24
+ extraversion: number;
25
+ agreeableness: number;
26
+ neuroticism: number;
27
+ };
28
+ }
29
+ export interface EmotionalStateResult {
30
+ summary: string;
31
+ primary: string;
32
+ intensity: number;
33
+ notes: string[];
34
+ }
35
+ /** Options for the analyzer functions. */
36
+ export interface AnalyzerOptions {
37
+ /** API key (Anthropic or OpenAI, depending on model). */
38
+ apiKey?: string;
39
+ /** Base URL override (for OpenAI-compatible endpoints). */
40
+ baseUrl?: string;
41
+ /** Model name for LLM analysis. */
42
+ model?: string;
43
+ /** Force a specific provider: "anthropic" | "openai". Auto-detected from model if omitted. */
44
+ provider?: "anthropic" | "openai";
45
+ /** Timeout in ms. */
46
+ timeoutMs?: number;
47
+ /** Injectable fetch function (for testing). */
48
+ fetchFn?: typeof fetch;
49
+ }
50
+ export declare function analyzePersonalityViaLLM(state: EmotionEngineState, opts: AnalyzerOptions): Promise<PersonalityAnalysisResult>;
51
+ export declare function describeEmotionalStateViaLLM(state: EmotionEngineState, opts: AnalyzerOptions): Promise<EmotionalStateResult>;