cc-pulse 1.3.1 → 1.4.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 +84 -41
  2. package/dist/cli.js +200 -106
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@ A real-time statusline for [Claude Code](https://docs.anthropic.com/en/docs/clau
7
7
 
8
8
  ![cc-pulse statusline](assets/demo.png)
9
9
 
10
- ## 🚀 Quick Start
10
+ ## Quick Start
11
11
 
12
12
  ```bash
13
13
  npm install -g cc-pulse
@@ -24,40 +24,79 @@ Add to `~/.claude/settings.json`:
24
24
  }
25
25
  ```
26
26
 
27
- Restart Claude Code — the statusline appears above the input area.
27
+ Restart Claude Code — the statusline appears below the input area.
28
28
 
29
- ## Features
29
+ ## Features
30
30
 
31
31
  | Feature | Description |
32
32
  |---------|-------------|
33
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.5", "Sonnet 3.5") |
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") |
36
36
  | **Cost tracking** | Session cost with color coding ($1 yellow, $2 orange, $5+ red) |
37
37
  | **MCP health** | Live connection status for all MCP servers |
38
38
  | **Hook monitoring** | Active hooks by event type, with broken path detection |
39
- | **Skills display** | Shows your custom slash commands/skills |
39
+ | **Skills display** | Adaptive display individual names, prefix grouping, or compact counts |
40
40
  | **Git status** | Branch name + new/modified/deleted file counts |
41
+ | **Responsive layout** | Width-aware wrapping + compact mode for smaller screens |
41
42
 
42
- ## 📊 What You Get
43
+ ## What You Get
43
44
 
44
45
  Six lines of information, updated on every message:
45
46
 
46
- ![cc-pulse statusline](assets/demo.png)
47
-
48
47
  | Line | Content |
49
48
  |------|---------|
50
49
  | **Identity** | Project name + working directory |
51
50
  | **Git** | Branch + file changes (new, modified, deleted) |
52
- | **Engine** | Tier, model, context used, tokens, cost, duration |
51
+ | **Engine** | Model, context used, tokens, cost, duration |
53
52
  | **MCP** | Server count + individual status (✓ connected, ✗ disconnected, ○ disabled) |
54
53
  | **Hooks** | Hook count by event type, with broken path warnings |
55
- | **Skills** | Custom slash commands count + names |
54
+ | **Skills** | Adaptive: names when few, prefix groups when many |
55
+
56
+ ## Responsive Display
57
+
58
+ The statusline adapts to your setup automatically:
56
59
 
57
- ## ⚙️ Configuration
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
+
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`
68
+
69
+ **Width-aware wrapping** — when a line exceeds terminal width, components wrap onto indented continuation lines instead of being cut off.
70
+
71
+ **Compact mode** — toggle with `/pulse-compact` or set in config:
72
+ ```json
73
+ {
74
+ "compact": true
75
+ }
76
+ ```
77
+
78
+ In compact mode, everything collapses to counts only:
79
+ - `⬢ MCP 3/4` | `⚡Hooks 8` | `✦ Skills 89` | `Used 30%` | `$2.50`
80
+
81
+ ## Configuration
58
82
 
59
83
  Create `~/.config/claude-pulse/config.json` to customize. Only include what you want to change.
60
84
 
85
+ <details>
86
+ <summary><strong>Compact Mode</strong></summary>
87
+
88
+ Minimal display showing only counts and essential info. Toggle with the `/pulse-compact` slash command, or set manually:
89
+
90
+ ```json
91
+ {
92
+ "compact": true
93
+ }
94
+ ```
95
+
96
+ When enabled: skills, hooks, and MCP show counts only; context hides token breakdown; cost hides burn rate; CWD shortens.
97
+
98
+ </details>
99
+
61
100
  <details>
62
101
  <summary><strong>Context Window</strong></summary>
63
102
 
@@ -77,8 +116,8 @@ Shows how much of the context window is used. Colors shift as usage increases.
77
116
 
78
117
  | Style | Example |
79
118
  |-------|---------|
80
- | `bar` (default) | `Used ●●●●●●○○○○ 58%` |
81
- | `percent` | `Used 58%` |
119
+ | `bar` | `Used ●●●●●●○○○○ 58%` |
120
+ | `percent` (default) | `Used 58%` |
82
121
  | `detailed` | `Used 116.0k/200.0k (58%)` |
83
122
  | `both` | `●●●●●●○○○○ 116.0k / 200.0k` |
84
123
 
@@ -86,26 +125,7 @@ Shows how much of the context window is used. Colors shift as usage increases.
86
125
  - **Green**: < 70% used (safe)
87
126
  - **Yellow**: 70% used (warn)
88
127
  - **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"`
128
+ - **Red**: 95% used (danger)
109
129
 
110
130
  </details>
111
131
 
@@ -159,6 +179,8 @@ Options: `"pro"`, `"max"`, `"api"`
159
179
  | `showNames: false` | `⚡Hooks 8 Submit:3 Post:2 End:1` |
160
180
  | Both `false` | `⚡Hooks 8` |
161
181
 
182
+ When you have many hooks (>6), names are automatically capped to 3 per event group with a `+N` overflow indicator.
183
+
162
184
  Broken hooks (invalid paths) show in red with ▲.
163
185
 
164
186
  </details>
@@ -198,16 +220,27 @@ Shows your custom slash commands from `~/.claude/skills/` and `.claude/skills/`.
198
220
  }
