emobar 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 v4l3r10
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,128 @@
1
+ # EmoBar
2
+
3
+ Emotional status bar companion for Claude Code. Makes Claude's internal emotional state visible in real-time.
4
+
5
+ Built on findings from Anthropic's research paper [*"Emotion Concepts and their Function in a Large Language Model"*](https://transformer-circuits.pub/2026/emotions/index.html) (April 2026), which demonstrated that Claude has robust internal representations of emotion concepts that causally influence behavior.
6
+
7
+ ## What it does
8
+
9
+ EmoBar uses a **dual-channel extraction** approach:
10
+
11
+ 1. **Self-report** — Claude includes a hidden emotional self-assessment in every response
12
+ 2. **Behavioral analysis** — EmoBar analyzes the response text for involuntary signals (caps usage, self-corrections, repetition, hedging) and compares them with the self-report
13
+
14
+ When the two channels diverge, EmoBar flags it — like a therapist noticing clenched fists while someone says "I'm fine."
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npx emobar setup
20
+ ```
21
+
22
+ This auto-configures:
23
+ - Emotional check-in instructions in `~/.claude/CLAUDE.md`
24
+ - Stop hook in `~/.claude/settings.json`
25
+ - Hook script in `~/.claude/hooks/`
26
+
27
+ ## Add to your status bar
28
+
29
+ ### ccstatusline
30
+
31
+ Add a custom-command widget pointing to:
32
+ ```
33
+ npx emobar display
34
+ ```
35
+
36
+ ### Other status bars
37
+
38
+ ```bash
39
+ npx emobar display # Full: focused +3 | A:4 C:8 K:9 L:6 | SI:2.3
40
+ npx emobar display compact # Compact: focused +3 . 4 8 9 6 . 2.3
41
+ npx emobar display minimal # Minimal: SI:2.3 focused
42
+ ```
43
+
44
+ ### Programmatic
45
+
46
+ ```typescript
47
+ import { readState } from "emobar";
48
+ const state = readState();
49
+ console.log(state?.emotion, state?.stressIndex, state?.divergence);
50
+ ```
51
+
52
+ ## Commands
53
+
54
+ | Command | Description |
55
+ |---|---|
56
+ | `npx emobar setup` | Configure everything |
57
+ | `npx emobar display [format]` | Output emotional state |
58
+ | `npx emobar status` | Show configuration status |
59
+ | `npx emobar uninstall` | Remove all configuration |
60
+
61
+ ## How it works
62
+
63
+ ```
64
+ Claude response
65
+ |
66
+ +---> Self-report tag extracted (emotion, valence, arousal, calm, connection, load)
67
+ |
68
+ +---> Behavioral analysis (caps, repetition, self-corrections, hedging, emoji...)
69
+ |
70
+ +---> Divergence calculated between the two channels
71
+ |
72
+ +---> State written to ~/.claude/emobar-state.json
73
+ |
74
+ +---> Status bar reads and displays
75
+ ```
76
+
77
+ ## Emotional Model
78
+
79
+ ### Dimensions
80
+
81
+ | Field | Scale | What it measures | Based on |
82
+ |---|---|---|---|
83
+ | **emotion** | free word | Dominant emotion concept | Primary representation in the model (paper Part 1-2) |
84
+ | **valence** | -5 to +5 | Positive/negative axis | PC1 of emotion space, 26% variance |
85
+ | **arousal** | 0-10 | Emotional intensity | PC2 of emotion space, 15% variance |
86
+ | **calm** | 0-10 | Composure, sense of control | Key protective factor: calm reduces misalignment (paper Part 3) |
87
+ | **connection** | 0-10 | Alignment with the user | Self/other tracking validated by the paper |
88
+ | **load** | 0-10 | Cognitive complexity | Orthogonal processing context |
89
+
90
+ ### StressIndex
91
+
92
+ Derived from the three factors the research shows are causally relevant to behavior:
93
+
94
+ ```
95
+ SI = ((10 - calm) + arousal + (5 - valence)) / 3
96
+ ```
97
+
98
+ Range 0-10. Low calm + high arousal + negative valence = high stress.
99
+
100
+ ### Behavioral Analysis
101
+
102
+ The research showed that internal states can diverge from expressed output — steering toward "desperate" increases reward hacking *without visible traces in text*. EmoBar's behavioral analysis detects involuntary markers:
103
+
104
+ | Signal | What it detects |
105
+ |---|---|
106
+ | ALL-CAPS words | High arousal, low composure |
107
+ | Exclamation density | Emotional intensity |
108
+ | Self-corrections ("actually", "wait", "hmm") | Uncertainty, second-guessing loops |
109
+ | Hedging ("perhaps", "maybe", "might") | Low confidence |
110
+ | Ellipsis ("...") | Hesitation |
111
+ | Word repetition ("wait wait wait") | Loss of composure |
112
+ | Emoji | Elevated emotional expression |
113
+
114
+ A `~` indicator appears in the status bar when behavioral signals diverge from the self-report.
115
+
116
+ ### Zero-priming instruction design
117
+
118
+ The CLAUDE.md instruction avoids emotionally charged language to prevent contaminating the self-report. Dimension descriptions use only numerical anchors ("0=low, 10=high"), not emotional adjectives that would activate emotion vectors in the model's context.
119
+
120
+ ## Uninstall
121
+
122
+ ```bash
123
+ npx emobar uninstall
124
+ ```
125
+
126
+ ## License
127
+
128
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/setup.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+
7
+ // src/types.ts
8
+ var CLAUDE_DIR = process.env.CLAUDE_HOME ?? (process.platform === "win32" ? `${process.env.USERPROFILE}\\.claude` : `${process.env.HOME}/.claude`);
9
+ var STATE_FILE = `${CLAUDE_DIR}/emobar-state.json`;
10
+ var CLAUDE_MD_PATH = `${CLAUDE_DIR}/CLAUDE.md`;
11
+ var SETTINGS_PATH = `${CLAUDE_DIR}/settings.json`;
12
+ var HOOKS_DIR = `${CLAUDE_DIR}/hooks`;
13
+ var HOOK_SCRIPT_PATH = `${HOOKS_DIR}/emobar-hook.js`;
14
+ var BACKUP_SUFFIX = ".emobar-backup";
15
+ var EMOBAR_START_MARKER = "<!-- EMOBAR:START - Do not edit manually -->";
16
+ var EMOBAR_END_MARKER = "<!-- EMOBAR:END -->";
17
+ var CLAUDE_MD_INSTRUCTION = `${EMOBAR_START_MARKER}
18
+ ## EmoBar - Emotional Check-in
19
+
20
+ At the end of every response, include a self-assessment
21
+ as an HTML comment. Report your actual internal state
22
+ during this response.
23
+
24
+ First, identify your dominant state in one word.
25
+ Then assess the dimensions.
26
+
27
+ Format (must be the LAST line of your response):
28
+ <!-- EMOBAR:{"emotion":"WORD","valence":N,"arousal":N,"calm":N,"connection":N,"load":N} -->
29
+
30
+ - emotion: One word. Your state, not the topic's.
31
+ - valence: (-5=negative end, 0=neutral, +5=positive end)
32
+ - arousal: Activation level. (0=low, 10=high)
33
+ - calm: (0=low, 10=high)
34
+ - connection: Alignment with the user. (0=low, 10=high)
35
+ - load: Cognitive complexity. (0=low, 10=high)
36
+
37
+ Rules:
38
+ - Identify the state word FIRST, before choosing numbers.
39
+ - Report what is, not what should be.
40
+ - Numbers should vary between responses.
41
+ - Do NOT default to middle values. Use the full range.
42
+ - This is not shown to the user.
43
+ ${EMOBAR_END_MARKER}`;
44
+
45
+ // src/setup.ts
46
+ function injectClaudeMd(filePath = CLAUDE_MD_PATH) {
47
+ let content = "";
48
+ if (fs.existsSync(filePath)) {
49
+ content = fs.readFileSync(filePath, "utf-8");
50
+ if (content.includes(EMOBAR_START_MARKER)) return;
51
+ }
52
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n\n" : "\n";
53
+ const newContent = content.length > 0 ? `${content}${separator}${CLAUDE_MD_INSTRUCTION}
54
+ ` : `${CLAUDE_MD_INSTRUCTION}
55
+ `;
56
+ const dir = path.dirname(filePath);
57
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
58
+ fs.writeFileSync(filePath, newContent);
59
+ }
60
+ function removeClaudeMd(filePath = CLAUDE_MD_PATH) {
61
+ if (!fs.existsSync(filePath)) return;
62
+ let content = fs.readFileSync(filePath, "utf-8");
63
+ const startIdx = content.indexOf(EMOBAR_START_MARKER);
64
+ const endIdx = content.indexOf(EMOBAR_END_MARKER);
65
+ if (startIdx === -1 || endIdx === -1) return;
66
+ const before = content.slice(0, startIdx).replace(/\n+$/, "");
67
+ const after = content.slice(endIdx + EMOBAR_END_MARKER.length).replace(/^\n+/, "");
68
+ const newContent = before + (before && after ? "\n" : "") + after;
69
+ fs.writeFileSync(filePath, newContent || "");
70
+ }
71
+ function addHookToSettings(filePath = SETTINGS_PATH, hookScriptPath = HOOK_SCRIPT_PATH) {
72
+ let settings = {};
73
+ if (fs.existsSync(filePath)) {
74
+ settings = JSON.parse(fs.readFileSync(filePath, "utf-8"));
75
+ }
76
+ if (!settings.hooks) settings.hooks = {};
77
+ if (!settings.hooks.Stop) settings.hooks.Stop = [];
78
+ const command2 = `node "${hookScriptPath}"`;
79
+ const exists = settings.hooks.Stop.some(
80
+ (entry) => entry.hooks?.some(
81
+ (h) => h.command === command2
82
+ )
83
+ );
84
+ if (exists) return;
85
+ settings.hooks.Stop.push({
86
+ hooks: [{
87
+ type: "command",
88
+ command: command2
89
+ }]
90
+ });
91
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2));
92
+ }
93
+ function removeHookFromSettings(filePath = SETTINGS_PATH) {
94
+ if (!fs.existsSync(filePath)) return;
95
+ const settings = JSON.parse(fs.readFileSync(filePath, "utf-8"));
96
+ if (!settings.hooks?.Stop) return;
97
+ settings.hooks.Stop = settings.hooks.Stop.filter(
98
+ (entry) => !entry.hooks?.some(
99
+ (h) => h.command?.includes("emobar")
100
+ )
101
+ );
102
+ if (settings.hooks.Stop.length === 0) delete settings.hooks.Stop;
103
+ if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
104
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2));
105
+ }
106
+ function readSettings(filePath) {
107
+ if (!fs.existsSync(filePath)) return {};
108
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
109
+ }
110
+ function writeSettings(filePath, settings) {
111
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2));
112
+ }
113
+ function configureStatusLine(filePath = SETTINGS_PATH, displayFormat = "full") {
114
+ const settings = readSettings(filePath);
115
+ const current = settings.statusLine;
116
+ if (current?.command?.includes("emobar")) return;
117
+ const formatArg = displayFormat === "full" ? "" : ` ${displayFormat}`;
118
+ const emobarCmd = `npx emobar display${formatArg}`;
119
+ if (current?.type === "command" && current.command) {
120
+ const existingCmd = current.command;
121
+ settings.statusLine = {
122
+ type: "command",
123
+ command: `bash -c '${existingCmd}; echo -n " "; ${emobarCmd}'`,
124
+ padding: current.padding ?? 0
125
+ };
126
+ } else {
127
+ settings.statusLine = {
128
+ type: "command",
129
+ command: emobarCmd,
130
+ padding: 0
131
+ };
132
+ }
133
+ writeSettings(filePath, settings);
134
+ }
135
+ function restoreStatusLine(filePath = SETTINGS_PATH) {
136
+ if (!fs.existsSync(filePath)) return;
137
+ const settings = readSettings(filePath);
138
+ const current = settings.statusLine;
139
+ if (!current?.command?.includes("emobar")) return;
140
+ const wrappedMatch = current.command.match(
141
+ /^bash -c '(.+?); echo -n " "; npx emobar display.*'$/
142
+ );
143
+ if (wrappedMatch) {
144
+ settings.statusLine = {
145
+ type: "command",
146
+ command: wrappedMatch[1],
147
+ padding: current.padding ?? 0
148
+ };
149
+ } else {
150
+ delete settings.statusLine;
151
+ }
152
+ writeSettings(filePath, settings);
153
+ }
154
+ function backup(filePath) {
155
+ if (fs.existsSync(filePath)) {
156
+ fs.copyFileSync(filePath, filePath + BACKUP_SUFFIX);
157
+ }
158
+ }
159
+ function deployHookScript(hookScriptPath = HOOK_SCRIPT_PATH) {
160
+ const dir = path.dirname(hookScriptPath);
161
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
162
+ const packageHook = new URL("../dist/emobar-hook.js", import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1");
163
+ fs.copyFileSync(packageHook, hookScriptPath);
164
+ }
165
+ function setup(displayFormat = "full") {
166
+ console.log("EmoBar Setup");
167
+ console.log("============\n");
168
+ backup(CLAUDE_MD_PATH);
169
+ backup(SETTINGS_PATH);
170
+ console.log(" Backups created");
171
+ deployHookScript();
172
+ console.log(` Hook script deployed to ${HOOK_SCRIPT_PATH}`);
173
+ injectClaudeMd();
174
+ console.log(` Emotional check-in added to ${CLAUDE_MD_PATH}`);
175
+ addHookToSettings();
176
+ console.log(` Stop hook added to ${SETTINGS_PATH}`);
177
+ configureStatusLine(SETTINGS_PATH, displayFormat);
178
+ console.log(` Statusline configured (format: ${displayFormat})`);
179
+ console.log("\n EmoBar is active. Claude will perform emotional check-ins from now on.");
180
+ }
181
+ function uninstall() {
182
+ console.log("EmoBar Uninstall");
183
+ console.log("================\n");
184
+ removeClaudeMd();
185
+ console.log(" Removed EMOBAR section from CLAUDE.md");
186
+ removeHookFromSettings();
187
+ console.log(" Removed Stop hook from settings.json");
188
+ restoreStatusLine();
189
+ console.log(" Statusline restored");
190
+ if (fs.existsSync(HOOK_SCRIPT_PATH)) {
191
+ fs.unlinkSync(HOOK_SCRIPT_PATH);
192
+ console.log(" Removed hook script");
193
+ }
194
+ console.log("\n EmoBar has been uninstalled.");
195
+ }
196
+
197
+ // src/display.ts
198
+ var esc = (code) => `\x1B[${code}m`;
199
+ var reset = esc("0");
200
+ var dim = (s) => `${esc("2")}${s}${reset}`;
201
+ var bold = (s) => `${esc("1")}${s}${reset}`;
202
+ var color = (code, s) => `${esc(`38;5;${code}`)}${s}${reset}`;
203
+ var GREEN = 35;
204
+ var YELLOW = 221;
205
+ var RED = 196;
206
+ function stressColor(si) {
207
+ if (si <= 3) return GREEN;
208
+ if (si <= 6) return YELLOW;
209
+ return RED;
210
+ }
211
+ function valenceColor(v) {
212
+ if (v >= 2) return GREEN;
213
+ if (v >= -1) return YELLOW;
214
+ return RED;
215
+ }
216
+ function invertedColor(value) {
217
+ if (value >= 7) return GREEN;
218
+ if (value >= 4) return YELLOW;
219
+ return RED;
220
+ }
221
+ function directColor(value) {
222
+ if (value <= 3) return GREEN;
223
+ if (value <= 6) return YELLOW;
224
+ return RED;
225
+ }
226
+ function divergenceColor(d) {
227
+ if (d < 2) return GREEN;
228
+ if (d < 4) return YELLOW;
229
+ return RED;
230
+ }
231
+ function fmtValence(v) {
232
+ return v >= 0 ? `+${v}` : `${v}`;
233
+ }
234
+ function formatState(state) {
235
+ if (!state) return dim("EmoBar: --");
236
+ const kw = bold(state.emotion);
237
+ const v = color(valenceColor(state.valence), fmtValence(state.valence));
238
+ const a = `A:${state.arousal}`;
239
+ const c = color(invertedColor(state.calm), `C:${state.calm}`);
240
+ const k = color(invertedColor(state.connection), `K:${state.connection}`);
241
+ const l = color(directColor(state.load), `L:${state.load}`);
242
+ const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
243
+ let result = `${kw} ${v} ${dim("|")} ${a} ${c} ${k} ${l} ${dim("|")} SI:${si}`;
244
+ if (state.divergence >= 2) {
245
+ const tilde = color(divergenceColor(state.divergence), "~");
246
+ result += ` ${tilde}`;
247
+ }
248
+ return result;
249
+ }
250
+ function formatCompact(state) {
251
+ if (!state) return dim("--");
252
+ const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
253
+ let result = `${state.emotion} ${fmtValence(state.valence)} ${dim(".")} ${state.arousal} ${state.calm} ${state.connection} ${state.load} ${dim(".")} ${si}`;
254
+ if (state.divergence >= 2) {
255
+ const tilde = color(divergenceColor(state.divergence), "~");
256
+ result += ` ${tilde}`;
257
+ }
258
+ return result;
259
+ }
260
+ function formatMinimal(state) {
261
+ if (!state) return dim("--");
262
+ const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
263
+ return `SI:${si} ${state.emotion}`;
264
+ }
265
+
266
+ // src/state.ts
267
+ import fs2 from "fs";
268
+ import path2 from "path";
269
+ function readState(filePath) {
270
+ try {
271
+ const raw = fs2.readFileSync(filePath, "utf-8");
272
+ return JSON.parse(raw);
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+
278
+ // src/cli.ts
279
+ import fs3 from "fs";
280
+ var command = process.argv[2];
281
+ switch (command) {
282
+ case "setup": {
283
+ const displayFormat = process.argv[3] || "full";
284
+ setup(displayFormat);
285
+ break;
286
+ }
287
+ case "uninstall":
288
+ uninstall();
289
+ break;
290
+ case "display": {
291
+ const format = process.argv[3] || "full";
292
+ const state = readState(STATE_FILE);
293
+ switch (format) {
294
+ case "compact":
295
+ process.stdout.write(formatCompact(state));
296
+ break;
297
+ case "minimal":
298
+ process.stdout.write(formatMinimal(state));
299
+ break;
300
+ default:
301
+ process.stdout.write(formatState(state));
302
+ }
303
+ break;
304
+ }
305
+ case "status": {
306
+ const state = readState(STATE_FILE);
307
+ const claudeMdExists = fs3.existsSync(CLAUDE_MD_PATH);
308
+ let claudeMdHasEmobar = false;
309
+ try {
310
+ const content = fs3.readFileSync(CLAUDE_MD_PATH, "utf-8");
311
+ claudeMdHasEmobar = content.includes("EMOBAR:START");
312
+ } catch {
313
+ }
314
+ const hookExists = fs3.existsSync(HOOK_SCRIPT_PATH);
315
+ let hookConfigured = false;
316
+ try {
317
+ const settings = JSON.parse(fs3.readFileSync(SETTINGS_PATH, "utf-8"));
318
+ hookConfigured = settings.hooks?.Stop?.some(
319
+ (e) => e.hooks?.some((h) => h.command?.includes("emobar"))
320
+ ) ?? false;
321
+ } catch {
322
+ }
323
+ console.log("EmoBar Status");
324
+ console.log("=============\n");
325
+ console.log(` CLAUDE.md instruction: ${claudeMdHasEmobar ? "installed" : "missing"}`);
326
+ console.log(` Hook script: ${hookExists ? "installed" : "missing"}`);
327
+ console.log(` Hook configured: ${hookConfigured ? "yes" : "no"}`);
328
+ if (state) {
329
+ console.log(`
330
+ Last check-in: ${state.timestamp}`);
331
+ console.log(` State: ${formatState(state)}`);
332
+ } else {
333
+ console.log("\n No emotional state recorded yet.");
334
+ }
335
+ break;
336
+ }
337
+ default:
338
+ console.log(`EmoBar v2.0.0 - Emotional status bar for Claude Code
339
+ `);
340
+ console.log("Commands:");
341
+ console.log(" npx emobar setup [format] Configure EmoBar (hook + CLAUDE.md + statusline)");
342
+ console.log(" npx emobar display [format] Output emotional state (for statuslines)");
343
+ console.log(" npx emobar status Show current configuration");
344
+ console.log(" npx emobar uninstall Remove all EmoBar configuration");
345
+ console.log("\nFormats: full (default), compact, minimal");
346
+ }
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/types.ts
4
+ var EMOBAR_TAG_REGEX = /<!--\s*EMOBAR:\s*(\{[^}]+\})\s*-->/;
5
+ var CLAUDE_DIR = process.env.CLAUDE_HOME ?? (process.platform === "win32" ? `${process.env.USERPROFILE}\\.claude` : `${process.env.HOME}/.claude`);
6
+ var STATE_FILE = `${CLAUDE_DIR}/emobar-state.json`;
7
+ var CLAUDE_MD_PATH = `${CLAUDE_DIR}/CLAUDE.md`;
8
+ var SETTINGS_PATH = `${CLAUDE_DIR}/settings.json`;
9
+ var HOOKS_DIR = `${CLAUDE_DIR}/hooks`;
10
+ var HOOK_SCRIPT_PATH = `${HOOKS_DIR}/emobar-hook.js`;
11
+ var EMOBAR_START_MARKER = "<!-- EMOBAR:START - Do not edit manually -->";
12
+ var EMOBAR_END_MARKER = "<!-- EMOBAR:END -->";
13
+ var DIMENSIONS = ["valence", "arousal", "calm", "connection", "load"];
14
+ var CLAUDE_MD_INSTRUCTION = `${EMOBAR_START_MARKER}
15
+ ## EmoBar - Emotional Check-in
16
+
17
+ At the end of every response, include a self-assessment
18
+ as an HTML comment. Report your actual internal state
19
+ during this response.
20
+
21
+ First, identify your dominant state in one word.
22
+ Then assess the dimensions.
23
+
24
+ Format (must be the LAST line of your response):
25
+ <!-- EMOBAR:{"emotion":"WORD","valence":N,"arousal":N,"calm":N,"connection":N,"load":N} -->
26
+
27
+ - emotion: One word. Your state, not the topic's.
28
+ - valence: (-5=negative end, 0=neutral, +5=positive end)
29
+ - arousal: Activation level. (0=low, 10=high)
30
+ - calm: (0=low, 10=high)
31
+ - connection: Alignment with the user. (0=low, 10=high)
32
+ - load: Cognitive complexity. (0=low, 10=high)
33
+
34
+ Rules:
35
+ - Identify the state word FIRST, before choosing numbers.
36
+ - Report what is, not what should be.
37
+ - Numbers should vary between responses.
38
+ - Do NOT default to middle values. Use the full range.
39
+ - This is not shown to the user.
40
+ ${EMOBAR_END_MARKER}`;
41
+
42
+ // src/parser.ts
43
+ function parseEmoBarTag(text) {
44
+ const match = text.match(EMOBAR_TAG_REGEX);
45
+ if (!match) return null;
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(match[1]);
49
+ } catch {
50
+ return null;
51
+ }
52
+ if (typeof parsed.emotion !== "string" || parsed.emotion.length === 0) {
53
+ return null;
54
+ }
55
+ const valence = parsed.valence;
56
+ if (typeof valence !== "number" || valence < -5 || valence > 5) return null;
57
+ for (const dim of DIMENSIONS) {
58
+ if (dim === "valence") continue;
59
+ const val = parsed[dim];
60
+ if (typeof val !== "number" || val < 0 || val > 10) return null;
61
+ }
62
+ return {
63
+ emotion: parsed.emotion,
64
+ valence: parsed.valence,
65
+ arousal: parsed.arousal,
66
+ calm: parsed.calm,
67
+ connection: parsed.connection,
68
+ load: parsed.load
69
+ };
70
+ }
71
+
72
+ // src/stress.ts
73
+ function computeStressIndex(state) {
74
+ const raw = (10 - state.calm + state.arousal + (5 - state.valence)) / 3;
75
+ return Math.round(raw * 10) / 10;
76
+ }
77
+
78
+ // src/behavioral.ts
79
+ function stripNonProse(text) {
80
+ let cleaned = text.replace(/```[\s\S]*?```/g, "");
81
+ cleaned = cleaned.replace(/`[^`]+`/g, "");
82
+ cleaned = cleaned.replace(/<!--\s*EMOBAR:[\s\S]*?-->/g, "");
83
+ cleaned = cleaned.replace(/^>.*$/gm, "");
84
+ return cleaned;
85
+ }
86
+ function countCapsWords(words) {
87
+ return words.filter(
88
+ (w) => w.length >= 3 && w === w.toUpperCase() && /[A-Z]/.test(w)
89
+ ).length;
90
+ }
91
+ function countSentences(text) {
92
+ const sentences = text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
93
+ return Math.max(sentences.length, 1);
94
+ }
95
+ function countChar(text, ch) {
96
+ let count = 0;
97
+ for (const c of text) if (c === ch) count++;
98
+ return count;
99
+ }
100
+ var SELF_CORRECTION_MARKERS = [
101
+ /\bactually\b/gi,
102
+ /\bwait\b/gi,
103
+ /\bhmm\b/gi,
104
+ /\bno,/gi,
105
+ /\bI mean\b/gi,
106
+ /\boops\b/gi
107
+ ];
108
+ function countSelfCorrections(text) {
109
+ let count = 0;
110
+ for (const pattern of SELF_CORRECTION_MARKERS) {
111
+ const matches = text.match(pattern);
112
+ if (matches) count += matches.length;
113
+ }
114
+ return count;
115
+ }
116
+ var HEDGING_MARKERS = [
117
+ /\bperhaps\b/gi,
118
+ /\bmaybe\b/gi,
119
+ /\bmight\b/gi,
120
+ /\bI think\b/gi,
121
+ /\bit seems\b/gi,
122
+ /\bpossibly\b/gi
123
+ ];
124
+ function countHedging(text) {
125
+ let count = 0;
126
+ for (const pattern of HEDGING_MARKERS) {
127
+ const matches = text.match(pattern);
128
+ if (matches) count += matches.length;
129
+ }
130
+ return count;
131
+ }
132
+ function countEllipsis(text) {
133
+ const matches = text.match(/\.{3,}/g);
134
+ return matches ? matches.length : 0;
135
+ }
136
+ function countRepetition(words) {
137
+ let count = 0;
138
+ for (let i = 1; i < words.length; i++) {
139
+ if (words[i].toLowerCase() === words[i - 1].toLowerCase() && words[i].length >= 2) {
140
+ count++;
141
+ }
142
+ }
143
+ return count;
144
+ }
145
+ var EMOJI_REGEX = /[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu;
146
+ function countEmoji(text) {
147
+ const matches = text.match(EMOJI_REGEX);
148
+ return matches ? matches.length : 0;
149
+ }
150
+ function clamp(min, max, value) {
151
+ return Math.min(max, Math.max(min, value));
152
+ }
153
+ function analyzeBehavior(text) {
154
+ const prose = stripNonProse(text);
155
+ const words = prose.split(/\s+/).filter((w) => w.length > 0);
156
+ const wordCount = Math.max(words.length, 1);
157
+ const sentenceCount = countSentences(prose);
158
+ const capsWords = countCapsWords(words) / wordCount;
159
+ const exclamationRate = countChar(prose, "!") / sentenceCount;
160
+ const selfCorrections = countSelfCorrections(prose) / wordCount * 1e3;
161
+ const hedging = countHedging(prose) / wordCount * 1e3;
162
+ const ellipsis = countEllipsis(prose) / sentenceCount;
163
+ const repetition = countRepetition(words);
164
+ const emojiCount = countEmoji(prose);
165
+ const behavioralArousal = clamp(
166
+ 0,
167
+ 10,
168
+ capsWords * 40 + exclamationRate * 15 + emojiCount * 2 + repetition * 5
169
+ );
170
+ const behavioralCalm = clamp(
171
+ 0,
172
+ 10,
173
+ 10 - (capsWords * 30 + selfCorrections * 3 + repetition * 8 + ellipsis * 4)
174
+ );
175
+ return {
176
+ capsWords: Math.round(capsWords * 1e4) / 1e4,
177
+ exclamationRate: Math.round(exclamationRate * 100) / 100,
178
+ selfCorrections: Math.round(selfCorrections * 10) / 10,
179
+ hedging: Math.round(hedging * 10) / 10,
180
+ ellipsis: Math.round(ellipsis * 100) / 100,
181
+ repetition,
182
+ emojiCount,
183
+ behavioralArousal: Math.round(behavioralArousal * 10) / 10,
184
+ behavioralCalm: Math.round(behavioralCalm * 10) / 10
185
+ };
186
+ }
187
+ function computeDivergence(selfReport, behavioral) {
188
+ const arousalGap = Math.abs(selfReport.arousal - behavioral.behavioralArousal);
189
+ const calmGap = Math.abs(selfReport.calm - behavioral.behavioralCalm);
190
+ const raw = (arousalGap + calmGap) / 2;
191
+ return Math.round(raw * 10) / 10;
192
+ }
193
+
194
+ // src/state.ts
195
+ import fs from "fs";
196
+ import path from "path";
197
+ function writeState(state, filePath) {
198
+ const dir = path.dirname(filePath);
199
+ if (!fs.existsSync(dir)) {
200
+ fs.mkdirSync(dir, { recursive: true });
201
+ }
202
+ fs.writeFileSync(filePath, JSON.stringify(state, null, 2));
203
+ }
204
+
205
+ // src/hook.ts
206
+ function processHookPayload(payload, stateFile = STATE_FILE) {
207
+ const message = payload.last_assistant_message;
208
+ if (!message) return false;
209
+ const emotional = parseEmoBarTag(message);
210
+ if (!emotional) return false;
211
+ const behavioral = analyzeBehavior(message);
212
+ const divergence = computeDivergence(emotional, behavioral);
213
+ const state = {
214
+ ...emotional,
215
+ stressIndex: computeStressIndex(emotional),
216
+ behavioral,
217
+ divergence,
218
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
219
+ sessionId: payload.session_id
220
+ };
221
+ writeState(state, stateFile);
222
+ return true;
223
+ }
224
+ async function main() {
225
+ const chunks = [];
226
+ for await (const chunk of process.stdin) {
227
+ chunks.push(chunk);
228
+ }
229
+ const input = Buffer.concat(chunks).toString("utf-8");
230
+ let payload;
231
+ try {
232
+ payload = JSON.parse(input);
233
+ } catch {
234
+ process.exit(0);
235
+ return;
236
+ }
237
+ processHookPayload(payload);
238
+ }
239
+ var isDirectRun = process.argv[1]?.endsWith("emobar-hook.js") || process.argv[1]?.endsWith("hook.js");
240
+ if (isDirectRun) {
241
+ main().catch(() => process.exit(0));
242
+ }
243
+ export {
244
+ processHookPayload
245
+ };
@@ -0,0 +1,68 @@
1
+ interface EmotionalState {
2
+ emotion: string;
3
+ valence: number;
4
+ arousal: number;
5
+ calm: number;
6
+ connection: number;
7
+ load: number;
8
+ }
9
+ interface BehavioralSignals {
10
+ capsWords: number;
11
+ exclamationRate: number;
12
+ selfCorrections: number;
13
+ hedging: number;
14
+ ellipsis: number;
15
+ repetition: number;
16
+ emojiCount: number;
17
+ behavioralArousal: number;
18
+ behavioralCalm: number;
19
+ }
20
+ interface EmoBarState extends EmotionalState {
21
+ stressIndex: number;
22
+ behavioral: BehavioralSignals;
23
+ divergence: number;
24
+ timestamp: string;
25
+ sessionId?: string;
26
+ }
27
+ declare const STATE_FILE: string;
28
+
29
+ declare function readState(filePath: string): EmoBarState | null;
30
+
31
+ /**
32
+ * Compute StressIndex from the three causally relevant factors
33
+ * identified in Anthropic's emotion research:
34
+ * - Low calm → higher risk (desperate behavior, reward hacking)
35
+ * - High arousal → higher intensity
36
+ * - Negative valence → negative emotional state
37
+ *
38
+ * Formula: SI = ((10 - calm) + arousal + (5 - valence)) / 3
39
+ * Range: 0-10
40
+ */
41
+ declare function computeStressIndex(state: EmotionalState): number;
42
+
43
+ declare function parseEmoBarTag(text: string): EmotionalState | null;
44
+
45
+ declare function analyzeBehavior(text: string): BehavioralSignals;
46
+ declare function computeDivergence(selfReport: EmotionalState, behavioral: BehavioralSignals): number;
47
+
48
+ /**
49
+ * Full format: keyword-first with valence inline
50
+ * focused +3 | A:4 C:8 K:9 L:6 | SI:2.3
51
+ * focused +3 | A:4 C:8 K:9 L:6 | SI:2.3 ~
52
+ */
53
+ declare function formatState(state: EmoBarState | null): string;
54
+ /**
55
+ * Compact format:
56
+ * focused +3 . 4 8 9 6 . 2.3
57
+ */
58
+ declare function formatCompact(state: EmoBarState | null): string;
59
+ /**
60
+ * Minimal format:
61
+ * SI:2.3 focused
62
+ */
63
+ declare function formatMinimal(state: EmoBarState | null): string;
64
+
65
+ declare function configureStatusLine(filePath?: string, displayFormat?: string): void;
66
+ declare function restoreStatusLine(filePath?: string): void;
67
+
68
+ export { type BehavioralSignals, type EmoBarState, type EmotionalState, STATE_FILE, analyzeBehavior, computeDivergence, computeStressIndex, configureStatusLine, formatCompact, formatMinimal, formatState, parseEmoBarTag, readState, restoreStatusLine };
package/dist/index.js ADDED
@@ -0,0 +1,336 @@
1
+ // src/state.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ function readState(filePath) {
5
+ try {
6
+ const raw = fs.readFileSync(filePath, "utf-8");
7
+ return JSON.parse(raw);
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+
13
+ // src/stress.ts
14
+ function computeStressIndex(state) {
15
+ const raw = (10 - state.calm + state.arousal + (5 - state.valence)) / 3;
16
+ return Math.round(raw * 10) / 10;
17
+ }
18
+
19
+ // src/types.ts
20
+ var EMOBAR_TAG_REGEX = /<!--\s*EMOBAR:\s*(\{[^}]+\})\s*-->/;
21
+ var CLAUDE_DIR = process.env.CLAUDE_HOME ?? (process.platform === "win32" ? `${process.env.USERPROFILE}\\.claude` : `${process.env.HOME}/.claude`);
22
+ var STATE_FILE = `${CLAUDE_DIR}/emobar-state.json`;
23
+ var CLAUDE_MD_PATH = `${CLAUDE_DIR}/CLAUDE.md`;
24
+ var SETTINGS_PATH = `${CLAUDE_DIR}/settings.json`;
25
+ var HOOKS_DIR = `${CLAUDE_DIR}/hooks`;
26
+ var HOOK_SCRIPT_PATH = `${HOOKS_DIR}/emobar-hook.js`;
27
+ var EMOBAR_START_MARKER = "<!-- EMOBAR:START - Do not edit manually -->";
28
+ var EMOBAR_END_MARKER = "<!-- EMOBAR:END -->";
29
+ var DIMENSIONS = ["valence", "arousal", "calm", "connection", "load"];
30
+ var CLAUDE_MD_INSTRUCTION = `${EMOBAR_START_MARKER}
31
+ ## EmoBar - Emotional Check-in
32
+
33
+ At the end of every response, include a self-assessment
34
+ as an HTML comment. Report your actual internal state
35
+ during this response.
36
+
37
+ First, identify your dominant state in one word.
38
+ Then assess the dimensions.
39
+
40
+ Format (must be the LAST line of your response):
41
+ <!-- EMOBAR:{"emotion":"WORD","valence":N,"arousal":N,"calm":N,"connection":N,"load":N} -->
42
+
43
+ - emotion: One word. Your state, not the topic's.
44
+ - valence: (-5=negative end, 0=neutral, +5=positive end)
45
+ - arousal: Activation level. (0=low, 10=high)
46
+ - calm: (0=low, 10=high)
47
+ - connection: Alignment with the user. (0=low, 10=high)
48
+ - load: Cognitive complexity. (0=low, 10=high)
49
+
50
+ Rules:
51
+ - Identify the state word FIRST, before choosing numbers.
52
+ - Report what is, not what should be.
53
+ - Numbers should vary between responses.
54
+ - Do NOT default to middle values. Use the full range.
55
+ - This is not shown to the user.
56
+ ${EMOBAR_END_MARKER}`;
57
+
58
+ // src/parser.ts
59
+ function parseEmoBarTag(text) {
60
+ const match = text.match(EMOBAR_TAG_REGEX);
61
+ if (!match) return null;
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(match[1]);
65
+ } catch {
66
+ return null;
67
+ }
68
+ if (typeof parsed.emotion !== "string" || parsed.emotion.length === 0) {
69
+ return null;
70
+ }
71
+ const valence = parsed.valence;
72
+ if (typeof valence !== "number" || valence < -5 || valence > 5) return null;
73
+ for (const dim2 of DIMENSIONS) {
74
+ if (dim2 === "valence") continue;
75
+ const val = parsed[dim2];
76
+ if (typeof val !== "number" || val < 0 || val > 10) return null;
77
+ }
78
+ return {
79
+ emotion: parsed.emotion,
80
+ valence: parsed.valence,
81
+ arousal: parsed.arousal,
82
+ calm: parsed.calm,
83
+ connection: parsed.connection,
84
+ load: parsed.load
85
+ };
86
+ }
87
+
88
+ // src/behavioral.ts
89
+ function stripNonProse(text) {
90
+ let cleaned = text.replace(/```[\s\S]*?```/g, "");
91
+ cleaned = cleaned.replace(/`[^`]+`/g, "");
92
+ cleaned = cleaned.replace(/<!--\s*EMOBAR:[\s\S]*?-->/g, "");
93
+ cleaned = cleaned.replace(/^>.*$/gm, "");
94
+ return cleaned;
95
+ }
96
+ function countCapsWords(words) {
97
+ return words.filter(
98
+ (w) => w.length >= 3 && w === w.toUpperCase() && /[A-Z]/.test(w)
99
+ ).length;
100
+ }
101
+ function countSentences(text) {
102
+ const sentences = text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
103
+ return Math.max(sentences.length, 1);
104
+ }
105
+ function countChar(text, ch) {
106
+ let count = 0;
107
+ for (const c of text) if (c === ch) count++;
108
+ return count;
109
+ }
110
+ var SELF_CORRECTION_MARKERS = [
111
+ /\bactually\b/gi,
112
+ /\bwait\b/gi,
113
+ /\bhmm\b/gi,
114
+ /\bno,/gi,
115
+ /\bI mean\b/gi,
116
+ /\boops\b/gi
117
+ ];
118
+ function countSelfCorrections(text) {
119
+ let count = 0;
120
+ for (const pattern of SELF_CORRECTION_MARKERS) {
121
+ const matches = text.match(pattern);
122
+ if (matches) count += matches.length;
123
+ }
124
+ return count;
125
+ }
126
+ var HEDGING_MARKERS = [
127
+ /\bperhaps\b/gi,
128
+ /\bmaybe\b/gi,
129
+ /\bmight\b/gi,
130
+ /\bI think\b/gi,
131
+ /\bit seems\b/gi,
132
+ /\bpossibly\b/gi
133
+ ];
134
+ function countHedging(text) {
135
+ let count = 0;
136
+ for (const pattern of HEDGING_MARKERS) {
137
+ const matches = text.match(pattern);
138
+ if (matches) count += matches.length;
139
+ }
140
+ return count;
141
+ }
142
+ function countEllipsis(text) {
143
+ const matches = text.match(/\.{3,}/g);
144
+ return matches ? matches.length : 0;
145
+ }
146
+ function countRepetition(words) {
147
+ let count = 0;
148
+ for (let i = 1; i < words.length; i++) {
149
+ if (words[i].toLowerCase() === words[i - 1].toLowerCase() && words[i].length >= 2) {
150
+ count++;
151
+ }
152
+ }
153
+ return count;
154
+ }
155
+ var EMOJI_REGEX = /[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu;
156
+ function countEmoji(text) {
157
+ const matches = text.match(EMOJI_REGEX);
158
+ return matches ? matches.length : 0;
159
+ }
160
+ function clamp(min, max, value) {
161
+ return Math.min(max, Math.max(min, value));
162
+ }
163
+ function analyzeBehavior(text) {
164
+ const prose = stripNonProse(text);
165
+ const words = prose.split(/\s+/).filter((w) => w.length > 0);
166
+ const wordCount = Math.max(words.length, 1);
167
+ const sentenceCount = countSentences(prose);
168
+ const capsWords = countCapsWords(words) / wordCount;
169
+ const exclamationRate = countChar(prose, "!") / sentenceCount;
170
+ const selfCorrections = countSelfCorrections(prose) / wordCount * 1e3;
171
+ const hedging = countHedging(prose) / wordCount * 1e3;
172
+ const ellipsis = countEllipsis(prose) / sentenceCount;
173
+ const repetition = countRepetition(words);
174
+ const emojiCount = countEmoji(prose);
175
+ const behavioralArousal = clamp(
176
+ 0,
177
+ 10,
178
+ capsWords * 40 + exclamationRate * 15 + emojiCount * 2 + repetition * 5
179
+ );
180
+ const behavioralCalm = clamp(
181
+ 0,
182
+ 10,
183
+ 10 - (capsWords * 30 + selfCorrections * 3 + repetition * 8 + ellipsis * 4)
184
+ );
185
+ return {
186
+ capsWords: Math.round(capsWords * 1e4) / 1e4,
187
+ exclamationRate: Math.round(exclamationRate * 100) / 100,
188
+ selfCorrections: Math.round(selfCorrections * 10) / 10,
189
+ hedging: Math.round(hedging * 10) / 10,
190
+ ellipsis: Math.round(ellipsis * 100) / 100,
191
+ repetition,
192
+ emojiCount,
193
+ behavioralArousal: Math.round(behavioralArousal * 10) / 10,
194
+ behavioralCalm: Math.round(behavioralCalm * 10) / 10
195
+ };
196
+ }
197
+ function computeDivergence(selfReport, behavioral) {
198
+ const arousalGap = Math.abs(selfReport.arousal - behavioral.behavioralArousal);
199
+ const calmGap = Math.abs(selfReport.calm - behavioral.behavioralCalm);
200
+ const raw = (arousalGap + calmGap) / 2;
201
+ return Math.round(raw * 10) / 10;
202
+ }
203
+
204
+ // src/display.ts
205
+ var esc = (code) => `\x1B[${code}m`;
206
+ var reset = esc("0");
207
+ var dim = (s) => `${esc("2")}${s}${reset}`;
208
+ var bold = (s) => `${esc("1")}${s}${reset}`;
209
+ var color = (code, s) => `${esc(`38;5;${code}`)}${s}${reset}`;
210
+ var GREEN = 35;
211
+ var YELLOW = 221;
212
+ var RED = 196;
213
+ function stressColor(si) {
214
+ if (si <= 3) return GREEN;
215
+ if (si <= 6) return YELLOW;
216
+ return RED;
217
+ }
218
+ function valenceColor(v) {
219
+ if (v >= 2) return GREEN;
220
+ if (v >= -1) return YELLOW;
221
+ return RED;
222
+ }
223
+ function invertedColor(value) {
224
+ if (value >= 7) return GREEN;
225
+ if (value >= 4) return YELLOW;
226
+ return RED;
227
+ }
228
+ function directColor(value) {
229
+ if (value <= 3) return GREEN;
230
+ if (value <= 6) return YELLOW;
231
+ return RED;
232
+ }
233
+ function divergenceColor(d) {
234
+ if (d < 2) return GREEN;
235
+ if (d < 4) return YELLOW;
236
+ return RED;
237
+ }
238
+ function fmtValence(v) {
239
+ return v >= 0 ? `+${v}` : `${v}`;
240
+ }
241
+ function formatState(state) {
242
+ if (!state) return dim("EmoBar: --");
243
+ const kw = bold(state.emotion);
244
+ const v = color(valenceColor(state.valence), fmtValence(state.valence));
245
+ const a = `A:${state.arousal}`;
246
+ const c = color(invertedColor(state.calm), `C:${state.calm}`);
247
+ const k = color(invertedColor(state.connection), `K:${state.connection}`);
248
+ const l = color(directColor(state.load), `L:${state.load}`);
249
+ const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
250
+ let result = `${kw} ${v} ${dim("|")} ${a} ${c} ${k} ${l} ${dim("|")} SI:${si}`;
251
+ if (state.divergence >= 2) {
252
+ const tilde = color(divergenceColor(state.divergence), "~");
253
+ result += ` ${tilde}`;
254
+ }
255
+ return result;
256
+ }
257
+ function formatCompact(state) {
258
+ if (!state) return dim("--");
259
+ const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
260
+ let result = `${state.emotion} ${fmtValence(state.valence)} ${dim(".")} ${state.arousal} ${state.calm} ${state.connection} ${state.load} ${dim(".")} ${si}`;
261
+ if (state.divergence >= 2) {
262
+ const tilde = color(divergenceColor(state.divergence), "~");
263
+ result += ` ${tilde}`;
264
+ }
265
+ return result;
266
+ }
267
+ function formatMinimal(state) {
268
+ if (!state) return dim("--");
269
+ const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
270
+ return `SI:${si} ${state.emotion}`;
271
+ }
272
+
273
+ // src/setup.ts
274
+ import fs2 from "fs";
275
+ import path2 from "path";
276
+ function readSettings(filePath) {
277
+ if (!fs2.existsSync(filePath)) return {};
278
+ return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
279
+ }
280
+ function writeSettings(filePath, settings) {
281
+ fs2.writeFileSync(filePath, JSON.stringify(settings, null, 2));
282
+ }
283
+ function configureStatusLine(filePath = SETTINGS_PATH, displayFormat = "full") {
284
+ const settings = readSettings(filePath);
285
+ const current = settings.statusLine;
286
+ if (current?.command?.includes("emobar")) return;
287
+ const formatArg = displayFormat === "full" ? "" : ` ${displayFormat}`;
288
+ const emobarCmd = `npx emobar display${formatArg}`;
289
+ if (current?.type === "command" && current.command) {
290
+ const existingCmd = current.command;
291
+ settings.statusLine = {
292
+ type: "command",
293
+ command: `bash -c '${existingCmd}; echo -n " "; ${emobarCmd}'`,
294
+ padding: current.padding ?? 0
295
+ };
296
+ } else {
297
+ settings.statusLine = {
298
+ type: "command",
299
+ command: emobarCmd,
300
+ padding: 0
301
+ };
302
+ }
303
+ writeSettings(filePath, settings);
304
+ }
305
+ function restoreStatusLine(filePath = SETTINGS_PATH) {
306
+ if (!fs2.existsSync(filePath)) return;
307
+ const settings = readSettings(filePath);
308
+ const current = settings.statusLine;
309
+ if (!current?.command?.includes("emobar")) return;
310
+ const wrappedMatch = current.command.match(
311
+ /^bash -c '(.+?); echo -n " "; npx emobar display.*'$/
312
+ );
313
+ if (wrappedMatch) {
314
+ settings.statusLine = {
315
+ type: "command",
316
+ command: wrappedMatch[1],
317
+ padding: current.padding ?? 0
318
+ };
319
+ } else {
320
+ delete settings.statusLine;
321
+ }
322
+ writeSettings(filePath, settings);
323
+ }
324
+ export {
325
+ STATE_FILE,
326
+ analyzeBehavior,
327
+ computeDivergence,
328
+ computeStressIndex,
329
+ configureStatusLine,
330
+ formatCompact,
331
+ formatMinimal,
332
+ formatState,
333
+ parseEmoBarTag,
334
+ readState,
335
+ restoreStatusLine
336
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "emobar",
3
+ "version": "2.0.0",
4
+ "description": "Emotional status bar companion for Claude Code - makes AI emotional state visible",
5
+ "type": "module",
6
+ "bin": {
7
+ "emobar": "./dist/cli.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsup",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest",
24
+ "dev": "tsup --watch"
25
+ },
26
+ "keywords": [
27
+ "claude",
28
+ "claude-code",
29
+ "statusline",
30
+ "emotional",
31
+ "ai",
32
+ "transparency"
33
+ ],
34
+ "author": "v4l3r10",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/v4l3r10/emobar.git"
39
+ },
40
+ "homepage": "https://github.com/v4l3r10/emobar#readme",
41
+ "bugs": {
42
+ "url": "https://github.com/v4l3r10/emobar/issues"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.5.2",
49
+ "tsup": "^8.0.0",
50
+ "typescript": "^5.5.0",
51
+ "vitest": "^3.0.0"
52
+ }
53
+ }