@scrylog/cli 0.3.0 → 0.3.2

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 (195) hide show
  1. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  2. package/dist/assets/highlights-ghv9g403.scm +205 -0
  3. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  4. package/dist/assets/highlights-r812a2qc.scm +150 -0
  5. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  6. package/dist/assets/injections-73j83es3.scm +27 -0
  7. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  8. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  9. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  10. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  12. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  13. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  14. package/dist/cli/index.js +499 -0
  15. package/dist/cli/index.js.map +61 -0
  16. package/dist/daemon/index.js +670 -0
  17. package/dist/daemon/index.js.map +1475 -0
  18. package/dist/hooks/claude.js +6 -0
  19. package/dist/hooks/claude.js.map +11 -0
  20. package/dist/hooks/codex.js +10 -0
  21. package/dist/hooks/codex.js.map +11 -0
  22. package/dist/mcp/index.js +100 -0
  23. package/dist/mcp/index.js.map +231 -0
  24. package/dist/oc/index.js +21 -0
  25. package/dist/oc/index.js.map +10 -0
  26. package/package.json +16 -13
  27. package/src/api/docs/sessions.keys.ts +0 -6
  28. package/src/api/docs/sessions.ts +0 -82
  29. package/src/api/docs/system.keys.ts +0 -5
  30. package/src/api/docs/system.ts +0 -69
  31. package/src/api/docs/usage.keys.ts +0 -3
  32. package/src/api/docs/usage.ts +0 -24
  33. package/src/api/docs/webhooks.keys.ts +0 -5
  34. package/src/api/docs/webhooks.ts +0 -78
  35. package/src/api/index.ts +0 -18
  36. package/src/api/lib/auth.ts +0 -43
  37. package/src/api/lib/errors.ts +0 -28
  38. package/src/api/lib/route.ts +0 -18
  39. package/src/api/models/claude.ts +0 -4
  40. package/src/api/models/codex.ts +0 -4
  41. package/src/api/models/opencode.ts +0 -11
  42. package/src/api/routes/sessions.ts +0 -51
  43. package/src/api/routes/system.ts +0 -106
  44. package/src/api/routes/usage.ts +0 -15
  45. package/src/api/routes/webhooks/claude.ts +0 -149
  46. package/src/api/routes/webhooks/codex.ts +0 -175
  47. package/src/api/routes/webhooks/opencode.ts +0 -112
  48. package/src/api/routes.ts +0 -93
  49. package/src/api/schemas/common.ts +0 -29
  50. package/src/api/schemas/sessions.ts +0 -86
  51. package/src/api/schemas/usage.ts +0 -51
  52. package/src/api/schemas/webhooks/claude.ts +0 -32
  53. package/src/api/schemas/webhooks/codex.ts +0 -34
  54. package/src/api/schemas/webhooks/opencode.ts +0 -29
  55. package/src/api/services/claude-session.ts +0 -9
  56. package/src/api/services/codex-session.ts +0 -8
  57. package/src/api/services/opencode-session.ts +0 -73
  58. package/src/classifier.ts +0 -206
  59. package/src/cli/commands.ts +0 -714
  60. package/src/cli/index.ts +0 -18
  61. package/src/cli/program.ts +0 -175
  62. package/src/cli/prompt.ts +0 -44
  63. package/src/daemon/cli.ts +0 -27
  64. package/src/daemon/collectors/claude/README.md +0 -108
  65. package/src/daemon/collectors/claude/collector.ts +0 -423
  66. package/src/daemon/collectors/claude/discovery.ts +0 -127
  67. package/src/daemon/collectors/claude/hooks.ts +0 -181
  68. package/src/daemon/collectors/claude/index.ts +0 -8
  69. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  70. package/src/daemon/collectors/claude/transcript.ts +0 -246
  71. package/src/daemon/collectors/codex/README.md +0 -147
  72. package/src/daemon/collectors/codex/collector.ts +0 -661
  73. package/src/daemon/collectors/codex/discovery.ts +0 -104
  74. package/src/daemon/collectors/codex/hooks.ts +0 -179
  75. package/src/daemon/collectors/codex/index.ts +0 -7
  76. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  77. package/src/daemon/collectors/codex/transcript.ts +0 -299
  78. package/src/daemon/collectors/git.ts +0 -353
  79. package/src/daemon/collectors/herdr/collector.ts +0 -128
  80. package/src/daemon/collectors/herdr/index.ts +0 -2
  81. package/src/daemon/collectors/herdr/normalization.ts +0 -111
  82. package/src/daemon/collectors/opencode/README.md +0 -106
  83. package/src/daemon/collectors/opencode/collector.ts +0 -545
  84. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  85. package/src/daemon/collectors/opencode/index.ts +0 -5
  86. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  87. package/src/daemon/collectors/opencode-db.ts +0 -294
  88. package/src/daemon/collectors/process.ts +0 -201
  89. package/src/daemon/collectors/tmux/README.md +0 -128
  90. package/src/daemon/collectors/tmux/collector.ts +0 -492
  91. package/src/daemon/collectors/tmux/enrichment.ts +0 -242
  92. package/src/daemon/collectors/tmux/index.ts +0 -4
  93. package/src/daemon/collectors/tmux/parse.ts +0 -128
  94. package/src/daemon/collectors/usage.ts +0 -259
  95. package/src/daemon/index.ts +0 -209
  96. package/src/daemon/scheduler.ts +0 -86
  97. package/src/daemon/store.ts +0 -630
  98. package/src/detect.ts +0 -143
  99. package/src/hooks/claude.ts +0 -596
  100. package/src/hooks/codex.ts +0 -517
  101. package/src/legacy/classifier.ts +0 -215
  102. package/src/legacy/detect.ts +0 -143
  103. package/src/mcp/client-server.ts +0 -21
  104. package/src/mcp/http-handler.ts +0 -126
  105. package/src/mcp/index.ts +0 -61
  106. package/src/mcp/resources/config-current.ts +0 -19
  107. package/src/mcp/resources/index.ts +0 -11
  108. package/src/mcp/resources/sessions-active.ts +0 -19
  109. package/src/mcp/resources/sessions-all.ts +0 -19
  110. package/src/mcp/schemas/sessions.ts +0 -3
  111. package/src/mcp/server.ts +0 -17
  112. package/src/mcp/services/api-adapter.ts +0 -48
  113. package/src/mcp/services/data-source.ts +0 -22
  114. package/src/mcp/services/store-adapter.ts +0 -40
  115. package/src/mcp/tools/get-active-sessions.ts +0 -16
  116. package/src/mcp/tools/get-session-children.ts +0 -24
  117. package/src/mcp/tools/get-session-messages.ts +0 -24
  118. package/src/mcp/tools/get-session.ts +0 -23
  119. package/src/mcp/tools/index.ts +0 -15
  120. package/src/mcp/tools/list-sessions.ts +0 -16
  121. package/src/oc/index.ts +0 -302
  122. package/src/service/manager.ts +0 -284
  123. package/src/shared/config.ts +0 -511
  124. package/src/shared/ipc.ts +0 -80
  125. package/src/shared/logger.ts +0 -248
  126. package/src/shared/paths.ts +0 -73
  127. package/src/shared/platform.ts +0 -110
  128. package/src/shared/session-exclusions.ts +0 -5
  129. package/src/shared/session-view.ts +0 -302
  130. package/src/shared/terminal-capabilities.ts +0 -130
  131. package/src/shared/types.ts +0 -330
  132. package/src/shared/utils.ts +0 -339
  133. package/src/shared/version.ts +0 -44
  134. package/src/tui/App.tsx +0 -240
  135. package/src/tui/client.ts +0 -163
  136. package/src/tui/components/BindingsModal.tsx +0 -82
  137. package/src/tui/components/DashboardHeader.tsx +0 -256
  138. package/src/tui/components/KeyBar.tsx +0 -67
  139. package/src/tui/components/SessionDetailModal.tsx +0 -169
  140. package/src/tui/components/SessionsView.tsx +0 -184
  141. package/src/tui/components/TabBar.tsx +0 -40
  142. package/src/tui/components/UsageSummaryPane.tsx +0 -272
  143. package/src/tui/components/UsageView.tsx +0 -242
  144. package/src/tui/components/tool-icons.ts +0 -24
  145. package/src/tui/index.tsx +0 -106
  146. package/src/tui/keybindings.ts +0 -49
  147. package/src/tui/model.ts +0 -295
  148. package/src/tui/state.ts +0 -77
  149. package/src/tui/theme.ts +0 -110
  150. package/src/types.ts +0 -113
  151. package/src/usage/README.md +0 -323
  152. package/src/usage/api.ts +0 -99
  153. package/src/usage/client.ts +0 -3
  154. package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
  155. package/src/usage/clients/anthropic-web-usage.ts +0 -329
  156. package/src/usage/clients/claude-cli-probe.ts +0 -513
  157. package/src/usage/clients/codex-app-server.ts +0 -354
  158. package/src/usage/clients/codex-oauth-usage.ts +0 -240
  159. package/src/usage/clients/opencode-go-api.ts +0 -100
  160. package/src/usage/clients/opencode-go-usage.ts +0 -577
  161. package/src/usage/errors.ts +0 -33
  162. package/src/usage/index.ts +0 -20
  163. package/src/usage/local/opencode-go-local.ts +0 -194
  164. package/src/usage/mappers.ts +0 -282
  165. package/src/usage/planner.ts +0 -118
  166. package/src/usage/providers/claude.ts +0 -233
  167. package/src/usage/providers/codex.ts +0 -115
  168. package/src/usage/providers/opencode-go.ts +0 -115
  169. package/src/usage/resolver.ts +0 -4
  170. package/src/usage/resolvers/browser-cookies.ts +0 -808
  171. package/src/usage/resolvers/claude-auth.ts +0 -208
  172. package/src/usage/resolvers/claude-cli.ts +0 -24
  173. package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
  174. package/src/usage/resolvers/claude-web.ts +0 -82
  175. package/src/usage/resolvers/codex-auth.ts +0 -174
  176. package/src/usage/resolvers/keychain-coordinator.ts +0 -397
  177. package/src/usage/resolvers/opencode-go-web.ts +0 -55
  178. package/src/usage/types.ts +0 -225
  179. package/src/usage/web-session-cache.ts +0 -37
  180. /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
  181. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
  182. /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
  183. /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
  184. /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
  185. /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
  186. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
  187. /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
  188. /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
  189. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
  190. /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
  191. /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
  192. /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
  193. /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
  194. /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
  195. /package/{src/tui → dist/cli}/assets/providers/unknown.svg +0 -0
