@npow/oh-my-claude 0.1.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 (57) hide show
  1. package/README.md +317 -0
  2. package/bin/omc.js +403 -0
  3. package/docs/architecture.md +198 -0
  4. package/docs/segment-contract.md +186 -0
  5. package/docs/theme-format.md +156 -0
  6. package/package.json +35 -0
  7. package/src/cache.js +102 -0
  8. package/src/color.js +105 -0
  9. package/src/compositor.js +163 -0
  10. package/src/config.js +146 -0
  11. package/src/plugins.js +72 -0
  12. package/src/runner.js +160 -0
  13. package/src/segments/achievement.js +68 -0
  14. package/src/segments/api-timer.js +55 -0
  15. package/src/segments/battle-log.js +55 -0
  16. package/src/segments/cat.js +89 -0
  17. package/src/segments/coffee-cup.js +81 -0
  18. package/src/segments/commit-msg.js +95 -0
  19. package/src/segments/context-bar.js +50 -0
  20. package/src/segments/context-percent.js +40 -0
  21. package/src/segments/context-tokens.js +52 -0
  22. package/src/segments/cost-budget.js +43 -0
  23. package/src/segments/coworker.js +137 -0
  24. package/src/segments/custom-text.js +25 -0
  25. package/src/segments/directory.js +75 -0
  26. package/src/segments/emoji-story.js +99 -0
  27. package/src/segments/flex-space.js +25 -0
  28. package/src/segments/fortune-cookie.js +131 -0
  29. package/src/segments/garden.js +57 -0
  30. package/src/segments/git-branch.js +36 -0
  31. package/src/segments/git-status.js +56 -0
  32. package/src/segments/horoscope.js +134 -0
  33. package/src/segments/index.js +65 -0
  34. package/src/segments/lines-changed.js +29 -0
  35. package/src/segments/model-name.js +28 -0
  36. package/src/segments/narrator.js +129 -0
  37. package/src/segments/output-style.js +25 -0
  38. package/src/segments/rpg-stats.js +119 -0
  39. package/src/segments/separator-arrow.js +22 -0
  40. package/src/segments/separator-pipe.js +22 -0
  41. package/src/segments/separator-space.js +22 -0
  42. package/src/segments/session-cost.js +72 -0
  43. package/src/segments/session-timer.js +53 -0
  44. package/src/segments/smart-nudge.js +97 -0
  45. package/src/segments/soundtrack.js +133 -0
  46. package/src/segments/speedrun.js +94 -0
  47. package/src/segments/stock-ticker.js +71 -0
  48. package/src/segments/streak.js +131 -0
  49. package/src/segments/tamagotchi.js +95 -0
  50. package/src/segments/token-sparkline.js +73 -0
  51. package/src/segments/version.js +27 -0
  52. package/src/segments/vibe-check.js +109 -0
  53. package/src/segments/vim-mode.js +29 -0
  54. package/src/segments/weather-report.js +88 -0
  55. package/themes/default.json +59 -0
  56. package/themes/minimal.json +37 -0
  57. package/themes/powerline.json +73 -0
