@thlinh/cc-statusline 2.0.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/.github/demo.png +0 -0
- package/LICENSE +21 -0
- package/README.md +61 -0
- package/bin/install.js +490 -0
- package/bin/providers/anthropic.sh +127 -0
- package/bin/providers/zai.sh +98 -0
- package/bin/shared-helpers.sh +117 -0
- package/package.json +23 -0
package/.github/demo.png
ADDED
|
Binary file
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kamran Ahmed
|
|
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,61 @@
|
|
|
1
|
+
# cc-statusline
|
|
2
|
+
|
|
3
|
+
Status line for Claude Code CLI. Works with Anthropic or Z.AI.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Shows your current model, context percentage, git branch, how long you've been working, and API usage depending on your provider.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @thlinh/cc-statusline
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This uses Anthropic by default and installs to `~/.claude`.
|
|
16
|
+
|
|
17
|
+
For Z.AI:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npx @thlinh/cc-statusline --provider=zai --dir ~/.claude-z
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For scripts or dotfiles (skip the prompts):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx @thlinh/cc-statusline --provider=zai --dir ~/.claude-z
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- [jq](https://jqlang.github.io/jq/) — parses JSON
|
|
32
|
+
- curl — fetches usage
|
|
33
|
+
- git — branch info (optional)
|
|
34
|
+
|
|
35
|
+
macOS: `brew install jq`
|
|
36
|
+
|
|
37
|
+
## Providers
|
|
38
|
+
|
|
39
|
+
**Anthropic** — pulls OAuth token from keychain, calls Anthropic's usage API, shows current/weekly/extra.
|
|
40
|
+
|
|
41
|
+
**Z.AI** — reads token from `~/.chelper/config.yaml`, calls Z.AI's quota API, shows token and tool usage.
|
|
42
|
+
|
|
43
|
+
## Uninstall
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx @thlinh/cc-statusline --uninstall --dir ~/.claude-z
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Restores your backup if you had one, otherwise removes the files.
|
|
50
|
+
|
|
51
|
+
## What gets installed
|
|
52
|
+
|
|
53
|
+
Three files end up in your claude config directory:
|
|
54
|
+
|
|
55
|
+
- `statusline.sh` — main script (generated when you run the installer)
|
|
56
|
+
- `statusline-helpers.sh` — shared utilities
|
|
57
|
+
- `statusline-provider.sh` — whichever provider you picked
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
package/bin/install.js
ADDED
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const os = require("os");
|
|
6
|
+
const readline = require("readline");
|
|
7
|
+
|
|
8
|
+
// ANSI colors
|
|
9
|
+
const blue = "\x1b[38;2;0;153;255m";
|
|
10
|
+
const green = "\x1b[38;2;0;175;80m";
|
|
11
|
+
const red = "\x1b[38;2;255;85;85m";
|
|
12
|
+
const yellow = "\x1b[38;2;230;200;0m";
|
|
13
|
+
const dim = "\x1b[2m";
|
|
14
|
+
const reset = "\x1b[0m";
|
|
15
|
+
|
|
16
|
+
function log(msg) {
|
|
17
|
+
console.log(` ${msg}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function success(msg) {
|
|
21
|
+
console.log(` ${green}✓${reset} ${msg}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function warn(msg) {
|
|
25
|
+
console.log(` ${yellow}!${reset} ${msg}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function fail(msg) {
|
|
29
|
+
console.error(` ${red}✗${reset} ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Parse command line arguments
|
|
33
|
+
function parseArgs() {
|
|
34
|
+
const args = {};
|
|
35
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
36
|
+
const arg = process.argv[i];
|
|
37
|
+
if (arg === "--uninstall") {
|
|
38
|
+
args.uninstall = true;
|
|
39
|
+
} else if (arg.startsWith("--dir=")) {
|
|
40
|
+
args.dir = arg.split("=")[1];
|
|
41
|
+
} else if (arg.startsWith("--provider=")) {
|
|
42
|
+
args.provider = arg.split("=")[1];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return args;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check required dependencies
|
|
49
|
+
function checkDeps() {
|
|
50
|
+
const { execSync } = require("child_process");
|
|
51
|
+
const missing = [];
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
execSync("which jq", { stdio: "ignore" });
|
|
55
|
+
} catch {
|
|
56
|
+
missing.push("jq");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
execSync("which curl", { stdio: "ignore" });
|
|
61
|
+
} catch {
|
|
62
|
+
missing.push("curl");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Git is optional
|
|
66
|
+
let hasGit = false;
|
|
67
|
+
try {
|
|
68
|
+
execSync("which git", { stdio: "ignore" });
|
|
69
|
+
hasGit = true;
|
|
70
|
+
} catch {
|
|
71
|
+
// Git is optional
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { missing, hasGit };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Prompt for provider selection
|
|
78
|
+
function promptProvider() {
|
|
79
|
+
const rl = readline.createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stdout,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
rl.question(
|
|
86
|
+
`\n ${blue}Which API provider are you using?${reset}\n ${dim}1) Anthropic${reset}\n ${dim}2) Z.AI${reset}\n ${blue}>${reset} `,
|
|
87
|
+
(answer) => {
|
|
88
|
+
rl.close();
|
|
89
|
+
const choice = answer.trim();
|
|
90
|
+
if (choice === "1" || choice.toLowerCase().startsWith("a")) {
|
|
91
|
+
resolve("anthropic");
|
|
92
|
+
} else if (choice === "2" || choice.toLowerCase().startsWith("z")) {
|
|
93
|
+
resolve("zai");
|
|
94
|
+
} else {
|
|
95
|
+
warn("Invalid choice, defaulting to Anthropic");
|
|
96
|
+
resolve("anthropic");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Get provider name (from flag or prompt)
|
|
104
|
+
async function getProvider(args) {
|
|
105
|
+
if (args.provider) {
|
|
106
|
+
const provider = args.provider.toLowerCase();
|
|
107
|
+
if (provider === "anthropic" || provider === "zai") {
|
|
108
|
+
return provider;
|
|
109
|
+
}
|
|
110
|
+
warn(`Unknown provider "${args.provider}", defaulting to Anthropic`);
|
|
111
|
+
return "anthropic";
|
|
112
|
+
}
|
|
113
|
+
return await promptProvider();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Uninstall function
|
|
117
|
+
function uninstall(targetDir) {
|
|
118
|
+
const STATUSLINE_DEST = path.join(targetDir, "statusline.sh");
|
|
119
|
+
const HELPERS_DEST = path.join(targetDir, "statusline-helpers.sh");
|
|
120
|
+
const PROVIDER_DEST = path.join(targetDir, "statusline-provider.sh");
|
|
121
|
+
const CACHE_DEST = path.join(targetDir, "statusline-cache.json");
|
|
122
|
+
const SETTINGS_FILE = path.join(targetDir, "settings.json");
|
|
123
|
+
const BACKUP_DEST = STATUSLINE_DEST + ".bak";
|
|
124
|
+
|
|
125
|
+
console.log();
|
|
126
|
+
console.log(` ${blue}Claude Line Uninstaller${reset}`);
|
|
127
|
+
console.log(` ${dim}───────────────────────${reset}`);
|
|
128
|
+
console.log();
|
|
129
|
+
|
|
130
|
+
if (fs.existsSync(BACKUP_DEST)) {
|
|
131
|
+
// Restore from backup
|
|
132
|
+
fs.copyFileSync(BACKUP_DEST, STATUSLINE_DEST);
|
|
133
|
+
fs.unlinkSync(BACKUP_DEST);
|
|
134
|
+
success(`Restored backup from ${dim}statusline.sh.bak${reset}`);
|
|
135
|
+
|
|
136
|
+
// Only remove cache file (runtime-generated, always safe)
|
|
137
|
+
if (fs.existsSync(CACHE_DEST)) {
|
|
138
|
+
fs.unlinkSync(CACHE_DEST);
|
|
139
|
+
success(`Removed ${dim}statusline-cache.json${reset}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
log(
|
|
143
|
+
`${dim}Note: Helper files (statusline-helpers.sh, statusline-provider.sh)${reset}`
|
|
144
|
+
);
|
|
145
|
+
log(
|
|
146
|
+
`${dim} were preserved as they may belong to the restored backup.${reset}`
|
|
147
|
+
);
|
|
148
|
+
log(
|
|
149
|
+
`${dim} Remove manually if not needed.${reset}`
|
|
150
|
+
);
|
|
151
|
+
} else if (fs.existsSync(STATUSLINE_DEST)) {
|
|
152
|
+
// No backup: remove all installed files
|
|
153
|
+
fs.unlinkSync(STATUSLINE_DEST);
|
|
154
|
+
success(`Removed ${dim}statusline.sh${reset}`);
|
|
155
|
+
|
|
156
|
+
if (fs.existsSync(HELPERS_DEST)) {
|
|
157
|
+
fs.unlinkSync(HELPERS_DEST);
|
|
158
|
+
success(`Removed ${dim}statusline-helpers.sh${reset}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (fs.existsSync(PROVIDER_DEST)) {
|
|
162
|
+
fs.unlinkSync(PROVIDER_DEST);
|
|
163
|
+
success(`Removed ${dim}statusline-provider.sh${reset}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (fs.existsSync(CACHE_DEST)) {
|
|
167
|
+
fs.unlinkSync(CACHE_DEST);
|
|
168
|
+
success(`Removed ${dim}statusline-cache.json${reset}`);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
warn("No statusline found — nothing to remove");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Remove statusLine from settings.json
|
|
175
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
176
|
+
try {
|
|
177
|
+
const settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
|
|
178
|
+
const expectedCommand = `bash "${targetDir}/statusline.sh"`;
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
settings.statusLine &&
|
|
182
|
+
settings.statusLine.type === "command" &&
|
|
183
|
+
settings.statusLine.command === expectedCommand
|
|
184
|
+
) {
|
|
185
|
+
delete settings.statusLine;
|
|
186
|
+
fs.writeFileSync(
|
|
187
|
+
SETTINGS_FILE,
|
|
188
|
+
JSON.stringify(settings, null, 2) + "\n"
|
|
189
|
+
);
|
|
190
|
+
success(`Removed statusLine from ${dim}settings.json${reset}`);
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
fail(`Could not parse ${SETTINGS_FILE} — ${err.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log();
|
|
198
|
+
log(`${green}Done!${reset} Restart Claude Code to apply changes.`);
|
|
199
|
+
console.log();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Generate statusline.sh content
|
|
203
|
+
function generateStatuslineScript(provider) {
|
|
204
|
+
const providerName = provider === "anthropic" ? "Anthropic" : "Z.AI";
|
|
205
|
+
|
|
206
|
+
return `#!/bin/bash
|
|
207
|
+
set -f
|
|
208
|
+
|
|
209
|
+
# Generated by cc-statusline installer
|
|
210
|
+
# Provider: ${providerName}
|
|
211
|
+
# DO NOT EDIT THIS FILE DIRECTLY - it will be overwritten on reinstall
|
|
212
|
+
|
|
213
|
+
input=$(cat)
|
|
214
|
+
|
|
215
|
+
if [ -z "$input" ]; then
|
|
216
|
+
printf "Claude"
|
|
217
|
+
exit 0
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# Self-locate script directory
|
|
221
|
+
SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
|
|
222
|
+
|
|
223
|
+
# Source shared helpers
|
|
224
|
+
. "\${SCRIPT_DIR}/statusline-helpers.sh"
|
|
225
|
+
|
|
226
|
+
# Source provider implementation
|
|
227
|
+
. "\${SCRIPT_DIR}/statusline-provider.sh"
|
|
228
|
+
|
|
229
|
+
# ── Extract JSON data ───────────────────────────────────
|
|
230
|
+
model_name=$(echo "$input" | jq -r '.model.display_name // "Claude"')
|
|
231
|
+
|
|
232
|
+
size=$(echo "$input" | jq -r '.context_window.context_window_size // 200000')
|
|
233
|
+
[ "$size" -eq 0 ] 2>/dev/null && size=200000
|
|
234
|
+
|
|
235
|
+
input_tokens=$(echo "$input" | jq -r '.context_window.current_usage.input_tokens // 0')
|
|
236
|
+
cache_create=$(echo "$input" | jq -r '.context_window.current_usage.cache_creation_input_tokens // 0')
|
|
237
|
+
cache_read=$(echo "$input" | jq -r '.context_window.current_usage.cache_read_input_tokens // 0')
|
|
238
|
+
current=$(( input_tokens + cache_create + cache_read ))
|
|
239
|
+
|
|
240
|
+
used_tokens=$(format_tokens $current)
|
|
241
|
+
total_tokens=$(format_tokens $size)
|
|
242
|
+
|
|
243
|
+
if [ "$size" -gt 0 ]; then
|
|
244
|
+
pct_used=$(( current * 100 / size ))
|
|
245
|
+
else
|
|
246
|
+
pct_used=0
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
effort="default"
|
|
250
|
+
settings_path="\${SCRIPT_DIR}/settings.json"
|
|
251
|
+
if [ -f "$settings_path" ]; then
|
|
252
|
+
effort=$(jq -r '.effortLevel // "default"' "$settings_path" 2>/dev/null)
|
|
253
|
+
fi
|
|
254
|
+
|
|
255
|
+
# ── LINE 1: Model │ Context % │ Directory (branch) │ Session │ Effort ──
|
|
256
|
+
sep=" \${dim}│\${reset} "
|
|
257
|
+
pct_color=$(color_for_pct "$pct_used")
|
|
258
|
+
cwd=$(echo "$input" | jq -r '.cwd // ""')
|
|
259
|
+
[ -z "$cwd" ] || [ "$cwd" = "null" ] && cwd=$(pwd)
|
|
260
|
+
dirname=$(basename "$cwd")
|
|
261
|
+
|
|
262
|
+
git_branch=""
|
|
263
|
+
git_dirty=""
|
|
264
|
+
if command -v git >/dev/null 2>&1 && git -C "$cwd" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
265
|
+
git_branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null)
|
|
266
|
+
if [ -n "$(git -C "$cwd" --no-optional-locks status --porcelain 2>/dev/null)" ]; then
|
|
267
|
+
git_dirty="*"
|
|
268
|
+
fi
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
session_duration=""
|
|
272
|
+
session_start=$(echo "$input" | jq -r '.session.start_time // empty')
|
|
273
|
+
if [ -n "$session_start" ] && [ "$session_start" != "null" ]; then
|
|
274
|
+
start_epoch=$(iso_to_epoch "$session_start")
|
|
275
|
+
if [ -n "$start_epoch" ]; then
|
|
276
|
+
now_epoch=$(date +%s)
|
|
277
|
+
elapsed=$(( now_epoch - start_epoch ))
|
|
278
|
+
if [ "$elapsed" -ge 3600 ]; then
|
|
279
|
+
session_duration="$(( elapsed / 3600 ))h$(( (elapsed % 3600) / 60 ))m"
|
|
280
|
+
elif [ "$elapsed" -ge 60 ]; then
|
|
281
|
+
session_duration="$(( elapsed / 60 ))m"
|
|
282
|
+
else
|
|
283
|
+
session_duration="\${elapsed}s"
|
|
284
|
+
fi
|
|
285
|
+
fi
|
|
286
|
+
fi
|
|
287
|
+
|
|
288
|
+
line1="\${blue}\${model_name}\${reset}"
|
|
289
|
+
line1+="\${sep}"
|
|
290
|
+
line1+="🧠 \${pct_color}\${pct_used}%${reset}"
|
|
291
|
+
line1+="\${sep}"
|
|
292
|
+
line1+="\${cyan}\${dirname}\${reset}"
|
|
293
|
+
if [ -n "$git_branch" ]; then
|
|
294
|
+
line1+=" \${green}(\${git_branch}\${red}\${git_dirty}\${green})\${reset}"
|
|
295
|
+
fi
|
|
296
|
+
if [ -n "$session_duration" ]; then
|
|
297
|
+
line1+="\${sep}"
|
|
298
|
+
line1+="\${dim}⏱ \${reset}\${white}\${session_duration}\${reset}"
|
|
299
|
+
fi
|
|
300
|
+
line1+="\${sep}"
|
|
301
|
+
case "$effort" in
|
|
302
|
+
high) line1+="\${magenta}● \${effort}\${reset}" ;;
|
|
303
|
+
medium) line1+="\${dim}◑ \${effort}\${reset}" ;;
|
|
304
|
+
low) line1+="\${dim}◔ \${effort}\${reset}" ;;
|
|
305
|
+
*) line1+="\${dim}◑ \${effort}\${reset}" ;;
|
|
306
|
+
esac
|
|
307
|
+
|
|
308
|
+
# ── Fetch provider usage data (cached) ──────────────────
|
|
309
|
+
cache_file="\${SCRIPT_DIR}/statusline-cache.json"
|
|
310
|
+
cache_max_age=60
|
|
311
|
+
needs_refresh=true
|
|
312
|
+
usage_data=""
|
|
313
|
+
|
|
314
|
+
if [ -f "$cache_file" ]; then
|
|
315
|
+
cache_mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null)
|
|
316
|
+
now=$(date +%s)
|
|
317
|
+
cache_age=$(( now - cache_mtime ))
|
|
318
|
+
if [ "$cache_age" -lt "$cache_max_age" ]; then
|
|
319
|
+
# Check if cached provider matches current provider
|
|
320
|
+
cached_provider=$(jq -r '.provider // empty' "$cache_file" 2>/dev/null)
|
|
321
|
+
if [ "$cached_provider" = "${provider}" ]; then
|
|
322
|
+
needs_refresh=false
|
|
323
|
+
usage_data=$(cat "$cache_file" 2>/dev/null | jq -r '.data // empty')
|
|
324
|
+
fi
|
|
325
|
+
fi
|
|
326
|
+
fi
|
|
327
|
+
|
|
328
|
+
if $needs_refresh; then
|
|
329
|
+
token=$(get_provider_token)
|
|
330
|
+
if [ -n "$token" ] && [ "$token" != "null" ]; then
|
|
331
|
+
response=$(fetch_usage_data "$token")
|
|
332
|
+
if [ -n "$response" ]; then
|
|
333
|
+
usage_data="$response"
|
|
334
|
+
echo '{"provider": "${provider}", "fetched_at": '$(date +%s)', "data": '$(echo "$response" | jq -c .)'}' > "$cache_file"
|
|
335
|
+
fi
|
|
336
|
+
fi
|
|
337
|
+
if [ -z "$usage_data" ] && [ -f "$cache_file" ]; then
|
|
338
|
+
usage_data=$(cat "$cache_file" 2>/dev/null | jq -r '.data // empty')
|
|
339
|
+
fi
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
# ── Provider usage lines ─────────────────────────────────
|
|
343
|
+
rate_lines=""
|
|
344
|
+
if [ -n "$usage_data" ] && echo "$usage_data" | jq -e . >/dev/null 2>&1; then
|
|
345
|
+
rate_lines=$(format_usage_lines "$usage_data")
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
# ── Output ──────────────────────────────────────────────
|
|
349
|
+
printf "%b" "$line1"
|
|
350
|
+
[ -n "$rate_lines" ] && printf "\\n\\n%b" "$rate_lines"
|
|
351
|
+
|
|
352
|
+
exit 0
|
|
353
|
+
`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Main install function
|
|
357
|
+
async function run() {
|
|
358
|
+
const args = parseArgs();
|
|
359
|
+
|
|
360
|
+
// Default directory
|
|
361
|
+
const targetDir = args.dir || path.join(os.homedir(), ".claude");
|
|
362
|
+
|
|
363
|
+
// Uninstall mode
|
|
364
|
+
if (args.uninstall) {
|
|
365
|
+
uninstall(targetDir);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Install mode
|
|
370
|
+
console.log();
|
|
371
|
+
console.log(` ${blue}Claude Line Installer${reset}`);
|
|
372
|
+
console.log(` ${dim}─────────────────────${reset}`);
|
|
373
|
+
console.log();
|
|
374
|
+
|
|
375
|
+
// Check dependencies
|
|
376
|
+
const { missing, hasGit } = checkDeps();
|
|
377
|
+
if (missing.length > 0) {
|
|
378
|
+
fail(`Missing required dependencies: ${missing.join(", ")}`);
|
|
379
|
+
log(` Install them and try again.`);
|
|
380
|
+
if (missing.includes("jq")) {
|
|
381
|
+
log(` ${dim}brew install jq${reset}`);
|
|
382
|
+
}
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
success("Dependencies found (jq, curl)");
|
|
386
|
+
|
|
387
|
+
if (!hasGit) {
|
|
388
|
+
warn("git not found - branch display will be disabled");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Get provider
|
|
392
|
+
const provider = await getProvider(args);
|
|
393
|
+
const providerName = provider === "anthropic" ? "Anthropic" : "Z.AI";
|
|
394
|
+
success(`Installing for ${providerName}`);
|
|
395
|
+
|
|
396
|
+
// Create target directory if needed
|
|
397
|
+
if (!fs.existsSync(targetDir)) {
|
|
398
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
399
|
+
success(`Created ${targetDir}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// File paths
|
|
403
|
+
const STATUSLINE_DEST = path.join(targetDir, "statusline.sh");
|
|
404
|
+
const HELPERS_DEST = path.join(targetDir, "statusline-helpers.sh");
|
|
405
|
+
const PROVIDER_DEST = path.join(targetDir, "statusline-provider.sh");
|
|
406
|
+
const SETTINGS_FILE = path.join(targetDir, "settings.json");
|
|
407
|
+
const BACKUP_DEST = STATUSLINE_DEST + ".bak";
|
|
408
|
+
|
|
409
|
+
// Source files
|
|
410
|
+
const HELPERS_SRC = path.resolve(__dirname, "shared-helpers.sh");
|
|
411
|
+
const PROVIDER_SRC = path.resolve(__dirname, "providers", `${provider}.sh`);
|
|
412
|
+
|
|
413
|
+
// Check source files exist
|
|
414
|
+
if (!fs.existsSync(HELPERS_SRC)) {
|
|
415
|
+
fail(`Source file not found: ${HELPERS_SRC}`);
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
if (!fs.existsSync(PROVIDER_SRC)) {
|
|
419
|
+
fail(`Source file not found: ${PROVIDER_SRC}`);
|
|
420
|
+
process.exit(1);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Backup existing statusline.sh
|
|
424
|
+
if (fs.existsSync(STATUSLINE_DEST)) {
|
|
425
|
+
fs.copyFileSync(STATUSLINE_DEST, BACKUP_DEST);
|
|
426
|
+
warn(`Backed up existing statusline to ${dim}statusline.sh.bak${reset}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Copy shared helpers
|
|
430
|
+
fs.copyFileSync(HELPERS_SRC, HELPERS_DEST);
|
|
431
|
+
fs.chmodSync(HELPERS_DEST, 0o644);
|
|
432
|
+
success(`Installed helpers to ${dim}${HELPERS_DEST}${reset}`);
|
|
433
|
+
|
|
434
|
+
// Copy provider script
|
|
435
|
+
fs.copyFileSync(PROVIDER_SRC, PROVIDER_DEST);
|
|
436
|
+
fs.chmodSync(PROVIDER_DEST, 0o644);
|
|
437
|
+
success(
|
|
438
|
+
`Installed ${providerName} provider to ${dim}${PROVIDER_DEST}${reset}`
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
// Generate and write statusline.sh
|
|
442
|
+
const statuslineContent = generateStatuslineScript(provider);
|
|
443
|
+
fs.writeFileSync(STATUSLINE_DEST, statuslineContent);
|
|
444
|
+
fs.chmodSync(STATUSLINE_DEST, 0o755);
|
|
445
|
+
success(`Installed statusline to ${dim}${STATUSLINE_DEST}${reset}`);
|
|
446
|
+
|
|
447
|
+
// Update settings.json
|
|
448
|
+
let settings = {};
|
|
449
|
+
if (fs.existsSync(SETTINGS_FILE)) {
|
|
450
|
+
try {
|
|
451
|
+
settings = JSON.parse(fs.readFileSync(SETTINGS_FILE, "utf-8"));
|
|
452
|
+
} catch {
|
|
453
|
+
fail(`Could not parse ${SETTINGS_FILE} — fix it manually`);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const statusLineConfig = {
|
|
459
|
+
type: "command",
|
|
460
|
+
command: `bash "${targetDir}/statusline.sh"`,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
if (
|
|
464
|
+
settings.statusLine &&
|
|
465
|
+
settings.statusLine.type === "command" &&
|
|
466
|
+
settings.statusLine.command === statusLineConfig.command
|
|
467
|
+
) {
|
|
468
|
+
success("Settings already configured");
|
|
469
|
+
} else {
|
|
470
|
+
settings.statusLine = statusLineConfig;
|
|
471
|
+
fs.writeFileSync(
|
|
472
|
+
SETTINGS_FILE,
|
|
473
|
+
JSON.stringify(settings, null, 2) + "\n"
|
|
474
|
+
);
|
|
475
|
+
success(`Updated ${dim}settings.json${reset} with statusLine config`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
console.log();
|
|
479
|
+
log(
|
|
480
|
+
`${green}Done!${reset} Restart Claude Code to see your new status line.`
|
|
481
|
+
);
|
|
482
|
+
console.log();
|
|
483
|
+
log(`${dim}Provider: ${providerName}${reset}`);
|
|
484
|
+
console.log();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
run().catch((err) => {
|
|
488
|
+
fail(err.message);
|
|
489
|
+
process.exit(1);
|
|
490
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Anthropic provider for cc-statusline
|
|
3
|
+
# Implements: get_provider_token, fetch_usage_data, format_usage_lines
|
|
4
|
+
|
|
5
|
+
# ── Get Anthropic OAuth token ─────────────────────────────
|
|
6
|
+
get_provider_token() {
|
|
7
|
+
local token=""
|
|
8
|
+
|
|
9
|
+
# Check environment variable first
|
|
10
|
+
if [ -n "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
|
|
11
|
+
echo "$CLAUDE_CODE_OAUTH_TOKEN"
|
|
12
|
+
return 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Check macOS keychain
|
|
16
|
+
if command -v security >/dev/null 2>&1; then
|
|
17
|
+
local blob
|
|
18
|
+
blob=$(security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null)
|
|
19
|
+
if [ -n "$blob" ]; then
|
|
20
|
+
token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
|
|
21
|
+
if [ -n "$token" ] && [ "$token" != "null" ]; then
|
|
22
|
+
echo "$token"
|
|
23
|
+
return 0
|
|
24
|
+
fi
|
|
25
|
+
fi
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# Check credentials file
|
|
29
|
+
local creds_file="${HOME}/.claude/.credentials.json"
|
|
30
|
+
if [ -f "$creds_file" ]; then
|
|
31
|
+
token=$(jq -r '.claudeAiOauth.accessToken // empty' "$creds_file" 2>/dev/null)
|
|
32
|
+
if [ -n "$token" ] && [ "$token" != "null" ]; then
|
|
33
|
+
echo "$token"
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Check Linux secret-tool
|
|
39
|
+
if command -v secret-tool >/dev/null 2>&1; then
|
|
40
|
+
local blob
|
|
41
|
+
blob=$(timeout 2 secret-tool lookup service "Claude Code-credentials" 2>/dev/null)
|
|
42
|
+
if [ -n "$blob" ]; then
|
|
43
|
+
token=$(echo "$blob" | jq -r '.claudeAiOauth.accessToken // empty' 2>/dev/null)
|
|
44
|
+
if [ -n "$token" ] && [ "$token" != "null" ]; then
|
|
45
|
+
echo "$token"
|
|
46
|
+
return 0
|
|
47
|
+
fi
|
|
48
|
+
fi
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
echo ""
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ── Fetch Anthropic usage data ────────────────────────────
|
|
55
|
+
fetch_usage_data() {
|
|
56
|
+
local token="$1"
|
|
57
|
+
[ -z "$token" ] && return 1
|
|
58
|
+
|
|
59
|
+
local response
|
|
60
|
+
response=$(curl -s --max-time 5 \
|
|
61
|
+
-H "Accept: application/json" \
|
|
62
|
+
-H "Content-Type: application/json" \
|
|
63
|
+
-H "Authorization: Bearer $token" \
|
|
64
|
+
-H "anthropic-beta: oauth-2025-04-20" \
|
|
65
|
+
-H "User-Agent: claude-code/2.1.34" \
|
|
66
|
+
"https://api.anthropic.com/api/oauth/usage" 2>/dev/null)
|
|
67
|
+
|
|
68
|
+
if [ -n "$response" ] && echo "$response" | jq -e '.five_hour' >/dev/null 2>&1; then
|
|
69
|
+
echo "$response"
|
|
70
|
+
return 0
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
return 1
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# ── Format Anthropic usage lines ──────────────────────────
|
|
77
|
+
format_usage_lines() {
|
|
78
|
+
local usage_data="$1"
|
|
79
|
+
[ -z "$usage_data" ] && return
|
|
80
|
+
|
|
81
|
+
local bar_width=10
|
|
82
|
+
local rate_lines=""
|
|
83
|
+
|
|
84
|
+
# Five hour (current) usage
|
|
85
|
+
local five_hour_pct five_hour_reset_iso five_hour_reset five_hour_bar five_hour_pct_color five_hour_pct_fmt
|
|
86
|
+
five_hour_pct=$(echo "$usage_data" | jq -r '.five_hour.utilization // 0' | awk '{printf "%.0f", $1}')
|
|
87
|
+
five_hour_reset_iso=$(echo "$usage_data" | jq -r '.five_hour.resets_at // empty')
|
|
88
|
+
five_hour_reset=$(format_reset_time "$five_hour_reset_iso" "time")
|
|
89
|
+
five_hour_bar=$(build_bar "$five_hour_pct" "$bar_width")
|
|
90
|
+
five_hour_pct_color=$(color_for_pct "$five_hour_pct")
|
|
91
|
+
five_hour_pct_fmt=$(printf "%3d" "$five_hour_pct")
|
|
92
|
+
|
|
93
|
+
rate_lines+="${white}current${reset} ${five_hour_bar} ${five_hour_pct_color}${five_hour_pct_fmt}%${reset} ${dim}⟳${reset} ${white}${five_hour_reset}${reset}"
|
|
94
|
+
|
|
95
|
+
# Seven day (weekly) usage
|
|
96
|
+
local seven_day_pct seven_day_reset_iso seven_day_reset seven_day_bar seven_day_pct_color seven_day_pct_fmt
|
|
97
|
+
seven_day_pct=$(echo "$usage_data" | jq -r '.seven_day.utilization // 0' | awk '{printf "%.0f", $1}')
|
|
98
|
+
seven_day_reset_iso=$(echo "$usage_data" | jq -r '.seven_day.resets_at // empty')
|
|
99
|
+
seven_day_reset=$(format_reset_time "$seven_day_reset_iso" "datetime")
|
|
100
|
+
seven_day_bar=$(build_bar "$seven_day_pct" "$bar_width")
|
|
101
|
+
seven_day_pct_color=$(color_for_pct "$seven_day_pct")
|
|
102
|
+
seven_day_pct_fmt=$(printf "%3d" "$seven_day_pct")
|
|
103
|
+
|
|
104
|
+
rate_lines+="\n${white}weekly${reset} ${seven_day_bar} ${seven_day_pct_color}${seven_day_pct_fmt}%${reset} ${dim}⟳${reset} ${white}${seven_day_reset}${reset}"
|
|
105
|
+
|
|
106
|
+
# Extra usage (monthly credits) - if enabled
|
|
107
|
+
local extra_enabled extra_pct extra_used extra_limit extra_bar extra_pct_color extra_reset extra_col
|
|
108
|
+
extra_enabled=$(echo "$usage_data" | jq -r '.extra_usage.is_enabled // false')
|
|
109
|
+
if [ "$extra_enabled" = "true" ]; then
|
|
110
|
+
extra_pct=$(echo "$usage_data" | jq -r '.extra_usage.utilization // 0' | awk '{printf "%.0f", $1}')
|
|
111
|
+
extra_used=$(echo "$usage_data" | jq -r '.extra_usage.used_credits // 0' | awk '{printf "%.2f", $1/100}')
|
|
112
|
+
extra_limit=$(echo "$usage_data" | jq -r '.extra_usage.monthly_limit // 0' | awk '{printf "%.2f", $1/100}')
|
|
113
|
+
extra_bar=$(build_bar "$extra_pct" "$bar_width")
|
|
114
|
+
extra_pct_color=$(color_for_pct "$extra_pct")
|
|
115
|
+
|
|
116
|
+
# Calculate next month reset
|
|
117
|
+
extra_reset=$(date -v+1m -v1d +"%b %-d" 2>/dev/null | tr '[:upper:]' '[:lower:]')
|
|
118
|
+
if [ -z "$extra_reset" ]; then
|
|
119
|
+
extra_reset=$(date -d "$(date +%Y-%m-01) +1 month" +"%b %-d" 2>/dev/null | tr '[:upper:]' '[:lower:]')
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
extra_col="${white}extra${reset} ${extra_bar} ${extra_pct_color}\$${extra_used}${dim}/${reset}${white}\$${extra_limit}${reset} ${dim}⟳${reset} ${white}${extra_reset}${reset}"
|
|
123
|
+
rate_lines+="\n${extra_col}"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
printf "%b" "$rate_lines"
|
|
127
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Z.AI provider for cc-statusline
|
|
3
|
+
# Implements: get_provider_token, fetch_usage_data, format_usage_lines
|
|
4
|
+
|
|
5
|
+
# ── Get Z.AI API token from config.yaml ───────────────────
|
|
6
|
+
get_provider_token() {
|
|
7
|
+
local config_file="${HOME}/.chelper/config.yaml"
|
|
8
|
+
[ ! -f "$config_file" ] && echo "" && return 1
|
|
9
|
+
|
|
10
|
+
# Extract api_key using basic string parsing (no yaml parser dependency)
|
|
11
|
+
local token
|
|
12
|
+
token=$(grep "^api_key:" "$config_file" | sed 's/^api_key:[[:space:]]*//' | tr -d '[:space:]"'"'')
|
|
13
|
+
|
|
14
|
+
if [ -n "$token" ]; then
|
|
15
|
+
echo "$token"
|
|
16
|
+
return 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo ""
|
|
20
|
+
return 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# ── Fetch Z.AI usage data ──────────────────────────────────
|
|
24
|
+
fetch_usage_data() {
|
|
25
|
+
local token="$1"
|
|
26
|
+
[ -z "$token" ] && return 1
|
|
27
|
+
|
|
28
|
+
local response
|
|
29
|
+
response=$(curl -s --max-time 5 \
|
|
30
|
+
-H "Accept: application/json" \
|
|
31
|
+
-H "Content-Type: application/json" \
|
|
32
|
+
-H "Authorization: Bearer $token" \
|
|
33
|
+
"https://api.z.ai/api/monitor/usage/quota/limit" 2>/dev/null)
|
|
34
|
+
|
|
35
|
+
if [ -n "$response" ] && echo "$response" | jq -e '.data' >/dev/null 2>&1; then
|
|
36
|
+
echo "$response"
|
|
37
|
+
return 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
return 1
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# ── Format Z.AI usage lines ────────────────────────────────
|
|
44
|
+
format_usage_lines() {
|
|
45
|
+
local usage_data="$1"
|
|
46
|
+
[ -z "$usage_data" ] && return
|
|
47
|
+
|
|
48
|
+
local bar_width=10
|
|
49
|
+
local rate_lines=""
|
|
50
|
+
|
|
51
|
+
# TOKENS_LIMIT (current)
|
|
52
|
+
local tokens_used tokens_limit tokens_pct tokens_reset_iso tokens_reset tokens_bar tokens_pct_color tokens_pct_fmt tokens_display
|
|
53
|
+
tokens_used=$(echo "$usage_data" | jq -r '.data.TOKENS_LIMIT.used // 0')
|
|
54
|
+
tokens_limit=$(echo "$usage_data" | jq -r '.data.TOKENS_LIMIT.limit // 1')
|
|
55
|
+
tokens_reset_iso=$(echo "$usage_data" | jq -r '.data.TOKENS_LIMIT.reset_time // empty')
|
|
56
|
+
|
|
57
|
+
if [ "$tokens_limit" -gt 0 ]; then
|
|
58
|
+
tokens_pct=$(( tokens_used * 100 / tokens_limit ))
|
|
59
|
+
else
|
|
60
|
+
tokens_pct=0
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
tokens_reset=$(format_reset_time "$tokens_reset_iso" "time")
|
|
64
|
+
tokens_bar=$(build_bar "$tokens_pct" "$bar_width")
|
|
65
|
+
tokens_pct_color=$(color_for_pct "$tokens_pct")
|
|
66
|
+
tokens_pct_fmt=$(printf "%3d" "$tokens_pct")
|
|
67
|
+
tokens_display=$(format_tokens "$tokens_used")
|
|
68
|
+
|
|
69
|
+
rate_lines+="${white}current${reset} ${tokens_bar} ${tokens_pct_color}${tokens_pct_fmt}%${reset} ${dim}(${reset}${white}${tokens_display}${reset}${dim})${reset} ${dim}⟳${reset} ${white}${tokens_reset}${reset}"
|
|
70
|
+
|
|
71
|
+
# TIME_LIMIT (tools/MCP) - no reset time shown
|
|
72
|
+
local time_used time_limit time_pct time_bar time_pct_color time_pct_fmt time_seconds
|
|
73
|
+
time_used=$(echo "$usage_data" | jq -r '.data.TIME_LIMIT.used // 0')
|
|
74
|
+
time_limit=$(echo "$usage_data" | jq -r '.data.TIME_LIMIT.limit // 1')
|
|
75
|
+
|
|
76
|
+
if [ "$time_limit" -gt 0 ]; then
|
|
77
|
+
time_pct=$(( time_used * 100 / time_limit ))
|
|
78
|
+
else
|
|
79
|
+
time_pct=0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
time_bar=$(build_bar "$time_pct" "$bar_width")
|
|
83
|
+
time_pct_color=$(color_for_pct "$time_pct")
|
|
84
|
+
time_pct_fmt=$(printf "%3d" "$time_pct")
|
|
85
|
+
|
|
86
|
+
# Convert seconds to readable format
|
|
87
|
+
if [ "$time_used" -ge 3600 ]; then
|
|
88
|
+
time_seconds="$(( time_used / 3600 ))h$(( (time_used % 3600) / 60 ))m"
|
|
89
|
+
elif [ "$time_used" -ge 60 ]; then
|
|
90
|
+
time_seconds="$(( time_used / 60 ))m"
|
|
91
|
+
else
|
|
92
|
+
time_seconds="${time_used}s"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
rate_lines+="\n${white}tools${reset} ${time_bar} ${time_pct_color}${time_pct_fmt}%${reset} ${dim}(${reset}${white}${time_seconds}${reset}${dim})${reset}"
|
|
96
|
+
|
|
97
|
+
printf "%b" "$rate_lines"
|
|
98
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared helper functions for cc-statusline
|
|
3
|
+
# This file is copied to ~/.claude/ or ~/.claude-z/ at install time
|
|
4
|
+
|
|
5
|
+
# ── Colors ──────────────────────────────────────────────
|
|
6
|
+
blue='\033[38;2;0;153;255m'
|
|
7
|
+
orange='\033[38;2;255;176;85m'
|
|
8
|
+
green='\033[38;2;0;175;80m'
|
|
9
|
+
cyan='\033[38;2;86;182;194m'
|
|
10
|
+
red='\033[38;2;255;85;85m'
|
|
11
|
+
yellow='\033[38;2;230;200;0m'
|
|
12
|
+
white='\033[38;2;220;220;220m'
|
|
13
|
+
magenta='\033[38;2;180;140;255m'
|
|
14
|
+
dim='\033[2m'
|
|
15
|
+
reset='\033[0m'
|
|
16
|
+
|
|
17
|
+
# ── Format token counts ───────────────────────────────────
|
|
18
|
+
format_tokens() {
|
|
19
|
+
local num=$1
|
|
20
|
+
if [ "$num" -ge 1000000 ]; then
|
|
21
|
+
awk "BEGIN {printf \"%.1fm\", $num / 1000000}"
|
|
22
|
+
elif [ "$num" -ge 1000 ]; then
|
|
23
|
+
awk "BEGIN {printf \"%.0fk\", $num / 1000}"
|
|
24
|
+
else
|
|
25
|
+
printf "%d" "$num"
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# ── Get color based on percentage ─────────────────────────
|
|
30
|
+
color_for_pct() {
|
|
31
|
+
local pct=$1
|
|
32
|
+
if [ "$pct" -ge 90 ]; then printf "$red"
|
|
33
|
+
elif [ "$pct" -ge 70 ]; then printf "$yellow"
|
|
34
|
+
elif [ "$pct" -ge 50 ]; then printf "$orange"
|
|
35
|
+
else printf "$green"
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# ── Build visual bar ─────────────────────────────────────
|
|
40
|
+
build_bar() {
|
|
41
|
+
local pct=$1
|
|
42
|
+
local width=$2
|
|
43
|
+
[ "$pct" -lt 0 ] 2>/dev/null && pct=0
|
|
44
|
+
[ "$pct" -gt 100 ] 2>/dev/null && pct=100
|
|
45
|
+
|
|
46
|
+
local filled=$(( pct * width / 100 ))
|
|
47
|
+
local empty=$(( width - filled ))
|
|
48
|
+
local bar_color
|
|
49
|
+
bar_color=$(color_for_pct "$pct")
|
|
50
|
+
|
|
51
|
+
local filled_str="" empty_str=""
|
|
52
|
+
for ((i=0; i<filled; i++)); do filled_str+="●"; done
|
|
53
|
+
for ((i=0; i<empty; i++)); do empty_str+="○"; done
|
|
54
|
+
|
|
55
|
+
printf "${bar_color}${filled_str}${dim}${empty_str}${reset}"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ── Convert ISO 8601 to epoch (cross-platform) ───────────
|
|
59
|
+
iso_to_epoch() {
|
|
60
|
+
local iso_str="$1"
|
|
61
|
+
|
|
62
|
+
local epoch
|
|
63
|
+
# Try GNU date first (Linux)
|
|
64
|
+
epoch=$(date -d "${iso_str}" +%s 2>/dev/null)
|
|
65
|
+
if [ -n "$epoch" ]; then
|
|
66
|
+
echo "$epoch"
|
|
67
|
+
return 0
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Strip milliseconds and timezone for macOS date
|
|
71
|
+
local stripped="${iso_str%%.*}"
|
|
72
|
+
stripped="${stripped%%Z}"
|
|
73
|
+
stripped="${stripped%%+*}"
|
|
74
|
+
stripped="${stripped%%-[0-9][0-9]:[0-9][0-9]}"
|
|
75
|
+
|
|
76
|
+
# Try macOS date with timezone handling
|
|
77
|
+
if [[ "$iso_str" == *"Z"* ]] || [[ "$iso_str" == *"+00:00"* ]] || [[ "$iso_str" == *"-00:00"* ]]; then
|
|
78
|
+
epoch=$(env TZ=UTC date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null)
|
|
79
|
+
else
|
|
80
|
+
epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$stripped" +%s 2>/dev/null)
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
if [ -n "$epoch" ]; then
|
|
84
|
+
echo "$epoch"
|
|
85
|
+
return 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
return 1
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# ── Format reset times ───────────────────────────────────
|
|
92
|
+
format_reset_time() {
|
|
93
|
+
local iso_str="$1"
|
|
94
|
+
local style="$2"
|
|
95
|
+
[ -z "$iso_str" ] || [ "$iso_str" = "null" ] && return
|
|
96
|
+
|
|
97
|
+
local epoch
|
|
98
|
+
epoch=$(iso_to_epoch "$iso_str")
|
|
99
|
+
[ -z "$epoch" ] && return
|
|
100
|
+
|
|
101
|
+
local result=""
|
|
102
|
+
case "$style" in
|
|
103
|
+
time)
|
|
104
|
+
result=$(date -j -r "$epoch" +"%l:%M%p" 2>/dev/null | sed 's/^ //; s/\.//g' | tr '[:upper:]' '[:lower:]')
|
|
105
|
+
[ -z "$result" ] && result=$(date -d "@$epoch" +"%l:%M%P" 2>/dev/null | sed 's/^ //; s/\.//g')
|
|
106
|
+
;;
|
|
107
|
+
datetime)
|
|
108
|
+
result=$(date -j -r "$epoch" +"%b %-d, %l:%M%p" 2>/dev/null | sed 's/ / /g; s/^ //; s/\.//g' | tr '[:upper:]' '[:lower:]')
|
|
109
|
+
[ -z "$result" ] && result=$(date -d "@$epoch" +"%b %-d, %l:%M%P" 2>/dev/null | sed 's/ / /g; s/^ //; s/\.//g')
|
|
110
|
+
;;
|
|
111
|
+
*)
|
|
112
|
+
result=$(date -j -r "$epoch" +"%b %-d" 2>/dev/null | tr '[:upper:]' '[:lower:]')
|
|
113
|
+
[ -z "$result" ] && result=$(date -d "@$epoch" +"%b %-d" 2>/dev/null)
|
|
114
|
+
;;
|
|
115
|
+
esac
|
|
116
|
+
printf "%s" "$result"
|
|
117
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thlinh/cc-statusline",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Multi-provider status line for Claude Code CLI supporting Anthropic and Z.AI APIs",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cc-statusline": "./bin/install.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"claude",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"statusline",
|
|
12
|
+
"cli",
|
|
13
|
+
"anthropic",
|
|
14
|
+
"zai",
|
|
15
|
+
"multi-provider"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Linh Truong",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/thlinhit/cc-statusline.git"
|
|
22
|
+
}
|
|
23
|
+
}
|