@usagefleet/cli 1.2.80 → 1.2.81
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/README.md +7 -4
- package/dist/claude-creds.js +3 -1
- package/dist/claude-limits.js +43 -43
- package/dist/collector.js +4 -3
- package/dist/config.js +2 -2
- package/dist/notifier.js +2 -4
- package/dist/paths.js +4 -1
- package/dist/release.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,9 +16,10 @@ usagefleet login uf_xxx
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
Same two commands on macOS, Linux and Windows (in PowerShell chain them with
|
|
19
|
-
`;` — 5.1 has no `&&`). `login`
|
|
20
|
-
with your session and
|
|
21
|
-
The dashboard fills in
|
|
19
|
+
`;` — 5.1 has no `&&`). `login` stores the token (the server checks it on the
|
|
20
|
+
first report, not at login), sets the collector to start with your session and
|
|
21
|
+
writes `~/.config/usagefleet/config.json` (mode `600`). The dashboard fills in
|
|
22
|
+
within a minute.
|
|
22
23
|
|
|
23
24
|
`login` takes the token and nothing else. The collector reports to
|
|
24
25
|
`usagefleet.com` and there is no way to redirect it: the request carries your
|
|
@@ -87,7 +88,9 @@ WinRT toast via `powershell.exe`.
|
|
|
87
88
|
|
|
88
89
|
A group can be set to **refuse new prompts** once it has burned its budget slice
|
|
89
90
|
(1/N of the account limit) for a window — a switch per window on the Groups
|
|
90
|
-
page, both off by default.
|
|
91
|
+
page, both off by default. Each device also has its own blocking toggle on the
|
|
92
|
+
Devices page: switched off, that machine is never refused, whatever its group
|
|
93
|
+
says. `usagefleet login` registers a Claude Code `UserPromptSubmit` hook in
|
|
91
94
|
`~/.claude/settings.json` (removed by `uninstall`, refreshed rather than stacked
|
|
92
95
|
on re-install, and skipped entirely with `USAGEFLEET_HOOK=0`):
|
|
93
96
|
|
package/dist/claude-creds.js
CHANGED
|
@@ -56,7 +56,9 @@ function persist(blob, from) {
|
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
58
|
// The password must go in argv: `security`'s stdin prompt reads at most 128
|
|
59
|
-
// bytes and would silently store a truncated (unparseable) blob.
|
|
59
|
+
// bytes and would silently store a truncated (unparseable) blob. Argv is
|
|
60
|
+
// briefly visible in the process list, but only to the same user — who can
|
|
61
|
+
// read the Keychain item anyway. Accepted.
|
|
60
62
|
execFileSync('security', ['add-generic-password', '-U', '-s', KEYCHAIN_SERVICE, '-a', userInfo().username, '-w', json], { stdio: 'ignore' });
|
|
61
63
|
// Trust nothing: a partial write here means a broken Claude Code login.
|
|
62
64
|
const stored = execFileSync('security', ['find-generic-password', '-s', KEYCHAIN_SERVICE, '-w'], {
|
package/dist/claude-limits.js
CHANGED
|
@@ -21,7 +21,9 @@ export function parseReset(v) {
|
|
|
21
21
|
/** Per-model utilization header: `anthropic-ratelimit-unified-<window>-<model>-utilization`
|
|
22
22
|
* (the account-wide headers have no `<model>` segment and don't match). */
|
|
23
23
|
const MODEL_UTIL_RE = /^anthropic-ratelimit-unified-(\d+[hdwm])-([a-z0-9][a-z0-9_.-]*)-utilization$/;
|
|
24
|
-
|
|
24
|
+
// Always reports source 'api': subscription logins never touch the header
|
|
25
|
+
// path (see fetchLimits), so headers can only come from an API-key ping.
|
|
26
|
+
export function parseLimitsHeaders(get, names = []) {
|
|
25
27
|
const modelLimits = [];
|
|
26
28
|
for (const raw of names) {
|
|
27
29
|
const m = raw.toLowerCase().match(MODEL_UTIL_RE);
|
|
@@ -42,15 +44,17 @@ export function parseLimitsHeaders(source, get, names = []) {
|
|
|
42
44
|
modelLimits,
|
|
43
45
|
sevenDayPct: parsePct(get('anthropic-ratelimit-unified-7d-utilization')),
|
|
44
46
|
sevenDayResetsAt: parseReset(get('anthropic-ratelimit-unified-7d-reset')),
|
|
45
|
-
source,
|
|
47
|
+
source: 'api',
|
|
46
48
|
};
|
|
47
49
|
}
|
|
48
50
|
const OAUTH_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
49
51
|
/**
|
|
50
52
|
* The OAuth usage endpoint Claude Code queries for /usage — the source of both
|
|
51
53
|
* the per-model caps its UI shows ("Fable · 24% used") and the account-wide
|
|
52
|
-
* numbers
|
|
53
|
-
*
|
|
54
|
+
* numbers, the ONLY limits source for subscription logins. Undocumented —
|
|
55
|
+
* parse defensively, but keep failure causes apart: an HTTP error names its
|
|
56
|
+
* status (401/403 reads as "token expired, open Claude Code to refresh",
|
|
57
|
+
* anything else as an outage to wait out), junk JSON returns null.
|
|
54
58
|
*/
|
|
55
59
|
async function fetchOauthUsage(token) {
|
|
56
60
|
const res = await fetch(OAUTH_USAGE_URL, {
|
|
@@ -62,7 +66,10 @@ async function fetchOauthUsage(token) {
|
|
|
62
66
|
signal: AbortSignal.timeout(15_000),
|
|
63
67
|
});
|
|
64
68
|
if (!res.ok) {
|
|
65
|
-
|
|
69
|
+
const hint = res.status === 401 || res.status === 403
|
|
70
|
+
? 'Claude OAuth token expired or revoked · open Claude Code to refresh it'
|
|
71
|
+
: 'transient? retrying next cycle';
|
|
72
|
+
throw new Error(`oauth/usage HTTP ${res.status} · ${hint}`);
|
|
66
73
|
}
|
|
67
74
|
const body = await res.json().catch(() => null);
|
|
68
75
|
if (process.env.USAGEFLEET_DEBUG_HEADERS) {
|
|
@@ -72,11 +79,10 @@ async function fetchOauthUsage(token) {
|
|
|
72
79
|
}
|
|
73
80
|
/**
|
|
74
81
|
* Account-wide 5h/7d numbers from an oauth/usage payload: percentages
|
|
75
|
-
* (`{utilization: 14}`), exactly what Claude's /usage screen shows
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* values survive.
|
|
82
|
+
* (`{utilization: 14}`), exactly what Claude's /usage screen shows — unlike
|
|
83
|
+
* the `-utilization` response headers, which now carry a 0–1 fraction on sub
|
|
84
|
+
* accounts. Missing fields stay null so `oauthLimitsReport` can tell an empty
|
|
85
|
+
* payload from a real one and skip the cycle.
|
|
80
86
|
*/
|
|
81
87
|
export function parseOauthAccount(body) {
|
|
82
88
|
const root = (typeof body === 'object' && body !== null ? body : {});
|
|
@@ -125,7 +131,7 @@ function modelKeyOf(name) {
|
|
|
125
131
|
* `{kind: "weekly_scoped", group: "weekly", percent, resets_at,
|
|
126
132
|
* scope: {model: {display_name: "Fable"}}}`) are exactly the per-model bars
|
|
127
133
|
* Claude's own UI renders. Account-wide entries have `scope: null` and are
|
|
128
|
-
* skipped (
|
|
134
|
+
* skipped (`parseOauthAccount` reads those from `five_hour`/`seven_day`).
|
|
129
135
|
*
|
|
130
136
|
* Fallback: legacy top-level `seven_day_<model>` objects with a `utilization`
|
|
131
137
|
* number (all null on current accounts, but cheap to keep).
|
|
@@ -151,7 +157,7 @@ export function parseOauthUsage(body) {
|
|
|
151
157
|
if (!name || typeof l.percent !== 'number') {
|
|
152
158
|
continue;
|
|
153
159
|
}
|
|
154
|
-
const window = l.group === 'session' ? '5h' :
|
|
160
|
+
const window = l.group === 'session' ? '5h' : '7d';
|
|
155
161
|
const key = modelKeyOf(name);
|
|
156
162
|
out.push({
|
|
157
163
|
model: key,
|
|
@@ -186,7 +192,7 @@ export function parseOauthUsage(body) {
|
|
|
186
192
|
}
|
|
187
193
|
/** Assemble a full LimitsReport from an oauth/usage payload, or null when the
|
|
188
194
|
* payload carries no account-wide percentage (endpoint down, shape changed) —
|
|
189
|
-
* the caller then
|
|
195
|
+
* the caller then skips this cycle rather than report degraded numbers. */
|
|
190
196
|
export function oauthLimitsReport(body) {
|
|
191
197
|
const account = parseOauthAccount(body);
|
|
192
198
|
if (account.fiveHourPct == null && account.sevenDayPct == null) {
|
|
@@ -197,36 +203,26 @@ export function oauthLimitsReport(body) {
|
|
|
197
203
|
/**
|
|
198
204
|
* Read the account's real rate-limit utilization.
|
|
199
205
|
*
|
|
200
|
-
* Subscription logins
|
|
201
|
-
* exact numbers Claude's own /usage screen shows (account-wide AND per-model)
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
+
* Subscription logins use the free OAuth usage endpoint, and ONLY it — the
|
|
207
|
+
* exact numbers Claude's own /usage screen shows (account-wide AND per-model),
|
|
208
|
+
* no tokens spent. When it yields nothing, this throws instead of falling back
|
|
209
|
+
* to the Messages-API ping: on sub accounts those `-utilization` headers now
|
|
210
|
+
* carry a 0–1 fraction that parsePct reads 100× low, and one such POST would
|
|
211
|
+
* overwrite the server's last-good percentages for the whole account. Skipping
|
|
212
|
+
* the cycle keeps last-good everywhere (the guard has its own staleness rule).
|
|
213
|
+
*
|
|
214
|
+
* API keys can't call oauth/usage; their headers still read 0–100, so they
|
|
215
|
+
* keep the 1-token header ping (same approach as Claude-Usage-Tracker).
|
|
206
216
|
*/
|
|
207
217
|
export async function fetchLimits(creds) {
|
|
208
218
|
if (creds.source === 'sub') {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
catch {
|
|
216
|
-
/* fall through to the header ping */
|
|
219
|
+
// fetchOauthUsage already threw on an HTTP error with its status; reaching
|
|
220
|
+
// here with no report means a 200 whose body carried no percentages.
|
|
221
|
+
const report = oauthLimitsReport(await fetchOauthUsage(creds.token));
|
|
222
|
+
if (!report) {
|
|
223
|
+
throw new Error('oauth/usage answered without percentages · shape changed? keeping last-good');
|
|
217
224
|
}
|
|
218
|
-
|
|
219
|
-
const headers = {
|
|
220
|
-
'anthropic-version': '2023-06-01',
|
|
221
|
-
'content-type': 'application/json',
|
|
222
|
-
};
|
|
223
|
-
if (creds.source === 'sub') {
|
|
224
|
-
headers['authorization'] = `Bearer ${creds.token}`;
|
|
225
|
-
headers['anthropic-beta'] = 'oauth-2025-04-20';
|
|
226
|
-
headers['user-agent'] = 'claude-code/2.1.5 (usagefleet)';
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
headers['x-api-key'] = creds.token;
|
|
225
|
+
return report;
|
|
230
226
|
}
|
|
231
227
|
const res = await fetch(MESSAGES_URL, {
|
|
232
228
|
body: JSON.stringify({
|
|
@@ -234,13 +230,17 @@ export async function fetchLimits(creds) {
|
|
|
234
230
|
messages: [{ role: 'user', content: 'hi' }],
|
|
235
231
|
model: 'claude-haiku-4-5-20251001',
|
|
236
232
|
}),
|
|
237
|
-
headers
|
|
233
|
+
headers: {
|
|
234
|
+
'anthropic-version': '2023-06-01',
|
|
235
|
+
'content-type': 'application/json',
|
|
236
|
+
'x-api-key': creds.token,
|
|
237
|
+
},
|
|
238
238
|
method: 'POST',
|
|
239
239
|
signal: AbortSignal.timeout(15_000),
|
|
240
240
|
});
|
|
241
241
|
// The unified rate-limit headers are (historically) present on success AND
|
|
242
|
-
// error responses — this
|
|
243
|
-
//
|
|
242
|
+
// error responses — this header-scraping path against the public Messages
|
|
243
|
+
// endpoint is undocumented and may break without notice. If a
|
|
244
244
|
// rejected response ALSO lacks the headers, the feature is unavailable; throw
|
|
245
245
|
// so the caller logs it instead of POSTing an all-null report silently.
|
|
246
246
|
// Diagnostic: dump every rate-limit header so unrecognized per-model names
|
|
@@ -252,7 +252,7 @@ export async function fetchLimits(creds) {
|
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
|
-
const report = parseLimitsHeaders(
|
|
255
|
+
const report = parseLimitsHeaders(n => res.headers.get(n), res.headers.keys());
|
|
256
256
|
const gotHeaders = report.fiveHourPct != null ||
|
|
257
257
|
report.sevenDayPct != null ||
|
|
258
258
|
report.fiveHourResetsAt != null ||
|
package/dist/collector.js
CHANGED
|
@@ -213,9 +213,10 @@ function planWall() {
|
|
|
213
213
|
return `device outside your plan's device limit · free a slot or upgrade at ${ENDPOINT}/devices · nothing is lost, uploads resume once it fits`;
|
|
214
214
|
}
|
|
215
215
|
/**
|
|
216
|
-
* Auto-detect the local Claude login, read the real 5h/weekly utilization
|
|
217
|
-
*
|
|
218
|
-
* returns null (and logs) when no login
|
|
216
|
+
* Auto-detect the local Claude login, read the real 5h/weekly utilization
|
|
217
|
+
* (oauth/usage for subscription logins, rate-limit headers for API keys), and
|
|
218
|
+
* report it to the server. Best-effort — returns null (and logs) when no login
|
|
219
|
+
* is found or the request fails.
|
|
219
220
|
*/
|
|
220
221
|
export async function reportLimitsOnce(cfg, log = () => {
|
|
221
222
|
/* empty */
|
package/dist/config.js
CHANGED
|
@@ -41,8 +41,8 @@ export function resolvePiDirs(env, fromFile) {
|
|
|
41
41
|
}
|
|
42
42
|
return [...new Set(raw.map(d => d.trim()).filter(d => d.length > 0))];
|
|
43
43
|
}
|
|
44
|
-
/** Optional scan root (USAGEFLEET_DESKTOP
|
|
45
|
-
*
|
|
44
|
+
/** Optional scan root (USAGEFLEET_DESKTOP): env "off"/"0" disables, env or
|
|
45
|
+
* config-file path overrides, else the auto-detected default. */
|
|
46
46
|
function resolveOptionalDir(env, fromFile, fallback) {
|
|
47
47
|
if (env === '0' || env?.toLowerCase() === 'off') {
|
|
48
48
|
return null;
|
package/dist/notifier.js
CHANGED
|
@@ -46,10 +46,8 @@ export function evaluateWindow(prev, pct, resetsAt, thresholds) {
|
|
|
46
46
|
if (top > lastBucket) {
|
|
47
47
|
return { fire: top, next: { lastBucket: top, resetsAt } };
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
return { fire: null, next: { lastBucket, resetsAt } };
|
|
49
|
+
// top <= lastBucket: the mark follows pct down (or holds); nothing fires.
|
|
50
|
+
return { fire: null, next: { lastBucket: top, resetsAt } };
|
|
53
51
|
}
|
|
54
52
|
/** Relative "resets in 12m" / "resets in 2h" suffix, or "" if unknown/past. */
|
|
55
53
|
function resetSuffix(resetsAt) {
|
package/dist/paths.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
// USAGEFLEET_PROJECTS is resolved in config.ts's precedence chain, not here —
|
|
4
|
+
// a second env read (with `??` vs `||` drift) once made an empty env var
|
|
5
|
+
// silently scan nothing.
|
|
3
6
|
export function defaultProjectsDir() {
|
|
4
|
-
return
|
|
7
|
+
return join(homedir(), '.claude', 'projects');
|
|
5
8
|
}
|
|
6
9
|
/** Claude Desktop's Electron userData dir, per-OS. Mirrors the app's own
|
|
7
10
|
* `app.getPath("userData")` (= platform appData + the "Claude" product name),
|
package/dist/release.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by .github/workflows/release.yml.
|
|
2
|
-
export const RELEASE_VERSION = "1.2.
|
|
2
|
+
export const RELEASE_VERSION = "1.2.81";
|