@daliovic/cc-statusline 1.0.0 → 1.1.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/README.md +21 -6
- package/dist/config.js +50 -0
- package/dist/statusline.js +33 -21
- 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,32 @@
|
|
|
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
|
|
7
|
+
if (process.argv.includes("--config")) {
|
|
8
|
+
import("./wizard.js").then(({ runWizard }) => runWizard()).catch(() => process.exit(1));
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
main().catch(() => process.exit(1));
|
|
12
|
+
}
|
|
13
|
+
// Load user config
|
|
14
|
+
const userConfig = loadConfig();
|
|
5
15
|
// === Configuration ===
|
|
6
16
|
const CONFIG = {
|
|
7
|
-
cacheTtlMs: parseInt(process.env.STATUSLINE_CACHE_TTL_MS ||
|
|
17
|
+
cacheTtlMs: parseInt(process.env.STATUSLINE_CACHE_TTL_MS || String(userConfig.thresholds.cacheTtlMs)),
|
|
8
18
|
cacheFile: join(homedir(), ".claude", "statusline-cache.json"),
|
|
9
19
|
credentialsFile: join(homedir(), ".claude", ".credentials.json"),
|
|
10
20
|
};
|
|
11
|
-
// === ANSI Colors ===
|
|
21
|
+
// === ANSI Colors (from config) ===
|
|
12
22
|
const color = {
|
|
13
23
|
reset: "\x1b[0m",
|
|
14
|
-
orange: "\x1b[38;5;208m",
|
|
15
24
|
dim: "\x1b[2m",
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
model: `\x1b[38;5;${userConfig.colors.model}m`,
|
|
26
|
+
context: `\x1b[38;5;${userConfig.colors.context}m`,
|
|
27
|
+
contextWarning: `\x1b[38;5;${userConfig.colors.contextWarning}m`,
|
|
28
|
+
usage: `\x1b[38;5;${userConfig.colors.usage}m`,
|
|
29
|
+
deltaUnder: `\x1b[38;5;${userConfig.colors.deltaUnder}m`,
|
|
30
|
+
deltaOver: `\x1b[38;5;${userConfig.colors.deltaOver}m`,
|
|
20
31
|
};
|
|
21
32
|
// === Helpers ===
|
|
22
33
|
function formatTokens(tokens) {
|
|
@@ -28,7 +39,7 @@ function formatTokens(tokens) {
|
|
|
28
39
|
function formatContext(ctx) {
|
|
29
40
|
if (ctx === undefined)
|
|
30
41
|
return "";
|
|
31
|
-
const c = ctx.percent >=
|
|
42
|
+
const c = ctx.percent >= userConfig.thresholds.contextWarning ? color.contextWarning : color.context;
|
|
32
43
|
return `${c}\u{1F4CA} ${Math.round(ctx.percent)}%${color.dim} ${formatTokens(ctx.tokens)}${color.reset}`;
|
|
33
44
|
}
|
|
34
45
|
function formatTimeHoursMinutes(isoDate) {
|
|
@@ -78,10 +89,10 @@ function formatDeltaTime(delta, windowHours) {
|
|
|
78
89
|
timeStr = `${days}d${String(hours).padStart(2, "0")}`;
|
|
79
90
|
}
|
|
80
91
|
if (delta < 0) {
|
|
81
|
-
return ` ${color.dim}${color.
|
|
92
|
+
return ` ${color.dim}${color.deltaUnder}\u{25BC}${timeStr}${color.reset}`;
|
|
82
93
|
}
|
|
83
94
|
else {
|
|
84
|
-
return ` ${color.dim}${color.
|
|
95
|
+
return ` ${color.dim}${color.deltaOver}\u{25B2}${timeStr}${color.reset}`;
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
// Context window sizes by model
|
|
@@ -202,33 +213,34 @@ async function main() {
|
|
|
202
213
|
// Build segments
|
|
203
214
|
const segments = [];
|
|
204
215
|
// Model
|
|
205
|
-
|
|
216
|
+
if (userConfig.show.model) {
|
|
217
|
+
segments.push(`${color.model}\u{1F916} ${modelDisplay}${color.reset}`);
|
|
218
|
+
}
|
|
206
219
|
// Context %
|
|
207
|
-
if (contextInfo !== undefined) {
|
|
220
|
+
if (userConfig.show.context && contextInfo !== undefined) {
|
|
208
221
|
segments.push(formatContext(contextInfo));
|
|
209
222
|
}
|
|
210
223
|
// Combined usage limits: ⏱ 18% 180 / 45% 48
|
|
211
|
-
if (usage?.five_hour || usage?.seven_day) {
|
|
224
|
+
if ((userConfig.show.usage5hr || userConfig.show.usage7day) && (usage?.five_hour || usage?.seven_day)) {
|
|
212
225
|
const fiveHr = usage.five_hour;
|
|
213
226
|
const sevenDay = usage.seven_day;
|
|
214
227
|
const parts = [];
|
|
215
|
-
if (fiveHr?.utilization !== undefined && fiveHr.resets_at) {
|
|
228
|
+
if (userConfig.show.usage5hr && fiveHr?.utilization !== undefined && fiveHr.resets_at) {
|
|
216
229
|
const time = formatTimeHoursMinutes(fiveHr.resets_at);
|
|
217
230
|
const delta = calcDelta(fiveHr.utilization, fiveHr.resets_at, 5);
|
|
218
|
-
const deltaStr = formatDeltaTime(delta, 5);
|
|
219
|
-
parts.push(`${color.
|
|
231
|
+
const deltaStr = userConfig.show.delta ? formatDeltaTime(delta, 5) : "";
|
|
232
|
+
parts.push(`${color.usage}${Math.round(fiveHr.utilization)}%${color.dim} ${time}${deltaStr}${color.reset}`);
|
|
220
233
|
}
|
|
221
|
-
if (sevenDay?.utilization !== undefined && sevenDay.resets_at) {
|
|
234
|
+
if (userConfig.show.usage7day && sevenDay?.utilization !== undefined && sevenDay.resets_at) {
|
|
222
235
|
const time = formatTimeDaysHours(sevenDay.resets_at);
|
|
223
236
|
const delta = calcDelta(sevenDay.utilization, sevenDay.resets_at, 168);
|
|
224
|
-
const deltaStr = formatDeltaTime(delta, 168);
|
|
225
|
-
parts.push(`${color.
|
|
237
|
+
const deltaStr = userConfig.show.delta ? formatDeltaTime(delta, 168) : "";
|
|
238
|
+
parts.push(`${color.usage}${Math.round(sevenDay.utilization)}%${color.dim} ${time}${deltaStr}${color.reset}`);
|
|
226
239
|
}
|
|
227
240
|
if (parts.length > 0) {
|
|
228
|
-
segments.push(`${color.
|
|
241
|
+
segments.push(`${color.model}\u{23F1}${color.reset} ${parts.join(`${color.dim}/${color.reset}`)}`);
|
|
229
242
|
}
|
|
230
243
|
}
|
|
231
244
|
// Output
|
|
232
245
|
console.log(segments.join(` ${color.dim}\u{2502}${color.reset} `));
|
|
233
246
|
}
|
|
234
|
-
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.0",
|
|
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"
|