howmuchleft 0.2.0 → 0.2.1

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.
package/README.md CHANGED
@@ -3,103 +3,32 @@
3
3
  [![npm version](https://img.shields.io/npm/v/howmuchleft)](https://www.npmjs.com/package/howmuchleft)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/howmuchleft)](https://www.npmjs.com/package/howmuchleft)
5
5
  [![license](https://img.shields.io/npm/l/howmuchleft)](https://github.com/smm-h/howmuchleft/blob/main/LICENSE)
6
- [![node](https://img.shields.io/node/v/howmuchleft)](https://nodejs.org)
7
6
 
8
- Pixel-perfect progress bars for your Claude Code statusline. See how much context and usage you have left at a glance.
7
+ **Know exactly how much context and usage you have left, right in your Claude Code statusline.**
9
8
 
10
- | Dark | Light |
11
- |---|---|
12
- | ![Dark mode demo](./assets/demo-dark.gif) | ![Light mode demo](./assets/demo-light.gif) |
9
+ ![Dark mode demo](./assets/demo-dark.gif)
13
10
 
14
- ## What you get
11
+ ![Light mode demo](./assets/demo-light.gif)
15
12
 
16
- Three bars with sub-cell precision using Unicode fractional block characters:
13
+ Three progress bars with sub-cell precision that shift from green to red as you approach your limits:
17
14
 
18
- | Bar | Shows | Extra info |
19
- |---|---|---|
20
- | Context window | How full your conversation is | Subscription tier, model name |
21
- | 5-hour usage | Rolling rate limit utilization | Time until reset, git branch/changes, lines added/removed |
22
- | Weekly usage | Rolling 7-day rate limit utilization | Time until reset, current directory |
15
+ | Bar | What it tracks |
16
+ |---|---|
17
+ | **Context window** | How full your conversation is, plus subscription tier and model |
18
+ | **5-hour usage** | Rolling rate limit, time until reset, git branch and diff stats |
19
+ | **Weekly usage** | Rolling 7-day rate limit, time until reset, current directory |
23
20
 
24
- Bars shift green to red as usage increases. Stale data (API unreachable) is prefixed with `~`. Works with Pro, Max 5x, Max 20x, and Team subscriptions. API key users see an "API" label with context bar only.
21
+ Works with Pro, Max 5x, Max 20x, and Team subscriptions. API key users see context bar only.
25
22
 
26
23
  ## Install
27
24
 
25
+ Two commands and you're done:
26
+
28
27
  ```bash
29
28
  npm install -g howmuchleft
30
29
  howmuchleft --install
31
30
  ```
32
31
 
33
- For multiple Claude Code config directories:
34
-
35
- ```bash
36
- howmuchleft --install ~/.claude-work
37
- howmuchleft --install ~/.claude-personal
38
- ```
39
-
40
- ## Configuration
41
-
42
- Config file: `~/.config/howmuchleft.json` (JSONC -- comments and trailing commas are allowed).
43
-
44
- ```jsonc
45
- {
46
- "progressLength": 12,
47
- "colorMode": "auto",
48
- "colors": [
49
- // Truecolor dark: RGB gradient + RGB background
50
- { "dark-mode": true, "true-color": true, "bg": [48, 48, 48], "gradient": [[0,215,0], [255,255,0], [255,0,0]] },
51
- // 256-color dark: index gradient + index background
52
- { "dark-mode": true, "true-color": false, "bg": 236, "gradient": [46, 226, 196] }
53
- ]
54
- }
55
- ```
56
-
57
- ### Top-level settings
58
-
59
- | Field | Default | Description |
60
- |---|---|---|
61
- | `progressLength` | `12` | Bar width in characters (3--40) |
62
- | `colorMode` | `"auto"` | `"auto"` (detect via `COLORTERM`), `"truecolor"`, or `"256"` |
63
- | `colors` | built-in | Array of color entries (see below) |
64
-
65
- ### Color entries
66
-
67
- Each entry in the `colors` array is matched against the current terminal. First match wins. Omit a condition to match both modes.
68
-
69
- | Field | Required | Description |
70
- |---|---|---|
71
- | `gradient` | Yes | Color stops: `[R,G,B]` arrays for truecolor, or integers (0--255) for 256-color |
72
- | `bg` | No | Empty bar background: `[R,G,B]` for truecolor, or integer (0--255) for 256-color |
73
- | `dark-mode` | No | Match dark (`true`) or light (`false`) terminals only |
74
- | `true-color` | No | Match truecolor (`true`) or 256-color (`false`) terminals only |
75
-
76
- Truecolor gradients are smoothly interpolated between stops -- 3 stops (green, yellow, red) is enough for a smooth bar. 256-color gradients snap to the nearest stop.
77
-
78
- To preview your current gradient: `howmuchleft --test-colors`
79
-
80
- ## CLI
81
-
82
- ```
83
- howmuchleft [config-dir] Run the statusline (called by Claude Code)
84
- howmuchleft --install [config-dir] Add to Claude Code settings.json
85
- howmuchleft --uninstall [config-dir] Remove from Claude Code settings.json
86
- howmuchleft --config Show config path and current settings
87
- howmuchleft --demo [seconds] Time-lapse animation (default 60s)
88
- howmuchleft --test-colors Preview gradient at seven sample levels
89
- howmuchleft --version Show version
90
- howmuchleft --help Show help
91
- ```
92
-
93
- ## How it works
94
-
95
- Claude Code invokes `howmuchleft` as a child process on each statusline render, piping a JSON object to stdin with model info, context window usage, cwd, and cost data. The script:
96
-
97
- 1. Parses the JSON from stdin
98
- 2. Fetches usage data from Anthropic's OAuth API (cached 60s, stale-data fallback on failure)
99
- 3. Auto-refreshes expired OAuth tokens
100
- 4. Runs `git status --porcelain=v2` in parallel for branch/change info
101
- 5. Renders three lines of ANSI-colored progress bars to stdout
102
-
103
32
  ## Uninstall
104
33
 
105
34
  ```bash
@@ -107,9 +36,12 @@ howmuchleft --uninstall
107
36
  npm uninstall -g howmuchleft
108
37
  ```
109
38
 
110
- ## Requirements
39
+ ## Customize
40
+
41
+ Config lives at `~/.config/howmuchleft.json` (JSONC -- comments allowed). See [`config.example.json`](./config.example.json) for all options.
42
+
43
+ - **`progressLength`** -- bar width in characters (default 12)
44
+ - **`colorMode`** -- `"auto"`, `"truecolor"`, or `"256"`
45
+ - **`colors`** -- custom gradient stops and background colors per theme/color-depth combo
111
46
 
112
- - Node.js >= 18
113
- - Claude Code with OAuth login (Pro, Max, or Team subscription)
114
- - `git` (optional, for branch/change display)
115
- - `gsettings` (Linux/GNOME) or `defaults` (macOS) for dark/light mode detection
47
+ Preview your current gradient: `howmuchleft --test-colors`
package/lib/statusline.js CHANGED
@@ -16,6 +16,7 @@
16
16
 
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
+ const crypto = require('crypto');
19
20
  const https = require('https');
20
21
  const { execFile, execFileSync } = require('child_process');
21
22
  const { promisify } = require('util');
@@ -238,12 +239,57 @@ function getClaudeDir() {
238
239
  return resolvePath(arg);
239
240
  }
240
241
 
242
+ /**
243
+ * Read credentials from the macOS Keychain.
244
+ * Claude Code stores OAuth tokens in the Keychain on macOS instead of
245
+ * (or in addition to) the .credentials.json file. The service name includes
246
+ * a hash of the config dir path to support multiple accounts.
247
+ */
248
+ function readKeychainCredentials(claudeDir) {
249
+ if (process.platform !== 'darwin') return null;
250
+ const hash = crypto.createHash('sha256').update(claudeDir).digest('hex').slice(0, 8);
251
+ // Try current hashed service name, then legacy unhashed name
252
+ for (const svc of [`Claude Code-credentials-${hash}`, 'Claude Code-credentials']) {
253
+ try {
254
+ const raw = execFileSync('security', [
255
+ 'find-generic-password', '-s', svc, '-w',
256
+ ], { encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
257
+ const parsed = JSON.parse(raw.trim());
258
+ if (parsed?.claudeAiOauth?.accessToken) return parsed;
259
+ } catch {}
260
+ }
261
+ return null;
262
+ }
263
+
264
+ // Cached per-process: credentials won't change during a single render cycle.
265
+ let _credentialsRead = false;
266
+ let _credentialsCache = null;
267
+
241
268
  function readCredentialsFile(claudeDir) {
269
+ if (_credentialsRead) return _credentialsCache;
270
+ _credentialsRead = true;
271
+
272
+ // Try file first (fast, no system prompt)
273
+ let fileData = null;
242
274
  try {
243
- return JSON.parse(fs.readFileSync(path.join(claudeDir, '.credentials.json'), 'utf8'));
244
- } catch {
245
- return null;
275
+ fileData = JSON.parse(fs.readFileSync(path.join(claudeDir, '.credentials.json'), 'utf8'));
276
+ } catch {}
277
+ if (fileData?.claudeAiOauth?.accessToken) {
278
+ _credentialsCache = fileData;
279
+ return fileData;
246
280
  }
281
+
282
+ // macOS: try Keychain when file lacks OAuth credentials
283
+ const keychainData = readKeychainCredentials(claudeDir);
284
+ if (keychainData) {
285
+ // Merge: preserve file data (mcpOAuth etc.) with keychain OAuth
286
+ const merged = { ...(fileData || {}), claudeAiOauth: keychainData.claudeAiOauth };
287
+ _credentialsCache = merged;
288
+ return merged;
289
+ }
290
+
291
+ _credentialsCache = fileData;
292
+ return fileData;
247
293
  }
248
294
 
249
295
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "howmuchleft",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Pixel-perfect progress bars showing how much context and usage you have left, right in your Claude Code statusline",
5
5
  "bin": {
6
6
  "howmuchleft": "bin/cli.js"