cc-pulse 1.3.2 → 1.5.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 (3) hide show
  1. package/README.md +144 -63
  2. package/dist/cli.js +213 -103
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,9 +5,15 @@
5
5
 
6
6
  A real-time statusline for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
7
7
 
8
- ![cc-pulse statusline](assets/demo.png)
8
+ **Full mode** shows names, groups, and token breakdown:
9
9
 
10
- ## 🚀 Quick Start
10
+ ![cc-pulse full mode](assets/demo-full.png)
11
+
12
+ **Compact mode** shows counts only:
13
+
14
+ ![cc-pulse compact mode](assets/demo-compact.png)
15
+
16
+ ## Quick Start
11
17
 
12
18
  ```bash
13
19
  npm install -g cc-pulse
@@ -24,44 +30,78 @@ Add to `~/.claude/settings.json`:
24
30
  }
25
31
  ```
26
32
 
27
- Restart Claude Code the statusline appears above the input area.
33
+ Restart Claude Code and the statusline appears below the input area.
28
34
 
29
- ## Features
35
+ ## Features
30
36
 
31
37
  | Feature | Description |
32
38
  |---------|-------------|
33
- | **Context usage** | Shows % used with color-coded bar green to red as you approach limits |
34
- | **Token breakdown** | Input ↓, output ↑, and cache tokens at a glance |
35
- | **Model info** | Shows model with version (e.g., "Opus 4.6", "Sonnet 4.5") |
36
- | **Cost tracking** | Session cost with color coding ($1 yellow, $2 orange, $5+ red) |
39
+ | **Context usage** | Percentage used, colour shifts from green to red as you approach limits |
40
+ | **Token breakdown** | Input, output, and cache tokens at a glance |
41
+ | **Model info** | Model family and version, e.g. "Opus 4.6" |
42
+ | **Cost tracking** | Session cost with colour coding |
37
43
  | **MCP health** | Live connection status for all MCP servers |
38
- | **Hook monitoring** | Active hooks by event type, with broken path detection |
39
- | **Skills display** | Shows your custom slash commands/skills |
40
- | **Git status** | Branch name + new/modified/deleted file counts |
44
+ | **Hook monitoring** | Active hooks by event type with broken path detection |
45
+ | **Skills display** | Adapts automatically: individual names, prefix grouping, or counts only |
46
+ | **Git status** | Branch name and file change counts |
47
+ | **Responsive layout** | Width-aware wrapping and a compact mode for smaller screens |
41
48
 
42
- ## 📊 What You Get
43
-
44
- Six lines of information, updated on every message:
45
-
46
- ![cc-pulse statusline](assets/demo.png)
49
+ ## What You Get
47
50
 
48
51
  | Line | Content |
49
52
  |------|---------|
50
- | **Identity** | Project name + working directory |
51
- | **Git** | Branch + file changes (new, modified, deleted) |
52
- | **Engine** | Tier, model, context used, tokens, cost, duration |
53
- | **MCP** | Server count + individual status (✓ connected, ✗ disconnected, ○ disabled) |
54
- | **Hooks** | Hook count by event type, with broken path warnings |
55
- | **Skills** | Custom slash commands count + names |
53
+ | **Identity** | Project name and working directory |
54
+ | **Git** | Branch and file changes (new, modified, deleted) |
55
+ | **Engine** | Model, context usage, tokens, cost, session duration |
56
+ | **MCP** | Server count and individual status |
57
+ | **Hooks** | Hook count by event type with broken path warnings |
58
+ | **Skills** | Names when few, prefix groups when many |
59
+
60
+ ## Responsive Display
61
+
62
+ The statusline adapts to your setup automatically.
63
+
64
+ **Skills** adapt based on count:
65
+ - 10 or fewer: lists all names, e.g. `✦ Skills 5 beads excalidraw mermaid tmux repomix`
66
+ - More than 10 with shared prefixes: groups them, e.g. `✦ Skills 89 bmad:77 beads excalidraw ...`
67
+ - More than 10 without groups: caps at 10 names with overflow, e.g. `✦ Skills 15 a b c ... +5`
68
+
69
+ **Hooks** adapt based on total count:
70
+ - 6 or fewer: shows all names per event, e.g. `⚡Hooks 4 Submit:2 lint,format Post:2 test,deploy`
71
+ - More than 6: caps names to 3 per group, e.g. `⚡Hooks 12 Submit:5 lint,format,check +2`
72
+
73
+ **Width-aware wrapping** breaks long lines at component boundaries instead of cutting them off. Items wrap at 5 per line for readability.
74
+
75
+ **Compact mode** collapses everything to counts only. Toggle with `/pulse-compact` or set in config:
76
+ ```json
77
+ {
78
+ "compact": true
79
+ }
80
+ ```
56
81
 