@@ -0,0 +1,55 @@
1
+ // src/segments/battle-log.js — Session framed as a dungeon crawl
2
+ // Zero dependencies. Node 18+ ESM.
3
+ //
4
+ // Context percentage maps to dungeon depth:
5
+ // >= 95% FINAL BOSS
6
+ // >= 80% Boss Battle
7
+ // >= 60% Deep Dungeon
8
+ // >= 40% Mid Dungeon
9
+ // >= 20% Exploring
10
+ // < 20% Base Camp
11
+ //
12
+ // Loot bonus: if lines_added > 0, appends gold count.
13
+
14
+ export const meta = {
15
+ name: 'battle-log',
16
+ description: 'Session framed as a dungeon crawl based on context depth',
17
+ requires: [],
18
+ defaultConfig: {
19
+ style: '',
20
+ },
21
+ };
22
+
23
+ const DEPTHS = [
24
+ { min: 95, text: () => '\u2694\uFE0F FINAL BOSS (95%)', style: 'bold red' },
25
+ { min: 80, text: (p) => `\u2694\uFE0F Boss Battle (${p}%)`, style: 'bold yellow' },
26
+ { min: 60, text: (p) => `\uD83C\uDFF0 Deep Dungeon (${p}%)`, style: 'yellow' },
27
+ { min: 40, text: (p) => `\uD83D\uDDE1\uFE0F Mid Dungeon (${p}%)`, style: 'cyan' },
28
+ { min: 20, text: (p) => `\uD83D\uDEAA Exploring (${p}%)`, style: 'green' },
29
+ { min: -Infinity, text: (p) => `\uD83C\uDFD5\uFE0F Base Camp (${p}%)`, style: 'dim' },
30
+ ];
31
+
32
+ /**
33
+ * @param {object} data - Parsed stdin JSON from Claude Code
34
+ * @param {object} config - Per-segment config from theme
35
+ * @returns {{text: string, style: string}|null}
36
+ */
37
+ export function render(data, config) {
38
+ const cfg = { ...meta.defaultConfig, ...config };
39
+
40
+ const pct = data?.context_window?.used_percentage;
41
+ if (pct == null) return null;
42
+
43
+ const rounded = Math.round(pct);
44
+ const depth = DEPTHS.find((d) => rounded >= d.min);
45
+ let text = depth.text(rounded);
46
+
47
+ const linesAdded = data?.cost?.total_lines_added ?? 0;
48
+ if (linesAdded > 0) {
49
+ text += ` +${linesAdded} gold`;
50
+ }
51
+
52
+ const style = cfg.style || depth.style;
53
+
54
+ return { text, style };
55
+ }
@@ -0,0 +1,89 @@
1
+ // src/segments/cat.js — A cat that does cat things based on session state
2
+ // Zero dependencies. Node 18+ ESM.
3
+ //
4
+ // States (priority order):
5
+ // 1. Sits on context window — context >= 90%
6
+ // 2. Pushes lines off desk — lines_removed > lines_added AND lines_removed > 20
7
+ // 3. Sleeping — api_duration > total_duration * 0.5 AND total_duration > 120s
8
+ // 4. Watches intently — lines_added > 200
9
+ // 5. Knocks wallet off table — cost > $10
10
+ // 6. Yawns — duration > 1 hour
11
+ // 7. Perks up — duration < 1 minute
12
+ // 8. Default — just a cat
13
+
14
+ export const meta = {
15
+ name: 'cat',
16
+ description: 'A cat that does cat things based on session state',
17
+ requires: [],
18
+ defaultConfig: {
19
+ style: '',
20
+ },
21
+ };
22
+
23
+ /**
24
+ * Determine the cat's current behavior from session metrics.
25
+ * Priority order is enforced by early returns.
26
+ *
27
+ * @param {object} data - Parsed stdin JSON from Claude Code
28
+ * @returns {{ text: string, style: string }}
29
+ */
30
+ function resolveCat(data) {
31
+ const context = data?.context_window?.used_percentage ?? 0;
32
+ const linesAdded = data?.cost?.total_lines_added ?? 0;
33
+ const linesRemoved = data?.cost?.total_lines_removed ?? 0;
34
+ const cost = data?.cost?.total_cost_usd ?? 0;
35
+ const totalDuration = data?.cost?.total_duration_ms ?? 0;
36
+ const apiDuration = data?.cost?.total_api_duration_ms ?? 0;
37
+
38
+ // 1. Sits on context window
39
+ if (context >= 90) {
40
+ return { text: '=^._.^= *sits on context window*', style: 'bold yellow' };
41
+ }
42
+
43
+ // 2. Pushes lines off desk
44
+ if (linesRemoved > linesAdded && linesRemoved > 20) {
45
+ return { text: `=^._.^= *pushes ${linesRemoved} lines off desk*`, style: 'red' };
46
+ }
47
+
48
+ // 3. Sleeping — API wait dominates a non-trivial session
49
+ if (totalDuration > 120_000 && apiDuration > totalDuration * 0.5) {
50
+ return { text: '=^._.^= zzz', style: 'dim' };
51
+ }
52
+
53
+ // 4. Watches intently
54
+ if (linesAdded > 200) {
55
+ return { text: '=^._.^= *watches intently*', style: 'cyan' };
56
+ }
57
+
58
+ // 5. Knocks wallet off table
59
+ if (cost > 10) {
60
+ return { text: '=^._.^= *knocks wallet off table*', style: 'yellow' };
61
+ }
62
+
63
+ // 6. Yawns — long session
64
+ if (totalDuration > 3_600_000) {
65
+ return { text: '=^._.^= *yawns*', style: 'dim' };
66
+ }
67
+
68
+ // 7. Perks up — fresh session
69
+ if (totalDuration < 60_000) {
70
+ return { text: '=^._.^= *perks up*', style: 'green' };
71
+ }
72
+
73
+ // 8. Default
74
+ return { text: '=^._.^=', style: 'dim' };
75
+ }
76
+
77
+ /**
78
+ * @param {object} data - Parsed stdin JSON from Claude Code
79
+ * @param {object} config - Per-segment config from theme
80
+ * @returns {{text: string, style: string}}
81
+ */
82
+ export function render(data, config) {
83
+ const cfg = { ...meta.defaultConfig, ...config };
84
+
85
+ const cat = resolveCat(data);
86
+ const style = cfg.style || cat.style;
87
+
88
+ return { text: cat.text, style };
89
+ }
@@ -0,0 +1,81 @@
1
+ // src/segments/coffee-cup.js — Coffee cup that drains over a 2-hour session
2
+ // Zero dependencies. Node 18+ ESM.
3
+ //
4
+ // Based on total_duration_ms:
5
+ // < 15min: [████] bold
6
+ // 15-30min: [███░] bold
7
+ // 30-45min: [██░░] (none)
8
+ // 45-60min: [█░░░] yellow
9
+ // 60-90min: [░░░░] dim
10
+ // 90min+: [ ] refill? bold red
11
+
12
+ const MIN_15 = 15 * 60 * 1000;
13
+ const MIN_30 = 30 * 60 * 1000;
14
+ const MIN_45 = 45 * 60 * 1000;
15
+ const MIN_60 = 60 * 60 * 1000;
16
+ const MIN_90 = 90 * 60 * 1000;
17
+
18
+ export const meta = {
19
+ name: 'coffee-cup',
20
+ description: 'A coffee cup that drains as the session progresses',
21
+ requires: [],
22
+ defaultConfig: {
23
+ style: '',
24
+ charFull: '\u2588', // █
25
+ charEmpty: '\u2591', // ░
26
+ },
27
+ };
28
+
29
+ /**
30
+ * Build the coffee cup display string.
31
+ *
32
+ * @param {number} fullCount - Number of full blocks (0-4)
33
+ * @param {string} charFull - Character for full portion
34
+ * @param {string} charEmpty - Character for empty portion
35
+ * @param {boolean} refill - Whether to append " refill?"
36
+ * @returns {string}
37
+ */
38
+ function buildCup(fullCount, charFull, charEmpty, refill) {
39
+ if (refill) {
40
+ return '[ ] refill?';
41
+ }
42
+ const full = charFull.repeat(fullCount);
43
+ const empty = charEmpty.repeat(4 - fullCount);
44
+ return `[${full}${empty}]`;
45
+ }
46
+
47
+ /**
48
+ * @param {object} data - Parsed stdin JSON from Claude Code
49
+ * @param {object} config - Per-segment config from theme
50
+ * @returns {{text: string, style: string}}
51
+ */
52
+ export function render(data, config) {
53
+ const cfg = { ...meta.defaultConfig, ...config };
54
+
55
+ const durationMs = data?.cost?.total_duration_ms ?? 0;
56
+
57
+ let text;
58
+ let levelStyle;
59
+
60
+ if (durationMs >= MIN_90) {
61
+ text = buildCup(0, cfg.charFull, cfg.charEmpty, true);
62
+ levelStyle = 'bold red';
63
+ } else if (durationMs >= MIN_60) {
64
+ text = buildCup(0, cfg.charFull, cfg.charEmpty, false);
65
+ levelStyle = 'dim';
66
+ } else if (durationMs >= MIN_45) {
67
+ text = buildCup(1, cfg.charFull, cfg.charEmpty, false);
68
+ levelStyle = 'yellow';
69
+ } else if (durationMs >= MIN_30) {
70
+ text = buildCup(2, cfg.charFull, cfg.charEmpty, false);
71
+ levelStyle = '';
72
+ } else if (durationMs >= MIN_15) {
73
+ text = buildCup(3, cfg.charFull, cfg.charEmpty, false);
74
+ levelStyle = 'bold';
75
+ } else {
76
+ text = buildCup(4, cfg.charFull, cfg.charEmpty, false);
77
+ levelStyle = 'bold';
78
+ }
79
+
80
+ return { text, style: cfg.style || levelStyle };
81
+ }
@@ -0,0 +1,95 @@
1
+ // src/segments/commit-msg.js — Preview what the commit message SHOULD be based on session activity
2
+ // Zero dependencies. Node 18+ ESM.
3
+ //
4
+ // Message selection (priority order, first match wins):
5
+ // 1. removed > added * 2 AND removed > 20 -> "fix: remove everything"
6
+ // 2. removed > added AND removed > 10 -> "refactor: simplify"
7
+ // 3. lines_added > 500 -> "feat: rewrite the entire codebase"
8
+ // 4. lines_added > 200 -> "feat: something incredible"
9
+ // 5. lines_added > 100 -> "feat: new feature"
10
+ // 6. lines_added > 50 -> "chore: updates"
11
+ // 7. lines_added > 10 -> "fix: the thing"
12
+ // 8. lines_added > 0 -> "style: whitespace"
13
+ // 9. default -> "docs: update README"
14
+ //
15
+ // Display: git commit -m "{message}" (or just the message if showPrefix is false)
16
+
17
+ export const meta = {
18
+ name: 'commit-msg',
19
+ description: 'Preview what the commit message should be based on session activity',
20
+ requires: [],
21
+ defaultConfig: {
22
+ style: 'dim',
23
+ showPrefix: true,
24
+ },
25
+ };
26
+
27
+ /**
28
+ * Determine the commit message based on session line change metrics.
29
+ *
30
+ * @param {object} data - Parsed stdin JSON from Claude Code
31
+ * @returns {string}
32
+ */
33
+ function resolveMessage(data) {
34
+ const linesAdded = data?.cost?.total_lines_added ?? 0;
35
+ const linesRemoved = data?.cost?.total_lines_removed ?? 0;
36
+
37
+ // 1. Massive net deletion
38
+ if (linesRemoved > linesAdded * 2 && linesRemoved > 20) {
39
+ return 'fix: remove everything';
40
+ }
41
+
42
+ // 2. Net deletion
43
+ if (linesRemoved > linesAdded && linesRemoved > 10) {
44
+ return 'refactor: simplify';
45
+ }
46
+
47
+ // 3. Huge additions
48
+ if (linesAdded > 500) {
49
+ return 'feat: rewrite the entire codebase';
50
+ }
51
+
52
+ // 4. Large additions
53
+ if (linesAdded > 200) {
54
+ return 'feat: something incredible';
55
+ }
56
+
57
+ // 5. Moderate additions
58
+ if (linesAdded > 100) {
59
+ return 'feat: new feature';
60
+ }
61
+
62
+ // 6. Small additions
63
+ if (linesAdded > 50) {
64
+ return 'chore: updates';
65
+ }
66
+
67
+ // 7. Minor additions
68
+ if (linesAdded > 10) {
69
+ return 'fix: the thing';
70
+ }
71
+
72
+ // 8. Tiny additions
73
+ if (linesAdded > 0) {
74
+ return 'style: whitespace';
75
+ }
76
+
77
+ // 9. Default — nothing changed
78
+ return 'docs: update README';
79
+ }
80
+
81
+ /**
82
+ * @param {object} data - Parsed stdin JSON from Claude Code
83
+ * @param {object} config - Per-segment config from theme
84
+ * @returns {{text: string, style: string}}
85
+ */
86
+ export function render(data, config) {
87
+ const cfg = { ...meta.defaultConfig, ...config };
88
+ const message = resolveMessage(data);
89
+
90
+ const text = cfg.showPrefix
91
+ ? `git commit -m "${message}"`
92
+ : message;
93
+
94
+ return { text, style: cfg.style };
95
+ }
@@ -0,0 +1,50 @@
1
+ // src/segments/context-bar.js — Visual progress bar for context window usage
2
+ // Zero dependencies. Node 18+ ESM.
3
+
4
+ export const meta = {
5
+ name: 'context-bar',
6
+ description: 'Visual progress bar showing context window usage percentage',
7
+ requires: [],
8
+ defaultConfig: {
9
+ width: 20,
10
+ charFilled: '\u2593',
11
+ charEmpty: '\u2591',
12
+ warnAt: 60,
13
+ criticalAt: 80,
14
+ showPercent: true,
15
+ styleOk: 'green',
16
+ styleWarn: 'bold yellow',
17
+ styleCritical: 'bold red',
18
+ },
19
+ };
20
+
21
+ /**
22
+ * @param {object} data - Parsed stdin JSON from Claude Code
23
+ * @param {object} config - Per-segment config from theme
24
+ * @returns {{text: string, style: string}|null}
25
+ */
26
+ export function render(data, config) {
27
+ const cfg = { ...meta.defaultConfig, ...config };
28
+
29
+ const pct = data?.context_window?.used_percentage;
30
+ if (pct == null) return null;
31
+
32
+ const filled = Math.round((pct / 100) * cfg.width);
33
+ const empty = cfg.width - filled;
34
+ let bar = cfg.charFilled.repeat(filled) + cfg.charEmpty.repeat(empty);
35
+
36
+ if (cfg.showPercent) {
37
+ bar += ` ${Math.round(pct)}%`;
38
+ }
39
+
40
+ let style;
41
+ if (pct >= cfg.criticalAt) {
42
+ style = cfg.styleCritical;
43
+ } else if (pct >= cfg.warnAt) {
44
+ style = cfg.styleWarn;
45
+ } else {
46
+ style = cfg.styleOk;
47
+ }
48
+
49
+ return { text: bar, style };
50
+ }
@@ -0,0 +1,40 @@
1
+ // src/segments/context-percent.js — Context window usage as percentage
2
+ // Zero dependencies. Node 18+ ESM.
3
+
4
+ export const meta = {
5
+ name: 'context-percent',
6
+ description: 'Shows context window usage as a percentage',
7
+ requires: [],
8
+ defaultConfig: {
9
+ style: 'white',
10
+ warnAt: 60,
11
+ criticalAt: 80,
12
+ styleWarn: 'bold yellow',
13
+ styleCritical: 'bold red',
14
+ },
15
+ };
16
+
17
+ /**
18
+ * @param {object} data - Parsed stdin JSON from Claude Code
19
+ * @param {object} config - Per-segment config from theme
20
+ * @returns {{text: string, style: string}|null}
21
+ */
22
+ export function render(data, config) {
23
+ const cfg = { ...meta.defaultConfig, ...config };
24
+
25
+ const pct = data?.context_window?.used_percentage;
26
+ if (pct == null) return null;
27
+
28
+ const rounded = Math.round(pct);
29
+
30
+ let style;
31
+ if (pct >= cfg.criticalAt) {
32
+ style = cfg.styleCritical;
33
+ } else if (pct >= cfg.warnAt) {
34
+ style = cfg.styleWarn;
35
+ } else {
36
+ style = cfg.style;
37
+ }
38
+
39
+ return { text: `${rounded}%`, style };
40
+ }
@@ -0,0 +1,52 @@
1
+ // src/segments/context-tokens.js — Token usage as "XXk/YYYk"
2
+ // Zero dependencies. Node 18+ ESM.
3
+
4
+ export const meta = {
5
+ name: 'context-tokens',
6
+ description: 'Shows token usage as used/total (e.g., "84k/200k")',
7
+ requires: [],
8
+ defaultConfig: {
9
+ style: 'dim',
10
+ format: 'short',
11
+ },
12
+ };
13
+
14
+ /**
15
+ * Format a number in short form (e.g., 84000 -> "84k").
16
+ * @param {number} n
17
+ * @returns {string}
18
+ */
19
+ function shortFormat(n) {
20
+ if (n >= 1000) {
21
+ return `${Math.round(n / 1000)}k`;
22
+ }
23
+ return String(n);
24
+ }
25
+
26
+ /**
27
+ * Format a number with commas (e.g., 84000 -> "84,000").
28
+ * @param {number} n
29
+ * @returns {string}
30
+ */
31
+ function fullFormat(n) {
32
+ return n.toLocaleString('en-US');
33
+ }
34
+
35
+ /**
36
+ * @param {object} data - Parsed stdin JSON from Claude Code
37
+ * @param {object} config - Per-segment config from theme
38
+ * @returns {{text: string, style: string}|null}
39
+ */
40
+ export function render(data, config) {
41
+ const cfg = { ...meta.defaultConfig, ...config };
42
+
43
+ const inputTokens = data?.cost?.total_input_tokens;
44
+ const windowSize = data?.context_window?.context_window_size;
45
+
46
+ if (!inputTokens || !windowSize) return null;
47
+
48
+ const fmt = cfg.format === 'full' ? fullFormat : shortFormat;
49
+ const text = `${fmt(inputTokens)}/${fmt(windowSize)}`;
50
+
51
+ return { text, style: cfg.style };
52
+ }
@@ -0,0 +1,43 @@
1
+ // src/segments/cost-budget.js — Cost vs budget display
2
+ // Zero dependencies. Node 18+ ESM.
3
+
4
+ export const meta = {
5
+ name: 'cost-budget',
6
+ description: 'Shows current cost vs budget (e.g., "$3.50/$10.00")',
7
+ requires: [],
8
+ defaultConfig: {
9
+ style: 'white',
10
+ budget: 10.0,
11
+ precision: 2,
12
+ warnAt: 0.8,
13
+ styleWarn: 'yellow',
14
+ styleCritical: 'red',
15
+ },
16
+ };
17
+
18
+ /**
19
+ * @param {object} data - Parsed stdin JSON from Claude Code
20
+ * @param {object} config - Per-segment config from theme
21
+ * @returns {{text: string, style: string}|null}
22
+ */
23
+ export function render(data, config) {
24
+ const cfg = { ...meta.defaultConfig, ...config };
25
+
26
+ const cost = data?.cost?.total_cost_usd;
27
+ if (cost == null) return null;
28
+
29
+ const ratio = cost / cfg.budget;
30
+
31
+ let style;
32
+ if (ratio >= 1.0) {
33
+ style = cfg.styleCritical;
34
+ } else if (ratio >= cfg.warnAt) {
35
+ style = cfg.styleWarn;
36
+ } else {
37
+ style = cfg.style;
38
+ }
39
+
40
+ const text = `$${cost.toFixed(cfg.precision)}/$${cfg.budget.toFixed(cfg.precision)}`;
41
+
42
+ return { text, style };
43
+ }
@@ -0,0 +1,137 @@
1
+ // src/segments/coworker.js — Fake Slack messages from a simulated coworker watching you code
2
+ // Zero dependencies. Node 18+ ESM.
3
+ //
4
+ // Message selection (priority order, first match wins):
5
+ // 1. context >= 90% -> "we need to talk about your context usage"
6
+ // 2. context >= 75% -> "getting a bit full in here"
7
+ // 3. cost >= $20 -> "finance wants to chat"
8
+ // 4. cost >= $10 -> "that's coming out of your bonus"
9
+ // 5. lines_added > 500 -> "are you rewriting the whole thing?"
10
+ // 6. lines_added > 200 -> "ship it already"
11
+ // 7. removed > added AND removed > 50 -> "delete is my favorite key too"
12
+ // 8. duration > 2hr -> "do you even go outside?"
13
+ // 9. duration > 1hr -> "still going huh"
14
+ // 10. duration < 1min -> "oh here we go again"
15
+ // 11. default -> random pick from "looking good" / "LGTM" / "carry on"
16
+ //
17
+ // Anti-flicker: message only changes every 5 renders.
18
+
19
+ export const meta = {
20
+ name: 'coworker',
21
+ description: 'Fake Slack messages from a simulated coworker reacting to your session',
22
+ requires: [],
23
+ defaultConfig: {
24
+ style: 'dim',
25
+ botName: 'bot',
26
+ },
27
+ };
28
+
29
+ // Module-level state for anti-flicker
30
+ let lastMessage = '';
31
+ let renderCount = 0;
32
+
33
+ /**
34
+ * Simple deterministic pick from an array based on a numeric seed.
35
+ *
36
+ * @param {string[]} choices
37
+ * @param {number} seed
38
+ * @returns {string}
39
+ */
40
+ function pick(choices, seed) {
41
+ return choices[Math.abs(seed) % choices.length];
42
+ }
43
+
44
+ /**
45
+ * Resolve the coworker message based on session state, in priority order.
46
+ *
47
+ * @param {object} data - Parsed stdin JSON from Claude Code
48
+ * @param {string} botName - Display name for the fake coworker
49
+ * @param {number} seed - Deterministic seed for default message picks
50
+ * @returns {string}
51
+ */
52
+ function resolveMessage(data, botName, seed) {
53
+ const contextPct = data?.context_window?.used_percentage ?? 0;
54
+ const cost = data?.cost?.total_cost_usd ?? 0;
55
+ const linesAdded = data?.cost?.total_lines_added ?? 0;
56
+ const linesRemoved = data?.cost?.total_lines_removed ?? 0;
57
+ const duration = data?.cost?.total_duration_ms ?? 0;
58
+
59
+ // 1. Context >= 90%
60
+ if (contextPct >= 90) {
61
+ return `@${botName}: 'we need to talk about your context usage'`;
62
+ }
63
+
64
+ // 2. Context >= 75%
65
+ if (contextPct >= 75) {
66
+ return `@${botName}: 'getting a bit full in here'`;
67
+ }
68
+
69
+ // 3. Cost >= $20
70
+ if (cost >= 20) {
71
+ return `@${botName}: 'finance wants to chat'`;
72
+ }
73
+
74
+ // 4. Cost >= $10
75
+ if (cost >= 10) {
76
+ return `@${botName}: 'that's coming out of your bonus'`;
77
+ }
78
+
79
+ // 5. Lines added > 500
80
+ if (linesAdded > 500) {
81
+ return `@${botName}: 'are you rewriting the whole thing?'`;
82
+ }
83
+
84
+ // 6. Lines added > 200
85
+ if (linesAdded > 200) {
86
+ return `@${botName}: 'ship it already'`;
87
+ }
88
+
89
+ // 7. Net deletion with significant removal
90
+ if (linesRemoved > linesAdded && linesRemoved > 50) {
91
+ return `@${botName}: 'delete is my favorite key too'`;
92
+ }
93
+
94
+ // 8. Duration > 2 hours
95
+ if (duration > 7_200_000) {
96
+ return `@${botName}: 'do you even go outside?'`;
97
+ }
98
+
99
+ // 9. Duration > 1 hour
100
+ if (duration > 3_600_000) {
101
+ return `@${botName}: 'still going huh'`;
102
+ }
103
+
104
+ // 10. Duration < 1 minute
105
+ if (duration < 60_000) {
106
+ return `@${botName}: 'oh here we go again'`;
107
+ }
108
+
109
+ // 11. Default — pick one deterministically
110
+ return pick(
111
+ [
112
+ `@${botName}: 'looking good'`,
113
+ `@${botName}: 'LGTM'`,
114
+ `@${botName}: 'carry on'`,
115
+ ],
116
+ seed,
117
+ );
118
+ }
119
+
120
+ /**
121
+ * @param {object} data - Parsed stdin JSON from Claude Code
122
+ * @param {object} config - Per-segment config from theme
123
+ * @returns {{text: string, style: string}}
124
+ */
125
+ export function render(data, config) {
126
+ const cfg = { ...meta.defaultConfig, ...config };
127
+
128
+ // Only update the message every 5 renders to prevent flickering
129
+ if (renderCount % 5 === 0) {
130
+ const seed = Math.floor(renderCount / 5);
131
+ lastMessage = resolveMessage(data, cfg.botName, seed);
132
+ }
133
+
134
+ renderCount++;
135
+
136
+ return { text: lastMessage, style: cfg.style };
137
+ }
@@ -0,0 +1,25 @@
1
+ // src/segments/custom-text.js — User-configurable static text segment
2
+ // Zero dependencies. Node 18+ ESM.
3
+
4
+ export const meta = {
5
+ name: 'custom-text',
6
+ description: 'Shows a user-configurable static text string',
7
+ requires: [],
8
+ defaultConfig: {
9
+ style: 'dim',
10
+ text: '',
11
+ },
12
+ };
13
+
14
+ /**
15
+ * @param {object} _data - Parsed stdin JSON from Claude Code (unused)
16
+ * @param {object} config - Per-segment config from theme
17
+ * @returns {{text: string, style: string}|null}
18
+ */
19
+ export function render(_data, config) {
20
+ const cfg = { ...meta.defaultConfig, ...config };
21
+
22
+ if (!cfg.text) return null;
23
+
24
+ return { text: cfg.text, style: cfg.style };
25
+ }