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 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
+ [![npm](https://img.shields.io/npm/v/claude-simple-status)](https://www.npmjs.com/package/claude-simple-status)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)
10
+ [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)]()
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
+ ![statusline screenshot](assets/statusline.png)
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));