57
- ## ⚙️ Configuration
82
+ ## Configuration
58
83
 
59
- Create `~/.config/claude-pulse/config.json` to customize. Only include what you want to change.
84
+ Create `~/.config/claude-pulse/config.json` to customise. Only include what you want to change.
85
+
86
+ <details>
87
+ <summary><strong>Compact Mode</strong></summary>
88
+
89
+ Minimal display with counts and essential info only. Toggle with `/pulse-compact` or set manually:
90
+
91
+ ```json
92
+ {
93
+ "compact": true
94
+ }
95
+ ```
96
+
97
+ Skills, hooks, and MCP show counts only. Context hides the token breakdown. Cost hides burn rate. CWD shortens.
98
+
99
+ </details>
60
100
 
61
101
  <details>
62
102
  <summary><strong>Context Window</strong></summary>
63
103
 
64
- Shows how much of the context window is used. Colors shift as usage increases.
104
+ Shows how much of the context window is used. Colours shift as usage increases.
65
105
 
66
106
  ```json
67
107
  {
@@ -82,30 +122,7 @@ Shows how much of the context window is used. Colors shift as usage increases.
82
122
  | `detailed` | `Used 116.0k/200.0k (58%)` |
83
123
  | `both` | `●●●●●●○○○○ 116.0k / 200.0k` |
84
124
 
85
- **Color thresholds** as used % increases:
86
- - **Green**: < 70% used (safe)
87
- - **Yellow**: 70% used (warn)
88
- - **Orange**: 85% used (critical)
89
- - **Red + 🔴**: 95% used (danger)
90
-
91
- </details>
92
-
93
- <details>
94
- <summary><strong>Subscription Tier</strong></summary>
95
-
96
- Set your plan manually (auto-detection isn't reliable):
97
-
98
- ```json
99
- {
100
- "components": {
101
- "tier": {
102
- "override": "max"
103
- }
104
- }
105
- }
106
- ```
107
-
108
- Options: `"pro"`, `"max"`, `"api"`
125
+ Colour thresholds: green below 70%, yellow at 70%, orange at 85%, red at 95%.
109
126
 
110
127
  </details>
111
128
 
@@ -126,14 +143,14 @@ Options: `"pro"`, `"max"`, `"api"`
126
143
 
127
144
  | Option | Effect |
128
145
  |--------|--------|
129
- | `showNames: true` | List each server with status |
130
- | `showOnlyProblems: true` | Hide line when all servers healthy |
131
- | `maxDisplay: 4` | Limit servers shown ("+N more" for rest) |
146
+ | `showNames: true` | List each server with its status |
147
+ | `showOnlyProblems: true` | Hide the line when all servers are healthy |
148
+ | `maxDisplay: 4` | Limit servers shown, with "+N more" for the rest |
132
149
 
133
150
  | Icon | Status |
134
151
  |------|--------|
135
152
  | ✓ | Connected |
136
- | ✗ | Disconnected (red) |
153
+ | ✗ | Disconnected |
137
154
  | ○ | Disabled |
138
155
  | ▲ | Error |
139
156
 
@@ -159,6 +176,8 @@ Options: `"pro"`, `"max"`, `"api"`
159
176
  | `showNames: false` | `⚡Hooks 8 Submit:3 Post:2 End:1` |
160
177
  | Both `false` | `⚡Hooks 8` |
161
178
 
179
+ With more than 6 hooks, names are capped to 3 per event group with a `+N` overflow count.
180
+
162
181
  Broken hooks (invalid paths) show in red with ▲.
163
182
 
164
183
  </details>
@@ -177,7 +196,7 @@ Broken hooks (invalid paths) show in red with ▲.
177
196
  }
178
197
  ```
179
198
 
180
- Color thresholds: green < $1, yellow $1-$2, orange $2-$5, red > $5
199
+ Colour thresholds: green below $1, yellow $1 to $2, orange $2 to $5, red above $5.
181
200
 
182
201
  </details>
183
202
 
@@ -198,16 +217,68 @@ Shows your custom slash commands from `~/.claude/skills/` and `.claude/skills/`.
198
217
  }
