@daliovic/cc-statusline 1.0.0 → 1.1.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 +21 -6
- package/dist/config.js +50 -0
- package/dist/statusline.js +33 -20
- package/dist/wizard.js +163 -0
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -18,14 +18,14 @@ A minimal, informative statusline for [Claude Code](https://claude.ai/claude-cod
|
|
|
18
18
|
### npm (recommended)
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npm install -g @daliovic/
|
|
21
|
+
npm install -g @daliovic/cc-statusline
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
### From source
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
git clone https://github.com/
|
|
28
|
-
cd
|
|
27
|
+
git clone https://github.com/daliovic/cc-statusline.git
|
|
28
|
+
cd cc-statusline
|
|
29
29
|
npm install
|
|
30
30
|
npm run build
|
|
31
31
|
```
|
|
@@ -38,7 +38,7 @@ Add to `~/.claude/settings.json`:
|
|
|
38
38
|
{
|
|
39
39
|
"statusLine": {
|
|
40
40
|
"type": "command",
|
|
41
|
-
"command": "
|
|
41
|
+
"command": "cc-statusline",
|
|
42
42
|
"padding": 0
|
|
43
43
|
}
|
|
44
44
|
}
|
|
@@ -49,17 +49,32 @@ Or if installed from source:
|
|
|
49
49
|
{
|
|
50
50
|
"statusLine": {
|
|
51
51
|
"type": "command",
|
|
52
|
-
"command": "node /path/to/
|
|
52
|
+
"command": "node /path/to/cc-statusline/dist/statusline.js",
|
|
53
53
|
"padding": 0
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
## Customization
|
|
59
|
+
|
|
60
|
+
Run the interactive configuration wizard:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cc-statusline --config
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This lets you customize:
|
|
67
|
+
- **Visibility** - Show/hide model, context, 5hr limit, 7day limit, delta arrows
|
|
68
|
+
- **Colors** - ANSI 256 color codes for each element
|
|
69
|
+
- **Thresholds** - Context warning percentage, cache TTL
|
|
70
|
+
|
|
71
|
+
Config is saved to `~/.claude/cc-statusline.json`.
|
|
72
|
+
|
|
58
73
|
### Environment Variables
|
|
59
74
|
|
|
60
75
|
| Variable | Default | Description |
|
|
61
76
|
|----------|---------|-------------|
|
|
62
|
-
| `STATUSLINE_CACHE_TTL_MS` |
|
|
77
|
+
| `STATUSLINE_CACHE_TTL_MS` | config value | API cache duration (overrides config) |
|
|
63
78
|
|
|
64
79
|
## How It Works
|
|
65
80
|
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
export const CONFIG_PATH = join(homedir(), ".claude", "cc-statusline.json");
|
|
5
|
+
export const DEFAULT_CONFIG = {
|
|
6
|
+
show: {
|
|
7
|
+
model: true,
|
|
8
|
+
context: true,
|
|
9
|
+
usage5hr: true,
|
|
10
|
+
usage7day: true,
|
|
11
|
+
delta: true,
|
|
12
|
+
},
|
|
13
|
+
thresholds: {
|
|
14
|
+
contextWarning: 75,
|
|
15
|
+
cacheTtlMs: 300000,
|
|
16
|
+
},
|
|
17
|
+
colors: {
|
|
18
|
+
model: 36, // cyan
|
|
19
|
+
context: 248, // gray
|
|
20
|
+
contextWarning: 208, // orange
|
|
21
|
+
usage: 248, // gray
|
|
22
|
+
deltaUnder: 32, // green
|
|
23
|
+
deltaOver: 31, // red
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
export function loadConfig() {
|
|
27
|
+
try {
|
|
28
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
29
|
+
return { ...DEFAULT_CONFIG };
|
|
30
|
+
}
|
|
31
|
+
const content = readFileSync(CONFIG_PATH, "utf-8");
|
|
32
|
+
const loaded = JSON.parse(content);
|
|
33
|
+
// Deep merge with defaults to handle missing fields
|
|
34
|
+
return {
|
|
35
|
+
show: { ...DEFAULT_CONFIG.show, ...loaded.show },
|
|
36
|
+
thresholds: { ...DEFAULT_CONFIG.thresholds, ...loaded.thresholds },
|
|
37
|
+
colors: { ...DEFAULT_CONFIG.colors, ...loaded.colors },
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { ...DEFAULT_CONFIG };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function saveConfig(config) {
|
|
45
|
+
const dir = dirname(CONFIG_PATH);
|
|
46
|
+
if (!existsSync(dir)) {
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
50
|
+
}
|
package/dist/statusline.js
CHANGED
|
@@ -2,21 +2,31 @@
|
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
// Handle --config flag first and exit
|
|
7
|
+
if (process.argv.includes("--config")) {
|
|
8
|
+
const { runWizard } = await import("./wizard.js");
|
|
9
|
+
await runWizard();
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
12
|
+
// Load user config
|
|
13
|
+
const userConfig = loadConfig();
|
|
5
14
|
// === Configuration ===
|
|
6
15
|
const CONFIG = {
|
|
7
|
-
cacheTtlMs: parseInt(process.env.STATUSLINE_CACHE_TTL_MS ||
|
|
16
|
+
cacheTtlMs: parseInt(process.env.STATUSLINE_CACHE_TTL_MS || String(userConfig.thresholds.cacheTtlMs)),
|
|
8
17
|
cacheFile: join(homedir(), ".claude", "statusline-cache.json"),
|
|
9
18
|
credentialsFile: join(homedir(), ".claude", ".credentials.json"),
|
|
10
19
|
};
|
|
11
|
-
// === ANSI Colors ===
|
|
20
|
+
// === ANSI Colors (from config) ===
|
|
12
21
|
const color = {
|
|
13
22
|
reset: "\x1b[0m",
|
|
14
|
-
orange: "\x1b[38;5;208m",
|
|
15
23
|
dim: "\x1b[2m",
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
model: `\x1b[38;5;${userConfig.colors.model}m`,
|
|
25
|
+
context: `\x1b[38;5;${userConfig.colors.context}m`,
|
|
26
|
+
contextWarning: `\x1b[38;5;${userConfig.colors.contextWarning}m`,
|
|
27
|
+
usage: `\x1b[38;5;${userConfig.colors.usage}m`,
|
|
28
|
+
deltaUnder: `\x1b[38;5;${userConfig.colors.deltaUnder}m`,
|
|
29
|
+
deltaOver: `\x1b[38;5;${userConfig.colors.deltaOver}m`,
|
|
20
30
|
};
|
|
21
31
|
// === Helpers ===
|
|
22
32
|
function formatTokens(tokens) {
|
|
@@ -28,7 +38,7 @@ function formatTokens(tokens) {
|
|
|
28
38
|
function formatContext(ctx) {
|
|
29
39
|
if (ctx === undefined)
|
|
30
40
|
return "";
|
|
31
|
-
const c = ctx.percent >=
|
|
41
|
+
const c = ctx.percent >= userConfig.thresholds.contextWarning ? color.contextWarning : color.context;
|
|
32
42
|
return `${c}\u{1F4CA} ${Math.round(ctx.percent)}%${color.dim} ${formatTokens(ctx.tokens)}${color.reset}`;
|
|
33
43
|
}
|
|
34
44
|
function formatTimeHoursMinutes(isoDate) {
|
|
@@ -78,10 +88,10 @@ function formatDeltaTime(delta, windowHours) {
|
|
|
78
88
|
timeStr = `${days}d${String(hours).padStart(2, "0")}`;
|
|
79
89
|
}
|
|
80
90
|
if (delta < 0) {
|
|
81
|
-
return ` ${color.dim}${color.
|
|
91
|
+
return ` ${color.dim}${color.deltaUnder}\u{25BC}${timeStr}${color.reset}`;
|
|
82
92
|
}
|
|
83
93
|
else {
|
|
84
|
-
return ` ${color.dim}${color.
|
|
94
|
+
return ` ${color.dim}${color.deltaOver}\u{25B2}${timeStr}${color.reset}`;
|
|
85
95
|
}
|
|
86
96
|
}
|
|
87
97
|
// Context window sizes by model
|
|
@@ -202,33 +212,36 @@ async function main() {
|
|
|
202
212
|
// Build segments
|
|
203
213
|
const segments = [];
|
|
204
214
|
// Model
|
|
205
|
-
|
|
215
|
+
if (userConfig.show.model) {
|
|
216
|
+
segments.push(`${color.model}\u{1F916} ${modelDisplay}${color.reset}`);
|
|
217
|
+
}
|
|
206
218
|
// Context %
|
|
207
|
-
if (contextInfo !== undefined) {
|
|
219
|
+
if (userConfig.show.context && contextInfo !== undefined) {
|
|
208
220
|
segments.push(formatContext(contextInfo));
|
|
209
221
|
}
|
|
210
222
|
// Combined usage limits: ⏱ 18% 180 / 45% 48
|
|
211
|
-
if (usage?.five_hour || usage?.seven_day) {
|
|
223
|
+
if ((userConfig.show.usage5hr || userConfig.show.usage7day) && (usage?.five_hour || usage?.seven_day)) {
|
|
212
224
|
const fiveHr = usage.five_hour;
|
|
213
225
|
const sevenDay = usage.seven_day;
|
|
214
226
|
const parts = [];
|
|
215
|
-
if (fiveHr?.utilization !== undefined && fiveHr.resets_at) {
|
|
227
|
+
if (userConfig.show.usage5hr && fiveHr?.utilization !== undefined && fiveHr.resets_at) {
|
|
216
228
|
const time = formatTimeHoursMinutes(fiveHr.resets_at);
|
|
217
229
|
const delta = calcDelta(fiveHr.utilization, fiveHr.resets_at, 5);
|
|
218
|
-
const deltaStr = formatDeltaTime(delta, 5);
|
|
219
|
-
parts.push(`${color.
|
|
230
|
+
const deltaStr = userConfig.show.delta ? formatDeltaTime(delta, 5) : "";
|
|
231
|
+
parts.push(`${color.usage}${Math.round(fiveHr.utilization)}%${color.dim} ${time}${deltaStr}${color.reset}`);
|
|
220
232
|
}
|
|
221
|
-
if (sevenDay?.utilization !== undefined && sevenDay.resets_at) {
|
|
233
|
+
if (userConfig.show.usage7day && sevenDay?.utilization !== undefined && sevenDay.resets_at) {
|
|
222
234
|
const time = formatTimeDaysHours(sevenDay.resets_at);
|
|
223
235
|
const delta = calcDelta(sevenDay.utilization, sevenDay.resets_at, 168);
|
|
224
|
-
const deltaStr = formatDeltaTime(delta, 168);
|
|
225
|
-
parts.push(`${color.
|
|
236
|
+
const deltaStr = userConfig.show.delta ? formatDeltaTime(delta, 168) : "";
|
|
237
|
+
parts.push(`${color.usage}${Math.round(sevenDay.utilization)}%${color.dim} ${time}${deltaStr}${color.reset}`);
|
|
226
238
|
}
|
|
227
239
|
if (parts.length > 0) {
|
|
228
|
-
segments.push(`${color.
|
|
240
|
+
segments.push(`${color.model}\u{23F1}${color.reset} ${parts.join(`${color.dim}/${color.reset}`)}`);
|
|
229
241
|
}
|
|
230
242
|
}
|
|
231
243
|
// Output
|
|
232
244
|
console.log(segments.join(` ${color.dim}\u{2502}${color.reset} `));
|
|
233
245
|
}
|
|
246
|
+
// Run main
|
|
234
247
|
main().catch(() => process.exit(1));
|
package/dist/wizard.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { select, checkbox, input, confirm } from "@inquirer/prompts";
|
|
2
|
+
import { DEFAULT_CONFIG, loadConfig, saveConfig, CONFIG_PATH } from "./config.js";
|
|
3
|
+
// ANSI color helper
|
|
4
|
+
function colorize(code, text) {
|
|
5
|
+
return `\x1b[38;5;${code}m${text}\x1b[0m`;
|
|
6
|
+
}
|
|
7
|
+
function showPreview(config) {
|
|
8
|
+
const c = config.colors;
|
|
9
|
+
const parts = [];
|
|
10
|
+
if (config.show.model) {
|
|
11
|
+
parts.push(colorize(c.model, "\u{1F916} Opus"));
|
|
12
|
+
}
|
|
13
|
+
if (config.show.context) {
|
|
14
|
+
parts.push(`${colorize(c.context, "\u{1F4CA} 45%")} / ${colorize(c.contextWarning, "78%")}`);
|
|
15
|
+
}
|
|
16
|
+
if (config.show.usage5hr || config.show.usage7day) {
|
|
17
|
+
const usageParts = [];
|
|
18
|
+
if (config.show.usage5hr)
|
|
19
|
+
usageParts.push(colorize(c.usage, "26% 2h09"));
|
|
20
|
+
if (config.show.usage7day)
|
|
21
|
+
usageParts.push(colorize(c.usage, "46% 2d15"));
|
|
22
|
+
if (config.show.delta) {
|
|
23
|
+
parts.push(`\u{23F1} ${usageParts.join("/")} ${colorize(c.deltaUnder, "\u{25BC}1h")} ${colorize(c.deltaOver, "\u{25B2}4h")}`);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
parts.push(`\u{23F1} ${usageParts.join("/")}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
console.log("\n Preview: " + parts.join(" \x1b[2m\u{2502}\x1b[0m ") + "\n");
|
|
30
|
+
}
|
|
31
|
+
async function visibilityMenu(config) {
|
|
32
|
+
const choices = [
|
|
33
|
+
{ name: "Model indicator", value: "model", checked: config.show.model },
|
|
34
|
+
{ name: "Context usage", value: "context", checked: config.show.context },
|
|
35
|
+
{ name: "5-hour usage limit", value: "usage5hr", checked: config.show.usage5hr },
|
|
36
|
+
{ name: "7-day usage limit", value: "usage7day", checked: config.show.usage7day },
|
|
37
|
+
{ name: "Budget delta arrows", value: "delta", checked: config.show.delta },
|
|
38
|
+
];
|
|
39
|
+
const selected = await checkbox({
|
|
40
|
+
message: "Toggle items to show (space to toggle, enter to confirm)",
|
|
41
|
+
choices,
|
|
42
|
+
});
|
|
43
|
+
config.show.model = selected.includes("model");
|
|
44
|
+
config.show.context = selected.includes("context");
|
|
45
|
+
config.show.usage5hr = selected.includes("usage5hr");
|
|
46
|
+
config.show.usage7day = selected.includes("usage7day");
|
|
47
|
+
config.show.delta = selected.includes("delta");
|
|
48
|
+
}
|
|
49
|
+
async function colorsMenu(config) {
|
|
50
|
+
const colorOptions = [
|
|
51
|
+
{ name: `Model (current: ${colorize(config.colors.model, String(config.colors.model))})`, value: "model" },
|
|
52
|
+
{ name: `Context (current: ${colorize(config.colors.context, String(config.colors.context))})`, value: "context" },
|
|
53
|
+
{ name: `Context warning (current: ${colorize(config.colors.contextWarning, String(config.colors.contextWarning))})`, value: "contextWarning" },
|
|
54
|
+
{ name: `Usage (current: ${colorize(config.colors.usage, String(config.colors.usage))})`, value: "usage" },
|
|
55
|
+
{ name: `Delta under budget (current: ${colorize(config.colors.deltaUnder, String(config.colors.deltaUnder))})`, value: "deltaUnder" },
|
|
56
|
+
{ name: `Delta over budget (current: ${colorize(config.colors.deltaOver, String(config.colors.deltaOver))})`, value: "deltaOver" },
|
|
57
|
+
{ name: "Back", value: "back" },
|
|
58
|
+
];
|
|
59
|
+
while (true) {
|
|
60
|
+
const choice = await select({
|
|
61
|
+
message: "Select color to change (ANSI 256 codes: 0-255)",
|
|
62
|
+
choices: colorOptions,
|
|
63
|
+
});
|
|
64
|
+
if (choice === "back")
|
|
65
|
+
break;
|
|
66
|
+
const current = config.colors[choice];
|
|
67
|
+
const newValue = await input({
|
|
68
|
+
message: `Enter ANSI color code (0-255, current: ${colorize(current, String(current))})`,
|
|
69
|
+
default: String(current),
|
|
70
|
+
validate: (val) => {
|
|
71
|
+
const num = parseInt(val, 10);
|
|
72
|
+
if (isNaN(num) || num < 0 || num > 255)
|
|
73
|
+
return "Enter a number 0-255";
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
config.colors[choice] = parseInt(newValue, 10);
|
|
78
|
+
// Update the menu option to show new color
|
|
79
|
+
const idx = colorOptions.findIndex(o => o.value === choice);
|
|
80
|
+
if (idx >= 0) {
|
|
81
|
+
const label = choice.replace(/([A-Z])/g, " $1").toLowerCase();
|
|
82
|
+
colorOptions[idx].name = `${label.charAt(0).toUpperCase() + label.slice(1)} (current: ${colorize(config.colors[choice], newValue)})`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function thresholdsMenu(config) {
|
|
87
|
+
const contextWarning = await input({
|
|
88
|
+
message: "Context warning threshold % (turns orange)",
|
|
89
|
+
default: String(config.thresholds.contextWarning),
|
|
90
|
+
validate: (val) => {
|
|
91
|
+
const num = parseInt(val, 10);
|
|
92
|
+
if (isNaN(num) || num < 1 || num > 100)
|
|
93
|
+
return "Enter 1-100";
|
|
94
|
+
return true;
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
config.thresholds.contextWarning = parseInt(contextWarning, 10);
|
|
98
|
+
const cacheTtl = await input({
|
|
99
|
+
message: "API cache TTL (milliseconds)",
|
|
100
|
+
default: String(config.thresholds.cacheTtlMs),
|
|
101
|
+
validate: (val) => {
|
|
102
|
+
const num = parseInt(val, 10);
|
|
103
|
+
if (isNaN(num) || num < 0)
|
|
104
|
+
return "Enter a positive number";
|
|
105
|
+
return true;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
config.thresholds.cacheTtlMs = parseInt(cacheTtl, 10);
|
|
109
|
+
}
|
|
110
|
+
export async function runWizard() {
|
|
111
|
+
console.log("\n\x1b[1mcc-statusline Configuration\x1b[0m\n");
|
|
112
|
+
const config = loadConfig();
|
|
113
|
+
showPreview(config);
|
|
114
|
+
while (true) {
|
|
115
|
+
const choice = await select({
|
|
116
|
+
message: "What would you like to configure?",
|
|
117
|
+
choices: [
|
|
118
|
+
{ name: "Visibility (show/hide items)", value: "visibility" },
|
|
119
|
+
{ name: "Colors", value: "colors" },
|
|
120
|
+
{ name: "Thresholds", value: "thresholds" },
|
|
121
|
+
{ name: "Reset to defaults", value: "reset" },
|
|
122
|
+
{ name: "Save & Exit", value: "save" },
|
|
123
|
+
{ name: "Exit without saving", value: "exit" },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
switch (choice) {
|
|
127
|
+
case "visibility":
|
|
128
|
+
await visibilityMenu(config);
|
|
129
|
+
showPreview(config);
|
|
130
|
+
break;
|
|
131
|
+
case "colors":
|
|
132
|
+
await colorsMenu(config);
|
|
133
|
+
showPreview(config);
|
|
134
|
+
break;
|
|
135
|
+
case "thresholds":
|
|
136
|
+
await thresholdsMenu(config);
|
|
137
|
+
break;
|
|
138
|
+
case "reset":
|
|
139
|
+
const confirmReset = await confirm({
|
|
140
|
+
message: "Reset all settings to defaults?",
|
|
141
|
+
default: false,
|
|
142
|
+
});
|
|
143
|
+
if (confirmReset) {
|
|
144
|
+
Object.assign(config, JSON.parse(JSON.stringify(DEFAULT_CONFIG)));
|
|
145
|
+
console.log(" Reset to defaults.\n");
|
|
146
|
+
showPreview(config);
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
case "save":
|
|
150
|
+
saveConfig(config);
|
|
151
|
+
console.log(`\n Saved to ${CONFIG_PATH}\n`);
|
|
152
|
+
return;
|
|
153
|
+
case "exit":
|
|
154
|
+
const confirmExit = await confirm({
|
|
155
|
+
message: "Exit without saving changes?",
|
|
156
|
+
default: false,
|
|
157
|
+
});
|
|
158
|
+
if (confirmExit)
|
|
159
|
+
return;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daliovic/cc-statusline",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Minimal Claude Code statusline with usage limits and budget tracking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/statusline.js",
|
|
@@ -34,6 +34,9 @@
|
|
|
34
34
|
"url": "https://github.com/daliovic/cc-statusline/issues"
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/daliovic/cc-statusline#readme",
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@inquirer/prompts": "^7.0.0"
|
|
39
|
+
},
|
|
37
40
|
"devDependencies": {
|
|
38
41
|
"@types/node": "^22.0.0",
|
|
39
42
|
"typescript": "^5.0.0"
|