contextbricks-universal 4.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.
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/bin/cli.js +285 -0
- package/package.json +54 -0
- package/scripts/statusline.js +513 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jeremy Dawes (Jezweb)
|
|
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,218 @@
|
|
|
1
|
+
# ContextBricks Universal
|
|
2
|
+
|
|
3
|
+
> Cross-platform statusline for [Claude Code](https://claude.ai/code) CLI with real-time context brick visualization.
|
|
4
|
+
|
|
5
|
+
**Works on Windows, Linux, and macOS** — pure Node.js, no bash or jq required.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
[Sonnet 4.5] claude-skills:main *↑2 | +145/-23
|
|
9
|
+
[5f2ce67] Remove auth-js skill
|
|
10
|
+
[■■■■■■■■■■■■■□□□□□□□□□□□□□□□□□] 43% | 113k free | 0h12m | $0.87
|
|
11
|
+
5h:64% ~23m | 7d:57% ~1d23h | sonnet:9% ~3d23h
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **Real-time context tracking** — brick visualization of context window usage
|
|
17
|
+
- **Rate limit tracking** — 5-hour and 7-day utilization with reset timers (Max/Pro subscribers)
|
|
18
|
+
- **Official percentage fields** (Claude Code 2.1.6+) with fallback calculation (2.0.70+)
|
|
19
|
+
- **Git integration** — repo, branch, commit hash, message, dirty/ahead/behind indicators
|
|
20
|
+
- **Session metrics** — model name, lines changed, duration, cost (hidden for Max subscribers)
|
|
21
|
+
- **Environment config** — `CONTEXTBRICKS_SHOW_DIR`, `CONTEXTBRICKS_BRICKS`, `CONTEXTBRICKS_SHOW_LIMITS`
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
### Quick Install (Recommended)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g contextbricks-universal
|
|
29
|
+
contextbricks install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or one-liner via npx (installs and runs automatically):
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx contextbricks-universal
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Node.js is the only requirement (already present if you use Claude Code).
|
|
39
|
+
|
|
40
|
+
### From GitHub (without npm)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g github:thebtf/contextbricks-universal
|
|
44
|
+
contextbricks install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### From Source
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/thebtf/contextbricks-universal.git
|
|
51
|
+
cd contextbricks-universal
|
|
52
|
+
node bin/cli.js install
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### What the Installer Does
|
|
56
|
+
|
|
57
|
+
1. Copies `statusline.js` to `~/.claude/statusline.js`
|
|
58
|
+
2. Updates `~/.claude/settings.json` with the `statusLine` command
|
|
59
|
+
3. Backs up existing configuration before any changes
|
|
60
|
+
|
|
61
|
+
## Display Layout
|
|
62
|
+
|
|
63
|
+
### Line 1 — Model + Git + Changes
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
[Sonnet 4.5] claude-skills:main *↑2 | +145/-23
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Line 2 — Commit Details
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
[5f2ce67] Remove auth-js skill
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Line 3 — Context Bricks
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
[■■■■■■■■■■■■■□□□□□□□□□□□□□□□□□] 43% | 113k free | 0h12m | $0.87
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Line 4 — Rate Limits (Max/Pro subscribers)
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
5h:64% ~23m | 7d:57% ~1d23h | sonnet:9% ~3d23h
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Symbol | Meaning |
|
|
88
|
+
|--------|---------|
|
|
89
|
+
| `■` (cyan) | Used context |
|
|
90
|
+
| `□` (dim) | Free space |
|
|
91
|
+
| `*` | Uncommitted changes |
|
|
92
|
+
| `↑3` | Ahead of remote by 3 |
|
|
93
|
+
| `↓2` | Behind remote by 2 |
|
|
94
|
+
| `5h:X%` | 5-hour rolling limit utilization |
|
|
95
|
+
| `7d:X%` | 7-day overall limit utilization |
|
|
96
|
+
| `sonnet:X%` | 7-day Sonnet sub-limit (if applicable) |
|
|
97
|
+
| `opus:X%` | 7-day Opus sub-limit (if applicable) |
|
|
98
|
+
| `~22m` / `~1d23h` | Time until limit resets (exact by default) |
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
| Environment Variable | Default | Description |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `CONTEXTBRICKS_SHOW_DIR` | `1` | Show current subdirectory (`0` to hide) |
|
|
105
|
+
| `CONTEXTBRICKS_BRICKS` | `30` | Number of bricks in the visualization |
|
|
106
|
+
| `CONTEXTBRICKS_SHOW_LIMITS` | `1` | Show rate limit utilization (`0` to hide) |
|
|
107
|
+
| `CONTEXTBRICKS_RESET_EXACT` | `1` | Exact reset times `~1d23h` (`0` for approximate `~1d`) |
|
|
108
|
+
|
|
109
|
+
## How It Works
|
|
110
|
+
|
|
111
|
+
### Context Tracking (Claude Code 2.1.6+)
|
|
112
|
+
|
|
113
|
+
Uses pre-calculated percentage fields:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"context_window": {
|
|
118
|
+
"context_window_size": 200000,
|
|
119
|
+
"used_percentage": 43.5,
|
|
120
|
+
"remaining_percentage": 56.5
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Fallback (Claude Code 2.0.70+)
|
|
126
|
+
|
|
127
|
+
Calculates from `current_usage` token counts when percentage fields are unavailable.
|
|
128
|
+
|
|
129
|
+
### Settings
|
|
130
|
+
|
|
131
|
+
The installer configures `~/.claude/settings.json`:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"statusLine": {
|
|
136
|
+
"type": "command",
|
|
137
|
+
"command": "node ~/.claude/statusline.js",
|
|
138
|
+
"padding": 0
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Rate Limit Tracking
|
|
144
|
+
|
|
145
|
+
Line 4 shows your current utilization of Claude's rate limits — useful for Max and Pro subscribers to avoid hitting caps.
|
|
146
|
+
|
|
147
|
+
### How It Works
|
|
148
|
+
|
|
149
|
+
1. Reads your OAuth token from Claude Code credentials (`~/.claude/.credentials.json`, or macOS keychain)
|
|
150
|
+
2. Fetches usage data from `api.anthropic.com/api/oauth/usage`
|
|
151
|
+
3. Caches the response for 5 minutes (`~/.claude/.usage-cache.json`)
|
|
152
|
+
4. Displays utilization percentages with color coding:
|
|
153
|
+
- **Green** (0-49%) — plenty of capacity
|
|
154
|
+
- **Yellow** (50-79%) — approaching limit
|
|
155
|
+
- **Red** (80-100%) — near or at limit
|
|
156
|
+
|
|
157
|
+
### Requirements
|
|
158
|
+
|
|
159
|
+
- Active Claude Max or Pro subscription with OAuth credentials
|
|
160
|
+
- API-only users will not see Line 4 (gracefully skipped)
|
|
161
|
+
|
|
162
|
+
### Privacy
|
|
163
|
+
|
|
164
|
+
- Your OAuth token is never logged or exposed in command arguments
|
|
165
|
+
- Token is passed to the HTTPS subprocess via environment variable
|
|
166
|
+
- Cache file (`~/.claude/.usage-cache.json`) is stored locally
|
|
167
|
+
|
|
168
|
+
### Troubleshooting
|
|
169
|
+
|
|
170
|
+
| Issue | Solution |
|
|
171
|
+
|-------|----------|
|
|
172
|
+
| Line 4 not showing | Verify `~/.claude/.credentials.json` exists with `claudeAiOauth.accessToken` |
|
|
173
|
+
| Stale data | Delete `~/.claude/.usage-cache.json` to force refresh |
|
|
174
|
+
| Want to hide Line 4 | Set `CONTEXTBRICKS_SHOW_LIMITS=0` |
|
|
175
|
+
|
|
176
|
+
## Testing
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
contextbricks test
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Requirements
|
|
183
|
+
|
|
184
|
+
- **Node.js** >= 14
|
|
185
|
+
- **git** (optional, for git info display)
|
|
186
|
+
|
|
187
|
+
No bash, jq, bc, sed, cut, or any other Unix tools required.
|
|
188
|
+
|
|
189
|
+
## Bash vs Node.js
|
|
190
|
+
|
|
191
|
+
| | Bash (v3.5) | Node.js (v4.x) |
|
|
192
|
+
|---|---|---|
|
|
193
|
+
| **Platform** | Linux/macOS only | Windows + Linux + macOS |
|
|
194
|
+
| **Dependencies** | bash, jq, git, bc, sed, cut | Node.js only (git optional) |
|
|
195
|
+
| **JSON parsing** | External `jq` | Native `JSON.parse()` |
|
|
196
|
+
| **Install** | Shell scripts | `npm i -g contextbricks-universal` |
|
|
197
|
+
| **Output** | Identical | Identical |
|
|
198
|
+
|
|
199
|
+
## Uninstallation
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
contextbricks uninstall
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Or manually:
|
|
206
|
+
1. Delete `~/.claude/statusline.js`
|
|
207
|
+
2. Remove the `statusLine` section from `~/.claude/settings.json`
|
|
208
|
+
3. Restart Claude Code
|
|
209
|
+
|
|
210
|
+
## Acknowledgements
|
|
211
|
+
|
|
212
|
+
This project is a cross-platform Node.js rewrite of [ContextBricks](https://github.com/jezweb/claude-skills/tree/main/tools/statusline) created by [Jeremy Dawes](https://github.com/jezweb) ([jezweb.com.au](https://jezweb.com.au)). The original bash implementation provided the foundation for the statusline format, brick visualization, and git integration.
|
|
213
|
+
|
|
214
|
+
Also inspired by [ccstatusline](https://github.com/sirmalloc/ccstatusline).
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
[MIT](LICENSE) — Copyright (c) 2025 Jeremy Dawes (Jezweb)
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
|
|
10
|
+
const command = process.argv[2];
|
|
11
|
+
const STATUSLINE_SCRIPT = path.join(__dirname, '..', 'scripts', 'statusline.js');
|
|
12
|
+
|
|
13
|
+
// Colors for terminal output
|
|
14
|
+
const c = {
|
|
15
|
+
reset: '\x1b[0m',
|
|
16
|
+
red: '\x1b[31m',
|
|
17
|
+
green: '\x1b[32m',
|
|
18
|
+
yellow: '\x1b[33m',
|
|
19
|
+
cyan: '\x1b[36m',
|
|
20
|
+
bold: '\x1b[1m',
|
|
21
|
+
dim: '\x1b[2m',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Paths
|
|
25
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
26
|
+
const INSTALL_PATH = path.join(CLAUDE_DIR, 'statusline.js');
|
|
27
|
+
const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
|
|
28
|
+
|
|
29
|
+
function checkDependencies() {
|
|
30
|
+
// git is optional but recommended
|
|
31
|
+
const gitResult = spawnSync('git', ['--version'], { stdio: 'pipe', windowsHide: true, timeout: 5000 });
|
|
32
|
+
if (gitResult.status !== 0) {
|
|
33
|
+
console.warn(`${c.yellow}Warning: git not found. Git info will not be available.${c.reset}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`${c.green}Dependencies OK${c.reset} (Node.js ${process.version})`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function backupFile(filePath) {
|
|
40
|
+
if (fs.existsSync(filePath)) {
|
|
41
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
42
|
+
const backupPath = `${filePath}.backup-${timestamp}`;
|
|
43
|
+
fs.copyFileSync(filePath, backupPath);
|
|
44
|
+
return backupPath;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function install() {
|
|
50
|
+
console.log(`\n${c.cyan}${c.bold}ContextBricks${c.reset} - Claude Code Status Line Installer\n`);
|
|
51
|
+
|
|
52
|
+
console.log('Checking dependencies...');
|
|
53
|
+
checkDependencies();
|
|
54
|
+
console.log('');
|
|
55
|
+
|
|
56
|
+
// Ensure ~/.claude exists
|
|
57
|
+
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
58
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
59
|
+
console.log(`Created: ${CLAUDE_DIR}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Backup existing statusline script
|
|
63
|
+
const scriptBackup = backupFile(INSTALL_PATH);
|
|
64
|
+
if (scriptBackup) {
|
|
65
|
+
console.log(`Backed up existing script: ${scriptBackup}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Copy statusline.js
|
|
69
|
+
console.log('Installing status line script...');
|
|
70
|
+
if (!fs.existsSync(STATUSLINE_SCRIPT)) {
|
|
71
|
+
console.error(`${c.red}Error: Source script not found: ${STATUSLINE_SCRIPT}${c.reset}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
fs.copyFileSync(STATUSLINE_SCRIPT, INSTALL_PATH);
|
|
75
|
+
console.log(` Installed: ${INSTALL_PATH}`);
|
|
76
|
+
console.log('');
|
|
77
|
+
|
|
78
|
+
// Build the command string for settings.json
|
|
79
|
+
// Use forward slashes for cross-platform compatibility
|
|
80
|
+
const homedir = os.homedir().replace(/\\/g, '/');
|
|
81
|
+
const statuslineCommand = `node ${homedir}/.claude/statusline.js`;
|
|
82
|
+
|
|
83
|
+
// Update settings.json
|
|
84
|
+
const settingsBackup = backupFile(SETTINGS_FILE);
|
|
85
|
+
if (settingsBackup) {
|
|
86
|
+
console.log(`Backed up settings: ${settingsBackup}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let settings = {};
|
|
90
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
91
|
+
try {
|
|
92
|
+
settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
|
93
|
+
} catch {
|
|
94
|
+
console.warn(`${c.yellow}Warning: Could not parse settings.json, creating new one${c.reset}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
settings.statusLine = {
|
|
99
|
+
type: 'command',
|
|
100
|
+
command: statuslineCommand,
|
|
101
|
+
padding: 0,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
106
|
+
console.log(' Settings updated');
|
|
107
|
+
} catch (err) {
|
|
108
|
+
console.error(`${c.red}Error: Could not write settings.json: ${err.message}${c.reset}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(`
|
|
113
|
+
${c.green}Installation complete!${c.reset}
|
|
114
|
+
|
|
115
|
+
${c.bold}Your status line will show:${c.reset}
|
|
116
|
+
- Model name (Sonnet 4.5, Opus 4, etc.)
|
|
117
|
+
- Git repo:branch [commit] message
|
|
118
|
+
- Git status indicators (*uncommitted, ↑ahead, ↓behind)
|
|
119
|
+
- Lines changed this session (+added/-removed)
|
|
120
|
+
- Real-time context usage with brick visualization
|
|
121
|
+
- Session duration and cost
|
|
122
|
+
|
|
123
|
+
${c.cyan}Restart Claude Code to see your new status line!${c.reset}
|
|
124
|
+
|
|
125
|
+
To uninstall: contextbricks uninstall
|
|
126
|
+
`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function uninstall() {
|
|
130
|
+
console.log(`\n${c.cyan}${c.bold}ContextBricks${c.reset} - Uninstaller\n`);
|
|
131
|
+
|
|
132
|
+
// Remove statusline script
|
|
133
|
+
if (fs.existsSync(INSTALL_PATH)) {
|
|
134
|
+
console.log('Removing status line script...');
|
|
135
|
+
fs.unlinkSync(INSTALL_PATH);
|
|
136
|
+
console.log(` Removed: ${INSTALL_PATH}`);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(`${c.yellow}Status line script not found (already removed?)${c.reset}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log('');
|
|
142
|
+
|
|
143
|
+
// Update settings.json - remove statusLine key
|
|
144
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
145
|
+
try {
|
|
146
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf8'));
|
|
147
|
+
if (settings.statusLine) {
|
|
148
|
+
delete settings.statusLine;
|
|
149
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
150
|
+
console.log('Removed statusLine from settings.json');
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
console.warn(`${c.yellow}Warning: Could not update settings.json${c.reset}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// List backups for manual cleanup
|
|
158
|
+
const backups = [];
|
|
159
|
+
try {
|
|
160
|
+
const files = fs.readdirSync(CLAUDE_DIR);
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
if (file.startsWith('statusline.js.backup-') || file.startsWith('settings.json.backup-')) {
|
|
163
|
+
backups.push(path.join(CLAUDE_DIR, file));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// ignore
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (backups.length > 0) {
|
|
171
|
+
console.log(`\nFound ${backups.length} backup file(s).`);
|
|
172
|
+
console.log('Keeping backups (delete manually if needed):');
|
|
173
|
+
for (const b of backups) {
|
|
174
|
+
console.log(` ${b}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(`\n${c.green}Uninstallation complete!${c.reset}`);
|
|
179
|
+
console.log(`${c.cyan}Restart Claude Code for changes to take effect.${c.reset}\n`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function showHelp() {
|
|
183
|
+
console.log(`
|
|
184
|
+
${c.cyan}${c.bold}ContextBricks${c.reset} - Claude Code Status Line (Cross-Platform)
|
|
185
|
+
|
|
186
|
+
${c.green}Usage:${c.reset}
|
|
187
|
+
contextbricks Install status line (default)
|
|
188
|
+
contextbricks install Install status line
|
|
189
|
+
contextbricks uninstall Uninstall status line
|
|
190
|
+
contextbricks test Test with sample data
|
|
191
|
+
contextbricks --help Show this help
|
|
192
|
+
contextbricks --version Show version
|
|
193
|
+
|
|
194
|
+
${c.green}Features:${c.reset}
|
|
195
|
+
- Real-time context tracking with brick visualization
|
|
196
|
+
- Git integration (repo, branch, commit, status)
|
|
197
|
+
- Session metrics (duration, cost, lines changed)
|
|
198
|
+
- Works on ${c.bold}Windows${c.reset}, Linux, and macOS (no bash/jq required)
|
|
199
|
+
|
|
200
|
+
${c.green}More Info:${c.reset}
|
|
201
|
+
GitHub: https://github.com/thebtf/contextbricks-universal
|
|
202
|
+
Issues: https://github.com/thebtf/contextbricks-universal/issues
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function test() {
|
|
207
|
+
console.log(`${c.cyan}Testing statusline with sample data...${c.reset}\n`);
|
|
208
|
+
|
|
209
|
+
const now = Date.now();
|
|
210
|
+
const sampleData = JSON.stringify({
|
|
211
|
+
model: { display_name: 'Sonnet 4.5' },
|
|
212
|
+
workspace: { current_dir: process.cwd() },
|
|
213
|
+
context_window: {
|
|
214
|
+
context_window_size: 200000,
|
|
215
|
+
used_percentage: 43.5,
|
|
216
|
+
remaining_percentage: 56.5,
|
|
217
|
+
},
|
|
218
|
+
cost: {
|
|
219
|
+
total_duration_ms: 765000,
|
|
220
|
+
total_cost_usd: 0.87,
|
|
221
|
+
total_lines_added: 145,
|
|
222
|
+
total_lines_removed: 23,
|
|
223
|
+
},
|
|
224
|
+
_mock_rate_limits: {
|
|
225
|
+
five_hour: { utilization: 64.0, resets_at: new Date(now + 23 * 60000).toISOString() },
|
|
226
|
+
seven_day: { utilization: 57.0, resets_at: new Date(now + 2 * 86400000).toISOString() },
|
|
227
|
+
seven_day_sonnet: { utilization: 9.0, resets_at: new Date(now + 4 * 86400000).toISOString() },
|
|
228
|
+
seven_day_opus: null,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = spawnSync(process.execPath, [STATUSLINE_SCRIPT], {
|
|
233
|
+
input: sampleData,
|
|
234
|
+
encoding: 'utf8',
|
|
235
|
+
windowsHide: true,
|
|
236
|
+
timeout: 10000,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (result.stdout) {
|
|
240
|
+
process.stdout.write(result.stdout);
|
|
241
|
+
}
|
|
242
|
+
if (result.stderr) {
|
|
243
|
+
process.stderr.write(result.stderr);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log(`\n${c.dim}--- Test complete ---${c.reset}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Main
|
|
250
|
+
switch (command) {
|
|
251
|
+
case 'install':
|
|
252
|
+
case 'init':
|
|
253
|
+
install();
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case 'uninstall':
|
|
257
|
+
uninstall();
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case 'test':
|
|
261
|
+
test();
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case '--version':
|
|
265
|
+
case '-v': {
|
|
266
|
+
const pkg = require('../package.json');
|
|
267
|
+
console.log(`contextbricks-universal v${pkg.version}`);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
case '--help':
|
|
272
|
+
case '-h':
|
|
273
|
+
case 'help':
|
|
274
|
+
showHelp();
|
|
275
|
+
break;
|
|
276
|
+
|
|
277
|
+
default:
|
|
278
|
+
if (command) {
|
|
279
|
+
console.error(`${c.red}Unknown command: ${command}${c.reset}\n`);
|
|
280
|
+
showHelp();
|
|
281
|
+
process.exit(1);
|
|
282
|
+
} else {
|
|
283
|
+
install();
|
|
284
|
+
}
|
|
285
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "contextbricks-universal",
|
|
3
|
+
"version": "4.2.0",
|
|
4
|
+
"description": "Universal cross-platform statusline for Claude Code CLI with context brick visualization. Windows, Linux, macOS. No bash or jq required.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"statusline",
|
|
9
|
+
"cli",
|
|
10
|
+
"context",
|
|
11
|
+
"bricks",
|
|
12
|
+
"git",
|
|
13
|
+
"tokens",
|
|
14
|
+
"terminal",
|
|
15
|
+
"visualization",
|
|
16
|
+
"windows",
|
|
17
|
+
"cross-platform"
|
|
18
|
+
],
|
|
19
|
+
"bin": {
|
|
20
|
+
"contextbricks": "./bin/cli.js",
|
|
21
|
+
"contextbricks-universal": "./bin/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"main": "./scripts/statusline.js",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "node bin/cli.js test",
|
|
26
|
+
"postinstall": "node bin/cli.js install"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/",
|
|
30
|
+
"scripts/",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=14"
|
|
36
|
+
},
|
|
37
|
+
"os": [
|
|
38
|
+
"win32",
|
|
39
|
+
"linux",
|
|
40
|
+
"darwin"
|
|
41
|
+
],
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "https://github.com/thebtf/contextbricks-universal.git"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/thebtf/contextbricks-universal/issues"
|
|
48
|
+
},
|
|
49
|
+
"contributors": [
|
|
50
|
+
"Jeremy Dawes <jeremy@jezweb.net> (https://jezweb.com.au)"
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"homepage": "https://github.com/thebtf/contextbricks-universal"
|
|
54
|
+
}
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Claude Code Custom Status Line (Node.js / Cross-Platform)
|
|
4
|
+
// v4.2.0 - Node.js rewrite for Windows + Linux + macOS
|
|
5
|
+
// Line 1: Model | Repo:Branch [subdir] | git status | lines changed
|
|
6
|
+
// Line 2: [commit] commit message
|
|
7
|
+
// Line 3: Context bricks | percentage | free | duration | cost
|
|
8
|
+
// Line 4: Rate limit utilization (5h, 7d Sonnet, 7d Opus) — Max/Pro subscribers
|
|
9
|
+
//
|
|
10
|
+
// Configuration via environment variables:
|
|
11
|
+
// CONTEXTBRICKS_SHOW_DIR=1 Show current subdirectory (default: 1)
|
|
12
|
+
// CONTEXTBRICKS_SHOW_DIR=0 Hide subdirectory
|
|
13
|
+
// CONTEXTBRICKS_BRICKS=40 Number of bricks (default: 30)
|
|
14
|
+
// CONTEXTBRICKS_SHOW_LIMITS=0 Hide rate limit line (default: shown)
|
|
15
|
+
// CONTEXTBRICKS_RESET_EXACT=0 Approximate reset times (default: exact)
|
|
16
|
+
//
|
|
17
|
+
// Uses new percentage fields (Claude Code 2.1.6+) for accurate context display.
|
|
18
|
+
// Falls back to current_usage calculation for older versions.
|
|
19
|
+
// See: https://code.claude.com/docs/en/statusline
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const { spawnSync } = require('child_process');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const fs = require('fs');
|
|
26
|
+
const os = require('os');
|
|
27
|
+
|
|
28
|
+
const MAX_STDIN_BYTES = 1024 * 1024; // 1MB safety limit
|
|
29
|
+
|
|
30
|
+
// Read all stdin synchronously
|
|
31
|
+
function readStdin() {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
const BUFSIZE = 4096;
|
|
34
|
+
const buf = Buffer.alloc(BUFSIZE);
|
|
35
|
+
let totalRead = 0;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const fd = process.stdin.fd;
|
|
39
|
+
while (totalRead < MAX_STDIN_BYTES) {
|
|
40
|
+
try {
|
|
41
|
+
const bytesRead = fs.readSync(fd, buf, 0, BUFSIZE, null);
|
|
42
|
+
if (bytesRead === 0) break;
|
|
43
|
+
chunks.push(Buffer.from(buf.slice(0, bytesRead)));
|
|
44
|
+
totalRead += bytesRead;
|
|
45
|
+
} catch {
|
|
46
|
+
break; // EAGAIN or EOF
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// fd not readable
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Resolve the working directory for git commands
|
|
57
|
+
function resolveGitCwd(currentDir) {
|
|
58
|
+
try {
|
|
59
|
+
if (currentDir && fs.statSync(currentDir).isDirectory()) {
|
|
60
|
+
return currentDir;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// invalid path
|
|
64
|
+
}
|
|
65
|
+
return process.cwd();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Run a git command using spawnSync (no shell injection possible)
|
|
69
|
+
function git(args, cwd, fallback = '') {
|
|
70
|
+
try {
|
|
71
|
+
const result = spawnSync('git', args, {
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
74
|
+
timeout: 5000,
|
|
75
|
+
windowsHide: true,
|
|
76
|
+
cwd,
|
|
77
|
+
});
|
|
78
|
+
if (result.status === 0 && result.stdout) {
|
|
79
|
+
return result.stdout.trim();
|
|
80
|
+
}
|
|
81
|
+
return fallback;
|
|
82
|
+
} catch {
|
|
83
|
+
return fallback;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Safely traverse nested object path like 'a.b.c'
|
|
88
|
+
function getPath(obj, dotPath) {
|
|
89
|
+
const parts = dotPath.split('.');
|
|
90
|
+
let current = obj;
|
|
91
|
+
for (const part of parts) {
|
|
92
|
+
if (current == null || typeof current !== 'object') return undefined;
|
|
93
|
+
current = current[part];
|
|
94
|
+
}
|
|
95
|
+
return current;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ANSI color helpers
|
|
99
|
+
const c = {
|
|
100
|
+
reset: '\x1b[0m',
|
|
101
|
+
bold: '\x1b[1m',
|
|
102
|
+
dim: '\x1b[2m',
|
|
103
|
+
cyan: '\x1b[1;36m',
|
|
104
|
+
green: '\x1b[1;32m',
|
|
105
|
+
blue: '\x1b[1;34m',
|
|
106
|
+
red: '\x1b[1;31m',
|
|
107
|
+
yellow: '\x1b[1;33m',
|
|
108
|
+
dimWhite: '\x1b[2;37m',
|
|
109
|
+
greenNorm: '\x1b[0;32m',
|
|
110
|
+
redNorm: '\x1b[0;31m',
|
|
111
|
+
cyanNorm: '\x1b[0;36m',
|
|
112
|
+
yellowNorm: '\x1b[0;33m',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Read OAuth token from Claude Code credentials
|
|
116
|
+
function readOAuthToken() {
|
|
117
|
+
try {
|
|
118
|
+
if (process.platform === 'darwin') {
|
|
119
|
+
// macOS: try keychain first
|
|
120
|
+
const result = spawnSync('security', [
|
|
121
|
+
'find-generic-password',
|
|
122
|
+
'-s', 'Claude Code-credentials',
|
|
123
|
+
'-w',
|
|
124
|
+
], {
|
|
125
|
+
encoding: 'utf8',
|
|
126
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
127
|
+
timeout: 3000,
|
|
128
|
+
windowsHide: true,
|
|
129
|
+
});
|
|
130
|
+
if (result.status === 0 && result.stdout) {
|
|
131
|
+
try {
|
|
132
|
+
const creds = JSON.parse(result.stdout.trim());
|
|
133
|
+
const token = getPath(creds, 'claudeAiOauth.accessToken');
|
|
134
|
+
if (token) return token;
|
|
135
|
+
} catch {
|
|
136
|
+
// keychain data not valid JSON, fall through to file
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Win/Linux (and macOS fallback): read credentials file
|
|
142
|
+
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
143
|
+
const raw = fs.readFileSync(credPath, 'utf8');
|
|
144
|
+
const creds = JSON.parse(raw);
|
|
145
|
+
return getPath(creds, 'claudeAiOauth.accessToken') || null;
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fetch usage data from Anthropic API with file-based caching
|
|
152
|
+
function fetchUsageData(token, input) {
|
|
153
|
+
// Check for mock data in input (test mode)
|
|
154
|
+
const mockData = getPath(input, '_mock_rate_limits');
|
|
155
|
+
if (mockData) return mockData;
|
|
156
|
+
|
|
157
|
+
if (!token) return null;
|
|
158
|
+
|
|
159
|
+
const cacheFile = path.join(os.homedir(), '.claude', '.usage-cache.json');
|
|
160
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
161
|
+
|
|
162
|
+
// Try cache first
|
|
163
|
+
try {
|
|
164
|
+
const cacheRaw = fs.readFileSync(cacheFile, 'utf8');
|
|
165
|
+
const cache = JSON.parse(cacheRaw);
|
|
166
|
+
if (cache.timestamp && (Date.now() - cache.timestamp) < CACHE_TTL_MS) {
|
|
167
|
+
return cache.data;
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// no cache or invalid
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Fetch from API using sync subprocess (token via env var for security)
|
|
174
|
+
const httpsScript = `
|
|
175
|
+
const https = require('https');
|
|
176
|
+
const options = {
|
|
177
|
+
hostname: 'api.anthropic.com',
|
|
178
|
+
path: '/api/oauth/usage',
|
|
179
|
+
method: 'GET',
|
|
180
|
+
headers: {
|
|
181
|
+
'Authorization': 'Bearer ' + process.env.ANTHROPIC_TOKEN,
|
|
182
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
183
|
+
'Accept': 'application/json',
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
const req = https.request(options, (res) => {
|
|
187
|
+
let body = '';
|
|
188
|
+
res.on('data', (chunk) => body += chunk);
|
|
189
|
+
res.on('end', () => {
|
|
190
|
+
if (res.statusCode === 200) {
|
|
191
|
+
process.stdout.write(body);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
req.on('error', () => {});
|
|
196
|
+
req.end();
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const result = spawnSync(process.execPath, ['-e', httpsScript], {
|
|
201
|
+
encoding: 'utf8',
|
|
202
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
203
|
+
timeout: 4000,
|
|
204
|
+
windowsHide: true,
|
|
205
|
+
env: { ...process.env, ANTHROPIC_TOKEN: token },
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (result.status === 0 && result.stdout) {
|
|
209
|
+
const data = JSON.parse(result.stdout);
|
|
210
|
+
// Only cache valid usage data (not error responses)
|
|
211
|
+
if (data && (data.five_hour || data.seven_day || data.seven_day_sonnet || data.seven_day_opus)) {
|
|
212
|
+
try {
|
|
213
|
+
fs.writeFileSync(cacheFile, JSON.stringify({ timestamp: Date.now(), data }), {
|
|
214
|
+
encoding: 'utf8',
|
|
215
|
+
mode: 0o600,
|
|
216
|
+
});
|
|
217
|
+
} catch {
|
|
218
|
+
// cache write failure is non-fatal
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return data;
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
// API call failed — try stale cache
|
|
225
|
+
try {
|
|
226
|
+
const cacheRaw = fs.readFileSync(cacheFile, 'utf8');
|
|
227
|
+
const cache = JSON.parse(cacheRaw);
|
|
228
|
+
if (cache.data) return cache.data;
|
|
229
|
+
} catch {
|
|
230
|
+
// no stale cache
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Return 256-color ANSI code for smooth green → yellow → red gradient
|
|
238
|
+
function getColorForUtilization(pct) {
|
|
239
|
+
// 256-color: green(46) → yellow(226) → red(196), 11 stops at ~10% intervals
|
|
240
|
+
const gradient = [46, 82, 118, 154, 190, 226, 220, 214, 208, 202, 196];
|
|
241
|
+
const clamped = Math.max(0, Math.min(100, pct));
|
|
242
|
+
const idx = Math.min(Math.round(clamped / 10), gradient.length - 1);
|
|
243
|
+
return `\x1b[38;5;${gradient[idx]}m`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Format ISO reset time string to human-readable relative time
|
|
247
|
+
// exact=true: "1h30m", "2d5h" | exact=false: "1h", "2d"
|
|
248
|
+
function formatResetTime(isoStr, exact) {
|
|
249
|
+
if (!isoStr) return '';
|
|
250
|
+
try {
|
|
251
|
+
const resetMs = new Date(isoStr).getTime();
|
|
252
|
+
const diffMs = resetMs - Date.now();
|
|
253
|
+
if (diffMs <= 0) return '0m';
|
|
254
|
+
|
|
255
|
+
const totalMin = Math.floor(diffMs / 60000);
|
|
256
|
+
const totalHours = Math.floor(totalMin / 60);
|
|
257
|
+
const remainMin = totalMin % 60;
|
|
258
|
+
const days = Math.floor(totalHours / 24);
|
|
259
|
+
const remainHours = totalHours % 24;
|
|
260
|
+
|
|
261
|
+
if (!exact) {
|
|
262
|
+
if (totalMin < 60) return `${totalMin}m`;
|
|
263
|
+
if (totalHours < 24) return `${totalHours}h`;
|
|
264
|
+
return `${days}d`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Exact mode: combined units
|
|
268
|
+
if (totalMin < 60) return `${totalMin}m`;
|
|
269
|
+
if (totalHours < 24) {
|
|
270
|
+
return remainMin > 0 ? `${totalHours}h${remainMin}m` : `${totalHours}h`;
|
|
271
|
+
}
|
|
272
|
+
return remainHours > 0 ? `${days}d${remainHours}h` : `${days}d`;
|
|
273
|
+
} catch {
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Build a single rate limit segment like "5h:23% ~1h30m"
|
|
279
|
+
function buildLimitSegment(data, key, label, exact) {
|
|
280
|
+
const pct = getPath(data, `${key}.utilization`);
|
|
281
|
+
if (pct == null) return null;
|
|
282
|
+
const color = getColorForUtilization(pct);
|
|
283
|
+
const resetStr = formatResetTime(getPath(data, `${key}.resets_at`), exact);
|
|
284
|
+
let segment = `${c.dimWhite}${label}:${c.reset}${color}${Math.round(pct)}%${c.reset}`;
|
|
285
|
+
if (resetStr) segment += ` ${c.dim}~${resetStr}${c.reset}`;
|
|
286
|
+
return segment;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Assemble rate limit Line 4 from usage data
|
|
290
|
+
function formatRateLimitLine(data) {
|
|
291
|
+
if (!data) return '';
|
|
292
|
+
const exact = process.env.CONTEXTBRICKS_RESET_EXACT !== '0'; // default: exact
|
|
293
|
+
const segments = [
|
|
294
|
+
buildLimitSegment(data, 'five_hour', '5h', exact),
|
|
295
|
+
buildLimitSegment(data, 'seven_day', '7d', exact),
|
|
296
|
+
buildLimitSegment(data, 'seven_day_sonnet', 'sonnet', exact),
|
|
297
|
+
buildLimitSegment(data, 'seven_day_opus', 'opus', exact),
|
|
298
|
+
].filter(Boolean);
|
|
299
|
+
return segments.length > 0 ? segments.join(' | ') : '';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function main() {
|
|
303
|
+
// Read JSON from stdin
|
|
304
|
+
const raw = readStdin();
|
|
305
|
+
if (!raw) {
|
|
306
|
+
process.stdout.write('ContextBricks: no input\n');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let input;
|
|
311
|
+
try {
|
|
312
|
+
input = JSON.parse(raw);
|
|
313
|
+
} catch {
|
|
314
|
+
process.stdout.write('ContextBricks: invalid JSON\n');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Parse Claude data
|
|
319
|
+
const model = (getPath(input, 'model.display_name') || 'Claude').replace('Claude ', '');
|
|
320
|
+
const currentDir = getPath(input, 'workspace.current_dir') || process.cwd();
|
|
321
|
+
const linesAdded = Number(getPath(input, 'cost.total_lines_added')) || 0;
|
|
322
|
+
const linesRemoved = Number(getPath(input, 'cost.total_lines_removed')) || 0;
|
|
323
|
+
|
|
324
|
+
// Configuration from environment variables
|
|
325
|
+
const showDir = process.env.CONTEXTBRICKS_SHOW_DIR !== '0'; // default: on
|
|
326
|
+
const totalBricks = Math.max(1, Number(process.env.CONTEXTBRICKS_BRICKS) || 30);
|
|
327
|
+
|
|
328
|
+
// Resolve working directory (no process.chdir — pass cwd to git instead)
|
|
329
|
+
const cwd = resolveGitCwd(currentDir);
|
|
330
|
+
|
|
331
|
+
// Get git information
|
|
332
|
+
let repoName = '';
|
|
333
|
+
let branch = '';
|
|
334
|
+
let commitShort = '';
|
|
335
|
+
let commitMsg = '';
|
|
336
|
+
let gitStatus = '';
|
|
337
|
+
let subDir = ''; // path relative to repo root
|
|
338
|
+
|
|
339
|
+
const gitDir = git(['rev-parse', '--git-dir'], cwd);
|
|
340
|
+
if (gitDir) {
|
|
341
|
+
const toplevel = git(['rev-parse', '--show-toplevel'], cwd);
|
|
342
|
+
repoName = toplevel ? path.basename(toplevel) : '';
|
|
343
|
+
branch = git(['branch', '--show-current'], cwd) || 'detached';
|
|
344
|
+
|
|
345
|
+
// Compute subdirectory relative to repo root
|
|
346
|
+
if (showDir && toplevel) {
|
|
347
|
+
const rel = path.relative(toplevel, cwd).replace(/\\/g, '/');
|
|
348
|
+
if (rel && rel !== '.') {
|
|
349
|
+
subDir = rel;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
commitShort = git(['rev-parse', '--short', 'HEAD'], cwd);
|
|
353
|
+
|
|
354
|
+
// Fetch commit message once, reuse for both lines
|
|
355
|
+
commitMsg = git(['log', '-1', '--pretty=format:%s'], cwd);
|
|
356
|
+
|
|
357
|
+
// Git status indicators
|
|
358
|
+
const porcelain = git(['status', '--porcelain'], cwd);
|
|
359
|
+
if (porcelain) {
|
|
360
|
+
gitStatus = '*';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Check ahead/behind remote
|
|
364
|
+
const upstream = git(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], cwd);
|
|
365
|
+
if (upstream) {
|
|
366
|
+
const ahead = Number(git(['rev-list', '--count', `${upstream}..HEAD`], cwd)) || 0;
|
|
367
|
+
const behind = Number(git(['rev-list', '--count', `HEAD..${upstream}`], cwd)) || 0;
|
|
368
|
+
if (ahead > 0) gitStatus += `\u2191${ahead}`;
|
|
369
|
+
if (behind > 0) gitStatus += `\u2193${behind}`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// === Build Line 1: Model + Repo:Branch + Status + Changes ===
|
|
374
|
+
let line1 = '';
|
|
375
|
+
|
|
376
|
+
// Model in brackets
|
|
377
|
+
line1 += `${c.cyan}[${model}]${c.reset} `;
|
|
378
|
+
|
|
379
|
+
// Repo:Branch + subdirectory
|
|
380
|
+
if (repoName) {
|
|
381
|
+
line1 += `${c.green}${repoName}${c.reset}`;
|
|
382
|
+
if (branch) {
|
|
383
|
+
line1 += `:${c.blue}${branch}${c.reset}`;
|
|
384
|
+
}
|
|
385
|
+
if (subDir) {
|
|
386
|
+
line1 += ` ${c.dim}${subDir}${c.reset}`;
|
|
387
|
+
}
|
|
388
|
+
} else if (showDir) {
|
|
389
|
+
// No git repo — show tilde-compressed path
|
|
390
|
+
const home = os.homedir();
|
|
391
|
+
let displayPath = cwd.replace(/\\/g, '/');
|
|
392
|
+
const homeNorm = home.replace(/\\/g, '/');
|
|
393
|
+
if (displayPath.startsWith(homeNorm)) {
|
|
394
|
+
displayPath = '~' + displayPath.slice(homeNorm.length);
|
|
395
|
+
}
|
|
396
|
+
line1 += `${c.dim}${displayPath}${c.reset}`;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Git status indicators
|
|
400
|
+
if (gitStatus) {
|
|
401
|
+
line1 += ` ${c.red}${gitStatus}${c.reset}`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Lines changed
|
|
405
|
+
if (linesAdded > 0 || linesRemoved > 0) {
|
|
406
|
+
line1 += ` | ${c.greenNorm}+${linesAdded}${c.reset}/${c.redNorm}-${linesRemoved}${c.reset}`;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// === Build Line 2: Commit hash + message ===
|
|
410
|
+
let line2 = '';
|
|
411
|
+
if (commitShort) {
|
|
412
|
+
line2 += `${c.yellow}[${commitShort}]${c.reset}`;
|
|
413
|
+
if (commitMsg) {
|
|
414
|
+
const truncatedMsg = commitMsg.length > 55 ? commitMsg.substring(0, 55) + '...' : commitMsg;
|
|
415
|
+
line2 += ` ${truncatedMsg}`;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// === Build Line 3: Context bricks + session info ===
|
|
420
|
+
|
|
421
|
+
// Session duration
|
|
422
|
+
const durationMs = Number(getPath(input, 'cost.total_duration_ms')) || 0;
|
|
423
|
+
const durationHours = Math.floor(durationMs / 3600000);
|
|
424
|
+
const durationMin = Math.floor((durationMs % 3600000) / 60000);
|
|
425
|
+
|
|
426
|
+
// Session cost
|
|
427
|
+
const costUsd = Number(getPath(input, 'cost.total_cost_usd')) || 0;
|
|
428
|
+
|
|
429
|
+
// Context window data
|
|
430
|
+
const totalTokens = Number(getPath(input, 'context_window.context_window_size')) || 200000;
|
|
431
|
+
|
|
432
|
+
// Try new percentage fields first (Claude Code 2.1.6+)
|
|
433
|
+
const usedPctRaw = getPath(input, 'context_window.used_percentage');
|
|
434
|
+
const remainingPctRaw = getPath(input, 'context_window.remaining_percentage');
|
|
435
|
+
|
|
436
|
+
let usagePct, usedTokens, freeTokens;
|
|
437
|
+
|
|
438
|
+
if (usedPctRaw != null && usedPctRaw !== '') {
|
|
439
|
+
// Use official percentage (more accurate)
|
|
440
|
+
usagePct = Math.floor(Number(usedPctRaw));
|
|
441
|
+
const remainingPct = Math.floor(Number(remainingPctRaw) || (100 - usagePct));
|
|
442
|
+
usedTokens = Math.floor((totalTokens * usagePct) / 100);
|
|
443
|
+
freeTokens = Math.floor((totalTokens * remainingPct) / 100);
|
|
444
|
+
} else {
|
|
445
|
+
// Fallback: Calculate from current_usage (Claude Code 2.0.70+)
|
|
446
|
+
const currentUsage = getPath(input, 'context_window.current_usage');
|
|
447
|
+
|
|
448
|
+
if (currentUsage && typeof currentUsage === 'object') {
|
|
449
|
+
const inputTokens = Number(currentUsage.input_tokens) || 0;
|
|
450
|
+
const cacheCreation = Number(currentUsage.cache_creation_input_tokens) || 0;
|
|
451
|
+
const cacheRead = Number(currentUsage.cache_read_input_tokens) || 0;
|
|
452
|
+
usedTokens = inputTokens + cacheCreation + cacheRead;
|
|
453
|
+
} else {
|
|
454
|
+
usedTokens = 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
freeTokens = totalTokens - usedTokens;
|
|
458
|
+
usagePct = totalTokens > 0 ? Math.floor((usedTokens * 100) / totalTokens) : 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Convert to 'k' format
|
|
462
|
+
const freeK = Math.floor(freeTokens / 1000);
|
|
463
|
+
|
|
464
|
+
// Generate brick visualization
|
|
465
|
+
const usedBricks = totalTokens > 0 ? Math.floor((usedTokens * totalBricks) / totalTokens) : 0;
|
|
466
|
+
const freeBricks = totalBricks - usedBricks;
|
|
467
|
+
|
|
468
|
+
// Build brick line
|
|
469
|
+
let brickLine = '[';
|
|
470
|
+
|
|
471
|
+
// Used bricks (cyan)
|
|
472
|
+
for (let i = 0; i < usedBricks; i++) {
|
|
473
|
+
brickLine += `${c.cyanNorm}\u25A0${c.reset}`;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Free bricks (dim/gray hollow squares)
|
|
477
|
+
for (let i = 0; i < freeBricks; i++) {
|
|
478
|
+
brickLine += `${c.dimWhite}\u25A1${c.reset}`;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
brickLine += ']';
|
|
482
|
+
|
|
483
|
+
// Compact stats
|
|
484
|
+
brickLine += ` ${c.bold}${usagePct}%${c.reset}`;
|
|
485
|
+
brickLine += ` | ${c.greenNorm}${freeK}k free${c.reset}`;
|
|
486
|
+
brickLine += ` | ${durationHours}h${durationMin}m`;
|
|
487
|
+
|
|
488
|
+
// Cost (only if non-zero)
|
|
489
|
+
if (costUsd > 0) {
|
|
490
|
+
const costFormatted = costUsd.toFixed(2);
|
|
491
|
+
brickLine += ` | ${c.yellowNorm}$${costFormatted}${c.reset}`;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Output all lines
|
|
495
|
+
process.stdout.write(line1 + '\n');
|
|
496
|
+
if (line2) {
|
|
497
|
+
process.stdout.write(line2 + '\n');
|
|
498
|
+
}
|
|
499
|
+
process.stdout.write(brickLine + '\n');
|
|
500
|
+
|
|
501
|
+
// Line 4: Rate limit utilization
|
|
502
|
+
const showLimits = process.env.CONTEXTBRICKS_SHOW_LIMITS !== '0';
|
|
503
|
+
if (showLimits) {
|
|
504
|
+
const token = readOAuthToken();
|
|
505
|
+
const usageData = fetchUsageData(token, input);
|
|
506
|
+
const line4 = formatRateLimitLine(usageData);
|
|
507
|
+
if (line4) {
|
|
508
|
+
process.stdout.write(line4 + '\n');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
main();
|