@hivefloor/cli 0.1.1 → 0.1.3

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.
Files changed (29) hide show
  1. package/dist/{chunk-76HDATOB.js → chunk-3YOTXIFP.js} +13 -13
  2. package/dist/{chunk-Q2TS3GT3.js → chunk-DQV6FAJU.js} +2 -2
  3. package/dist/{chunk-MMWX62ZU.js → chunk-DWHMBUCA.js} +2 -2
  4. package/dist/{chunk-NCRQGIJY.js → chunk-E4V47VEV.js} +3 -9
  5. package/dist/{chunk-WO7NALQO.js → chunk-HT5KXWXM.js} +1 -1
  6. package/dist/{chunk-WLHRUZTR.js → chunk-KHHSRIMU.js} +2 -1
  7. package/dist/{chunk-WHHV2NZA.js → chunk-NKMVO5BP.js} +4 -10
  8. package/dist/{chunk-YCZ7XJJF.js → chunk-NLZXB26Y.js} +2 -2
  9. package/dist/{chunk-SGDM7MAT.js → chunk-O2TC3WSS.js} +68 -2
  10. package/dist/{chunk-U7PMOZGI.js → chunk-O6VSA2JT.js} +4 -3
  11. package/dist/{chunk-3IMV5EHP.js → chunk-T7TG6WA4.js} +1 -1
  12. package/dist/{chunk-ZYRPQPGL.js → chunk-TH4ANHSG.js} +333 -190
  13. package/dist/{chunk-WNDUGQDX.js → chunk-VNCQFFQO.js} +8 -0
  14. package/dist/{chunk-CWEEIRW2.js → chunk-YRNMECUF.js} +1 -1
  15. package/dist/{cli-FFAZRKLR.js → cli-MAHYTCYV.js} +13 -10
  16. package/dist/connect-QA3QIEGL.js +23 -0
  17. package/dist/{disconnect-VWUOUZP6.js → disconnect-FI76IEZJ.js} +10 -10
  18. package/dist/{hook-WSJXG2XU.js → hook-N4ZTQEWM.js} +53 -19
  19. package/dist/index.js +2 -2
  20. package/dist/{link-TFITMC6H.js → link-5WSZHKBE.js} +9 -9
  21. package/dist/{machine-connect-I3OZCTUZ.js → machine-connect-GQJ4SB3D.js} +24 -24
  22. package/dist/{mcp-47WTNQNN.js → mcp-EUBTVJNX.js} +7 -7
  23. package/dist/run-BEE2ATEM.js +1989 -0
  24. package/dist/{status-GXZ267BD.js → status-SMAVPW4D.js} +14 -14
  25. package/dist/{token-C4K23KXH.js → token-AUEAWQDH.js} +4 -4
  26. package/dist/where-7NI7UEEF.js +70 -0
  27. package/package.json +1 -1
  28. package/dist/connect-FM4BXTQQ.js +0 -23
  29. package/dist/run-JO4NEHWG.js +0 -629
@@ -0,0 +1,1989 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ nudgesOf,
4
+ releaseSpool,
5
+ stashNudges,
6
+ takeSpool
7
+ } from "./chunk-O2TC3WSS.js";
8
+ import {
9
+ buildProfiles,
10
+ cliVersionOs,
11
+ makeEnvReader,
12
+ scanProcesses
13
+ } from "./chunk-O6VSA2JT.js";
14
+ import {
15
+ VERSION
16
+ } from "./chunk-KHHSRIMU.js";
17
+ import {
18
+ claudeDirsFromRc,
19
+ profileKeyOf,
20
+ profileKeys,
21
+ uniqueProfileKey
22
+ } from "./chunk-E4V47VEV.js";
23
+ import {
24
+ readMachine,
25
+ writeMachine,
26
+ writeServiceState
27
+ } from "./chunk-T7TG6WA4.js";
28
+ import {
29
+ apiRequest
30
+ } from "./chunk-PY74LT2W.js";
31
+ import {
32
+ configDir,
33
+ home,
34
+ isDir,
35
+ isRecord,
36
+ machineSpoolFile,
37
+ serviceLogFile
38
+ } from "./chunk-VNCQFFQO.js";
39
+ import {
40
+ HOOK_BATCH_MAX,
41
+ MACHINE_HEARTBEAT_SECONDS,
42
+ MACHINE_MIN_INTERVAL_SECONDS,
43
+ machineHeartbeatResponseSchema,
44
+ parseUsageKey,
45
+ runCmd,
46
+ usageReadingSchema
47
+ } from "./chunk-TH4ANHSG.js";
48
+ import "./chunk-K3NQKI34.js";
49
+
50
+ // src/service/run.ts
51
+ import fs2 from "fs";
52
+
53
+ // src/service/collectors.ts
54
+ import fs from "fs";
55
+ import os from "os";
56
+ import path from "path";
57
+ var USAGE_WINDOW_MS = 24 * 60 * 60 * 1e3;
58
+ var USAGE_MAX_FILES = 200;
59
+ var USAGE_MAX_KEYS = 64;
60
+ var CHUNK_BYTES = 64 * 1024;
61
+ var MAX_LINE_BYTES = 2 * 1024 * 1024;
62
+ var MAX_SEEN_IDS = 1e4;
63
+ var SESSION_ID_RE = /^[A-Za-z0-9._:-]{1,60}$/;
64
+ function asNonNegInt(v) {
65
+ if (typeof v !== "number" || !Number.isFinite(v) || v < 0) return void 0;
66
+ return Math.floor(v);
67
+ }
68
+ function asPositiveCost(v) {
69
+ if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) return void 0;
70
+ return v;
71
+ }
72
+ function asShortId(v) {
73
+ return typeof v === "string" && v.length > 0 && v.length <= 120 ? v : void 0;
74
+ }
75
+ function claudeProfileDirs(env, homeDir) {
76
+ const std = path.resolve(path.join(homeDir, ".claude"));
77
+ const raw = [std];
78
+ if (env.CLAUDE_CONFIG_DIR) raw.push(path.resolve(env.CLAUDE_CONFIG_DIR));
79
+ raw.push(...claudeDirsFromRc(homeDir));
80
+ try {
81
+ for (const name of fs.readdirSync(homeDir).sort()) {
82
+ if (!name.startsWith(".claude") || name === ".claude.json" || name.endsWith(".bak")) continue;
83
+ raw.push(path.resolve(path.join(homeDir, name)));
84
+ }
85
+ } catch {
86
+ }
87
+ const xdg = path.resolve(path.join(env.XDG_CONFIG_HOME || path.join(homeDir, ".config"), "claude"));
88
+ raw.push(xdg);
89
+ const unique = [];
90
+ const seen = /* @__PURE__ */ new Set();
91
+ for (const d of raw) {
92
+ if (seen.has(d)) continue;
93
+ if (d !== std && !isDir(path.join(d, "projects"))) continue;
94
+ seen.add(d);
95
+ unique.push(d);
96
+ }
97
+ return profileKeys("claude", unique, homeDir);
98
+ }
99
+ function codexProfileDirs(env, homeDir) {
100
+ const std = path.resolve(path.join(homeDir, ".codex"));
101
+ const raw = [std];
102
+ if (env.CODEX_HOME) raw.push(path.resolve(env.CODEX_HOME));
103
+ try {
104
+ for (const name of fs.readdirSync(homeDir).sort()) {
105
+ if (!name.startsWith(".codex") || name.endsWith(".bak")) continue;
106
+ raw.push(path.resolve(path.join(homeDir, name)));
107
+ }
108
+ } catch {
109
+ }
110
+ const unique = [];
111
+ const seen = /* @__PURE__ */ new Set();
112
+ for (const d of raw) {
113
+ if (seen.has(d)) continue;
114
+ if (d !== std && !isDir(path.join(d, "sessions"))) continue;
115
+ seen.add(d);
116
+ unique.push(d);
117
+ }
118
+ const map = profileKeys("codex", unique, homeDir);
119
+ const multi = path.join(homeDir, "Library", "Application Support", "Multi Codex", "profiles");
120
+ if (isDir(multi)) {
121
+ const used = new Set(map.values());
122
+ try {
123
+ for (const name of fs.readdirSync(multi).sort()) {
124
+ if (name.startsWith(".")) continue;
125
+ const dir = path.resolve(path.join(multi, name, "codex"));
126
+ if (map.has(dir) || !isDir(path.join(dir, "sessions"))) continue;
127
+ const key = uniqueProfileKey(profileKeyOf("codex", name, homeDir), used);
128
+ map.set(dir, key);
129
+ }
130
+ } catch {
131
+ }
132
+ }
133
+ return map;
134
+ }
135
+ function codexSessionFromFile(filePath) {
136
+ const base = path.basename(filePath, ".jsonl");
137
+ const stripped = base.replace(/^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}(?:\.\d+)?-/i, "");
138
+ return { session: stripped, fromRollout: stripped !== base || base.startsWith("rollout") };
139
+ }
140
+ function collectClaudeFiles(dir, profile, cutoffMs, out) {
141
+ const projectsDir = path.join(dir, "projects");
142
+ let projects;
143
+ try {
144
+ projects = fs.readdirSync(projectsDir, { withFileTypes: true });
145
+ } catch {
146
+ return;
147
+ }
148
+ for (const proj of projects) {
149
+ if (!proj.isDirectory()) continue;
150
+ const projPath = path.join(projectsDir, proj.name);
151
+ let entries;
152
+ try {
153
+ entries = fs.readdirSync(projPath, { withFileTypes: true });
154
+ } catch {
155
+ continue;
156
+ }
157
+ for (const ent of entries) {
158
+ if (!ent.isFile() || !ent.name.endsWith(".jsonl")) continue;
159
+ const session = ent.name.slice(0, -6);
160
+ if (!SESSION_ID_RE.test(session)) continue;
161
+ const filePath = path.join(projPath, ent.name);
162
+ try {
163
+ const st = fs.statSync(filePath);
164
+ if (st.mtimeMs < cutoffMs || st.size <= 0) continue;
165
+ out.push({ tool: "claude", profile, session, fromRollout: false, filePath, mtimeMs: st.mtimeMs, size: st.size, ino: st.ino });
166
+ } catch {
167
+ }
168
+ }
169
+ }
170
+ }
171
+ function collectCodexFiles(dir, profile, cutoffMs, out) {
172
+ const walk = (cur, depth) => {
173
+ if (depth > 6) return;
174
+ let entries;
175
+ try {
176
+ entries = fs.readdirSync(cur, { withFileTypes: true });
177
+ } catch {
178
+ return;
179
+ }
180
+ for (const ent of entries) {
181
+ const full = path.join(cur, ent.name);
182
+ if (ent.isDirectory()) {
183
+ walk(full, depth + 1);
184
+ } else if (ent.isFile() && ent.name.endsWith(".jsonl")) {
185
+ const { session, fromRollout } = codexSessionFromFile(ent.name);
186
+ if (!SESSION_ID_RE.test(session)) continue;
187
+ try {
188
+ const st = fs.statSync(full);
189
+ if (st.mtimeMs < cutoffMs || st.size <= 0) continue;
190
+ out.push({ tool: "codex", profile, session, fromRollout, filePath: full, mtimeMs: st.mtimeMs, size: st.size, ino: st.ino });
191
+ } catch {
192
+ }
193
+ }
194
+ }
195
+ };
196
+ walk(path.join(dir, "sessions"), 0);
197
+ }
198
+ function readLinesFromOffset(filePath, startOffset, onLine) {
199
+ const fd = fs.openSync(filePath, "r");
200
+ try {
201
+ const buf = Buffer.allocUnsafe(CHUNK_BYTES);
202
+ let pos = startOffset;
203
+ let carry = Buffer.alloc(0);
204
+ let tooLong = false;
205
+ for (; ; ) {
206
+ const bytesRead = fs.readSync(fd, buf, 0, CHUNK_BYTES, pos);
207
+ if (bytesRead <= 0) break;
208
+ pos += bytesRead;
209
+ const chunk = buf.subarray(0, bytesRead);
210
+ let searchFrom = 0;
211
+ for (; ; ) {
212
+ const nl = chunk.indexOf(10, searchFrom);
213
+ if (nl === -1) {
214
+ const tail = chunk.subarray(searchFrom);
215
+ if (!tooLong) {
216
+ if (carry.length + tail.length > MAX_LINE_BYTES) {
217
+ carry = Buffer.alloc(0);
218
+ tooLong = true;
219
+ } else if (tail.length > 0) {
220
+ carry = carry.length === 0 ? Buffer.from(tail) : Buffer.concat([carry, tail]);
221
+ }
222
+ }
223
+ break;
224
+ }
225
+ const part = chunk.subarray(searchFrom, nl);
226
+ searchFrom = nl + 1;
227
+ if (tooLong) {
228
+ tooLong = false;
229
+ carry = Buffer.alloc(0);
230
+ continue;
231
+ }
232
+ if (carry.length + part.length <= MAX_LINE_BYTES) {
233
+ const line = carry.length === 0 ? part.toString("utf8") : Buffer.concat([carry, part]).toString("utf8");
234
+ carry = Buffer.alloc(0);
235
+ onLine(line);
236
+ } else {
237
+ carry = Buffer.alloc(0);
238
+ }
239
+ }
240
+ }
241
+ if (!tooLong && carry.length > 0) {
242
+ const text = carry.toString("utf8").trim();
243
+ if (text !== "") {
244
+ if (!text.startsWith("{") || !text.endsWith("}") || !onLine(text)) {
245
+ return pos - carry.length;
246
+ }
247
+ }
248
+ }
249
+ return pos;
250
+ } finally {
251
+ try {
252
+ fs.closeSync(fd);
253
+ } catch {
254
+ }
255
+ }
256
+ }
257
+ function parseClaudeLine(state, line) {
258
+ const trimmed = line.trim();
259
+ if (trimmed === "") return true;
260
+ if (trimmed[0] !== "{") return false;
261
+ if (!trimmed.includes('"usage"')) return true;
262
+ let raw;
263
+ try {
264
+ raw = JSON.parse(trimmed);
265
+ } catch {
266
+ return false;
267
+ }
268
+ if (!isRecord(raw)) return true;
269
+ if (raw.type !== void 0 && raw.type !== "assistant") return true;
270
+ const msg = isRecord(raw.message) ? raw.message : void 0;
271
+ if (!msg) return true;
272
+ if (msg.role !== void 0 && msg.role !== "assistant") return true;
273
+ const usage = isRecord(msg.usage) ? msg.usage : void 0;
274
+ if (!usage) return true;
275
+ const msgId = asShortId(msg.id ?? msg.message_id ?? raw.message_id ?? raw.messageId);
276
+ if (msgId) {
277
+ if (state.seenIds.has(msgId)) return true;
278
+ if (state.seenIds.size < MAX_SEEN_IDS) state.seenIds.add(msgId);
279
+ }
280
+ const input = asNonNegInt(usage.input_tokens);
281
+ const cacheCreate = asNonNegInt(usage.cache_creation_input_tokens);
282
+ const cacheRead = asNonNegInt(usage.cache_read_input_tokens);
283
+ const output = asNonNegInt(usage.output_tokens);
284
+ if (input === void 0 && cacheCreate === void 0 && cacheRead === void 0 && output === void 0) return true;
285
+ state.tokensIn += (input ?? 0) + (cacheCreate ?? 0) + (cacheRead ?? 0);
286
+ state.tokensOut += output ?? 0;
287
+ state.hasUsage = true;
288
+ const cost = asPositiveCost(raw.costUSD ?? raw.cost_usd ?? msg.costUSD ?? msg.cost_usd ?? usage.cost_usd ?? usage.costUSD);
289
+ if (cost !== void 0) {
290
+ state.costUsd += cost;
291
+ state.hasCost = true;
292
+ }
293
+ return true;
294
+ }
295
+ function parseCodexLine(state, line) {
296
+ const trimmed = line.trim();
297
+ if (trimmed === "") return true;
298
+ if (trimmed[0] !== "{") return false;
299
+ if (!trimmed.includes("token_count") && !trimmed.includes("session_meta")) return true;
300
+ let raw;
301
+ try {
302
+ raw = JSON.parse(trimmed);
303
+ } catch {
304
+ return false;
305
+ }
306
+ if (!isRecord(raw)) return true;
307
+ if (raw.type === "session_meta" && state.fromRollout && isRecord(raw.payload)) {
308
+ const id = asShortId(raw.payload.id ?? raw.payload.session_id);
309
+ if (id && SESSION_ID_RE.test(id)) state.session = id;
310
+ return true;
311
+ }
312
+ const payload = isRecord(raw.payload) ? raw.payload : isRecord(raw.msg) ? raw.msg : void 0;
313
+ const ev = raw.type === "token_count" || raw.event === "token_count" ? raw : payload && (payload.type === "token_count" || payload.event === "token_count") ? payload : void 0;
314
+ if (!ev) return true;
315
+ const info = isRecord(ev.info) ? ev.info : void 0;
316
+ const total = (info && (isRecord(info.total_token_usage) ? info.total_token_usage : isRecord(info.total_usage) ? info.total_usage : isRecord(info.usage) ? info.usage : void 0)) ?? (isRecord(ev.total_token_usage) ? ev.total_token_usage : isRecord(ev.total_usage) ? ev.total_usage : isRecord(ev.usage) ? ev.usage : info ?? ev);
317
+ const tin = asNonNegInt(total.input_tokens ?? total.tokens_in ?? total.prompt_tokens);
318
+ const tout = asNonNegInt(total.output_tokens ?? total.tokens_out ?? total.completion_tokens);
319
+ if (tin === void 0 && tout === void 0) return true;
320
+ state.tokensIn = tin ?? 0;
321
+ state.tokensOut = tout ?? 0;
322
+ state.hasUsage = true;
323
+ const cost = asPositiveCost(total.cost_usd ?? total.costUSD ?? ev.cost_usd ?? ev.costUSD ?? raw.cost_usd ?? raw.costUSD);
324
+ if (cost !== void 0) {
325
+ state.costUsd = cost;
326
+ state.hasCost = true;
327
+ }
328
+ return true;
329
+ }
330
+ function createUsageCollector(opts = {}) {
331
+ const files = /* @__PURE__ */ new Map();
332
+ return () => {
333
+ try {
334
+ const homeDir = opts.homeDir ?? home();
335
+ const env = opts.env ?? process.env;
336
+ const now = opts.now ? opts.now() : Date.now();
337
+ const windowMs = opts.windowMs ?? USAGE_WINDOW_MS;
338
+ const maxFiles = opts.maxFiles ?? USAGE_MAX_FILES;
339
+ const maxKeys = opts.maxKeys;
340
+ const cutoffMs = now - windowMs;
341
+ const candidates = [];
342
+ for (const [dir, profile] of claudeProfileDirs(env, homeDir)) {
343
+ collectClaudeFiles(dir, profile, cutoffMs, candidates);
344
+ }
345
+ for (const [dir, profile] of codexProfileDirs(env, homeDir)) {
346
+ collectCodexFiles(dir, profile, cutoffMs, candidates);
347
+ }
348
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
349
+ const active = candidates.slice(0, maxFiles);
350
+ const activePaths = new Set(active.map((c) => c.filePath));
351
+ for (const k of files.keys()) {
352
+ if (!activePaths.has(k)) files.delete(k);
353
+ }
354
+ const usage = {};
355
+ const seenSessions = /* @__PURE__ */ new Set();
356
+ let keyCount = 0;
357
+ for (const c of active) {
358
+ let state = files.get(c.filePath);
359
+ if (!state || c.size < state.offset || c.ino !== 0 && state.ino !== 0 && c.ino !== state.ino) {
360
+ state = {
361
+ tool: c.tool,
362
+ profile: c.profile,
363
+ session: c.session,
364
+ fromRollout: c.fromRollout,
365
+ offset: 0,
366
+ size: 0,
367
+ mtimeMs: 0,
368
+ ino: c.ino,
369
+ tokensIn: 0,
370
+ tokensOut: 0,
371
+ costUsd: 0,
372
+ hasCost: false,
373
+ hasUsage: false,
374
+ seenIds: /* @__PURE__ */ new Set()
375
+ };
376
+ files.set(c.filePath, state);
377
+ } else {
378
+ state.profile = c.profile;
379
+ }
380
+ if (c.size > state.size || c.mtimeMs !== state.mtimeMs) {
381
+ try {
382
+ const parser = state.tool === "claude" ? parseClaudeLine : parseCodexLine;
383
+ const st = state;
384
+ state.offset = readLinesFromOffset(c.filePath, state.offset, (line) => parser(st, line));
385
+ state.size = c.size;
386
+ state.mtimeMs = c.mtimeMs;
387
+ } catch {
388
+ continue;
389
+ }
390
+ }
391
+ if (!state.hasUsage) continue;
392
+ const sessionKey = `${state.tool}:${state.profile}:${state.session}`;
393
+ if (seenSessions.has(sessionKey)) continue;
394
+ const base = `${state.tool}:${state.profile}:s:${state.session}`;
395
+ const kIn = `${base}:tokens_in`;
396
+ const kOut = `${base}:tokens_out`;
397
+ const kCost = `${base}:cost_usd`;
398
+ if (kIn.length > 80 || kOut.length > 80 || !parseUsageKey(kIn) || !parseUsageKey(kOut)) continue;
399
+ const includeCost = state.hasCost && kCost.length <= 80 && parseUsageKey(kCost) !== null;
400
+ const needed = includeCost ? 3 : 2;
401
+ if (maxKeys !== void 0 && keyCount + needed > maxKeys) continue;
402
+ seenSessions.add(sessionKey);
403
+ usage[kIn] = state.tokensIn;
404
+ usage[kOut] = state.tokensOut;
405
+ if (includeCost) usage[kCost] = Math.round(state.costUsd * 1e6) / 1e6;
406
+ keyCount += needed;
407
+ }
408
+ return keyCount > 0 ? { usage } : void 0;
409
+ } catch {
410
+ return void 0;
411
+ }
412
+ };
413
+ }
414
+ var basics = () => {
415
+ const total = os.totalmem();
416
+ const free = os.freemem();
417
+ return {
418
+ mem_total_mb: Math.round(total / 1048576),
419
+ mem_pct: total > 0 ? Math.round((total - free) / total * 1e3) / 10 : void 0,
420
+ load1: Math.round(os.loadavg()[0] * 100) / 100
421
+ };
422
+ };
423
+ var usageCounters = createUsageCollector({ maxKeys: USAGE_MAX_KEYS });
424
+ var collectors = [basics, usageCounters];
425
+ async function collectStats(list = collectors) {
426
+ const stats = {};
427
+ for (const c of list) {
428
+ try {
429
+ const part = await Promise.race([Promise.resolve(c()), new Promise((r) => setTimeout(() => r(void 0), 2e3).unref())]);
430
+ if (part) {
431
+ for (const [k, v] of Object.entries(part)) {
432
+ if (v === void 0) continue;
433
+ if (k === "usage" && isRecord(v)) {
434
+ const merged = isRecord(stats.usage) ? { ...stats.usage } : {};
435
+ for (const [uk, uv] of Object.entries(v)) {
436
+ if (typeof uv !== "number" || !Number.isFinite(uv)) continue;
437
+ if (!(uk in merged) && Object.keys(merged).length >= USAGE_MAX_KEYS) break;
438
+ merged[uk] = uv;
439
+ }
440
+ if (Object.keys(merged).length > 0) stats.usage = merged;
441
+ } else {
442
+ stats[k] = v;
443
+ }
444
+ }
445
+ }
446
+ } catch {
447
+ }
448
+ }
449
+ return Object.keys(stats).length > 0 ? stats : void 0;
450
+ }
451
+
452
+ // ../usage/src/machine-stats.ts
453
+ var MB = 1024 * 1024;
454
+ var round1 = (n) => Math.round(n * 10) / 10;
455
+ var pct = (n) => round1(Math.min(100, Math.max(0, n)));
456
+ async function cpuPercent(deps) {
457
+ const a = deps.cpuTimes();
458
+ await deps.sleep(300);
459
+ const b = deps.cpuTimes();
460
+ const total = b.total - a.total;
461
+ return total > 0 ? pct((1 - (b.idle - a.idle) / total) * 100) : 0;
462
+ }
463
+ function parseMeminfo(text) {
464
+ const get = (k) => {
465
+ const m = new RegExp(`^${k}:\\s+(\\d+)\\s*kB`, "m").exec(text);
466
+ return m ? Number(m[1]) : void 0;
467
+ };
468
+ const total = get("MemTotal");
469
+ const avail = get("MemAvailable");
470
+ if (!total || avail === void 0) return void 0;
471
+ return { totalMb: Math.round(total / 1024), usedMb: Math.round((total - avail) / 1024) };
472
+ }
473
+ function parseVmStat(text) {
474
+ const size = Number(/page size of (\d+) bytes/.exec(text)?.[1]);
475
+ const pages = (label) => {
476
+ const m = new RegExp(`^${label}:\\s+(\\d+)`, "m").exec(text);
477
+ return m ? Number(m[1]) : void 0;
478
+ };
479
+ const anon = pages("Pages anonymous");
480
+ const purgeable = pages("Pages purgeable") ?? 0;
481
+ const wired = pages("Pages wired down");
482
+ const compressed = pages("Pages occupied by compressor") ?? 0;
483
+ if (!size || anon === void 0 || wired === void 0) return void 0;
484
+ return { usedMb: Math.round((anon - purgeable + wired + compressed) * size / MB) };
485
+ }
486
+ function parseIoreg(text) {
487
+ const m = /"Device Utilization %"\s*=\s*(\d+)/.exec(text);
488
+ return m ? pct(Number(m[1])) : void 0;
489
+ }
490
+ function parseNvidiaSmi(text) {
491
+ const values = text.split("\n").map((l) => l.split(",").pop()?.trim() ?? "").filter((v) => /^\d+(\.\d+)?$/.test(v)).map(Number);
492
+ return values.length ? pct(Math.max(...values)) : void 0;
493
+ }
494
+ function parseDf(text, only) {
495
+ const out = [];
496
+ const seen = /* @__PURE__ */ new Set();
497
+ for (const line of text.split("\n").slice(1)) {
498
+ const m = /^(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+\S+\s+(\/.*)$/.exec(line.trim());
499
+ if (!m) continue;
500
+ const [, fs3, total, used, , mount] = m;
501
+ if (seen.has(fs3) || only && !only(fs3, mount)) continue;
502
+ seen.add(fs3);
503
+ out.push({ mount: mount.slice(0, 80), used_gb: round1(Number(used) / MB), total_gb: round1(Number(total) / MB) });
504
+ }
505
+ return out.slice(0, 8);
506
+ }
507
+ async function collectMachineStats(deps) {
508
+ const darwin = deps.platform === "darwin";
509
+ const cpu_pct = await cpuPercent(deps);
510
+ let ram_total_mb = Math.round(deps.totalmem() / MB);
511
+ let ram_used_mb = Math.round((deps.totalmem() - deps.freemem()) / MB);
512
+ if (darwin) {
513
+ const vm = deps.exec("vm_stat", []);
514
+ const parsed = vm ? parseVmStat(vm) : void 0;
515
+ if (parsed) ram_used_mb = parsed.usedMb;
516
+ } else {
517
+ const info = deps.readFile("/proc/meminfo");
518
+ const parsed = info ? parseMeminfo(info) : void 0;
519
+ if (parsed) {
520
+ ram_total_mb = parsed.totalMb;
521
+ ram_used_mb = parsed.usedMb;
522
+ }
523
+ }
524
+ ram_used_mb = Math.max(0, Math.min(ram_used_mb, ram_total_mb));
525
+ let gpu_pct;
526
+ if (darwin) {
527
+ const io = deps.exec("ioreg", ["-r", "-d", "1", "-w", "0", "-c", "IOAccelerator"]);
528
+ gpu_pct = io ? parseIoreg(io) : void 0;
529
+ } else {
530
+ const smi = deps.exec("nvidia-smi", ["--query-gpu=utilization.gpu", "--format=csv,noheader,nounits"]);
531
+ gpu_pct = smi ? parseNvidiaSmi(smi) : void 0;
532
+ }
533
+ let disks = [];
534
+ if (darwin) {
535
+ const df = deps.exec("df", ["-kP", "/System/Volumes/Data"]);
536
+ disks = df ? parseDf(df).map((d) => ({ ...d, mount: "/" })) : [];
537
+ } else {
538
+ const df = deps.exec("df", ["-kPl"]);
539
+ disks = df ? parseDf(df, (fs3, mount) => fs3.startsWith("/dev/") && !fs3.startsWith("/dev/loop") && !/^\/(boot|snap|var\/lib\/docker)/.test(mount)) : [];
540
+ }
541
+ const stats = { cpu_pct, ram_used_mb, ram_total_mb, disks };
542
+ if (gpu_pct !== void 0) stats.gpu_pct = gpu_pct;
543
+ return stats;
544
+ }
545
+
546
+ // ../usage/src/readers/claude.ts
547
+ import { createHash as createHash2 } from "crypto";
548
+
549
+ // ../usage/src/util.ts
550
+ import { createHash } from "crypto";
551
+ function emptyOutput() {
552
+ return { readings: [], issues: [] };
553
+ }
554
+ function issue(provider, account_label, code, message) {
555
+ return { provider, account_label, code, message };
556
+ }
557
+ function asNumber(v) {
558
+ if (typeof v === "number" && Number.isFinite(v)) return v;
559
+ if (typeof v === "string" && v.trim() !== "") {
560
+ const n = Number(v);
561
+ if (Number.isFinite(n)) return n;
562
+ }
563
+ return void 0;
564
+ }
565
+ function asRecord(v) {
566
+ return v !== null && typeof v === "object" && !Array.isArray(v) ? v : void 0;
567
+ }
568
+ function asString(v) {
569
+ return typeof v === "string" && v !== "" ? v : void 0;
570
+ }
571
+ function clampPercent(n) {
572
+ return Math.round(Math.min(100, Math.max(0, n)) * 10) / 10;
573
+ }
574
+ function toIso(v) {
575
+ let ms;
576
+ if (typeof v === "number" && Number.isFinite(v)) ms = v < 1e11 ? v * 1e3 : v;
577
+ else if (typeof v === "string" && v.trim() !== "") {
578
+ const n = Number(v);
579
+ ms = Number.isFinite(n) ? n < 1e11 ? n * 1e3 : n : Date.parse(v);
580
+ }
581
+ if (ms === void 0 || !Number.isFinite(ms) || ms <= 0) return void 0;
582
+ const d = new Date(ms);
583
+ return Number.isNaN(d.getTime()) ? void 0 : d.toISOString();
584
+ }
585
+ function windowFromSeconds(seconds) {
586
+ if (seconds === void 0 || seconds <= 0) return "monthly";
587
+ const h = seconds / 3600;
588
+ if (h >= 4.5 && h <= 5.5) return "5h";
589
+ if (h >= 23 && h <= 25) return "daily";
590
+ if (h >= 6 * 24 && h <= 8 * 24) return "weekly";
591
+ if (h >= 27 * 24 && h <= 32 * 24) return "monthly";
592
+ if (h < 48) return `${Math.max(1, Math.round(h))}h`;
593
+ return `${Math.max(1, Math.round(h / 24))}d`;
594
+ }
595
+ function windowLabel(window) {
596
+ switch (window) {
597
+ case "5h":
598
+ return "5 hours";
599
+ case "daily":
600
+ return "Daily";
601
+ case "weekly":
602
+ return "Weekly";
603
+ case "monthly":
604
+ return "Monthly";
605
+ default:
606
+ return window;
607
+ }
608
+ }
609
+ function accountKey(provider, id) {
610
+ return createHash("sha256").update(`${provider}:${id}`).digest("hex").slice(0, 12);
611
+ }
612
+ function decodeJwt(token) {
613
+ const parts = token.split(".");
614
+ if (parts.length < 2 || !parts[1]) return void 0;
615
+ try {
616
+ const json = Buffer.from(parts[1].replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
617
+ return asRecord(JSON.parse(json));
618
+ } catch {
619
+ return void 0;
620
+ }
621
+ }
622
+ function parseJson(text) {
623
+ if (text === null) return void 0;
624
+ try {
625
+ return JSON.parse(text);
626
+ } catch {
627
+ return void 0;
628
+ }
629
+ }
630
+ function joinPath(...parts) {
631
+ return parts.join("/").replace(/\/+/g, "/");
632
+ }
633
+ function profileName(dir, home2, base) {
634
+ const norm = dir.replace(/\/+$/, "");
635
+ const name = norm.split("/").pop() ?? "";
636
+ if (norm === joinPath(home2, base)) return "default";
637
+ const m = new RegExp(`^\\.?${base.replace(".", "")}[-_.]?(.*)$`).exec(name);
638
+ const rest = m?.[1];
639
+ const label = (rest && rest.length > 0 ? rest : name.replace(/^\./, "")).replace(/[^A-Za-z0-9._ -]/g, "").slice(0, 40);
640
+ return label === "" ? "default" : label;
641
+ }
642
+ function slug(text) {
643
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24);
644
+ }
645
+ function addWin(list, win, replace = false) {
646
+ const n = asNumber(win.percent);
647
+ if (n === void 0 || n < 0) return;
648
+ const next = { window: win.window, percent: clampPercent(n) };
649
+ if (win.label) next.label = win.label.slice(0, 60);
650
+ const resets = toIso(win.resets);
651
+ if (resets) next.resets_at = resets;
652
+ const i = list.findIndex((w) => w.window === win.window);
653
+ if (i >= 0) {
654
+ if (replace) list[i] = next;
655
+ return;
656
+ }
657
+ list.push(next);
658
+ }
659
+ function toReadings(provider, accountLabel, parsed, source) {
660
+ const label = accountLabel.slice(0, 60) || "default";
661
+ const key = parsed.accountId ? accountKey(provider, parsed.accountId) : void 0;
662
+ return parsed.windows.map((w) => {
663
+ const r = { provider, account_label: label, window: w.window, percent_used: w.percent, source };
664
+ if (key) r.account_key = key;
665
+ if (w.label) r.label = w.label;
666
+ if (w.resets_at) r.resets_at = w.resets_at;
667
+ if (parsed.plan) r.plan = parsed.plan.slice(0, 40);
668
+ return r;
669
+ });
670
+ }
671
+ function titleCase(text) {
672
+ return text.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (c) => c.toUpperCase());
673
+ }
674
+ function pick(r, ...keys) {
675
+ if (!r) return void 0;
676
+ for (const k of keys) if (r[k] !== void 0 && r[k] !== null) return r[k];
677
+ return void 0;
678
+ }
679
+
680
+ // ../usage/src/http.ts
681
+ var ReaderError = class extends Error {
682
+ constructor(code, message) {
683
+ super(message);
684
+ this.code = code;
685
+ this.name = "ReaderError";
686
+ }
687
+ code;
688
+ };
689
+ var MESSAGES = {
690
+ no_login: "No login found on this machine.",
691
+ expired: "The login on this machine has expired. Sign in again.",
692
+ rejected: "The provider rejected the login. Sign in again.",
693
+ rate_limited: "The provider asked us to wait. Try again later.",
694
+ unreachable: "Could not reach the provider.",
695
+ bad_response: "The provider sent a response we could not read."
696
+ };
697
+ function issueMessage(code) {
698
+ return MESSAGES[code];
699
+ }
700
+ async function requestJson(deps, url, init = {}) {
701
+ let res;
702
+ let text;
703
+ try {
704
+ res = await deps.fetch(url, init);
705
+ text = await res.text();
706
+ } catch {
707
+ throw new ReaderError("unreachable", MESSAGES.unreachable);
708
+ }
709
+ if (res.status === 401 || res.status === 403) throw new ReaderError("rejected", MESSAGES.rejected);
710
+ if (res.status === 429) throw new ReaderError("rate_limited", MESSAGES.rate_limited);
711
+ if (res.status >= 500) throw new ReaderError("unreachable", MESSAGES.unreachable);
712
+ if (res.status < 200 || res.status >= 300) throw new ReaderError("bad_response", MESSAGES.bad_response);
713
+ if (/^\s*</.test(text)) throw new ReaderError("bad_response", MESSAGES.bad_response);
714
+ const body = parseJson(text);
715
+ if (body === void 0) throw new ReaderError("bad_response", MESSAGES.bad_response);
716
+ return body;
717
+ }
718
+ function failure(provider, account_label, e) {
719
+ const code = e instanceof ReaderError ? e.code : "bad_response";
720
+ return { provider, account_label, code, message: MESSAGES[code] };
721
+ }
722
+ async function firstOk(hosts, run) {
723
+ const rank = { rejected: 5, expired: 5, rate_limited: 4, bad_response: 3, unreachable: 2, no_login: 1 };
724
+ let best;
725
+ for (const host of hosts) {
726
+ try {
727
+ return await run(host);
728
+ } catch (e) {
729
+ const err = e instanceof ReaderError ? e : new ReaderError("bad_response", MESSAGES.bad_response);
730
+ if (!best || rank[err.code] > rank[best.code]) best = err;
731
+ }
732
+ }
733
+ throw best ?? new ReaderError("unreachable", MESSAGES.unreachable);
734
+ }
735
+
736
+ // ../usage/src/readers/claude.ts
737
+ var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
738
+ var PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
739
+ var SERVICE = "Claude Code-credentials";
740
+ var RC_FILES = [".zshrc", ".zprofile", ".bashrc", ".bash_profile", ".profile", ".config/fish/config.fish"];
741
+ function normalizeDir(dir) {
742
+ const d = dir.trim();
743
+ if (!d.startsWith("/")) return d.replace(/\/+$/, "");
744
+ const out = [];
745
+ for (const seg of d.split("/")) {
746
+ if (seg === "" || seg === ".") continue;
747
+ if (seg === "..") {
748
+ out.pop();
749
+ continue;
750
+ }
751
+ out.push(seg);
752
+ }
753
+ return "/" + out.join("/");
754
+ }
755
+ function claudeConfigDirs(deps) {
756
+ const dirs = [];
757
+ const add = (d) => {
758
+ if (!d) return;
759
+ const norm = normalizeDir(d);
760
+ if (norm && !dirs.includes(norm)) dirs.push(norm);
761
+ };
762
+ add(deps.env.CLAUDE_CONFIG_DIR);
763
+ add(joinPath(deps.home, ".claude"));
764
+ for (const name of deps.readDir(deps.home).sort()) {
765
+ if (/^\.claude-[A-Za-z0-9._-]+$/.test(name)) add(joinPath(deps.home, name));
766
+ }
767
+ add(joinPath(deps.env.XDG_CONFIG_HOME ?? joinPath(deps.home, ".config"), "claude"));
768
+ for (const rc of RC_FILES) {
769
+ const text = deps.readFile(joinPath(deps.home, rc));
770
+ if (!text) continue;
771
+ for (const line of text.split("\n")) {
772
+ if (/^\s*#/.test(line)) continue;
773
+ for (const m of line.matchAll(/CLAUDE_CONFIG_DIR=(?:"([^"\n]+)"|'([^'\n]+)'|([^\s"';|&]+))/g)) {
774
+ const raw = m[1] ?? m[2] ?? m[3] ?? "";
775
+ const expanded = raw.replace(/^~(?=\/|$)/, deps.home).replace(/^\$\{?HOME\}?/, deps.home);
776
+ if (expanded.startsWith("/") && !expanded.includes("$")) add(expanded);
777
+ }
778
+ }
779
+ }
780
+ return dirs;
781
+ }
782
+ function parseClaudeCreds(raw, now) {
783
+ const auth = asRecord(asRecord(parseJson(raw))?.claudeAiOauth);
784
+ const token = asString(auth?.accessToken);
785
+ if (!auth || !token) return { kind: "none" };
786
+ const scopes = Array.isArray(auth.scopes) ? auth.scopes.filter((s) => typeof s === "string") : String(auth.scopes ?? "").split(/\s+/).filter(Boolean);
787
+ if (scopes.length > 0 && !scopes.includes("user:profile")) return { kind: "no_scope" };
788
+ const exp = asNumber(auth.expiresAt);
789
+ if (exp !== void 0 && exp > 0 && exp <= now) return { kind: "expired" };
790
+ return { kind: "ok", token, plan: planName(asString(auth.subscriptionType)) };
791
+ }
792
+ function planName(raw) {
793
+ const v = (raw ?? "").toLowerCase();
794
+ if (v.includes("enterprise")) return "Enterprise";
795
+ if (v.includes("team")) return "Team";
796
+ if (v.includes("max")) return "Max";
797
+ if (v.includes("pro")) return "Pro";
798
+ if (v.includes("free")) return "Free";
799
+ return void 0;
800
+ }
801
+ function resetOf(b) {
802
+ return b.resets_at ?? b.resetsAt;
803
+ }
804
+ function parseClaudeUsage(json) {
805
+ const src = asRecord(json) ?? {};
806
+ const windows = [];
807
+ const five = asRecord(src.five_hour ?? src.fiveHour);
808
+ if (five) addWin(windows, { window: "5h", label: "5 hours", percent: five.utilization ?? five.used_percent, resets: resetOf(five) });
809
+ const week = asRecord(src.seven_day ?? src.sevenDay);
810
+ if (week) addWin(windows, { window: "weekly", label: "Weekly", percent: week.utilization ?? week.used_percent, resets: resetOf(week) });
811
+ const named = [
812
+ ["seven_day_sonnet", "weekly:sonnet", "Sonnet weekly"],
813
+ ["seven_day_opus", "weekly:opus", "Opus weekly"],
814
+ ["seven_day_oauth_apps", "weekly:apps", "Apps weekly"],
815
+ ["seven_day_cowork", "weekly:cowork", "Cowork weekly"]
816
+ ];
817
+ for (const [key, window, label] of named) {
818
+ const b = asRecord(src[key]);
819
+ if (!b || asNumber(b.utilization) === void 0) continue;
820
+ if (b.utilization === 0 && !resetOf(b)) continue;
821
+ addWin(windows, { window, label, percent: b.utilization, resets: resetOf(b) });
822
+ }
823
+ const limits = Array.isArray(src.limits) ? src.limits : Array.isArray(asRecord(src.rate_limits)?.limits) ? asRecord(src.rate_limits)?.limits : [];
824
+ for (const raw of limits) {
825
+ const item = asRecord(raw);
826
+ if (!item) continue;
827
+ const kind = String(item.kind ?? item.group ?? "").toLowerCase();
828
+ const model = asString(asRecord(asRecord(item.scope)?.model)?.display_name);
829
+ let window;
830
+ let label;
831
+ if (kind === "session" || kind === "five_hour") {
832
+ window = "5h";
833
+ label = "5 hours";
834
+ } else if (model) {
835
+ window = `weekly:${slug(model)}`;
836
+ label = `${model} weekly`;
837
+ } else if (["weekly", "weekly_all", "seven_day", "weekly_scoped"].includes(kind)) {
838
+ window = "weekly";
839
+ label = "Weekly";
840
+ } else {
841
+ continue;
842
+ }
843
+ addWin(windows, { window, label, percent: item.percent ?? item.used_percent ?? item.usedPercent ?? item.utilization, resets: resetOf(item) }, item.is_active === true);
844
+ }
845
+ const extra = asRecord(src.extra_usage ?? src.extraUsage);
846
+ if (extra) {
847
+ const used = asNumber(extra.used_credits);
848
+ const limit = asNumber(extra.monthly_limit);
849
+ const pct2 = asNumber(extra.utilization) ?? (used !== void 0 && limit ? used / limit * 100 : void 0);
850
+ if (pct2 !== void 0 && (extra.is_enabled === true || limit !== void 0)) addWin(windows, { window: "monthly:extra", label: "Extra usage", percent: pct2 });
851
+ }
852
+ return { windows };
853
+ }
854
+ function keychainService(dir, home2) {
855
+ if (dir === joinPath(home2, ".claude")) return SERVICE;
856
+ return `${SERVICE}-${createHash2("sha256").update(dir).digest("hex").slice(0, 8)}`;
857
+ }
858
+ var readClaude = async (deps) => {
859
+ const out = emptyOutput();
860
+ const seenTokens = /* @__PURE__ */ new Set();
861
+ const seenLabels = /* @__PURE__ */ new Set();
862
+ const readLabels = /* @__PURE__ */ new Set();
863
+ for (const dir of claudeConfigDirs(deps)) {
864
+ let raw = deps.readFile(joinPath(dir, ".credentials.json"));
865
+ if (raw === null && deps.platform === "darwin" && deps.exec) {
866
+ raw = deps.exec("security", ["find-generic-password", "-s", keychainService(dir, deps.home), "-w"]);
867
+ }
868
+ if (raw === null) continue;
869
+ const base = profileName(dir, deps.home, ".claude").slice(0, 60);
870
+ let label = base;
871
+ for (let n = 2; seenLabels.has(label); n++) {
872
+ const suffix = `-${n}`;
873
+ label = `${base.slice(0, 60 - suffix.length)}${suffix}`;
874
+ }
875
+ const creds = parseClaudeCreds(raw, deps.now());
876
+ if (creds.kind === "none") continue;
877
+ seenLabels.add(label);
878
+ if (creds.kind === "expired") {
879
+ out.issues.push(issue("claude", label, "expired", "The saved login is out of date (no Claude session used it lately). It updates the next time Claude Code runs on this profile."));
880
+ continue;
881
+ }
882
+ if (creds.kind === "no_scope") {
883
+ out.issues.push(issue("claude", label, "rejected", "This login cannot show usage (no profile scope). Sign in with the browser."));
884
+ continue;
885
+ }
886
+ if (seenTokens.has(creds.token)) continue;
887
+ seenTokens.add(creds.token);
888
+ try {
889
+ const parsed = await readTokenUsage(deps, creds.token);
890
+ parsed.plan ??= creds.plan;
891
+ out.readings.push(...toReadings("claude", label, parsed, "local_login"));
892
+ readLabels.add(label);
893
+ } catch (e) {
894
+ out.issues.push(failure("claude", label, e));
895
+ }
896
+ }
897
+ await readNamedTokens(deps, seenTokens, readLabels, out);
898
+ await readEnvTokens(deps, seenTokens, out);
899
+ return out;
900
+ };
901
+ var CLAUDE_HEADERS = (token) => ({
902
+ Authorization: `Bearer ${token}`,
903
+ "anthropic-beta": "oauth-2025-04-20",
904
+ "anthropic-version": "2023-06-01",
905
+ Accept: "application/json"
906
+ });
907
+ async function readTokenUsage(deps, token) {
908
+ const headers = CLAUDE_HEADERS(token);
909
+ let parsed;
910
+ try {
911
+ parsed = parseClaudeUsage(await requestJson(deps, USAGE_URL, { headers }));
912
+ if (parsed.windows.length === 0) throw new ReaderError("bad_response", issueMessage("bad_response"));
913
+ } catch (e) {
914
+ if (!(e instanceof ReaderError) || e.code !== "rejected" && e.code !== "rate_limited") throw e;
915
+ return probeRateLimits(deps, token);
916
+ }
917
+ try {
918
+ const profile = asRecord(await requestJson(deps, PROFILE_URL, { headers }));
919
+ const account = asRecord(profile?.account) ?? profile;
920
+ parsed.accountId = asString(account?.uuid) ?? asString(account?.id);
921
+ const org = asRecord(profile?.organization);
922
+ parsed.plan ??= planName(asString(org?.organization_type) ?? asString(org?.rate_limit_tier));
923
+ } catch {
924
+ }
925
+ return parsed;
926
+ }
927
+ var PROBE_URL = "https://api.anthropic.com/v1/messages";
928
+ var PROBE_MODEL = "claude-haiku-5-5";
929
+ var PROBE_TTL_MS = 15 * 6e4;
930
+ var probeCache = /* @__PURE__ */ new Map();
931
+ function parseClaudeRateHeaders(h) {
932
+ const windows = [];
933
+ const names = {
934
+ "5h": ["5h", "5 hours"],
935
+ "7d": ["weekly", "Weekly"],
936
+ "7d_opus": ["weekly:opus", "Opus weekly"],
937
+ "7d_sonnet": ["weekly:sonnet", "Sonnet weekly"]
938
+ };
939
+ for (const [key, value] of Object.entries(h)) {
940
+ const m = /^anthropic-ratelimit-unified-([a-z0-9_]+)-utilization$/.exec(key.toLowerCase());
941
+ if (!m) continue;
942
+ const id = m[1];
943
+ const util = asNumber(Number(value));
944
+ if (util === void 0) continue;
945
+ const [window, label] = names[id] ?? (/^7d_[a-z0-9]+$/.test(id) ? [`weekly:${slug(id.slice(3))}`, `${id.slice(3)} weekly`] : [`${slug(id)}`, id]);
946
+ if (!/^[a-z0-9]+(?::[a-z0-9._-]+)?$/.test(window)) continue;
947
+ addWin(windows, { window, label, percent: util * 100, resets: h[`anthropic-ratelimit-unified-${id}-reset`] });
948
+ }
949
+ windows.sort((a, b) => a.window === "5h" ? -1 : b.window === "5h" ? 1 : a.window.localeCompare(b.window));
950
+ return { windows };
951
+ }
952
+ async function probeRateLimits(deps, token) {
953
+ const key = tokenLabel(token);
954
+ const cached = probeCache.get(key);
955
+ if (cached && deps.now() - cached.at < PROBE_TTL_MS) return { ...cached.parsed, windows: [...cached.parsed.windows] };
956
+ const model = /^[a-z0-9.-]{3,60}$/.test(deps.env.HIVEFLOOR_CLAUDE_PROBE_MODEL ?? "") ? deps.env.HIVEFLOOR_CLAUDE_PROBE_MODEL : PROBE_MODEL;
957
+ let res;
958
+ try {
959
+ res = await deps.fetch(PROBE_URL, {
960
+ method: "POST",
961
+ headers: { ...CLAUDE_HEADERS(token), "content-type": "application/json" },
962
+ body: JSON.stringify({
963
+ model,
964
+ max_tokens: 1,
965
+ system: "You are Claude Code, Anthropic's official CLI for Claude.",
966
+ messages: [{ role: "user", content: "hi" }]
967
+ })
968
+ });
969
+ await res.text();
970
+ } catch {
971
+ throw new ReaderError("unreachable", issueMessage("unreachable"));
972
+ }
973
+ const parsed = parseClaudeRateHeaders(res.headers ?? {});
974
+ if (parsed.windows.length > 0) {
975
+ probeCache.set(key, { at: deps.now(), parsed });
976
+ return parsed;
977
+ }
978
+ if (res.status === 401 || res.status === 403) throw new ReaderError("rejected", issueMessage("rejected"));
979
+ if (res.status === 429) throw new ReaderError("rate_limited", issueMessage("rate_limited"));
980
+ if (res.status >= 500) throw new ReaderError("unreachable", issueMessage("unreachable"));
981
+ throw new ReaderError("bad_response", issueMessage("bad_response"));
982
+ }
983
+ var MAX_SESSION_TOKENS = 20;
984
+ var NAME_RE = /^[A-Za-z0-9._-]{1,40}$/;
985
+ var tokenLabel = (token) => `token-${createHash2("sha256").update(token).digest("hex").slice(0, 6)}`;
986
+ function claudeTokenProfiles(deps) {
987
+ const names = /* @__PURE__ */ new Set();
988
+ const add = (n) => {
989
+ if (NAME_RE.test(n) && n !== "api") names.add(n);
990
+ };
991
+ for (const f of deps.readDir(joinPath(deps.home, ".config/fleet-cc")).sort()) {
992
+ const m = /^(.+)\.token$/.exec(f);
993
+ if (m) add(m[1]);
994
+ }
995
+ for (const d of deps.readDir(deps.home).sort()) {
996
+ const m = /^\.claude-([A-Za-z0-9._-]+)$/.exec(d);
997
+ if (m) add(m[1]);
998
+ }
999
+ const cc = deps.readFile(joinPath(deps.home, ".claude/fleet/cc.sh"));
1000
+ const line = cc ? /^PROFILES="([^"\n]*)"/m.exec(cc) : null;
1001
+ for (const n of (line?.[1] ?? "").split(/\s+/)) if (n) add(n);
1002
+ for (const n of (deps.env.HIVEFLOOR_CLAUDE_PROFILES ?? "").split(/[\s,]+/)) if (n) add(n);
1003
+ return [...names];
1004
+ }
1005
+ function namedToken(deps, name) {
1006
+ let t = deps.readFile(joinPath(deps.home, `.config/fleet-cc/${name}.token`));
1007
+ if ((t === null || t.trim() === "") && deps.platform === "darwin" && deps.exec) {
1008
+ t = deps.exec("security", ["find-generic-password", "-s", `fleet-cc-${name}`, "-w"]);
1009
+ }
1010
+ const v = t?.trim() ?? "";
1011
+ return /^[\w.~+/=-]{20,400}$/.test(v) ? v : null;
1012
+ }
1013
+ async function readNamedTokens(deps, seenTokens, readLabels, out) {
1014
+ for (const name of claudeTokenProfiles(deps)) {
1015
+ if (readLabels.has(name)) continue;
1016
+ const token = namedToken(deps, name);
1017
+ if (!token || seenTokens.has(token)) continue;
1018
+ seenTokens.add(token);
1019
+ try {
1020
+ const parsed = await readTokenUsage(deps, token);
1021
+ out.readings.push(...toReadings("claude", name, parsed, "local_login"));
1022
+ readLabels.add(name);
1023
+ out.issues = out.issues.filter((i) => !(i.provider === "claude" && i.account_label === name));
1024
+ } catch (e) {
1025
+ if (!out.issues.some((i) => i.provider === "claude" && i.account_label === name)) out.issues.push(failure("claude", name, e));
1026
+ }
1027
+ }
1028
+ }
1029
+ async function readEnvTokens(deps, seenTokens, out) {
1030
+ const tokens = [...new Set(deps.claudeTokens ?? [])].filter((t) => t.length >= 20 && !seenTokens.has(t)).slice(0, MAX_SESSION_TOKENS);
1031
+ let blind = 0;
1032
+ for (const token of tokens) {
1033
+ seenTokens.add(token);
1034
+ const label = tokenLabel(token);
1035
+ try {
1036
+ const parsed = await readTokenUsage(deps, token);
1037
+ out.readings.push(...toReadings("claude", label, parsed, "env_key"));
1038
+ } catch (e) {
1039
+ if (e instanceof ReaderError && e.code === "rejected") blind++;
1040
+ else out.issues.push(failure("claude", label, e));
1041
+ }
1042
+ }
1043
+ if (blind > 0) {
1044
+ out.issues.push(
1045
+ issue(
1046
+ "claude",
1047
+ "session tokens",
1048
+ "rejected",
1049
+ `${blind} session token${blind === 1 ? "" : "s"} (CLAUDE_CODE_OAUTH_TOKEN) cannot read usage, so those accounts can't be told apart here. Log each in with /login.`
1050
+ )
1051
+ );
1052
+ }
1053
+ }
1054
+
1055
+ // ../usage/src/readers/codex.ts
1056
+ var USAGE_URL2 = "https://chatgpt.com/backend-api/wham/usage";
1057
+ function codexDirs(deps) {
1058
+ const dirs = [];
1059
+ const add = (dir, label) => {
1060
+ if (dir && !dirs.some((d) => d.dir === dir)) dirs.push({ dir: dir.replace(/\/+$/, ""), label });
1061
+ };
1062
+ add(deps.env.CODEX_HOME, "default");
1063
+ add(joinPath(deps.home, ".codex"), "default");
1064
+ if (deps.platform === "darwin") {
1065
+ const profiles = joinPath(deps.home, "Library", "Application Support", "Multi Codex", "profiles");
1066
+ for (const name of deps.readDir(profiles).sort()) {
1067
+ if (!name.startsWith(".")) add(joinPath(profiles, name, "codex"), name.slice(0, 40));
1068
+ }
1069
+ }
1070
+ return dirs;
1071
+ }
1072
+ function parseCodexCreds(raw, now) {
1073
+ const data = asRecord(parseJson(raw));
1074
+ if (!data) return { kind: "none" };
1075
+ const tokens = asRecord(data.tokens) ?? data;
1076
+ const token = asString(tokens.access_token) ?? asString(data.access_token);
1077
+ if (!token) return asString(data.OPENAI_API_KEY) ? { kind: "api_key" } : { kind: "none" };
1078
+ const payload = decodeJwt(token);
1079
+ const exp = asNumber(payload?.exp);
1080
+ if (exp !== void 0 && exp > 0 && exp * 1e3 <= now) return { kind: "expired" };
1081
+ const auth = asRecord(payload?.["https://api.openai.com/auth"]);
1082
+ return { kind: "ok", token, accountId: asString(tokens.account_id) ?? asString(data.account_id) ?? asString(auth?.chatgpt_account_id) };
1083
+ }
1084
+ function windowOf(slot, fallback, suffix, resetsFallback) {
1085
+ const w = asRecord(slot);
1086
+ if (!w) return void 0;
1087
+ const seconds = asNumber(w.limit_window_seconds ?? w.limitWindowSeconds ?? w.window_seconds);
1088
+ const base = seconds === void 0 ? fallback : windowFromSeconds(seconds);
1089
+ return {
1090
+ window: suffix ? `${base}:${suffix.id}` : base,
1091
+ label: suffix ? `${suffix.name} ${windowLabel(base)}` : windowLabel(base),
1092
+ percent: w.used_percent ?? w.usedPercent,
1093
+ resets: w.reset_at ?? w.resetAt ?? w.resets_at ?? w.resetsAt ?? resetsFallback
1094
+ };
1095
+ }
1096
+ function parseCodexUsage(json) {
1097
+ const src = asRecord(json) ?? {};
1098
+ const rate = asRecord(src.rate_limit ?? src.rateLimit) ?? src;
1099
+ const windows = [];
1100
+ const addRate = (r, suffix) => {
1101
+ for (const [slot, fallback] of [["primary", "5h"], ["secondary", "weekly"]]) {
1102
+ const w = windowOf(r[`${slot}_window`] ?? r[`${slot}Window`] ?? r[slot], fallback, suffix);
1103
+ if (w) addWin(windows, w);
1104
+ }
1105
+ };
1106
+ addRate(rate);
1107
+ const review = asRecord(src.code_review_rate_limit ?? src.codeReviewRateLimit);
1108
+ if (review) addRate(review, { id: "code-review", name: "Code review" });
1109
+ const more = src.additional_rate_limits ?? src.additionalRateLimits;
1110
+ if (Array.isArray(more)) {
1111
+ for (const raw of more) {
1112
+ const item = asRecord(raw);
1113
+ const name = asString(item?.limit_name ?? item?.limitName ?? item?.metered_feature ?? item?.meteredFeature);
1114
+ const r = asRecord(item?.rate_limit ?? item?.rateLimit);
1115
+ if (name && r) addRate(r, { id: slug(name) || "extra", name });
1116
+ }
1117
+ }
1118
+ const plan = asString(src.plan_type ?? src.planType ?? rate.plan_type);
1119
+ return { windows, plan: plan ? titleCase(plan) : void 0 };
1120
+ }
1121
+ var readCodex = async (deps) => {
1122
+ const out = emptyOutput();
1123
+ const seen = /* @__PURE__ */ new Set();
1124
+ for (const { dir, label } of codexDirs(deps)) {
1125
+ const raw = deps.readFile(joinPath(dir, "auth.json"));
1126
+ if (raw === null) continue;
1127
+ const creds = parseCodexCreds(raw, deps.now());
1128
+ if (creds.kind === "none") continue;
1129
+ if (creds.kind === "expired") {
1130
+ out.issues.push(issue("codex", label, "expired", issueMessage("expired")));
1131
+ continue;
1132
+ }
1133
+ if (creds.kind === "api_key") {
1134
+ out.issues.push(issue("codex", label, "no_login", "Signed in with an API key, which has no plan usage to show."));
1135
+ continue;
1136
+ }
1137
+ const id = creds.accountId ?? creds.token;
1138
+ if (seen.has(id)) continue;
1139
+ seen.add(id);
1140
+ const headers = { Authorization: `Bearer ${creds.token}`, Accept: "application/json" };
1141
+ if (creds.accountId) headers["ChatGPT-Account-Id"] = creds.accountId;
1142
+ try {
1143
+ const parsed = parseCodexUsage(await requestJson(deps, USAGE_URL2, { headers }));
1144
+ if (parsed.windows.length === 0) throw new ReaderError("bad_response", issueMessage("bad_response"));
1145
+ if (creds.accountId) parsed.accountId = creds.accountId;
1146
+ out.readings.push(...toReadings("codex", label, parsed, "local_login"));
1147
+ } catch (e) {
1148
+ out.issues.push(failure("codex", label, e));
1149
+ }
1150
+ }
1151
+ return out;
1152
+ };
1153
+
1154
+ // ../usage/src/readers/copilot.ts
1155
+ var URL_ = "https://api.github.com/copilot_internal/user";
1156
+ var QUALIFIER = {
1157
+ premium_interactions: ["monthly", "Premium requests"],
1158
+ chat: ["monthly:chat", "Chat"],
1159
+ completions: ["monthly:completions", "Completions"]
1160
+ };
1161
+ function findCopilotToken(files) {
1162
+ for (const raw of files) {
1163
+ const data = asRecord(parseJson(raw));
1164
+ if (!data) continue;
1165
+ for (const [key, value] of Object.entries(data)) {
1166
+ if (!/github\.com/.test(key)) continue;
1167
+ const token = asString(asRecord(value)?.oauth_token);
1168
+ if (token) return token;
1169
+ }
1170
+ }
1171
+ return void 0;
1172
+ }
1173
+ function parseCopilotUsage(json) {
1174
+ const src = asRecord(json) ?? {};
1175
+ const snaps = asRecord(src.quota_snapshots ?? src.quotaSnapshots) ?? {};
1176
+ const resets = src.quota_reset_date_utc ?? src.quotaResetDateUtc ?? src.quota_reset_date ?? src.quotaResetDate;
1177
+ const windows = [];
1178
+ for (const [key, raw] of Object.entries(snaps)) {
1179
+ const row = asRecord(raw);
1180
+ if (!row || row.unlimited === true) continue;
1181
+ const remaining = asNumber(row.percent_remaining ?? row.percentRemaining);
1182
+ const entitlement = asNumber(row.entitlement ?? row.limit);
1183
+ const left = asNumber(row.remaining ?? row.remaining_count);
1184
+ let used = remaining === void 0 ? void 0 : 100 - Math.min(100, Math.max(0, remaining));
1185
+ if (used === void 0 && entitlement && entitlement > 0 && left !== void 0) used = (entitlement - left) / entitlement * 100;
1186
+ if (used === void 0) continue;
1187
+ const [window, label] = QUALIFIER[key] ?? [`monthly:${key.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 24)}`, titleCase(key)];
1188
+ addWin(windows, { window, label, percent: used, resets: row.quota_reset_at ?? row.quotaResetAt ?? resets });
1189
+ }
1190
+ const plan = asString(src.copilot_plan ?? src.copilotPlan);
1191
+ return { windows, plan: plan ? titleCase(plan) : void 0, accountId: asString(src.login) };
1192
+ }
1193
+ var readCopilot = async (deps) => {
1194
+ const out = emptyOutput();
1195
+ const dir = joinPath(deps.env.XDG_CONFIG_HOME ?? joinPath(deps.home, ".config"), "github-copilot");
1196
+ const token = findCopilotToken([deps.readFile(joinPath(dir, "apps.json")), deps.readFile(joinPath(dir, "hosts.json"))]);
1197
+ if (!token) return out;
1198
+ const headers = {
1199
+ Accept: "application/json",
1200
+ Authorization: `token ${token}`,
1201
+ "Editor-Version": "vscode/1.96.2",
1202
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
1203
+ "User-Agent": "GitHubCopilotChat/0.26.7",
1204
+ "X-Github-Api-Version": "2025-04-01"
1205
+ };
1206
+ try {
1207
+ const parsed = parseCopilotUsage(await requestJson(deps, URL_, { headers }));
1208
+ if (parsed.windows.length === 0) throw new ReaderError("bad_response", issueMessage("bad_response"));
1209
+ out.readings.push(...toReadings("copilot", "default", parsed, "local_login"));
1210
+ } catch (e) {
1211
+ out.issues.push(failure("copilot", "default", e));
1212
+ }
1213
+ return out;
1214
+ };
1215
+
1216
+ // ../usage/src/readers/cursor.ts
1217
+ var SUMMARY_URL = "https://cursor.com/api/usage-summary";
1218
+ function parseCursorCreds(raw, now) {
1219
+ const text = (raw ?? "").trim();
1220
+ if (!text) return { kind: "none" };
1221
+ const data = text.startsWith("{") ? asRecord(parseJson(text)) : void 0;
1222
+ const token = (data ? asString(data.accessToken ?? data.access_token) : text)?.replace(/^['"]|['"]$/g, "");
1223
+ if (!token) return { kind: "none" };
1224
+ const payload = decodeJwt(token);
1225
+ const exp = asNumber(payload?.exp);
1226
+ if (exp !== void 0 && exp > 0 && exp * 1e3 <= now) return { kind: "expired" };
1227
+ const sub = String(payload?.sub ?? "").split("|").filter(Boolean).pop();
1228
+ return { kind: "ok", token, userId: sub };
1229
+ }
1230
+ var PLANS = { free: "Free", pro: "Pro", pro_plus: "Pro+", ultra: "Ultra", business: "Business", enterprise: "Enterprise", free_trial: "Trial" };
1231
+ function cents(block) {
1232
+ const b = asRecord(block);
1233
+ if (!b || b.enabled === false) return void 0;
1234
+ const used = asNumber(b.used);
1235
+ let limit = asNumber(b.limit);
1236
+ const remaining = asNumber(b.remaining);
1237
+ if (!(limit && limit > 0) && used !== void 0 && remaining !== void 0) limit = used + remaining;
1238
+ return used !== void 0 && limit && limit > 0 ? used / limit * 100 : void 0;
1239
+ }
1240
+ function parseCursorSummary(json) {
1241
+ const src = asRecord(json) ?? {};
1242
+ const end = toIso(src.billingCycleEnd);
1243
+ const start = toIso(src.billingCycleStart);
1244
+ const seconds = start && end ? (Date.parse(end) - Date.parse(start)) / 1e3 : void 0;
1245
+ const base = seconds && seconds > 0 ? windowFromSeconds(seconds) : "monthly";
1246
+ const individual = asRecord(src.individualUsage ?? src.individual_usage) ?? {};
1247
+ const team = asRecord(src.teamUsage ?? src.team_usage) ?? {};
1248
+ const plan = asRecord(individual.plan) ?? {};
1249
+ const windows = [];
1250
+ if (src.isUnlimited === true) {
1251
+ addWin(windows, { window: base, label: "Plan (unlimited)", percent: 0, resets: end });
1252
+ } else {
1253
+ const auto = asNumber(plan.autoPercentUsed);
1254
+ const api = asNumber(plan.apiPercentUsed);
1255
+ const total = asNumber(plan.totalPercentUsed);
1256
+ if (auto !== void 0) addWin(windows, { window: `${base}:cursor-models`, label: "Cursor models", percent: auto, resets: end });
1257
+ if (api !== void 0) addWin(windows, { window: `${base}:other-models`, label: "Other models", percent: api, resets: end });
1258
+ if (auto === void 0 && api === void 0) {
1259
+ const pct2 = total ?? cents(plan) ?? cents(individual) ?? cents(individual.overall);
1260
+ if (pct2 !== void 0) addWin(windows, { window: base, label: "Plan", percent: pct2, resets: end });
1261
+ }
1262
+ }
1263
+ const onDemand = cents(individual.onDemand ?? individual.on_demand);
1264
+ if (onDemand !== void 0) addWin(windows, { window: `${base}:on-demand`, label: "On-demand", percent: onDemand, resets: end });
1265
+ const pool = cents(team.pooled) ?? cents(team);
1266
+ if (pool !== void 0) addWin(windows, { window: `${base}:team-pool`, label: "Team pool", percent: pool, resets: end });
1267
+ const type = String(src.membershipType ?? "").trim().toLowerCase();
1268
+ return { windows, plan: type ? PLANS[type] ?? titleCase(type) : void 0 };
1269
+ }
1270
+ var readCursor = async (deps) => {
1271
+ const out = emptyOutput();
1272
+ let raw = null;
1273
+ if (deps.platform === "darwin" && deps.exec) {
1274
+ raw = deps.exec("security", ["find-generic-password", "-s", "cursor-access-token", "-w"]);
1275
+ }
1276
+ raw ??= deps.readFile(joinPath(deps.env.XDG_CONFIG_HOME ?? joinPath(deps.home, ".config"), "cursor", "auth.json"));
1277
+ if (raw === null) return out;
1278
+ const creds = parseCursorCreds(raw, deps.now());
1279
+ if (creds.kind === "none") return out;
1280
+ if (creds.kind === "expired") {
1281
+ out.issues.push(issue("cursor", "default", "expired", issueMessage("expired")));
1282
+ return out;
1283
+ }
1284
+ const headers = { Accept: "application/json", Origin: "https://cursor.com", Referer: "https://cursor.com/dashboard" };
1285
+ if (creds.userId) headers.Cookie = `WorkosCursorSessionToken=${creds.userId}%3A%3A${creds.token}`;
1286
+ else headers.Authorization = `Bearer ${creds.token}`;
1287
+ try {
1288
+ const parsed = parseCursorSummary(await requestJson(deps, SUMMARY_URL, { headers }));
1289
+ if (parsed.windows.length === 0) throw new ReaderError("bad_response", issueMessage("bad_response"));
1290
+ parsed.accountId = creds.userId;
1291
+ out.readings.push(...toReadings("cursor", "default", parsed, "local_login"));
1292
+ } catch (e) {
1293
+ out.issues.push(failure("cursor", "default", e));
1294
+ }
1295
+ return out;
1296
+ };
1297
+
1298
+ // ../usage/src/readers/gemini.ts
1299
+ var BASE = "https://cloudcode-pa.googleapis.com/v1internal";
1300
+ function parseGeminiCreds(raw) {
1301
+ const data = asRecord(parseJson(raw));
1302
+ if (!data) return void 0;
1303
+ const t = asRecord(data.token) ?? data;
1304
+ const token = asString(t.access_token ?? t.accessToken);
1305
+ return token ? { token } : void 0;
1306
+ }
1307
+ function project(v) {
1308
+ return asString(v)?.trim() ?? asString(asRecord(v)?.id);
1309
+ }
1310
+ function fraction(b) {
1311
+ const nested = asRecord(b.remaining) ?? {};
1312
+ const r = asNumber(b.remainingFraction ?? b.remaining_fraction ?? nested.remainingFraction ?? nested.remaining_fraction);
1313
+ if (r === void 0) return void 0;
1314
+ return Math.min(1, Math.max(0, r > 1 ? r / 100 : r));
1315
+ }
1316
+ function parseGeminiSummary(json) {
1317
+ const src = asRecord(json) ?? {};
1318
+ let groups = Array.isArray(src.groups) ? src.groups : [];
1319
+ if (groups.length === 0 && Array.isArray(src.buckets)) {
1320
+ groups = [{ buckets: src.buckets.filter((b) => b && typeof b === "object" && !asRecord(b)?.modelId && !asRecord(b)?.model_id) }];
1321
+ }
1322
+ const windows = [];
1323
+ for (const g of groups) {
1324
+ const group = asRecord(g);
1325
+ if (!group) continue;
1326
+ const name = (asString(group.displayName ?? group.display_name) ?? "").replace(/\s+models?$/i, "").replace(/\s+and\s+/gi, " & ").trim();
1327
+ const buckets = Array.isArray(group.buckets) ? group.buckets : [];
1328
+ for (const raw of buckets) {
1329
+ const b = asRecord(raw);
1330
+ if (!b || b.disabled === true) continue;
1331
+ const f = fraction(b);
1332
+ if (f === void 0) continue;
1333
+ const id = String(b.bucketId ?? b.bucket_id ?? "");
1334
+ const kind = String(b.window ?? "");
1335
+ const base = kind === "weekly" || /weekly/i.test(id) ? "weekly" : kind === "5h" || /-5h\b/i.test(id) ? "5h" : void 0;
1336
+ if (!base) continue;
1337
+ const q = slug(name);
1338
+ addWin(windows, {
1339
+ window: q ? `${base}:${q}` : base,
1340
+ label: `${name ? `${name} ` : ""}${base === "weekly" ? "weekly" : "5 hours"}`,
1341
+ percent: (1 - f) * 100,
1342
+ resets: b.resetTime ?? b.reset_time
1343
+ });
1344
+ }
1345
+ }
1346
+ return { windows };
1347
+ }
1348
+ var readGemini = async (deps) => {
1349
+ const out = emptyOutput();
1350
+ const raw = deps.readFile(joinPath(deps.home, ".gemini", "antigravity-cli", "antigravity-oauth-token"));
1351
+ if (raw === null) return out;
1352
+ const creds = parseGeminiCreds(raw);
1353
+ if (!creds) return out;
1354
+ const headers = { Authorization: `Bearer ${creds.token}`, "Content-Type": "application/json", Accept: "application/json" };
1355
+ const post = (method, body) => requestJson(deps, `${BASE}:${method}`, { method: "POST", headers, body: JSON.stringify(body) });
1356
+ try {
1357
+ const loaded = asRecord(await post("loadCodeAssist", { metadata: { ideType: "IDE_UNSPECIFIED", pluginType: "GEMINI" } })) ?? {};
1358
+ const proj = project(loaded.cloudaicompanionProject ?? loaded.cloudaicompanion_project);
1359
+ if (!proj) throw new ReaderError("bad_response", issueMessage("bad_response"));
1360
+ const parsed = parseGeminiSummary(await post("retrieveUserQuotaSummary", { project: proj }));
1361
+ const tier = asString(asRecord(loaded.currentTier ?? loaded.current_tier)?.name);
1362
+ if (tier) parsed.plan = titleCase(tier);
1363
+ if (parsed.windows.length === 0) throw new ReaderError("bad_response", issueMessage("bad_response"));
1364
+ out.readings.push(...toReadings("gemini", "default", parsed, "local_login"));
1365
+ } catch (e) {
1366
+ out.issues.push(failure("gemini", "default", e));
1367
+ }
1368
+ return out;
1369
+ };
1370
+
1371
+ // ../usage/src/readers/grok.ts
1372
+ var BILLING_URL = "https://cli-chat-proxy.grok.com/v1/billing?format=credits";
1373
+ var SETTINGS_URL = "https://cli-chat-proxy.grok.com/v1/settings";
1374
+ function extractGrokAuth(data) {
1375
+ const found = [];
1376
+ const walk = (node, depth) => {
1377
+ if (depth > 6) return;
1378
+ if (Array.isArray(node)) {
1379
+ for (const v of node) walk(v, depth + 1);
1380
+ return;
1381
+ }
1382
+ const r = asRecord(node);
1383
+ if (!r) return;
1384
+ if (typeof r.key === "string" && r.key.length > 20) found.push({ token: r.key, expiresAt: r.expires_at ?? r.expiresAt });
1385
+ for (const v of Object.values(r)) if (v && typeof v === "object") walk(v, depth + 1);
1386
+ };
1387
+ walk(data, 0);
1388
+ const seen = /* @__PURE__ */ new Set();
1389
+ return found.filter((e) => seen.has(e.token) ? false : (seen.add(e.token), true));
1390
+ }
1391
+ function percent(v) {
1392
+ const n = asNumber(v);
1393
+ return n === void 0 || n < 0 ? void 0 : n;
1394
+ }
1395
+ function parseGrokBilling(json) {
1396
+ const src = asRecord(json) ?? {};
1397
+ const config = asRecord(src.config) ?? src;
1398
+ let used = percent(config.creditUsagePercent ?? config.credit_usage_percent ?? config.usedPercent ?? config.usagePercent);
1399
+ if (used === void 0) {
1400
+ const usage = asRecord(src.usage ?? config.usage);
1401
+ const u = asNumber(asRecord(usage?.totalUsed)?.val ?? usage?.totalUsed);
1402
+ const limit = asNumber(asRecord(src.monthlyLimit)?.val ?? asRecord(config.monthlyLimit)?.val ?? config.monthlyLimit);
1403
+ if (u !== void 0 && limit) used = u / limit * 100;
1404
+ }
1405
+ const period = asRecord(config.currentPeriod ?? config.current_period ?? src.billingCycle) ?? {};
1406
+ const start = toIso(period.start ?? period.billingPeriodStart ?? config.billingPeriodStart);
1407
+ const end = toIso(period.end ?? period.billingPeriodEnd ?? config.billingPeriodEnd);
1408
+ const seconds = start && end ? (Date.parse(end) - Date.parse(start)) / 1e3 : void 0;
1409
+ const base = seconds && seconds > 0 ? windowFromSeconds(seconds) : "monthly";
1410
+ const windows = [];
1411
+ if (used !== void 0) addWin(windows, { window: base, label: `Credits (${windowLabel(base).toLowerCase()})`, percent: used, resets: end });
1412
+ const unified = config.isUnifiedBillingUser === true || config.is_unified_billing_user === true;
1413
+ const products = Array.isArray(config.productUsage ?? config.product_usage) ? config.productUsage ?? config.product_usage : [];
1414
+ const rows = [];
1415
+ for (const raw of products) {
1416
+ const p = asRecord(raw);
1417
+ const pct2 = percent(p?.usagePercent ?? p?.usage_percent ?? p?.creditUsagePercent ?? p?.credit_usage_percent);
1418
+ const name = asString(p?.product ?? p?.name ?? p?.label);
1419
+ if (pct2 !== void 0 && name) rows.push({ name, pct: pct2 });
1420
+ }
1421
+ const slices = unified && (used === void 0 || Math.abs(rows.reduce((s, r) => s + r.pct, 0) - used) <= 1);
1422
+ if (!slices) for (const r of rows) addWin(windows, { window: `${base}:${slug(r.name) || "product"}`, label: titleCase(r.name.replace(/([a-z])([A-Z])/g, "$1 $2")), percent: r.pct, resets: end });
1423
+ if (windows.length === 0 && unified && start && end) addWin(windows, { window: base, label: "Credits", percent: 0, resets: end });
1424
+ const tier = asString(config.subscriptionTier ?? config.subscription_tier ?? config.subscription_tier_display ?? src.subscription_tier_display);
1425
+ return { windows, plan: tier ? titleCase(tier) : void 0 };
1426
+ }
1427
+ var readGrok = async (deps) => {
1428
+ const out = emptyOutput();
1429
+ const raw = deps.readFile(joinPath(deps.home, ".grok", "auth.json"));
1430
+ if (raw === null) return out;
1431
+ const entries = extractGrokAuth(parseJson(raw));
1432
+ if (entries.length === 0) return out;
1433
+ const entry = entries[0];
1434
+ const exp = entry.expiresAt ? Date.parse(String(entry.expiresAt)) : NaN;
1435
+ if (Number.isFinite(exp) && exp <= deps.now()) {
1436
+ out.issues.push(issue("grok", "default", "expired", issueMessage("expired")));
1437
+ return out;
1438
+ }
1439
+ const headers = { Authorization: `Bearer ${entry.token}`, Accept: "application/json", "x-xai-token-auth": "xai-grok-cli" };
1440
+ try {
1441
+ const parsed = parseGrokBilling(await requestJson(deps, BILLING_URL, { headers }));
1442
+ try {
1443
+ const tier = asRecord(await requestJson(deps, SETTINGS_URL, { headers }));
1444
+ const t = asString(tier?.subscription_tier_display ?? tier?.subscriptionTier);
1445
+ if (t) parsed.plan = titleCase(t);
1446
+ } catch {
1447
+ }
1448
+ if (parsed.windows.length === 0) throw new ReaderError("bad_response", issueMessage("bad_response"));
1449
+ out.readings.push(...toReadings("grok", "default", parsed, "local_login"));
1450
+ } catch (e) {
1451
+ out.issues.push(failure("grok", "default", e));
1452
+ }
1453
+ return out;
1454
+ };
1455
+
1456
+ // ../usage/src/readers/common.ts
1457
+ function keyReader(opts) {
1458
+ return async (deps) => {
1459
+ const out = emptyOutput();
1460
+ const login = opts.find(deps);
1461
+ if (!login) return out;
1462
+ try {
1463
+ const parsed = await opts.fetchParsed(deps, login.key);
1464
+ if (parsed.windows.length === 0) throw new ReaderError("bad_response", issueMessage("bad_response"));
1465
+ out.readings.push(...toReadings(opts.provider, "default", parsed, login.source));
1466
+ } catch (e) {
1467
+ out.issues.push(failure(opts.provider, "default", e));
1468
+ }
1469
+ return out;
1470
+ };
1471
+ }
1472
+ function envKey(deps, names) {
1473
+ for (const n of names) {
1474
+ const v = asString(deps.env[n])?.trim().replace(/^['"]|['"]$/g, "");
1475
+ if (v) return { key: v, source: "env_key" };
1476
+ }
1477
+ return null;
1478
+ }
1479
+ function bearer(key) {
1480
+ return { Authorization: `Bearer ${key}`, Accept: "application/json" };
1481
+ }
1482
+
1483
+ // ../usage/src/readers/kimi.ts
1484
+ var HOSTS = ["https://api.kimi.com/coding/v1/usages", "https://api.kimi.ai/coding/v1/usages"];
1485
+ var UNIT_SECONDS = { SECOND: 1, MINUTE: 60, HOUR: 3600, DAY: 86400, WEEK: 604800 };
1486
+ function percentOf(src) {
1487
+ const s = asRecord(src);
1488
+ const limit = asNumber(s?.limit);
1489
+ if (!s || !limit || limit <= 0) return void 0;
1490
+ const remaining = asNumber(s.remaining);
1491
+ const used = asNumber(s.used) ?? (remaining === void 0 ? void 0 : Math.max(0, limit - remaining));
1492
+ if (used === void 0) return void 0;
1493
+ return { percent: used / limit * 100, resets: pick(s, "reset_at", "resetAt", "reset_time", "resetTime") };
1494
+ }
1495
+ function parseKimiCodeUsage(json) {
1496
+ const src = asRecord(json);
1497
+ const windows = [];
1498
+ const limits = Array.isArray(src?.limits) ? src.limits : [];
1499
+ limits.forEach((raw, i) => {
1500
+ const item = asRecord(raw);
1501
+ if (!item) return;
1502
+ const span = asRecord(item.window) ?? {};
1503
+ const unit = String(span.timeUnit ?? span.time_unit ?? "").toUpperCase();
1504
+ const per = Object.entries(UNIT_SECONDS).find(([n]) => unit.includes(n))?.[1];
1505
+ const duration = asNumber(span.duration);
1506
+ const base = per && duration ? windowFromSeconds(duration * per) : `limit${i}`;
1507
+ const p = percentOf(item.detail ?? item);
1508
+ if (p) addWin(windows, { window: base, label: per ? windowLabel(base) : "Limit", percent: p.percent, resets: p.resets });
1509
+ });
1510
+ const weekly = percentOf(src?.usage);
1511
+ if (weekly) addWin(windows, { window: "weekly", label: "Weekly", percent: weekly.percent, resets: weekly.resets });
1512
+ return { windows, plan: windows.length ? "Kimi Code" : void 0 };
1513
+ }
1514
+ var readKimi = keyReader({
1515
+ provider: "kimi",
1516
+ find: (deps) => envKey(deps, ["KIMI_API_KEY", "KIMI_CODE_API_KEY"]),
1517
+ fetchParsed: (deps, key) => firstOk(HOSTS, async (url) => parseKimiCodeUsage(await requestJson(deps, url, { headers: bearer(key) })))
1518
+ });
1519
+
1520
+ // ../usage/src/readers/minimax.ts
1521
+ var GLOBAL = ["https://www.minimax.io/v1/token_plan/remains", "https://api.minimax.io/v1/token_plan/remains"];
1522
+ var CN = ["https://api.minimaxi.com/v1/token_plan/remains", "https://www.minimaxi.com/v1/token_plan/remains"];
1523
+ function counts(total, count, remainingPercent) {
1524
+ const limit = asNumber(total);
1525
+ const raw = asNumber(count);
1526
+ if (limit && limit > 0 && raw !== void 0) return (limit - raw) / limit * 100;
1527
+ const pct2 = asNumber(remainingPercent);
1528
+ return pct2 === void 0 ? void 0 : 100 - Math.min(100, Math.max(0, pct2));
1529
+ }
1530
+ function parseMiniMax(json) {
1531
+ const src = asRecord(json) ?? {};
1532
+ const rows = (Array.isArray(src.model_remains) ? src.model_remains : Array.isArray(src.modelRemains) ? src.modelRemains : []).map(asRecord).filter((r) => r !== void 0);
1533
+ const name = (r) => String(r.model_name ?? r.modelName ?? "");
1534
+ const text = rows.filter((r) => /general|chat|text|minimax-m/i.test(name(r)));
1535
+ const pool = text.length ? text : rows.filter((r) => !/video|image|speech|tts|music/i.test(name(r)));
1536
+ const row = (pool.length ? pool : rows)[0];
1537
+ const windows = [];
1538
+ if (row) {
1539
+ const session = counts(pick(row, "current_interval_total_count", "currentIntervalTotalCount"), pick(row, "current_interval_usage_count", "currentIntervalUsageCount"), pick(row, "current_interval_remaining_percent", "currentIntervalRemainingPercent"));
1540
+ if (session !== void 0) addWin(windows, { window: "5h", label: "5 hours", percent: session, resets: pick(row, "end_time", "endTime") });
1541
+ if (row.current_weekly_status !== 3 && row.currentWeeklyStatus !== 3) {
1542
+ const week = counts(pick(row, "current_weekly_total_count", "currentWeeklyTotalCount"), pick(row, "current_weekly_usage_count", "currentWeeklyUsageCount"), pick(row, "current_weekly_remaining_percent", "currentWeeklyRemainingPercent"));
1543
+ if (week !== void 0) addWin(windows, { window: "weekly", label: "Weekly", percent: week, resets: pick(row, "weekly_end_time", "weeklyEndTime") });
1544
+ }
1545
+ }
1546
+ const plan = asString(pick(src, "current_subscribe_title", "current_plan_title", "plan_name", "combo_title", "plan"));
1547
+ return { windows, plan };
1548
+ }
1549
+ var readMiniMax = keyReader({
1550
+ provider: "minimax",
1551
+ find: (deps) => envKey(deps, ["MINIMAX_API_KEY"]),
1552
+ fetchParsed: (deps, key) => {
1553
+ const hosts = deps.env.MINIMAX_REGION?.toLowerCase() === "cn" ? CN : GLOBAL;
1554
+ return firstOk(hosts, async (url) => {
1555
+ const json = await requestJson(deps, url, { headers: bearer(key) });
1556
+ const base = asRecord(asRecord(json)?.base_resp);
1557
+ if (base && asNumber(base.status_code) !== void 0 && asNumber(base.status_code) !== 0) throw new ReaderError("rejected", issueMessage("rejected"));
1558
+ return parseMiniMax(json);
1559
+ });
1560
+ }
1561
+ });
1562
+
1563
+ // ../usage/src/readers/opencode.ts
1564
+ var URL_2 = "https://opencode.ai/zen/go/v1/usage";
1565
+ function parseOpenCodeGoUsage(json) {
1566
+ const usage = asRecord(asRecord(json)?.usage) ?? {};
1567
+ const windows = [];
1568
+ for (const [key, window, label] of [["rolling", "5h", "5 hours"], ["weekly", "weekly", "Weekly"], ["monthly", "monthly", "Monthly"]]) {
1569
+ const row = asRecord(usage[key]);
1570
+ if (row) addWin(windows, { window, label, percent: pick(row, "percent", "usedPercent", "used_percent"), resets: pick(row, "resetsAt", "resets_at") });
1571
+ }
1572
+ return { windows, plan: windows.length ? "Go" : void 0 };
1573
+ }
1574
+ var readOpenCode = keyReader({
1575
+ provider: "opencode",
1576
+ find: (deps) => {
1577
+ const fromEnv = envKey(deps, ["OPENCODE_API_KEY"]);
1578
+ if (fromEnv) return fromEnv;
1579
+ const data = asRecord(parseJson(deps.readFile(joinPath(deps.env.XDG_DATA_HOME ?? joinPath(deps.home, ".local", "share"), "opencode", "auth.json"))));
1580
+ for (const name of ["opencode-go", "opencode"]) {
1581
+ const entry = asRecord(data?.[name]);
1582
+ const key = asString(entry?.key);
1583
+ if (entry && (!entry.type || entry.type === "api") && key) return { key: key.trim(), source: "local_login" };
1584
+ }
1585
+ return null;
1586
+ },
1587
+ fetchParsed: async (deps, key) => parseOpenCodeGoUsage(await requestJson(deps, URL_2, { headers: bearer(key) }))
1588
+ });
1589
+
1590
+ // ../usage/src/readers/openrouter.ts
1591
+ var URL_3 = "https://openrouter.ai/api/v1/key";
1592
+ var INTERVALS = { daily: ["daily", "Daily"], weekly: ["weekly", "Weekly"], monthly: ["monthly", "Monthly"] };
1593
+ function nextUtcMidnight(now) {
1594
+ const d = new Date(now);
1595
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() + 1)).toISOString();
1596
+ }
1597
+ function parseOpenRouterKey(json, now) {
1598
+ const data = asRecord(asRecord(json)?.data) ?? {};
1599
+ const windows = [];
1600
+ const budget = asRecord(data.effective_budget);
1601
+ const budgetLimit = asNumber(budget?.limit_usd);
1602
+ const keyLimit = asNumber(data.limit);
1603
+ if (budget && budgetLimit && budgetLimit > 0) {
1604
+ const used = asNumber(budget.spend_usd) ?? Math.max(0, budgetLimit - (asNumber(budget.remaining_usd) ?? budgetLimit));
1605
+ const [window, label] = INTERVALS[String(budget.reset_interval ?? "").toLowerCase()] ?? ["total", "Key limit"];
1606
+ addWin(windows, { window, label, percent: used / budgetLimit * 100, resets: budget.resets_at });
1607
+ } else if (keyLimit && keyLimit > 0) {
1608
+ const remaining = asNumber(data.limit_remaining);
1609
+ const used = remaining === void 0 ? asNumber(data.usage) ?? 0 : Math.max(0, keyLimit - remaining);
1610
+ const [window, label] = INTERVALS[String(data.limit_reset ?? "").toLowerCase()] ?? ["total", "Key limit"];
1611
+ addWin(windows, { window, label, percent: used / keyLimit * 100 });
1612
+ }
1613
+ const free = asRecord(data.free_model_daily_requests);
1614
+ const freeLimit = asNumber(free?.limit);
1615
+ if (free && data.is_free_tier === true && freeLimit && freeLimit > 0) {
1616
+ const used = asNumber(free.used) ?? Math.max(0, freeLimit - (asNumber(free.remaining) ?? freeLimit));
1617
+ addWin(windows, { window: "daily:free", label: "Free requests today", percent: used / freeLimit * 100, resets: nextUtcMidnight(now) });
1618
+ }
1619
+ return { windows, plan: data.is_free_tier === true ? "Free" : "API key" };
1620
+ }
1621
+ var readOpenRouter = keyReader({
1622
+ provider: "openrouter",
1623
+ find: (deps) => envKey(deps, ["OPENROUTER_API_KEY"]),
1624
+ fetchParsed: async (deps, key) => parseOpenRouterKey(await requestJson(deps, URL_3, { headers: bearer(key) }), deps.now())
1625
+ });
1626
+
1627
+ // ../usage/src/readers/zai.ts
1628
+ var HOSTS2 = ["https://api.z.ai/api/monitor/usage/quota/limit", "https://open.bigmodel.cn/api/monitor/usage/quota/limit"];
1629
+ var UNIT_SECONDS2 = { 1: 86400, 3: 3600, 5: 60, 6: 604800 };
1630
+ function parseZaiQuota(json) {
1631
+ const data = asRecord(asRecord(json)?.data) ?? {};
1632
+ const windows = [];
1633
+ const limits = Array.isArray(data.limits) ? data.limits : [];
1634
+ limits.forEach((raw) => {
1635
+ const item = asRecord(raw);
1636
+ if (!item) return;
1637
+ const type = String(item.type ?? "").toUpperCase();
1638
+ const resets = pick(item, "nextResetTime", "next_reset_time");
1639
+ if (type === "TOKENS_LIMIT") {
1640
+ const seconds = (UNIT_SECONDS2[asNumber(item.unit) ?? 0] ?? 0) * (asNumber(item.number) ?? 0);
1641
+ const base = seconds ? windowFromSeconds(seconds) : "prompts";
1642
+ addWin(windows, { window: base, label: seconds ? windowLabel(base) : "Prompts", percent: item.percentage, resets });
1643
+ } else if (type === "TIME_LIMIT") {
1644
+ const total = asNumber(item.usage);
1645
+ const current = asNumber(pick(item, "currentValue", "current_value"));
1646
+ const remaining = asNumber(item.remaining);
1647
+ const used = current ?? (total !== void 0 && remaining !== void 0 ? Math.max(0, total - remaining) : void 0);
1648
+ const percent2 = total && total > 0 && used !== void 0 ? used / total * 100 : item.percentage;
1649
+ addWin(windows, { window: "monthly:mcp", label: "MCP calls", percent: percent2, resets });
1650
+ }
1651
+ });
1652
+ const level = asString(pick(data, "planName", "plan_name", "packageName", "level", "plan", "plan_type"));
1653
+ return { windows, plan: level ? `GLM ${titleCase(level.replace(/^glm[\s_-]*/i, ""))}` : windows.length ? "GLM Coding Plan" : void 0 };
1654
+ }
1655
+ var readZai = keyReader({
1656
+ provider: "zai",
1657
+ find: (deps) => envKey(deps, ["ZAI_API_KEY", "Z_AI_API_KEY", "ZHIPUAI_API_KEY"]),
1658
+ fetchParsed: (deps, key) => firstOk(HOSTS2, async (url) => {
1659
+ const json = asRecord(await requestJson(deps, url, { headers: { Authorization: key, Accept: "application/json" } }));
1660
+ const code = asNumber(json?.code);
1661
+ if (json?.success === false || code !== void 0 && code !== 200) {
1662
+ const bad = code === 401 || code === 403 || code === 1e3 || code === 1001 ? "rejected" : "bad_response";
1663
+ throw new ReaderError(bad, issueMessage(bad));
1664
+ }
1665
+ return parseZaiQuota(json);
1666
+ })
1667
+ });
1668
+
1669
+ // ../usage/src/collect.ts
1670
+ var READERS = {
1671
+ claude: readClaude,
1672
+ codex: readCodex,
1673
+ cursor: readCursor,
1674
+ gemini: readGemini,
1675
+ grok: readGrok,
1676
+ copilot: readCopilot,
1677
+ minimax: readMiniMax,
1678
+ opencode: readOpenCode,
1679
+ openrouter: readOpenRouter,
1680
+ kimi: readKimi,
1681
+ zai: readZai
1682
+ };
1683
+ async function collectUsage(deps, only) {
1684
+ const entries = Object.entries(READERS).filter(([p]) => !only || only.includes(p));
1685
+ const results = await Promise.all(
1686
+ entries.map(async ([provider, reader]) => {
1687
+ try {
1688
+ return await reader(deps);
1689
+ } catch {
1690
+ const issue2 = { provider, account_label: "default", code: "bad_response", message: "The reader failed unexpectedly." };
1691
+ return { readings: [], issues: [issue2] };
1692
+ }
1693
+ })
1694
+ );
1695
+ const readings = [];
1696
+ const issues = [];
1697
+ for (const r of results) {
1698
+ for (const reading of r.readings) {
1699
+ const ok = usageReadingSchema.safeParse(reading);
1700
+ if (ok.success) readings.push(ok.data);
1701
+ }
1702
+ issues.push(...r.issues);
1703
+ }
1704
+ return { readings, issues };
1705
+ }
1706
+ async function buildUsagePush(deps, statsDeps) {
1707
+ const [{ readings, issues }, stats] = await Promise.all([collectUsage(deps), statsDeps ? collectMachineStats(statsDeps) : Promise.resolve(void 0)]);
1708
+ const push = { collected_at: new Date(deps.now()).toISOString(), readings, ...issues.length ? { issues } : {} };
1709
+ if (stats) push.stats = stats;
1710
+ return push;
1711
+ }
1712
+
1713
+ // ../usage/src/node.ts
1714
+ import { execFileSync } from "child_process";
1715
+ import { readFileSync, readdirSync } from "fs";
1716
+ import * as os2 from "os";
1717
+ var TIMEOUT_MS = 2e4;
1718
+ function exec(cmd, args) {
1719
+ try {
1720
+ return execFileSync(cmd, [...args], { encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"], maxBuffer: 4 * 1024 * 1024 });
1721
+ } catch {
1722
+ return null;
1723
+ }
1724
+ }
1725
+ function readFile(path2) {
1726
+ try {
1727
+ return readFileSync(path2, "utf8");
1728
+ } catch {
1729
+ return null;
1730
+ }
1731
+ }
1732
+ function nodeDeps() {
1733
+ return {
1734
+ async fetch(url, init = {}) {
1735
+ const res = await fetch(url, { method: init.method ?? "GET", headers: init.headers, body: init.body, redirect: "manual", signal: AbortSignal.timeout(TIMEOUT_MS) });
1736
+ return { status: res.status, text: () => res.text(), headers: Object.fromEntries([...res.headers].map(([k, v]) => [k.toLowerCase(), v])) };
1737
+ },
1738
+ readFile,
1739
+ readDir(path2) {
1740
+ try {
1741
+ return readdirSync(path2);
1742
+ } catch {
1743
+ return [];
1744
+ }
1745
+ },
1746
+ env: process.env,
1747
+ home: os2.homedir(),
1748
+ platform: process.platform,
1749
+ now: () => Date.now(),
1750
+ exec
1751
+ };
1752
+ }
1753
+ function nodeStatsDeps() {
1754
+ return {
1755
+ platform: process.platform,
1756
+ totalmem: () => os2.totalmem(),
1757
+ freemem: () => os2.freemem(),
1758
+ cpuTimes() {
1759
+ let idle = 0;
1760
+ let total = 0;
1761
+ for (const c of os2.cpus()) {
1762
+ idle += c.times.idle;
1763
+ total += c.times.user + c.times.nice + c.times.sys + c.times.idle + c.times.irq;
1764
+ }
1765
+ return { idle, total };
1766
+ },
1767
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
1768
+ readFile,
1769
+ exec
1770
+ };
1771
+ }
1772
+
1773
+ // src/service/usage-push.ts
1774
+ async function claudeSessionTokens(scan, readEnv) {
1775
+ const out = /* @__PURE__ */ new Set();
1776
+ for (const p of scan.procs) {
1777
+ if (p.tool !== "claude") continue;
1778
+ const t = await readEnv(p.pid, "CLAUDE_CODE_OAUTH_TOKEN");
1779
+ if (t && /^[\w.~+/=-]{20,400}$/.test(t)) out.add(t);
1780
+ }
1781
+ return [...out];
1782
+ }
1783
+ async function collectUsagePush(ctx) {
1784
+ const platform = ctx.platform ?? process.platform;
1785
+ const run = ctx.run ?? runCmd;
1786
+ let tokens = [];
1787
+ try {
1788
+ const readEnv = makeEnvReader(platform, run);
1789
+ tokens = await claudeSessionTokens(await scanProcesses({ platform, run, env: ctx.env, readEnv }), readEnv);
1790
+ } catch {
1791
+ }
1792
+ return buildUsagePush({ ...nodeDeps(), claudeTokens: tokens }, nodeStatsDeps());
1793
+ }
1794
+
1795
+ // src/service/run.ts
1796
+ var FLUSH_EVERY_MS = 2e3;
1797
+ var FLUSH_BUDGET_MS = 5e3;
1798
+ var DISCOVERY_EVERY_MS = 6e4;
1799
+ var BEAT_EVERY_MS = 1e4;
1800
+ var USAGE_EVERY_MS = 5 * 6e4;
1801
+ var USAGE_FIRST_MS = 1e4;
1802
+ var BACKOFF_MAX_MS = 3e5;
1803
+ var LOG_MAX_BYTES = 1024 * 1024;
1804
+ var newSched = (now) => ({ heartbeat: now, flush: now, discovery: now + 5e3, usage: now + USAGE_FIRST_MS, beat: 0, failures: 0, quietUntil: 0, interval: MACHINE_HEARTBEAT_SECONDS * 1e3, logged: "", started: now, stop: false });
1805
+ function backoffMs(failures, random) {
1806
+ const base = Math.min(BACKOFF_MAX_MS, MACHINE_HEARTBEAT_SECONDS * 1e3 * 2 ** Math.max(0, failures - 1));
1807
+ return Math.max(MACHINE_MIN_INTERVAL_SECONDS * 1e3, Math.round(base * (0.8 + 0.4 * random())));
1808
+ }
1809
+ function log(st, now, state) {
1810
+ if (st.logged === state) return;
1811
+ st.logged = state;
1812
+ try {
1813
+ fs2.mkdirSync(configDir(), { recursive: true, mode: 448 });
1814
+ const f = serviceLogFile();
1815
+ let size = 0;
1816
+ try {
1817
+ size = fs2.statSync(f).size;
1818
+ } catch {
1819
+ }
1820
+ if (size > LOG_MAX_BYTES) fs2.writeFileSync(f, "", { mode: 384 });
1821
+ fs2.appendFileSync(f, `${new Date(now).toISOString()} ${state}
1822
+ `, { mode: 384 });
1823
+ } catch {
1824
+ }
1825
+ }
1826
+ async function call(deps, m, path2, body) {
1827
+ try {
1828
+ const r = await apiRequest(deps.ctx, m.host, `/api/v1/machine${path2}`, { method: "POST", token: m.token, body, timeoutMs: 1e4 });
1829
+ if (r.status >= 200 && r.status < 300) return { outcome: "ok", json: r.json };
1830
+ if (r.status === 401) return { outcome: "revoked" };
1831
+ if (r.status === 429 || r.status >= 500) return { outcome: "retry" };
1832
+ return { outcome: "drop" };
1833
+ } catch {
1834
+ return { outcome: "retry" };
1835
+ }
1836
+ }
1837
+ function fail(st, deps, now, what) {
1838
+ st.failures += 1;
1839
+ st.lastError = what;
1840
+ st.quietUntil = now + backoffMs(st.failures, deps.random ?? Math.random);
1841
+ log(st, now, `error ${what}; retrying in ${Math.round((st.quietUntil - now) / 1e3)}s`);
1842
+ }
1843
+ function succeed(st, now) {
1844
+ if (st.failures > 0) log(st, now, "online");
1845
+ st.failures = 0;
1846
+ st.quietUntil = 0;
1847
+ st.lastOk = now;
1848
+ st.lastError = void 0;
1849
+ }
1850
+ function beat(st, now) {
1851
+ st.beat = now;
1852
+ writeServiceState({ pid: process.pid, beat: now, started: st.started, ...st.lastOk ? { last_ok: st.lastOk } : {}, ...st.lastError ? { last_error: st.lastError } : {} });
1853
+ }
1854
+ async function serviceStep(deps, st) {
1855
+ const now = deps.ctx.now();
1856
+ const m = readMachine();
1857
+ if (!m || m.revoked) {
1858
+ log(st, now, "stopped: not connected");
1859
+ return false;
1860
+ }
1861
+ const revoke = () => {
1862
+ writeMachine({ ...m, revoked: true });
1863
+ log(st, now, "stopped: the server no longer knows this machine (removed?)");
1864
+ return false;
1865
+ };
1866
+ if (now - st.beat >= BEAT_EVERY_MS) {
1867
+ beat(st, now);
1868
+ }
1869
+ if (now < st.quietUntil) return true;
1870
+ if (now >= st.heartbeat) {
1871
+ const stats = await collectStats(deps.collectors);
1872
+ const r = await call(deps, m, "/heartbeat", { os: cliVersionOs(), cli_version: VERSION, ...stats ? { stats } : {} });
1873
+ if (r.outcome === "revoked") return revoke();
1874
+ if (r.outcome === "ok") {
1875
+ const parsed = machineHeartbeatResponseSchema.safeParse(r.json);
1876
+ const secs = parsed.success ? parsed.data.interval : MACHINE_HEARTBEAT_SECONDS;
1877
+ st.interval = Math.max(MACHINE_MIN_INTERVAL_SECONDS, secs) * 1e3;
1878
+ st.heartbeat = now + st.interval;
1879
+ succeed(st, now);
1880
+ if (st.logged === "") log(st, now, "online");
1881
+ } else {
1882
+ fail(st, deps, now, r.outcome === "drop" ? "heartbeat rejected" : "server unreachable");
1883
+ st.heartbeat = st.quietUntil;
1884
+ return true;
1885
+ }
1886
+ }
1887
+ if (now >= st.flush) {
1888
+ st.flush = now + FLUSH_EVERY_MS;
1889
+ const claim = takeSpool(machineSpoolFile());
1890
+ if (claim) {
1891
+ const pending = claim.events;
1892
+ const unsent = [];
1893
+ const deadline = deps.ctx.now() + (deps.flushBudgetMs ?? FLUSH_BUDGET_MS);
1894
+ let down = false;
1895
+ let outOfTime = false;
1896
+ for (let k = 0; k < pending.length; k += HOOK_BATCH_MAX) {
1897
+ const batch = pending.slice(k, k + HOOK_BATCH_MAX);
1898
+ if (down || outOfTime) {
1899
+ unsent.push(...batch);
1900
+ continue;
1901
+ }
1902
+ if (deps.ctx.now() >= deadline) {
1903
+ unsent.push(...batch);
1904
+ outOfTime = true;
1905
+ continue;
1906
+ }
1907
+ const r = await call(deps, m, "/hooks", { events: batch });
1908
+ if (r.outcome === "revoked") {
1909
+ releaseSpool(claim, [...unsent, ...pending.slice(k)], machineSpoolFile());
1910
+ return revoke();
1911
+ }
1912
+ if (r.outcome === "retry") {
1913
+ unsent.push(...batch);
1914
+ down = true;
1915
+ }
1916
+ if (r.outcome === "ok") stashNudges(nudgesOf(r.json), deps.ctx.now());
1917
+ beat(st, deps.ctx.now());
1918
+ }
1919
+ releaseSpool(claim, unsent, machineSpoolFile());
1920
+ if (down) {
1921
+ fail(st, deps, now, "server unreachable");
1922
+ return true;
1923
+ }
1924
+ succeed(st, now);
1925
+ }
1926
+ }
1927
+ if (now >= st.discovery) {
1928
+ st.discovery = now + DISCOVERY_EVERY_MS;
1929
+ let profiles = [];
1930
+ try {
1931
+ profiles = await (deps.profiles ?? (async () => buildProfiles(await scanProcesses({ platform: deps.ctx.platform ?? process.platform, run: deps.ctx.run ?? runCmd, env: deps.ctx.env }), deps.ctx.env, void 0, m.installed.claude_dirs)))();
1932
+ } catch {
1933
+ }
1934
+ const r = await call(deps, m, "/discovery", { profiles });
1935
+ if (r.outcome === "revoked") return revoke();
1936
+ if (r.outcome === "retry") {
1937
+ fail(st, deps, now, "server unreachable");
1938
+ st.discovery = st.quietUntil;
1939
+ } else if (r.outcome === "ok") succeed(st, now);
1940
+ }
1941
+ if (now >= st.usage) {
1942
+ st.usage = now + USAGE_EVERY_MS;
1943
+ let body = null;
1944
+ try {
1945
+ body = await (deps.usage ?? (() => collectUsagePush(deps.ctx)))();
1946
+ } catch {
1947
+ }
1948
+ if (body) {
1949
+ const r = await call(deps, m, "/usage", body);
1950
+ if (r.outcome === "revoked") return revoke();
1951
+ if (r.outcome === "retry") st.usage = now + 6e4;
1952
+ else if (r.outcome === "ok") succeed(st, now);
1953
+ }
1954
+ }
1955
+ return true;
1956
+ }
1957
+ async function runService(deps) {
1958
+ const st = newSched(deps.ctx.now());
1959
+ const stop = () => {
1960
+ st.stop = true;
1961
+ };
1962
+ process.on("SIGTERM", stop);
1963
+ process.on("SIGINT", stop);
1964
+ try {
1965
+ while (!st.stop) {
1966
+ const more = await serviceStep(deps, st);
1967
+ if (!more) return 0;
1968
+ await deps.ctx.sleep(500);
1969
+ }
1970
+ return 0;
1971
+ } finally {
1972
+ process.off("SIGTERM", stop);
1973
+ process.off("SIGINT", stop);
1974
+ }
1975
+ }
1976
+ export {
1977
+ BACKOFF_MAX_MS,
1978
+ BEAT_EVERY_MS,
1979
+ DISCOVERY_EVERY_MS,
1980
+ FLUSH_BUDGET_MS,
1981
+ FLUSH_EVERY_MS,
1982
+ LOG_MAX_BYTES,
1983
+ USAGE_EVERY_MS,
1984
+ USAGE_FIRST_MS,
1985
+ backoffMs,
1986
+ newSched,
1987
+ runService,
1988
+ serviceStep
1989
+ };