claude-simple-status 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 +25 -3
  2. package/package.json +2 -2
  3. package/statusline.mjs +48 -33
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)
10
10
  [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)]()
11
11
 
12
- A simple, no-frills statusline for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) that shows what matters: **git branch, model, context usage, and quota**.
12
+ A simple, no-frills statusline for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) that shows what matters: **project name, git branch, model, context usage, quota, and API costs**.
13
13
 
14
14
  ![statusline screenshot](assets/statusline.png)
15
15
 
@@ -19,7 +19,10 @@ A simple, no-frills statusline for [Claude Code](https://docs.anthropic.com/en/d
19
19
  - **Cross-platform** — works on macOS, Linux, and Windows
20
20
  - **Non-blocking** — returns cached data instantly, refreshes quota in the background
21
21
  - **Color-coded** — green/orange/red percentages at a glance
22
- - **Git-aware** — shows the current branch name in repos
22
+ - **Project name** — bold uppercase project directory name so you never mix up sessions
23
+ - **Git-aware** — shows the current branch name in repos (cached 30s to reduce overhead)
24
+ - **API cost tracking** — pay-as-you-go API users see cumulative session cost instead of quota
25
+ - **Stale-aware** — shows `--` for quota values when cache is outdated, real values appear after first refresh
23
26
  - **Timezone-smart** — quota reset time converted to your local timezone
24
27
 
25
28
  If the quota API is unreachable, a red `ERR` indicator appears at the end and clears automatically once the connection recovers.
@@ -92,16 +95,23 @@ To uninstall, remove `~/.claude/statusline/` and the `"statusLine"` block from s
92
95
 
93
96
  ## How it works
94
97
 
95
- 1. Receives model/context info from Claude Code via stdin (JSON)
98
+ 1. Receives model/context/cost info from Claude Code via stdin (JSON)
96
99
  2. Reads cached quota data and returns immediately (never blocks the UI)
97
100
  3. If the cache is stale (>2 minutes), refreshes from Anthropic's OAuth API in the background
98
101
  4. Converts UTC reset time to your local timezone
99
102
  5. Outputs a formatted statusline with ANSI colors
100
103
 
104
+ **Subscription users** see quota percentages and reset times. **API (pay-as-you-go) users** see cumulative session cost (e.g. `$4.72`) — calculated by Claude Code from actual token usage, no external pricing lookups needed.
105
+
101
106
  Quota data is cached to the system temp directory and refreshed every 2 minutes. Since Claude Code calls the statusline on every message update, this avoids excessive API calls while keeping the data fresh.
102
107
 
103
108
  ## Troubleshooting
104
109
 
110
+ **Indicators:**
111
+ - `--` for quota values means the cache is stale (>5 minutes old) — values appear after the first background refresh
112
+ - `?` means quota data has never been fetched yet
113
+ - `ERR` (red) means the last quota fetch failed — clears automatically on recovery
114
+
105
115
  If the statusline shows `ERR`, check the error log:
106
116
 
107
117
  ```bash
@@ -122,6 +132,18 @@ rm /tmp/claude-statusline-quota.json
122
132
  Remove-Item $env:TEMP\claude-statusline-quota.json
123
133
  ```
124
134
 
135
+ ## Related projects
136
+
137
+ ### [claude-rig](https://github.com/edimuj/claude-rig)
138
+
139
+ Run multiple isolated Claude Code configurations simultaneously — each with its own plugins, skills, MCP servers, and settings. When a session is launched through claude-rig, the active profile name appears in the statusline in bold magenta:
140
+
141
+ ```
142
+ MY-PROJECT [main] | minimal | Opus 4.6 | 12% | 14:30 | 5h:34% | 7d:12%
143
+ ```
144
+
145
+ No configuration needed — claude-simple-status detects claude-rig automatically. Users not using claude-rig are unaffected.
146
+
125
147
  ## Contributing
126
148
 
127
149
  Contributions are welcome! This project follows a few principles:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-simple-status",
3
- "version": "1.0.2",
4
- "description": "A simple statusline for Claude Code — git branch, model, context usage, and quota at a glance",
3
+ "version": "1.2.0",
4
+ "description": "A simple statusline for Claude Code — project name, git branch, model, context usage, quota, and API costs at a glance",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "claude-simple-status": "statusline.mjs"
package/statusline.mjs CHANGED
@@ -2,11 +2,10 @@
2
2
  // Claude Code Statusline - Shows Branch | Model | Context % | Next Reset | 5h Quota % | 7d Quota %
3
3
  // Cross-platform Node.js version (no dependencies)
4
4
 
5
- import { readFileSync, writeFileSync, mkdirSync, rmdirSync, statSync, existsSync, appendFileSync } from 'fs';
5
+ import { readFileSync, writeFileSync, mkdirSync, rmdirSync, statSync, existsSync } from 'fs';
6
6
  import { homedir, tmpdir } from 'os';
7
- import { join } from 'path';
7
+ import { join, basename } from 'path';
8
8
  import { spawn, execSync } from 'child_process';
9
- import { request } from 'https';
10
9
 
11
10
  // Handle --uninstall flag (workaround: npm doesn't run preuninstall for global packages)
12
11
  if (process.argv.includes('--uninstall')) {
@@ -31,6 +30,8 @@ const GREEN = '\x1b[0;32m';
31
30
  const ORANGE = '\x1b[0;33m';
32
31
  const RED = '\x1b[0;31m';
33
32
  const CYAN = '\x1b[0;36m';
33
+ const WHITE_BOLD = '\x1b[1;37m';
34
+ const MAGENTA_BOLD = '\x1b[1;35m';
34
35
  const YELLOW_BOLD = '\x1b[1;33m';
35
36
  const RESET = '\x1b[0m';
36
37
 
@@ -40,7 +41,10 @@ const CACHE_FILE = join(tmpdir(), 'claude-statusline-quota.json');
40
41
  const LOCK_DIR = join(tmpdir(), 'claude-statusline-quota.lock');
41
42
  const ERROR_FILE = join(tmpdir(), 'claude-statusline-error');
42
43
  const LOG_FILE = join(tmpdir(), 'claude-statusline.log');
43
- const CACHE_MAX_AGE = 120; // seconds
44
+ const CACHE_MAX_AGE = 120; // seconds - when to fetch
45
+ const CACHE_STALE_AGE = 300; // seconds - when to show "--" instead of old values
46
+ const GIT_BRANCH_CACHE = join(tmpdir(), 'claude-statusline-branches.json');
47
+ const GIT_BRANCH_MAX_AGE = 30; // seconds
44
48
 
45
49
  // Color a percentage value based on thresholds
46
50
  function colorPct(val) {
@@ -89,30 +93,6 @@ function acquireLock() {
89
93
  }
90
94
  }
91
95
 
92
- // Release lock
93
- function releaseLock() {
94
- try { rmdirSync(LOCK_DIR); } catch {}
95
- }
96
-
97
- // Log error with timestamp
98
- function logError(msg) {
99
- const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
100
- try {
101
- appendFileSync(LOG_FILE, `[${ts}] ${msg}\n`);
102
- writeFileSync(ERROR_FILE, msg);
103
- // Trim log to last 50 lines
104
- const lines = readFileSync(LOG_FILE, 'utf8').split('\n').filter(Boolean);
105
- if (lines.length > 50) {
106
- writeFileSync(LOG_FILE, lines.slice(-50).join('\n') + '\n');
107
- }
108
- } catch {}
109
- }
110
-
111
- // Clear error state
112
- function clearError() {
113
- try { writeFileSync(ERROR_FILE, ''); } catch {}
114
- }
115
-
116
96
  // Spawn background refresh process
117
97
  function refreshInBackground(token) {
118
98
  const child = spawn(process.execPath, [
@@ -187,13 +167,22 @@ function toLocalTime(isoString) {
187
167
  }
188
168
  }
189
169
 
190
- // Get current git branch name
170
+ // Get current git branch name (cached per cwd, 30s TTL)
191
171
  function getGitBranch() {
172
+ const cwd = process.cwd();
192
173
  try {
193
- return execSync('git rev-parse --abbrev-ref HEAD', {
174
+ const cache = readJsonFile(GIT_BRANCH_CACHE) || {};
175
+ const entry = cache[cwd];
176
+ if (entry && (Date.now() - entry.ts) < GIT_BRANCH_MAX_AGE * 1000) {
177
+ return entry.branch;
178
+ }
179
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
194
180
  timeout: 1000,
195
181
  stdio: ['ignore', 'pipe', 'ignore']
196
182
  }).toString().trim();
183
+ cache[cwd] = { branch, ts: Date.now() };
184
+ try { writeFileSync(GIT_BRANCH_CACHE, JSON.stringify(cache)); } catch {}
185
+ return branch;
197
186
  } catch {
198
187
  return null;
199
188
  }
@@ -210,10 +199,18 @@ async function main() {
210
199
  // Parse Claude Code input
211
200
  let model = 'Unknown';
212
201
  let contextUsed = 0;
202
+ let totalCostUsd = null;
203
+ let projectName = null;
213
204
  try {
214
205
  const data = JSON.parse(input);
215
206
  model = data.model?.display_name || 'Unknown';
216
207
  contextUsed = data.context_window?.used_percentage || 0;
208
+ if (typeof data.cost?.total_cost_usd === 'number') {
209
+ totalCostUsd = data.cost.total_cost_usd;
210
+ }
211
+ if (data.workspace?.project_dir) {
212
+ projectName = basename(data.workspace.project_dir).toUpperCase();
213
+ }
217
214
  } catch {}
218
215
 
219
216
  // Get OAuth token
@@ -237,12 +234,17 @@ async function main() {
237
234
  }
238
235
  }
239
236
 
240
- // Parse quota data
237
+ // Parse quota data (show "--" if cache is too stale)
241
238
  let fiveHourPct = '?';
242
239
  let sevenDayPct = '?';
243
240
  let resetLocal = '--:--';
241
+ const cacheIsStale = !quotaData || getFileAge(CACHE_FILE) > CACHE_STALE_AGE;
244
242
 
245
- if (quotaData) {
243
+ if (cacheIsStale) {
244
+ fiveHourPct = '--';
245
+ sevenDayPct = '--';
246
+ resetLocal = '--:--';
247
+ } else if (quotaData) {
246
248
  if (quotaData.five_hour === null || quotaData.seven_day === null) {
247
249
  // Organization/team plan without individual quota
248
250
  fiveHourPct = 'N/A';
@@ -262,16 +264,29 @@ async function main() {
262
264
  hasError = errContent.length > 0;
263
265
  } catch {}
264
266
 
267
+ // Get rig profile (claude-rig sets CLAUDE_CONFIG_DIR to ~/.claude-rig/profiles/<name>)
268
+ const rigProfile = (() => {
269
+ const configDir = process.env.CLAUDE_CONFIG_DIR;
270
+ if (!configDir) return null;
271
+ const match = configDir.match(/\.claude-rig\/profiles\/([^/]+)\/?$/);
272
+ return match ? match[1] : null;
273
+ })();
274
+
265
275
  // Get git branch
266
276
  const branch = getGitBranch();
267
277
 
268
278
  // Build output
269
- let output = `${branch ? `${YELLOW_BOLD}${branch}${RESET} | ` : ''}${CYAN}${model}${RESET} | ${colorPct(contextUsed)}`;
279
+ const projectSegment = projectName
280
+ ? `${WHITE_BOLD}${projectName}${branch ? ` ${YELLOW_BOLD}[${branch}]` : ''}${RESET}`
281
+ : (branch ? `${YELLOW_BOLD}${branch}${RESET}` : '');
282
+ let output = `${projectSegment ? `${projectSegment} | ` : ''}${rigProfile ? `${MAGENTA_BOLD}${rigProfile}${RESET} | ` : ''}${CYAN}${model}${RESET} | ${colorPct(contextUsed)}`;
270
283
  if (token) {
271
284
  output += ` | ${resetLocal} | 5h:${colorPct(fiveHourPct)} | 7d:${colorPct(sevenDayPct)}`;
272
285
  if (hasError) {
273
286
  output += ` | ${RED}ERR${RESET}`;
274
287
  }
288
+ } else if (totalCostUsd !== null) {
289
+ output += ` | ${GREEN}$${totalCostUsd.toFixed(2)}${RESET}`;
275
290
  }
276
291
 
277
292
  process.stdout.write(output);