@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,75 @@
1
+ // src/segments/directory.js — Current working directory display
2
+ // Zero dependencies. Node 18+ ESM.
3
+
4
+ import { homedir } from 'node:os';
5
+
6
+ export const meta = {
7
+ name: 'directory',
8
+ description: 'Shows the current working directory',
9
+ requires: [],
10
+ defaultConfig: {
11
+ style: 'white',
12
+ format: 'basename',
13
+ icon: false,
14
+ },
15
+ };
16
+
17
+ /**
18
+ * Fish-style path abbreviation: abbreviate all path components except the last
19
+ * to their first character, and replace $HOME with ~.
20
+ *
21
+ * Example: /Users/npow/code/myproject -> ~/c/m/myproject
22
+ *
23
+ * @param {string} dirPath - Full directory path
24
+ * @returns {string}
25
+ */
26
+ function fishFormat(dirPath) {
27
+ const home = homedir();
28
+ let p = dirPath;
29
+
30
+ // Replace home dir with ~
31
+ if (p === home) return '~';
32
+ if (p.startsWith(home + '/')) {
33
+ p = '~' + p.slice(home.length);
34
+ }
35
+
36
+ const parts = p.split('/');
37
+ if (parts.length <= 1) return p;
38
+
39
+ // Abbreviate all components except the last to their first character
40
+ const abbreviated = parts.map((part, i) => {
41
+ if (i === parts.length - 1) return part;
42
+ if (part === '~') return '~';
43
+ if (part === '') return '';
44
+ return part[0];
45
+ });
46
+
47
+ return abbreviated.join('/');
48
+ }
49
+
50
+ /**
51
+ * @param {object} data - Parsed stdin JSON from Claude Code
52
+ * @param {object} config - Per-segment config from theme
53
+ * @returns {{text: string, style: string}|null}
54
+ */
55
+ export function render(data, config) {
56
+ const cfg = { ...meta.defaultConfig, ...config };
57
+
58
+ const dirPath = data?.workspace?.current_dir;
59
+ if (!dirPath) return null;
60
+
61
+ let display;
62
+ if (cfg.format === 'full') {
63
+ display = dirPath;
64
+ } else if (cfg.format === 'fish') {
65
+ display = fishFormat(dirPath);
66
+ } else {
67
+ // basename
68
+ const parts = dirPath.split('/');
69
+ display = parts[parts.length - 1] || dirPath;
70
+ }
71
+
72
+ const text = cfg.icon ? `\uF115 ${display}` : display;
73
+
74
+ return { text, style: cfg.style };
75
+ }
@@ -0,0 +1,99 @@
1
+ // src/segments/emoji-story.js — Summarize the session as a growing sequence of emojis
2
+ // Zero dependencies. Node 18+ ESM.
3
+ //
4
+ // On each render, checks conditions in priority order and appends the FIRST
5
+ // matching emoji that isn't already the last element of the story array.
6
+ // The story grows over time, capped at maxLength (default 12).
7
+ // Module-level state: the story array persists across renders within a process.
8
+
9
+ /** @type {string[]} */
10
+ const story = [];
11
+
12
+ /**
13
+ * Condition table — checked in order, first match wins.
14
+ * Each entry: { test(data) -> boolean, emoji: string }
15
+ */
16
+ const CONDITIONS = [
17
+ {
18
+ test: (d) => (d?.cost?.total_lines_added ?? 0) > 0,
19
+ emoji: '\u{1F4DD}', // 📝
20
+ },
21
+ {
22
+ test: (d) => (d?.cost?.total_lines_added ?? 0) > 50,
23
+ emoji: '\u270F\uFE0F', // ✏️
24
+ },
25
+ {
26
+ test: (d) =>
27
+ (d?.cost?.total_lines_removed ?? 0) > (d?.cost?.total_lines_added ?? 0),
28
+ emoji: '\u{1F5D1}\uFE0F', // 🗑️
29
+ },
30
+ {
31
+ test: (d) => (d?.cost?.total_lines_added ?? 0) > 200,
32
+ emoji: '\u{1F3D7}\uFE0F', // 🏗️
33
+ },
34
+ {
35
+ test: (d) => (d?.cost?.total_cost_usd ?? 0) > 5,
36
+ emoji: '\u{1F4B0}', // 💰
37
+ },
38
+ {
39
+ test: (d) => (d?.context_window?.used_percentage ?? 0) >= 50,
40
+ emoji: '\u23F3', // ⏳
41
+ },
42
+ {
43
+ test: (d) => (d?.context_window?.used_percentage ?? 0) >= 80,
44
+ emoji: '\u{1F525}', // 🔥
45
+ },
46
+ {
47
+ test: (d) => (d?.context_window?.used_percentage ?? 0) >= 95,
48
+ emoji: '\u{1F480}', // 💀
49
+ },
50
+ {
51
+ test: (d) => (d?.cost?.total_duration_ms ?? 0) > 1_800_000,
52
+ emoji: '\u2615', // ☕
53
+ },
54
+ {
55
+ test: (d) => (d?.cost?.total_duration_ms ?? 0) > 3_600_000,
56
+ emoji: '\u{1F3C3}', // 🏃
57
+ },
58
+ ];
59
+
60
+ export const meta = {
61
+ name: 'emoji-story',
62
+ description: 'Summarize the session as a growing sequence of emojis',
63
+ requires: [],
64
+ defaultConfig: {
65
+ style: 'dim',
66
+ maxLength: 12,
67
+ },
68
+ };
69
+
70
+ /**
71
+ * @param {object} data - Parsed stdin JSON from Claude Code
72
+ * @param {object} config - Per-segment config from theme
73
+ * @returns {{text: string, style: string}|null}
74
+ */
75
+ export function render(data, config) {
76
+ const cfg = { ...meta.defaultConfig, ...config };
77
+ const maxLen = cfg.maxLength ?? 12;
78
+ const last = story.length > 0 ? story[story.length - 1] : null;
79
+
80
+ // Find the first matching condition whose emoji isn't already the last one
81
+ for (const cond of CONDITIONS) {
82
+ if (cond.test(data) && cond.emoji !== last) {
83
+ story.push(cond.emoji);
84
+ break;
85
+ }
86
+ }
87
+
88
+ // Cap length by shifting from front
89
+ while (story.length > maxLen) {
90
+ story.shift();
91
+ }
92
+
93
+ if (story.length === 0) return null;
94
+
95
+ const text = story.join('');
96
+ const style = cfg.style || 'dim';
97
+
98
+ return { text, style };
99
+ }
@@ -0,0 +1,25 @@
1
+ // src/segments/flex-space.js — Flex-space marker for right-alignment
2
+ // Zero dependencies. Node 18+ ESM.
3
+ //
4
+ // The compositor recognises the magic "__FLEX__" token and replaces it
5
+ // with the necessary padding to push subsequent segments to the right.
6
+ // For now, the segment simply emits the marker.
7
+
8
+ export const meta = {
9
+ name: 'flex-space',
10
+ description: 'Emits a marker the compositor uses for right-alignment',
11
+ requires: [],
12
+ defaultConfig: {
13
+ style: '',
14
+ },
15
+ };
16
+
17
+ /**
18
+ * @param {object} _data - Parsed stdin JSON from Claude Code (unused)
19
+ * @param {object} config - Per-segment config from theme
20
+ * @returns {{text: string, style: string}}
21
+ */
22
+ export function render(_data, config) {
23
+ const cfg = { ...meta.defaultConfig, ...config };
24
+ return { text: '__FLEX__', style: cfg.style };
25
+ }
@@ -0,0 +1,131 @@
1
+ // src/segments/fortune-cookie.js — Developer-themed fortune cookie wisdom
2
+ // Zero dependencies. Node 18+ ESM.
3
+ //
4
+ // Fortune selection:
5
+ // - Uses session_id as a seed when available (deterministic per session).
6
+ // - Falls back to a module-level random pick done once at import time.
7
+ // - Rotates to a new fortune every `rotateEvery` renders (default: 10).
8
+
9
+ export const meta = {
10
+ name: 'fortune-cookie',
11
+ description: 'Shows a rotating developer-themed fortune cookie message',
12
+ requires: [],
13
+ defaultConfig: {
14
+ style: 'dim italic',
15
+ showEmoji: true,
16
+ rotateEvery: 10,
17
+ },
18
+ };
19
+
20
+ const FORTUNES = [
21
+ 'The bug you seek is in the file you haven\'t read',
22
+ 'A commit a day keeps the rebase away',
23
+ 'There are only two hard things: cache invalidation, naming things, and off-by-one errors',
24
+ 'The best code is no code at all',
25
+ 'Legacy code is just code without tests',
26
+ '"It works on my machine" is not a deployment strategy',
27
+ 'First, solve the problem. Then, write the code.',
28
+ 'Code is read more often than it is written',
29
+ 'Weeks of coding can save you hours of planning',
30
+ 'There is no cloud, only someone else\'s computer',
31
+ 'A well-placed log statement is worth a thousand debugger sessions',
32
+ 'The fastest algorithm is the one you don\'t run',
33
+ 'Today\'s TODO is tomorrow\'s tech debt',
34
+ 'Delete code with confidence; version control remembers',
35
+ 'Ship it, then fix it — but actually fix it',
36
+ 'The tests you skip today are the bugs you debug tomorrow',
37
+ 'Every regex problem creates two problems',
38
+ 'Your future self is your most important code reviewer',
39
+ 'Production is the only staging environment that matters',
40
+ 'Simplicity is the ultimate sophistication, especially in code',
41
+ 'If it hurts, do it more often — that\'s what CI is for',
42
+ 'Premature optimization is the root of all evil, but so is premature abstraction',
43
+ 'git push --force and you shall receive... merge conflicts',
44
+ 'The best error message is the one that never shows up',
45
+ 'A function should do one thing, and do it well',
46
+ 'Comments lie; code doesn\'t. But types are even more honest.',
47
+ 'The hardest bugs to fix are the ones you can\'t reproduce',
48
+ 'You are not your code. Your code is not you. Ship it.',
49
+ 'Good code is its own best documentation',
50
+ 'Measure twice, deploy once',
51
+ 'There\'s no shame in reading the docs',
52
+ 'If you can\'t explain it simply, you don\'t understand it well enough to code it',
53
+ 'The best time to refactor was yesterday. The second best time is now.',
54
+ 'Any sufficiently advanced configuration is indistinguishable from code',
55
+ 'The code compiles; therefore, ship it',
56
+ 'A linter catches what pride misses',
57
+ 'Debugging is like being a detective in a crime movie where you are also the murderer',
58
+ 'Never trust user input. Never trust your own input either.',
59
+ 'Workarounds are just features with low self-esteem',
60
+ 'One does not simply deploy on Friday',
61
+ 'The database is always the bottleneck. Always.',
62
+ 'Naming variables well is an act of kindness to your future self',
63
+ 'console.log-driven development: timeless, reliable, shameless',
64
+ 'All abstractions are leaky. Budget for the drip.',
65
+ 'Your dependencies have dependencies. It\'s turtles all the way down.',
66
+ 'The PR that\'s "almost ready" is never almost ready',
67
+ 'One cannot simply grep their way out of a design problem',
68
+ 'The best meetings are pull request reviews',
69
+ 'Every line you don\'t write is a line you don\'t have to maintain',
70
+ 'The build is red. It is always red. We ship anyway.',
71
+ 'Sleep on it. The bug will still be there tomorrow, but you\'ll be smarter.',
72
+ 'Copying from Stack Overflow is research. Citing it is professionalism.',
73
+ 'The fastest code is the code that never runs',
74
+ 'Feature flags: Schrodinger\'s deployment',
75
+ 'There are two types of software: the kind people complain about, and the kind nobody uses',
76
+ 'If your tests pass on the first try, your tests are wrong',
77
+ 'A monolith is just a microservice that kept eating',
78
+ ];
79
+
80
+ const EMOJI = '\u{1F960}'; // fortune cookie emoji
81
+
82
+ // Module-level fallback seed (used when session_id is not available).
83
+ // Computed once at import time so it remains stable across renders.
84
+ const FALLBACK_SEED = Math.floor(Math.random() * FORTUNES.length);
85
+
86
+ // Module-level render counter for rotation tracking
87
+ let renderCount = 0;
88
+
89
+ /**
90
+ * Simple string-to-integer hash. Produces a non-negative integer from a
91
+ * string, used to derive a deterministic fortune index from session_id.
92
+ *
93
+ * @param {string} str
94
+ * @returns {number}
95
+ */
96
+ function hashString(str) {
97
+ let hash = 5381;
98
+ for (let i = 0; i < str.length; i++) {
99
+ // hash * 33 + charCode (djb2)
100
+ hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
101
+ }
102
+ return Math.abs(hash);
103
+ }
104
+
105
+ /**
106
+ * @param {object} data - Parsed stdin JSON from Claude Code
107
+ * @param {object} config - Per-segment config from theme
108
+ * @returns {{text: string, style: string}}
109
+ */
110
+ export function render(data, config) {
111
+ const cfg = { ...meta.defaultConfig, ...config };
112
+ const rotateEvery = cfg.rotateEvery > 0 ? cfg.rotateEvery : 10;
113
+
114
+ // Derive a base seed: session_id when available, otherwise the module-level fallback
115
+ const sessionId = data?.session_id;
116
+ const baseSeed = sessionId ? hashString(sessionId) : FALLBACK_SEED;
117
+
118
+ // Rotation offset: advances by 1 every `rotateEvery` renders
119
+ const rotation = Math.floor(renderCount / rotateEvery);
120
+
121
+ // Pick a fortune deterministically from (baseSeed + rotation) mod length
122
+ const index = (baseSeed + rotation) % FORTUNES.length;
123
+ const fortune = FORTUNES[index];
124
+
125
+ renderCount++;
126
+
127
+ const prefix = cfg.showEmoji ? `${EMOJI} ` : '';
128
+ const text = `${prefix}${fortune}`;
129
+
130
+ return { text, style: cfg.style };
131
+ }
@@ -0,0 +1,57 @@
1
+ // src/segments/garden.js — ASCII plants that grow as you code
2
+ // Zero dependencies. Node 18+ ESM.
3
+ //
4
+ // Growth stages (based on total_lines_added):
5
+ // 0 lines: (.) — seed dim
6
+ // 1-49: (,) — sprouting dim
7
+ // 50-99: (Y) — seedling green
8
+ // 100-199: (🌱) — small plant green
9
+ // 200-349: (🌿) — growing bold green
10
+ // 350-499: (🌻) — flowering bold yellow
11
+ // 500+: (🌳) — full tree bold green
12
+ //
13
+ // Override: context >= 95% → (🥀) wilted, red
14
+
15
+ const STAGES = [
16
+ { min: 500, text: '(🌳)', style: 'bold green' },
17
+ { min: 350, text: '(🌻)', style: 'bold yellow' },
18
+ { min: 200, text: '(🌿)', style: 'bold green' },
19
+ { min: 100, text: '(🌱)', style: 'green' },
20
+ { min: 50, text: '(Y)', style: 'green' },
21
+ { min: 1, text: '(,)', style: 'dim' },
22
+ { min: 0, text: '(.)', style: 'dim' },
23
+ ];
24
+
25
+ const WILTED = { text: '(🥀)', style: 'red' };
26
+
27
+ export const meta = {
28
+ name: 'garden',
29
+ description: 'ASCII plants that grow with each 50 lines of code added',
30
+ requires: [],
31
+ defaultConfig: {
32
+ style: '',
33
+ },
34
+ };
35
+
36
+ /**
37
+ * @param {object} data - Parsed stdin JSON from Claude Code
38
+ * @param {object} config - Per-segment config from theme
39
+ * @returns {{text: string, style: string}}
40
+ */
41
+ export function render(data, config) {
42
+ const cfg = { ...meta.defaultConfig, ...config };
43
+
44
+ const contextPct = data?.context_window?.used_percentage ?? 0;
45
+
46
+ // Context >= 95% overrides everything — plant is wilted
47
+ if (contextPct >= 95) {
48
+ return { text: WILTED.text, style: cfg.style || WILTED.style };
49
+ }
50
+
51
+ const linesAdded = data?.cost?.total_lines_added ?? 0;
52
+
53
+ // Find the first stage whose minimum is met (sorted highest-first)
54
+ const stage = STAGES.find((s) => linesAdded >= s.min) ?? STAGES[STAGES.length - 1];
55
+
56
+ return { text: stage.text, style: cfg.style || stage.style };
57
+ }
@@ -0,0 +1,36 @@
1
+ // src/segments/git-branch.js — Current git branch name
2
+ // Zero dependencies. Node 18+ ESM.
3
+
4
+ import { cachedExec } from '../cache.js';
5
+
6
+ export const meta = {
7
+ name: 'git-branch',
8
+ description: 'Shows the current git branch name',
9
+ requires: ['git'],
10
+ defaultConfig: {
11
+ style: 'green',
12
+ icon: false,
13
+ maxLength: 30,
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 branch = cachedExec('git-branch', 'git rev-parse --abbrev-ref HEAD');
26
+ if (!branch) return null;
27
+
28
+ let display = branch;
29
+ if (display.length > cfg.maxLength) {
30
+ display = display.slice(0, cfg.maxLength - 3) + '...';
31
+ }
32
+
33
+ const text = cfg.icon ? `\uE0A0 ${display}` : display;
34
+
35
+ return { text, style: cfg.style };
36
+ }
@@ -0,0 +1,56 @@
1
+ // src/segments/git-status.js — Git working tree status indicators
2
+ // Zero dependencies. Node 18+ ESM.
3
+
4
+ import { cachedExec } from '../cache.js';
5
+
6
+ export const meta = {
7
+ name: 'git-status',
8
+ description: 'Shows git status indicators: staged, modified, untracked counts',
9
+ requires: ['git'],
10
+ defaultConfig: {
11
+ style: 'yellow',
12
+ format: 'short',
13
+ },
14
+ };
15
+
16
+ /**
17
+ * Parse the trimmed output of `wc -l` to an integer.
18
+ * @param {string} output
19
+ * @returns {number}
20
+ */
21
+ function parseCount(output) {
22
+ const n = parseInt(output.trim(), 10);
23
+ return Number.isNaN(n) ? 0 : n;
24
+ }
25
+
26
+ /**
27
+ * @param {object} data - Parsed stdin JSON from Claude Code
28
+ * @param {object} config - Per-segment config from theme
29
+ * @returns {{text: string, style: string}|null}
30
+ */
31
+ export function render(data, config) {
32
+ const cfg = { ...meta.defaultConfig, ...config };
33
+
34
+ const staged = parseCount(cachedExec('git-staged', 'git diff --cached --numstat | wc -l'));
35
+ const modified = parseCount(cachedExec('git-modified', 'git diff --numstat | wc -l'));
36
+ const untracked = parseCount(cachedExec('git-untracked', 'git ls-files --others --exclude-standard | wc -l'));
37
+
38
+ if (staged === 0 && modified === 0 && untracked === 0) return null;
39
+
40
+ let text;
41
+ if (cfg.format === 'detailed') {
42
+ const parts = [];
43
+ if (staged > 0) parts.push(`staged:${staged}`);
44
+ if (modified > 0) parts.push(`mod:${modified}`);
45
+ if (untracked > 0) parts.push(`new:${untracked}`);
46
+ text = parts.join(' ');
47
+ } else {
48
+ const parts = [];
49
+ if (staged > 0) parts.push(`+${staged}`);
50
+ if (modified > 0) parts.push(`~${modified}`);
51
+ if (untracked > 0) parts.push(`?${untracked}`);
52
+ text = parts.join(' ');
53
+ }
54
+
55
+ return { text, style: cfg.style };
56
+ }
@@ -0,0 +1,134 @@
1
+ // src/segments/horoscope.js — Coding horoscope: daily prediction based on day-of-week and session metrics
2
+ // Zero dependencies. Node 18+ ESM.
3
+ //
4
+ // Sign selection: day of month mapped to zodiac sign (1-2=Aries, 3-4=Taurus, ..., 23-24=Pisces, 25+=cycles).
5
+ // Prediction selection: deterministic hash of (dayOfYear + signIndex) picks from 36 horoscope templates.
6
+ // Same sign and prediction all day for a given calendar date.
7
+
8
+ export const meta = {
9
+ name: 'horoscope',
10
+ description: 'Daily coding horoscope based on zodiac sign derived from the day of the month',
11
+ requires: [],
12
+ defaultConfig: {
13
+ style: 'dim italic',
14
+ showSign: true,
15
+ },
16
+ };
17
+
18
+ const ZODIAC = [
19
+ { name: 'Aries', symbol: '\u2648' },
20
+ { name: 'Taurus', symbol: '\u2649' },
21
+ { name: 'Gemini', symbol: '\u264A' },
22
+ { name: 'Cancer', symbol: '\u264B' },
23
+ { name: 'Leo', symbol: '\u264C' },
24
+ { name: 'Virgo', symbol: '\u264D' },
25
+ { name: 'Libra', symbol: '\u264E' },
26
+ { name: 'Scorpio', symbol: '\u264F' },
27
+ { name: 'Sagittarius', symbol: '\u2650' },
28
+ { name: 'Capricorn', symbol: '\u2651' },
29
+ { name: 'Aquarius', symbol: '\u2652' },
30
+ { name: 'Pisces', symbol: '\u2653' },
31
+ ];
32
+
33
+ const PREDICTIONS = [
34
+ 'Mercury is in retrograde. Avoid force-pushing.',
35
+ 'The stars align for a major refactor.',
36
+ 'Today favors writing tests over features.',
37
+ 'A mysterious bug will reveal itself before lunch.',
38
+ 'Your linter will betray you today.',
39
+ 'Commit early, commit often. The cosmos demands it.',
40
+ 'An unexpected dependency update brings chaos.',
41
+ 'The code review gods smile upon you.',
42
+ 'Beware of scope creep during the afternoon.',
43
+ 'A senior engineer will question your naming conventions.',
44
+ "Today's lucky number: 0-indexed.",
45
+ 'Your pull request will be approved on the first try.',
46
+ 'Retrograde warning: do not run migrations today.',
47
+ 'The tests you skip today will haunt you tomorrow.',
48
+ 'A merge conflict is written in your stars.',
49
+ 'Your TODO comments will outlive you.',
50
+ 'Pair programming brings unexpected breakthroughs.',
51
+ 'Avoid premature optimization. The stars are watching.',
52
+ 'A forgotten console.log will make it to production.',
53
+ 'Today is a good day to update your README.',
54
+ 'The deployment pipeline favors the bold.',
55
+ 'A type error lurks where you least expect it.',
56
+ 'Your git stash holds forgotten treasures.',
57
+ 'The standup will be mercifully short today.',
58
+ 'An off-by-one error approaches from the east.',
59
+ 'Trust your instincts. Revert that last commit.',
60
+ 'A rubber duck will solve your hardest problem.',
61
+ 'Null checks in your future. Many null checks.',
62
+ 'The intern will find a bug you missed.',
63
+ 'Today your regex will actually work on the first try.',
64
+ 'Beware of yak shaving disguised as productivity.',
65
+ 'The build will break, but not by your hand.',
66
+ 'A legacy system calls out for your attention.',
67
+ 'Your branch name will spark joy today.',
68
+ 'The documentation you write today saves future-you.',
69
+ 'Venus enters your CI/CD house. Deploys go smoothly.',
70
+ ];
71
+
72
+ /**
73
+ * Derive the zodiac sign index from the day of the month.
74
+ * Days 1-2 = 0 (Aries), 3-4 = 1 (Taurus), ..., 23-24 = 11 (Pisces), then cycles.
75
+ *
76
+ * @param {number} dayOfMonth - 1-31
77
+ * @returns {number} Index into ZODIAC (0-11)
78
+ */
79
+ function getSignIndex(dayOfMonth) {
80
+ return Math.floor((dayOfMonth - 1) / 2) % ZODIAC.length;
81
+ }
82
+
83
+ /**
84
+ * Compute the day of the year (1-366) for a given Date.
85
+ *
86
+ * @param {Date} date
87
+ * @returns {number}
88
+ */
89
+ function dayOfYear(date) {
90
+ const start = new Date(date.getFullYear(), 0, 0);
91
+ const diff = date - start;
92
+ const oneDay = 86_400_000;
93
+ return Math.floor(diff / oneDay);
94
+ }
95
+
96
+ /**
97
+ * Simple deterministic hash: combine dayOfYear and signIndex into an integer.
98
+ * Uses a basic multiplicative hash to spread values across the prediction list.
99
+ *
100
+ * @param {number} doy - Day of year (1-366)
101
+ * @param {number} signIdx - Sign index (0-11)
102
+ * @returns {number} Non-negative integer
103
+ */
104
+ function deterministicHash(doy, signIdx) {
105
+ // Multiplicative hash with golden-ratio-derived constant.
106
+ // Different enough from simple modulo to avoid obvious patterns.
107
+ let h = (doy * 2654435761 + signIdx * 40503) & 0x7fffffff;
108
+ h = ((h >>> 16) ^ h) & 0x7fffffff;
109
+ return h;
110
+ }
111
+
112
+ /**
113
+ * @param {object} data - Parsed stdin JSON from Claude Code
114
+ * @param {object} config - Per-segment config from theme
115
+ * @returns {{text: string, style: string}|null}
116
+ */
117
+ export function render(data, config) {
118
+ const cfg = { ...meta.defaultConfig, ...config };
119
+
120
+ const now = new Date();
121
+ const dom = now.getDate();
122
+ const doy = dayOfYear(now);
123
+
124
+ const signIdx = getSignIndex(dom);
125
+ const sign = ZODIAC[signIdx];
126
+
127
+ const hash = deterministicHash(doy, signIdx);
128
+ const prediction = PREDICTIONS[hash % PREDICTIONS.length];
129
+
130
+ const prefix = cfg.showSign ? `${sign.symbol} ` : '';
131
+ const text = `${prefix}${prediction}`;
132
+
133
+ return { text, style: cfg.style };
134
+ }
@@ -0,0 +1,65 @@
1
+ // Built-in segment registry
2
+ // Each segment is imported and registered by its meta.name.
3
+
4
+ import * as modelName from './model-name.js';
5
+ import * as contextBar from './context-bar.js';
6
+ import * as contextPercent from './context-percent.js';
7
+ import * as contextTokens from './context-tokens.js';
8
+ import * as sessionCost from './session-cost.js';
9
+ import * as costBudget from './cost-budget.js';
10
+ import * as gitBranch from './git-branch.js';
11
+ import * as gitStatus from './git-status.js';
12
+ import * as directory from './directory.js';
13
+ import * as sessionTimer from './session-timer.js';
14
+ import * as apiTimer from './api-timer.js';
15
+ import * as linesChanged from './lines-changed.js';
16
+ import * as vimMode from './vim-mode.js';
17
+ import * as version from './version.js';
18
+ import * as outputStyle from './output-style.js';
19
+ import * as separatorPipe from './separator-pipe.js';
20
+ import * as separatorArrow from './separator-arrow.js';
21
+ import * as separatorSpace from './separator-space.js';
22
+ import * as flexSpace from './flex-space.js';
23
+ import * as customText from './custom-text.js';
24
+ import * as tamagotchi from './tamagotchi.js';
25
+ import * as smartNudge from './smart-nudge.js';
26
+ import * as narrator from './narrator.js';
27
+ import * as streak from './streak.js';
28
+ import * as soundtrack from './soundtrack.js';
29
+ import * as vibeCheck from './vibe-check.js';
30
+ import * as achievement from './achievement.js';
31
+ import * as tokenSparkline from './token-sparkline.js';
32
+ import * as fortuneCookie from './fortune-cookie.js';
33
+ import * as horoscope from './horoscope.js';
34
+ import * as coworker from './coworker.js';
35
+ import * as commitMsg from './commit-msg.js';
36
+ import * as garden from './garden.js';
37
+ import * as coffeeCup from './coffee-cup.js';
38
+ import * as battleLog from './battle-log.js';
39
+ import * as cat from './cat.js';
40
+ import * as weatherReport from './weather-report.js';
41
+ import * as emojiStory from './emoji-story.js';
42
+ import * as speedrun from './speedrun.js';
43
+ import * as stockTicker from './stock-ticker.js';
44
+ import * as rpgStats from './rpg-stats.js';
45
+
46
+ const allSegments = [
47
+ modelName, contextBar, contextPercent, contextTokens,
48
+ sessionCost, costBudget, gitBranch, gitStatus,
49
+ directory, sessionTimer, apiTimer, linesChanged,
50
+ vimMode, version, outputStyle,
51
+ separatorPipe, separatorArrow, separatorSpace, flexSpace, customText,
52
+ tamagotchi, vibeCheck, achievement, tokenSparkline,
53
+ smartNudge, narrator, streak, soundtrack, fortuneCookie, horoscope,
54
+ coworker, commitMsg, garden, coffeeCup,
55
+ battleLog, cat, weatherReport,
56
+ emojiStory, speedrun,
57
+ stockTicker, rpgStats,
58
+ ];
59
+
60
+ export const segments = {};
61
+ for (const seg of allSegments) {
62
+ if (seg.meta?.name) {
63
+ segments[seg.meta.name] = seg;
64
+ }
65
+ }