199
218
  ```
200
219
 
201
- | Setting | Result |
202
- |---------|--------|
203
- | Both `true` | `✦ Skills 5 commit,pr,branch` |
204
- | `showNames: false` | `✦ Skills 5` |
205
- | `maxDisplay: 3` | Shows first 3 names + overflow count |
220
+ The display adapts automatically based on how many skills you have. See [Responsive Display](#responsive-display) above.
206
221
 
207
222
  Broken skills (missing SKILL.md or invalid frontmatter) show in red with ▲.
208
223
 
209
224
  </details>
210
225
 
226
+ <details>
227
+ <summary><strong>CWD (Working Directory)</strong></summary>
228
+
229
+ Control how the current directory is displayed:
230
+
231
+ ```json
232
+ {
233
+ "components": {
234
+ "cwd": {
235
+ "style": "short",
236
+ "maxLength": 30,
237
+ "showIcon": true
238
+ }
239
+ }
240
+ }
241
+ ```
242
+
243
+ | Style | Example |
244
+ |-------|---------|
245
+ | `short` (default) | `~/…/fix-1612` |
246
+ | `full` | `/home/user/.worktree/my-project/2026-02-13/fix-1612` |
247
+ | `basename` | `fix-1612` |
248
+ | `project` | Project folder name |
249
+
250
+ Increase `maxLength` to show more of the path, or use `basename` if you only care about the folder name.
251
+
252
+ </details>
253
+
254
+ <details>
255
+ <summary><strong>Dividers</strong></summary>
256
+
257
+ Add horizontal line separators between all status sections:
258
+
259
+ ```json
260
+ {
261
+ "dividers": true
262
+ }
263
+ ```
264
+
265
+ Off by default.
266
+
267
+ </details>
268
+
269
+ <details>
270
+ <summary><strong>Section Separators</strong></summary>
271
+
272
+ Add light `---` separators after MCP, hooks, and skills sections. On by default in full mode, hidden in compact mode.
273
+
274
+ ```json
275
+ {
276
+ "sectionSeparators": false
277
+ }
278
+ ```
279
+
280
+ </details>
281
+
211
282
  <details>
212
283
  <summary><strong>Layout</strong></summary>
213
284
 
@@ -224,7 +295,7 @@ The 6-line structure is fixed. You can toggle lines and change separators:
224
295
 
225
296
  | Line | Key | Toggleable |
226
297
  |------|-----|------------|
227
- | Identity | | No (branding) |
298
+ | Identity | n/a | No |
228
299
  | Git | `git` | Yes |
229
300
  | Engine | `engine` | Yes |
230
301
  | MCP | `mcp` | Yes |
@@ -238,7 +309,7 @@ The 6-line structure is fixed. You can toggle lines and change separators:
238
309
 
239
310
  | Component | Key Options |
240
311
  |-----------|-------------|
241
- | `model` | `showIcon: true` adds emoji per model |
312
+ | `model` | `showIcon: true` (default), custom `icons: { opus, sonnet, haiku }` |
242
313
  | `session` | `showDuration: true`, `showId: false` |
243
314
  | `cache` | Shows cache hit rate |
244
315
  | `linesChanged` | Shows `+added -removed` |
@@ -248,7 +319,17 @@ All components accept `"enabled": false` to hide them.
248
319
 
249
320
  </details>
250
321
 
251
- ## 🛠️ Development
322
+ ## Slash Commands
323
+
324
+ cc-pulse ships with a skill you can install to your Claude Code skills directory:
325
+
326
+ | Command | Description |
327
+ |---------|-------------|
328
+ | `/pulse-compact` | Toggle compact mode on/off |
329
+
330
+ Copy `skills/pulse-compact/` to `~/.claude/skills/` or your project's `.claude/skills/`.
331
+
332
+ ## Development
252
333
 
253
334
  ```bash
254
335
  git clone https://github.com/ali-nr/claude-pulse.git
@@ -258,7 +339,7 @@ bun run build
258
339
  bun test