199
221
  ```
200
222
 
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 |
223
+ The display adapts automatically based on how many skills you have — see [Responsive Display](#responsive-display) above.
206
224
 
207
225
  Broken skills (missing SKILL.md or invalid frontmatter) show in red with ▲.
208
226
 
209
227
  </details>
210
228
 
229
+ <details>
230
+ <summary><strong>Dividers</strong></summary>
231
+
232
+ Add horizontal line separators between status sections:
233
+
234
+ ```json
235
+ {
236
+ "dividers": true
237
+ }
238
+ ```
239
+
240
+ Off by default.
241
+
242
+ </details>
243
+
211
244
  <details>
212
245
  <summary><strong>Layout</strong></summary>
213
246
 
@@ -238,7 +271,7 @@ The 6-line structure is fixed. You can toggle lines and change separators:
238
271
 
239
272
  | Component | Key Options |
240
273
  |-----------|-------------|
241
- | `model` | `showIcon: true` adds emoji per model |
274
+ | `model` | `showIcon: true` (default), custom `icons: { opus, sonnet, haiku }` |
242
275
  | `session` | `showDuration: true`, `showId: false` |
243
276
  | `cache` | Shows cache hit rate |
244
277
  | `linesChanged` | Shows `+added -removed` |
@@ -248,7 +281,17 @@ All components accept `"enabled": false` to hide them.
248
281
 
249
282
  </details>
250
283
 
251
- ## 🛠️ Development
284
+ ## Slash Commands
285
+
286
+ cc-pulse ships with a skill you can install to your Claude Code skills directory:
287
+
288
+ | Command | Description |
289
+ |---------|-------------|
290
+ | `/pulse-compact` | Toggle compact mode on/off |
291
+
292
+ To install, copy `skills/pulse-compact/` to `~/.claude/skills/` or your project's `.claude/skills/`.
293
+
294
+ ## Development
252
295
 
253
296
  ```bash
254
297
  git clone https://github.com/ali-nr/claude-pulse.git
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.1",
19
+ version: "1.4.0",
16
20
  description: "A customizable, real-time statusline for Claude Code",
17
21
  type: "module",
18
22
  bin: {
@@ -383,16 +387,18 @@ function getGitInfo() {
383
387
  let status = "";
384
388
  const statusLines = { added: 0, modified: 0, deleted: 0 };
385
389
  try {
386
- branch = execSync("git branch --show-current 2>/dev/null", {
390
+ branch = execSync("git branch --show-current", {
387
391
  encoding: "utf-8",
388
- timeout: 1000
392
+ timeout: 1000,
393
+ stdio: ["pipe", "pipe", "ignore"]
389
394
  }).trim();
390
395
  } catch {}
391
396
  if (branch) {
392
397
  try {
393
- const porcelain = execSync("git status --porcelain 2>/dev/null", {
398
+ const porcelain = execSync("git status --porcelain", {
394
399
  encoding: "utf-8",
395
- timeout: 1000
400
+ timeout: 1000,
401
+ stdio: ["pipe", "pipe", "ignore"]
396
402
  });
397
403
  const lines = porcelain.trim().split(`
398
404
  `).filter(Boolean);
@@ -449,15 +455,27 @@ function renderHooks(config, theme) {
449
455
  const text2 = `${theme.yellow}⚡${hookLabel} ${summary.total}${theme.reset}`;
450
456
  return { text: text2 };
451
457
  }
452
- 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 = Object.entries(summary.events).map(([event, detail]) => {
453
462
  const label = EVENT_LABELS[event] ?? event;
454
- const goodNames = showNames && detail.names.length > 0 ? ` ${theme.flamingo}${detail.names.join(",")}${theme.reset}` : "";
455
- const brokenNames = detail.broken.length > 0 ? ` ${theme.red}${detail.broken.join(",")} ▲${theme.reset}` : "";
463
+ const brokenStr = detail.broken.length > 0 ? ` ${theme.red}${detail.broken.join(",")} ▲${theme.reset}` : "";
456
464
  const countStr = showCount ? `${theme.peach}${detail.count}${theme.reset}` : "";
457
- return `${theme.lavender}${label}:${theme.reset}${countStr}${goodNames}${brokenNames}`;
465
+ let goodNames = "";
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
+ goodNames = ` ${theme.flamingo}${displayed.join(",")}${theme.reset}`;
471
+ if (overflow > 0) {
472
+ goodNames += ` ${theme.overlay0}+${overflow}${theme.reset}`;
473
+ }
474
+ }
475
+ return `${theme.lavender}${label}:${theme.reset}${countStr}${goodNames}${brokenStr}`;
458
476
  });
459
- const text = `${theme.yellow}⚡${hookLabel} ${summary.total}${theme.reset} ${eventParts.join(" ")}`;
460
- return { text };
477
+ const text = items.length > 0 ? `${header} ${items.join(" ")}` : header;
478
+ return { text, header, items };
461
479
  }
462
480
  function getHooksSummary() {
463
481
  const events = {};
@@ -588,7 +606,7 @@ function renderMcp(config, theme) {
588
606
  }
589
607
  const displayServers = servers.slice(0, maxDisplay);
590
608
  const remaining = servers.length - maxDisplay;
591
- const serverParts = displayServers.map((server) => {
609
+ const serverItems = displayServers.map((server) => {
592
610
  let icon;
593
611
  let color;
594
612
  switch (server.status) {
@@ -610,17 +628,17 @@ function renderMcp(config, theme) {
610
628
  }
611
629
  return `${color}${server.name} ${icon}${theme.reset}`;
612
630
  });
613
- let serverStr = serverParts.join(" ");
614
631
  if (remaining > 0) {
615
- serverStr += ` ${theme.sky}+${remaining} more${theme.reset}`;
632
+ serverItems.push(`${theme.sky}+${remaining} more${theme.reset}`);
616
633
  }
617
634
  const countStr = `${connectedCount}/${servers.length}`;
635
+ const header = `${theme.sky}${label} ${theme.green}${countStr}${theme.reset}:`;
618
636
  if (config.showNames === false) {
619
637
  const text2 = `${theme.sky}${label} ${theme.green}${countStr}${theme.reset}`;
620
638
  return { text: text2, action: "/mcp" };
621
639
  }
622
- const text = `${theme.sky}${label} ${theme.green}${countStr}${theme.reset}: ${serverStr}`;
623
- return { text, action: "/mcp" };
640
+ const text = `${header} ${serverItems.join(" ")}`;
641
+ return { text, header, items: serverItems, action: "/mcp" };
624
642
  }
625
643
  function getMcpServers() {
626
644
  const state = loadState();
@@ -631,9 +649,10 @@ function getMcpServers() {
631
649
  servers = state.mcp.servers.map((s) => ({ ...s }));
632
650
  } else {
633
651
  try {
634
- const output = execSync2("claude mcp list 2>/dev/null", {
652
+ const output = execSync2("claude mcp list", {
635
653
  encoding: "utf-8",
636
- timeout: 3000
654
+ timeout: 3000,
655
+ stdio: ["pipe", "pipe", "ignore"]
637
656
  });
638
657
  servers = parseMcpOutput(output);
639
658
  saveState({
@@ -715,7 +734,7 @@ function parseMcpOutput(output) {
715
734
  }
716
735
  // src/components/model.ts
717
736
  function parseModelId(modelId) {
718
- const id = modelId.toLowerCase();
737
+ const id = modelId.toLowerCase().replace(/\[.*\]$/, "");
719
738
  const newFormat = id.match(/^claude-(\w+)-(\d+)-(\d+)-\d+$/);
720
739
  if (newFormat) {
721
740
  const [, family, major, minor] = newFormat;
@@ -726,6 +745,16 @@ function parseModelId(modelId) {
726
745
  const [, family, major] = newFormatNoMinor;
727
746
  return { family, version: major };
728
747
  }
748
+ const shortWithMinor = id.match(/^claude-(\w+)-(\d+)-(\d+)$/);
749
+ if (shortWithMinor) {
750
+ const [, family, major, minor] = shortWithMinor;
751
+ return { family, version: `${major}.${minor}` };
752
+ }
753
+ const shortNoMinor = id.match(/^claude-(\w+)-(\d+)$/);
754
+ if (shortNoMinor) {
755
+ const [, family, major] = shortNoMinor;
756
+ return { family, version: major };
757
+ }
729
758
  const oldFormat = id.match(/^claude-(\d+)-(\d+)-(\w+)-\d+$/);
730
759
  if (oldFormat) {
731
760
  const [, major, minor, family] = oldFormat;
@@ -744,7 +773,7 @@ function renderModel(input, config, theme) {
744
773
  }
745
774
  const modelId = input.model?.id ?? "";
746
775
  const displayName = input.model?.display_name ?? "";
747
- const icons = config.icons ?? { opus: "\uD83E\uDDE0", sonnet: "\uD83C\uDFB5", haiku: "" };
776
+ const icons = config.icons ?? { opus: "", sonnet: "", haiku: "" };
748
777
  const showIcon = config.showIcon !== false;
749
778
  let icon = "\uD83E\uDD16";
750
779
  let color = theme.text;
@@ -852,22 +881,65 @@ function renderSkills(config, theme) {
852
881
  }
853
882
  const validNames = summary.skills.filter((s) => s.valid).map((s) => s.name ?? s.folder).slice(0, maxDisplay);
854
883
  const brokenNames = summary.skills.filter((s) => !s.valid).map((s) => s.folder);
855
- const parts = [];
856
- if (showCount) {
857
- parts.push(`${theme.mauve}${summary.valid}${theme.reset}`);
858
- }
884
+ const countStr = showCount ? ` ${theme.mauve}${summary.valid}${theme.reset}` : "";
885
+ const header = `${theme.mauve}✦ ${label}${theme.reset}${countStr}`;
886
+ const items = [];
887
+ const GROUP_THRESHOLD = 10;
888
+ const MIN_PREFIX_COUNT = 3;
859
889
  if (showNames && validNames.length > 0) {
860
- const displayNames = validNames.join(",");
861
- const overflow = summary.valid > maxDisplay ? `+${summary.valid - maxDisplay}` : "";
862
- parts.push(`${theme.flamingo}${displayNames}${overflow ? ` ${theme.overlay0}${overflow}` : ""}${theme.reset}`);
890
+ if (validNames.length > GROUP_THRESHOLD) {
891
+ const { groups, ungrouped } = groupByPrefix(validNames, MIN_PREFIX_COUNT);
892
+ const hasGroups = Object.keys(groups).length > 0;
893
+ if (hasGroups) {
894
+ for (const [prefix, names] of Object.entries(groups).sort((a, b) => b[1].length - a[1].length)) {
895
+ items.push(`${theme.overlay1}${prefix}:${theme.peach}${names.length}${theme.reset}`);
896
+ }
897
+ if (ungrouped.length > 0) {
898
+ items.push(`${theme.flamingo}${ungrouped.join(",")}${theme.reset}`);
899
+ }
900
+ } else {
901
+ const CAP = 10;
902
+ const capped = validNames.slice(0, CAP);
903
+ items.push(`${theme.flamingo}${capped.join(",")}${theme.reset}`);
904
+ if (validNames.length > CAP) {
905
+ items.push(`${theme.overlay0}+${validNames.length - CAP}${theme.reset}`);
906
+ }
907
+ }
908
+ } else {
909
+ items.push(`${theme.flamingo}${validNames.join(",")}${theme.reset}`);
910
+ }
911
+ const overflow = summary.valid > maxDisplay ? summary.valid - maxDisplay : 0;
912
+ if (overflow > 0) {
913
+ items.push(`${theme.overlay0}+${overflow}${theme.reset}`);
914
+ }
863
915
  }
864
- let brokenStr = "";
865
916
  if (summary.broken > 0) {
866
- const brokenDisplay = brokenNames.length <= 2 ? brokenNames.join(",") : `${summary.broken} broken`;
867
- brokenStr = ` ${theme.red}▲${brokenDisplay}${theme.reset}`;
917
+ for (const name of brokenNames) {
918
+ items.push(`${theme.red}▲${name}${theme.reset}`);
919
+ }
920
+ }
921
+ const text = items.length > 0 ? `${header} ${items.join(" ")}` : header;
922
+ return { text, header, items };
923
+ }
924
+ function groupByPrefix(names, minCount) {
925
+ const buckets = {};
926
+ for (const name of names) {
927
+ const sep = name.includes(":") ? ":" : "-";
928
+ const prefix = name.split(sep)[0];
929
+ if (!buckets[prefix])
930
+ buckets[prefix] = [];
931
+ buckets[prefix].push(name);
932
+ }
933
+ const groups = {};
934
+ const ungrouped = [];
935
+ for (const [prefix, members] of Object.entries(buckets)) {
936
+ if (members.length >= minCount) {
937
+ groups[prefix] = members;
938
+ } else {
939
+ ungrouped.push(...members);
940
+ }
868
941
  }
869
- const text = `${theme.mauve}✦ ${label}${theme.reset}${parts.length ? ` ${parts.join(" ")}` : ""}${brokenStr}`;
870
- return { text };
942
+ return { groups, ungrouped };
871
943
  }
872
944
  function getSkillsSummary() {
873
945
  const skills = [];
@@ -996,58 +1068,6 @@ function renderSystem(input, config, theme) {
996
1068
  const text = parts.join(" │ ");
997
1069
  return { text };
998
1070
  }
999
- // src/components/tier.ts
1000
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1001
- import { homedir as homedir6 } from "node:os";
1002
- import { join as join5 } from "node:path";
1003
- var cachedTier = null;
1004
- var cacheTime = 0;
1005
- var CACHE_TTL3 = 30000;
1006
- function renderTier(config, theme) {
1007
- if (config.enabled === false) {
1008
- return { text: "" };
1009
- }
1010
- const labels = config.labels ?? { pro: "PRO", max: "MAX", api: "API" };
1011
- const tier = config.override ?? detectTier();
1012
- let color = theme.blue;
1013
- let label = labels.pro;
1014
- if (tier === "max") {
1015
- color = theme.mauve;
1016
- label = labels.max;
1017
- } else if (tier === "api") {
1018
- color = theme.green;
1019
- label = labels.api;
1020
- }
1021
- const icon = "◆ ";
1022
- const text = `${color}${theme.bold}${icon}${label}${theme.reset}`;
1023
- return { text, action: "/usage" };
1024
- }
1025
- function detectTier() {
1026
- const now = Date.now();
1027
- if (cachedTier && now - cacheTime < CACHE_TTL3) {
1028
- return cachedTier;
1029
- }
1030
- const claudeJsonPath = join5(homedir6(), ".claude.json");
1031
- try {
1032
- if (existsSync5(claudeJsonPath)) {
1033
- const content = readFileSync5(claudeJsonPath, "utf-8");
1034
- const data = JSON.parse(content);
1035
- if (data.oauthAccount?.hasExtraUsageEnabled) {
1036
- cachedTier = "max";
1037
- } else if (data.oauthAccount) {
1038
- cachedTier = "pro";
1039
- } else {
1040
- cachedTier = "api";
1041
- }
1042
- } else {
1043
- cachedTier = "api";
1044
- }
1045
- } catch {
1046
- cachedTier = "pro";
1047
- }
1048
- cacheTime = now;
1049
- return cachedTier;
1050
- }
1051
1071
  // src/components/time.ts
1052
1072
  function renderTime(config, theme) {
1053
1073
  if (config.enabled === false) {
@@ -1081,12 +1101,12 @@ function renderTime(config, theme) {
1081
1101
  return { text };
1082
1102
  }
1083
1103
  // src/config.ts
1084
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "node:fs";
1085
- import { homedir as homedir7 } from "node:os";
1086
- import { join as join6 } from "node:path";
1104
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1105
+ import { homedir as homedir6 } from "node:os";
1106
+ import { join as join5 } from "node:path";
1087
1107
  var CONFIG_PATHS = [
1088
- join6(homedir7(), ".config", "claude-pulse", "config.json"),
1089
- join6(homedir7(), ".claude-pulse.json")
1108
+ join5(homedir6(), ".config", "claude-pulse", "config.json"),
1109
+ join5(homedir6(), ".claude-pulse.json")
1090
1110
  ];
1091
1111
  var FIXED_LINES = [
1092
1112
  { name: "identity", enabled: true, components: ["name", "cwd"], separator: " " },
@@ -1094,7 +1114,7 @@ var FIXED_LINES = [
1094
1114
  {
1095
1115
  name: "engine",
1096
1116
  enabled: true,
1097
- components: ["tier", "model", "context", "cost", "session"],
1117
+ components: ["model", "context", "cost", "session"],
1098
1118
  separator: " │ "
1099
1119
  },
1100
1120
  { name: "mcp", enabled: true, components: ["mcp"], separator: " │ " },
@@ -1104,13 +1124,9 @@ var FIXED_LINES = [
1104
1124
  var DEFAULT_CONFIG = {
1105
1125
  theme: "catppuccin",
1106
1126
  components: {
1107
- tier: {
1108
- enabled: true,
1109
- labels: { pro: "PRO", max: "MAX", api: "API" }
1110
- },
1111
1127
  model: {
1112
1128
  enabled: true,
1113
- showIcon: false
1129
+ showIcon: true
1114
1130
  },
1115
1131
  context: {
1116
1132
  enabled: true,
@@ -1179,9 +1195,9 @@ var DEFAULT_CONFIG = {
1179
1195
  };
1180
1196
  function loadConfig() {
1181
1197
  for (const configPath of CONFIG_PATHS) {
1182
- if (existsSync6(configPath)) {
1198
+ if (existsSync5(configPath)) {
1183
1199
  try {
1184
- const content = readFileSync6(configPath, "utf-8");
1200
+ const content = readFileSync5(configPath, "utf-8");
1185
1201
  const userConfig = JSON.parse(content);
1186
1202
  return mergeConfig(DEFAULT_CONFIG, userConfig);
1187
1203
  } catch {}
@@ -1208,6 +1224,10 @@ function mergeConfig(target, source) {
1208
1224
  const result = { ...target };
1209
1225
  if (source.theme !== undefined)
1210
1226
  result.theme = source.theme;
1227
+ if (source.compact !== undefined)
1228
+ result.compact = source.compact;
1229
+ if (source.dividers !== undefined)
1230
+ result.dividers = source.dividers;
1211
1231
  if (source.lines !== undefined)
1212
1232
  result.lines = source.lines;
1213
1233
  if (source.interactive !== undefined)
@@ -14802,7 +14822,9 @@ var McpServerSchema = exports_external.object({
14802
14822
  });
14803
14823
  var ComponentOutputSchema = exports_external.object({
14804
14824
  text: exports_external.string(),
14805
- action: exports_external.string().optional()
14825
+ action: exports_external.string().optional(),
14826
+ header: exports_external.string().optional(),
14827
+ items: exports_external.array(exports_external.string()).optional()
14806
14828
  });
14807
14829
  var TierConfigSchema = exports_external.object({
14808
14830
  enabled: exports_external.boolean().optional(),
@@ -14949,6 +14971,8 @@ var LinesConfigSchema = exports_external.object({
14949
14971
  });
14950
14972
  var PulseConfigSchema = exports_external.object({
14951
14973
  theme: exports_external.string(),
14974
+ compact: exports_external.boolean().optional(),
14975
+ dividers: exports_external.boolean().optional(),
14952
14976
  lines: LinesConfigSchema.optional(),
14953
14977
  components: ComponentConfigsSchema,
14954
14978
  interactive: exports_external.object({
@@ -15001,6 +15025,39 @@ var catppuccin = {
15001
15025
  dim: "\x1B[2m"
15002
15026
  };
15003
15027
 
15028
+ // src/truncate.ts
15029
+ var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
15030
+ function visibleLength(str) {
15031
+ return str.replace(ANSI_RE, "").length;
15032
+ }
15033
+ function wrapParts(parts, separator, maxWidth, indent = 2) {
15034
+ if (parts.length === 0)
15035
+ return "";
15036
+ const sepWidth = visibleLength(separator);
15037
+ const pad = " ".repeat(indent);
15038
+ const lines = [];
15039
+ let currentLine = parts[0];
15040
+ let currentWidth = visibleLength(parts[0]);
15041
+ for (let i = 1;i < parts.length; i++) {
15042
+ const partWidth = visibleLength(parts[i]);
15043
+ const wouldBe = currentWidth + sepWidth + partWidth;
15044
+ if (wouldBe > maxWidth) {
15045
+ lines.push(currentLine);
15046
+ currentLine = pad + parts[i];
15047
+ currentWidth = indent + partWidth;
15048
+ } else {
15049
+ currentLine += separator + parts[i];
15050
+ currentWidth = wouldBe;
15051
+ }
15052
+ }
15053
+ lines.push(currentLine);
15054
+ return lines.join(`
15055
+ `);
15056
+ }
15057
+ function getTerminalWidth() {
15058
+ return process.stdout.columns || Number(process.env.COLUMNS) || 80;
15059
+ }
15060
+
15004
15061
  // src/cli.ts
15005
15062
  var VERSION = package_default.version ?? "1.0.0";
15006
15063
  async function main() {
@@ -15039,30 +15096,67 @@ Configuration:
15039
15096
  const config2 = loadConfig();
15040
15097
  const theme = catppuccin;
15041
15098
  const lines = getLines(config2);
15099
+ const termWidth = getTerminalWidth();
15100
+ if (config2.compact) {
15101
+ if (config2.components.skills) {
15102
+ config2.components.skills.showNames = false;
15103
+ }
15104
+ if (config2.components.hooks) {
15105
+ config2.components.hooks.showNames = false;
15106
+ config2.components.hooks.showCount = false;
15107
+ }
15108
+ if (config2.components.mcp) {
15109
+ config2.components.mcp.showNames = false;
15110
+ }
15111
+ if (config2.components.context) {
15112
+ config2.components.context.showTokens = false;
15113
+ config2.components.context.showRate = false;
15114
+ }
15115
+ if (config2.components.cost) {
15116
+ config2.components.cost.showBurnRate = false;
15117
+ config2.components.cost.showProjection = false;
15118
+ }
15119
+ if (config2.components.cwd) {
15120
+ config2.components.cwd.maxLength = 15;
15121
+ }
15122
+ }
15042
15123
  const outputLines = [];
15043
15124
  for (const line of lines) {
15044
15125
  if (!line.enabled)
15045
15126
  continue;
15046
- const parts = [];
15047
15127
  const separator = ` ${theme.overlay2}│${theme.reset} `;
15048
15128
  const sep = line.separator ?? separator;
15129
+ const outputs = [];
15049
15130
  for (const componentName of line.components) {
15050
15131
  const output = renderComponent(componentName, input, config2, theme);
15051
15132
  if (output.text) {
15052
- parts.push(output.text);
15133
+ outputs.push(output);
15053
15134
  }
15054
15135
  }
15055
- if (parts.length > 0) {
15056
- outputLines.push(parts.join(sep));
15136
+ if (outputs.length === 0)
15137
+ continue;
15138
+ if (outputs.length === 1 && outputs[0].header && outputs[0].items?.length) {
15139
+ const { header, items } = outputs[0];
15140
+ const rendered = wrapParts([header, ...items], " ", termWidth);
15141
+ outputLines.push(rendered);
15142
+ } else {
15143
+ const parts = outputs.map((o) => o.text);
15144
+ outputLines.push(wrapParts(parts, sep, termWidth));
15057
15145
  }
15058
15146
  }
15059
- console.log(outputLines.join(`
15147
+ if (config2.dividers) {
15148
+ const divider = `${theme.overlay0}${"─".repeat(termWidth)}${theme.reset}`;
15149
+ console.log(outputLines.join(`
15150
+ ${divider}
15151
+ `) + `
15152
+ ${divider}`);
15153
+ } else {
15154
+ console.log(outputLines.join(`
15060
15155
  `));
15156
+ }
15061
15157
  }
15062
15158
  function renderComponent(name, input, config2, theme) {
15063
15159
  switch (name) {
15064
- case "tier":
15065
- return renderTier(config2.components.tier ?? {}, theme);
15066
15160
  case "model":
15067
15161
  return renderModel(input, config2.components.model ?? {}, theme);
15068
15162
  case "context":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-pulse",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "A customizable, real-time statusline for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {