@usagefleet/cli 1.2.79 → 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 +13 -10
- package/dist/claude-creds.js +3 -1
- package/dist/claude-limits.js +86 -40
- package/dist/collector.js +4 -3
- package/dist/config.js +2 -2
- package/dist/index.js +12 -6
- package/dist/notifier.js +9 -8
- package/dist/parser.js +15 -8
- package/dist/paths.js +4 -1
- package/dist/release.js +1 -1
- package/dist/service.js +17 -8
- package/dist/ui.js +3 -2
- 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
|
|
@@ -63,11 +64,11 @@ shell once. `uninstall` removes both again, and self-update keeps them current.
|
|
|
63
64
|
is at-least-once — the server dedups on `uuid`.
|
|
64
65
|
- **Your real limit %** — the collector uses the Claude login already on the
|
|
65
66
|
machine (subscription OAuth from `claude`: macOS login Keychain, elsewhere
|
|
66
|
-
`<config dir>/.credentials.json`; falling back to `ANTHROPIC_API_KEY`)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
leave the machine — only the percentages
|
|
70
|
-
login was found.
|
|
67
|
+
`<config dir>/.credentials.json`; falling back to `ANTHROPIC_API_KEY`) and
|
|
68
|
+
reads the same 5h/weekly percentages Claude's own `/usage` screen shows —
|
|
69
|
+
from the free OAuth usage endpoint for subscriptions, or a 1-token Messages
|
|
70
|
+
ping for API keys. Credentials never leave the machine — only the percentages
|
|
71
|
+
do. `usagefleet status` shows which login was found.
|
|
71
72
|
|
|
72
73
|
Uploaded per record: token counts, model, session id, hostname, working
|
|
73
74
|
directory, git branch. Prompts, responses and file contents are never read.
|
|
@@ -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
|
|
|
@@ -127,7 +130,7 @@ Env vars override the file:
|
|
|
127
130
|
| `USAGEFLEET_DESKTOP` | override the Claude Desktop sessions dir; `off` to skip it |
|
|
128
131
|
| `USAGEFLEET_PI` | override the pi sessions dirs, comma-separated; `off` to skip |
|
|
129
132
|
| `USAGEFLEET_INTERVAL` | watch poll seconds (default 15) |
|
|
130
|
-
| `USAGEFLEET_LIMITS_INTERVAL` | seconds between limit
|
|
133
|
+
| `USAGEFLEET_LIMITS_INTERVAL` | seconds between limit reports (default 60; 300 on API keys, where each report costs a 1-token ping) |
|
|
131
134
|
| `USAGEFLEET_NOTIFY` | desktop notifications, on by default |
|
|
132
135
|
| `USAGEFLEET_NOTIFY_THRESHOLDS` | utilization % that trigger an alert (default `80,95`) |
|
|
133
136
|
| `USAGEFLEET_BATCH` | records per upload (default 100, server caps at 1000) |
|
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,17 +44,19 @@ 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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* The OAuth usage endpoint Claude Code queries for /usage — the source of both
|
|
53
|
+
* the per-model caps its UI shows ("Fable · 24% used") and the account-wide
|
|
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
|
-
async function
|
|
59
|
+
async function fetchOauthUsage(token) {
|
|
56
60
|
const res = await fetch(OAUTH_USAGE_URL, {
|
|
57
61
|
headers: {
|
|
58
62
|
'anthropic-beta': 'oauth-2025-04-20',
|
|
@@ -62,26 +66,59 @@ async function fetchOauthModelLimits(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) {
|
|
69
76
|
console.error(`[debug] oauth/usage: ${JSON.stringify(body)}`);
|
|
70
77
|
}
|
|
71
|
-
return
|
|
78
|
+
return body;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Account-wide 5h/7d numbers from an oauth/usage payload: percentages
|
|
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.
|
|
86
|
+
*/
|
|
87
|
+
export function parseOauthAccount(body) {
|
|
88
|
+
const root = (typeof body === 'object' && body !== null ? body : {});
|
|
89
|
+
const window = (key) => {
|
|
90
|
+
const raw = root[key];
|
|
91
|
+
const entry = (typeof raw === 'object' && raw !== null ? raw : {});
|
|
92
|
+
return {
|
|
93
|
+
pct: typeof entry.utilization === 'number' ? clampPct(entry.utilization) : null,
|
|
94
|
+
resetsAt: typeof entry.resets_at === 'string' ? parseReset(entry.resets_at) : null,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
const five = window('five_hour');
|
|
98
|
+
const seven = window('seven_day');
|
|
99
|
+
return {
|
|
100
|
+
fiveHourPct: five.pct,
|
|
101
|
+
fiveHourResetsAt: five.resetsAt,
|
|
102
|
+
sevenDayPct: seven.pct,
|
|
103
|
+
sevenDayResetsAt: seven.resetsAt,
|
|
104
|
+
};
|
|
72
105
|
}
|
|
73
106
|
/** Both sources report 0–100 percentages: the `-utilization` headers ("37") and
|
|
74
107
|
* oauth/usage's `utilization`/`percent` fields. Clamping is all they need — a
|
|
75
108
|
* 0–1 "fraction form" heuristic would read a real 1% as 100%, which reaches the
|
|
76
109
|
* headline number, the critical notification and guard's prompt block.
|
|
77
110
|
*
|
|
111
|
+
* One decimal is kept (not rounded to whole): the server multiplies the group
|
|
112
|
+
* split by the group count, so integer quantization would amplify to whole
|
|
113
|
+
* points on the dashboard.
|
|
114
|
+
*
|
|
78
115
|
* Non-finite in, null out: `Infinity` (a JSON `1e999`, or a junk header) would
|
|
79
116
|
* otherwise clamp to a perfectly plausible 100 and block every prompt. */
|
|
80
117
|
function clampPct(n) {
|
|
81
118
|
if (!Number.isFinite(n)) {
|
|
82
119
|
return null;
|
|
83
120
|
}
|
|
84
|
-
return Math.min(100, Math.max(0, Math.round(n)));
|
|
121
|
+
return Math.min(100, Math.max(0, Math.round(n * 10) / 10));
|
|
85
122
|
}
|
|
86
123
|
/** Normalize a scope's model name to a header-safe key ("Fable" → "fable"). */
|
|
87
124
|
function modelKeyOf(name) {
|
|
@@ -94,7 +131,7 @@ function modelKeyOf(name) {
|
|
|
94
131
|
* `{kind: "weekly_scoped", group: "weekly", percent, resets_at,
|
|
95
132
|
* scope: {model: {display_name: "Fable"}}}`) are exactly the per-model bars
|
|
96
133
|
* Claude's own UI renders. Account-wide entries have `scope: null` and are
|
|
97
|
-
* skipped (
|
|
134
|
+
* skipped (`parseOauthAccount` reads those from `five_hour`/`seven_day`).
|
|
98
135
|
*
|
|
99
136
|
* Fallback: legacy top-level `seven_day_<model>` objects with a `utilization`
|
|
100
137
|
* number (all null on current accounts, but cheap to keep).
|
|
@@ -120,7 +157,7 @@ export function parseOauthUsage(body) {
|
|
|
120
157
|
if (!name || typeof l.percent !== 'number') {
|
|
121
158
|
continue;
|
|
122
159
|
}
|
|
123
|
-
const window = l.group === 'session' ? '5h' :
|
|
160
|
+
const window = l.group === 'session' ? '5h' : '7d';
|
|
124
161
|
const key = modelKeyOf(name);
|
|
125
162
|
out.push({
|
|
126
163
|
model: key,
|
|
@@ -153,23 +190,39 @@ export function parseOauthUsage(body) {
|
|
|
153
190
|
}
|
|
154
191
|
return out;
|
|
155
192
|
}
|
|
193
|
+
/** Assemble a full LimitsReport from an oauth/usage payload, or null when the
|
|
194
|
+
* payload carries no account-wide percentage (endpoint down, shape changed) —
|
|
195
|
+
* the caller then skips this cycle rather than report degraded numbers. */
|
|
196
|
+
export function oauthLimitsReport(body) {
|
|
197
|
+
const account = parseOauthAccount(body);
|
|
198
|
+
if (account.fiveHourPct == null && account.sevenDayPct == null) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return { ...account, modelLimits: parseOauthUsage(body), source: 'sub' };
|
|
202
|
+
}
|
|
156
203
|
/**
|
|
157
|
-
* Read the account's real rate-limit utilization.
|
|
158
|
-
*
|
|
159
|
-
*
|
|
204
|
+
* Read the account's real rate-limit utilization.
|
|
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).
|
|
160
216
|
*/
|
|
161
217
|
export async function fetchLimits(creds) {
|
|
162
|
-
const headers = {
|
|
163
|
-
'anthropic-version': '2023-06-01',
|
|
164
|
-
'content-type': 'application/json',
|
|
165
|
-
};
|
|
166
218
|
if (creds.source === 'sub') {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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');
|
|
224
|
+
}
|
|
225
|
+
return report;
|
|
173
226
|
}
|
|
174
227
|
const res = await fetch(MESSAGES_URL, {
|
|
175
228
|
body: JSON.stringify({
|
|
@@ -177,13 +230,17 @@ export async function fetchLimits(creds) {
|
|
|
177
230
|
messages: [{ role: 'user', content: 'hi' }],
|
|
178
231
|
model: 'claude-haiku-4-5-20251001',
|
|
179
232
|
}),
|
|
180
|
-
headers
|
|
233
|
+
headers: {
|
|
234
|
+
'anthropic-version': '2023-06-01',
|
|
235
|
+
'content-type': 'application/json',
|
|
236
|
+
'x-api-key': creds.token,
|
|
237
|
+
},
|
|
181
238
|
method: 'POST',
|
|
182
239
|
signal: AbortSignal.timeout(15_000),
|
|
183
240
|
});
|
|
184
241
|
// The unified rate-limit headers are (historically) present on success AND
|
|
185
|
-
// error responses — this
|
|
186
|
-
//
|
|
242
|
+
// error responses — this header-scraping path against the public Messages
|
|
243
|
+
// endpoint is undocumented and may break without notice. If a
|
|
187
244
|
// rejected response ALSO lacks the headers, the feature is unavailable; throw
|
|
188
245
|
// so the caller logs it instead of POSTing an all-null report silently.
|
|
189
246
|
// Diagnostic: dump every rate-limit header so unrecognized per-model names
|
|
@@ -195,7 +252,7 @@ export async function fetchLimits(creds) {
|
|
|
195
252
|
}
|
|
196
253
|
}
|
|
197
254
|
}
|
|
198
|
-
const report = parseLimitsHeaders(
|
|
255
|
+
const report = parseLimitsHeaders(n => res.headers.get(n), res.headers.keys());
|
|
199
256
|
const gotHeaders = report.fiveHourPct != null ||
|
|
200
257
|
report.sevenDayPct != null ||
|
|
201
258
|
report.fiveHourResetsAt != null ||
|
|
@@ -204,16 +261,5 @@ export async function fetchLimits(creds) {
|
|
|
204
261
|
if (!res.ok && !gotHeaders) {
|
|
205
262
|
throw new Error(`limits unavailable: HTTP ${res.status} with no rate-limit headers`);
|
|
206
263
|
}
|
|
207
|
-
// Subscription logins: merge in the per-model caps from the OAuth usage
|
|
208
|
-
// endpoint (the ping headers never include them). Best-effort — keep the
|
|
209
|
-
// header-derived report on any failure.
|
|
210
|
-
if (creds.source === 'sub' && report.modelLimits.length === 0) {
|
|
211
|
-
try {
|
|
212
|
-
report.modelLimits = await fetchOauthModelLimits(creds.token);
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
/* endpoint unavailable — report account-wide limits only */
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
264
|
return report;
|
|
219
265
|
}
|
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/index.js
CHANGED
|
@@ -70,11 +70,14 @@ async function cmdWatch() {
|
|
|
70
70
|
const cfg = loadConfig();
|
|
71
71
|
const raw = Number(flag('interval') ?? process.env.USAGEFLEET_INTERVAL ?? 15);
|
|
72
72
|
const interval = Math.max(1, Number.isFinite(raw) && raw > 0 ? raw : 15) * 1000;
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
// Limits reporting is decoupled from the much faster usage poll. Default
|
|
74
|
+
// interval depends on how the reading is fetched: subscription logins use the
|
|
75
|
+
// free oauth/usage endpoint (60s keeps the dashboard split fresh at zero token
|
|
76
|
+
// cost), API keys pay a 1-token Messages ping per reading (300s). An explicit
|
|
77
|
+
// USAGEFLEET_LIMITS_INTERVAL overrides both.
|
|
78
|
+
const rawLimits = Number(process.env.USAGEFLEET_LIMITS_INTERVAL);
|
|
79
|
+
const explicitLimits = Number.isFinite(rawLimits) && rawLimits > 0 ? rawLimits * 1000 : null;
|
|
80
|
+
let limitsInterval = explicitLimits ?? 60_000;
|
|
78
81
|
let lastLimitsAt = 0;
|
|
79
82
|
// Self-update: once at startup, then every USAGEFLEET_UPDATE_INTERVAL seconds
|
|
80
83
|
// (default 6h — a release lands on a device the same day, not the next).
|
|
@@ -112,6 +115,9 @@ async function cmdWatch() {
|
|
|
112
115
|
if (limits) {
|
|
113
116
|
line(note, limitsSummary(limits));
|
|
114
117
|
}
|
|
118
|
+
if (explicitLimits === null) {
|
|
119
|
+
limitsInterval = limits?.source === 'api' ? 300_000 : 60_000;
|
|
120
|
+
}
|
|
115
121
|
}
|
|
116
122
|
}
|
|
117
123
|
catch (error) {
|
|
@@ -248,7 +254,7 @@ function cmdConfig() {
|
|
|
248
254
|
['USAGEFLEET_DESKTOP', 'override the Claude Desktop dir ("off" disables)'],
|
|
249
255
|
['USAGEFLEET_PI', 'override pi session dirs, comma-separated'],
|
|
250
256
|
['USAGEFLEET_INTERVAL', 'watch interval seconds (default 15)'],
|
|
251
|
-
['USAGEFLEET_LIMITS_INTERVAL', 'seconds between limits
|
|
257
|
+
['USAGEFLEET_LIMITS_INTERVAL', 'seconds between limits reports (default 60; 300 on API keys)'],
|
|
252
258
|
['USAGEFLEET_BATCH', 'records per upload (default 100, max 1000)'],
|
|
253
259
|
['USAGEFLEET_NOTIFY', 'desktop notifications (0 disables)'],
|
|
254
260
|
['USAGEFLEET_NOTIFY_THRESHOLDS', 'comma list of % alerts (default 80,95)'],
|
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) {
|
|
@@ -88,13 +86,16 @@ export function maybeNotify(report, cfg = loadNotifyConfig(), log = () => {
|
|
|
88
86
|
const state = readStore(path).notify;
|
|
89
87
|
const five = evaluateWindow(state.fiveHour, report.fiveHourPct, report.fiveHourResetsAt, cfg.thresholds);
|
|
90
88
|
const seven = evaluateWindow(state.sevenDay, report.sevenDayPct, report.sevenDayResetsAt, cfg.thresholds);
|
|
89
|
+
// Readings can carry a decimal; notification copy rounds to whole.
|
|
90
|
+
const fivePct = Math.round(report.fiveHourPct ?? 0);
|
|
91
|
+
const sevenPct = Math.round(report.sevenDayPct ?? 0);
|
|
91
92
|
if (five.fire != null) {
|
|
92
|
-
sendNotification('Claude usage · 5-hour limit', `${
|
|
93
|
-
log('ok', `notified · 5h at ${
|
|
93
|
+
sendNotification('Claude usage · 5-hour limit', `${fivePct}% of your 5-hour limit used${resetSuffix(report.fiveHourResetsAt)}.`, { urgency: urgencyFor(five.fire) });
|
|
94
|
+
log('ok', `notified · 5h at ${fivePct}% · crossed ${five.fire}%`);
|
|
94
95
|
}
|
|
95
96
|
if (seven.fire != null) {
|
|
96
|
-
sendNotification('Claude usage · weekly limit', `${
|
|
97
|
-
log('ok', `notified · weekly at ${
|
|
97
|
+
sendNotification('Claude usage · weekly limit', `${sevenPct}% of your weekly limit used${resetSuffix(report.sevenDayResetsAt)}.`, { urgency: urgencyFor(seven.fire) });
|
|
98
|
+
log('ok', `notified · weekly at ${sevenPct}% · crossed ${seven.fire}%`);
|
|
98
99
|
}
|
|
99
100
|
updateStore(path, store => {
|
|
100
101
|
store.notify = { fiveHour: five.next, sevenDay: seven.next };
|
package/dist/parser.js
CHANGED
|
@@ -9,15 +9,15 @@ function validTimestamp(v) {
|
|
|
9
9
|
}
|
|
10
10
|
return new Date().toISOString();
|
|
11
11
|
}
|
|
12
|
+
/** Cache-write tokens, with the per-TTL breakdown when the log carries it.
|
|
13
|
+
* 5m and 1h writes are priced differently (1.25× vs 2× input), so the server
|
|
14
|
+
* wants the split — null means "the log predates the breakdown", not zero. */
|
|
12
15
|
function cacheCreation(u) {
|
|
13
|
-
if (typeof u.cache_creation_input_tokens === 'number') {
|
|
14
|
-
return u.cache_creation_input_tokens;
|
|
15
|
-
}
|
|
16
16
|
const c = u.cache_creation;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
17
|
+
const five = typeof c?.ephemeral_5m_input_tokens === 'number' ? c.ephemeral_5m_input_tokens : null;
|
|
18
|
+
const oneHour = typeof c?.ephemeral_1h_input_tokens === 'number' ? c.ephemeral_1h_input_tokens : null;
|
|
19
|
+
const total = typeof u.cache_creation_input_tokens === 'number' ? u.cache_creation_input_tokens : (five ?? 0) + (oneHour ?? 0);
|
|
20
|
+
return { five, oneHour, total };
|
|
21
21
|
}
|
|
22
22
|
/** pi agent session line: `{type:"message", id, timestamp, message:{role:"assistant",
|
|
23
23
|
* provider, model, responseId, usage:{input, output, cacheRead, cacheWrite}}}`.
|
|
@@ -47,6 +47,10 @@ function parsePiLine(o, sessionCwd) {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
return {
|
|
50
|
+
// pi's usage line does not say which TTL the cache write used — leave the
|
|
51
|
+
// breakdown unknown so the server prices it by the user's TTL setting.
|
|
52
|
+
cacheCreation1h: null,
|
|
53
|
+
cacheCreation5m: null,
|
|
50
54
|
cacheCreationTokens: u.cacheWrite ?? 0,
|
|
51
55
|
cacheReadTokens: u.cacheRead ?? 0,
|
|
52
56
|
cwd: sessionCwd,
|
|
@@ -109,8 +113,11 @@ export function parseLine(line, source = 'cli', sessionCwd = null) {
|
|
|
109
113
|
return null;
|
|
110
114
|
}
|
|
111
115
|
const u = message.usage;
|
|
116
|
+
const cache = cacheCreation(u);
|
|
112
117
|
return {
|
|
113
|
-
|
|
118
|
+
cacheCreation1h: cache.oneHour,
|
|
119
|
+
cacheCreation5m: cache.five,
|
|
120
|
+
cacheCreationTokens: cache.total,
|
|
114
121
|
cacheReadTokens: u.cache_read_input_tokens ?? 0,
|
|
115
122
|
cwd: str(o.cwd),
|
|
116
123
|
gitBranch: str(o.gitBranch),
|
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";
|
package/dist/service.js
CHANGED
|
@@ -309,27 +309,36 @@ ${envXml}
|
|
|
309
309
|
catch {
|
|
310
310
|
/* not loaded yet — fine */
|
|
311
311
|
}
|
|
312
|
+
let loaded = false;
|
|
312
313
|
try {
|
|
313
314
|
execFileSync('launchctl', ['bootstrap', domain, path], {
|
|
314
315
|
stdio: 'inherit',
|
|
315
316
|
});
|
|
317
|
+
loaded = true;
|
|
316
318
|
}
|
|
317
319
|
catch {
|
|
318
320
|
try {
|
|
319
321
|
execFileSync('launchctl', ['load', path], { stdio: 'inherit' });
|
|
322
|
+
loaded = true;
|
|
320
323
|
}
|
|
321
324
|
catch {
|
|
322
325
|
/* report below; user can load manually */
|
|
323
326
|
}
|
|
324
327
|
}
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
328
|
+
// Only when the (re)load failed — i.e. the old job is still resident, so this
|
|
329
|
+
// is the one thing that can swap it. On a job launchd just started via
|
|
330
|
+
// RunAtLoad, `kickstart -k` kills that instance and the respawn waits out
|
|
331
|
+
// ThrottleInterval: a guaranteed 30s hole where `status` right after an
|
|
332
|
+
// update reads "stopped".
|
|
333
|
+
if (!loaded) {
|
|
334
|
+
try {
|
|
335
|
+
execFileSync('launchctl', ['kickstart', '-k', `${domain}/${LABEL}`], {
|
|
336
|
+
stdio: 'ignore',
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
/* best-effort */
|
|
341
|
+
}
|
|
333
342
|
}
|
|
334
343
|
console.log(step('service', 'launchd · starts at login'));
|
|
335
344
|
console.log(row('logs', tilde(macLogDir())));
|
package/dist/ui.js
CHANGED
|
@@ -86,9 +86,10 @@ export function line(glyph, text) {
|
|
|
86
86
|
}
|
|
87
87
|
/** Neutral stream glyph, for messages that are neither good nor bad news. */
|
|
88
88
|
export const note = dim('·');
|
|
89
|
-
/** Percentage as a fixed-width string, so successive log lines line up.
|
|
89
|
+
/** Percentage as a fixed-width string, so successive log lines line up.
|
|
90
|
+
* Readings can carry a decimal; display rounds to whole. */
|
|
90
91
|
export function pct(value) {
|
|
91
|
-
return `${value
|
|
92
|
+
return `${value === null ? '?' : Math.round(value)}%`.padStart(4);
|
|
92
93
|
}
|
|
93
94
|
/** Usage bar, coloured by how close the window is to its limit.
|
|
94
95
|
* Unknown usage renders as an empty bar rather than a missing column. */
|