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