259
340
  ```
260
341
 
261
- Use full path in settings: `"command": "node /path/to/claude-pulse/dist/cli.js"`
342
+ For local testing, use the full path in settings: `"command": "node /path/to/claude-pulse/dist/cli.js"`
262
343
 
263
344
  ## License
264
345
 
package/dist/cli.js CHANGED
@@ -1,18 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
3
7
  var __export = (target, all) => {
4
8
  for (var name in all)
5
9
  __defProp(target, name, {
6
10
  get: all[name],
7
11
  enumerable: true,
8
12
  configurable: true,
9
- set: (newValue) => all[name] = () => newValue
13
+ set: __exportSetter.bind(all, name)
10
14
  });
11
15
  };
12
16
  // package.json
13
17
  var package_default = {
14
18
  name: "cc-pulse",
15
- version: "1.3.2",
19
+ version: "1.5.0",
16
20
  description: "A customizable, real-time statusline for Claude Code",
17
21
  type: "module",
18
22
  bin: {
@@ -451,15 +455,30 @@ function renderHooks(config, theme) {
451
455
  const text2 = `${theme.yellow}⚡${hookLabel} ${summary.total}${theme.reset}`;
452
456
  return { text: text2 };
453
457
  }
454
- const eventParts = Object.entries(summary.events).map(([event, detail]) => {
458
+ const header = `${theme.yellow}⚡${hookLabel} ${summary.total}${theme.reset}`;
459
+ const MAX_NAMES_PER_GROUP = 3;
460
+ const compact = summary.total > 6;
461
+ const items = [];
462
+ for (const [event, detail] of Object.entries(summary.events)) {
455
463
  const label = EVENT_LABELS[event] ?? event;
456
- const goodNames = showNames && detail.names.length > 0 ? ` ${theme.flamingo}${detail.names.join(",")}${theme.reset}` : "";
457
- const brokenNames = detail.broken.length > 0 ? ` ${theme.red}${detail.broken.join(",")} ▲${theme.reset}` : "";
458
464
  const countStr = showCount ? `${theme.peach}${detail.count}${theme.reset}` : "";
459
- return `${theme.lavender}${label}:${theme.reset}${countStr}${goodNames}${brokenNames}`;
460
- });
461
- const text = `${theme.yellow}⚡${hookLabel} ${summary.total}${theme.reset} ${eventParts.join(" ")}`;
462
- return { text };
465
+ const eventTag = `${theme.lavender}${label}:${theme.reset}${countStr}`;
466
+ if (showNames && detail.names.length > 0) {
467
+ const cap = compact ? MAX_NAMES_PER_GROUP : detail.names.length;
468
+ const displayed = detail.names.slice(0, cap);
469
+ const overflow = detail.names.length - cap;
470
+ const nameStr = `${theme.flamingo}${displayed.join(",")}${theme.reset}`;
471
+ const overflowStr = overflow > 0 ? ` ${theme.overlay0}+${overflow}${theme.reset}` : "";
472
+ items.push(`${eventTag} ${nameStr}${overflowStr}`);
473
+ } else {
474
+ items.push(eventTag);
475
+ }
476
+ if (detail.broken.length > 0) {
477
+ items.push(`${theme.red}${detail.broken.join(",")} ▲${theme.reset}`);
478
+ }
479
+ }
480
+ const text = items.length > 0 ? `${header} ${items.join(" ")}` : header;
481
+ return { text, header, items };
463
482
  }
464
483
  function getHooksSummary() {
465
484
  const events = {};
@@ -479,9 +498,11 @@ function extractHookInfo(command) {
479
498
  const parts = command.split(/\s+/);
480
499
  for (const part of parts) {
481
500
  if (part.includes("/")) {
482
- const base = part.split("/").pop() ?? part;
501
+ const cleaned = part.replace(/^["']|["']$/g, "");
502
+ const expanded = cleaned.replace(/\$(\w+)/g, (_, v) => process.env[v] ?? `$${v}`);
503
+ const base = expanded.split("/").pop() ?? expanded;
483
504
  const name2 = base.replace(/\.[^.]+$/, "");
484
- const broken = !existsSync2(part);
505
+ const broken = !existsSync2(expanded);
485
506
  return { name: name2, broken };
486
507
  }
487
508
  }
@@ -590,7 +611,7 @@ function renderMcp(config, theme) {
590
611
  }
591
612
  const displayServers = servers.slice(0, maxDisplay);
592
613
  const remaining = servers.length - maxDisplay;
593
- const serverParts = displayServers.map((server) => {
614
+ const serverItems = displayServers.map((server) => {
594
615
  let icon;
595
616
  let color;
596
617
  switch (server.status) {
@@ -612,17 +633,17 @@ function renderMcp(config, theme) {
612
633
  }
613
634
  return `${color}${server.name} ${icon}${theme.reset}`;
614
635
  });
615
- let serverStr = serverParts.join(" ");
616
636
  if (remaining > 0) {
617
- serverStr += ` ${theme.sky}+${remaining} more${theme.reset}`;
637
+ serverItems.push(`${theme.sky}+${remaining} more${theme.reset}`);
618
638
  }
619
639
  const countStr = `${connectedCount}/${servers.length}`;
640
+ const header = `${theme.sky}${label} ${theme.green}${countStr}${theme.reset}:`;
620
641
  if (config.showNames === false) {
621
642
  const text2 = `${theme.sky}${label} ${theme.green}${countStr}${theme.reset}`;
622
643
  return { text: text2, action: "/mcp" };
623
644
  }
624
- const text = `${theme.sky}${label} ${theme.green}${countStr}${theme.reset}: ${serverStr}`;
625
- return { text, action: "/mcp" };
645
+ const text = `${header} ${serverItems.join(" ")}`;
646
+ return { text, header, items: serverItems, action: "/mcp" };
626
647
  }
627
648
  function getMcpServers() {
628
649
  const state = loadState();
@@ -718,7 +739,7 @@ function parseMcpOutput(output) {
718
739
  }
719
740
  // src/components/model.ts
720
741
  function parseModelId(modelId) {
721
- const id = modelId.toLowerCase();
742
+ const id = modelId.toLowerCase().replace(/\[.*\]$/, "");
722
743
  const newFormat = id.match(/^claude-(\w+)-(\d+)-(\d+)-\d+$/);
723
744
  if (newFormat) {
724
745
  const [, family, major, minor] = newFormat;
@@ -729,6 +750,16 @@ function parseModelId(modelId) {
729
750
  const [, family, major] = newFormatNoMinor;
730
751
  return { family, version: major };
731
752
  }
753
+ const shortWithMinor = id.match(/^claude-(\w+)-(\d+)-(\d+)$/);
754
+ if (shortWithMinor) {
755
+ const [, family, major, minor] = shortWithMinor;
756
+ return { family, version: `${major}.${minor}` };
757
+ }
758
+ const shortNoMinor = id.match(/^claude-(\w+)-(\d+)$/);
759
+ if (shortNoMinor) {
760
+ const [, family, major] = shortNoMinor;
761
+ return { family, version: major };
762
+ }
732
763
  const oldFormat = id.match(/^claude-(\d+)-(\d+)-(\w+)-\d+$/);
733
764
  if (oldFormat) {
734
765
  const [, major, minor, family] = oldFormat;
@@ -747,7 +778,7 @@ function renderModel(input, config, theme) {
747
778
  }
748
779
  const modelId = input.model?.id ?? "";
749
780
  const displayName = input.model?.display_name ?? "";
750
- const icons = config.icons ?? { opus: "\uD83E\uDDE0", sonnet: "\uD83C\uDFB5", haiku: "" };
781
+ const icons = config.icons ?? { opus: "", sonnet: "", haiku: "" };
751
782
  const showIcon = config.showIcon !== false;
752
783
  let icon = "\uD83E\uDD16";
753
784
  let color = theme.text;
@@ -855,22 +886,69 @@ function renderSkills(config, theme) {
855
886
  }
856
887
  const validNames = summary.skills.filter((s) => s.valid).map((s) => s.name ?? s.folder).slice(0, maxDisplay);
857
888
  const brokenNames = summary.skills.filter((s) => !s.valid).map((s) => s.folder);
858
- const parts = [];
859
- if (showCount) {
860
- parts.push(`${theme.mauve}${summary.valid}${theme.reset}`);
861
- }
889
+ const countStr = showCount ? ` ${theme.mauve}${summary.valid}${theme.reset}` : "";
890
+ const header = `${theme.mauve}✦ ${label}${theme.reset}${countStr}`;
891
+ const items = [];
892
+ const GROUP_THRESHOLD = 10;
893
+ const MIN_PREFIX_COUNT = 3;
862
894
  if (showNames && validNames.length > 0) {
863
- const displayNames = validNames.join(",");
864
- const overflow = summary.valid > maxDisplay ? `+${summary.valid - maxDisplay}` : "";
865
- parts.push(`${theme.flamingo}${displayNames}${overflow ? ` ${theme.overlay0}${overflow}` : ""}${theme.reset}`);
895
+ if (validNames.length > GROUP_THRESHOLD) {
896
+ const { groups, ungrouped } = groupByPrefix(validNames, MIN_PREFIX_COUNT);
897
+ const hasGroups = Object.keys(groups).length > 0;
898
+ if (hasGroups) {
899
+ for (const [prefix, names] of Object.entries(groups).sort((a, b) => b[1].length - a[1].length)) {
900
+ items.push(`${theme.overlay1}${prefix}:${theme.peach}${names.length}${theme.reset}`);
901
+ }
902
+ for (const name of ungrouped) {
903
+ items.push(`${theme.flamingo}${name}${theme.reset}`);
904
+ }
905
+ } else {
906
+ const CAP = 10;
907
+ const capped = validNames.slice(0, CAP);
908
+ for (const name of capped) {
909
+ items.push(`${theme.flamingo}${name}${theme.reset}`);
910
+ }
911
+ if (validNames.length > CAP) {
912
+ items.push(`${theme.overlay0}+${validNames.length - CAP}${theme.reset}`);
913
+ }
914
+ }
915
+ } else {
916
+ for (const name of validNames) {
917
+ items.push(`${theme.flamingo}${name}${theme.reset}`);
918
+ }
919
+ }
920
+ const overflow = summary.valid > maxDisplay ? summary.valid - maxDisplay : 0;
921
+ if (overflow > 0) {
922
+ items.push(`${theme.overlay0}+${overflow}${theme.reset}`);
923
+ }
866
924
  }
867
- let brokenStr = "";
868
925
  if (summary.broken > 0) {
869
- const brokenDisplay = brokenNames.length <= 2 ? brokenNames.join(",") : `${summary.broken} broken`;
870
- brokenStr = ` ${theme.red}▲${brokenDisplay}${theme.reset}`;
926
+ for (const name of brokenNames) {
927
+ items.push(`${theme.red}▲${name}${theme.reset}`);
928
+ }
929
+ }
930
+ const text = items.length > 0 ? `${header} ${items.join(" ")}` : header;
931
+ return { text, header, items };
932
+ }
933
+ function groupByPrefix(names, minCount) {
934
+ const buckets = {};
935
+ for (const name of names) {
936
+ const sep = name.includes(":") ? ":" : "-";
937
+ const prefix = name.split(sep)[0];
938
+ if (!buckets[prefix])
939
+ buckets[prefix] = [];
940
+ buckets[prefix].push(name);
941
+ }
942
+ const groups = {};
943
+ const ungrouped = [];
944
+ for (const [prefix, members] of Object.entries(buckets)) {
945
+ if (members.length >= minCount) {
946
+ groups[prefix] = members;
947
+ } else {
948
+ ungrouped.push(...members);
949
+ }
871
950
  }
872
- const text = `${theme.mauve}✦ ${label}${theme.reset}${parts.length ? ` ${parts.join(" ")}` : ""}${brokenStr}`;
873
- return { text };
951
+ return { groups, ungrouped };
874
952
  }
875
953
  function getSkillsSummary() {
876
954
  const skills = [];
@@ -999,58 +1077,6 @@ function renderSystem(input, config, theme) {
999
1077
  const text = parts.join(" │ ");
1000
1078
  return { text };
1001
1079
  }
1002
- // src/components/tier.ts
1003
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1004
- import { homedir as homedir6 } from "node:os";
1005
- import { join as join5 } from "node:path";
1006
- var cachedTier = null;
1007
- var cacheTime = 0;
1008
- var CACHE_TTL3 = 30000;
1009
- function renderTier(config, theme) {
1010
- if (config.enabled === false) {
1011
- return { text: "" };
1012
- }
1013
- const labels = config.labels ?? { pro: "PRO", max: "MAX", api: "API" };
1014
- const tier = config.override ?? detectTier();
1015
- let color = theme.blue;
1016
- let label = labels.pro;
1017
- if (tier === "max") {
1018
- color = theme.mauve;
1019
- label = labels.max;
1020
- } else if (tier === "api") {
1021
- color = theme.green;
1022
- label = labels.api;
1023
- }
1024
- const icon = "◆ ";
1025
- const text = `${color}${theme.bold}${icon}${label}${theme.reset}`;
1026
- return { text, action: "/usage" };
1027
- }
1028
- function detectTier() {
1029
- const now = Date.now();
1030
- if (cachedTier && now - cacheTime < CACHE_TTL3) {
1031
- return cachedTier;
1032
- }
1033
- const claudeJsonPath = join5(homedir6(), ".claude.json");
1034
- try {
1035
- if (existsSync5(claudeJsonPath)) {
1036
- const content = readFileSync5(claudeJsonPath, "utf-8");
1037
- const data = JSON.parse(content);
1038
- if (data.oauthAccount?.hasExtraUsageEnabled) {
1039
- cachedTier = "max";
1040
- } else if (data.oauthAccount) {
1041
- cachedTier = "pro";
1042
- } else {
1043
- cachedTier = "api";
1044
- }
1045
- } else {
1046
- cachedTier = "api";
1047
- }
1048
- } catch {
1049
- cachedTier = "pro";
1050
- }
1051
- cacheTime = now;
1052
- return cachedTier;
1053
- }
1054
1080
  // src/components/time.ts
1055
1081
  function renderTime(config, theme) {
1056
1082
  if (config.enabled === false) {
@@ -1084,12 +1110,12 @@ function renderTime(config, theme) {
1084
1110
  return { text };
1085
1111
  }
1086
1112
  // src/config.ts
1087
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
1088
- import { homedir as homedir7 } from "node:os";
1089
- import { join as join6 } from "node:path";
1113
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1114
+ import { homedir as homedir6 } from "node:os";
1115
+ import { join as join5 } from "node:path";
1090
1116
  var CONFIG_PATHS = [
1091
- join6(homedir7(), ".config", "claude-pulse", "config.json"),
1092
- join6(homedir7(), ".claude-pulse.json")
1117
+ join5(homedir6(), ".config", "claude-pulse", "config.json"),
1118
+ join5(homedir6(), ".claude-pulse.json")
1093
1119
  ];
1094
1120
  var FIXED_LINES = [
1095
1121
  { name: "identity", enabled: true, components: ["name", "cwd"], separator: " " },
@@ -1097,7 +1123,7 @@ var FIXED_LINES = [
1097
1123
  {
1098
1124
  name: "engine",
1099
1125
  enabled: true,
1100
- components: ["tier", "model", "context", "cost", "session"],
1126
+ components: ["model", "context", "cost", "session"],
1101
1127
  separator: " │ "
1102
1128
  },
1103
1129
  { name: "mcp", enabled: true, components: ["mcp"], separator: " │ " },
@@ -1107,13 +1133,9 @@ var FIXED_LINES = [
1107
1133
  var DEFAULT_CONFIG = {
1108
1134
  theme: "catppuccin",
1109
1135
  components: {
1110
- tier: {
1111
- enabled: true,
1112
- labels: { pro: "PRO", max: "MAX", api: "API" }
1113
- },
1114
1136
  model: {
1115
1137
  enabled: true,
1116
- showIcon: false
1138
+ showIcon: true
1117
1139
  },
1118
1140
  context: {
1119
1141
  enabled: true,
@@ -1182,9 +1204,9 @@ var DEFAULT_CONFIG = {
1182
1204
  };
1183
1205
  function loadConfig() {
1184
1206
  for (const configPath of CONFIG_PATHS) {
1185
- if (existsSync6(configPath)) {
1207
+ if (existsSync5(configPath)) {
1186
1208
  try {
1187
- const content = readFileSync6(configPath, "utf-8");
1209
+ const content = readFileSync5(configPath, "utf-8");
1188
1210
  const userConfig = JSON.parse(content);
1189
1211
  return mergeConfig(DEFAULT_CONFIG, userConfig);
1190
1212
  } catch {}
@@ -1211,6 +1233,12 @@ function mergeConfig(target, source) {
1211
1233
  const result = { ...target };
1212
1234
  if (source.theme !== undefined)
1213
1235
  result.theme = source.theme;
1236
+ if (source.compact !== undefined)
1237
+ result.compact = source.compact;
1238
+ if (source.dividers !== undefined)
1239
+ result.dividers = source.dividers;
1240
+ if (source.sectionSeparators !== undefined)
1241
+ result.sectionSeparators = source.sectionSeparators;
1214
1242
  if (source.lines !== undefined)
1215
1243
  result.lines = source.lines;
1216
1244
  if (source.interactive !== undefined)
@@ -14805,7 +14833,9 @@ var McpServerSchema = exports_external.object({
14805
14833
  });
14806
14834
  var ComponentOutputSchema = exports_external.object({
14807
14835
  text: exports_external.string(),
14808
- action: exports_external.string().optional()
14836
+ action: exports_external.string().optional(),
14837
+ header: exports_external.string().optional(),
14838
+ items: exports_external.array(exports_external.string()).optional()
14809
14839
  });
14810
14840
  var TierConfigSchema = exports_external.object({
14811
14841
  enabled: exports_external.boolean().optional(),
@@ -14952,6 +14982,9 @@ var LinesConfigSchema = exports_external.object({
14952
14982
  });
14953
14983
  var PulseConfigSchema = exports_external.object({
14954
14984
  theme: exports_external.string(),
14985
+ compact: exports_external.boolean().optional(),
14986
+ dividers: exports_external.boolean().optional(),
14987
+ sectionSeparators: exports_external.boolean().optional(),
14955
14988
  lines: LinesConfigSchema.optional(),
14956
14989
  components: ComponentConfigsSchema,
14957
14990
  interactive: exports_external.object({
@@ -15004,6 +15037,43 @@ var catppuccin = {
15004
15037
  dim: "\x1B[2m"
15005
15038
  };
15006
15039
 
15040
+ // src/truncate.ts
15041
+ var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
15042
+ function visibleLength(str) {
15043
+ return str.replace(ANSI_RE, "").length;
15044
+ }
15045
+ function wrapParts(parts, separator, maxWidth, indent = 2, maxPerLine = 0) {
15046
+ if (parts.length === 0)
15047
+ return "";
15048
+ const sepWidth = visibleLength(separator);
15049
+ const pad = " ".repeat(indent);
15050
+ const lines = [];
15051
+ let currentLine = parts[0];
15052
+ let currentWidth = visibleLength(parts[0]);
15053
+ let partsOnLine = 1;
15054
+ for (let i = 1;i < parts.length; i++) {
15055
+ const partWidth = visibleLength(parts[i]);
15056
+ const wouldBe = currentWidth + sepWidth + partWidth;
15057
+ const hitMax = maxPerLine > 0 && partsOnLine >= maxPerLine;
15058
+ if (wouldBe > maxWidth || hitMax) {
15059
+ lines.push(currentLine);
15060
+ currentLine = pad + parts[i];
15061
+ currentWidth = indent + partWidth;
15062
+ partsOnLine = 1;
15063
+ } else {
15064
+ currentLine += separator + parts[i];
15065
+ currentWidth = wouldBe;
15066
+ partsOnLine++;
15067
+ }
15068
+ }
15069
+ lines.push(currentLine);
15070
+ return lines.join(`
15071
+ `);
15072
+ }
15073
+ function getTerminalWidth() {
15074
+ return process.stdout.columns || Number(process.env.COLUMNS) || 80;
15075
+ }
15076
+
15007
15077
  // src/cli.ts
15008
15078
  var VERSION = package_default.version ?? "1.0.0";
15009
15079
  async function main() {
@@ -15042,30 +15112,70 @@ Configuration:
15042
15112
  const config2 = loadConfig();
15043
15113
  const theme = catppuccin;
15044
15114
  const lines = getLines(config2);
15115
+ const termWidth = getTerminalWidth();
15116
+ if (config2.compact) {
15117
+ if (config2.components.skills) {
15118
+ config2.components.skills.showNames = false;
15119
+ }
15120
+ if (config2.components.hooks) {
15121
+ config2.components.hooks.showNames = false;
15122
+ config2.components.hooks.showCount = false;
15123
+ }
15124
+ if (config2.components.mcp) {
15125
+ config2.components.mcp.showNames = false;
15126
+ }
15127
+ if (config2.components.context) {
15128
+ config2.components.context.showTokens = false;
15129
+ config2.components.context.showRate = false;
15130
+ }
15131
+ if (config2.components.cost) {
15132
+ config2.components.cost.showBurnRate = false;
15133
+ config2.components.cost.showProjection = false;
15134
+ }
15135
+ if (config2.components.cwd) {
15136
+ config2.components.cwd.maxLength = 15;
15137
+ }
15138
+ }
15045
15139
  const outputLines = [];
15046
15140
  for (const line of lines) {
15047
15141
  if (!line.enabled)
15048
15142
  continue;
15049
- const parts = [];
15050
15143
  const separator = ` ${theme.overlay2}│${theme.reset} `;
15051
15144
  const sep = line.separator ?? separator;
15145
+ const outputs = [];
15052
15146
  for (const componentName of line.components) {
15053
15147
  const output = renderComponent(componentName, input, config2, theme);
15054
15148
  if (output.text) {
15055
- parts.push(output.text);
15149
+ outputs.push(output);
15056
15150
  }
15057
15151
  }
15058
- if (parts.length > 0) {
15059
- outputLines.push(parts.join(sep));
15152
+ if (outputs.length === 0)
15153
+ continue;
15154
+ if (outputs.length === 1 && outputs[0].header && outputs[0].items?.length) {
15155
+ const { header, items } = outputs[0];
15156
+ const rendered = wrapParts([header, ...items], " ", termWidth, 2, 4);
15157
+ outputLines.push(rendered);
15158
+ } else {
15159
+ const parts = outputs.map((o) => o.text);
15160
+ outputLines.push(wrapParts(parts, sep, termWidth));
15161
+ }
15162
+ if (!config2.compact && config2.sectionSeparators !== false && (line.name === "mcp" || line.name === "hooks" || line.name === "skills")) {
15163
+ outputLines.push(`${theme.overlay0}---${theme.reset}`);
15060
15164
  }
15061
15165
  }
15062
- console.log(outputLines.join(`
15166
+ if (config2.dividers) {
15167
+ const divider = `${theme.overlay0}${"─".repeat(termWidth)}${theme.reset}`;
15168
+ console.log(`${outputLines.join(`
15169
+ ${divider}
15170
+ `)}
15171
+ ${divider}`);
15172
+ } else {
15173
+ console.log(outputLines.join(`
15063
15174
  `));
15175
+ }
15064
15176
  }
15065
15177
  function renderComponent(name, input, config2, theme) {
15066
15178
  switch (name) {
15067
- case "tier":
15068
- return renderTier(config2.components.tier ?? {}, theme);
15069
15179
  case "model":
15070
15180
  return renderModel(input, config2.components.model ?? {}, theme);
15071
15181
  case "context":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-pulse",
3
- "version": "1.3.2",
3
+ "version": "1.5.0",
4
4
  "description": "A customizable, real-time statusline for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {