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.
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/README.zh-CN.md +53 -0
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/coding-usage-bar-limit.png +0 -0
- package/assets/coding-usage-bar-ok.png +0 -0
- package/assets/coding-usage-bar-over.png +0 -0
- package/assets/coding-usage-bar-under.png +0 -0
- package/assets/provider-claude.png +0 -0
- package/assets/provider-codex.png +0 -0
- package/assets/provider-deepseek.png +0 -0
- package/assets/provider-glm-dark.png +0 -0
- package/assets/provider-glm.png +0 -0
- package/assets/provider-marks.png +0 -0
- package/assets/provider-minimax.png +0 -0
- package/dist/burn.d.ts +3 -0
- package/dist/burn.js +164 -0
- package/dist/card.d.ts +4 -0
- package/dist/card.js +79 -0
- package/dist/claude.d.ts +9 -0
- package/dist/claude.js +141 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +129 -0
- package/dist/codex.d.ts +6 -0
- package/dist/codex.js +128 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +64 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +16 -0
- package/dist/deepseek.d.ts +18 -0
- package/dist/deepseek.js +47 -0
- package/dist/doctor.d.ts +6 -0
- package/dist/doctor.js +225 -0
- package/dist/format.d.ts +9 -0
- package/dist/format.js +92 -0
- package/dist/fs-util.d.ts +6 -0
- package/dist/fs-util.js +39 -0
- package/dist/glm.d.ts +22 -0
- package/dist/glm.js +62 -0
- package/dist/install.d.ts +25 -0
- package/dist/install.js +432 -0
- package/dist/menubar.d.ts +23 -0
- package/dist/menubar.js +880 -0
- package/dist/minimax.d.ts +47 -0
- package/dist/minimax.js +100 -0
- package/dist/node-runtime.d.ts +1 -0
- package/dist/node-runtime.js +29 -0
- package/dist/notifier.d.ts +7 -0
- package/dist/notifier.js +121 -0
- package/dist/paths.d.ts +5 -0
- package/dist/paths.js +45 -0
- package/dist/runtime.d.ts +23 -0
- package/dist/runtime.js +244 -0
- package/dist/star.d.ts +15 -0
- package/dist/star.js +117 -0
- package/dist/status.d.ts +14 -0
- package/dist/status.js +65 -0
- package/dist/store.d.ts +4 -0
- package/dist/store.js +29 -0
- package/dist/types.d.ts +101 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +12 -0
- package/dist/usage.js +56 -0
- package/docs/assets/hero.png +0 -0
- package/fixtures/claude/latest.json +20 -0
- package/fixtures/claude/samples.jsonl +3 -0
- package/fixtures/codex/latest.json +20 -0
- package/fixtures/codex/samples.jsonl +3 -0
- package/fixtures/deepseek/latest.json +12 -0
- package/package.json +59 -0
package/dist/claude.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { makeProviderUsage, normalizeWindow } from "./usage.js";
|
|
4
|
+
export function usageFromClaudeStatusLine(input) {
|
|
5
|
+
if (input === null || typeof input !== "object") {
|
|
6
|
+
throw new Error("Claude status line input must be a JSON object");
|
|
7
|
+
}
|
|
8
|
+
const record = input;
|
|
9
|
+
const rateLimits = record.rate_limits;
|
|
10
|
+
if (!rateLimits || typeof rateLimits !== "object") {
|
|
11
|
+
throw new Error("Claude usage unavailable: status line input has no rate_limits field");
|
|
12
|
+
}
|
|
13
|
+
const fiveHour = normalizeWindow("five_hour", rateLimits.five_hour, 300);
|
|
14
|
+
const sevenDay = normalizeWindow("seven_day", rateLimits.seven_day, 10080);
|
|
15
|
+
if (!fiveHour || !sevenDay) {
|
|
16
|
+
throw new Error("Claude usage unavailable: missing five_hour or seven_day rate limit window");
|
|
17
|
+
}
|
|
18
|
+
return makeProviderUsage({
|
|
19
|
+
provider: "claude",
|
|
20
|
+
source: "claude_statusline_stdin",
|
|
21
|
+
fiveHour,
|
|
22
|
+
sevenDay,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function readStdin() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let data = "";
|
|
28
|
+
process.stdin.setEncoding("utf8");
|
|
29
|
+
process.stdin.on("data", (chunk) => {
|
|
30
|
+
data += chunk;
|
|
31
|
+
});
|
|
32
|
+
process.stdin.on("end", () => resolve(data));
|
|
33
|
+
process.stdin.on("error", reject);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function readClaudeSettings(file) {
|
|
37
|
+
let contents;
|
|
38
|
+
try {
|
|
39
|
+
contents = fs.readFileSync(file, "utf8");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code === "ENOENT") {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`Cannot read Claude settings at ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(contents);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw new Error(`Invalid JSON in Claude settings at ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export function getClaudeStatusLineCommand(settings) {
|
|
55
|
+
const statusLine = settings?.statusLine;
|
|
56
|
+
if (statusLine && typeof statusLine === "object") {
|
|
57
|
+
const command = statusLine.command;
|
|
58
|
+
return typeof command === "string" ? command : null;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function splitShellWords(command) {
|
|
63
|
+
const words = [];
|
|
64
|
+
let current = "";
|
|
65
|
+
let quote = null;
|
|
66
|
+
let escaped = false;
|
|
67
|
+
for (const char of command) {
|
|
68
|
+
if (escaped) {
|
|
69
|
+
current += char;
|
|
70
|
+
escaped = false;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (char === "\\" && quote !== "'") {
|
|
74
|
+
escaped = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if ((char === "'" || char === "\"") && !quote) {
|
|
78
|
+
quote = char;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (char === quote) {
|
|
82
|
+
quote = null;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (/\s/.test(char) && !quote) {
|
|
86
|
+
if (current) {
|
|
87
|
+
words.push(current);
|
|
88
|
+
current = "";
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
current += char;
|
|
93
|
+
}
|
|
94
|
+
if (current) {
|
|
95
|
+
words.push(current);
|
|
96
|
+
}
|
|
97
|
+
return words;
|
|
98
|
+
}
|
|
99
|
+
function statusLineScriptCandidates(command) {
|
|
100
|
+
const words = splitShellWords(command);
|
|
101
|
+
const candidates = [command];
|
|
102
|
+
const interpreter = path.basename(words[0] ?? "");
|
|
103
|
+
if (["bash", "sh", "zsh", "node"].includes(interpreter)) {
|
|
104
|
+
for (const word of words.slice(1)) {
|
|
105
|
+
if (word === "-c") {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (word.startsWith("-")) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
candidates.push(word);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...new Set(candidates)];
|
|
116
|
+
}
|
|
117
|
+
export function claudeStatusLineHasIngest(command, options) {
|
|
118
|
+
if (!command) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (command.includes("coding-usage-bar ingest claude-statusline")
|
|
122
|
+
|| command.includes(options.appCliPath)
|
|
123
|
+
|| command === options.managedScriptPath) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
for (const candidate of statusLineScriptCandidates(command)) {
|
|
127
|
+
try {
|
|
128
|
+
if (!fs.statSync(candidate).isFile()) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const script = fs.readFileSync(candidate, "utf8");
|
|
132
|
+
if (script.includes("coding-usage-bar ingest claude-statusline") || script.includes(options.appCliPath)) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Ignore command strings that are not readable script paths.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { usageFromClaudeStatusLine, readStdin } from "./claude.js";
|
|
3
|
+
import { install, uninstall } from "./install.js";
|
|
4
|
+
import { saveUsage } from "./store.js";
|
|
5
|
+
import { formatStatusRows, formatAnalysisDetail, formatIssues, formatProviderMeta } from "./format.js";
|
|
6
|
+
import { loadDisplayStatusSnapshot, collectStatusSnapshot } from "./runtime.js";
|
|
7
|
+
import { runDaemonOnce } from "./daemon.js";
|
|
8
|
+
import { doctorHasFailures, formatDoctor, runDoctor } from "./doctor.js";
|
|
9
|
+
import { installMenuBar, renderMenuBar, uninstallMenuBar, toggleCompactMode } from "./menubar.js";
|
|
10
|
+
import { buildPaths } from "./paths.js";
|
|
11
|
+
import { maybePromptForStar } from "./star.js";
|
|
12
|
+
function hasFlag(args, flag) {
|
|
13
|
+
return args.includes(flag);
|
|
14
|
+
}
|
|
15
|
+
function printHelp() {
|
|
16
|
+
console.log(`Coding Usage Bar
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
coding-usage-bar install [--dry-run]
|
|
20
|
+
coding-usage-bar uninstall [--dry-run]
|
|
21
|
+
coding-usage-bar doctor [--dry-run]
|
|
22
|
+
coding-usage-bar status [--fixtures] [--json] [--refresh]
|
|
23
|
+
coding-usage-bar menubar render
|
|
24
|
+
coding-usage-bar menubar install [--dry-run]
|
|
25
|
+
coding-usage-bar menubar uninstall [--dry-run]
|
|
26
|
+
coding-usage-bar menubar toggle-compact
|
|
27
|
+
coding-usage-bar ingest claude-statusline
|
|
28
|
+
coding-usage-bar daemon [--once] [--dry-run]
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
async function main() {
|
|
32
|
+
const args = process.argv.slice(2);
|
|
33
|
+
const command = args[0];
|
|
34
|
+
const subcommand = args[1];
|
|
35
|
+
const dryRun = hasFlag(args, "--dry-run");
|
|
36
|
+
try {
|
|
37
|
+
if (!command || command === "-h" || command === "--help") {
|
|
38
|
+
printHelp();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (command === "install") {
|
|
42
|
+
const messages = install({ dryRun });
|
|
43
|
+
console.log(messages.join("\n"));
|
|
44
|
+
if (messages.some((message) => message.startsWith("Installed and restarted launchd agent:"))) {
|
|
45
|
+
const starMessages = maybePromptForStar(buildPaths(), { dryRun, env: process.env });
|
|
46
|
+
if (starMessages.length > 0) {
|
|
47
|
+
console.log(starMessages.join("\n"));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (command === "uninstall") {
|
|
53
|
+
console.log(uninstall({ dryRun }).join("\n"));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (command === "doctor") {
|
|
57
|
+
const checks = runDoctor({ dryRun });
|
|
58
|
+
console.log(formatDoctor(checks));
|
|
59
|
+
if (doctorHasFailures(checks)) {
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (command === "status") {
|
|
65
|
+
const fixtures = hasFlag(args, "--fixtures");
|
|
66
|
+
const json = hasFlag(args, "--json");
|
|
67
|
+
const refresh = hasFlag(args, "--refresh");
|
|
68
|
+
const snapshot = refresh || fixtures
|
|
69
|
+
? await collectStatusSnapshot({ fixtures })
|
|
70
|
+
: loadDisplayStatusSnapshot();
|
|
71
|
+
if (json) {
|
|
72
|
+
console.log(JSON.stringify(snapshot, null, 2));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const usages = snapshot.providers.map((provider) => provider.usage);
|
|
76
|
+
const analyses = snapshot.providers.map((provider) => provider.analysis);
|
|
77
|
+
console.log(formatStatusRows(usages, analyses));
|
|
78
|
+
console.log("");
|
|
79
|
+
for (const analysis of analyses) {
|
|
80
|
+
console.log(formatAnalysisDetail(analysis));
|
|
81
|
+
}
|
|
82
|
+
const meta = formatProviderMeta(snapshot);
|
|
83
|
+
if (meta) {
|
|
84
|
+
console.log("");
|
|
85
|
+
console.log(meta);
|
|
86
|
+
}
|
|
87
|
+
const issues = formatIssues(snapshot.issues);
|
|
88
|
+
if (issues) {
|
|
89
|
+
console.log("");
|
|
90
|
+
console.log(issues);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (command === "menubar" && subcommand === "render") {
|
|
95
|
+
console.log(renderMenuBar());
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (command === "menubar" && subcommand === "install") {
|
|
99
|
+
console.log(installMenuBar({ dryRun }).join("\n"));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (command === "menubar" && subcommand === "uninstall") {
|
|
103
|
+
console.log(uninstallMenuBar({ dryRun }).join("\n"));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (command === "menubar" && subcommand === "toggle-compact") {
|
|
107
|
+
const isCompact = toggleCompactMode();
|
|
108
|
+
console.log(isCompact ? "Compact" : "Expanded");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (command === "ingest" && subcommand === "claude-statusline") {
|
|
112
|
+
const input = await readStdin();
|
|
113
|
+
const parsed = JSON.parse(input);
|
|
114
|
+
const usage = usageFromClaudeStatusLine(parsed);
|
|
115
|
+
saveUsage(usage);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (command === "daemon") {
|
|
119
|
+
console.log((await runDaemonOnce({ dryRun })).join("\n"));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`Unknown command: ${args.join(" ")}`);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
await main();
|
package/dist/codex.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ProviderUsage } from "./types.js";
|
|
2
|
+
export declare function usageFromCodexRateLimits(rateLimits: unknown, options: {
|
|
3
|
+
observedAt?: string;
|
|
4
|
+
source: string;
|
|
5
|
+
}): ProviderUsage | null;
|
|
6
|
+
export declare function collectCodexUsage(codexHome?: string): ProviderUsage;
|
package/dist/codex.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isDir } from "./fs-util.js";
|
|
4
|
+
import { normalizeWindow } from "./usage.js";
|
|
5
|
+
function walkJsonlFiles(dir, result = []) {
|
|
6
|
+
if (!isDir(dir)) {
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
10
|
+
const fullPath = path.join(dir, entry.name);
|
|
11
|
+
if (entry.isDirectory()) {
|
|
12
|
+
if (entry.name === "node_modules" || entry.name === ".git") {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
walkJsonlFiles(fullPath, result);
|
|
16
|
+
}
|
|
17
|
+
else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
18
|
+
result.push({ file: fullPath, mtimeMs: fs.statSync(fullPath).mtimeMs });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
function codexSessionRoots(codexHome) {
|
|
24
|
+
return ["sessions", "archived_sessions"]
|
|
25
|
+
.map((name) => path.join(codexHome, name))
|
|
26
|
+
.filter(isDir);
|
|
27
|
+
}
|
|
28
|
+
function parseObservedMs(raw, fallbackMs) {
|
|
29
|
+
if (typeof raw === "string") {
|
|
30
|
+
const parsed = Date.parse(raw);
|
|
31
|
+
if (Number.isFinite(parsed)) {
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
36
|
+
return raw > 10_000_000_000 ? raw : raw * 1000;
|
|
37
|
+
}
|
|
38
|
+
return fallbackMs;
|
|
39
|
+
}
|
|
40
|
+
export function usageFromCodexRateLimits(rateLimits, options) {
|
|
41
|
+
if (rateLimits === null || typeof rateLimits !== "object") {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const record = rateLimits;
|
|
45
|
+
if (typeof record.limit_name === "string") {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const windows = [record.primary, record.secondary]
|
|
49
|
+
.map((raw) => {
|
|
50
|
+
if (raw === null || typeof raw !== "object") {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const rawRecord = raw;
|
|
54
|
+
const rawWindowMinutes = rawRecord.window_minutes ?? rawRecord.windowMinutes;
|
|
55
|
+
const windowMinutes = typeof rawWindowMinutes === "number"
|
|
56
|
+
? rawWindowMinutes
|
|
57
|
+
: typeof rawWindowMinutes === "string" && rawWindowMinutes.trim() !== ""
|
|
58
|
+
? Number(rawWindowMinutes)
|
|
59
|
+
: null;
|
|
60
|
+
const name = windowMinutes === 300 ? "five_hour" : windowMinutes === 10080 ? "seven_day" : null;
|
|
61
|
+
return name ? normalizeWindow(name, raw, name === "five_hour" ? 300 : 10080) : null;
|
|
62
|
+
})
|
|
63
|
+
.filter((window) => window !== null);
|
|
64
|
+
if (windows.length === 0) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
provider: "codex",
|
|
69
|
+
source: options.source,
|
|
70
|
+
observedAt: options.observedAt ?? new Date().toISOString(),
|
|
71
|
+
planType: typeof record.plan_type === "string" ? record.plan_type : null,
|
|
72
|
+
windows,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function latestCandidateFromFile(file) {
|
|
76
|
+
const stat = fs.statSync(file);
|
|
77
|
+
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/);
|
|
78
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
79
|
+
const line = lines[i]?.trim();
|
|
80
|
+
if (!line || !line.includes("rate_limits")) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(line);
|
|
85
|
+
const payload = parsed.payload;
|
|
86
|
+
const rateLimits = payload?.rate_limits ?? parsed.rate_limits;
|
|
87
|
+
const observedMs = parseObservedMs(parsed.timestamp ?? parsed.ts, stat.mtimeMs);
|
|
88
|
+
const usage = usageFromCodexRateLimits(rateLimits, {
|
|
89
|
+
source: file,
|
|
90
|
+
observedAt: new Date(observedMs).toISOString(),
|
|
91
|
+
});
|
|
92
|
+
if (usage) {
|
|
93
|
+
return { usage, observedMs };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
export function collectCodexUsage(codexHome = path.join(process.env.HOME ?? "", ".codex")) {
|
|
103
|
+
if (!isDir(codexHome)) {
|
|
104
|
+
throw new Error(`Codex usage unavailable: ${codexHome} does not exist`);
|
|
105
|
+
}
|
|
106
|
+
const files = codexSessionRoots(codexHome)
|
|
107
|
+
.flatMap((root) => walkJsonlFiles(root))
|
|
108
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
109
|
+
let latest = null;
|
|
110
|
+
for (const { file, mtimeMs } of files) {
|
|
111
|
+
if (latest && mtimeMs < latest.observedMs) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
const candidate = latestCandidateFromFile(file);
|
|
116
|
+
if (candidate && (!latest || candidate.observedMs > latest.observedMs)) {
|
|
117
|
+
latest = candidate;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!latest) {
|
|
125
|
+
throw new Error("Codex usage unavailable: no local session JSONL entry with payload.rate_limits was found. Run Codex CLI/App once and try again.");
|
|
126
|
+
}
|
|
127
|
+
return latest.usage;
|
|
128
|
+
}
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readJsonFile, writeJsonAtomic } from "./fs-util.js";
|
|
2
|
+
import { buildPaths } from "./paths.js";
|
|
3
|
+
const DEFAULT_PROVIDERS = ["codex", "claude", "glm", "deepseek", "minimax"];
|
|
4
|
+
const PROVIDERS = new Set(["codex", "claude", "glm", "deepseek", "minimax"]);
|
|
5
|
+
function normalizeProviders(value) {
|
|
6
|
+
if (!Array.isArray(value)) {
|
|
7
|
+
return DEFAULT_PROVIDERS;
|
|
8
|
+
}
|
|
9
|
+
const providers = value.filter((item) => {
|
|
10
|
+
return typeof item === "string" && PROVIDERS.has(item);
|
|
11
|
+
});
|
|
12
|
+
return providers.length > 0 ? [...new Set(providers)] : DEFAULT_PROVIDERS;
|
|
13
|
+
}
|
|
14
|
+
function envProviders() {
|
|
15
|
+
const raw = process.env.CODING_USAGE_BAR_PROVIDERS;
|
|
16
|
+
if (!raw) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return normalizeProviders(raw.split(",").map((item) => item.trim()));
|
|
20
|
+
}
|
|
21
|
+
export function defaultConfig() {
|
|
22
|
+
return {
|
|
23
|
+
providers: DEFAULT_PROVIDERS,
|
|
24
|
+
glm: { baseUrl: "https://open.bigmodel.cn", apiKey: "" },
|
|
25
|
+
deepseek: { apiKey: "" },
|
|
26
|
+
minimax: { region: "cn", apiKey: "" },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function readConfig(paths = buildPaths()) {
|
|
30
|
+
const fromEnv = envProviders();
|
|
31
|
+
if (fromEnv) {
|
|
32
|
+
return { providers: fromEnv };
|
|
33
|
+
}
|
|
34
|
+
const fileConfig = readJsonFile(paths.configFile);
|
|
35
|
+
if (!fileConfig) {
|
|
36
|
+
return defaultConfig();
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
providers: normalizeProviders(fileConfig.providers),
|
|
40
|
+
glm: fileConfig.glm ?? defaultConfig().glm,
|
|
41
|
+
deepseek: fileConfig.deepseek ?? defaultConfig().deepseek,
|
|
42
|
+
minimax: fileConfig.minimax ?? defaultConfig().minimax,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function ensureConfig(paths = buildPaths()) {
|
|
46
|
+
const existing = readJsonFile(paths.configFile);
|
|
47
|
+
if (!existing) {
|
|
48
|
+
writeJsonAtomic(paths.configFile, defaultConfig());
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (!existing.glm) {
|
|
52
|
+
writeJsonAtomic(paths.configFile, { ...existing, glm: defaultConfig().glm });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (!existing.deepseek) {
|
|
56
|
+
const current = readJsonFile(paths.configFile) ?? existing;
|
|
57
|
+
writeJsonAtomic(paths.configFile, { ...current, deepseek: defaultConfig().deepseek });
|
|
58
|
+
}
|
|
59
|
+
const latest = readJsonFile(paths.configFile) ?? existing;
|
|
60
|
+
if (!latest.minimax) {
|
|
61
|
+
writeJsonAtomic(paths.configFile, { ...latest, minimax: defaultConfig().minimax });
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
package/dist/daemon.d.ts
ADDED
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { collectStatusSnapshot } from "./runtime.js";
|
|
2
|
+
import { markNotified, sendNotification, shouldNotify } from "./notifier.js";
|
|
3
|
+
export async function runDaemonOnce(options = {}) {
|
|
4
|
+
const snapshot = await collectStatusSnapshot();
|
|
5
|
+
const messages = [];
|
|
6
|
+
for (const { analysis } of snapshot.providers) {
|
|
7
|
+
if (!shouldNotify(analysis)) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
messages.push(sendNotification(analysis, options.dryRun));
|
|
11
|
+
if (!options.dryRun) {
|
|
12
|
+
markNotified(analysis);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return messages.length ? messages : ["No notification needed."];
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DeepseekConfig, ProviderUsage } from "./types.js";
|
|
2
|
+
interface DeepseekBalanceInfo {
|
|
3
|
+
currency: string;
|
|
4
|
+
total_balance: string;
|
|
5
|
+
granted_balance: string;
|
|
6
|
+
topped_up_balance: string;
|
|
7
|
+
}
|
|
8
|
+
interface DeepseekBalanceResponse {
|
|
9
|
+
is_available: boolean;
|
|
10
|
+
balance_infos: DeepseekBalanceInfo[];
|
|
11
|
+
}
|
|
12
|
+
export declare function fetchDeepseekBalance(config: DeepseekConfig): Promise<DeepseekBalanceResponse>;
|
|
13
|
+
export declare function usageFromDeepseekBalance(balance: DeepseekBalanceResponse, options: {
|
|
14
|
+
observedAt?: string;
|
|
15
|
+
source: string;
|
|
16
|
+
}): ProviderUsage | null;
|
|
17
|
+
export declare function collectDeepseekUsage(config: DeepseekConfig): Promise<ProviderUsage>;
|
|
18
|
+
export {};
|
package/dist/deepseek.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = "https://api.deepseek.com";
|
|
2
|
+
export async function fetchDeepseekBalance(config) {
|
|
3
|
+
const url = `${DEFAULT_BASE_URL}/user/balance`;
|
|
4
|
+
const response = await fetch(url, {
|
|
5
|
+
method: "GET",
|
|
6
|
+
headers: {
|
|
7
|
+
"Authorization": `Bearer ${config.apiKey}`,
|
|
8
|
+
"Content-Type": "application/json",
|
|
9
|
+
},
|
|
10
|
+
signal: AbortSignal.timeout(15_000),
|
|
11
|
+
});
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(`DeepSeek balance API returned HTTP ${response.status}`);
|
|
14
|
+
}
|
|
15
|
+
return (await response.json());
|
|
16
|
+
}
|
|
17
|
+
export function usageFromDeepseekBalance(balance, options) {
|
|
18
|
+
const info = balance.balance_infos?.[0];
|
|
19
|
+
if (!info) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
provider: "deepseek",
|
|
24
|
+
source: options.source,
|
|
25
|
+
observedAt: options.observedAt ?? new Date().toISOString(),
|
|
26
|
+
planType: null,
|
|
27
|
+
windows: [],
|
|
28
|
+
balance: {
|
|
29
|
+
total: info.total_balance,
|
|
30
|
+
currency: info.currency,
|
|
31
|
+
isAvailable: balance.is_available,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function collectDeepseekUsage(config) {
|
|
36
|
+
if (!config.apiKey) {
|
|
37
|
+
throw new Error("DeepSeek API key not configured. Edit ~/.coding-usage-bar/config.json to set deepseek.apiKey.");
|
|
38
|
+
}
|
|
39
|
+
const balance = await fetchDeepseekBalance(config);
|
|
40
|
+
const usage = usageFromDeepseekBalance(balance, {
|
|
41
|
+
source: `${DEFAULT_BASE_URL}/user/balance`,
|
|
42
|
+
});
|
|
43
|
+
if (!usage) {
|
|
44
|
+
throw new Error("DeepSeek balance response did not contain expected data");
|
|
45
|
+
}
|
|
46
|
+
return usage;
|
|
47
|
+
}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DoctorCheck } from "./types.js";
|
|
2
|
+
export declare function runDoctor(options?: {
|
|
3
|
+
dryRun?: boolean;
|
|
4
|
+
}): DoctorCheck[];
|
|
5
|
+
export declare function formatDoctor(checks: DoctorCheck[]): string;
|
|
6
|
+
export declare function doctorHasFailures(checks: DoctorCheck[]): boolean;
|