cc-pulse 1.4.0 → 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 +88 -50
  2. package/dist/cli.js +40 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -5,7 +5,13 @@
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
+
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)
9
15
 
10
16
  ## Quick Start
11
17
 
@@ -24,68 +30,63 @@ Add to `~/.claude/settings.json`:
24
30
  }
25
31
  ```
26
32
 
27
- Restart Claude Code the statusline appears below the input area.
33
+ Restart Claude Code and the statusline appears below the input area.
28
34
 
29
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 |
39
+ | **Context usage** | Percentage used, colour shifts from green to red as you approach limits |
34
40
  | **Token breakdown** | Input, output, and cache tokens at a glance |
35
- | **Model info** | Shows model with version (e.g., "Opus 4.6", "◆ Sonnet 4") |
36
- | **Cost tracking** | Session cost with color coding ($1 yellow, $2 orange, $5+ red) |
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** | Adaptive display individual names, prefix grouping, or compact counts |
40
- | **Git status** | Branch name + new/modified/deleted file counts |
41
- | **Responsive layout** | Width-aware wrapping + compact mode for smaller screens |
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 |
42
48
 
43
49
  ## What You Get
44
50
 
45
- Six lines of information, updated on every message:
46
-
47
51
  | Line | Content |
48
52
  |------|---------|
49
- | **Identity** | Project name + working directory |
50
- | **Git** | Branch + file changes (new, modified, deleted) |
51
- | **Engine** | Model, context used, tokens, cost, duration |
52
- | **MCP** | Server count + individual status (✓ connected, ✗ disconnected, ○ disabled) |
53
- | **Hooks** | Hook count by event type, with broken path warnings |
54
- | **Skills** | Adaptive: names when few, prefix groups when many |
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 |
55
59
 
56
60
  ## Responsive Display
57
61
 
58
- The statusline adapts to your setup automatically:
62
+ The statusline adapts to your setup automatically.
59
63
 
60
- **Skills** adapts based on count:
61
- - **10 or fewer**: lists all names `✦ Skills 5 beads,excalidraw,mermaid,tmux,repomix`
62
- - **More than 10 with shared prefixes**: groups them `✦ Skills 89 bmad:77 beads,excalidraw,mermaid,...`
63
- - **More than 10, no groups**: caps at 10 names with overflow `✦ Skills 15 a,b,c,d,e,f,g,h,i,j +5`
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`
64
68
 
65
- **Hooks** adapts based on total count:
66
- - **6 or fewer**: shows all names per event `⚡Hooks 4 Submit:2 lint,format Post:2 test,deploy`
67
- - **More than 6**: caps names to 3 per group `⚡Hooks 12 Submit:5 lint,format,check +2`
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`
68
72
 
69
- **Width-aware wrapping** when a line exceeds terminal width, components wrap onto indented continuation lines instead of being cut off.
73
+ **Width-aware wrapping** breaks long lines at component boundaries instead of cutting them off. Items wrap at 5 per line for readability.
70
74
 
71
- **Compact mode** toggle with `/pulse-compact` or set in config:
75
+ **Compact mode** collapses everything to counts only. Toggle with `/pulse-compact` or set in config:
72
76
  ```json
73
77
  {
74
78
  "compact": true
75
79
  }
76
80
  ```
77
81
 
78
- In compact mode, everything collapses to counts only:
79
- - `⬢ MCP 3/4` | `⚡Hooks 8` | `✦ Skills 89` | `Used 30%` | `$2.50`
80
-
81
82
  ## Configuration
82
83
 
83
- 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.
84
85
 
85
86
  <details>
86
87
  <summary><strong>Compact Mode</strong></summary>
87
88
 
88
- Minimal display showing only counts and essential info. Toggle with the `/pulse-compact` slash command, or set manually:
89
+ Minimal display with counts and essential info only. Toggle with `/pulse-compact` or set manually:
89
90
 
90
91
  ```json
