claude-simple-status 1.0.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.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/package.json +38 -0
- package/scripts/setup.mjs +86 -0
- package/statusline.mjs +259 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Edin Mujkanovic
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/claude-simple-status-mascot-512.png" width="256" alt="claude-simple-status mascot">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# claude-simple-status
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/claude-simple-status)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
[](https://nodejs.org/)
|
|
10
|
+
[]()
|
|
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**.
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **Zero dependencies** — single Node.js script, no runtime dependencies
|
|
19
|
+
- **Cross-platform** — works on macOS, Linux, and Windows
|
|
20
|
+
- **Non-blocking** — returns cached data instantly, refreshes quota in the background
|
|
21
|
+
- **Color-coded** — green/orange/red percentages at a glance
|
|
22
|
+
- **Git-aware** — shows the current branch name in repos
|
|
23
|
+
- **Timezone-smart** — quota reset time converted to your local timezone
|
|
24
|
+
|
|
25
|
+
If the quota API is unreachable, a red `ERR` indicator appears at the end and clears automatically once the connection recovers.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g claude-simple-status
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it — Claude Code is configured automatically. The statusline appears immediately.
|
|
34
|
+
|
|
35
|
+
To uninstall (also cleans up the Claude Code config):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm uninstall -g claude-simple-status
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
<details>
|
|
42
|
+
<summary>Alternative: shell script (macOS / Linux)</summary>
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
curl -fsSL https://raw.githubusercontent.com/edimuj/claude-simple-status/main/install.sh | bash
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
</details>
|
|
49
|
+
|
|
50
|
+
<details>
|
|
51
|
+
<summary>Alternative: PowerShell (Windows)</summary>
|
|
52
|
+
|
|
53
|
+
```powershell
|
|
54
|
+
irm https://raw.githubusercontent.com/edimuj/claude-simple-status/main/install.ps1 | iex
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
</details>
|
|
58
|
+
|
|
59
|
+
<details>
|
|
60
|
+
<summary>Manual installation</summary>
|
|
61
|
+
|
|
62
|
+
**1. Copy the script**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mkdir -p ~/.claude/statusline
|
|
66
|
+
curl -o ~/.claude/statusline/statusline.mjs \
|
|
67
|
+
https://raw.githubusercontent.com/edimuj/claude-simple-status/main/statusline.mjs
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**2. Configure Claude Code**
|
|
71
|
+
|
|
72
|
+
Add to your `~/.claude/settings.json`:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"statusLine": {
|
|
77
|
+
"type": "command",
|
|
78
|
+
"command": "node ~/.claude/statusline/statusline.mjs"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
To uninstall, remove `~/.claude/statusline/` and the `"statusLine"` block from settings.json.
|
|
84
|
+
|
|
85
|
+
</details>
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Claude Code CLI
|
|
90
|
+
- Node.js (v18+)
|
|
91
|
+
|
|
92
|
+
## How it works
|
|
93
|
+
|
|
94
|
+
1. Receives model/context info from Claude Code via stdin (JSON)
|
|
95
|
+
2. Reads cached quota data and returns immediately (never blocks the UI)
|
|
96
|
+
3. If the cache is stale (>2 minutes), refreshes from Anthropic's OAuth API in the background
|
|
97
|
+
4. Converts UTC reset time to your local timezone
|
|
98
|
+
5. Outputs a formatted statusline with ANSI colors
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
102
|
+
## Troubleshooting
|
|
103
|
+
|
|
104
|
+
If the statusline shows `ERR`, check the error log:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# macOS/Linux
|
|
108
|
+
cat /tmp/claude-statusline.log
|
|
109
|
+
|
|
110
|
+
# Windows (PowerShell)
|
|
111
|
+
Get-Content $env:TEMP\claude-statusline.log
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
To force a fresh quota fetch, clear the cache:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# macOS/Linux
|
|
118
|
+
rm /tmp/claude-statusline-quota.json
|
|
119
|
+
|
|
120
|
+
# Windows (PowerShell)
|
|
121
|
+
Remove-Item $env:TEMP\claude-statusline-quota.json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Contributing
|
|
125
|
+
|
|
126
|
+
Contributions are welcome! This project follows a few principles:
|
|
127
|
+
|
|
128
|
+
- Single file, zero dependencies
|
|
129
|
+
- Cross-platform (macOS, Linux, Windows)
|
|
130
|
+
- Never block the UI
|
|
131
|
+
|
|
132
|
+
Open an [issue](https://github.com/edimuj/claude-simple-status/issues) or submit a pull request.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
[MIT](https://opensource.org/licenses/MIT)
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-simple-status",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A simple statusline for Claude Code — git branch, model, context usage, and quota at a glance",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-simple-status": "./statusline.mjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node scripts/setup.mjs install",
|
|
11
|
+
"preuninstall": "node scripts/setup.mjs uninstall"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"statusline.mjs",
|
|
15
|
+
"scripts/setup.mjs"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"statusline",
|
|
21
|
+
"status-bar",
|
|
22
|
+
"anthropic",
|
|
23
|
+
"cli"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/edimuj/claude-simple-status.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/edimuj/claude-simple-status",
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/edimuj/claude-simple-status/issues"
|
|
32
|
+
},
|
|
33
|
+
"author": "Edin Mujkanovic",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Postinstall / preuninstall hook for claude-simple-status
|
|
3
|
+
// Configures (or removes) the statusLine entry in ~/.claude/settings.json
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join, dirname } from 'path';
|
|
8
|
+
|
|
9
|
+
const SETTINGS_FILE = join(homedir(), '.claude', 'settings.json');
|
|
10
|
+
const COMMAND = 'claude-simple-status';
|
|
11
|
+
|
|
12
|
+
function readSettings() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(SETTINGS_FILE, 'utf8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeSettings(settings) {
|
|
21
|
+
mkdirSync(dirname(SETTINGS_FILE), { recursive: true });
|
|
22
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function install() {
|
|
26
|
+
try {
|
|
27
|
+
const settings = readSettings() || {};
|
|
28
|
+
|
|
29
|
+
if (settings.statusLine) {
|
|
30
|
+
if (settings.statusLine.command === COMMAND) {
|
|
31
|
+
console.log('claude-simple-status is already configured.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log('Note: statusLine is already configured in ~/.claude/settings.json.');
|
|
36
|
+
console.log('To switch to claude-simple-status, update your settings:');
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(' "statusLine": {');
|
|
39
|
+
console.log(' "type": "command",');
|
|
40
|
+
console.log(` "command": "${COMMAND}"`);
|
|
41
|
+
console.log(' }');
|
|
42
|
+
console.log('');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
settings.statusLine = {
|
|
47
|
+
type: 'command',
|
|
48
|
+
command: COMMAND
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
writeSettings(settings);
|
|
52
|
+
console.log('');
|
|
53
|
+
console.log('claude-simple-status installed!');
|
|
54
|
+
console.log('Your statusline will appear at the bottom of Claude Code.');
|
|
55
|
+
console.log('');
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.log(`Note: Could not auto-configure Claude Code (${err.message}).`);
|
|
58
|
+
console.log('Add this manually to ~/.claude/settings.json:');
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(' "statusLine": {');
|
|
61
|
+
console.log(' "type": "command",');
|
|
62
|
+
console.log(` "command": "${COMMAND}"`);
|
|
63
|
+
console.log(' }');
|
|
64
|
+
console.log('');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function uninstall() {
|
|
69
|
+
try {
|
|
70
|
+
const settings = readSettings();
|
|
71
|
+
if (!settings || !settings.statusLine) return;
|
|
72
|
+
if (settings.statusLine.command !== COMMAND) return;
|
|
73
|
+
|
|
74
|
+
delete settings.statusLine;
|
|
75
|
+
writeSettings(settings);
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log('claude-simple-status removed from Claude Code settings.');
|
|
78
|
+
console.log('');
|
|
79
|
+
} catch {
|
|
80
|
+
// Best-effort cleanup, don't fail the uninstall
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const action = process.argv[2];
|
|
85
|
+
if (action === 'install') install();
|
|
86
|
+
else if (action === 'uninstall') uninstall();
|
package/statusline.mjs
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code Statusline - Shows Branch | Model | Context % | Next Reset | 5h Quota % | 7d Quota %
|
|
3
|
+
// Cross-platform Node.js version (no dependencies)
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, rmdirSync, statSync, existsSync, appendFileSync } from 'fs';
|
|
6
|
+
import { homedir, tmpdir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { spawn, execSync } from 'child_process';
|
|
9
|
+
import { request } from 'https';
|
|
10
|
+
|
|
11
|
+
// ANSI color codes
|
|
12
|
+
const GREEN = '\x1b[0;32m';
|
|
13
|
+
const ORANGE = '\x1b[0;33m';
|
|
14
|
+
const RED = '\x1b[0;31m';
|
|
15
|
+
const CYAN = '\x1b[0;36m';
|
|
16
|
+
const YELLOW_BOLD = '\x1b[1;33m';
|
|
17
|
+
const RESET = '\x1b[0m';
|
|
18
|
+
|
|
19
|
+
// File paths
|
|
20
|
+
const CREDS_FILE = join(homedir(), '.claude', '.credentials.json');
|
|
21
|
+
const CACHE_FILE = join(tmpdir(), 'claude-statusline-quota.json');
|
|
22
|
+
const LOCK_DIR = join(tmpdir(), 'claude-statusline-quota.lock');
|
|
23
|
+
const ERROR_FILE = join(tmpdir(), 'claude-statusline-error');
|
|
24
|
+
const LOG_FILE = join(tmpdir(), 'claude-statusline.log');
|
|
25
|
+
const CACHE_MAX_AGE = 120; // seconds
|
|
26
|
+
|
|
27
|
+
// Color a percentage value based on thresholds
|
|
28
|
+
function colorPct(val) {
|
|
29
|
+
if (typeof val !== 'number' || isNaN(val)) {
|
|
30
|
+
return val === 'N/A' ? 'N/A' : `${val}`;
|
|
31
|
+
}
|
|
32
|
+
const intVal = Math.floor(val);
|
|
33
|
+
if (intVal <= 50) return `${GREEN}${val}%${RESET}`;
|
|
34
|
+
if (intVal <= 69) return `${ORANGE}${val}%${RESET}`;
|
|
35
|
+
return `${RED}${val}%${RESET}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Get file age in seconds
|
|
39
|
+
function getFileAge(filepath) {
|
|
40
|
+
try {
|
|
41
|
+
const stats = statSync(filepath);
|
|
42
|
+
return Math.floor((Date.now() - stats.mtimeMs) / 1000);
|
|
43
|
+
} catch {
|
|
44
|
+
return Infinity;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Read JSON file safely
|
|
49
|
+
function readJsonFile(filepath) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(filepath, 'utf8'));
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Clean up stale lock (older than 30s)
|
|
58
|
+
function cleanStaleLock() {
|
|
59
|
+
if (existsSync(LOCK_DIR) && getFileAge(LOCK_DIR) > 30) {
|
|
60
|
+
try { rmdirSync(LOCK_DIR); } catch {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Acquire lock atomically (mkdir fails if exists)
|
|
65
|
+
function acquireLock() {
|
|
66
|
+
try {
|
|
67
|
+
mkdirSync(LOCK_DIR);
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Release lock
|
|
75
|
+
function releaseLock() {
|
|
76
|
+
try { rmdirSync(LOCK_DIR); } catch {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Log error with timestamp
|
|
80
|
+
function logError(msg) {
|
|
81
|
+
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
82
|
+
try {
|
|
83
|
+
appendFileSync(LOG_FILE, `[${ts}] ${msg}\n`);
|
|
84
|
+
writeFileSync(ERROR_FILE, msg);
|
|
85
|
+
// Trim log to last 50 lines
|
|
86
|
+
const lines = readFileSync(LOG_FILE, 'utf8').split('\n').filter(Boolean);
|
|
87
|
+
if (lines.length > 50) {
|
|
88
|
+
writeFileSync(LOG_FILE, lines.slice(-50).join('\n') + '\n');
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Clear error state
|
|
94
|
+
function clearError() {
|
|
95
|
+
try { writeFileSync(ERROR_FILE, ''); } catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Spawn background refresh process
|
|
99
|
+
function refreshInBackground(token) {
|
|
100
|
+
const child = spawn(process.execPath, [
|
|
101
|
+
'-e',
|
|
102
|
+
`
|
|
103
|
+
const { mkdirSync, rmdirSync, writeFileSync, readFileSync, appendFileSync } = require('fs');
|
|
104
|
+
const { request } = require('https');
|
|
105
|
+
const CACHE_FILE = ${JSON.stringify(CACHE_FILE)};
|
|
106
|
+
const LOCK_DIR = ${JSON.stringify(LOCK_DIR)};
|
|
107
|
+
const ERROR_FILE = ${JSON.stringify(ERROR_FILE)};
|
|
108
|
+
const LOG_FILE = ${JSON.stringify(LOG_FILE)};
|
|
109
|
+
|
|
110
|
+
function logError(msg) {
|
|
111
|
+
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
|
112
|
+
try {
|
|
113
|
+
appendFileSync(LOG_FILE, '[' + ts + '] ' + msg + '\\n');
|
|
114
|
+
writeFileSync(ERROR_FILE, msg);
|
|
115
|
+
const lines = readFileSync(LOG_FILE, 'utf8').split('\\n').filter(Boolean);
|
|
116
|
+
if (lines.length > 50) writeFileSync(LOG_FILE, lines.slice(-50).join('\\n') + '\\n');
|
|
117
|
+
} catch {}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const req = request({
|
|
121
|
+
hostname: 'api.anthropic.com',
|
|
122
|
+
path: '/api/oauth/usage',
|
|
123
|
+
method: 'GET',
|
|
124
|
+
headers: {
|
|
125
|
+
'Authorization': 'Bearer ${token}',
|
|
126
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
127
|
+
'Accept': 'application/json',
|
|
128
|
+
'User-Agent': 'claude-code/2.1.12'
|
|
129
|
+
},
|
|
130
|
+
timeout: 10000
|
|
131
|
+
}, (res) => {
|
|
132
|
+
let data = '';
|
|
133
|
+
res.on('data', chunk => data += chunk);
|
|
134
|
+
res.on('end', () => {
|
|
135
|
+
if (res.statusCode === 200) {
|
|
136
|
+
try {
|
|
137
|
+
JSON.parse(data);
|
|
138
|
+
writeFileSync(CACHE_FILE, data);
|
|
139
|
+
try { writeFileSync(ERROR_FILE, ''); } catch {}
|
|
140
|
+
} catch { logError('Invalid JSON'); }
|
|
141
|
+
} else if (res.statusCode !== 401) {
|
|
142
|
+
// Skip 401 - token not ready yet at startup, will retry next cycle
|
|
143
|
+
logError('HTTP ' + res.statusCode);
|
|
144
|
+
}
|
|
145
|
+
try { rmdirSync(LOCK_DIR); } catch {}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
req.on('error', () => { logError('Connection failed'); try { rmdirSync(LOCK_DIR); } catch {} });
|
|
149
|
+
req.on('timeout', () => { req.destroy(); logError('Timeout'); try { rmdirSync(LOCK_DIR); } catch {} });
|
|
150
|
+
req.end();
|
|
151
|
+
`
|
|
152
|
+
], {
|
|
153
|
+
detached: true,
|
|
154
|
+
stdio: 'ignore'
|
|
155
|
+
});
|
|
156
|
+
child.unref();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Convert UTC ISO time to local HH:mm
|
|
160
|
+
function toLocalTime(isoString) {
|
|
161
|
+
if (!isoString) return '--:--';
|
|
162
|
+
try {
|
|
163
|
+
const date = new Date(isoString);
|
|
164
|
+
// Round to nearest minute
|
|
165
|
+
date.setSeconds(date.getSeconds() + 30);
|
|
166
|
+
return date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
|
|
167
|
+
} catch {
|
|
168
|
+
return '--:--';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Get current git branch name
|
|
173
|
+
function getGitBranch() {
|
|
174
|
+
try {
|
|
175
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
176
|
+
timeout: 1000,
|
|
177
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
178
|
+
}).toString().trim();
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Main
|
|
185
|
+
async function main() {
|
|
186
|
+
// Read stdin
|
|
187
|
+
let input = '';
|
|
188
|
+
for await (const chunk of process.stdin) {
|
|
189
|
+
input += chunk;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Parse Claude Code input
|
|
193
|
+
let model = 'Unknown';
|
|
194
|
+
let contextUsed = 0;
|
|
195
|
+
try {
|
|
196
|
+
const data = JSON.parse(input);
|
|
197
|
+
model = data.model?.display_name || 'Unknown';
|
|
198
|
+
contextUsed = data.context_window?.used_percentage || 0;
|
|
199
|
+
} catch {}
|
|
200
|
+
|
|
201
|
+
// Get OAuth token
|
|
202
|
+
let token = null;
|
|
203
|
+
const creds = readJsonFile(CREDS_FILE);
|
|
204
|
+
if (creds?.claudeAiOauth?.accessToken) {
|
|
205
|
+
token = creds.claudeAiOauth.accessToken;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Read cached quota data (never block on fetch)
|
|
209
|
+
let quotaData = readJsonFile(CACHE_FILE);
|
|
210
|
+
|
|
211
|
+
// Check if refresh needed and spawn background fetch
|
|
212
|
+
if (token) {
|
|
213
|
+
cleanStaleLock();
|
|
214
|
+
const cacheAge = getFileAge(CACHE_FILE);
|
|
215
|
+
const needRefresh = !quotaData || cacheAge >= CACHE_MAX_AGE;
|
|
216
|
+
|
|
217
|
+
if (needRefresh && acquireLock()) {
|
|
218
|
+
refreshInBackground(token);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Parse quota data
|
|
223
|
+
let fiveHourPct = '?';
|
|
224
|
+
let sevenDayPct = '?';
|
|
225
|
+
let resetLocal = '--:--';
|
|
226
|
+
|
|
227
|
+
if (quotaData) {
|
|
228
|
+
if (quotaData.five_hour === null || quotaData.seven_day === null) {
|
|
229
|
+
// Organization/team plan without individual quota
|
|
230
|
+
fiveHourPct = 'N/A';
|
|
231
|
+
sevenDayPct = 'N/A';
|
|
232
|
+
resetLocal = 'N/A';
|
|
233
|
+
} else {
|
|
234
|
+
fiveHourPct = quotaData.five_hour?.utilization ?? '?';
|
|
235
|
+
sevenDayPct = quotaData.seven_day?.utilization ?? '?';
|
|
236
|
+
resetLocal = toLocalTime(quotaData.five_hour?.resets_at);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check for error state
|
|
241
|
+
let hasError = false;
|
|
242
|
+
try {
|
|
243
|
+
const errContent = readFileSync(ERROR_FILE, 'utf8').trim();
|
|
244
|
+
hasError = errContent.length > 0;
|
|
245
|
+
} catch {}
|
|
246
|
+
|
|
247
|
+
// Get git branch
|
|
248
|
+
const branch = getGitBranch();
|
|
249
|
+
|
|
250
|
+
// Build output
|
|
251
|
+
let output = `${branch ? `${YELLOW_BOLD}${branch}${RESET} | ` : ''}${CYAN}${model}${RESET} | ${colorPct(contextUsed)} | ${resetLocal} | 5h:${colorPct(fiveHourPct)} | 7d:${colorPct(sevenDayPct)}`;
|
|
252
|
+
if (hasError) {
|
|
253
|
+
output += ` | ${RED}ERR${RESET}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
process.stdout.write(output);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
main().catch(() => process.exit(1));
|