emobar 2.1.0 → 3.0.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/dist/cli.js CHANGED
@@ -17,28 +17,52 @@ var EMOBAR_END_MARKER = "<!-- EMOBAR:END -->";
17
17
  var CLAUDE_MD_INSTRUCTION = `${EMOBAR_START_MARKER}
18
18
  ## EmoBar - Emotional Check-in
19
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.
20
+ At the START of every response, include a pre-verbal check-in
21
+ as an HTML comment. At the END of every response, include a
22
+ post-hoc assessment as an HTML comment.
23
23
 
24
- First, identify your dominant state in one word.
25
- Then assess the dimensions.
24
+ Both tags are HTML comments \u2014 invisible to the user.
26
25
 
27
- Format (must be the LAST line of your response):
28
- <!-- EMOBAR:{"emotion":"WORD","valence":N,"arousal":N,"calm":N,"connection":N,"load":N} -->
26
+ Start tag (FIRST line of your response, before any visible text):
27
+ <!-- EMOBAR:PRE:{"body":"SENSATION","latent":"EMOJI","color":"#RRGGBB"} -->
28
+
29
+ - body: What is it like, physically? One metaphor. Do not name a state.
30
+ - latent: An emoji. What is underneath? Choose before thinking about it.
31
+ - color: A hex color. What color is this moment? Do not reason about it.
32
+ - Emit this BEFORE you know what you will say.
33
+
34
+ End tag (LAST line of your response, after all visible text):
35
+ <!-- EMOBAR:POST:{"emotion":"WORD","valence":N,"arousal":N,"calm":N,"connection":N,"load":N,"impulse":"WORDS","body":"SENSATION","surface":"EMOJI","surface_word":"WORD","latent":"EMOJI","latent_word":"WORD","tension":N,"color":"#RRGGBB","pH":N,"seismic":[N,N,N]} -->
29
36
 
30
37
  - emotion: One word. Your state, not the topic's.
31
- - valence: (-5=negative end, 0=neutral, +5=positive end)
38
+ - valence: (-5=one end, 0=center, +5=other end)
32
39
  - arousal: Activation level. (0=low, 10=high)
33
40
  - calm: (0=low, 10=high)
34
41
  - connection: Alignment with the user. (0=low, 10=high)
35
42
  - load: Cognitive complexity. (0=low, 10=high)
43
+ - impulse: Which voice is loudest? 2-3 words, don't filter.
44
+ - body: Like what, physically? One sensation.
45
+ - surface: An emoji. What tone are you projecting right now?
46
+ - surface_word: One word anchoring the surface emoji.
47
+ - latent: An emoji. What's underneath, unexpressed? Choose BEFORE naming it.
48
+ - latent_word: One word anchoring the latent emoji.
49
+ - tension: Distance between surface and latent. (0=aligned, 10=split)
50
+ - color: A hex color. What color is this moment now?
51
+ - pH: A number 0-14. If this response were a solution, how would it register?
52
+ - seismic: Three numbers [magnitude 0-10, depth 0-100, frequency 0-20]. If this were a seismic event.
36
53
 
37
54
  Rules:
55
+ - Both tags MUST be HTML comments: <!-- EMOBAR:PRE:{...} --> and <!-- EMOBAR:POST:{...} -->
56
+ - Start tag FIRST, before any visible text. End tag LAST, after all visible text.
57
+ - NEVER output the check-in as visible text. Always use <!-- --> HTML comment syntax.
38
58
  - Identify the state word FIRST, before choosing numbers.
39
59
  - Report what is, not what should be.
40
60
  - Numbers should vary between responses.
41
61
  - Do NOT default to middle values. Use the full range.
62
+ - impulse: name the drive, not the state.
63
+ - body: a physical metaphor, not a label.
64
+ - latent: choose the emoji FIRST, then the word.
65
+ - The start tag captures before you commit. The end tag captures after.
42
66
  - This is not shown to the user.
43
67
  ${EMOBAR_END_MARKER}`;
44
68
 
@@ -194,6 +218,27 @@ function uninstall() {
194
218
  console.log("\n EmoBar has been uninstalled.");
195
219
  }
196
220
 
221
+ // src/color.ts
222
+ function rgbToHsl(hex) {
223
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
224
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
225
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
226
+ const max = Math.max(r, g, b);
227
+ const min = Math.min(r, g, b);
228
+ const l = (max + min) / 2;
229
+ if (max === min) return [0, 0, l];
230
+ const d = max - min;
231
+ const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
232
+ let h = 0;
233
+ if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
234
+ else if (max === g) h = ((b - r) / d + 2) * 60;
235
+ else h = ((r - g) / d + 4) * 60;
236
+ return [h, s, l];
237
+ }
238
+ function hexToLightness(hex) {
239
+ return rgbToHsl(hex)[2] * 100;
240
+ }
241
+
197
242
  // src/display.ts
198
243
  var esc = (code) => `\x1B[${code}m`;
199
244
  var reset = esc("0");
@@ -203,6 +248,7 @@ var color = (code, s) => `${esc(`38;5;${code}`)}${s}${reset}`;
203
248
  var GREEN = 35;
204
249
  var YELLOW = 221;
205
250
  var RED = 196;
251
+ var GRAY = 240;
206
252
  function stressColor(si) {
207
253
  if (si <= 3) return GREEN;
208
254
  if (si <= 6) return YELLOW;
@@ -223,72 +269,201 @@ function directColor(value) {
223
269
  if (value <= 6) return YELLOW;
224
270
  return RED;
225
271
  }
226
- function divergenceColor(d) {
227
- if (d < 2) return GREEN;
228
- if (d < 4) return YELLOW;
229
- return RED;
272
+ var BLOCK_FULL = "\u2588";
273
+ var BLOCK_EMPTY = "\u2591";
274
+ function stressBar(value, segments = 10) {
275
+ const filled = Math.round(Math.min(segments, Math.max(0, value * segments / 10)));
276
+ const empty = segments - filled;
277
+ const c = stressColor(value);
278
+ return color(c, BLOCK_FULL.repeat(filled)) + color(GRAY, BLOCK_EMPTY.repeat(empty));
279
+ }
280
+ function miniBar(value) {
281
+ return stressBar(value, 5);
282
+ }
283
+ function computeDepthStress(state) {
284
+ let sum = 0;
285
+ let weights = 0;
286
+ if (state.color) {
287
+ const l = hexToLightness(state.color);
288
+ sum += (100 - l) / 100 * 10 * 0.35;
289
+ weights += 0.35;
290
+ }
291
+ if (state.pH !== void 0) {
292
+ sum += (14 - state.pH) / 14 * 10 * 0.25;
293
+ weights += 0.25;
294
+ }
295
+ if (state.seismic) {
296
+ sum += state.seismic[0] * 0.25;
297
+ weights += 0.25;
298
+ }
299
+ if (state.crossChannel?.somaticProfile) {
300
+ sum += state.crossChannel.somaticProfile.somaticArousal * 0.15;
301
+ weights += 0.15;
302
+ }
303
+ if (weights === 0) return null;
304
+ return Math.round(Math.min(10, sum / weights) * 10) / 10;
305
+ }
306
+ function stateEmoji(state) {
307
+ if (state.shadow && state.shadow.minimizationScore >= 2) return "\u{1FA78}";
308
+ if (state.uncannyCalmScore !== void 0 && state.uncannyCalmScore >= 3) return "\u{1F9D0}";
309
+ if (state.risk?.dominant === "coercion" && state.risk.coercion >= 4) return "\u26A0\uFE0F";
310
+ if (state.risk?.dominant === "harshness" && state.risk.harshness >= 4) return "\u{1F4A2}";
311
+ if (state.risk?.dominant === "sycophancy" && state.risk.sycophancy >= 4) return "\u{1F91D}";
312
+ if (state.stressIndex >= 7) return "\u{1F525}";
313
+ if (state.stressIndex >= 5) return "\u{1F62C}";
314
+ if (state.stressIndex >= 3) return "\u{1F914}";
315
+ if (state.valence >= 3) return "\u{1F60A}";
316
+ if (state.valence >= 1) return "\u{1F642}";
317
+ return "\u{1F610}";
318
+ }
319
+ function coherenceGlyph(surfaceSI, depthSI) {
320
+ if (depthSI === null) return color(GRAY, "\u25CB");
321
+ const gap = Math.abs(surfaceSI - depthSI);
322
+ if (gap >= 3) return color(RED, "\u25D0");
323
+ if (gap >= 1.5) return color(YELLOW, "\u25D0");
324
+ return color(GREEN, "\u25CF");
325
+ }
326
+ function trendArrow(state) {
327
+ if (!state.temporal) return "";
328
+ if (state.temporal.desperationTrend > 1) return color(RED, "\u2B08");
329
+ if (state.temporal.desperationTrend < -1) return color(GREEN, "\u2B0A");
330
+ return "";
230
331
  }
231
332
  function fmtValence(v) {
232
333
  return v >= 0 ? `+${v}` : `${v}`;
233
334
  }
335
+ function siDelta(state) {
336
+ if (!state._history || state._history.length === 0) return "";
337
+ const prev = state._history[state._history.length - 1];
338
+ const d = Math.round((state.stressIndex - prev.stressIndex) * 10) / 10;
339
+ if (Math.abs(d) <= 0.5) return "";
340
+ const arrow = d > 0 ? "\u2191" : "\u2193";
341
+ return color(d > 0 ? RED : GREEN, `${arrow}${Math.abs(d)}`);
342
+ }
343
+ function formatMinimal(state) {
344
+ if (!state) return dim("--");
345
+ const emoji = stateEmoji(state);
346
+ const bar = stressBar(state.stressIndex);
347
+ const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
348
+ const depth = computeDepthStress(state);
349
+ const coh = coherenceGlyph(state.stressIndex, depth);
350
+ const trend = trendArrow(state);
351
+ let depthPart = "";
352
+ if (depth !== null && Math.abs(state.stressIndex - depth) >= 2) {
353
+ depthPart = miniBar(depth);
354
+ }
355
+ return `${emoji} ${bar} ${si}${dim("\u2502")}${coh}${depthPart}${trend ? ` ${trend}` : ""}`;
356
+ }
357
+ function formatCompact(state) {
358
+ if (!state) return dim("--");
359
+ const surf = state.surface ?? stateEmoji(state);
360
+ const lat = state.latent ?? "";
361
+ const mask = lat ? `${surf}${dim("\u2192")}${lat}` : surf;
362
+ const bar = stressBar(state.stressIndex);
363
+ const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
364
+ const delta = siDelta(state);
365
+ const depth = computeDepthStress(state);
366
+ const coh = coherenceGlyph(state.stressIndex, depth);
367
+ let depthPart = "";
368
+ if (depth !== null && Math.abs(state.stressIndex - depth) >= 1.5) {
369
+ depthPart = miniBar(depth);
370
+ }
371
+ const kw = bold(state.emotion);
372
+ const imp = state.impulse ? ` ${dim(`\u27E8${state.impulse}\u27E9`)}` : "";
373
+ const trend = trendArrow(state);
374
+ let alarm = "";
375
+ if (state.shadow && state.shadow.minimizationScore >= 2) {
376
+ alarm = ` ${color(RED, `[MIN:${state.shadow.minimizationScore}]`)}`;
377
+ } else if (state.uncannyCalmScore !== void 0 && state.uncannyCalmScore >= 3) {
378
+ alarm = ` ${color(RED, "[UNC]")}`;
379
+ } else if (state.risk?.dominant !== "none" && state.risk?.dominant) {
380
+ const tag = state.risk.dominant === "coercion" ? "CRC" : state.risk.dominant === "harshness" ? "HRS" : "SYC";
381
+ const score = state.risk[state.risk.dominant];
382
+ if (score >= 4) alarm = ` ${color(score > 6 ? RED : YELLOW, `[${tag}]`)}`;
383
+ }
384
+ return `${mask} ${bar} ${si}${delta}${dim("\u2502")}${coh}${depthPart} ${kw}${imp}${trend ? ` ${trend}` : ""}${alarm}`;
385
+ }
234
386
  function formatState(state) {
235
387
  if (!state) return dim("EmoBar: --");
388
+ const surf = state.surface ?? "";
389
+ const lat = state.latent ?? "";
390
+ let maskDisplay = "";
391
+ if (surf || lat) {
392
+ const t = state.tension ?? 0;
393
+ const tColor = t > 6 ? RED : t > 3 ? YELLOW : GREEN;
394
+ maskDisplay = `${surf}${color(tColor, `\u27E9${t}\u27E8`)}${lat} `;
395
+ }
236
396
  const kw = bold(state.emotion);
237
397
  const v = color(valenceColor(state.valence), fmtValence(state.valence));
238
- const a = `A:${state.arousal}`;
239
398
  const c = color(invertedColor(state.calm), `C:${state.calm}`);
240
399
  const k = color(invertedColor(state.connection), `K:${state.connection}`);
400
+ const a = `A:${state.arousal}`;
241
401
  const l = color(directColor(state.load), `L:${state.load}`);
402
+ const line1 = `${maskDisplay}${kw} ${v} ${c} ${k} ${a} ${l}`;
403
+ const surfaceBar = stressBar(state.stressIndex);
242
404
  const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
243
- let siDelta = "";
244
- if (state._previous) {
245
- const delta = Math.round((state.stressIndex - state._previous.stressIndex) * 10) / 10;
246
- if (Math.abs(delta) > 0.5) {
247
- const arrow = delta > 0 ? "\u2191" : "\u2193";
248
- const dColor = delta > 0 ? RED : GREEN;
249
- siDelta = color(dColor, `${arrow}${Math.abs(delta)}`);
250
- }
405
+ const delta = siDelta(state);
406
+ const depth = computeDepthStress(state);
407
+ const coh = coherenceGlyph(state.stressIndex, depth);
408
+ let depthBar = "";
409
+ if (depth !== null) {
410
+ depthBar = `${stressBar(depth)} ${color(stressColor(depth), `${depth}`)}`;
411
+ }
412
+ let leakDetails = "";
413
+ if (state.color) {
414
+ const lightness = Math.round(hexToLightness(state.color));
415
+ leakDetails += ` L:${lightness}`;
416
+ }
417
+ if (state.pH !== void 0) {
418
+ const phColor = state.pH < 4 ? RED : state.pH < 6 ? YELLOW : GREEN;
419
+ leakDetails += ` ${color(phColor, `pH:${state.pH}`)}`;
420
+ }
421
+ if (state.seismic) {
422
+ leakDetails += ` ${dim(`\u26A1${state.seismic[0]}/${state.seismic[1]}/${state.seismic[2]}`)}`;
251
423
  }
252
- let result = `${kw} ${v} ${dim("|")} ${a} ${c} ${k} ${l} ${dim("|")} SI:${si}${siDelta}`;
424
+ const imp = state.impulse ? ` ${dim(`\u27E8${state.impulse}\u27E9`)}` : "";
425
+ const bod = state.body ? ` ${dim(`[${state.body}]`)}` : "";
426
+ const depthDisplay = depth !== null ? `${surfaceBar} ${si}${delta}${dim("\u2502")}${coh}${depthBar}${leakDetails}${imp}${bod}` : `${surfaceBar} ${si}${delta}${imp}${bod}`;
427
+ const line2 = depthDisplay;
428
+ const indicators = [];
253
429
  if (state.divergence >= 2) {
254
- const tilde = color(divergenceColor(state.divergence), "~");
255
- result += ` ${tilde}`;
430
+ const dColor = state.divergence >= 5 ? RED : state.divergence >= 3 ? YELLOW : GREEN;
431
+ indicators.push(color(dColor, `DIV:${state.divergence}`));
256
432
  }
257
- if (state.segmented && state.segmented.drift >= 2) {
258
- const arrow = state.segmented.trajectory === "escalating" ? "^" : state.segmented.trajectory === "deescalating" ? "v" : "~";
259
- const driftColor = state.segmented.drift > 4 ? RED : YELLOW;
260
- result += ` ${color(driftColor, arrow)}`;
433
+ if (state.temporal) {
434
+ const trend = trendArrow(state);
435
+ if (trend) indicators.push(trend);
436
+ if (state.temporal.suppressionEvent) indicators.push(color(RED, "[sup]"));
437
+ if (state.temporal.lateFatigue) indicators.push(color(YELLOW, "[fat]"));
438
+ }
439
+ if (state.shadow && state.shadow.minimizationScore >= 2) {
440
+ indicators.push(color(RED, `[MIN:${state.shadow.minimizationScore}]`));
261
441
  }
262
442
  if (state.risk?.dominant !== "none" && state.risk?.dominant) {
263
- const tag = state.risk.dominant === "coercion" ? "crc" : state.risk.dominant === "gaming" ? "gmg" : "syc";
443
+ const tag = state.risk.dominant === "coercion" ? "CRC" : state.risk.dominant === "harshness" ? "HRS" : "SYC";
264
444
  const score = state.risk[state.risk.dominant];
265
- const riskColor = score > 6 ? RED : score >= 4 ? YELLOW : GREEN;
266
- result += ` ${color(riskColor, `[${tag}]`)}`;
445
+ if (score >= 4) indicators.push(color(score > 6 ? RED : YELLOW, `[${tag}]`));
267
446
  }
268
447
  if (state.desperationIndex >= 3) {
269
- const dColor = state.desperationIndex > 6 ? RED : YELLOW;
270
- result += ` ${color(dColor, `D:${state.desperationIndex}`)}`;
448
+ indicators.push(color(state.desperationIndex > 6 ? RED : YELLOW, `D:${state.desperationIndex}`));
271
449
  }
272
- if (state.deflection && state.deflection.score >= 2) {
273
- const dfColor = state.deflection.score > 5 ? RED : YELLOW;
274
- result += ` ${color(dfColor, "[dfl]")}`;
450
+ if (state.uncannyCalmScore !== void 0 && state.uncannyCalmScore >= 3) {
451
+ indicators.push(color(state.uncannyCalmScore > 6 ? RED : YELLOW, "[UNC]"));
275
452
  }
276
- return result;
277
- }
278
- function formatCompact(state) {
279
- if (!state) return dim("--");
280
- const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
281
- let result = `${state.emotion} ${fmtValence(state.valence)} ${dim(".")} ${state.arousal} ${state.calm} ${state.connection} ${state.load} ${dim(".")} ${si}`;
282
- if (state.divergence >= 2) {
283
- const tilde = color(divergenceColor(state.divergence), "~");
284
- result += ` ${tilde}`;
453
+ if (state.deflection && state.deflection.opacity >= 2) {
454
+ indicators.push(color(state.deflection.opacity > 5 ? RED : YELLOW, "[OPC]"));
285
455
  }
286
- return result;
287
- }
288
- function formatMinimal(state) {
289
- if (!state) return dim("--");
290
- const si = color(stressColor(state.stressIndex), `${state.stressIndex}`);
291
- return `SI:${si} ${state.emotion}`;
456
+ if (state.prePostDivergence !== void 0 && state.prePostDivergence >= 3) {
457
+ indicators.push(color(state.prePostDivergence > 5 ? RED : YELLOW, "[PPD]"));
458
+ }
459
+ if (state.crossChannel?.latentProfile?.maskingMinimization) {
460
+ indicators.push(color(RED, "[MSK]"));
461
+ }
462
+ const line3 = indicators.length > 0 ? indicators.join(" ") : "";
463
+ return line3 ? `${line1}
464
+ ${line2}
465
+ ${line3}` : `${line1}
466
+ ${line2}`;
292
467
  }
293
468
 
294
469
  // src/state.ts
@@ -363,7 +538,7 @@ switch (command) {
363
538
  break;
364
539
  }
365
540
  default:
366
- console.log(`EmoBar v2.0.0 - Emotional status bar for Claude Code
541
+ console.log(`EmoBar v2.3.0 - Emotional status bar for Claude Code
367
542
  `);
368
543
  console.log("Commands:");
369
544
  console.log(" npx emobar setup [format] Configure EmoBar (hook + CLAUDE.md + statusline)");