coding-usage-bar 0.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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +220 -0
  3. package/README.zh-CN.md +53 -0
  4. package/THIRD_PARTY_NOTICES.md +16 -0
  5. package/assets/coding-usage-bar-limit.png +0 -0
  6. package/assets/coding-usage-bar-ok.png +0 -0
  7. package/assets/coding-usage-bar-over.png +0 -0
  8. package/assets/coding-usage-bar-under.png +0 -0
  9. package/assets/provider-claude.png +0 -0
  10. package/assets/provider-codex.png +0 -0
  11. package/assets/provider-deepseek.png +0 -0
  12. package/assets/provider-glm-dark.png +0 -0
  13. package/assets/provider-glm.png +0 -0
  14. package/assets/provider-marks.png +0 -0
  15. package/assets/provider-minimax.png +0 -0
  16. package/dist/burn.d.ts +3 -0
  17. package/dist/burn.js +164 -0
  18. package/dist/card.d.ts +4 -0
  19. package/dist/card.js +79 -0
  20. package/dist/claude.d.ts +9 -0
  21. package/dist/claude.js +141 -0
  22. package/dist/cli.d.ts +2 -0
  23. package/dist/cli.js +129 -0
  24. package/dist/codex.d.ts +6 -0
  25. package/dist/codex.js +128 -0
  26. package/dist/config.d.ts +4 -0
  27. package/dist/config.js +64 -0
  28. package/dist/daemon.d.ts +3 -0
  29. package/dist/daemon.js +16 -0
  30. package/dist/deepseek.d.ts +18 -0
  31. package/dist/deepseek.js +47 -0
  32. package/dist/doctor.d.ts +6 -0
  33. package/dist/doctor.js +225 -0
  34. package/dist/format.d.ts +9 -0
  35. package/dist/format.js +92 -0
  36. package/dist/fs-util.d.ts +6 -0
  37. package/dist/fs-util.js +39 -0
  38. package/dist/glm.d.ts +22 -0
  39. package/dist/glm.js +62 -0
  40. package/dist/install.d.ts +25 -0
  41. package/dist/install.js +432 -0
  42. package/dist/menubar.d.ts +23 -0
  43. package/dist/menubar.js +880 -0
  44. package/dist/minimax.d.ts +47 -0
  45. package/dist/minimax.js +100 -0
  46. package/dist/node-runtime.d.ts +1 -0
  47. package/dist/node-runtime.js +29 -0
  48. package/dist/notifier.d.ts +7 -0
  49. package/dist/notifier.js +121 -0
  50. package/dist/paths.d.ts +5 -0
  51. package/dist/paths.js +45 -0
  52. package/dist/runtime.d.ts +23 -0
  53. package/dist/runtime.js +244 -0
  54. package/dist/star.d.ts +15 -0
  55. package/dist/star.js +117 -0
  56. package/dist/status.d.ts +14 -0
  57. package/dist/status.js +65 -0
  58. package/dist/store.d.ts +4 -0
  59. package/dist/store.js +29 -0
  60. package/dist/types.d.ts +101 -0
  61. package/dist/types.js +1 -0
  62. package/dist/usage.d.ts +12 -0
  63. package/dist/usage.js +56 -0
  64. package/docs/assets/hero.png +0 -0
  65. package/fixtures/claude/latest.json +20 -0
  66. package/fixtures/claude/samples.jsonl +3 -0
  67. package/fixtures/codex/latest.json +20 -0
  68. package/fixtures/codex/samples.jsonl +3 -0
  69. package/fixtures/deepseek/latest.json +12 -0
  70. package/package.json +59 -0
@@ -0,0 +1,47 @@
1
+ import { MinimaxConfig, ProviderUsage } from "./types.js";
2
+ interface MinimaxModelRemain {
3
+ model_name: string;
4
+ start_time: number;
5
+ end_time: number;
6
+ remains_time: number;
7
+ current_interval_total_count: number;
8
+ current_interval_usage_count: number;
9
+ current_interval_remaining_percent?: number;
10
+ current_weekly_total_count: number;
11
+ current_weekly_usage_count: number;
12
+ current_weekly_remaining_percent?: number;
13
+ weekly_start_time: number;
14
+ weekly_end_time: number;
15
+ weekly_remains_time: number;
16
+ }
17
+ interface MinimaxQuotaResponse {
18
+ model_remains: MinimaxModelRemain[];
19
+ category_remains?: MinimaxCategoryRemain[];
20
+ base_resp: {
21
+ status_code: number;
22
+ status_msg: string;
23
+ };
24
+ }
25
+ interface MinimaxCategoryRemain {
26
+ start_time: number;
27
+ end_time: number;
28
+ remains_time: number;
29
+ current_interval_total_count: number;
30
+ current_interval_usage_count: number;
31
+ current_interval_remaining_percent?: number;
32
+ current_weekly_total_count: number;
33
+ current_weekly_usage_count: number;
34
+ current_weekly_remaining_percent?: number;
35
+ weekly_start_time: number;
36
+ weekly_end_time: number;
37
+ weekly_remains_time: number;
38
+ category: string;
39
+ display_name: string;
40
+ }
41
+ export declare function fetchMinimaxQuota(config: MinimaxConfig): Promise<MinimaxQuotaResponse>;
42
+ export declare function usageFromMinimaxQuota(quota: MinimaxQuotaResponse, options: {
43
+ observedAt?: string;
44
+ source: string;
45
+ }): ProviderUsage | null;
46
+ export declare function collectMinimaxUsage(config: MinimaxConfig): Promise<ProviderUsage>;
47
+ export {};
@@ -0,0 +1,100 @@
1
+ import { makeProviderUsage, normalizeWindow } from "./usage.js";
2
+ const CN_BASE_URL = "https://api.minimaxi.com";
3
+ const GLOBAL_BASE_URL = "https://api.minimax.io";
4
+ function baseUrl(config) {
5
+ return config.region === "global" ? GLOBAL_BASE_URL : CN_BASE_URL;
6
+ }
7
+ export async function fetchMinimaxQuota(config) {
8
+ const url = `${baseUrl(config)}/v1/token_plan/remains`;
9
+ const response = await fetch(url, {
10
+ method: "GET",
11
+ headers: {
12
+ "Authorization": `Bearer ${config.apiKey}`,
13
+ "Content-Type": "application/json",
14
+ },
15
+ signal: AbortSignal.timeout(15_000),
16
+ });
17
+ if (!response.ok) {
18
+ throw new Error(`MiniMax quota API returned HTTP ${response.status}`);
19
+ }
20
+ const data = (await response.json());
21
+ if (data.base_resp?.status_code !== 0) {
22
+ throw new Error(`MiniMax API error: ${data.base_resp?.status_msg ?? "unknown error"} (code ${data.base_resp?.status_code})`);
23
+ }
24
+ return data;
25
+ }
26
+ export function usageFromMinimaxQuota(quota, options) {
27
+ const models = quota.model_remains;
28
+ const categories = quota.category_remains;
29
+ if (!models || models.length === 0) {
30
+ return null;
31
+ }
32
+ // Prefer the "general" model entry — it reflects the text/chat plan this CLI
33
+ // is most often used for. Fall back to the first model so we never return
34
+ // null for an account that only reports other models.
35
+ const primary = models.find((m) => m.model_name === "general") ?? models[0];
36
+ let fiveHourPercent = 0;
37
+ let fiveHourReset = primary.end_time;
38
+ let weeklyPercent = 0;
39
+ let weeklyReset = primary.weekly_end_time;
40
+ if (categories && categories.length > 0) {
41
+ const textGen = categories.find((c) => c.category === "text_generation");
42
+ if (textGen) {
43
+ fiveHourPercent = percentFromQuota(textGen.current_interval_total_count, textGen.current_interval_usage_count, textGen.current_interval_remaining_percent);
44
+ fiveHourReset = textGen.end_time;
45
+ weeklyPercent = percentFromQuota(textGen.current_weekly_total_count, textGen.current_weekly_usage_count, textGen.current_weekly_remaining_percent) ?? fiveHourPercent;
46
+ weeklyReset = textGen.weekly_end_time;
47
+ }
48
+ }
49
+ else {
50
+ fiveHourPercent = percentFromQuota(primary.current_interval_total_count, primary.current_interval_usage_count, primary.current_interval_remaining_percent);
51
+ weeklyPercent = percentFromQuota(primary.current_weekly_total_count, primary.current_weekly_usage_count, primary.current_weekly_remaining_percent) ?? fiveHourPercent;
52
+ }
53
+ const fiveHour = normalizeWindow("five_hour", {
54
+ used_percent: fiveHourPercent,
55
+ resets_at: new Date(fiveHourReset).toISOString(),
56
+ }, 300);
57
+ const sevenDay = normalizeWindow("seven_day", {
58
+ used_percent: weeklyPercent,
59
+ resets_at: new Date(weeklyReset).toISOString(),
60
+ }, 10080);
61
+ if (!fiveHour || !sevenDay) {
62
+ return null;
63
+ }
64
+ return makeProviderUsage({
65
+ provider: "minimax",
66
+ source: options.source,
67
+ observedAt: options.observedAt,
68
+ planType: primary.model_name ?? null,
69
+ fiveHour,
70
+ sevenDay,
71
+ });
72
+ }
73
+ // MiniMax reports usage two ways depending on plan type. Count-based accounts
74
+ // (e.g. `video`) populate current_*_usage_count / current_*_total_count.
75
+ // Credit-based accounts (e.g. `general`) keep total at 0 and expose
76
+ // current_*_remaining_percent in 0-100. Prefer the count ratio when total > 0,
77
+ // otherwise derive from the remaining percent, and only fall back to 0 when
78
+ // neither signal is present.
79
+ function percentFromQuota(totalCount, usageCount, remainingPercent) {
80
+ if (typeof totalCount === "number" && totalCount > 0 && typeof usageCount === "number") {
81
+ return Math.max(0, Math.min(100, (usageCount / totalCount) * 100));
82
+ }
83
+ if (typeof remainingPercent === "number" && Number.isFinite(remainingPercent)) {
84
+ return Math.max(0, Math.min(100, 100 - remainingPercent));
85
+ }
86
+ return 0;
87
+ }
88
+ export async function collectMinimaxUsage(config) {
89
+ if (!config.apiKey) {
90
+ throw new Error("MiniMax API key not configured. Edit ~/.coding-usage-bar/config.json to set minimax.apiKey.");
91
+ }
92
+ const quota = await fetchMinimaxQuota(config);
93
+ const usage = usageFromMinimaxQuota(quota, {
94
+ source: `${baseUrl(config)}/v1/token_plan/remains`,
95
+ });
96
+ if (!usage) {
97
+ throw new Error("MiniMax quota response did not contain expected token plan data");
98
+ }
99
+ return usage;
100
+ }
@@ -0,0 +1 @@
1
+ export declare function stableNodeExecutable(): string;
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs";
2
+ import { execFileSync } from "node:child_process";
3
+ function executable(file) {
4
+ try {
5
+ fs.accessSync(file, fs.constants.X_OK);
6
+ return fs.statSync(file).isFile();
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ }
12
+ export function stableNodeExecutable() {
13
+ const override = process.env.CODING_USAGE_BAR_NODE?.trim();
14
+ if (override) {
15
+ if (!override.startsWith("/") || !executable(override)) {
16
+ throw new Error(`CODING_USAGE_BAR_NODE must be an executable absolute path: ${override}`);
17
+ }
18
+ return override;
19
+ }
20
+ let pathNode = "";
21
+ try {
22
+ pathNode = execFileSync("/usr/bin/which", ["node"], { encoding: "utf8" }).trim();
23
+ }
24
+ catch {
25
+ // Fall through to known stable links and the current executable.
26
+ }
27
+ return [pathNode, "/opt/homebrew/bin/node", "/usr/local/bin/node", process.execPath]
28
+ .find((candidate) => candidate && executable(candidate)) ?? process.execPath;
29
+ }
@@ -0,0 +1,7 @@
1
+ import { BurnAnalysis, RuntimePaths } from "./types.js";
2
+ export declare function commandExists(command: string): boolean;
3
+ export declare function notificationBackend(): "osascript" | "terminal-notifier" | "burnt-toast" | "notify-send" | "unsupported";
4
+ export declare function shouldNotify(analysis: BurnAnalysis, paths?: RuntimePaths, now?: Date): boolean;
5
+ export declare function markNotified(analysis: BurnAnalysis, paths?: RuntimePaths, now?: Date): void;
6
+ export declare function iconForAnalysis(analysis: BurnAnalysis): string;
7
+ export declare function sendNotification(analysis: BurnAnalysis, dryRun?: boolean): string;
@@ -0,0 +1,121 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import os from "node:os";
4
+ import { pathToFileURL } from "node:url";
5
+ import { readJsonFile, writeJsonAtomic } from "./fs-util.js";
6
+ import { buildPaths, installedAssetPath } from "./paths.js";
7
+ import { formatDurationUntil } from "./format.js";
8
+ import { writeNotificationCard } from "./card.js";
9
+ export function commandExists(command) {
10
+ const result = spawnSync("/bin/sh", ["-c", `command -v ${command}`], {
11
+ stdio: "ignore",
12
+ });
13
+ return result.status === 0;
14
+ }
15
+ export function notificationBackend() {
16
+ if (process.platform === "darwin") {
17
+ return commandExists("terminal-notifier") ? "terminal-notifier" : "osascript";
18
+ }
19
+ if (process.platform === "win32") {
20
+ return "burnt-toast";
21
+ }
22
+ return commandExists("notify-send") ? "notify-send" : "unsupported";
23
+ }
24
+ function cooldownMinutes(profile) {
25
+ return profile === "high" ? 15 : 30;
26
+ }
27
+ function notificationKey(analysis) {
28
+ return `${analysis.provider}:${analysis.state}`;
29
+ }
30
+ export function shouldNotify(analysis, paths = buildPaths(), now = new Date()) {
31
+ if (analysis.state === "RAW" || analysis.state === "ON_TRACK") {
32
+ return false;
33
+ }
34
+ const state = readJsonFile(paths.notificationStateFile) ?? {};
35
+ const key = notificationKey(analysis);
36
+ const previous = state[key];
37
+ const resetAt = analysis.fiveHour?.resetsAt;
38
+ if (!previous) {
39
+ return true;
40
+ }
41
+ if (previous.resetAt && resetAt && previous.resetAt !== resetAt) {
42
+ return true;
43
+ }
44
+ const elapsedMinutes = (now.getTime() - Date.parse(previous.notifiedAt)) / 60_000;
45
+ return elapsedMinutes >= cooldownMinutes(analysis.profile);
46
+ }
47
+ export function markNotified(analysis, paths = buildPaths(), now = new Date()) {
48
+ const state = readJsonFile(paths.notificationStateFile) ?? {};
49
+ state[notificationKey(analysis)] = {
50
+ notifiedAt: now.toISOString(),
51
+ resetAt: analysis.fiveHour?.resetsAt,
52
+ };
53
+ writeJsonAtomic(paths.notificationStateFile, state);
54
+ }
55
+ function notificationText(analysis) {
56
+ const provider = analysis.provider === "claude" ? "Claude" : analysis.provider === "glm" ? "GLM" : "Codex";
57
+ const suffix = analysis.fiveHour ? `剩 ${formatDurationUntil(analysis.fiveHour.resetsAt)} 重置。` : "";
58
+ if (analysis.state === "UNDER_BURN") {
59
+ return {
60
+ title: `Coding Usage Bar: ${provider} 节奏偏慢`,
61
+ message: `${analysis.message} ${suffix}`.trim(),
62
+ };
63
+ }
64
+ if (analysis.state === "OVER_BURN") {
65
+ return {
66
+ title: `Coding Usage Bar: ${provider} 节奏过快`,
67
+ message: `${analysis.message} ${suffix}`.trim(),
68
+ };
69
+ }
70
+ return {
71
+ title: `Coding Usage Bar: ${provider} 接近限额`,
72
+ message: `${analysis.message} ${suffix}`.trim(),
73
+ };
74
+ }
75
+ export function iconForAnalysis(analysis) {
76
+ if (analysis.state === "UNDER_BURN") {
77
+ return installedAssetPath(os.homedir(), "coding-usage-bar-under.png");
78
+ }
79
+ if (analysis.state === "OVER_BURN") {
80
+ return installedAssetPath(os.homedir(), "coding-usage-bar-over.png");
81
+ }
82
+ if (analysis.state === "LIMIT_RISK") {
83
+ return installedAssetPath(os.homedir(), "coding-usage-bar-limit.png");
84
+ }
85
+ return installedAssetPath(os.homedir(), "coding-usage-bar-ok.png");
86
+ }
87
+ function iconUrl(iconPath) {
88
+ return pathToFileURL(iconPath).toString();
89
+ }
90
+ export function sendNotification(analysis, dryRun = false) {
91
+ const backend = notificationBackend();
92
+ const { title, message } = notificationText(analysis);
93
+ const appIcon = iconForAnalysis(analysis);
94
+ if (dryRun) {
95
+ return `[dry-run] ${backend}: dynamic-card ${title} - ${message}`;
96
+ }
97
+ if (backend === "terminal-notifier") {
98
+ const args = ["-title", title, "-message", message, "-group", `coding-usage-bar.${analysis.provider}`];
99
+ const cardPath = writeNotificationCard(analysis);
100
+ if (existsSync(cardPath)) {
101
+ args.push("-contentImage", iconUrl(cardPath));
102
+ }
103
+ if (existsSync(appIcon)) {
104
+ args.push("-appIcon", iconUrl(appIcon));
105
+ }
106
+ execFileSync("terminal-notifier", args);
107
+ return `${backend}: ${title}`;
108
+ }
109
+ if (backend === "osascript") {
110
+ execFileSync("osascript", ["-e", `display notification ${JSON.stringify(message)} with title ${JSON.stringify(title)}`]);
111
+ return `${backend}: ${title}`;
112
+ }
113
+ if (backend === "notify-send") {
114
+ execFileSync("notify-send", [title, message]);
115
+ return `${backend}: ${title}`;
116
+ }
117
+ if (os.platform() === "win32") {
118
+ throw new Error("Windows notification unavailable: install PowerShell module BurntToast.");
119
+ }
120
+ throw new Error("Desktop notification unavailable on this platform.");
121
+ }
@@ -0,0 +1,5 @@
1
+ import { RuntimePaths, ProviderId } from "./types.js";
2
+ export declare function buildPaths(homeDir?: string): RuntimePaths;
3
+ export declare function installedAssetPath(homeDir: string, assetName: string): string;
4
+ export declare function providerLatestPath(paths: RuntimePaths, provider: ProviderId): string;
5
+ export declare function providerSamplesPath(paths: RuntimePaths, provider: ProviderId): string;
package/dist/paths.js ADDED
@@ -0,0 +1,45 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ export function buildPaths(homeDir = os.homedir()) {
4
+ const stateDir = path.join(homeDir, ".coding-usage-bar");
5
+ return {
6
+ homeDir,
7
+ stateDir,
8
+ configFile: path.join(stateDir, "config.json"),
9
+ claudeDir: path.join(stateDir, "claude"),
10
+ codexDir: path.join(stateDir, "codex"),
11
+ glmDir: path.join(stateDir, "glm"),
12
+ deepseekDir: path.join(stateDir, "deepseek"),
13
+ minimaxDir: path.join(stateDir, "minimax"),
14
+ notificationStateFile: path.join(stateDir, "notifications.json"),
15
+ statusFile: path.join(stateDir, "status.json"),
16
+ starPromptFile: path.join(stateDir, "star-prompt.json"),
17
+ cliBinDir: path.join(homeDir, ".local", "bin"),
18
+ cliBinFile: path.join(homeDir, ".local", "bin", "coding-usage-bar"),
19
+ swiftBarPluginDir: path.join(homeDir, "Library", "Application Support", "SwiftBar", "Plugins"),
20
+ swiftBarPluginFile: path.join(homeDir, "Library", "Application Support", "SwiftBar", "Plugins", "coding-usage-bar.1m.js"),
21
+ launchAgentFile: path.join(homeDir, "Library", "LaunchAgents", "com.duying.coding-usage-bar.plist"),
22
+ claudeSettingsFile: path.join(homeDir, ".claude", "settings.json"),
23
+ claudeStatusLineScript: path.join(stateDir, "claude", "statusline.sh"),
24
+ };
25
+ }
26
+ export function installedAssetPath(homeDir, assetName) {
27
+ return path.join(homeDir, ".coding-usage-bar", "app", "assets", assetName);
28
+ }
29
+ function providerDir(paths, provider) {
30
+ if (provider === "glm")
31
+ return paths.glmDir;
32
+ if (provider === "deepseek")
33
+ return paths.deepseekDir;
34
+ if (provider === "minimax")
35
+ return paths.minimaxDir;
36
+ if (provider === "claude")
37
+ return paths.claudeDir;
38
+ return paths.codexDir;
39
+ }
40
+ export function providerLatestPath(paths, provider) {
41
+ return path.join(providerDir(paths, provider), "latest.json");
42
+ }
43
+ export function providerSamplesPath(paths, provider) {
44
+ return path.join(providerDir(paths, provider), "samples.jsonl");
45
+ }
@@ -0,0 +1,23 @@
1
+ import { BurnAnalysis, BurnProfile, ProviderUsage, RuntimePaths, StatusIssue, StatusSnapshot } from "./types.js";
2
+ export declare function loadFixtureUsages(fixturesDir: string): ProviderUsage[];
3
+ export declare function collectLocalState(options?: {
4
+ fixtures?: boolean;
5
+ }): Promise<{
6
+ usages: ProviderUsage[];
7
+ issues: StatusIssue[];
8
+ }>;
9
+ export declare function collectLocalUsages(options?: {
10
+ fixtures?: boolean;
11
+ }): Promise<ProviderUsage[]>;
12
+ export declare function analyzeUsages(usages: ProviderUsage[], profile: BurnProfile): BurnAnalysis[];
13
+ export declare function readProfile(): BurnProfile;
14
+ export declare function loadFixtureSamples(provider: string): ProviderUsage[];
15
+ export declare function analyzeFixtureUsages(usages: ProviderUsage[], profile: BurnProfile): BurnAnalysis[];
16
+ export declare function collectStatusSnapshot(options?: {
17
+ fixtures?: boolean;
18
+ }): Promise<StatusSnapshot>;
19
+ export declare function loadDisplayStatusSnapshot(options?: {
20
+ fixtures?: boolean;
21
+ refresh?: boolean;
22
+ paths?: RuntimePaths;
23
+ }): StatusSnapshot;
@@ -0,0 +1,244 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { readConfig } from "./config.js";
5
+ import { collectCodexUsage } from "./codex.js";
6
+ import { readJsonFile } from "./fs-util.js";
7
+ import { collectGlmUsage } from "./glm.js";
8
+ import { collectDeepseekUsage } from "./deepseek.js";
9
+ import { collectMinimaxUsage } from "./minimax.js";
10
+ import { buildPaths } from "./paths.js";
11
+ import { loadLatestUsage, saveUsage } from "./store.js";
12
+ import { createStatusSnapshot, loadStatusSnapshot, refreshStatusSnapshotFreshness, saveStatusSnapshot, } from "./status.js";
13
+ const FIXTURES_DIR = fileURLToPath(new URL("../fixtures", import.meta.url));
14
+ export function loadFixtureUsages(fixturesDir) {
15
+ const providers = ["claude", "codex", "glm", "deepseek", "minimax"];
16
+ return providers
17
+ .map((provider) => readJsonFile(path.join(fixturesDir, provider, "latest.json")))
18
+ .filter((item) => item !== null);
19
+ }
20
+ export async function collectLocalState(options = {}) {
21
+ if (options.fixtures) {
22
+ return { usages: loadFixtureUsages(FIXTURES_DIR), issues: [] };
23
+ }
24
+ const paths = buildPaths();
25
+ const config = readConfig(paths);
26
+ const monitored = new Set(config.providers);
27
+ const usages = [];
28
+ const issues = [];
29
+ if (monitored.has("codex")) {
30
+ try {
31
+ const codex = collectCodexUsage(path.join(paths.homeDir, ".codex"));
32
+ saveUsage(codex, paths);
33
+ usages.push(codex);
34
+ }
35
+ catch (error) {
36
+ const cached = loadLatestUsage("codex", paths);
37
+ if (cached) {
38
+ usages.push(cached);
39
+ issues.push({
40
+ provider: "codex",
41
+ severity: "warning",
42
+ code: "CODEX_USING_CACHE",
43
+ message: `Codex live usage unavailable; using cached usage from ${cached.observedAt}. ${error instanceof Error ? error.message : String(error)}`,
44
+ });
45
+ }
46
+ else {
47
+ issues.push({
48
+ provider: "codex",
49
+ severity: "error",
50
+ code: "CODEX_USAGE_MISSING",
51
+ message: error instanceof Error ? error.message : String(error),
52
+ });
53
+ }
54
+ }
55
+ }
56
+ if (monitored.has("claude")) {
57
+ const claude = loadLatestUsage("claude", paths);
58
+ if (claude) {
59
+ usages.push(claude);
60
+ }
61
+ else {
62
+ issues.push({
63
+ provider: "claude",
64
+ severity: "warning",
65
+ code: "CLAUDE_INGEST_MISSING",
66
+ message: "Claude usage missing; add Coding Usage Bar ingest to Claude Code statusLine.command or remove claude from ~/.coding-usage-bar/config.json providers.",
67
+ });
68
+ }
69
+ }
70
+ if (monitored.has("glm")) {
71
+ const glmConfig = config.glm;
72
+ if (!glmConfig?.apiKey) {
73
+ issues.push({
74
+ provider: "glm",
75
+ severity: "warning",
76
+ code: "GLM_API_KEY_MISSING",
77
+ message: "GLM API key not configured. Edit ~/.coding-usage-bar/config.json to set glm.apiKey.",
78
+ });
79
+ }
80
+ else {
81
+ try {
82
+ const glm = await collectGlmUsage(glmConfig);
83
+ saveUsage(glm, paths);
84
+ usages.push(glm);
85
+ }
86
+ catch (error) {
87
+ const cached = loadLatestUsage("glm", paths);
88
+ if (cached) {
89
+ usages.push(cached);
90
+ issues.push({
91
+ provider: "glm",
92
+ severity: "warning",
93
+ code: "GLM_USING_CACHE",
94
+ message: `GLM live usage unavailable; using cached usage from ${cached.observedAt}. ${error instanceof Error ? error.message : String(error)}`,
95
+ });
96
+ }
97
+ else {
98
+ issues.push({
99
+ provider: "glm",
100
+ severity: "error",
101
+ code: "GLM_USAGE_MISSING",
102
+ message: error instanceof Error ? error.message : String(error),
103
+ });
104
+ }
105
+ }
106
+ }
107
+ }
108
+ if (monitored.has("deepseek")) {
109
+ const deepseekConfig = config.deepseek;
110
+ if (!deepseekConfig?.apiKey) {
111
+ issues.push({
112
+ provider: "deepseek",
113
+ severity: "warning",
114
+ code: "DEEPSEEK_API_KEY_MISSING",
115
+ message: "DeepSeek API key not configured. Edit ~/.coding-usage-bar/config.json to set deepseek.apiKey.",
116
+ });
117
+ }
118
+ else {
119
+ try {
120
+ const deepseek = await collectDeepseekUsage(deepseekConfig);
121
+ saveUsage(deepseek, paths);
122
+ usages.push(deepseek);
123
+ }
124
+ catch (error) {
125
+ const cached = loadLatestUsage("deepseek", paths);
126
+ if (cached) {
127
+ usages.push(cached);
128
+ issues.push({
129
+ provider: "deepseek",
130
+ severity: "warning",
131
+ code: "DEEPSEEK_USING_CACHE",
132
+ message: `DeepSeek live balance unavailable; using cached data from ${cached.observedAt}. ${error instanceof Error ? error.message : String(error)}`,
133
+ });
134
+ }
135
+ else {
136
+ issues.push({
137
+ provider: "deepseek",
138
+ severity: "error",
139
+ code: "DEEPSEEK_USAGE_MISSING",
140
+ message: error instanceof Error ? error.message : String(error),
141
+ });
142
+ }
143
+ }
144
+ }
145
+ }
146
+ if (monitored.has("minimax")) {
147
+ const minimaxConfig = config.minimax;
148
+ if (!minimaxConfig?.apiKey) {
149
+ issues.push({
150
+ provider: "minimax",
151
+ severity: "warning",
152
+ code: "MINIMAX_API_KEY_MISSING",
153
+ message: "MiniMax API key not configured. Edit ~/.coding-usage-bar/config.json to set minimax.apiKey.",
154
+ });
155
+ }
156
+ else {
157
+ try {
158
+ const minimax = await collectMinimaxUsage(minimaxConfig);
159
+ saveUsage(minimax, paths);
160
+ usages.push(minimax);
161
+ }
162
+ catch (error) {
163
+ const cached = loadLatestUsage("minimax", paths);
164
+ if (cached) {
165
+ usages.push(cached);
166
+ issues.push({
167
+ provider: "minimax",
168
+ severity: "warning",
169
+ code: "MINIMAX_USING_CACHE",
170
+ message: `MiniMax live usage unavailable; using cached usage from ${cached.observedAt}. ${error instanceof Error ? error.message : String(error)}`,
171
+ });
172
+ }
173
+ else {
174
+ issues.push({
175
+ provider: "minimax",
176
+ severity: "error",
177
+ code: "MINIMAX_USAGE_MISSING",
178
+ message: error instanceof Error ? error.message : String(error),
179
+ });
180
+ }
181
+ }
182
+ }
183
+ }
184
+ return { usages, issues };
185
+ }
186
+ export async function collectLocalUsages(options = {}) {
187
+ return (await collectLocalState(options)).usages;
188
+ }
189
+ export function analyzeUsages(usages, profile) {
190
+ const paths = buildPaths();
191
+ return createStatusSnapshot(usages, profile, { paths }).providers.map((provider) => provider.analysis);
192
+ }
193
+ export function readProfile() {
194
+ const profile = process.env.CODING_USAGE_BAR_PROFILE;
195
+ return profile === "high" ? "high" : "low";
196
+ }
197
+ export function loadFixtureSamples(provider) {
198
+ const file = path.join(FIXTURES_DIR, provider, "samples.jsonl");
199
+ if (!fs.existsSync(file)) {
200
+ return [];
201
+ }
202
+ return fs
203
+ .readFileSync(file, "utf8")
204
+ .split(/\r?\n/)
205
+ .filter(Boolean)
206
+ .map((line) => JSON.parse(line));
207
+ }
208
+ export function analyzeFixtureUsages(usages, profile) {
209
+ const fixtureSamples = new Map(usages.map((usage) => [usage.provider, loadFixtureSamples(usage.provider)]));
210
+ return createStatusSnapshot(usages, profile, { fixtureSamples }).providers.map((provider) => provider.analysis);
211
+ }
212
+ export async function collectStatusSnapshot(options = {}) {
213
+ const { usages, issues } = await collectLocalState(options);
214
+ const profile = readProfile();
215
+ const fixtureSamples = options.fixtures
216
+ ? new Map(usages.map((usage) => [usage.provider, loadFixtureSamples(usage.provider)]))
217
+ : undefined;
218
+ const snapshot = createStatusSnapshot(usages, profile, { fixtureSamples, issues });
219
+ if (!options.fixtures) {
220
+ saveStatusSnapshot(snapshot);
221
+ }
222
+ return snapshot;
223
+ }
224
+ export function loadDisplayStatusSnapshot(options = {}) {
225
+ if (options.fixtures || options.refresh) {
226
+ throw new Error("Use collectStatusSnapshot() for async refresh; loadDisplayStatusSnapshot is sync (file-only)");
227
+ }
228
+ const snapshot = loadStatusSnapshot(options.paths);
229
+ if (snapshot) {
230
+ return refreshStatusSnapshotFreshness(snapshot);
231
+ }
232
+ return {
233
+ generatedAt: new Date().toISOString(),
234
+ profile: readProfile(),
235
+ providers: [],
236
+ issues: [
237
+ {
238
+ severity: "warning",
239
+ code: "STATUS_MISSING",
240
+ message: `No Coding Usage Bar status snapshot found at ${options.paths?.statusFile ?? buildPaths().statusFile}. Run coding-usage-bar daemon --once or coding-usage-bar status --refresh.`,
241
+ },
242
+ ],
243
+ };
244
+ }
package/dist/star.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { RuntimePaths } from "./types.js";
2
+ interface StarPromptOptions {
3
+ dryRun: boolean;
4
+ isInteractive?: boolean;
5
+ env?: Record<string, string | undefined>;
6
+ canStarWithGh?: boolean | (() => boolean);
7
+ confirmStar?: (prompt: string) => boolean;
8
+ starRepo?: (repo: string) => void;
9
+ now?: () => Date;
10
+ }
11
+ export declare function isStarConsent(answer: string): boolean;
12
+ export declare function ghStarArgs(repo: string): string[];
13
+ export declare function ghAuthStatusArgs(): string[];
14
+ export declare function maybePromptForStar(paths: RuntimePaths, options: StarPromptOptions): string[];
15
+ export {};