@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 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/claude-statusline
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/YOUR_USERNAME/claude-statusline.git
28
- cd claude-statusline
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": "claude-statusline",
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/claude-statusline/dist/statusline.js",
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` | `300000` | API cache duration (5 min) |
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
+ }
@@ -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 || "300000"), // 5 minutes default
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
- gray: "\x1b[38;5;248m",
17
- cyan: "\x1b[36m",
18
- green: "\x1b[32m",
19
- red: "\x1b[31m",
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 >= 75 ? color.orange : color.gray;
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.green}\u{25BC}${timeStr}${color.reset}`;
92
+ return ` ${color.dim}${color.deltaUnder}\u{25BC}${timeStr}${color.reset}`;
82
93
  }
83
94
  else {
84
- return ` ${color.dim}${color.red}\u{25B2}${timeStr}${color.reset}`;
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
- segments.push(`${color.cyan}\u{1F916} ${modelDisplay}${color.reset}`);
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.gray}${Math.round(fiveHr.utilization)}%${color.dim} ${time}${deltaStr}${color.reset}`);
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.gray}${Math.round(sevenDay.utilization)}%${color.dim} ${time}${deltaStr}${color.reset}`);
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.cyan}\u{23F1}${color.reset} ${parts.join(`${color.dim}/${color.reset}`)}`);
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.0.0",
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"