@@ -1,513 +0,0 @@
1
- import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
2
- import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import type { UsageClient } from "../client";
6
- import { classifyUsageError, UsageError } from "../errors";
7
- import type { ClaudeUsageSnapshot, RateWindowLegacy } from "../types";
8
-
9
- export type ClaudeProbeStrategy = "script-darwin" | "script-linux" | "expect" | "plain";
10
-
11
- export class TimeoutError extends UsageError {
12
- constructor(message: string) {
13
- super("transient", message);
14
- this.name = "TimeoutError";
15
- }
16
- }
17
-
18
- interface ProbeOptions {
19
- initialTimeoutMs?: number;
20
- retryTimeoutMs?: number;
21
- killGraceMs?: number;
22
- maxOutputBytes?: number;
23
- cooldownMs?: number;
24
- strategies?: readonly ClaudeProbeStrategy[];
25
- }
26
-
27
- function parseResetDate(text: string): Date | null {
28
- const match = text.match(/Resets at (.+?)(?:\s*\(([^)]+)\))?$/i);
29
- if (!match) return null;
30
- const datePart = match[1]!.trim();
31
- const direct = new Date(datePart);
32
- if (!Number.isNaN(direct.getTime())) return direct;
33
- const withYear = new Date(`${datePart} ${new Date().getFullYear()}`);
34
- return Number.isNaN(withYear.getTime()) ? null : withYear;
35
- }
36
-
37
- function parsePercent(line: string): number | null {
38
- const match = line.match(/(\d{1,3}(?:\.\d+)?)\s*%/);
39
- if (!match) return null;
40
- const value = Number.parseFloat(match[1]!);
41
- return Number.isNaN(value) ? null : value;
42
- }
43
-
44
- function toRateWindow(usedPercent: number, resetText: string | null, windowMinutes: number): RateWindowLegacy {
45
- return {
46
- usedPercent: Math.max(0, Math.min(100, usedPercent)),
47
- windowMinutes,
48
- resetsAt: resetText ? parseResetDate(resetText) : null,
49
- resetDescription: resetText,
50
- };
51
- }
52
-
53
- const SCRIPT_FAILURE_PATTERNS: RegExp[] = [
54
- /tcgetattr/i,
55
- /Operation not supported on socket/i,
56
- ];
57
-
58
- function stripAnsi(text: string): string {
59
- // eslint-disable-next-line no-control-regex
60
- return text.replace(/\u001B\[[0-9;?]*[a-zA-Z]/g, "").replace(/\u001B\][^\u0007]*\u0007/g, "");
61
- }
62
-
63
- function looksLikeLauncherFailure(output: string, maxOutputBytes: number): boolean {
64
- if (!output.trim()) return false;
65
- if (output.length > Math.min(maxOutputBytes, 512)) return false;
66
- const plain = stripAnsi(output);
67
- return SCRIPT_FAILURE_PATTERNS.some((pattern) => pattern.test(plain));
68
- }
69
-
70
- function defaultWaitingStrategies(): ClaudeProbeStrategy[] {
71
- if (process.platform === "darwin") return ["script-darwin", "expect", "plain"];
72
- if (process.platform === "linux") {
73
- const strategies: ClaudeProbeStrategy[] = [];
74
- if (existsSync("/usr/bin/script")) strategies.push("script-linux");
75
- if (existsSync("/usr/bin/expect")) strategies.push("expect");
76
- strategies.push("plain");
77
- return strategies;
78
- }
79
- return ["plain"];
80
- }
81
-
82
- function buildExpectScript(binaryPath: string, timeoutMs: number): string {
83
- const seconds = Math.max(5, Math.ceil(timeoutMs / 1000));
84
- const usageAt = Math.min(4000, Math.max(2500, seconds * 250));
85
- const statusAt = usageAt + 2000;
86
- const exitAt = Math.max(statusAt + 1000, timeoutMs - 1000);
87
- const escaped = binaryPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
88
- return `set timeout ${seconds}
89
- spawn "${escaped}"
90
- after ${usageAt} { send "/usage\\r" }
91
- after ${statusAt} { send "/status\\r" }
92
- after ${exitAt} { exit 0 }
93
- expect eof
94
- `;
95
- }
96
-
97
- /**
98
- * Runs one launcher strategy to capture stdout/stderr, killing the process on
99
- * timeout. Resolves with captured text and nothing else: a launcher that
100
- * produces no bytes or only failure signatures is retried by the caller.
101
- */
102
- function spawnStrategy( strategy: ClaudeProbeStrategy,
103
- binaryPath: string,
104
- timeoutMs: number,
105
- signal?: AbortSignal,
106
- maxOutputBytes = 1024 * 1024,
107
- killGraceMs = 500,
108
- ): Promise<string> {
109
- const useScript = process.platform === "darwin";
110
- let command = binaryPath;
111
- let args: string[] = [];
112
- let stdin: string | null = null;
113
- let expectDir: string | null = null;
114
-
115
- if (strategy === "script-darwin") {
116
- command = "script";
117
- args = ["-q", "/dev/null", binaryPath];
118
- } else if (strategy === "script-linux") {
119
- command = "script";
120
- args = ["-q", "-c", binaryPath, "/dev/null"];
121
- } else if (strategy === "expect") {
122
- expectDir = mkdtempSync(join(tmpdir(), "scrylog-expect-"));
123
- const scriptPath = join(expectDir, "probe.exp");
124
- writeFileSync(scriptPath, buildExpectScript(binaryPath, timeoutMs), { mode: 0o600 });
125
- command = "expect";
126
- args = [scriptPath];
127
- }
128
-
129
- const cleanupDir = (path: string | null): void => {
130
- if (!path) return;
131
- try {
132
- rmSync(path, { recursive: true, force: true });
133
- } catch {
134
- // Bun can throw EFAULT when rmSync races with closeNotify teardown of
135
- // the expect child; the temp dir is per-run and below tmpdir(), so a
136
- // missed unlink is harmless and must not kill the daemon.
137
- }
138
- };
139
-
140
- return new Promise((resolve, reject) => {
141
- if (signal?.aborted) {
142
- cleanupDir(expectDir);
143
- reject(new UsageError("cancelled", "Claude CLI probe cancelled"));
144
- return;
145
- }
146
-
147
- let child: ChildProcessWithoutNullStreams;
148
- try {
149
- child = spawn(command, args, { env: process.env, stdio: ["pipe", "pipe", "pipe"] });
150
- } catch (error) {
151
- cleanupDir(expectDir);
152
- reject(new UsageError("transient", `Could not launch Claude CLI: ${error instanceof Error ? error.message : String(error)}`));
153
- return;
154
- }
155
- const chunks: Buffer[] = [];
156
- let outputBytes = 0;
157
- let terminalError: Error | null = null;
158
- let terminating = false;
159
- let settled = false;
160
- let killTimer: ReturnType<typeof setTimeout> | undefined;
161
-
162
- const commandTimers: Array<ReturnType<typeof setTimeout>> = [];
163
- const clearTimers = () => {
164
- clearTimeout(timeoutTimer);
165
- for (const timer of commandTimers) clearTimeout(timer);
166
- if (killTimer) clearTimeout(killTimer);
167
- };
168
- const terminate = (error: Error) => {
169
- if (terminating || settled) return;
170
- terminating = true;
171
- terminalError = error;
172
- child.stdin.end();
173
- child.kill("SIGTERM");
174
- killTimer = setTimeout(() => child.kill("SIGKILL"), killGraceMs);
175
- };
176
- const onAbort = () => terminate(new UsageError("cancelled", "Claude CLI probe cancelled"));
177
- const timeoutTimer = setTimeout(
178
- () => terminate(new TimeoutError(`Claude CLI probe (${strategy}) timed out after ${timeoutMs}ms`)),
179
- timeoutMs,
180
- );
181
-
182
- signal?.addEventListener("abort", onAbort, { once: true });
183
- const capture = (data: Buffer) => {
184
- if (terminating) return;
185
- outputBytes += data.byteLength;
186
- if (outputBytes > maxOutputBytes) {
187
- terminate(new UsageError("invalid-response", `Claude CLI output exceeded ${maxOutputBytes} bytes`));
188
- return;
189
- }
190
- chunks.push(Buffer.from(data));
191
- };
192
- child.stdout.on("data", capture);
193
- child.stderr.on("data", capture);
194
- child.once("error", (error) => {
195
- if (settled) return;
196
- settled = true;
197
- clearTimers();
198
- cleanupDir(expectDir);
199
- signal?.removeEventListener("abort", onAbort);
200
- reject(new UsageError("transient", `Could not launch Claude CLI (${strategy}): ${error.message}`));
201
- });
202
- child.once("close", () => {
203
- if (settled) return;
204
- settled = true;
205
- clearTimers();
206
- cleanupDir(expectDir);
207
- signal?.removeEventListener("abort", onAbort);
208
- if (terminalError) reject(terminalError);
209
- else resolve(Buffer.concat(chunks).toString("utf8"));
210
- });
211
-
212
- if (stdin) {
213
- child.stdin.write(stdin);
214
- child.stdin.end();
215
- commandTimers.push(setTimeout(() => child.stdin.end(), 500));
216
- return;
217
- }
218
- child.stdin.write("/usage\n");
219
- commandTimers.push(setTimeout(() => {
220
- if (terminating || child.stdin.destroyed) return;
221
- child.stdin.write("/status\n");
222
- commandTimers.push(setTimeout(() => child.stdin.end(), 500));
223
- }, 500));
224
- });
225
- }
226
-
227
- export function probeClaudeCli(
228
- binaryPath: string,
229
- timeoutMs: number,
230
- signal?: AbortSignal,
231
- maxOutputBytes = 1024 * 1024,
232
- killGraceMs = 500,
233
- strategies: readonly ClaudeProbeStrategy[] = defaultWaitingStrategies(),
234
- spawnOne: typeof spawnStrategy = spawnStrategy,
235
- ): Promise<string> {
236
- const deadline = Date.now() + timeoutMs;
237
- return (async () => {
238
- let lastFailure: Error | null = null;
239
- for (const strategy of strategies) {
240
- const remaining = deadline - Date.now();
241
- if (remaining < 2_500) break;
242
- try {
243
- const output = await spawnOne(
244
- strategy,
245
- binaryPath,
246
- remaining,
247
- signal,
248
- maxOutputBytes,
249
- killGraceMs,
250
- );
251
- if (output.trim() && !looksLikeLauncherFailure(output, maxOutputBytes)) return output;
252
- // The launcher itself failed (for example `script` without a TTY on
253
- // socket stdin) — fall through to the next strategy.
254
- } catch (catchError) {
255
- if (classifyUsageError(catchError).kind === "cancelled") throw catchError;
256
- lastFailure = catchError instanceof Error ? catchError : new Error(String(catchError));
257
- }
258
- }
259
- throw new UsageError(
260
- "transient",
261
- `Could not launch Claude CLI in a usable terminal (tried: ${strategies.join(" → ")}; last: ${lastFailure?.message ?? "no output"})`,
262
- );
263
- })();
264
- }
265
-
266
- export class ClaudeCliProbeClient implements UsageClient<string, ClaudeUsageSnapshot> {
267
- private readonly inFlight = new Map<string, Promise<ClaudeUsageSnapshot>>();
268
- private readonly cooldownUntil = new Map<string, number>();
269
-
270
- constructor(
271
- private readonly options: ProbeOptions = {},
272
- private readonly probe: (
273
- binaryPath: string,
274
- timeoutMs: number,
275
- signal?: AbortSignal,
276
- maxOutputBytes?: number,
277
- killGraceMs?: number,
278
- strategies?: readonly ClaudeProbeStrategy[],
279
- ) => Promise<string> = probeClaudeCli,
280
- ) {}
281
-
282
- fetch(binaryPath: string, options?: { signal?: AbortSignal }): Promise<ClaudeUsageSnapshot> {
283
- const existing = this.inFlight.get(binaryPath);
284
- if (existing) return this.waitForShared(existing, options?.signal);
285
- if ((this.cooldownUntil.get(binaryPath) ?? 0) > Date.now()) {
286
- return Promise.reject(new UsageError("transient", "Claude CLI recovery is cooling down after a failed attempt"));
287
- }
288
-
289
- const attempt = this.fetchOnce(binaryPath, options?.signal)
290
- .catch((error) => {
291
- // A cancelled poll is not a failed attempt: the next poll should retry
292
- // immediately instead of being penalized by the failure cooldown.
293
- if (classifyUsageError(error).kind !== "cancelled") {
294
- this.cooldownUntil.set(binaryPath, Date.now() + (this.options.cooldownMs ?? 30_000));
295
- }
296
- throw error;
297
- })
298
- .finally(() => this.inFlight.delete(binaryPath));
299
- this.inFlight.set(binaryPath, attempt);
300
- return attempt;
301
- }
302
-
303
- private async fetchOnce(binaryPath: string, signal?: AbortSignal): Promise<ClaudeUsageSnapshot> {
304
- let output: string;
305
- try {
306
- output = await this.probe(
307
- binaryPath,
308
- this.options.initialTimeoutMs ?? 10_000,
309
- signal,
310
- this.options.maxOutputBytes,
311
- this.options.killGraceMs,
312
- this.options.strategies,
313
- );
314
- } catch (error) {
315
- if (!(error instanceof TimeoutError) || signal?.aborted) throw error;
316
- output = await this.probe(
317
- binaryPath,
318
- this.options.retryTimeoutMs ?? 24_000,
319
- signal,
320
- this.options.maxOutputBytes,
321
- this.options.killGraceMs,
322
- this.options.strategies,
323
- );
324
- }
325
- return parseClaudeCliOutput(output);
326
- }
327
-
328
- private waitForShared(promise: Promise<ClaudeUsageSnapshot>, signal?: AbortSignal): Promise<ClaudeUsageSnapshot> {
329
- if (!signal) return promise;
330
- if (signal.aborted) return Promise.reject(new UsageError("cancelled", "Claude CLI probe cancelled"));
331
- return new Promise((resolve, reject) => {
332
- const abort = () => reject(new UsageError("cancelled", "Claude CLI probe cancelled"));
333
- signal.addEventListener("abort", abort, { once: true });
334
- promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
335
- });
336
- }
337
- }
338
-
339
- export function parseClaudeCliOutput(output: string): ClaudeUsageSnapshot {
340
- const lines = output.split(/\r?\n/);
341
- if (claudeCliOutputIndicatesExpiredSession(output)) {
342
- throw new UsageError("invalid-credentials", "Claude CLI session expired — run \"claude\" to re-login");
343
- }
344
- let primary: RateWindowLegacy | null = null;
345
- let secondary: RateWindowLegacy | null = null;
346
- let opus: RateWindowLegacy | null = null;
347
- let accountEmail: string | null = null;
348
- let accountOrganization: string | null = null;
349
- const accountOrganizationId: string | null = null;
350
- let loginMethod: string | null = null;
351
-
352
- for (let i = 0; i < lines.length; i++) {
353
- const line = lines[i]!;
354
- const lower = line.toLowerCase();
355
- const identity = line.match(/(account_email|account_org|login_method)[:=]\s*(.+)/i);
356
- if (identity?.[1].toLowerCase() === "account_email") accountEmail = identity[2]!.trim();
357
- if (identity?.[1].toLowerCase() === "account_org") accountOrganization = identity[2]!.trim();
358
- if (identity?.[1].toLowerCase() === "login_method") loginMethod = identity[2]!.trim();
359
-
360
- const percent = parsePercent(line);
361
- const resetLine = lines[i + 1] || "";
362
- const resetText = resetLine.includes("Resets") ? resetLine.trim() : null;
363
- if (lower.includes("current session") && percent !== null) {
364
- primary = toRateWindow(100 - percent, resetText, 300);
365
- }
366
- if (lower.includes("current week") && percent !== null) {
367
- const window = toRateWindow(100 - percent, resetText, 10080);
368
- if (lower.includes("opus") || lower.includes("sonnet only")) opus = window;
369
- else secondary = window;
370
- }
371
- }
372
-
373
- if (!primary) {
374
- const modern = parseModernClaudeUsageOutput(lines, output);
375
- if (modern) return modern;
376
- if (outputIndicatesNoPlanUsage(output)) {
377
- throw new UsageError(
378
- "invalid-response",
379
- "Claude CLI output contains no plan usage windows (plan may not expose usage — e.g. API Usage Billing)",
380
- );
381
- }
382
- throw new UsageError(
383
- "invalid-response",
384
- `Could not parse primary usage window from Claude CLI output (${describeUnparseableOutput(output)})`,
385
- );
386
- }
387
- return {
388
- source: "cli",
389
- primary,
390
- secondary,
391
- opus,
392
- extraRateWindows: [],
393
- providerCost: null,
394
- updatedAt: new Date(),
395
- accountEmail,
396
- accountOrganization,
397
- accountOrganizationId,
398
- loginMethod,
399
- subscriptionType: null,
400
- rateLimitTier: null,
401
- };
402
- }
403
-
404
- /**
405
- * Scanner for current Claude Code `/usage` renderings (v2.6.x "Usage" tabs and
406
- * later). The legacy "Current session: NN%" block is gone; the panel renders
407
- * `NN%`-bearing rows whose labels mention session/week tokens elsewhere on the
408
- * same line or its immediate neighbors. Percentage values are remaining
409
- * (legacy conversion applies when the row reads as "remaining").
410
- */
411
- export function parseModernClaudeUsageOutput(lines: string[], output: string): ClaudeUsageSnapshot | null {
412
- const plain = lines.map((line) => stripAnsi(line));
413
- const collapsed = plain.join("\n").replace(/[ ]+/g, " ").toLowerCase();
414
- if (!/\b\d{1,3}(?:\.\d+)?\s*%/.test(collapsed)) return null;
415
-
416
- interface WindowHit {
417
- percent: number;
418
- resetText: string | null;
419
- windowMinutes: number;
420
- kind: "primary" | "secondary" | "opus";
421
- }
422
- const hits: WindowHit[] = [];
423
- for (let i = 0; i < plain.length; i++) {
424
- const raw = plain[i]!;
425
- const lower = raw.toLowerCase();
426
- const percent = parsePercent(raw);
427
- if (percent === null) continue; // Consider a bounded text window around the percent occurrence: the same
428
- // line and the immediately previous line carry the window label (the
429
- // terminal renders the panel in a grid; legacy puts both on one line,
430
- // modern renderings may separate them by one row).
431
- const label = lower;
432
- const labelContext = `${plain[i - 1] ?? ""} ${raw}`.toLowerCase();
433
-
434
- const reset = findResetNearby(plain, i);
435
-
436
- if (
437
- /(?:5[- ]?hour|session|rolling)/.test(labelContext) ||
438
- /(?:5[- ]?hour|session|rolling)/.test(label)
439
- ) {
440
- if (!hits.some((hit) => hit.kind === "primary")) {
441
- hits.push({ kind: "primary", percent, resetText: reset, windowMinutes: 300 });
442
- }
443
- continue;
444
- }
445
- if (/(?:week|7[- ]?day)/.test(labelContext)) {
446
- const isOpus = /opus|sonnet only/.test(labelContext);
447
- const kind: WindowHit["kind"] = isOpus ? "opus" : "secondary";
448
- if (!hits.some((hit) => hit.kind === kind)) {
449
- hits.push({ kind, percent, resetText: reset, windowMinutes: 10080 });
450
- }
451
- }
452
- }
453
-
454
- const primary = hits.find((hit) => hit.kind === "primary");
455
- const secondary = hits.find((hit) => hit.kind === "secondary");
456
- const opus = hits.find((hit) => hit.kind === "opus");
457
- if (!primary) return null;
458
-
459
- // Claude renders remaining percentages in both legacy and modern panels;
460
- // normalize to usedPercent like the legacy path.
461
- const remainingToUsed = (percent: number): number => Math.max(0, Math.min(100, 100 - percent));
462
-
463
- const identity = output.match(/(account_email|account_org|login_method)[:=]\s*(.+)/i);
464
- return {
465
- source: "cli",
466
- primary: toRateWindow(remainingToUsed(primary.percent), primary.resetText, primary.windowMinutes),
467
- secondary: secondary ? toRateWindow(remainingToUsed(secondary.percent), secondary.resetText, secondary.windowMinutes) : null,
468
- opus: opus ? toRateWindow(remainingToUsed(opus.percent), opus.resetText, opus.windowMinutes) : null,
469
- extraRateWindows: [],
470
- providerCost: null,
471
- updatedAt: new Date(),
472
- accountEmail: identity?.[1]?.toLowerCase() === "account_email" ? identity[2]!.trim() : null,
473
- accountOrganization: identity?.[1]?.toLowerCase() === "account_org" ? identity[2]!.trim() : null,
474
- accountOrganizationId: null,
475
- loginMethod: identity?.[1]?.toLowerCase() === "login_method" ? identity[2]!.trim() : null,
476
- subscriptionType: null,
477
- rateLimitTier: null,
478
- };
479
- }
480
-
481
- function findResetNearby(lines: string[], percentIndex: number): string | null {
482
- for (let offset = 1; offset <= 2; offset++) {
483
- const line = lines[percentIndex + offset];
484
- if (!line) break;
485
- const trimmed = line.trim();
486
- if (/resets?\b/i.test(trimmed)) return trimmed;
487
- }
488
- return null;
489
- }
490
-
491
- const PLAN_USAGE_ABSENT_SIGNATURES: RegExp[] = [/total cost/i, /usage:\s*\d+ (?:input|tokens)/i, /total duration/i];
492
-
493
- function outputIndicatesNoPlanUsage(output: string): boolean {
494
- const plain = stripAnsi(output);
495
- if (/\d{1,3}(?:\.\d+)?\s*%/.test(plain)) return false;
496
- return PLAN_USAGE_ABSENT_SIGNATURES.every((signature) => signature.test(plain));
497
- }
498
-
499
- function describeUnparseableOutput(output: string): string {
500
- const plain = stripAnsi(output);
501
- if (!plain.trim()) return "empty output";
502
- const firstLine = plain.split(/\r?\n/).find((line) => line.trim())?.trim() ?? "";
503
- return firstLine.length > 120 ? `${firstLine.slice(0, 120)}…` : firstLine;
504
- }
505
-
506
- function claudeCliOutputIndicatesExpiredSession(output: string): boolean {
507
- const lower = output.toLowerCase();
508
- return (
509
- lower.includes("failed to authenticate") ||
510
- lower.includes("oauth session expired") ||
511
- lower.includes("could not be refreshed")
512
- );
513
- }