@usagefleet/cli 1.2.172 → 1.2.173
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 +20 -6
- package/dist/collector.js +4 -2
- package/dist/index.js +3 -2
- package/dist/notifier.js +46 -18
- package/dist/release.js +1 -1
- package/dist/store.js +1 -0
- package/dist/uploader.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,15 +97,28 @@ directory, git branch. Prompts, responses and file contents are never read.
|
|
|
97
97
|
|
|
98
98
|
## Desktop notifications
|
|
99
99
|
|
|
100
|
-
The first time a window crosses a threshold
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
The first time a window crosses a threshold you get a desktop notification —
|
|
101
|
+
once per threshold per window, re-armed when the window resets or usage falls
|
|
102
|
+
5 points under the last alert. Which thresholds is the alert **mode**:
|
|
103
|
+
|
|
104
|
+
| Mode | Alerts at (% of the window) |
|
|
105
|
+
|------|-----------------------------|
|
|
106
|
+
| `off` | never |
|
|
107
|
+
| `quiet` | 95, 100 |
|
|
108
|
+
| `standard` (default) | 80, 95 |
|
|
109
|
+
| `steady` | 50, 60, 70, 80, 90, 95, 100 |
|
|
110
|
+
| `critical` | 80, 90, 95, 96, 97, 98, 99, then every 0.1 (99.1, 99.2 … 99.9), 100 |
|
|
111
|
+
|
|
112
|
+
Pick one per device on the **Devices** page, or pin it on the machine with
|
|
113
|
+
`USAGEFLEET_NOTIFY_MODE=critical` / `"notifyMode": "critical"` in the config
|
|
114
|
+
file (the local value wins over the Devices page). macOS shows it from a small bundled UsageFleet app (allow it once when
|
|
103
115
|
macOS asks; a click opens the dashboard), Linux uses `notify-send` (falling back to
|
|
104
116
|
`kdialog --passivepopup`; install `libnotify-bin` if neither exists), Windows a
|
|
105
117
|
WinRT toast via `powershell.exe`.
|
|
106
118
|
|
|
107
|
-
`usagefleet notify-test` confirms it works
|
|
108
|
-
`USAGEFLEET_NOTIFY_THRESHOLDS="50,80,
|
|
119
|
+
`usagefleet notify-test` confirms it works and prints the thresholds in effect.
|
|
120
|
+
For a list of your own use `USAGEFLEET_NOTIFY_THRESHOLDS="50,80,99.5"` (it
|
|
121
|
+
replaces the mode's list), disable everything with `USAGEFLEET_NOTIFY=0`.
|
|
109
122
|
|
|
110
123
|
## Blocking prompts over the limit
|
|
111
124
|
|
|
@@ -165,7 +178,8 @@ it (`usagefleet config` prints this same table):
|
|
|
165
178
|
| `interval` | `USAGEFLEET_INTERVAL` | watch poll seconds (default 15) |
|
|
166
179
|
| `limitsInterval` | `USAGEFLEET_LIMITS_INTERVAL` | seconds between limit reports (default 60; 300 on API keys, where each report costs a 1-token ping) |
|
|
167
180
|
| `notifications` | `USAGEFLEET_NOTIFY` | desktop notifications, on by default (`false`/`0` disables) |
|
|
168
|
-
| `
|
|
181
|
+
| `notifyMode` | `USAGEFLEET_NOTIFY_MODE` | alert preset: `off`, `quiet`, `standard`, `steady`, `critical` (default: the Devices page value, else `standard`) |
|
|
182
|
+
| `notifyThresholds` | `USAGEFLEET_NOTIFY_THRESHOLDS` | utilization % that trigger an alert, replacing the mode's list — array in the file, comma list in env, one decimal kept |
|
|
169
183
|
| `batch` | `USAGEFLEET_BATCH` | records per upload (default 100, server caps at 1000) |
|
|
170
184
|
| `update` | `USAGEFLEET_UPDATE` | `false`/`0` turns the self-update check off |
|
|
171
185
|
| `updateInterval` | `USAGEFLEET_UPDATE_INTERVAL` | seconds between update checks (default `21600` = 6h) |
|
package/dist/collector.js
CHANGED
|
@@ -6,7 +6,7 @@ import { detectClaudeCreds, macKeychainDenied } from './claude-creds.js';
|
|
|
6
6
|
import { fetchLimits } from './claude-limits.js';
|
|
7
7
|
import { ENDPOINT } from './config.js';
|
|
8
8
|
import { listJcodeSessions, readJcodeSession } from './jcode.js';
|
|
9
|
-
import { maybeNotify } from './notifier.js';
|
|
9
|
+
import { loadNotifyConfig, maybeNotify } from './notifier.js';
|
|
10
10
|
import { detectOs } from './os.js';
|
|
11
11
|
import { RELEASE_VERSION } from './release.js';
|
|
12
12
|
import { listJsonlFiles } from './scanner.js';
|
|
@@ -461,7 +461,9 @@ async function reportClaudeCodeLimits(cfg, log) {
|
|
|
461
461
|
if (groupLimits) {
|
|
462
462
|
maybeNotify(groupLimits.group === null
|
|
463
463
|
? report
|
|
464
|
-
: { ...report, fiveHourPct: groupLimits.sessionPct, sevenDayPct: groupLimits.weeklyPct },
|
|
464
|
+
: { ...report, fiveHourPct: groupLimits.sessionPct, sevenDayPct: groupLimits.weeklyPct },
|
|
465
|
+
// The device's mode from the Devices page rides along on the same answer.
|
|
466
|
+
loadNotifyConfig(undefined, undefined, groupLimits.notifyMode), log);
|
|
465
467
|
}
|
|
466
468
|
return { ...report, account };
|
|
467
469
|
}
|
package/dist/index.js
CHANGED
|
@@ -166,7 +166,7 @@ async function cmdWatch() {
|
|
|
166
166
|
function cmdNotifyTest() {
|
|
167
167
|
const cfg = loadNotifyConfig();
|
|
168
168
|
if (!cfg.enabled) {
|
|
169
|
-
console.log(warn('notify', 'disabled ·
|
|
169
|
+
console.log(warn('notify', 'disabled · mode off, or USAGEFLEET_NOTIFY=0 / `"notifications": false` · see `usagefleet config`'));
|
|
170
170
|
return;
|
|
171
171
|
}
|
|
172
172
|
sendNotification('usagefleet', 'Test notification — desktop alerts are working.', {
|
|
@@ -294,7 +294,8 @@ function cmdConfig() {
|
|
|
294
294
|
],
|
|
295
295
|
['batch', 'USAGEFLEET_BATCH', 'records per upload (default 100, max 1000)'],
|
|
296
296
|
['notifications', 'USAGEFLEET_NOTIFY', 'desktop notifications (false/0 disables)'],
|
|
297
|
-
['
|
|
297
|
+
['notifyMode', 'USAGEFLEET_NOTIFY_MODE', 'alert preset: off | quiet | standard | steady | critical'],
|
|
298
|
+
['notifyThresholds', 'USAGEFLEET_NOTIFY_THRESHOLDS', '% alerts · array; env comma list (replaces the preset)'],
|
|
298
299
|
['hook', 'USAGEFLEET_HOOK', 'register the prompt guard on install (false/0 skips)'],
|
|
299
300
|
['update', 'USAGEFLEET_UPDATE', 'self-update while watching (false/0 disables)'],
|
|
300
301
|
['updateInterval', 'USAGEFLEET_UPDATE_INTERVAL', 'seconds between update checks (default 21600)'],
|
package/dist/notifier.js
CHANGED
|
@@ -1,17 +1,41 @@
|
|
|
1
1
|
import { flagOff } from './config.js';
|
|
2
2
|
import { sendNotification } from './notify.js';
|
|
3
3
|
import { readStore, storePath, updateStore } from './store.js';
|
|
4
|
-
|
|
4
|
+
/** Thresholds per mode. `critical` thins out near the wall on purpose: the last
|
|
5
|
+
* few points are where a window is actually lost, so they get a tenth-of-a-point
|
|
6
|
+
* step. Mirrored by the Devices page (apps/web/src/lib/notify-modes.ts). */
|
|
7
|
+
export const NOTIFY_MODES = {
|
|
8
|
+
critical: [80, 90, 95, 96, 97, 98, 99, 99.1, 99.2, 99.3, 99.4, 99.5, 99.6, 99.7, 99.8, 99.9, 100],
|
|
9
|
+
off: [],
|
|
10
|
+
quiet: [95, 100],
|
|
11
|
+
standard: [80, 95],
|
|
12
|
+
steady: [50, 60, 70, 80, 90, 95, 100],
|
|
13
|
+
};
|
|
14
|
+
const DEFAULT_MODE = 'standard';
|
|
15
|
+
/** Points under the mark pct must fall before the mark is lowered and re-armed. */
|
|
16
|
+
const REARM_MARGIN = 5;
|
|
17
|
+
/** Utilization % from which an alert is urgent (`critical` on Linux). */
|
|
18
|
+
const DANGER_ZONE = 95;
|
|
19
|
+
/** Case-insensitive; anything that is not a mode name is null, so a typo in one
|
|
20
|
+
* layer falls through to the next instead of silencing every alert. */
|
|
21
|
+
export function parseNotifyMode(value) {
|
|
22
|
+
const mode = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
23
|
+
return Object.hasOwn(NOTIFY_MODES, mode) ? mode : null;
|
|
24
|
+
}
|
|
5
25
|
/** Resolve notify config, env first then the config file. Enabled by default;
|
|
6
26
|
* disable with USAGEFLEET_NOTIFY=0 (also: false/off/no) or `"notifications":
|
|
7
|
-
* false`.
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
27
|
+
* false`. The preset comes from USAGEFLEET_NOTIFY_MODE, the `notifyMode` file
|
|
28
|
+
* key, then `serverMode` (the one set for this device on the Devices page);
|
|
29
|
+
* an explicit list from USAGEFLEET_NOTIFY_THRESHOLDS (comma list, e.g.
|
|
30
|
+
* "50,80,95") or a `notifyThresholds` array replaces the preset's thresholds. */
|
|
31
|
+
export function loadNotifyConfig(env = process.env, file = readStore(), serverMode = null) {
|
|
32
|
+
const mode = parseNotifyMode(env.USAGEFLEET_NOTIFY_MODE) ?? parseNotifyMode(file.notifyMode) ?? serverMode ?? DEFAULT_MODE;
|
|
33
|
+
const enabled = !flagOff(env.USAGEFLEET_NOTIFY, file.notifications) && mode !== 'off';
|
|
11
34
|
const raw = env.USAGEFLEET_NOTIFY_THRESHOLDS;
|
|
12
35
|
const candidates = raw?.trim() ? raw.split(',').map(s => Number(s.trim())) : (file.notifyThresholds ?? []);
|
|
13
|
-
|
|
14
|
-
const
|
|
36
|
+
// One decimal, like the readings themselves: 99.1 must stay 99.1.
|
|
37
|
+
const parsed = candidates.map(n => Math.round(n * 10) / 10).filter(n => Number.isFinite(n) && n > 0 && n <= 100);
|
|
38
|
+
const thresholds = parsed.length > 0 ? [...new Set(parsed)].toSorted((a, b) => a - b) : NOTIFY_MODES[mode];
|
|
15
39
|
return { enabled, thresholds };
|
|
16
40
|
}
|
|
17
41
|
/**
|
|
@@ -20,8 +44,10 @@ export function loadNotifyConfig(env = process.env, file = readStore()) {
|
|
|
20
44
|
*
|
|
21
45
|
* A window rollover (resetsAt moves) resets the high-water mark first, so the
|
|
22
46
|
* first crossing in a new window always re-notifies. If utilization later drops
|
|
23
|
-
* below the mark within the SAME window (e.g. a server correction), the mark
|
|
24
|
-
* lowered so a subsequent re-cross notifies again.
|
|
47
|
+
* well below the mark within the SAME window (e.g. a server correction), the mark
|
|
48
|
+
* is lowered so a subsequent re-cross notifies again. A small dip does not move
|
|
49
|
+
* it: a group's slice is derived and wobbles between readings, and a mark that
|
|
50
|
+
* followed every wobble would re-alert each time pct straddled a threshold.
|
|
25
51
|
*/
|
|
26
52
|
export function evaluateWindow(prev, pct, resetsAt, thresholds) {
|
|
27
53
|
const rolledOver = !prev || !sameWindow(prev.resetsAt, resetsAt);
|
|
@@ -30,7 +56,7 @@ export function evaluateWindow(prev, pct, resetsAt, thresholds) {
|
|
|
30
56
|
const windowResetsAt = rolledOver ? resetsAt : prev.resetsAt;
|
|
31
57
|
if (pct == null) {
|
|
32
58
|
// No reading this cycle — keep the mark, just track the (possibly new) window.
|
|
33
|
-
return { fire: null, next: { lastBucket, resetsAt: windowResetsAt } };
|
|
59
|
+
return { fire: null, urgent: false, next: { lastBucket, resetsAt: windowResetsAt } };
|
|
34
60
|
}
|
|
35
61
|
// Highest threshold the current pct has reached (thresholds are ascending).
|
|
36
62
|
let top = 0;
|
|
@@ -40,10 +66,15 @@ export function evaluateWindow(prev, pct, resetsAt, thresholds) {
|
|
|
40
66
|
}
|
|
41
67
|
}
|
|
42
68
|
if (top > lastBucket) {
|
|
43
|
-
|
|
69
|
+
// `critical` is sticky on most Linux daemons, so only entering the danger zone and
|
|
70
|
+
// the wall get it; a dense tail (99.1, 99.2, ...) must not pile up.
|
|
71
|
+
const urgent = (lastBucket < DANGER_ZONE && top >= DANGER_ZONE) || top >= 100;
|
|
72
|
+
return { fire: top, urgent, next: { lastBucket: top, resetsAt: windowResetsAt } };
|
|
44
73
|
}
|
|
45
|
-
// top <= lastBucket:
|
|
46
|
-
|
|
74
|
+
// top <= lastBucket: nothing fires. The mark holds through a wobble and follows
|
|
75
|
+
// pct down only on a real drop.
|
|
76
|
+
const mark = pct < lastBucket - REARM_MARGIN ? top : lastBucket;
|
|
77
|
+
return { fire: null, urgent: false, next: { lastBucket: mark, resetsAt: windowResetsAt } };
|
|
47
78
|
}
|
|
48
79
|
// oauth/usage jitters resets_at by milliseconds per request; a real rollover moves it by hours.
|
|
49
80
|
function sameWindow(a, b) {
|
|
@@ -71,9 +102,6 @@ function resetSuffix(resetsAt) {
|
|
|
71
102
|
}
|
|
72
103
|
return ` · resets in ${Math.round(h / 24)}d`;
|
|
73
104
|
}
|
|
74
|
-
function urgencyFor(bucket) {
|
|
75
|
-
return bucket >= 95 ? 'critical' : 'normal';
|
|
76
|
-
}
|
|
77
105
|
/**
|
|
78
106
|
* Notify on freshly-crossed 5h/weekly thresholds, deduped across runs via the
|
|
79
107
|
* store's `notify` section. Best-effort and self-contained: it owns its state
|
|
@@ -96,11 +124,11 @@ export function maybeNotify(report, cfg = loadNotifyConfig(), log = () => {
|
|
|
96
124
|
const fivePct = fmt(report.fiveHourPct ?? 0);
|
|
97
125
|
const sevenPct = fmt(report.sevenDayPct ?? 0);
|
|
98
126
|
if (five.fire != null) {
|
|
99
|
-
sendNotification('Claude usage · 5-hour limit', `${fivePct}% of your 5-hour limit used${resetSuffix(report.fiveHourResetsAt)}.`, { urgency:
|
|
127
|
+
sendNotification('Claude usage · 5-hour limit', `${fivePct}% of your 5-hour limit used${resetSuffix(report.fiveHourResetsAt)}.`, { urgency: five.urgent ? 'critical' : 'normal' });
|
|
100
128
|
log('ok', `notified · 5h at ${fivePct}% · crossed ${five.fire}%`);
|
|
101
129
|
}
|
|
102
130
|
if (seven.fire != null) {
|
|
103
|
-
sendNotification('Claude usage · weekly limit', `${sevenPct}% of your weekly limit used${resetSuffix(report.sevenDayResetsAt)}.`, { urgency:
|
|
131
|
+
sendNotification('Claude usage · weekly limit', `${sevenPct}% of your weekly limit used${resetSuffix(report.sevenDayResetsAt)}.`, { urgency: seven.urgent ? 'critical' : 'normal' });
|
|
104
132
|
log('ok', `notified · weekly at ${sevenPct}% · crossed ${seven.fire}%`);
|
|
105
133
|
}
|
|
106
134
|
updateStore(path, store => {
|
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.173";
|
package/dist/store.js
CHANGED
|
@@ -61,6 +61,7 @@ function normalize(raw) {
|
|
|
61
61
|
fiveHour: { ...freshWindow(), ...raw.notify?.fiveHour },
|
|
62
62
|
sevenDay: { ...freshWindow(), ...raw.notify?.sevenDay },
|
|
63
63
|
},
|
|
64
|
+
notifyMode: raw.notifyMode,
|
|
64
65
|
notifyThresholds: raw.notifyThresholds,
|
|
65
66
|
piDir: raw.piDir,
|
|
66
67
|
pjDir: raw.pjDir,
|
package/dist/uploader.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ENDPOINT, tokenFor } from './config.js';
|
|
2
|
+
import { parseNotifyMode } from './notifier.js';
|
|
2
3
|
const MAX_ATTEMPTS = 6;
|
|
3
4
|
const REQUEST_TIMEOUT_MS = 15_000;
|
|
4
5
|
function sleep(ms) {
|
|
@@ -115,7 +116,12 @@ export async function fetchGroupLimits(cfg, accountExtId) {
|
|
|
115
116
|
if (typeof body?.sessionPct !== 'number' || typeof body.weeklyPct !== 'number') {
|
|
116
117
|
return null;
|
|
117
118
|
}
|
|
118
|
-
return {
|
|
119
|
+
return {
|
|
120
|
+
group: body.group ?? null,
|
|
121
|
+
notifyMode: parseNotifyMode(body.notifyMode),
|
|
122
|
+
sessionPct: body.sessionPct,
|
|
123
|
+
weeklyPct: body.weeklyPct,
|
|
124
|
+
};
|
|
119
125
|
}
|
|
120
126
|
catch {
|
|
121
127
|
return null;
|