coding-usage-bar 0.2.3 → 0.3.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/dist/format.d.ts +1 -0
- package/dist/format.js +25 -0
- package/dist/kimi.d.ts +1 -0
- package/dist/kimi.js +45 -0
- package/dist/menubar.d.ts +1 -1
- package/dist/menubar.js +23 -14
- package/dist/status.js +17 -1
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/dist/format.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BurnAnalysis, StatusIssue, ProviderUsage, UsageWindow, StatusSnapshot } from "./types.js";
|
|
2
2
|
export declare function formatDurationUntil(iso: string, now?: Date): string;
|
|
3
|
+
export declare function formatResetTime(iso: string, now?: Date): string;
|
|
3
4
|
export declare function formatProviderLabel(provider: string): "Claude" | "GLM" | "DeepSeek" | "MiniMax" | "Kimi" | "Codex";
|
|
4
5
|
export declare function formatWindowLabel(window: UsageWindow): "5h" | "7d";
|
|
5
6
|
export declare function usageBar(usedPercent: number, width?: number): string;
|
package/dist/format.js
CHANGED
|
@@ -11,6 +11,31 @@ export function formatDurationUntil(iso, now = new Date()) {
|
|
|
11
11
|
}
|
|
12
12
|
return `${mins}m`;
|
|
13
13
|
}
|
|
14
|
+
// Absolute reset label for surfaces that re-render on a timer (SwiftBar).
|
|
15
|
+
// A countdown changes every minute even when the underlying usage data is
|
|
16
|
+
// unchanged, which forces SwiftBar to rebuild the menu and repaint the menu
|
|
17
|
+
// bar on every refresh. An absolute clock time is a pure function of the
|
|
18
|
+
// reset timestamp, so unchanged data renders to identical bytes and SwiftBar
|
|
19
|
+
// skips the update entirely. `now` only decides "due" and whether the day
|
|
20
|
+
// prefix is needed, both of which change at most once per window/day.
|
|
21
|
+
export function formatResetTime(iso, now = new Date()) {
|
|
22
|
+
const resetMs = Date.parse(iso);
|
|
23
|
+
if (!Number.isFinite(resetMs)) {
|
|
24
|
+
return "due";
|
|
25
|
+
}
|
|
26
|
+
if (resetMs <= now.getTime()) {
|
|
27
|
+
return "due";
|
|
28
|
+
}
|
|
29
|
+
const reset = new Date(resetMs);
|
|
30
|
+
const sameDay = reset.getFullYear() === now.getFullYear()
|
|
31
|
+
&& reset.getMonth() === now.getMonth()
|
|
32
|
+
&& reset.getDate() === now.getDate();
|
|
33
|
+
const clock = `${String(reset.getHours()).padStart(2, "0")}:${String(reset.getMinutes()).padStart(2, "0")}`;
|
|
34
|
+
if (sameDay) {
|
|
35
|
+
return clock;
|
|
36
|
+
}
|
|
37
|
+
return `${reset.getMonth() + 1}/${reset.getDate()} ${clock}`;
|
|
38
|
+
}
|
|
14
39
|
function windowLabel(window) {
|
|
15
40
|
return window.name === "five_hour" ? "5h" : "7d";
|
|
16
41
|
}
|
package/dist/kimi.d.ts
CHANGED
|
@@ -28,5 +28,6 @@ export declare function usageFromKimiUsages(usages: KimiUsagesResponse, options:
|
|
|
28
28
|
}): ProviderUsage | null;
|
|
29
29
|
export declare function readClaudeLanesKimiConfig(configFile: string): KimiConfig | null;
|
|
30
30
|
export declare function resolveKimiConfig(config: KimiConfig | undefined, homeDir?: string): KimiConfig;
|
|
31
|
+
export declare function probeKimiAccessFrozen(config: KimiConfig): Promise<boolean>;
|
|
31
32
|
export declare function collectKimiUsage(config: KimiConfig): Promise<ProviderUsage>;
|
|
32
33
|
export {};
|
package/dist/kimi.js
CHANGED
|
@@ -132,6 +132,41 @@ export function resolveKimiConfig(config, homeDir = os.homedir()) {
|
|
|
132
132
|
apiKey: config?.apiKey || lanes?.apiKey,
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
|
+
// Kimi's monthly billing-cycle quota can freeze all chat access while
|
|
136
|
+
// /v1/usages still reports headroom in the 5h/weekly windows (the monthly
|
|
137
|
+
// number itself is not exposed on any API-key-authenticated endpoint; it only
|
|
138
|
+
// shows up in the logged-in web console). Detect the freeze with a
|
|
139
|
+
// zero-cost probe: the billing-cycle gate runs before model validation, so a
|
|
140
|
+
// chat request naming a nonexistent model returns 403 access_terminated_error
|
|
141
|
+
// when frozen — and a "model not found" error (no inference, no quota use)
|
|
142
|
+
// when healthy.
|
|
143
|
+
const ACCESS_PROBE_MODEL = "coding-usage-bar-access-probe";
|
|
144
|
+
export async function probeKimiAccessFrozen(config) {
|
|
145
|
+
const baseUrl = (config.baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
146
|
+
const response = await fetch(`${baseUrl}/v1/chat/completions`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
model: ACCESS_PROBE_MODEL,
|
|
154
|
+
max_tokens: 1,
|
|
155
|
+
messages: [{ role: "user", content: "hi" }],
|
|
156
|
+
}),
|
|
157
|
+
signal: AbortSignal.timeout(10_000),
|
|
158
|
+
});
|
|
159
|
+
if (response.status !== 403) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const body = (await response.json());
|
|
164
|
+
return body?.error?.type === "access_terminated_error";
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
135
170
|
export async function collectKimiUsage(config) {
|
|
136
171
|
if (!config.apiKey) {
|
|
137
172
|
throw new Error("Kimi API key not configured. Edit ~/.coding-usage-bar/config.json to set kimi.apiKey, or configure a kimi.com lane in ~/.config/claude-lanes/config.env.");
|
|
@@ -144,5 +179,15 @@ export async function collectKimiUsage(config) {
|
|
|
144
179
|
if (!usage) {
|
|
145
180
|
throw new Error("Kimi usages response did not contain expected 5h/7d quota data");
|
|
146
181
|
}
|
|
182
|
+
// The probe must never fail the collection: on network errors the window
|
|
183
|
+
// data stays valid and simply renders unblocked.
|
|
184
|
+
try {
|
|
185
|
+
if (await probeKimiAccessFrozen(config)) {
|
|
186
|
+
usage.blocked = { reason: "monthly quota exhausted; access frozen until the next billing cycle" };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Probe unavailable; keep the usage data as-is.
|
|
191
|
+
}
|
|
147
192
|
return usage;
|
|
148
193
|
}
|
package/dist/menubar.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare function openSwiftBar(): string[];
|
|
|
14
14
|
export declare function addSwiftBarToLoginItems(options?: {
|
|
15
15
|
dryRun?: boolean;
|
|
16
16
|
}): string[];
|
|
17
|
-
export declare function renderMenuBar(snapshot?: StatusSnapshot, paths?: RuntimePaths): string;
|
|
17
|
+
export declare function renderMenuBar(snapshot?: StatusSnapshot, paths?: RuntimePaths, now?: Date): string;
|
|
18
18
|
export declare function swiftBarStatusItemVisibilityKeys(defaultsOutput: string): string[];
|
|
19
19
|
export declare function installMenuBar(options?: {
|
|
20
20
|
dryRun?: boolean;
|
package/dist/menubar.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { ensureDir, isFile } from "./fs-util.js";
|
|
6
|
-
import {
|
|
6
|
+
import { formatProviderLabel, formatResetTime } from "./format.js";
|
|
7
7
|
import { buildPaths } from "./paths.js";
|
|
8
8
|
import { stableNodeExecutable } from "./node-runtime.js";
|
|
9
9
|
import { loadDisplayStatusSnapshot } from "./runtime.js";
|
|
@@ -338,7 +338,9 @@ function titleBars(provider) {
|
|
|
338
338
|
return [{ pct: isAvailable ? 100 : 0, lightColor, darkColor }];
|
|
339
339
|
}
|
|
340
340
|
const state = provider.analysis.state;
|
|
341
|
-
|
|
341
|
+
// Blocked (Kimi billing-cycle freeze): grey bars say "no access" without
|
|
342
|
+
// lying about the 5h/7d window numbers.
|
|
343
|
+
const [lightColor, darkColor] = (provider.usage.blocked ? MUTED_COLOR : STATE_COLOR[state]).split(",");
|
|
342
344
|
const fiveHour = provider.analysis.fiveHour;
|
|
343
345
|
const sevenDay = provider.analysis.sevenDay;
|
|
344
346
|
const bars = [];
|
|
@@ -450,10 +452,10 @@ function coloredMeter(usedPercent, fillColor, width = METER_WIDTH) {
|
|
|
450
452
|
return (filled > 0 ? ansiRGB(fillColor, "█".repeat(filled)) : "")
|
|
451
453
|
+ (empty > 0 ? ansiRGB(MUTED_COLOR, "░".repeat(empty)) : "");
|
|
452
454
|
}
|
|
453
|
-
function usageLine(label, usedPercent, resetsAt, color, barImage) {
|
|
455
|
+
function usageLine(label, usedPercent, resetsAt, color, now, barImage) {
|
|
454
456
|
const percent = `${Math.round(usedPercent).toString().padStart(3)}%`;
|
|
455
457
|
if (barImage) {
|
|
456
|
-
return line(`${label} ${percent} reset ${
|
|
458
|
+
return line(`${label} ${percent} reset ${formatResetTime(resetsAt, now)}`, {
|
|
457
459
|
image: barImage.image,
|
|
458
460
|
width: barImage.width,
|
|
459
461
|
height: barImage.height,
|
|
@@ -463,7 +465,7 @@ function usageLine(label, usedPercent, resetsAt, color, barImage) {
|
|
|
463
465
|
});
|
|
464
466
|
}
|
|
465
467
|
const bar = coloredMeter(usedPercent, color);
|
|
466
|
-
return line(`${label} ${bar} ${ansiRGB(color, percent)} reset ${
|
|
468
|
+
return line(`${label} ${bar} ${ansiRGB(color, percent)} reset ${formatResetTime(resetsAt, now)}`, {
|
|
467
469
|
ansi: true,
|
|
468
470
|
font: ROW_FONT,
|
|
469
471
|
size: 12,
|
|
@@ -490,8 +492,8 @@ function titleIconParams() {
|
|
|
490
492
|
}
|
|
491
493
|
return { sfimage: "flame.fill", sfcolor: RAW_COLOR };
|
|
492
494
|
}
|
|
493
|
-
function
|
|
494
|
-
return
|
|
495
|
+
function anyProviderStale(snapshot) {
|
|
496
|
+
return snapshot.providers.some((item) => item.meta.stale);
|
|
495
497
|
}
|
|
496
498
|
function issueLabel(code) {
|
|
497
499
|
if (code === "CLAUDE_INGEST_MISSING") {
|
|
@@ -517,7 +519,11 @@ function issueLabel(code) {
|
|
|
517
519
|
}
|
|
518
520
|
return code.replaceAll("_", " ").toLowerCase();
|
|
519
521
|
}
|
|
520
|
-
|
|
522
|
+
// `now` only feeds low-frequency decisions (reset due/day prefix). The output
|
|
523
|
+
// must stay byte-identical across renders while the snapshot is unchanged, so
|
|
524
|
+
// SwiftBar's content guard can skip the menu rebuild and menu bar repaint;
|
|
525
|
+
// never render anything derived from wall-clock age or countdowns here.
|
|
526
|
+
export function renderMenuBar(snapshot = loadDisplayStatusSnapshot(), paths = buildPaths(), now = new Date()) {
|
|
521
527
|
const compact = readCompactMode(paths);
|
|
522
528
|
const providers = titleProviders(snapshot);
|
|
523
529
|
const title = providers.map(titleSegment).join(` ${TITLE_SEPARATOR} `);
|
|
@@ -571,7 +577,7 @@ export function renderMenuBar(snapshot = loadDisplayStatusSnapshot(), paths = bu
|
|
|
571
577
|
sfcolor: STATE_COLOR[topState],
|
|
572
578
|
badge: snapshot.profile.toUpperCase(),
|
|
573
579
|
}),
|
|
574
|
-
muted(
|
|
580
|
+
muted(anyProviderStale(snapshot) ? "Data stale" : "Data fresh"),
|
|
575
581
|
"---",
|
|
576
582
|
];
|
|
577
583
|
const barDataList = [];
|
|
@@ -579,7 +585,7 @@ export function renderMenuBar(snapshot = loadDisplayStatusSnapshot(), paths = bu
|
|
|
579
585
|
if (item.usage.balance && item.usage.windows.length === 0) {
|
|
580
586
|
continue;
|
|
581
587
|
}
|
|
582
|
-
const [lc, dc] = STATE_COLOR[item.analysis.state].split(",");
|
|
588
|
+
const [lc, dc] = (item.usage.blocked ? MUTED_COLOR : STATE_COLOR[item.analysis.state]).split(",");
|
|
583
589
|
const f = windowByName(item, "five_hour");
|
|
584
590
|
const s = windowByName(item, "seven_day");
|
|
585
591
|
if (f) {
|
|
@@ -614,17 +620,20 @@ export function renderMenuBar(snapshot = loadDisplayStatusSnapshot(), paths = bu
|
|
|
614
620
|
lines.push("---");
|
|
615
621
|
continue;
|
|
616
622
|
}
|
|
617
|
-
lines.push(line(`${formatProviderLabel(item.usage.provider)} ${STATE_LABEL[item.analysis.state]}`, {
|
|
623
|
+
lines.push(line(`${formatProviderLabel(item.usage.provider)} ${item.usage.blocked ? "Blocked" : STATE_LABEL[item.analysis.state]}`, {
|
|
618
624
|
...providerIconParams(item.usage.provider),
|
|
619
625
|
color: TEXT_COLOR,
|
|
620
626
|
size: 14,
|
|
621
|
-
badge: providerBadge(item),
|
|
627
|
+
badge: item.usage.blocked ? "Frozen" : providerBadge(item),
|
|
622
628
|
}));
|
|
623
629
|
if (five) {
|
|
624
|
-
lines.push(usageLine("5h", five.usedPercent, five.resetsAt, TEXT_COLOR, dropdownBars[barIdx++]));
|
|
630
|
+
lines.push(usageLine("5h", five.usedPercent, five.resetsAt, TEXT_COLOR, now, dropdownBars[barIdx++]));
|
|
625
631
|
}
|
|
626
632
|
if (seven) {
|
|
627
|
-
lines.push(usageLine("7d", seven.usedPercent, seven.resetsAt, TEXT_COLOR, dropdownBars[barIdx++]));
|
|
633
|
+
lines.push(usageLine("7d", seven.usedPercent, seven.resetsAt, TEXT_COLOR, now, dropdownBars[barIdx++]));
|
|
634
|
+
}
|
|
635
|
+
if (item.usage.blocked) {
|
|
636
|
+
lines.push(muted(`Blocked ${item.usage.blocked.reason}. 5h/7d numbers above still reflect the window quotas.`));
|
|
628
637
|
}
|
|
629
638
|
lines.push(muted(targetLabel(item)));
|
|
630
639
|
lines.push(line(item.analysis.message, { color: MUTED_COLOR, size: 12, length: 84 }));
|
package/dist/status.js
CHANGED
|
@@ -12,6 +12,22 @@ function buildProviderMeta(usage, generatedAt, staleAfterSeconds) {
|
|
|
12
12
|
stale: ageSeconds > staleAfterSeconds,
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
+
// Coarse buckets keep the message stable between renders: an exact age changes
|
|
16
|
+
// every second, which would defeat SwiftBar's unchanged-content guard and force
|
|
17
|
+
// a menu rebuild each refresh while the data itself is unchanged.
|
|
18
|
+
function staleAgeLabel(ageSeconds) {
|
|
19
|
+
const buckets = [
|
|
20
|
+
{ seconds: 86_400, label: "1d" },
|
|
21
|
+
{ seconds: 43_200, label: "12h" },
|
|
22
|
+
{ seconds: 10_800, label: "3h" },
|
|
23
|
+
{ seconds: 3_600, label: "1h" },
|
|
24
|
+
{ seconds: 1_800, label: "30m" },
|
|
25
|
+
{ seconds: 600, label: "10m" },
|
|
26
|
+
{ seconds: 60, label: "1m" },
|
|
27
|
+
];
|
|
28
|
+
const hit = buckets.find((bucket) => ageSeconds >= bucket.seconds);
|
|
29
|
+
return hit ? `over ${hit.label}` : "under 1m";
|
|
30
|
+
}
|
|
15
31
|
function staleIssuesFor(providers) {
|
|
16
32
|
return providers
|
|
17
33
|
.filter((provider) => provider.meta.stale)
|
|
@@ -19,7 +35,7 @@ function staleIssuesFor(providers) {
|
|
|
19
35
|
provider: provider.usage.provider,
|
|
20
36
|
severity: "warning",
|
|
21
37
|
code: "USAGE_STALE",
|
|
22
|
-
message: `${provider.usage.provider} usage is stale; latest observation is ${provider.meta.ageSeconds}
|
|
38
|
+
message: `${provider.usage.provider} usage is stale; latest observation is ${staleAgeLabel(provider.meta.ageSeconds)} old.`,
|
|
23
39
|
}));
|
|
24
40
|
}
|
|
25
41
|
export function createStatusSnapshot(usages, profile, options = {}) {
|
package/dist/types.d.ts
CHANGED