claudeline 1.0.0 → 1.2.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.
@@ -0,0 +1,187 @@
1
+ ---
2
+ name: configure-statusline
3
+ description: Configure claudeline status line for Claude Code. Use when user wants to set up, customize, or change their status line display. Helps users choose themes, build custom formats, and install to global or project scope.
4
+ tools: Read, Bash, AskUserQuestion
5
+ ---
6
+
7
+ # Configure Claudeline Status Line
8
+
9
+ Help users configure their Claude Code status line using claudeline.
10
+
11
+ ## Workflow
12
+
13
+ ### Step 1: Understand User's Needs
14
+
15
+ Ask the user what they want:
16
+
17
+ ```
18
+ What would you like to do?
19
+ 1. **Quick setup** - Install a preset theme
20
+ 2. **Custom format** - Build a custom status line
21
+ 3. **See current** - Check what's currently configured
22
+ 4. **Uninstall** - Remove status line configuration
23
+ ```
24
+
25
+ ### Step 2: Determine Scope
26
+
27
+ Ask where to install:
28
+
29
+ ```
30
+ Where should the status line be configured?
31
+ 1. **Global** (~/.claude/settings.json) - Applies to all projects
32
+ 2. **Project** (.claude/settings.json) - Only this project
33
+ ```
34
+
35
+ ### Step 3A: Quick Setup (Theme Selection)
36
+
37
+ If user wants a theme, show available options:
38
+
39
+ | Theme | Description | Preview |
40
+ |-------|-------------|---------|
41
+ | `minimal` | Just model and directory | `Sonnet 4 myproject` |
42
+ | `default` | Model, directory, git info | `[Sonnet 4] 📁 myproject \| 🌿 main ✓` |
43
+ | `powerline` | Powerline style with colors | `Sonnet 4 myproject main ✓` |
44
+ | `full` | Everything including cost/context | `[Sonnet 4] ~/project → main ✓ \| [████░░] 45% \| $0.42` |
45
+ | `git` | Git-focused with detailed status | `[Sonnet 4] 📁 myproject \| 🌿 main ✓ ↑2` |
46
+ | `tokens` | Token/context focused | `Sonnet 4 \| 🟢 50k/200k \| +133` |
47
+ | `dashboard` | Full dashboard view | `[Sonnet 4] myproject \| main ✓ \| 45% \| $0.42 \| 14:32` |
48
+ | `compact` | Minimal with slashes | `Sonnet 4 / myproject / main` |
49
+ | `colorful` | Vibrant colors | Colored model, dir, branch, status |
50
+
51
+ Install with:
52
+ ```bash
53
+ npx claudeline --theme <name> --install [--project]
54
+ ```
55
+
56
+ ### Step 3B: Custom Format Builder
57
+
58
+ If user wants custom, guide them through building a format string.
59
+
60
+ #### Available Components
61
+
62
+ **Model/Session:**
63
+ - `claude:model` - Model name (Sonnet 4)
64
+ - `claude:model-letter` - First letter (S)
65
+
66
+ **Directory:**
67
+ - `fs:dir` - Current directory name
68
+ - `fs:home` - Path with ~ for home
69
+ - `fs:project` - Project name
70
+
71
+ **Git:**
72
+ - `git:branch` - Branch name
73
+ - `git:status` - ✓ or *
74
+ - `git:ahead-behind` - ↑2↓1
75
+ - `git:dirty` - Combined status
76
+
77
+ **Context:**
78
+ - `ctx:percent` - Usage % (45%)
79
+ - `ctx:bar` - Progress bar [████░░]
80
+ - `ctx:emoji` - 🟢🟡🟠🔴
81
+ - `ctx:tokens` - 50k/200k
82
+
83
+ **Cost:**
84
+ - `cost:total` - $0.42
85
+ - `cost:duration` - 5m
86
+ - `cost:lines` - +133
87
+
88
+ **Time:**
89
+ - `time:now` - 14:32
90
+ - `time:date` - Feb 1
91
+
92
+ **Separators:** `sep:pipe` (\|), `sep:arrow` (→), `sep:dot` (•), `sep:slash` (/)
93
+
94
+ **Emojis:** `emoji:folder` (📁), `emoji:branch` (🌿), `emoji:rocket` (🚀)
95
+
96
+ **Styling:** Prefix with `green:`, `bold:`, `cyan:`, etc.
97
+
98
+ **Grouping:** `[...]` adds brackets, `if:git(...)` for conditionals
99
+
100
+ #### Example Formats
101
+
102
+ ```bash
103
+ # Simple
104
+ "claude:model fs:dir git:branch"
105
+
106
+ # With separators
107
+ "claude:model sep:pipe fs:dir sep:arrow git:branch"
108
+
109
+ # With colors
110
+ "bold:cyan:claude:model fs:dir green:git:branch"
111
+
112
+ # With conditionals (only show git info in git repos)
113
+ "claude:model fs:dir if:git(sep:pipe git:branch git:status)"
114
+
115
+ # Full custom
116
+ "[bold:cyan:claude:model] emoji:folder fs:dir sep:arrow green:git:branch git:status sep:pipe ctx:percent"
117
+ ```
118
+
119
+ Install custom format:
120
+ ```bash
121
+ npx claudeline "<format>" --install [--project]
122
+ ```
123
+
124
+ ### Step 4: Verify Installation
125
+
126
+ After installing, show the user:
127
+
128
+ 1. What was written to settings.json
129
+ 2. Remind them to restart Claude Code
130
+
131
+ ```bash
132
+ # Check current configuration
133
+ cat ~/.claude/settings.json | grep -A5 statusLine
134
+
135
+ # Or for project
136
+ cat .claude/settings.json | grep -A5 statusLine
137
+ ```
138
+
139
+ ### Step 5: Test Before Installing (Optional)
140
+
141
+ User can test their format:
142
+
143
+ ```bash
144
+ # See sample data
145
+ npx claudeline --preview
146
+
147
+ # Test with sample data
148
+ echo '{"model":{"display_name":"Sonnet 4"},"workspace":{"current_dir":"/test/project"}}' | npx claudeline run "claude:model fs:dir"
149
+ ```
150
+
151
+ ## Commands Reference
152
+
153
+ ```bash
154
+ # Install theme globally
155
+ npx claudeline --theme minimal --install
156
+
157
+ # Install theme to project only
158
+ npx claudeline --theme powerline --install --project
159
+
160
+ # Install custom format
161
+ npx claudeline "claude:model fs:dir git:branch" --install
162
+
163
+ # List all themes
164
+ npx claudeline --themes
165
+
166
+ # List all components
167
+ npx claudeline --list
168
+
169
+ # Uninstall
170
+ npx claudeline --uninstall [--project]
171
+
172
+ # Preview sample data
173
+ npx claudeline --preview
174
+ ```
175
+
176
+ ## Troubleshooting
177
+
178
+ **Status line not showing?**
179
+ - Restart Claude Code after installing
180
+ - Check settings.json has the statusLine config
181
+
182
+ **Command not found?**
183
+ - Ensure Node.js/npm is installed
184
+ - Try `npx claudeline --help`
185
+
186
+ **Want to use bun instead of npx?**
187
+ - Use `--use-bunx` flag when installing
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "claudeline",
3
+ "description": "Customizable status line for Claude Code. Configure themes and custom formats for your terminal status display.",
4
+ "version": "1.0.0",
5
+ "author": {
6
+ "name": "Luca Silverentand"
7
+ }
8
+ }
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # claudeline
2
2
 
3
- Customizable status line for [Claude Code](https://claude.ai/code). Display model info, git status, context usage, costs, and more in your terminal.
3
+ Customizable status line for [Claude Code](https://claude.ai/code). Display model info, git status, context usage, costs, API usage limits, and more in your terminal.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
8
  # Install with a theme
9
- npx claudeline --theme minimal --install
9
+ npx claudeline --theme luca --install
10
10
 
11
11
  # Or with a custom format
12
12
  npx claudeline "claude:model fs:dir git:branch" --install
@@ -39,17 +39,21 @@ npx claudeline --uninstall
39
39
  |-------|--------|
40
40
  | `minimal` | `claude:model fs:dir` |
41
41
  | `default` | `[claude:model] emoji:folder fs:dir if:git(sep:pipe emoji:branch git:branch git:status)` |
42
- | `powerline` | `bold:cyan:claude:model sep:powerline fs:dir if:git(sep:powerline green:git:branch git:status)` |
42
+ | `powerline` | `bold:cyan:claude:model sep:powerline fs:dir if:git(...)` |
43
43
  | `full` | `[bold:cyan:claude:model] fs:home sep:arrow green:git:branch git:status sep:pipe ctx:bar ctx:percent sep:pipe cost:total` |
44
- | `git` | `[claude:model] emoji:folder fs:dir sep:pipe emoji:branch git:branch git:status git:ahead-behind if:dirty(sep:pipe git:staged git:modified git:untracked)` |
44
+ | `git` | `[claude:model] emoji:folder fs:dir sep:pipe emoji:branch git:branch git:status git:ahead-behind if:dirty(...)` |
45
45
  | `tokens` | `claude:model sep:pipe ctx:emoji ctx:tokens sep:pipe cost:lines` |
46
46
  | `dev` | `[fs:dir] git:branch sep:pipe env:node sep:pipe time:now` |
47
47
  | `dashboard` | `[bold:claude:model] fs:dir sep:pipe git:branch git:status sep:pipe ctx:percent sep:pipe cost:total sep:pipe time:now` |
48
48
  | `context-focus` | `claude:model sep:pipe ctx:bar sep:pipe ctx:tokens sep:pipe ctx:emoji` |
49
49
  | `cost` | `claude:model sep:pipe cost:total sep:pipe cost:duration sep:pipe cost:lines-both` |
50
50
  | `simple` | `claude:model fs:dir git:branch` |
51
+ | `verbose` | `[claude:model] [claude:version] sep:pipe fs:home sep:pipe git:branch git:status git:ahead-behind sep:pipe ctx:bar ctx:percent sep:pipe cost:total cost:duration sep:pipe time:now` |
51
52
  | `compact` | `claude:model sep:slash fs:dir sep:slash git:branch` |
52
53
  | `colorful` | `bold:magenta:claude:model sep:arrow cyan:fs:dir sep:arrow green:git:branch yellow:git:status sep:arrow blue:ctx:percent` |
54
+ | `luca` | Nerd font icons, colored repo:branch, dirty state, cost, 5h/weekly usage bars |
55
+
56
+ When you install with `--theme`, claudeline stores a `theme:name` reference so your status line automatically picks up theme updates when you upgrade.
53
57
 
54
58
  ## Format Syntax
55
59
 
@@ -87,6 +91,7 @@ Show components only when conditions are met:
87
91
  ```
88
92
  if:git(git:branch git:status) # only in git repos
89
93
  if:dirty(text:UNCOMMITTED) # only when working tree is dirty
94
+ if:subdir(fs:relative) # only when in a subdirectory (not git root)
90
95
  if:node(env:node) # only in Node.js projects
91
96
  if:python(env:python) # only in Python projects
92
97
  if:rust(emoji:rust) # only in Rust projects
@@ -120,7 +125,7 @@ Available separators:
120
125
  - `powerline` → ``
121
126
  - `powerline-left` → ``
122
127
  - `space` → ` `
123
- - `none` → (empty)
128
+ - `none` → (empty, useful for glueing components together)
124
129
 
125
130
  ## Components Reference
126
131
 
@@ -151,7 +156,7 @@ Available separators:
151
156
  | `ctx:emoji` | Status emoji (🟢🟡🟠🔴) |
152
157
  | `ctx:used-tokens` | Total used tokens |
153
158
 
154
- ### Cost/Usage
159
+ ### Cost
155
160
 
156
161
  | Component | Description |
157
162
  |-----------|-------------|
@@ -164,6 +169,26 @@ Available separators:
164
169
  | `cost:removed` | Lines removed |
165
170
  | `cost:lines-both` | Lines added and removed |
166
171
 
172
+ ### Usage/Limits
173
+
174
+ Fetches your 5-hour and 7-day API utilization from Claude's OAuth API. Data is cached for 5 minutes in `$TMPDIR/claudeline-usage-cache.json`, shared across all sessions system-wide. The OAuth token is read automatically from macOS Keychain.
175
+
176
+ | Component | Description | Example |
177
+ |-----------|-------------|---------|
178
+ | `usage:5h` | 5-hour utilization % | `12%` |
179
+ | `usage:week` | 7-day utilization % | `58%` |
180
+ | `usage:7d` | Alias for `usage:week` | `58%` |
181
+ | `usage:5h-reset` | Time until 5h reset | `3h 32m` |
182
+ | `usage:week-reset` | Time until weekly reset | `2d 5h` |
183
+ | `usage:7d-reset` | Alias for `usage:week-reset` | `2d 5h` |
184
+ | `usage:5h-bar` | 5h progress bar with H label | `H▰▰▱▱▱▱▱▱` |
185
+ | `usage:5h-bar:N` | 5h bar with width N | `H▰▱▱▱▱` |
186
+ | `usage:week-bar` | Weekly progress bar with W label | `W▰▰▰▰▰▱▱▱` |
187
+ | `usage:week-bar:N` | Weekly bar with width N | `W▰▰▰▱▱` |
188
+ | `usage:5h-emoji` | 5h status emoji (🟢🟡🟠🔴) | `🟢` |
189
+ | `usage:week-emoji` | Weekly status emoji | `🟡` |
190
+ | `usage:7d-emoji` | Alias for `usage:week-emoji` | `🟡` |
191
+
167
192
  ### File System
168
193
 
169
194
  | Component | Description |
@@ -191,7 +216,8 @@ Available separators:
191
216
  | `git:staged` | Staged files (●N) |
192
217
  | `git:modified` | Modified files (+N) |
193
218
  | `git:untracked` | Untracked files (?N) |
194
- | `git:dirty` | Combined dirty status |
219
+ | `git:dirty` | Combined dirty status with per-type colors (green staged, red untracked, yellow modified) |
220
+ | `git:repo-branch` | Condensed `repo:branch` format |
195
221
  | `git:commit` | Short commit hash |
196
222
  | `git:commit-long` | Full commit hash |
197
223
  | `git:tag` | Current tag |
@@ -249,9 +275,31 @@ Available separators:
249
275
  | `time:minute` | Minute |
250
276
  | `time:elapsed` | Session elapsed time |
251
277
 
278
+ ### Nerd Font Icons
279
+
280
+ Use `nerd:name` for Nerd Font glyphs. Requires a [Nerd Font](https://www.nerdfonts.com/) in your terminal.
281
+
282
+ **Files:** `folder`, `folder-open`, `file`, `file-code`
283
+
284
+ **Git:** `branch`, `repo`, `commit`, `merge`, `tag`, `stash`, `pr`, `diff`, `compare`
285
+
286
+ **Status:** `check`, `x`, `warn`, `error`, `info`, `question`, `bell`, `pin`
287
+
288
+ **Decorative:** `star`, `fire`, `rocket`, `sparkle`, `lightning`, `heart`, `diamond`, `circle`, `square`, `triangle`
289
+
290
+ **Tech:** `node`, `python`, `rust`, `go`, `ruby`, `java`, `docker`, `terminal`, `code`, `database`, `cloud`, `server`, `package`, `gear`, `lock`, `unlock`, `key`, `shield`
291
+
292
+ **Arrows:** `up`, `down`, `left`, `right`, `arrow-right`, `arrow-left`
293
+
294
+ **Time:** `clock`, `calendar`, `history`
295
+
296
+ **OS:** `apple`, `linux`, `windows`
297
+
298
+ **Other:** `search`, `eye`, `bug`, `wrench`, `plug`, `wifi`, `bluetooth`, `cpu`, `memory`, `home`, `user`
299
+
252
300
  ### Emojis
253
301
 
254
- Use `emoji:name` for any of these:
302
+ Use `emoji:name` for Unicode emojis (no Nerd Font required):
255
303
 
256
304
  **Files:** `folder` 📁, `file` 📄, `home` 🏠
257
305
 
@@ -319,7 +367,7 @@ When you run `--install`, claudeline updates your `~/.claude/settings.json`:
319
367
  {
320
368
  "statusLine": {
321
369
  "type": "command",
322
- "command": "npx claudeline run 'claude:model fs:dir'",
370
+ "command": "npx claudeline run theme:luca",
323
371
  "padding": 0
324
372
  }
325
373
  }
@@ -337,6 +385,9 @@ npx claudeline --preview
337
385
 
338
386
  # Test with sample data
339
387
  echo '{"model":{"display_name":"Sonnet 4"}}' | npx claudeline run "claude:model fs:dir"
388
+
389
+ # Test a theme
390
+ echo '{"model":{"display_name":"Opus"}}' | npx claudeline run theme:dashboard
340
391
  ```
341
392
 
342
393
  ## Package Managers
package/dist/index.js CHANGED
@@ -148,6 +148,9 @@ function parseComponent(token) {
148
148
  if (parts[0] === "emoji") {
149
149
  return { type: "emoji", key: parts[1] || "", styles };
150
150
  }
151
+ if (parts[0] === "nerd") {
152
+ return { type: "nerd", key: parts[1] || "", styles };
153
+ }
151
154
  if (parts[0] === "sep") {
152
155
  return { type: "sep", key: parts[1] || "pipe", styles };
153
156
  }
@@ -200,6 +203,7 @@ function listComponents() {
200
203
  "tag",
201
204
  "remote",
202
205
  "repo",
206
+ "repo-branch",
203
207
  "user",
204
208
  "email",
205
209
  "remote-url"
@@ -255,6 +259,24 @@ function listComponents() {
255
259
  "elapsed"
256
260
  ]
257
261
  },
262
+ {
263
+ name: "Usage/Limits",
264
+ prefix: "usage",
265
+ items: [
266
+ "5h",
267
+ "week",
268
+ "7d",
269
+ "5h-reset",
270
+ "week-reset",
271
+ "7d-reset",
272
+ "5h-bar",
273
+ "5h-bar:N",
274
+ "week-bar",
275
+ "week-bar:N",
276
+ "5h-emoji",
277
+ "week-emoji"
278
+ ]
279
+ },
258
280
  {
259
281
  name: "Separators",
260
282
  prefix: "sep",
@@ -327,6 +349,65 @@ function listComponents() {
327
349
  "money"
328
350
  ]
329
351
  },
352
+ {
353
+ name: "Nerd Font Icons",
354
+ prefix: "nerd",
355
+ items: [
356
+ "folder",
357
+ "folder-open",
358
+ "file",
359
+ "file-code",
360
+ "branch",
361
+ "repo",
362
+ "commit",
363
+ "merge",
364
+ "tag",
365
+ "stash",
366
+ "pr",
367
+ "diff",
368
+ "check",
369
+ "x",
370
+ "warn",
371
+ "error",
372
+ "info",
373
+ "star",
374
+ "fire",
375
+ "rocket",
376
+ "sparkle",
377
+ "lightning",
378
+ "heart",
379
+ "node",
380
+ "python",
381
+ "rust",
382
+ "go",
383
+ "ruby",
384
+ "java",
385
+ "docker",
386
+ "terminal",
387
+ "code",
388
+ "database",
389
+ "cloud",
390
+ "server",
391
+ "package",
392
+ "gear",
393
+ "lock",
394
+ "key",
395
+ "shield",
396
+ "clock",
397
+ "calendar",
398
+ "apple",
399
+ "linux",
400
+ "windows",
401
+ "search",
402
+ "eye",
403
+ "bug",
404
+ "wrench",
405
+ "cpu",
406
+ "memory",
407
+ "home",
408
+ "user"
409
+ ]
410
+ },
330
411
  {
331
412
  name: "Text",
332
413
  prefix: "text",
@@ -391,7 +472,7 @@ var THEMES = {
391
472
  nerd: "emoji:node env:node-short sep:dot emoji:folder fs:dir sep:dot emoji:branch git:branch git:status",
392
473
  compact: "claude:model sep:slash fs:dir sep:slash git:branch",
393
474
  colorful: "bold:magenta:claude:model sep:arrow cyan:fs:dir sep:arrow green:git:branch yellow:git:status sep:arrow blue:ctx:percent",
394
- luca: "claude:model-letter git:repo git:branch git:dirty cost:total"
475
+ luca: "bold:magenta:claude:model-letter sep:pipe cyan:nerd:repo cyan:git:repo sep:none dim:text:: sep:none green:git:branch if:subdir(sep:none white:text:/ sep:none white:fs:relative) if:dirty(sep:pipe git:dirty) sep:pipe white:cost:total sep:newline bold:white:usage:5h-bar:8 usage:5h dim:usage:5h-reset sep:pipe bold:white:usage:week-bar:8 usage:week dim:usage:week-reset"
395
476
  };
396
477
  function getTheme(name) {
397
478
  return THEMES[name] || null;
@@ -543,10 +624,10 @@ function getSampleDataJson() {
543
624
  }
544
625
 
545
626
  // src/runtime.ts
546
- import * as path2 from "path";
547
- import * as os2 from "os";
548
- import * as fs2 from "fs";
549
- import { execSync } from "child_process";
627
+ import * as path3 from "path";
628
+ import * as os3 from "os";
629
+ import * as fs3 from "fs";
630
+ import { execSync as execSync2 } from "child_process";
550
631
 
551
632
  // src/separators.ts
552
633
  var SEPARATORS = {
@@ -569,6 +650,9 @@ var SEPARATORS = {
569
650
  bullet: " \u25E6 ",
570
651
  diamond: " \u25C7 ",
571
652
  star: " \u2605 ",
653
+ // Line break
654
+ newline: "\n",
655
+ br: "\n",
572
656
  // Powerline-style
573
657
  powerline: "",
574
658
  "powerline-left": "",
@@ -640,6 +724,225 @@ function getEmoji(name) {
640
724
  return EMOJIS[name] || "";
641
725
  }
642
726
 
727
+ // src/usage.ts
728
+ import * as fs2 from "fs";
729
+ import * as path2 from "path";
730
+ import * as os2 from "os";
731
+ import { execSync } from "child_process";
732
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
733
+ var CACHE_FILE = path2.join(os2.tmpdir(), "claudeline-usage-cache.json");
734
+ function readCache() {
735
+ try {
736
+ const raw = fs2.readFileSync(CACHE_FILE, "utf8");
737
+ const cached = JSON.parse(raw);
738
+ if (Date.now() - cached.fetched_at < CACHE_TTL_MS) {
739
+ return cached;
740
+ }
741
+ } catch {
742
+ }
743
+ return null;
744
+ }
745
+ function writeCache(data) {
746
+ try {
747
+ fs2.writeFileSync(CACHE_FILE, JSON.stringify({ data, fetched_at: Date.now() }));
748
+ } catch {
749
+ }
750
+ }
751
+ function getOAuthToken() {
752
+ try {
753
+ const raw = execSync(
754
+ 'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
755
+ { encoding: "utf8" }
756
+ ).trim();
757
+ const parsed = JSON.parse(raw);
758
+ if (typeof parsed === "string") return parsed;
759
+ if (parsed.claudeAiOauth?.accessToken) return parsed.claudeAiOauth.accessToken;
760
+ if (parsed.accessToken) return parsed.accessToken;
761
+ if (parsed.access_token) return parsed.access_token;
762
+ return null;
763
+ } catch {
764
+ return null;
765
+ }
766
+ }
767
+ function fetchUsage() {
768
+ const token = getOAuthToken();
769
+ if (!token) return null;
770
+ try {
771
+ const result = execSync(
772
+ `curl -s --max-time 5 -H "Authorization: Bearer ${token}" -H "anthropic-beta: oauth-2025-04-20" "https://api.anthropic.com/api/oauth/usage"`,
773
+ { encoding: "utf8" }
774
+ ).trim();
775
+ const data = JSON.parse(result);
776
+ if (data.five_hour && data.seven_day) {
777
+ return data;
778
+ }
779
+ return null;
780
+ } catch {
781
+ return null;
782
+ }
783
+ }
784
+ function getUsageData() {
785
+ const cached = readCache();
786
+ if (cached) return cached.data;
787
+ const data = fetchUsage();
788
+ if (data) {
789
+ writeCache(data);
790
+ }
791
+ return data;
792
+ }
793
+ function formatTimeUntil(isoDate) {
794
+ const reset = new Date(isoDate).getTime();
795
+ const now = Date.now();
796
+ let diff = Math.max(0, reset - now);
797
+ if (diff === 0) return "now";
798
+ const days = Math.floor(diff / 864e5);
799
+ diff %= 864e5;
800
+ const hours = Math.floor(diff / 36e5);
801
+ diff %= 36e5;
802
+ const minutes = Math.floor(diff / 6e4);
803
+ if (days > 0) return `${days}d ${hours}h`;
804
+ if (hours > 0) return `${hours}h ${minutes}m`;
805
+ return `${minutes}m`;
806
+ }
807
+ function makeBar(pct, width, label) {
808
+ const filled = Math.round(pct / 100 * width);
809
+ const bar = "\u25B0".repeat(filled) + "\u25B1".repeat(width - filled);
810
+ return label ? label + bar : bar;
811
+ }
812
+ function evaluateUsageComponent(key, args) {
813
+ const data = getUsageData();
814
+ if (!data) return "";
815
+ switch (key) {
816
+ case "5h":
817
+ return Math.round(data.five_hour.utilization) + "%";
818
+ case "week":
819
+ case "7d":
820
+ return Math.round(data.seven_day.utilization) + "%";
821
+ case "5h-reset":
822
+ return formatTimeUntil(data.five_hour.resets_at);
823
+ case "week-reset":
824
+ case "7d-reset":
825
+ return formatTimeUntil(data.seven_day.resets_at);
826
+ case "5h-bar": {
827
+ const width = args ? parseInt(args, 10) || 10 : 10;
828
+ return makeBar(data.five_hour.utilization, width, "H");
829
+ }
830
+ case "week-bar":
831
+ case "7d-bar": {
832
+ const width = args ? parseInt(args, 10) || 10 : 10;
833
+ return makeBar(data.seven_day.utilization, width, "W");
834
+ }
835
+ case "5h-emoji": {
836
+ const pct = data.five_hour.utilization;
837
+ if (pct < 50) return "\u{1F7E2}";
838
+ if (pct < 75) return "\u{1F7E1}";
839
+ if (pct < 90) return "\u{1F7E0}";
840
+ return "\u{1F534}";
841
+ }
842
+ case "week-emoji":
843
+ case "7d-emoji": {
844
+ const pct = data.seven_day.utilization;
845
+ if (pct < 50) return "\u{1F7E2}";
846
+ if (pct < 75) return "\u{1F7E1}";
847
+ if (pct < 90) return "\u{1F7E0}";
848
+ return "\u{1F534}";
849
+ }
850
+ default:
851
+ return "";
852
+ }
853
+ }
854
+
855
+ // src/nerdfonts.ts
856
+ var NERD_ICONS = {
857
+ // Files & Folders
858
+ "folder": "\uF07B",
859
+ "folder-open": "\uF07C",
860
+ "file": "\uF15B",
861
+ "file-code": "\uF1C9",
862
+ // Git
863
+ "branch": "\uE725",
864
+ "repo": "\uF401",
865
+ "commit": "\uF417",
866
+ "merge": "\uF419",
867
+ "tag": "\uF412",
868
+ "stash": "\uF01C",
869
+ "pr": "\uF407",
870
+ "diff": "\uF440",
871
+ "compare": "\uF47D",
872
+ // Status
873
+ "check": "\uF00C",
874
+ "x": "\uF00D",
875
+ "warn": "\uF071",
876
+ "error": "\uF057",
877
+ "info": "\uF05A",
878
+ "question": "\uF059",
879
+ "bell": "\uF0F3",
880
+ "pin": "\uF08D",
881
+ // Decorative
882
+ "star": "\uF005",
883
+ "fire": "\uF490",
884
+ "rocket": "\uF135",
885
+ "sparkle": "\uF005",
886
+ "lightning": "\uF0E7",
887
+ "heart": "\uF004",
888
+ "diamond": "\uF219",
889
+ "circle": "\uF111",
890
+ "square": "\uF0C8",
891
+ "triangle": "\uF0D8",
892
+ // Tech
893
+ "node": "\uE718",
894
+ "python": "\uE73C",
895
+ "rust": "\uE7A8",
896
+ "go": "\uE626",
897
+ "ruby": "\uE739",
898
+ "java": "\uE738",
899
+ "docker": "\uE7B0",
900
+ "terminal": "\uF120",
901
+ "code": "\uF121",
902
+ "database": "\uF1C0",
903
+ "cloud": "\uF0C2",
904
+ "server": "\uF233",
905
+ "package": "\uF487",
906
+ "gear": "\uF013",
907
+ "lock": "\uF023",
908
+ "unlock": "\uF09C",
909
+ "key": "\uF084",
910
+ "shield": "\uF132",
911
+ // Arrows
912
+ "up": "\uF062",
913
+ "down": "\uF063",
914
+ "left": "\uF060",
915
+ "right": "\uF061",
916
+ "arrow-right": "\uF061",
917
+ "arrow-left": "\uF060",
918
+ // Time
919
+ "clock": "\uF017",
920
+ "calendar": "\uF073",
921
+ "history": "\uF1DA",
922
+ // Money
923
+ "money": "\uF0D6",
924
+ "dollar": "\uF155",
925
+ // OS
926
+ "apple": "\uF179",
927
+ "linux": "\uF17C",
928
+ "windows": "\uF17A",
929
+ // Misc
930
+ "search": "\uF002",
931
+ "eye": "\uF06E",
932
+ "bug": "\uF188",
933
+ "wrench": "\uF0AD",
934
+ "plug": "\uF1E6",
935
+ "wifi": "\uF1EB",
936
+ "bluetooth": "\uF293",
937
+ "cpu": "\uF2DB",
938
+ "memory": "\uF538",
939
+ "home": "\uF015",
940
+ "user": "\uF007"
941
+ };
942
+ function getNerdIcon(name) {
943
+ return NERD_ICONS[name] || "";
944
+ }
945
+
643
946
  // src/runtime.ts
644
947
  function formatTokens(n) {
645
948
  if (n >= 1e6) return Math.round(n / 1e6) + "M";
@@ -654,7 +957,7 @@ function formatDuration(ms) {
654
957
  }
655
958
  function execCommand(cmd) {
656
959
  try {
657
- return execSync(cmd, { encoding: "utf8" }).trim();
960
+ return execSync2(cmd, { encoding: "utf8" }).trim();
658
961
  } catch {
659
962
  return "";
660
963
  }
@@ -690,13 +993,13 @@ function evaluateFsComponent(key, data) {
690
993
  case "path":
691
994
  return data.workspace?.current_dir || data.cwd || "";
692
995
  case "dir":
693
- return path2.basename(data.workspace?.current_dir || data.cwd || "");
996
+ return path3.basename(data.workspace?.current_dir || data.cwd || "");
694
997
  case "project":
695
- return path2.basename(data.workspace?.project_dir || "");
998
+ return path3.basename(data.workspace?.project_dir || "");
696
999
  case "project-path":
697
1000
  return data.workspace?.project_dir || "";
698
1001
  case "home":
699
- return (data.workspace?.current_dir || data.cwd || "").replace(os2.homedir(), "~");
1002
+ return (data.workspace?.current_dir || data.cwd || "").replace(os3.homedir(), "~");
700
1003
  case "cwd":
701
1004
  return data.cwd || "";
702
1005
  case "relative": {
@@ -705,33 +1008,33 @@ function evaluateFsComponent(key, data) {
705
1008
  if (proj && curr.startsWith(proj)) {
706
1009
  return curr.slice(proj.length + 1) || ".";
707
1010
  }
708
- return path2.basename(curr);
1011
+ return path3.basename(curr);
709
1012
  }
710
1013
  default:
711
1014
  return "";
712
1015
  }
713
1016
  }
714
- function evaluateGitComponent(key) {
1017
+ function evaluateGitComponent(key, noColor = false) {
715
1018
  switch (key) {
716
1019
  case "branch":
717
1020
  return execCommand("git branch --show-current 2>/dev/null");
718
1021
  case "status":
719
1022
  try {
720
- execSync("git diff --quiet 2>/dev/null");
1023
+ execSync2("git diff --quiet 2>/dev/null");
721
1024
  return "\u2713";
722
1025
  } catch {
723
1026
  return "*";
724
1027
  }
725
1028
  case "status-emoji":
726
1029
  try {
727
- execSync("git diff --quiet 2>/dev/null");
1030
+ execSync2("git diff --quiet 2>/dev/null");
728
1031
  return "\u2728";
729
1032
  } catch {
730
1033
  return "\u{1F4DD}";
731
1034
  }
732
1035
  case "status-word":
733
1036
  try {
734
- execSync("git diff --quiet 2>/dev/null");
1037
+ execSync2("git diff --quiet 2>/dev/null");
735
1038
  return "clean";
736
1039
  } catch {
737
1040
  return "dirty";
@@ -774,9 +1077,16 @@ function evaluateGitComponent(key) {
774
1077
  const untracked = parseInt(execCommand("git ls-files --others --exclude-standard 2>/dev/null | wc -l")) || 0;
775
1078
  if (staged === 0 && modified === 0 && untracked === 0) return "";
776
1079
  const parts = [];
777
- if (staged > 0) parts.push("+" + staged);
778
- if (untracked > 0) parts.push("-" + untracked);
779
- if (modified > 0) parts.push("~" + modified);
1080
+ if (noColor) {
1081
+ if (staged > 0) parts.push("+" + staged);
1082
+ if (untracked > 0) parts.push("-" + untracked);
1083
+ if (modified > 0) parts.push("~" + modified);
1084
+ } else {
1085
+ const r = `\x1B[${RESET}m`;
1086
+ if (staged > 0) parts.push(`\x1B[${COLORS.green}m+${staged}${r}`);
1087
+ if (untracked > 0) parts.push(`\x1B[${COLORS.red}m-${untracked}${r}`);
1088
+ if (modified > 0) parts.push(`\x1B[${COLORS.yellow}m~${modified}${r}`);
1089
+ }
780
1090
  return parts.join(" ");
781
1091
  }
782
1092
  case "commit":
@@ -788,7 +1098,12 @@ function evaluateGitComponent(key) {
788
1098
  case "remote":
789
1099
  return execCommand("git remote 2>/dev/null").split("\n")[0] || "";
790
1100
  case "repo":
791
- return path2.basename(execCommand("git rev-parse --show-toplevel 2>/dev/null"));
1101
+ return path3.basename(execCommand("git rev-parse --show-toplevel 2>/dev/null"));
1102
+ case "repo-branch": {
1103
+ const r = path3.basename(execCommand("git rev-parse --show-toplevel 2>/dev/null"));
1104
+ const b = execCommand("git branch --show-current 2>/dev/null");
1105
+ return r && b ? `${r}:${b}` : r || b;
1106
+ }
792
1107
  case "user":
793
1108
  return execCommand("git config user.name 2>/dev/null");
794
1109
  case "email":
@@ -901,13 +1216,13 @@ function evaluateEnvComponent(key) {
901
1216
  return match?.[1] || "";
902
1217
  }
903
1218
  case "user":
904
- return os2.userInfo().username;
1219
+ return os3.userInfo().username;
905
1220
  case "hostname":
906
- return os2.hostname();
1221
+ return os3.hostname();
907
1222
  case "hostname-short":
908
- return os2.hostname().split(".")[0];
1223
+ return os3.hostname().split(".")[0];
909
1224
  case "shell":
910
- return path2.basename(process.env.SHELL || "");
1225
+ return path3.basename(process.env.SHELL || "");
911
1226
  case "term":
912
1227
  return process.env.TERM || "";
913
1228
  case "os":
@@ -915,11 +1230,11 @@ function evaluateEnvComponent(key) {
915
1230
  case "arch":
916
1231
  return process.arch;
917
1232
  case "os-release":
918
- return os2.release();
1233
+ return os3.release();
919
1234
  case "cpus":
920
- return os2.cpus().length.toString();
1235
+ return os3.cpus().length.toString();
921
1236
  case "memory":
922
- return Math.round(os2.totalmem() / 1024 / 1024 / 1024) + "GB";
1237
+ return Math.round(os3.totalmem() / 1024 / 1024 / 1024) + "GB";
923
1238
  default:
924
1239
  return "";
925
1240
  }
@@ -971,33 +1286,37 @@ function evaluateCondition(condition) {
971
1286
  switch (condition) {
972
1287
  case "git":
973
1288
  try {
974
- execSync("git rev-parse --git-dir 2>/dev/null");
1289
+ execSync2("git rev-parse --git-dir 2>/dev/null");
975
1290
  return true;
976
1291
  } catch {
977
1292
  return false;
978
1293
  }
979
1294
  case "dirty":
980
1295
  try {
981
- execSync("git diff --quiet 2>/dev/null");
1296
+ execSync2("git diff --quiet 2>/dev/null");
982
1297
  return false;
983
1298
  } catch {
984
1299
  return true;
985
1300
  }
986
1301
  case "clean":
987
1302
  try {
988
- execSync("git diff --quiet 2>/dev/null");
1303
+ execSync2("git diff --quiet 2>/dev/null");
989
1304
  return true;
990
1305
  } catch {
991
1306
  return false;
992
1307
  }
1308
+ case "subdir": {
1309
+ const root = execCommand("git rev-parse --show-toplevel 2>/dev/null");
1310
+ return root !== "" && process.cwd() !== root;
1311
+ }
993
1312
  case "node":
994
- return fs2.existsSync("package.json");
1313
+ return fs3.existsSync("package.json");
995
1314
  case "python":
996
- return fs2.existsSync("pyproject.toml") || fs2.existsSync("setup.py") || fs2.existsSync("requirements.txt");
1315
+ return fs3.existsSync("pyproject.toml") || fs3.existsSync("setup.py") || fs3.existsSync("requirements.txt");
997
1316
  case "rust":
998
- return fs2.existsSync("Cargo.toml");
1317
+ return fs3.existsSync("Cargo.toml");
999
1318
  case "go":
1000
- return fs2.existsSync("go.mod");
1319
+ return fs3.existsSync("go.mod");
1001
1320
  default:
1002
1321
  return true;
1003
1322
  }
@@ -1012,7 +1331,7 @@ function evaluateComponent(comp, data, options) {
1012
1331
  result = evaluateFsComponent(comp.key, data);
1013
1332
  break;
1014
1333
  case "git":
1015
- result = evaluateGitComponent(comp.key);
1334
+ result = evaluateGitComponent(comp.key, options.noColor);
1016
1335
  break;
1017
1336
  case "ctx":
1018
1337
  result = evaluateContextComponent(comp.key, data, comp.args);
@@ -1023,6 +1342,9 @@ function evaluateComponent(comp, data, options) {
1023
1342
  case "env":
1024
1343
  result = evaluateEnvComponent(comp.key);
1025
1344
  break;
1345
+ case "usage":
1346
+ result = evaluateUsageComponent(comp.key, comp.args);
1347
+ break;
1026
1348
  case "time":
1027
1349
  result = evaluateTimeComponent(comp.key, data);
1028
1350
  break;
@@ -1032,6 +1354,9 @@ function evaluateComponent(comp, data, options) {
1032
1354
  case "emoji":
1033
1355
  result = options.noEmoji ? "" : getEmoji(comp.key);
1034
1356
  break;
1357
+ case "nerd":
1358
+ result = options.noEmoji ? "" : getNerdIcon(comp.key);
1359
+ break;
1035
1360
  case "text":
1036
1361
  result = comp.key;
1037
1362
  break;
@@ -1064,7 +1389,7 @@ function evaluateComponents(components, data, options) {
1064
1389
  const comp = components[i];
1065
1390
  const prev = components[i - 1];
1066
1391
  const value = evaluateComponent(comp, data, options);
1067
- if (i > 0 && comp.type !== "sep" && prev?.type !== "sep" && value) {
1392
+ if (i > 0 && comp.type !== "sep" && comp.type !== "conditional" && prev?.type !== "sep" && value) {
1068
1393
  parts.push(" ");
1069
1394
  }
1070
1395
  if (value) {
@@ -1083,7 +1408,7 @@ function evaluateFormat(format, data, options = {}) {
1083
1408
  }
1084
1409
 
1085
1410
  // src/index.ts
1086
- var VERSION = "1.0.0";
1411
+ var VERSION = "1.1.0";
1087
1412
  async function readStdin() {
1088
1413
  return new Promise((resolve, reject) => {
1089
1414
  let input = "";
@@ -1099,9 +1424,19 @@ async function readStdin() {
1099
1424
  }
1100
1425
  program.command("run <format>").description("Evaluate a format string and output the status line").option("--disable-emoji", "Disable emoji output").option("--disable-color", "Disable color output").action(async (format, options) => {
1101
1426
  try {
1427
+ let formatStr = format;
1428
+ if (format.startsWith("theme:")) {
1429
+ const themeName = format.slice(6);
1430
+ const theme = getTheme(themeName);
1431
+ if (!theme) {
1432
+ console.error(`Unknown theme: ${themeName}`);
1433
+ process.exit(1);
1434
+ }
1435
+ formatStr = theme;
1436
+ }
1102
1437
  const input = await readStdin();
1103
1438
  const data = JSON.parse(input);
1104
- const output = evaluateFormat(format, data, {
1439
+ const output = evaluateFormat(formatStr, data, {
1105
1440
  noEmoji: options.disableEmoji ?? false,
1106
1441
  noColor: options.disableColor ?? false
1107
1442
  });
@@ -1160,6 +1495,23 @@ Multiple style prefixes can be chained: bold:green:git:branch
1160
1495
  console.error(`Available themes: ${Object.keys(THEMES).join(", ")}`);
1161
1496
  process.exit(1);
1162
1497
  }
1498
+ if (options.install) {
1499
+ const result = install(`theme:${options.theme}`, options.project, {
1500
+ useBunx: options.useBunx,
1501
+ useNpx: options.useNpx,
1502
+ globalInstall: options.globalInstall,
1503
+ noEmoji: !options.emoji,
1504
+ noColor: !options.color
1505
+ });
1506
+ if (result.success) {
1507
+ console.log("\u2713 " + result.message);
1508
+ console.log("\nRestart Claude Code to see your new status line!");
1509
+ } else {
1510
+ console.error("\u2717 " + result.message);
1511
+ process.exit(1);
1512
+ }
1513
+ return;
1514
+ }
1163
1515
  formatStr = theme;
1164
1516
  }
1165
1517
  if (!formatStr) {
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "claudeline",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Customizable status line generator for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "claudeline": "./dist/index.js"
8
8
  },
9
9
  "files": [
10
- "dist"
10
+ "dist",
11
+ ".claude",
12
+ ".claude-plugin"
11
13
  ],
12
14
  "scripts": {
13
15
  "build": "tsup src/index.ts --format esm --dts --clean",
@@ -19,9 +21,10 @@
19
21
  "claude-code",
20
22
  "statusline",
21
23
  "cli",
22
- "terminal"
24
+ "terminal",
25
+ "claude-plugin"
23
26
  ],
24
- "author": "",
27
+ "author": "Luca Silverentand",
25
28
  "license": "MIT",
26
29
  "devDependencies": {
27
30
  "@types/node": "^20.11.0",