91
92
  {
@@ -93,14 +94,14 @@ Minimal display showing only counts and essential info. Toggle with the `/pulse-
93
94
  }
94
95
  ```
95
96
 
96
- When enabled: skills, hooks, and MCP show counts only; context hides token breakdown; cost hides burn rate; CWD shortens.
97
+ Skills, hooks, and MCP show counts only. Context hides the token breakdown. Cost hides burn rate. CWD shortens.
97
98
 
98
99
  </details>
99
100
 
100
101
  <details>
101
102
  <summary><strong>Context Window</strong></summary>
102
103
 
103
- 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.
104
105
 
105
106
  ```json
106
107
  {
@@ -121,11 +122,7 @@ Shows how much of the context window is used. Colors shift as usage increases.
121
122
  | `detailed` | `Used 116.0k/200.0k (58%)` |
122
123
  | `both` | `●●●●●●○○○○ 116.0k / 200.0k` |
123
124
 
124
- **Color thresholds** as used % increases:
125
- - **Green**: < 70% used (safe)
126
- - **Yellow**: 70% used (warn)
127
- - **Orange**: 85% used (critical)
128
- - **Red**: 95% used (danger)
125
+ Colour thresholds: green below 70%, yellow at 70%, orange at 85%, red at 95%.
129
126
 
130
127
  </details>
131
128
 
@@ -146,14 +143,14 @@ Shows how much of the context window is used. Colors shift as usage increases.
146
143
 
147
144
  | Option | Effect |
148
145
  |--------|--------|
149
- | `showNames: true` | List each server with status |
150
- | `showOnlyProblems: true` | Hide line when all servers healthy |
151
- | `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 |
152
149
 
153
150
  | Icon | Status |
154
151
  |------|--------|
155
152
  | ✓ | Connected |
156
- | ✗ | Disconnected (red) |
153
+ | ✗ | Disconnected |
157
154
  | ○ | Disabled |
158
155
  | ▲ | Error |
159
156
 
@@ -179,7 +176,7 @@ Shows how much of the context window is used. Colors shift as usage increases.
179
176
  | `showNames: false` | `⚡Hooks 8 Submit:3 Post:2 End:1` |
180
177
  | Both `false` | `⚡Hooks 8` |
181
178
 
182
- When you have many hooks (>6), names are automatically capped to 3 per event group with a `+N` overflow indicator.
179
+ With more than 6 hooks, names are capped to 3 per event group with a `+N` overflow count.
183
180
 
184
181
  Broken hooks (invalid paths) show in red with ▲.
185
182
 
@@ -199,7 +196,7 @@ Broken hooks (invalid paths) show in red with ▲.
199
196
  }
200
197
  ```
201
198
 
202
- 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.
203
200
 
204
201
  </details>
205
202
 
@@ -220,16 +217,44 @@ Shows your custom slash commands from `~/.claude/skills/` and `.claude/skills/`.
220
217
  }
221
218
  ```
222
219
 
223
- The display adapts automatically based on how many skills you have see [Responsive Display](#responsive-display) above.
220
+ The display adapts automatically based on how many skills you have. See [Responsive Display](#responsive-display) above.
224
221
 
225
222
  Broken skills (missing SKILL.md or invalid frontmatter) show in red with ▲.
226
223
 
227
224
  </details>
228
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
+
229
254
  <details>
230
255
  <summary><strong>Dividers</strong></summary>
231
256
 
232
- Add horizontal line separators between status sections:
257
+ Add horizontal line separators between all status sections:
233
258
 
234
259
  ```json
235
260
  {
@@ -241,6 +266,19 @@ Off by default.
241
266
 
242
267
  </details>
243
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
+
244
282
  <details>
245
283
  <summary><strong>Layout</strong></summary>
246
284
 
@@ -257,7 +295,7 @@ The 6-line structure is fixed. You can toggle lines and change separators:
257
295
 
258
296
  | Line | Key | Toggleable |
259
297
  |------|-----|------------|
260
- | Identity | | No (branding) |
298
+ | Identity | n/a | No |
261
299
  | Git | `git` | Yes |
262
300
  | Engine | `engine` | Yes |
263
301
  | MCP | `mcp` | Yes |
@@ -289,7 +327,7 @@ cc-pulse ships with a skill you can install to your Claude Code skills directory
289
327
  |---------|-------------|
290
328
  | `/pulse-compact` | Toggle compact mode on/off |
291
329
 
292
- To install, copy `skills/pulse-compact/` to `~/.claude/skills/` or your project's `.claude/skills/`.
330
+ Copy `skills/pulse-compact/` to `~/.claude/skills/` or your project's `.claude/skills/`.
293
331
 
294
332
  ## Development
295
333
 
@@ -301,7 +339,7 @@ bun run build
301
339
  bun test
302
340
  ```
303
341
 
304
- 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"`
305
343
 
306
344
  ## License
307
345
 
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ var __export = (target, all) => {
16
16
  // package.json
17
17
  var package_default = {
18
18
  name: "cc-pulse",
19
- version: "1.4.0",
19
+ version: "1.5.0",
20
20
  description: "A customizable, real-time statusline for Claude Code",
21
21
  type: "module",
22
22
  bin: {
@@ -458,22 +458,25 @@ function renderHooks(config, theme) {
458
458
  const header = `${theme.yellow}⚡${hookLabel} ${summary.total}${theme.reset}`;
459
459
  const MAX_NAMES_PER_GROUP = 3;
460
460
  const compact = summary.total > 6;
461
- const items = Object.entries(summary.events).map(([event, detail]) => {
461
+ const items = [];
462
+ for (const [event, detail] of Object.entries(summary.events)) {
462
463
  const label = EVENT_LABELS[event] ?? event;
463
- const brokenStr = detail.broken.length > 0 ? ` ${theme.red}${detail.broken.join(",")} ▲${theme.reset}` : "";
464
464
  const countStr = showCount ? `${theme.peach}${detail.count}${theme.reset}` : "";
465
- let goodNames = "";
465
+ const eventTag = `${theme.lavender}${label}:${theme.reset}${countStr}`;
466
466
  if (showNames && detail.names.length > 0) {
467
467
  const cap = compact ? MAX_NAMES_PER_GROUP : detail.names.length;
468
468
  const displayed = detail.names.slice(0, cap);
469
469
  const overflow = detail.names.length - cap;
470
- goodNames = ` ${theme.flamingo}${displayed.join(",")}${theme.reset}`;
471
- if (overflow > 0) {
472
- goodNames += ` ${theme.overlay0}+${overflow}${theme.reset}`;
473
- }
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);
474
475
  }
475
- return `${theme.lavender}${label}:${theme.reset}${countStr}${goodNames}${brokenStr}`;
476
- });
476
+ if (detail.broken.length > 0) {
477
+ items.push(`${theme.red}${detail.broken.join(",")} ▲${theme.reset}`);
478
+ }
479
+ }
477
480
  const text = items.length > 0 ? `${header} ${items.join(" ")}` : header;
478
481
  return { text, header, items };
479
482
  }
@@ -495,9 +498,11 @@ function extractHookInfo(command) {
495
498
  const parts = command.split(/\s+/);
496
499
  for (const part of parts) {
497
500
  if (part.includes("/")) {
498
- 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;
499
504
  const name2 = base.replace(/\.[^.]+$/, "");
500
- const broken = !existsSync2(part);
505
+ const broken = !existsSync2(expanded);
501
506
  return { name: name2, broken };
502
507
  }
503
508
  }
@@ -894,19 +899,23 @@ function renderSkills(config, theme) {
894
899
  for (const [prefix, names] of Object.entries(groups).sort((a, b) => b[1].length - a[1].length)) {
895
900
  items.push(`${theme.overlay1}${prefix}:${theme.peach}${names.length}${theme.reset}`);
896
901
  }
897
- if (ungrouped.length > 0) {
898
- items.push(`${theme.flamingo}${ungrouped.join(",")}${theme.reset}`);
902
+ for (const name of ungrouped) {
903
+ items.push(`${theme.flamingo}${name}${theme.reset}`);
899
904
  }
900
905
  } else {
901
906
  const CAP = 10;
902
907
  const capped = validNames.slice(0, CAP);
903
- items.push(`${theme.flamingo}${capped.join(",")}${theme.reset}`);
908
+ for (const name of capped) {
909
+ items.push(`${theme.flamingo}${name}${theme.reset}`);
910
+ }
904
911
  if (validNames.length > CAP) {
905
912
  items.push(`${theme.overlay0}+${validNames.length - CAP}${theme.reset}`);
906
913
  }
907
914
  }
908
915
  } else {
909
- items.push(`${theme.flamingo}${validNames.join(",")}${theme.reset}`);
916
+ for (const name of validNames) {
917
+ items.push(`${theme.flamingo}${name}${theme.reset}`);
918
+ }
910
919
  }
911
920
  const overflow = summary.valid > maxDisplay ? summary.valid - maxDisplay : 0;
912
921
  if (overflow > 0) {
@@ -1228,6 +1237,8 @@ function mergeConfig(target, source) {
1228
1237
  result.compact = source.compact;
1229
1238
  if (source.dividers !== undefined)
1230
1239
  result.dividers = source.dividers;
1240
+ if (source.sectionSeparators !== undefined)
1241
+ result.sectionSeparators = source.sectionSeparators;
1231
1242
  if (source.lines !== undefined)
1232
1243
  result.lines = source.lines;
1233
1244
  if (source.interactive !== undefined)
@@ -14973,6 +14984,7 @@ var PulseConfigSchema = exports_external.object({
14973
14984
  theme: exports_external.string(),
14974
14985
  compact: exports_external.boolean().optional(),
14975
14986
  dividers: exports_external.boolean().optional(),
14987
+ sectionSeparators: exports_external.boolean().optional(),
14976
14988
  lines: LinesConfigSchema.optional(),
14977
14989
  components: ComponentConfigsSchema,
14978
14990
  interactive: exports_external.object({
@@ -15030,7 +15042,7 @@ var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
15030
15042
  function visibleLength(str) {
15031
15043
  return str.replace(ANSI_RE, "").length;
15032
15044
  }
15033
- function wrapParts(parts, separator, maxWidth, indent = 2) {
15045
+ function wrapParts(parts, separator, maxWidth, indent = 2, maxPerLine = 0) {
15034
15046
  if (parts.length === 0)
15035
15047
  return "";
15036
15048
  const sepWidth = visibleLength(separator);
@@ -15038,16 +15050,20 @@ function wrapParts(parts, separator, maxWidth, indent = 2) {
15038
15050
  const lines = [];
15039
15051
  let currentLine = parts[0];
15040
15052
  let currentWidth = visibleLength(parts[0]);
15053
+ let partsOnLine = 1;
15041
15054
  for (let i = 1;i < parts.length; i++) {
15042
15055
  const partWidth = visibleLength(parts[i]);
15043
15056
  const wouldBe = currentWidth + sepWidth + partWidth;
15044
- if (wouldBe > maxWidth) {
15057
+ const hitMax = maxPerLine > 0 && partsOnLine >= maxPerLine;
15058
+ if (wouldBe > maxWidth || hitMax) {
15045
15059
  lines.push(currentLine);
15046
15060
  currentLine = pad + parts[i];
15047
15061
  currentWidth = indent + partWidth;
15062
+ partsOnLine = 1;
15048
15063
  } else {
15049
15064
  currentLine += separator + parts[i];
15050
15065
  currentWidth = wouldBe;
15066
+ partsOnLine++;
15051
15067
  }
15052
15068
  }
15053
15069
  lines.push(currentLine);
@@ -15137,18 +15153,21 @@ Configuration:
15137
15153
  continue;
15138
15154
  if (outputs.length === 1 && outputs[0].header && outputs[0].items?.length) {
15139
15155
  const { header, items } = outputs[0];
15140
- const rendered = wrapParts([header, ...items], " ", termWidth);
15156
+ const rendered = wrapParts([header, ...items], " ", termWidth, 2, 4);
15141
15157
  outputLines.push(rendered);
15142
15158
  } else {
15143
15159
  const parts = outputs.map((o) => o.text);
15144
15160
  outputLines.push(wrapParts(parts, sep, termWidth));
15145
15161
  }
15162
+ if (!config2.compact && config2.sectionSeparators !== false && (line.name === "mcp" || line.name === "hooks" || line.name === "skills")) {
15163
+ outputLines.push(`${theme.overlay0}---${theme.reset}`);
15164
+ }
15146
15165
  }
15147
15166
  if (config2.dividers) {
15148
15167
  const divider = `${theme.overlay0}${"─".repeat(termWidth)}${theme.reset}`;
15149
- console.log(outputLines.join(`
15168
+ console.log(`${outputLines.join(`
15150
15169
  ${divider}
15151
- `) + `
15170
+ `)}
15152
15171
  ${divider}`);
15153
15172
  } else {
15154
15173
  console.log(outputLines.join(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-pulse",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "A customizable, real-time statusline for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {