coding-usage-bar 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/burn.d.ts +1 -0
- package/dist/burn.js +8 -2
- package/dist/cli.js +8 -0
- package/dist/runtime.d.ts +6 -0
- package/dist/runtime.js +29 -4
- package/dist/status.d.ts +4 -0
- package/dist/status.js +122 -3
- package/package.json +1 -1
package/dist/burn.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { BurnAnalysis, BurnProfile, ProviderUsage } from "./types.js";
|
|
2
2
|
export declare function estimateConversionRate(samples: ProviderUsage[]): number | null;
|
|
3
|
+
export declare function analyzeUsageWithConversionRate(usage: ProviderUsage, conversionRate: number | null, profile?: BurnProfile, now?: Date): BurnAnalysis;
|
|
3
4
|
export declare function analyzeUsage(usage: ProviderUsage, samples: ProviderUsage[], profile?: BurnProfile, now?: Date): BurnAnalysis;
|
package/dist/burn.js
CHANGED
|
@@ -74,7 +74,7 @@ function messageForState(provider, state, fiveUsed, target) {
|
|
|
74
74
|
}
|
|
75
75
|
return `${label} burn pace is on track (${round(fiveUsed)}%, target ${round(target.min)}%-${round(target.max)}%).`;
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
function analyzeUsageWithRateResolver(usage, resolveConversionRate, profile = "low", now = new Date()) {
|
|
78
78
|
const fiveHour = findWindow(usage, "five_hour");
|
|
79
79
|
const sevenDay = findWindow(usage, "seven_day");
|
|
80
80
|
if (usage.balance && usage.windows.length === 0) {
|
|
@@ -118,7 +118,7 @@ export function analyzeUsage(usage, samples, profile = "low", now = new Date())
|
|
|
118
118
|
message,
|
|
119
119
|
};
|
|
120
120
|
}
|
|
121
|
-
const conversionRate =
|
|
121
|
+
const conversionRate = resolveConversionRate();
|
|
122
122
|
if (conversionRate === null) {
|
|
123
123
|
return {
|
|
124
124
|
provider: usage.provider,
|
|
@@ -162,3 +162,9 @@ export function analyzeUsage(usage, samples, profile = "low", now = new Date())
|
|
|
162
162
|
}),
|
|
163
163
|
};
|
|
164
164
|
}
|
|
165
|
+
export function analyzeUsageWithConversionRate(usage, conversionRate, profile = "low", now = new Date()) {
|
|
166
|
+
return analyzeUsageWithRateResolver(usage, () => conversionRate, profile, now);
|
|
167
|
+
}
|
|
168
|
+
export function analyzeUsage(usage, samples, profile = "low", now = new Date()) {
|
|
169
|
+
return analyzeUsageWithRateResolver(usage, () => estimateConversionRate(samples), profile, now);
|
|
170
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { installMenuBar, renderMenuBar, uninstallMenuBar, toggleCompactMode } fr
|
|
|
10
10
|
import { buildPaths } from "./paths.js";
|
|
11
11
|
import { bakeGlyphAtlases } from "./glyphs.js";
|
|
12
12
|
import { maybePromptForStar } from "./star.js";
|
|
13
|
+
import { updateStatusSnapshotUsage } from "./status.js";
|
|
13
14
|
function hasFlag(args, flag) {
|
|
14
15
|
return args.includes(flag);
|
|
15
16
|
}
|
|
@@ -119,6 +120,13 @@ async function main() {
|
|
|
119
120
|
const parsed = JSON.parse(input);
|
|
120
121
|
const usage = usageFromClaudeStatusLine(parsed);
|
|
121
122
|
saveUsage(usage);
|
|
123
|
+
try {
|
|
124
|
+
updateStatusSnapshotUsage(usage);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// The provider cache is authoritative. A later daemon run can rebuild
|
|
128
|
+
// status.json if this best-effort display update loses lock contention.
|
|
129
|
+
}
|
|
122
130
|
return;
|
|
123
131
|
}
|
|
124
132
|
if (command === "daemon") {
|
package/dist/runtime.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ export declare function analyzeUsages(usages: ProviderUsage[], profile: BurnProf
|
|
|
13
13
|
export declare function readProfile(): BurnProfile;
|
|
14
14
|
export declare function loadFixtureSamples(provider: string): ProviderUsage[];
|
|
15
15
|
export declare function analyzeFixtureUsages(usages: ProviderUsage[], profile: BurnProfile): BurnAnalysis[];
|
|
16
|
+
export declare function commitCollectedStatusSnapshot(usages: ProviderUsage[], issues: StatusIssue[], options?: {
|
|
17
|
+
paths?: RuntimePaths;
|
|
18
|
+
profile?: BurnProfile;
|
|
19
|
+
generatedAt?: Date;
|
|
20
|
+
fixtureSamples?: Map<string, ProviderUsage[]>;
|
|
21
|
+
}): StatusSnapshot;
|
|
16
22
|
export declare function collectStatusSnapshot(options?: {
|
|
17
23
|
fixtures?: boolean;
|
|
18
24
|
}): Promise<StatusSnapshot>;
|
package/dist/runtime.js
CHANGED
|
@@ -10,7 +10,7 @@ import { collectKimiUsage, resolveKimiConfig } from "./kimi.js";
|
|
|
10
10
|
import { collectMinimaxUsage } from "./minimax.js";
|
|
11
11
|
import { buildPaths } from "./paths.js";
|
|
12
12
|
import { loadLatestUsage, saveUsage } from "./store.js";
|
|
13
|
-
import { createStatusSnapshot, loadStatusSnapshot, refreshStatusSnapshotFreshness, saveStatusSnapshot, } from "./status.js";
|
|
13
|
+
import { createStatusSnapshot, loadStatusSnapshot, reconcileLatestUsages, replaceStatusSnapshotUsage, refreshStatusSnapshotFreshness, saveStatusSnapshot, withStatusSnapshotLock, } from "./status.js";
|
|
14
14
|
const FIXTURES_DIR = fileURLToPath(new URL("../fixtures", import.meta.url));
|
|
15
15
|
export function loadFixtureUsages(fixturesDir) {
|
|
16
16
|
const providers = ["claude", "codex", "glm", "deepseek", "minimax", "kimi"];
|
|
@@ -248,17 +248,42 @@ export function analyzeFixtureUsages(usages, profile) {
|
|
|
248
248
|
const fixtureSamples = new Map(usages.map((usage) => [usage.provider, loadFixtureSamples(usage.provider)]));
|
|
249
249
|
return createStatusSnapshot(usages, profile, { fixtureSamples }).providers.map((provider) => provider.analysis);
|
|
250
250
|
}
|
|
251
|
+
export function commitCollectedStatusSnapshot(usages, issues, options = {}) {
|
|
252
|
+
const paths = options.paths ?? buildPaths();
|
|
253
|
+
const profile = options.profile ?? readProfile();
|
|
254
|
+
const now = options.generatedAt ?? new Date();
|
|
255
|
+
const candidate = createStatusSnapshot(usages, profile, {
|
|
256
|
+
paths,
|
|
257
|
+
generatedAt: now,
|
|
258
|
+
fixtureSamples: options.fixtureSamples,
|
|
259
|
+
issues,
|
|
260
|
+
});
|
|
261
|
+
return withStatusSnapshotLock(paths, () => {
|
|
262
|
+
const existing = loadStatusSnapshot(paths);
|
|
263
|
+
const cached = reconcileLatestUsages(usages, paths);
|
|
264
|
+
let snapshot = candidate;
|
|
265
|
+
for (const usage of cached) {
|
|
266
|
+
snapshot = replaceStatusSnapshotUsage(snapshot, usage, now);
|
|
267
|
+
}
|
|
268
|
+
if (existing) {
|
|
269
|
+
for (const provider of existing.providers) {
|
|
270
|
+
snapshot = replaceStatusSnapshotUsage(snapshot, provider.usage, now);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
saveStatusSnapshot(snapshot, paths);
|
|
274
|
+
return snapshot;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
251
277
|
export async function collectStatusSnapshot(options = {}) {
|
|
252
278
|
const { usages, issues } = await collectLocalState(options);
|
|
253
279
|
const profile = readProfile();
|
|
254
280
|
const fixtureSamples = options.fixtures
|
|
255
281
|
? new Map(usages.map((usage) => [usage.provider, loadFixtureSamples(usage.provider)]))
|
|
256
282
|
: undefined;
|
|
257
|
-
const snapshot = createStatusSnapshot(usages, profile, { fixtureSamples, issues });
|
|
258
283
|
if (!options.fixtures) {
|
|
259
|
-
|
|
284
|
+
return commitCollectedStatusSnapshot(usages, issues, { profile });
|
|
260
285
|
}
|
|
261
|
-
return
|
|
286
|
+
return createStatusSnapshot(usages, profile, { fixtureSamples, issues });
|
|
262
287
|
}
|
|
263
288
|
export function loadDisplayStatusSnapshot(options = {}) {
|
|
264
289
|
if (options.fixtures || options.refresh) {
|
package/dist/status.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { BurnProfile, ProviderUsage, RuntimePaths, StatusIssue, StatusSnapshot } from "./types.js";
|
|
2
|
+
export declare function withStatusSnapshotLock<T>(paths: RuntimePaths, action: () => T): T;
|
|
3
|
+
export declare function reconcileLatestUsages(usages: ProviderUsage[], paths: RuntimePaths): ProviderUsage[];
|
|
2
4
|
export declare function createStatusSnapshot(usages: ProviderUsage[], profile: BurnProfile, options?: {
|
|
3
5
|
paths?: RuntimePaths;
|
|
4
6
|
generatedAt?: Date;
|
|
@@ -12,3 +14,5 @@ export declare function refreshStatusSnapshotFreshness(snapshot: StatusSnapshot,
|
|
|
12
14
|
}): StatusSnapshot;
|
|
13
15
|
export declare function saveStatusSnapshot(snapshot: StatusSnapshot, paths?: RuntimePaths): void;
|
|
14
16
|
export declare function loadStatusSnapshot(paths?: RuntimePaths): StatusSnapshot | null;
|
|
17
|
+
export declare function replaceStatusSnapshotUsage(existing: StatusSnapshot | null, usage: ProviderUsage, now?: Date): StatusSnapshot;
|
|
18
|
+
export declare function updateStatusSnapshotUsage(usage: ProviderUsage, paths?: RuntimePaths): StatusSnapshot;
|
package/dist/status.js
CHANGED
|
@@ -1,8 +1,91 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { analyzeUsage, analyzeUsageWithConversionRate } from "./burn.js";
|
|
4
|
+
import { ensureDir, readJsonFile, writeJsonAtomic } from "./fs-util.js";
|
|
5
|
+
import { buildPaths, providerLatestPath } from "./paths.js";
|
|
4
6
|
import { loadSamples } from "./store.js";
|
|
5
7
|
const DEFAULT_STALE_AFTER_SECONDS = 10 * 60;
|
|
8
|
+
const STATUS_LOCK_RETRY_MS = 5;
|
|
9
|
+
const STATUS_LOCK_TIMEOUT_MS = 2_000;
|
|
10
|
+
const STATUS_LOCK_STALE_MS = 10_000;
|
|
11
|
+
const lockWaitArray = new Int32Array(new SharedArrayBuffer(4));
|
|
12
|
+
function waitForStatusLock() {
|
|
13
|
+
Atomics.wait(lockWaitArray, 0, 0, STATUS_LOCK_RETRY_MS);
|
|
14
|
+
}
|
|
15
|
+
export function withStatusSnapshotLock(paths, action) {
|
|
16
|
+
const lockFile = `${paths.statusFile}.lock`;
|
|
17
|
+
const deadline = Date.now() + STATUS_LOCK_TIMEOUT_MS;
|
|
18
|
+
ensureDir(path.dirname(lockFile));
|
|
19
|
+
let descriptor = null;
|
|
20
|
+
while (descriptor === null) {
|
|
21
|
+
try {
|
|
22
|
+
descriptor = fs.openSync(lockFile, "wx", 0o600);
|
|
23
|
+
fs.writeFileSync(descriptor, `${process.pid}\n`, "utf8");
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== "EEXIST") {
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const ageMs = Date.now() - fs.statSync(lockFile).mtimeMs;
|
|
31
|
+
if (ageMs > STATUS_LOCK_STALE_MS) {
|
|
32
|
+
fs.unlinkSync(lockFile);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (statError) {
|
|
37
|
+
if (statError.code === "ENOENT") {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
throw statError;
|
|
41
|
+
}
|
|
42
|
+
if (Date.now() >= deadline) {
|
|
43
|
+
throw new Error(`Timed out waiting for status snapshot lock at ${lockFile}`);
|
|
44
|
+
}
|
|
45
|
+
waitForStatusLock();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return action();
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
fs.closeSync(descriptor);
|
|
53
|
+
try {
|
|
54
|
+
fs.unlinkSync(lockFile);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code !== "ENOENT") {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function observedAtMs(usage) {
|
|
64
|
+
const value = Date.parse(usage.observedAt);
|
|
65
|
+
return Number.isFinite(value) ? value : null;
|
|
66
|
+
}
|
|
67
|
+
function newestUsage(usages) {
|
|
68
|
+
return usages.reduce((latest, candidate) => {
|
|
69
|
+
const latestTime = observedAtMs(latest);
|
|
70
|
+
const candidateTime = observedAtMs(candidate);
|
|
71
|
+
return candidateTime !== null && (latestTime === null || candidateTime > latestTime)
|
|
72
|
+
? candidate
|
|
73
|
+
: latest;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
export function reconcileLatestUsages(usages, paths) {
|
|
77
|
+
return usages.map((usage) => {
|
|
78
|
+
const cached = readJsonFile(providerLatestPath(paths, usage.provider));
|
|
79
|
+
if (!cached || cached.provider !== usage.provider) {
|
|
80
|
+
return usage;
|
|
81
|
+
}
|
|
82
|
+
const cachedTime = observedAtMs(cached);
|
|
83
|
+
const usageTime = observedAtMs(usage);
|
|
84
|
+
return cachedTime !== null && (usageTime === null || cachedTime > usageTime)
|
|
85
|
+
? cached
|
|
86
|
+
: usage;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
6
89
|
function buildProviderMeta(usage, generatedAt, staleAfterSeconds) {
|
|
7
90
|
const ageSeconds = Math.max(0, Math.round((generatedAt.getTime() - Date.parse(usage.observedAt)) / 1000));
|
|
8
91
|
return {
|
|
@@ -79,3 +162,39 @@ export function saveStatusSnapshot(snapshot, paths = buildPaths()) {
|
|
|
79
162
|
export function loadStatusSnapshot(paths = buildPaths()) {
|
|
80
163
|
return readJsonFile(paths.statusFile);
|
|
81
164
|
}
|
|
165
|
+
export function replaceStatusSnapshotUsage(existing, usage, now = new Date()) {
|
|
166
|
+
if (!existing) {
|
|
167
|
+
return createStatusSnapshot([usage], "low", {
|
|
168
|
+
generatedAt: now,
|
|
169
|
+
fixtureSamples: new Map([[usage.provider, []]]),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const previous = existing.providers.find((provider) => provider.usage.provider === usage.provider);
|
|
173
|
+
const selectedUsage = previous ? newestUsage([previous.usage, usage]) : usage;
|
|
174
|
+
const conversionRate = previous?.analysis.target?.conversionRate ?? null;
|
|
175
|
+
const provider = {
|
|
176
|
+
usage: selectedUsage,
|
|
177
|
+
analysis: analyzeUsageWithConversionRate(selectedUsage, conversionRate, existing.profile, now),
|
|
178
|
+
meta: buildProviderMeta(selectedUsage, now, DEFAULT_STALE_AFTER_SECONDS),
|
|
179
|
+
};
|
|
180
|
+
const providers = previous
|
|
181
|
+
? existing.providers.map((item) => item.usage.provider === usage.provider ? provider : item)
|
|
182
|
+
: [...existing.providers, provider];
|
|
183
|
+
return refreshStatusSnapshotFreshness({
|
|
184
|
+
...existing,
|
|
185
|
+
generatedAt: now.toISOString(),
|
|
186
|
+
providers,
|
|
187
|
+
issues: existing.issues.filter((issue) => (issue.code !== "USAGE_STALE"
|
|
188
|
+
&& !(issue.provider === usage.provider && issue.code === "CLAUDE_INGEST_MISSING"))),
|
|
189
|
+
}, { now });
|
|
190
|
+
}
|
|
191
|
+
export function updateStatusSnapshotUsage(usage, paths = buildPaths()) {
|
|
192
|
+
return withStatusSnapshotLock(paths, () => {
|
|
193
|
+
const now = new Date();
|
|
194
|
+
const existing = loadStatusSnapshot(paths);
|
|
195
|
+
const cached = reconcileLatestUsages([usage], paths)[0];
|
|
196
|
+
const snapshot = replaceStatusSnapshotUsage(existing, newestUsage([usage, cached]), now);
|
|
197
|
+
saveStatusSnapshot(snapshot, paths);
|
|
198
|
+
return snapshot;
|
|
199
|
+
});
|
|
200
|
+
}
|