cc-pulse 1.0.2 → 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.
Files changed (3) hide show
  1. package/README.md +95 -96
  2. package/dist/cli.js +225 -50
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,38 +1,19 @@
1
- # claude-pulse
1
+ # cc-pulse
2
2
 
3
- A real-time statusline for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
4
-
5
- ![claude-pulse statusline](assets/demo.png)
6
-
7
- ## Why
8
-
9
- - **Context & cost** — context window %, input/output/cache token breakdown, cost with burn rate, session duration
10
- - **MCP server health** — connection status for every server: connected, disconnected, disabled, or erroring
11
- - **Hook monitoring** — all hooks by event type, with broken path detection
12
- - **Git at a glance** — branch, new/modified/deleted file counts
13
- - **Fully customizable** — every component is independently configurable with multiple display styles
14
-
15
- ## What You Get
3
+ [![npm version](https://img.shields.io/npm/v/cc-pulse)](https://www.npmjs.com/package/cc-pulse)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
16
5
 
17
- Five lines of information, updated on every message:
18
-
19
- | Line | What it shows |
20
- |------|---------------|
21
- | **Identity** | Project name + working directory |
22
- | **Git** | Current branch + file changes (new, modified, deleted) |
23
- | **Engine** | Model, remaining % until compaction, token cost, session duration |
24
- | **MCP** | Server connections with health status (connected, disconnected, disabled, error) |
25
- | **Hooks** | Active hooks by event type, with broken path detection |
6
+ A real-time statusline for [Claude Code](https://docs.anthropic.com/en/docs/claude-code).
26
7
 
27
- Context goes from green to red as it approaches compaction. Cost goes from green to red. Failed MCP servers and broken hooks are highlighted immediately.
8
+ ![cc-pulse statusline](assets/demo.png)
28
9
 
29
- ## Install
10
+ ## 🚀 Quick Start
30
11
 
31
12
  ```bash
32
13
  npm install -g cc-pulse
33
14
  ```
34
15
 
35
- Add to your Claude Code settings (`~/.claude/settings.json`):
16
+ Add to `~/.claude/settings.json`:
36
17
 
37
18
  ```json
38
19
  {
@@ -43,104 +24,115 @@ Add to your Claude Code settings (`~/.claude/settings.json`):
43
24
  }
44
25
  ```
45
26
 
46
- Restart Claude Code. The statusline appears above the input area.
27
+ Restart Claude Code the statusline appears above the input area.
47
28
 
48
- <details>
49
- <summary><strong>Install from source</strong></summary>
29
+ ## ✨ Features
50
30
 
51
- ```bash
52
- git clone https://github.com/ali-nr/claude-pulse.git
53
- cd claude-pulse
54
- bun install
55
- bun run build
56
- ```
31
+ | Feature | Description |
32
+ |---------|-------------|
33
+ | **→Compact indicator** | Shows remaining % until auto-compaction — colors shift from green to red as you approach the limit |
34
+ | **Token breakdown** | Input ↓, output ↑, and cache ⟳ tokens at a glance |
35
+ | **Cost tracking** | Session cost with color coding ($1 yellow, $2 orange, $5+ red) |
36
+ | **MCP health** | Live connection status for all MCP servers |
37
+ | **Hook monitoring** | Active hooks by event type, with broken path detection |
38
+ | **Git status** | Branch name + new/modified/deleted file counts |
57
39
 
58
- Use the full path in settings: `"command": "node /path/to/claude-pulse/dist/cli.js"`
40
+ ## 📊 What You Get
59
41
 
60
- </details>
42
+ Five lines of information, updated on every message:
43
+
44
+ ![cc-pulse statusline](assets/demo.png)
61
45
 
62
- ## Customize
46
+ | Line | Content |
47
+ |------|---------|
48
+ | **Identity** | Project name + working directory |
49
+ | **Git** | Branch + file changes (new, modified, deleted) |
50
+ | **Engine** | Tier, model, context remaining, tokens, cost, duration |
51
+ | **MCP** | Server count + individual status (✓ connected, ✗ disconnected, ○ disabled) |
52
+ | **Hooks** | Hook count by event type, with broken path warnings |
63
53
 
64
- Create `~/.config/claude-pulse/config.json` to override defaults. You only need to include what you want to change.
54
+ ## ⚙️ Configuration
55
+
56
+ Create `~/.config/claude-pulse/config.json` to customize. Only include what you want to change.
65
57
 
66
58
  <details>
67
- <summary><strong>Layout</strong></summary>
59
+ <summary><strong>Context Window</strong></summary>
68
60
 
69
- The 5-line structure is fixed. You can enable/disable lines and change separators:
61
+ The `→Compact` indicator shows remaining space until auto-compaction. When it reaches 0%, Claude compacts the conversation.
70
62
 
71
63
  ```json
72
64
  {
73
- "lines": {
74
- "hooks": { "enabled": false },
75
- "engine": { "separator": " | " }
65
+ "components": {
66
+ "context": {
67
+ "style": "bar",
68
+ "showTokens": true,
69
+ "thresholds": { "warn": 70, "critical": 85, "danger": 95 }
70
+ }
76
71
  }
77
72
  }
78
73
  ```
79
74
 
80
- | Line | Key | Can toggle |
81
- |------|-----|------------|
82
- | Identity | | No (fixed branding) |
83
- | Git | `git` | Yes |
84
- | Engine | `engine` | Yes |
85
- | MCP | `mcp` | Yes |
86
- | Hooks | `hooks` | Yes |
75
+ | Style | Example |
76
+ |-------|---------|
77
+ | `bar` (default) | `→Compact ●●●●●●○○○○ 58%` |
78
+ | `compact` | `→Compact 58%` |
79
+ | `detailed` | `→Compact 116.0k/200.0k (58%)` |
80
+ | `both` | `●●●●●●○○○○ free:116.0k used:84.0k` |
81
+
82
+ **Color thresholds** — as remaining % drops:
83
+ - **Green**: > 30% remaining (safe)
84
+ - **Yellow**: 30% remaining (warn)
85
+ - **Orange**: 15% remaining (critical)
86
+ - **Red + 🔴**: 5% remaining (danger)
87
87
 
88
88
  </details>
89
89
 
90
90
  <details>
91
- <summary><strong>Context window</strong></summary>
91
+ <summary><strong>Subscription Tier</strong></summary>
92
92
 
93
- Shows remaining space until auto-compaction triggers. When it reaches 0%, Claude will compact the conversation.
93
+ Set your plan manually (auto-detection isn't reliable):
94
94
 
95
95
  ```json
96
96
  {
97
97
  "components": {
98
- "context": {
99
- "style": "compact",
100
- "showTokens": true,
101
- "showRate": false,
102
- "thresholds": { "warn": 70, "critical": 85, "danger": 95 }
98
+ "tier": {
99
+ "override": "max"
103
100
  }
104
101
  }
105
102
  }
106
103
  ```
107
104
 
108
- | Style | Example |
109
- |-------|---------|
110
- | `compact` | `→Compact 58%` |
111
- | `bar` | `●●●●●●○○○○ 58%` |
112
- | `detailed` | `116.0k/200.0k (58%)` |
113
- | `both` | `●●●●●●○○○○ free:116.0k used:84.0k` |
114
-
115
- Enable `showTokens` to see `↓input ↑output ⟳cache` breakdown.
105
+ Options: `"pro"`, `"max"`, `"api"`
116
106
 
117
107
  </details>
118
108
 
119
109
  <details>
120
- <summary><strong>MCP servers</strong></summary>
110
+ <summary><strong>MCP Servers</strong></summary>
121
111
 
122
112
  ```json
123
113
  {
124
114
  "components": {
125
115
  "mcp": {
126
116
  "showNames": true,
127
- "showOnlyProblems": true,
117
+ "showOnlyProblems": false,
128
118
  "maxDisplay": 4
129
119
  }
130
120
  }
131
121
  }
132
122
  ```
133
123
 
134
- - `showNames: true` list each server with status icon
135
- - `showOnlyProblems: true` — hide MCP line when everything is healthy
136
- - Failed/disconnected servers always show in red
124
+ | Option | Effect |
125
+ |--------|--------|
126
+ | `showNames: true` | List each server with status |
127
+ | `showOnlyProblems: true` | Hide line when all servers healthy |
128
+ | `maxDisplay: 4` | Limit servers shown ("+N more" for rest) |
137
129
 
138
130
  | Icon | Status |
139
131
  |------|--------|
140
- | `✓` | Connected |
141
- | `✗` | Disconnected |
142
- | `○` | Disabled |
143
- | `▲` | Error |
132
+ | | Connected |
133
+ | | Disconnected (red) |
134
+ | | Disabled |
135
+ | | Error |
144
136
 
145
137
  </details>
146
138
 
@@ -160,11 +152,11 @@ Enable `showTokens` to see `↓input ↑output ⟳cache` breakdown.
160
152
 
161
153
  | Setting | Result |
162
154
  |---------|--------|
163
- | Both `true` (default) | `⚡Hooks 8 Submit:3 timezone-context,best-practices Post:2 lint-check` |
164
- | `showNames: false` | `⚡Hooks 8 Submit:3 Post:2 Start:2 End:1` |
155
+ | Both `true` | `⚡Hooks 8 Submit:3 timezone-context,best-practices` |
156
+ | `showNames: false` | `⚡Hooks 8 Submit:3 Post:2 End:1` |
165
157
  | Both `false` | `⚡Hooks 8` |
166
158
 
167
- Broken hooks (invalid file paths) always show in red with `▲`.
159
+ Broken hooks (invalid paths) show in red with ▲.
168
160
 
169
161
  </details>
170
162
 
@@ -182,54 +174,61 @@ Broken hooks (invalid file paths) always show in red with `▲`.
182
174
  }
183
175
  ```
184
176
 
185
- Colors change automatically: green < $1, yellow $1-$2, peach $2-$5, red > $5. Burn rate appears after the session is longer than 1 minute.
177
+ Color thresholds: green < $1, yellow $1-$2, orange $2-$5, red > $5
186
178
 
187
179
  </details>
188
180
 
189
181
  <details>
190
- <summary><strong>Subscription tier</strong></summary>
182
+ <summary><strong>Layout</strong></summary>
191
183
 
192
- Disabled by default. There's no official way to detect your plan, so set it manually:
184
+ The 5-line structure is fixed. You can toggle lines and change separators:
193
185
 
194
186
  ```json
195
187
  {
196
- "components": {
197
- "tier": {
198
- "enabled": true,
199
- "override": "max"
200
- }
188
+ "lines": {
189
+ "hooks": { "enabled": false },
190
+ "engine": { "separator": " | " }
201
191
  }
202
192
  }
203
193
  ```
204
194
 
205
- Options: `"pro"`, `"max"`, `"api"`.
195
+ | Line | Key | Toggleable |
196
+ |------|-----|------------|
197
+ | Identity | — | No (branding) |
198
+ | Git | `git` | Yes |
199
+ | Engine | `engine` | Yes |
200
+ | MCP | `mcp` | Yes |
201
+ | Hooks | `hooks` | Yes |
206
202
 
207
203
  </details>
208
204
 
209
205
  <details>
210
- <summary><strong>Other components</strong></summary>
206
+ <summary><strong>All Components</strong></summary>
211
207
 
212
208
  | Component | Key Options |
213
209
  |-----------|-------------|
214
210
  | `model` | `showIcon: true` adds emoji per model |
215
211
  | `session` | `showDuration: true`, `showId: false` |
216
- | `cache` | Shows cache hit rate percentage |
217
- | `linesChanged` | Shows `+added -removed` lines changed |
218
- | `time` | `format: "12h"` or `"24h"`, `showTimezone: true` |
212
+ | `cache` | Shows cache hit rate |
213
+ | `linesChanged` | Shows `+added -removed` |
214
+ | `time` | `format: "12h"/"24h"`, `showTimezone: true` |
219
215
 
220
216
  All components accept `"enabled": false` to hide them.
221
217
 
222
218
  </details>
223
219
 
224
- ## Development
220
+ ## 🛠️ Development
225
221
 
226
222
  ```bash
227
- bun install # Install dependencies
228
- bun run build # Build to dist/
229
- bun run lint # Run Biome linter
230
- bun run dev # Watch mode
223
+ git clone https://github.com/ali-nr/claude-pulse.git
224
+ cd claude-pulse
225
+ bun install
226
+ bun run build
227
+ bun test
231
228
  ```
232
229
 
230
+ Use full path in settings: `"command": "node /path/to/claude-pulse/dist/cli.js"`
231
+
233
232
  ## License
234
233
 
235
234
  MIT
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ var __export = (target, all) => {
12
12
  // package.json
13
13
  var package_default = {
14
14
  name: "cc-pulse",
15
- version: "1.0.2",
15
+ version: "1.2.0",
16
16
  description: "A customizable, real-time statusline for Claude Code",
17
17
  type: "module",
18
18
  bin: {
@@ -100,40 +100,35 @@ function renderContext(input, config, theme) {
100
100
  const ctx = input.context_window;
101
101
  const thresholds = config.thresholds ?? { warn: 70, critical: 85, danger: 95 };
102
102
  const usedPercent = ctx?.used_percentage ?? 0;
103
- const remainingPercent = 100 - usedPercent;
104
103
  let color = theme.green;
105
104
  let indicator = "";
106
- if (remainingPercent <= 100 - thresholds.danger) {
105
+ if (usedPercent >= thresholds.danger) {
107
106
  color = theme.red;
108
107
  indicator = " \uD83D\uDD34";
109
- } else if (remainingPercent <= 100 - thresholds.critical) {
108
+ } else if (usedPercent >= thresholds.critical) {
110
109
  color = theme.peach;
111
110
  indicator = " ⚠️";
112
- } else if (remainingPercent <= 100 - thresholds.warn) {
111
+ } else if (usedPercent >= thresholds.warn) {
113
112
  color = theme.yellow;
114
113
  }
115
- const label = config.label ?? "→Compact";
114
+ const label = config.label ?? "Used";
116
115
  const style = config.style ?? "bar";
117
116
  let display;
118
- if (style === "compact") {
119
- display = `${Math.round(remainingPercent)}%`;
117
+ if (style === "percent") {
118
+ display = `${Math.round(usedPercent)}%`;
120
119
  } else if (style === "detailed" && ctx) {
121
120
  const totalUsed = ctx.total_input_tokens + ctx.total_output_tokens;
122
121
  const windowSize = ctx.context_window_size;
123
- const freeTokens = Math.max(0, windowSize - totalUsed);
124
- display = `${formatTokens(freeTokens)}/${formatTokens(windowSize)} (${Math.round(remainingPercent)}%)`;
122
+ display = `${formatTokens(totalUsed)}/${formatTokens(windowSize)} (${Math.round(usedPercent)}%)`;
125
123
  } else if (style === "bar" || style === "both") {
126
124
  const windowSize = ctx?.context_window_size || 200000;
127
125
  const totalUsed = (ctx?.total_input_tokens || 0) + (ctx?.total_output_tokens || 0);
128
- const freeTokens = Math.max(0, windowSize - totalUsed);
129
- const remaining = Math.round(remainingPercent / 10);
130
- const depleted = 10 - remaining;
131
- const bar = `${color}${"●".repeat(remaining)}${theme.reset}${"○".repeat(depleted)}`;
132
- const freeLabel = `${color}free:${formatTokens(freeTokens)}${theme.reset}`;
133
- const usedLabel = `${theme.overlay0}used:${formatTokens(totalUsed)}${theme.reset}`;
134
- display = style === "both" ? `${bar} ${freeLabel} ${usedLabel}` : `${bar} ${color}${Math.round(remainingPercent)}%${theme.reset}`;
126
+ const used = Math.round(usedPercent / 10);
127
+ const free = 10 - used;
128
+ const bar = `${color}${"●".repeat(used)}${theme.reset}${"○".repeat(free)}`;
129
+ display = style === "both" ? `${bar} ${color}${formatTokens(totalUsed)}${theme.reset} ${theme.overlay0}/ ${formatTokens(windowSize)}${theme.reset}` : `${bar} ${color}${Math.round(usedPercent)}%${theme.reset}`;
135
130
  } else {
136
- display = `${Math.round(remainingPercent)}%`;
131
+ display = `${Math.round(usedPercent)}%`;
137
132
  }
138
133
  let tokenInfo = "";
139
134
  if (config.showTokens && ctx) {
@@ -157,7 +152,7 @@ function renderContext(input, config, theme) {
157
152
  }
158
153
  }
159
154
  let hint = "";
160
- if (config.showCompactHint && remainingPercent <= 20) {
155
+ if (config.showCompactHint && usedPercent >= 80) {
161
156
  hint = " \uD83D\uDCA1/compact";
162
157
  }
163
158
  const labelStr = label ? `${label} ` : "";
@@ -719,29 +714,73 @@ function parseMcpOutput(output) {
719
714
  return servers;
720
715
  }
721
716
  // src/components/model.ts
717
+ function parseModelId(modelId) {
718
+ const id = modelId.toLowerCase();
719
+ const newFormat = id.match(/^claude-(\w+)-(\d+)-(\d+)-\d+$/);
720
+ if (newFormat) {
721
+ const [, family, major, minor] = newFormat;
722
+ return { family, version: `${major}.${minor}` };
723
+ }
724
+ const newFormatNoMinor = id.match(/^claude-(\w+)-(\d+)-\d{8}$/);
725
+ if (newFormatNoMinor) {
726
+ const [, family, major] = newFormatNoMinor;
727
+ return { family, version: major };
728
+ }
729
+ const oldFormat = id.match(/^claude-(\d+)-(\d+)-(\w+)-\d+$/);
730
+ if (oldFormat) {
731
+ const [, major, minor, family] = oldFormat;
732
+ return { family, version: `${major}.${minor}` };
733
+ }
734
+ const oldFormatNoMinor = id.match(/^claude-(\d+)-(\w+)-\d+$/);
735
+ if (oldFormatNoMinor) {
736
+ const [, major, family] = oldFormatNoMinor;
737
+ return { family, version: major };
738
+ }
739
+ return null;
740
+ }
722
741
  function renderModel(input, config, theme) {
723
742
  if (config.enabled === false) {
724
743
  return { text: "" };
725
744
  }
745
+ const modelId = input.model?.id ?? "";
726
746
  const displayName = input.model?.display_name ?? "";
727
- const modelName = displayName.toLowerCase();
728
747
  const icons = config.icons ?? { opus: "\uD83E\uDDE0", sonnet: "\uD83C\uDFB5", haiku: "⚡" };
729
748
  const showIcon = config.showIcon !== false;
730
749
  let icon = "\uD83E\uDD16";
731
750
  let color = theme.text;
732
751
  let modelLabel = displayName;
733
- if (modelName.includes("opus")) {
734
- icon = icons.opus;
735
- color = theme.mauve;
736
- modelLabel = "Opus";
737
- } else if (modelName.includes("sonnet")) {
738
- icon = icons.sonnet;
739
- color = theme.blue;
740
- modelLabel = "Sonnet";
741
- } else if (modelName.includes("haiku")) {
742
- icon = icons.haiku;
743
- color = theme.green;
744
- modelLabel = "Haiku";
752
+ const parsed = parseModelId(modelId);
753
+ if (parsed) {
754
+ const family = parsed.family;
755
+ const version = parsed.version;
756
+ if (family === "opus") {
757
+ icon = icons.opus;
758
+ color = theme.mauve;
759
+ modelLabel = `Opus ${version}`;
760
+ } else if (family === "sonnet") {
761
+ icon = icons.sonnet;
762
+ color = theme.blue;
763
+ modelLabel = `Sonnet ${version}`;
764
+ } else if (family === "haiku") {
765
+ icon = icons.haiku;
766
+ color = theme.green;
767
+ modelLabel = `Haiku ${version}`;
768
+ }
769
+ } else {
770
+ const modelName = displayName.toLowerCase();
771
+ if (modelName.includes("opus")) {
772
+ icon = icons.opus;
773
+ color = theme.mauve;
774
+ modelLabel = "Opus";
775
+ } else if (modelName.includes("sonnet")) {
776
+ icon = icons.sonnet;
777
+ color = theme.blue;
778
+ modelLabel = "Sonnet";
779
+ } else if (modelName.includes("haiku")) {
780
+ icon = icons.haiku;
781
+ color = theme.green;
782
+ modelLabel = "Haiku";
783
+ }
745
784
  }
746
785
  const label = config.label !== undefined ? config.label : modelLabel;
747
786
  const iconStr = showIcon ? `${icon} ` : "";
@@ -794,6 +833,124 @@ function renderSession(input, config, theme) {
794
833
  const text = `${labelPart}${parts.join(" ")}`;
795
834
  return { text };
796
835
  }
836
+ // src/components/skills.ts
837
+ import { existsSync as existsSync4, readdirSync, readFileSync as readFileSync4 } from "node:fs";
838
+ import { homedir as homedir5 } from "node:os";
839
+ import { join as join4 } from "node:path";
840
+ function renderSkills(config, theme) {
841
+ if (config.enabled === false) {
842
+ return { text: "" };
843
+ }
844
+ const summary = getSkillsSummary();
845
+ const label = config.label ?? "Skills";
846
+ const showNames = config.showNames !== false;
847
+ const showCount = config.showCount !== false;
848
+ const maxDisplay = config.maxDisplay ?? Infinity;
849
+ if (summary.total === 0) {
850
+ const text2 = `${theme.mauve}\uD83C\uDFAF ${label} ${theme.overlay0}0${theme.reset}`;
851
+ return { text: text2 };
852
+ }
853
+ const validNames = summary.skills.filter((s) => s.valid).map((s) => s.name ?? s.folder).slice(0, maxDisplay);
854
+ 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
+ }
859
+ 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}`);
863
+ }
864
+ let brokenStr = "";
865
+ if (summary.broken > 0) {
866
+ const brokenDisplay = brokenNames.length <= 2 ? brokenNames.join(",") : `${summary.broken} broken`;
867
+ brokenStr = ` ${theme.red}▲${brokenDisplay}${theme.reset}`;
868
+ }
869
+ const text = `${theme.mauve}\uD83C\uDFAF ${label}${theme.reset}${parts.length ? ` ${parts.join(" ")}` : ""}${brokenStr}`;
870
+ return { text };
871
+ }
872
+ function getSkillsSummary() {
873
+ const skills = [];
874
+ const userSkillsPath = join4(homedir5(), ".claude", "skills");
875
+ scanSkillsDirectory(userSkillsPath, skills);
876
+ const projectSkillsPath = join4(process.cwd(), ".claude", "skills");
877
+ scanSkillsDirectory(projectSkillsPath, skills);
878
+ const seen = new Set;
879
+ const deduped = [];
880
+ for (const skill of skills.reverse()) {
881
+ if (!seen.has(skill.folder)) {
882
+ seen.add(skill.folder);
883
+ deduped.push(skill);
884
+ }
885
+ }
886
+ const valid = deduped.filter((s) => s.valid).length;
887
+ const broken = deduped.filter((s) => !s.valid).length;
888
+ return {
889
+ skills: deduped.reverse(),
890
+ valid,
891
+ broken,
892
+ total: deduped.length
893
+ };
894
+ }
895
+ function scanSkillsDirectory(dirPath, skills) {
896
+ if (!existsSync4(dirPath))
897
+ return;
898
+ try {
899
+ const entries = readdirSync(dirPath, { withFileTypes: true });
900
+ for (const entry of entries) {
901
+ if (!entry.isDirectory())
902
+ continue;
903
+ const skillInfo = validateSkill(dirPath, entry.name);
904
+ skills.push(skillInfo);
905
+ }
906
+ } catch {}
907
+ }
908
+ function validateSkill(skillsDir, folder) {
909
+ const skillPath = join4(skillsDir, folder);
910
+ const skillMdPath = join4(skillPath, "SKILL.md");
911
+ if (!existsSync4(skillMdPath)) {
912
+ return { folder, valid: false, error: "missing_file" };
913
+ }
914
+ try {
915
+ const content = readFileSync4(skillMdPath, "utf-8");
916
+ const frontmatter = parseFrontmatter(content);
917
+ if (!frontmatter) {
918
+ return { folder, valid: false, error: "invalid_frontmatter" };
919
+ }
920
+ if (!frontmatter.name || !frontmatter.description) {
921
+ return { folder, valid: false, error: "missing_fields" };
922
+ }
923
+ return { folder, valid: true, name: frontmatter.name };
924
+ } catch {
925
+ return { folder, valid: false, error: "invalid_frontmatter" };
926
+ }
927
+ }
928
+ function parseFrontmatter(content) {
929
+ const lines = content.split(`
930
+ `);
931
+ if (lines[0]?.trim() !== "---") {
932
+ return null;
933
+ }
934
+ let endIndex = -1;
935
+ for (let i = 1;i < lines.length; i++) {
936
+ if (lines[i]?.trim() === "---") {
937
+ endIndex = i;
938
+ break;
939
+ }
940
+ }
941
+ if (endIndex === -1) {
942
+ return null;
943
+ }
944
+ const result = {};
945
+ for (let i = 1;i < endIndex; i++) {
946
+ const line = lines[i];
947
+ const match = line?.match(/^(\w+):\s*["']?(.+?)["']?\s*$/);
948
+ if (match) {
949
+ result[match[1]] = match[2];
950
+ }
951
+ }
952
+ return result;
953
+ }
797
954
  // src/components/system.ts
798
955
  function formatTokens2(tokens) {
799
956
  if (tokens >= 1e6) {
@@ -840,9 +997,9 @@ function renderSystem(input, config, theme) {
840
997
  return { text };
841
998
  }
842
999
  // src/components/tier.ts
843
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
844
- import { homedir as homedir5 } from "node:os";
845
- import { join as join4 } from "node:path";
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";
846
1003
  var cachedTier = null;
847
1004
  var cacheTime = 0;
848
1005
  var CACHE_TTL3 = 30000;
@@ -870,10 +1027,10 @@ function detectTier() {
870
1027
  if (cachedTier && now - cacheTime < CACHE_TTL3) {
871
1028
  return cachedTier;
872
1029
  }
873
- const claudeJsonPath = join4(homedir5(), ".claude.json");
1030
+ const claudeJsonPath = join5(homedir6(), ".claude.json");
874
1031
  try {
875
- if (existsSync4(claudeJsonPath)) {
876
- const content = readFileSync4(claudeJsonPath, "utf-8");
1032
+ if (existsSync5(claudeJsonPath)) {
1033
+ const content = readFileSync5(claudeJsonPath, "utf-8");
877
1034
  const data = JSON.parse(content);
878
1035
  if (data.oauthAccount?.hasExtraUsageEnabled) {
879
1036
  cachedTier = "max";
@@ -924,12 +1081,12 @@ function renderTime(config, theme) {
924
1081
  return { text };
925
1082
  }
926
1083
  // src/config.ts
927
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
928
- import { homedir as homedir6 } from "node:os";
929
- import { join as join5 } from "node:path";
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";
930
1087
  var CONFIG_PATHS = [
931
- join5(homedir6(), ".config", "claude-pulse", "config.json"),
932
- join5(homedir6(), ".claude-pulse.json")
1088
+ join6(homedir7(), ".config", "claude-pulse", "config.json"),
1089
+ join6(homedir7(), ".claude-pulse.json")
933
1090
  ];
934
1091
  var FIXED_LINES = [
935
1092
  { name: "identity", enabled: true, components: ["name", "cwd"], separator: " " },
@@ -941,7 +1098,8 @@ var FIXED_LINES = [
941
1098
  separator: " │ "
942
1099
  },
943
1100
  { name: "mcp", enabled: true, components: ["mcp"], separator: " │ " },
944
- { name: "hooks", enabled: true, components: ["hooks"], separator: " │ " }
1101
+ { name: "hooks", enabled: true, components: ["hooks"], separator: " │ " },
1102
+ { name: "skills", enabled: true, components: ["skills"], separator: " │ " }
945
1103
  ];
946
1104
  var DEFAULT_CONFIG = {
947
1105
  theme: "catppuccin",
@@ -956,7 +1114,7 @@ var DEFAULT_CONFIG = {
956
1114
  },
957
1115
  context: {
958
1116
  enabled: true,
959
- label: "→Compact",
1117
+ label: "Used",
960
1118
  style: "bar",
961
1119
  showRate: false,
962
1120
  showTokens: true,
@@ -1003,6 +1161,12 @@ var DEFAULT_CONFIG = {
1003
1161
  },
1004
1162
  cache: {
1005
1163
  enabled: true
1164
+ },
1165
+ skills: {
1166
+ enabled: true,
1167
+ label: "Skills",
1168
+ showCount: true,
1169
+ showNames: true
1006
1170
  }
1007
1171
  },
1008
1172
  interactive: {
@@ -1015,9 +1179,9 @@ var DEFAULT_CONFIG = {
1015
1179
  };
1016
1180
  function loadConfig() {
1017
1181
  for (const configPath of CONFIG_PATHS) {
1018
- if (existsSync5(configPath)) {
1182
+ if (existsSync6(configPath)) {
1019
1183
  try {
1020
- const content = readFileSync5(configPath, "utf-8");
1184
+ const content = readFileSync6(configPath, "utf-8");
1021
1185
  const userConfig = JSON.parse(content);
1022
1186
  return mergeConfig(DEFAULT_CONFIG, userConfig);
1023
1187
  } catch {}
@@ -14662,7 +14826,7 @@ var ModelConfigSchema = exports_external.object({
14662
14826
  var ContextConfigSchema = exports_external.object({
14663
14827
  enabled: exports_external.boolean().optional(),
14664
14828
  label: exports_external.string().optional(),
14665
- style: exports_external.enum(["bar", "percent", "both", "detailed", "compact"]).optional(),
14829
+ style: exports_external.enum(["bar", "percent", "both", "detailed"]).optional(),
14666
14830
  showRate: exports_external.boolean().optional(),
14667
14831
  showCompactHint: exports_external.boolean().optional(),
14668
14832
  showTokens: exports_external.boolean().optional(),
@@ -14746,6 +14910,13 @@ var CacheConfigSchema = exports_external.object({
14746
14910
  showHitRate: exports_external.boolean().optional(),
14747
14911
  showTokensSaved: exports_external.boolean().optional()
14748
14912
  });
14913
+ var SkillsConfigSchema = exports_external.object({
14914
+ enabled: exports_external.boolean().optional(),
14915
+ label: exports_external.string().optional(),
14916
+ showCount: exports_external.boolean().optional(),
14917
+ showNames: exports_external.boolean().optional(),
14918
+ maxDisplay: exports_external.number().optional()
14919
+ });
14749
14920
  var ComponentConfigsSchema = exports_external.object({
14750
14921
  tier: TierConfigSchema.optional(),
14751
14922
  model: ModelConfigSchema.optional(),
@@ -14762,7 +14933,8 @@ var ComponentConfigsSchema = exports_external.object({
14762
14933
  status: exports_external.object({ enabled: exports_external.boolean().optional() }).optional(),
14763
14934
  linesChanged: LinesChangedConfigSchema.optional(),
14764
14935
  hooks: HooksConfigSchema.optional(),
14765
- cache: CacheConfigSchema.optional()
14936
+ cache: CacheConfigSchema.optional(),
14937
+ skills: SkillsConfigSchema.optional()
14766
14938
  });
14767
14939
  var LineOverrideSchema = exports_external.object({
14768
14940
  enabled: exports_external.boolean().optional(),
@@ -14772,7 +14944,8 @@ var LinesConfigSchema = exports_external.object({
14772
14944
  git: LineOverrideSchema.optional(),
14773
14945
  engine: LineOverrideSchema.optional(),
14774
14946
  mcp: LineOverrideSchema.optional(),
14775
- hooks: LineOverrideSchema.optional()
14947
+ hooks: LineOverrideSchema.optional(),
14948
+ skills: LineOverrideSchema.optional()
14776
14949
  });
14777
14950
  var PulseConfigSchema = exports_external.object({
14778
14951
  theme: exports_external.string(),
@@ -14920,6 +15093,8 @@ function renderComponent(name, input, config2, theme) {
14920
15093
  return renderHooks(config2.components.hooks ?? {}, theme);
14921
15094
  case "cache":
14922
15095
  return renderCache(input, config2.components.cache ?? {}, theme);
15096
+ case "skills":
15097
+ return renderSkills(config2.components.skills ?? {}, theme);
14923
15098
  default:
14924
15099
  return { text: "" };
14925
15100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-pulse",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "A customizable, real-time statusline for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {