@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,208 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import type { TokenResolver } from "../resolver";
5
- import type { ClaudeOAuthCredentials } from "../types";
6
- import { IS_MAC } from "../../shared/platform";
7
- import { keychainCoordinator, CLAUDE_SAFE_STORAGE_ITEMS } from "./keychain-coordinator";
8
- import { ClaudeDesktopAuthResolver } from "./claude-desktop-auth";
9
-
10
- export const CLAUDE_KEYCHAIN_TOKEN_SERVICES = ["claude-api-oauth", "Claude Code-credentials"] as const;
11
-
12
- const DEFAULT_STALE_RESCAN_INTERVAL_MS = 10 * 60 * 1_000;
13
-
14
- export interface ClaudeAuthResolverOptions {
15
- candidatesSource?: () => Promise<ClaudeOAuthCredentials[]>;
16
- staleRescanIntervalMs?: number;
17
- now?: () => number;
18
- }
19
-
20
- export class ClaudeAuthResolver implements TokenResolver<ClaudeOAuthCredentials> {
21
- private cached?: ClaudeOAuthCredentials;
22
- private nextScanAt = 0;
23
- private readonly staleRescanIntervalMs: number;
24
- private readonly now: () => number;
25
- /** Last blocking keychain description seen while resolving candidates. */
26
- private lastKeychainFailure: string | null = null;
27
-
28
- constructor(
29
- private readonly options: ClaudeAuthResolverOptions = {},
30
- ) {
31
- this.staleRescanIntervalMs = options.staleRescanIntervalMs ?? DEFAULT_STALE_RESCAN_INTERVAL_MS;
32
- this.now = options.now ?? Date.now;
33
- }
34
-
35
- private readonly debug = (message: string, meta?: Record<string, unknown>) => {
36
- // Avoid coupling to logger; stderr debug line is acceptable for resolver diagnostics.
37
- if (process.env.SCRYLOG_DEBUG_AUTH) {
38
- process.stderr.write(`[scrylog:claude-auth] ${message}${meta ? ` ${JSON.stringify(meta)}` : ""}\n`);
39
- }
40
- };
41
-
42
- async resolve(): Promise<ClaudeOAuthCredentials | null> {
43
- const cached = this.cached;
44
- if (cached && !cached.isStale) return cached;
45
- // Debounce full candidate scans (which include keychain reads) while a
46
- // cached-but-stale credential exists, so polls never scan every cycle.
47
- if (cached && this.now() < this.nextScanAt) return cached;
48
- const selected = selectClaudeCredentials(await this.resolveCandidates());
49
- this.cached = selected ?? undefined;
50
- this.nextScanAt = this.now() + this.staleRescanIntervalMs;
51
- this.lastKeychainFailure = selected
52
- ? null
53
- : keychainCoordinator.describeBlockingFailure([
54
- { service: CLAUDE_KEYCHAIN_TOKEN_SERVICES[0]! },
55
- { service: CLAUDE_KEYCHAIN_TOKEN_SERVICES[1]! },
56
- ...CLAUDE_SAFE_STORAGE_ITEMS,
57
- ]);
58
- return this.cached ?? null;
59
- }
60
-
61
- /**
62
- * Actionable keychain diagnostic for the latest resolution attempt, or null
63
- * when the last attempt's failure was not keychain-related (missing items,
64
- * absent caches are not surfaced).
65
- */
66
- describeKeychainFailure(): string | null {
67
- return this.lastKeychainFailure;
68
- }
69
-
70
- /** Invalidates the daemon-lifetime credential cache (e.g. after a 401). */
71
- invalidate(): void {
72
- this.cached = undefined;
73
- this.nextScanAt = 0;
74
- for (const service of CLAUDE_KEYCHAIN_TOKEN_SERVICES) {
75
- keychainCoordinator.clearMemo(service);
76
- }
77
- }
78
-
79
- async resolveCandidates(): Promise<ClaudeOAuthCredentials[]> {
80
- if (this.options.candidatesSource) return this.options.candidatesSource();
81
- return this.resolveCandidatesUncached();
82
- }
83
-
84
- private async resolveCandidatesUncached(): Promise<ClaudeOAuthCredentials[]> {
85
- const candidates: ClaudeOAuthCredentials[] = [];
86
-
87
- // 1. macOS Keychain is the primary store on macOS.
88
- if (IS_MAC) {
89
- for (const service of CLAUDE_KEYCHAIN_TOKEN_SERVICES) {
90
- const creds = await this.tryService(service);
91
- if (creds) candidates.push(creds);
92
- }
93
- } else {
94
- this.debug("Keychain not available on this platform", { platform: process.platform });
95
- }
96
-
97
- // 2. Claude Desktop app local token cache (macOS / Windows only).
98
- // Reads the Electron safeStorage-encrypted `oauth:tokenCache` from
99
- // ~/Library/Application Support/Claude/config.json (macOS) or
100
- // %APPDATA%/Anthropic/Claude/config.json (Windows). This covers the
101
- // common case where the user has Claude Desktop but not Claude Code CLI.
102
- const desktopCreds = await new ClaudeDesktopAuthResolver().resolve();
103
- if (desktopCreds) candidates.push(desktopCreds);
104
-
105
- // 3. Linux/WSL/Windows: Claude Code stores credentials in ~/.claude/.credentials.json
106
- const fileCreds = this.tryCredentialsFile();
107
- if (fileCreds) candidates.push(fileCreds);
108
-
109
- // 4. Universal env-var fallback
110
- const envToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
111
- if (envToken) {
112
- candidates.push({
113
- accessToken: envToken,
114
- owner: "environment",
115
- credentialSource: "environment",
116
- });
117
- }
118
-
119
- return candidates;
120
- }
121
-
122
- async isAvailable(): Promise<boolean> {
123
- return (await this.resolve()) !== null;
124
- }
125
-
126
- private async tryService(service: string): Promise<ClaudeOAuthCredentials | null> {
127
- try {
128
- const output = await keychainCoordinator.read(service);
129
- if (!output) return null;
130
- return parseClaudeCredentialsJson(output, "keychain");
131
- } catch (error) {
132
- this.debug("Keychain lookup failed", { service, error: error instanceof Error ? error.message : String(error) });
133
- return null;
134
- }
135
- }
136
-
137
- private tryCredentialsFile(): ClaudeOAuthCredentials | null {
138
- // On macOS, Keychain is authoritative; skip the file unless explicitly requested.
139
- if (IS_MAC && !process.env.CLAUDE_CODE_USE_CREDENTIALS_FILE) return null;
140
-
141
- const claudeDir = process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
142
- const path = join(claudeDir, ".credentials.json");
143
- try {
144
- const raw = readFileSync(path, "utf8");
145
- return parseClaudeCredentialsJson(raw, "credentials-file");
146
- } catch (error) {
147
- this.debug("Failed to read credentials file", { path, error: error instanceof Error ? error.message : String(error) });
148
- return null;
149
- }
150
- }
151
-
152
- }
153
-
154
- const EXPIRY_SKEW_MS = 60_000;
155
-
156
- function parseExpiry(value: unknown): number | undefined | null {
157
- if (value === undefined || value === null || value === "") return undefined;
158
- const millis = typeof value === "number"
159
- ? value
160
- : typeof value === "string" && /^\d+$/.test(value.trim())
161
- ? Number(value)
162
- : typeof value === "string"
163
- ? Date.parse(value)
164
- : Number.NaN;
165
- return Number.isFinite(millis) && millis > 0 ? millis : null;
166
- }
167
-
168
- export function parseClaudeCredentialsJson(
169
- input: string,
170
- credentialSource: "keychain" | "credentials-file",
171
- now = Date.now(),
172
- ): ClaudeOAuthCredentials | null {
173
- let parsed: unknown;
174
- try {
175
- parsed = JSON.parse(input);
176
- } catch {
177
- return null;
178
- }
179
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
180
- const outer = parsed as Record<string, unknown>;
181
- const inner = outer.claudeAiOauth && typeof outer.claudeAiOauth === "object" && !Array.isArray(outer.claudeAiOauth)
182
- ? outer.claudeAiOauth as Record<string, unknown>
183
- : outer;
184
- const token = inner.accessToken ?? inner.access_token;
185
- if (typeof token !== "string" || !token.trim()) return null;
186
-
187
- const expiryMillis = parseExpiry(inner.expiresAt ?? inner.expires_at);
188
- if (expiryMillis === null) return null;
189
- return {
190
- accessToken: token.trim(),
191
- refreshToken: typeof (inner.refreshToken ?? inner.refresh_token) === "string"
192
- ? inner.refreshToken as string ?? inner.refresh_token as string
193
- : undefined,
194
- expiresAt: expiryMillis === undefined ? undefined : new Date(expiryMillis).toISOString(),
195
- owner: "claude",
196
- credentialSource,
197
- isStale: expiryMillis === undefined || expiryMillis <= now + EXPIRY_SKEW_MS,
198
- scopes: Array.isArray(inner.scopes) && inner.scopes.every((scope) => typeof scope === "string")
199
- ? inner.scopes
200
- : undefined,
201
- rateLimitTier: typeof inner.rateLimitTier === "string" ? inner.rateLimitTier : undefined,
202
- subscriptionType: typeof inner.subscriptionType === "string" ? inner.subscriptionType : undefined,
203
- };
204
- }
205
-
206
- export function selectClaudeCredentials(candidates: ClaudeOAuthCredentials[]): ClaudeOAuthCredentials | null {
207
- return candidates.find((candidate) => !candidate.isStale) ?? candidates[0] ?? null;
208
- }
@@ -1,24 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { whichCommand } from "../../shared/platform";
4
- import type { TokenResolver } from "../resolver";
5
-
6
- export class ClaudeCliResolver implements TokenResolver<string> {
7
- async resolve(): Promise<string | null> {
8
- const paths = [
9
- process.env.CLAUDE_BIN_OVERRIDE,
10
- `${homedir()}/.claude/local/claude`,
11
- "/usr/local/bin/claude",
12
- ];
13
- for (const p of paths) {
14
- if (p && existsSync(p)) return p;
15
- }
16
- const which = whichCommand("claude");
17
- if (which) return which;
18
- return null;
19
- }
20
-
21
- async isAvailable(): Promise<boolean> {
22
- return (await this.resolve()) !== null;
23
- }
24
- }
@@ -1,170 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import type { TokenResolver } from "../resolver";
5
- import type { ClaudeOAuthCredentials } from "../types";
6
- import { IS_MAC, IS_WINDOWS } from "../../shared/platform";
7
- import { getChromiumSafeStorageKey, decryptSafeStorageBlob } from "./browser-cookies";
8
- import { keychainCoordinator, CLAUDE_SAFE_STORAGE_ITEMS } from "./keychain-coordinator";
9
-
10
- /**
11
- * Shape of a single entry in the `oauth:tokenCache` field of Claude Desktop's
12
- * `config.json`. The key in the outer Record is a colon-separated composite:
13
- * `<accountId>:<orgId>:<audience>:<space-separated scopes>`
14
- */
15
- interface TokenCacheEntry {
16
- token: string;
17
- refreshToken?: string;
18
- /** Milliseconds since Unix epoch */
19
- expiresAt?: number;
20
- subscriptionType?: string;
21
- rateLimitTier?: string;
22
- }
23
-
24
- /**
25
- * Reads the Claude Desktop app's local OAuth token cache and surfaces it as a
26
- * `ClaudeOAuthCredentials` so the OAuth usage route works without Claude Code CLI.
27
- *
28
- * Data source:
29
- * macOS: ~/Library/Application Support/Claude/config.json → oauth:tokenCache
30
- * Windows: %APPDATA%/Anthropic/Claude/config.json → oauth:tokenCache
31
- * Linux: Claude Desktop is unavailable; resolver always returns null.
32
- *
33
- * The `oauth:tokenCache` value is an Electron safeStorage blob (base64, v10/v11
34
- * AES-128-CBC) decrypted with the Keychain key from "Claude Safe Storage".
35
- */
36
- export class ClaudeDesktopAuthResolver implements TokenResolver<ClaudeOAuthCredentials> {
37
- private readonly debug = (message: string, meta?: Record<string, unknown>) => {
38
- if (process.env.SCRYLOG_DEBUG_AUTH) {
39
- process.stderr.write(
40
- `[scrylog:claude-desktop-auth] ${message}${meta ? ` ${JSON.stringify(meta)}` : ""}\n`
41
- );
42
- }
43
- };
44
-
45
- async resolve(): Promise<ClaudeOAuthCredentials | null> {
46
- const configPath = this.getConfigPath();
47
- if (!configPath) {
48
- this.debug("Claude Desktop not supported on this platform", { platform: process.platform });
49
- return null;
50
- }
51
- if (!existsSync(configPath)) {
52
- this.debug("Claude Desktop config.json not found", { path: configPath });
53
- return null;
54
- }
55
-
56
- try {
57
- const raw = readFileSync(configPath, "utf8");
58
- const cfg = JSON.parse(raw) as Record<string, unknown>;
59
- const blobB64 = cfg["oauth:tokenCache"];
60
- if (typeof blobB64 !== "string" || !blobB64) {
61
- this.debug("oauth:tokenCache missing or not a string in config.json");
62
- return null;
63
- }
64
-
65
- const buf = Buffer.from(blobB64, "base64");
66
- const key = await getChromiumSafeStorageKey("Claude Safe Storage", "Claude");
67
- if (!key) {
68
- this.debug("Failed to retrieve Claude Safe Storage key from Keychain");
69
- return null;
70
- }
71
-
72
- const plaintext = decryptSafeStorageBlob(buf, key);
73
- if (!plaintext) {
74
- this.debug("Failed to decrypt oauth:tokenCache blob");
75
- return null;
76
- }
77
-
78
- const cache = JSON.parse(plaintext) as Record<string, TokenCacheEntry>;
79
- const creds = this.selectBestEntry(cache);
80
- if (creds) {
81
- this.debug("Resolved Claude Desktop OAuth credentials");
82
- } else {
83
- this.debug("No usable entry found in oauth:tokenCache");
84
- }
85
- return creds;
86
- } catch (err) {
87
- this.debug("Error reading Claude Desktop config", {
88
- error: err instanceof Error ? err.message : String(err),
89
- });
90
- return null;
91
- }
92
- }
93
-
94
- async isAvailable(): Promise<boolean> {
95
- return (await this.resolve()) !== null;
96
- }
97
-
98
- /** Actionable keychain diagnostic for the latest resolution attempt. */
99
- describeKeychainFailure(): string | null {
100
- return keychainCoordinator.describeBlockingFailure(CLAUDE_SAFE_STORAGE_ITEMS);
101
- }
102
-
103
- private getConfigPath(): string | null {
104
- const home = homedir();
105
- if (IS_MAC) {
106
- return join(home, "Library", "Application Support", "Claude", "config.json");
107
- }
108
- if (IS_WINDOWS) {
109
- const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
110
- return join(appData, "Anthropic", "Claude", "config.json");
111
- }
112
- // Linux: Claude Desktop is not available
113
- return null;
114
- }
115
-
116
- /**
117
- * Pick the best (longest-lived, has user:profile scope) non-expired token entry.
118
- *
119
- * The cache key format is:
120
- * `<accountId>:<orgId>:<audience>:<space-separated scopes>`
121
- * e.g.
122
- * `uuid1:uuid2:https://api.anthropic.com:user:inference user:file_upload user:profile ...`
123
- *
124
- * AnthropicOAuthUsageClient requires the `user:profile` scope, so entries that
125
- * lack it are skipped.
126
- */
127
- private selectBestEntry(
128
- cache: Record<string, TokenCacheEntry>
129
- ): ClaudeOAuthCredentials | null {
130
- const now = Date.now();
131
- let bestCreds: ClaudeOAuthCredentials | null = null;
132
- let bestExpiresAt = -Infinity;
133
- let bestIsStale = true;
134
-
135
- for (const [scopeKey, entry] of Object.entries(cache)) {
136
- if (!entry.token) continue;
137
- // Require user:profile scope for the OAuth usage endpoint
138
- if (!scopeKey.includes("user:profile")) continue;
139
-
140
- const isStale = entry.expiresAt === undefined || entry.expiresAt <= now + 60_000;
141
- const expiresAt = entry.expiresAt ?? -Infinity;
142
- if ((!isStale && bestIsStale) || (isStale === bestIsStale && expiresAt > bestExpiresAt)) {
143
- bestExpiresAt = expiresAt;
144
- bestIsStale = isStale;
145
-
146
- // Parse space-separated scopes from the composite key.
147
- // Key format after the audience URL: "...:<audience>:<scopes>"
148
- const audienceMatch = scopeKey.match(/:[^:]+:\/\/[^:]+:(.+)$/);
149
- const scopes = audienceMatch ? audienceMatch[1].split(" ") : undefined;
150
-
151
- bestCreds = {
152
- accessToken: entry.token,
153
- refreshToken: entry.refreshToken,
154
- expiresAt:
155
- entry.expiresAt !== undefined
156
- ? new Date(entry.expiresAt).toISOString()
157
- : undefined,
158
- owner: "claude",
159
- credentialSource: "claude-desktop",
160
- isStale,
161
- scopes,
162
- subscriptionType: entry.subscriptionType,
163
- rateLimitTier: entry.rateLimitTier,
164
- };
165
- }
166
- }
167
-
168
- return bestCreds;
169
- }
170
- }
@@ -1,82 +0,0 @@
1
- import type { TokenResolver } from "../resolver";
2
- import type { ClaudeWebSession } from "../types";
3
- import { usageWebSessionCache } from "../web-session-cache";
4
- import { BrowserCookieSourceCursor, buildCookieHeader, hasBrowserCookieSources } from "./browser-cookies";
5
- import { keychainCoordinator, CLAUDE_SAFE_STORAGE_ITEMS } from "./keychain-coordinator";
6
-
7
- export const CLAUDE_WEB_CACHE_KEY = "claude-web";
8
-
9
- export class ClaudeWebResolver implements TokenResolver<ClaudeWebSession> {
10
- constructor(
11
- private readonly cursorFactory: () => BrowserCookieSourceCursor = () =>
12
- new BrowserCookieSourceCursor("claude.ai", ["sessionKey"], { preferredBrowserId: "claude-desktop" }),
13
- ) {}
14
-
15
- resolve(): Promise<ClaudeWebSession | null> {
16
- return Promise.resolve().then(() => this.buildSession());
17
- }
18
-
19
- /**
20
- * Cache-only resolution for planning and Web extras. Never reads a browser
21
- * cookie store and never requests a Safe Storage key.
22
- */
23
- async resolveCached(): Promise<ClaudeWebSession | null> {
24
- const observed = usageWebSessionCache.observe(CLAUDE_WEB_CACHE_KEY);
25
- if (!observed.value) return null;
26
- return {
27
- cookieHeader: observed.value,
28
- cookieCandidates: [observed.value],
29
- cacheRevision: observed.revision,
30
- };
31
- }
32
-
33
- /**
34
- * True when an import could yield candidates: a cached value exists or some
35
- * browser import source is present. Cheap existence checks only — no SQLite
36
- * reads and no Keychain requests.
37
- */
38
- async hasImportSources(): Promise<boolean> {
39
- if (usageWebSessionCache.observe(CLAUDE_WEB_CACHE_KEY).value) return true;
40
- return hasBrowserCookieSources();
41
- }
42
-
43
- private async buildSession(): Promise<ClaudeWebSession | null> {
44
- const observed = usageWebSessionCache.observe(CLAUDE_WEB_CACHE_KEY);
45
- if (observed.value) {
46
- return {
47
- cookieHeader: observed.value,
48
- cookieCandidates: [observed.value],
49
- cacheRevision: observed.revision,
50
- };
51
- }
52
-
53
- // Cache miss: import the first source. Later sources are only inspected
54
- // when the client advances the cursor after a classified auth rejection.
55
- const cursor = this.cursorFactory();
56
- const candidate = await cursor.next();
57
- if (!candidate) return null;
58
- const header = buildCookieHeader(candidate.cookies);
59
- if (!header) return null;
60
- return {
61
- cookieHeader: header,
62
- cookieCandidates: [header],
63
- cacheRevision: observed.revision,
64
- importNext: async () => {
65
- const next = await cursor.next();
66
- return next ? buildCookieHeader(next.cookies) : null;
67
- },
68
- };
69
- }
70
-
71
- async isAvailable(): Promise<boolean> {
72
- return (await this.resolve()) !== null;
73
- }
74
-
75
- /**
76
- * Actionable keychain diagnostic for the latest import attempt (e.g. the
77
- * Safe Storage read failed), or null when the miss was not keychain-related.
78
- */
79
- describeKeychainFailure(): string | null {
80
- return keychainCoordinator.describeBlockingFailure(CLAUDE_SAFE_STORAGE_ITEMS);
81
- }
82
- }
@@ -1,174 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import type { TokenResolver } from "../resolver";
5
- import type { CodexOAuthCredentials } from "../types";
6
- import { UsageError } from "../errors";
7
-
8
- const NATIVE_EXPIRY_MARGIN_MS = 5 * 60 * 1_000;
9
- const NATIVE_REFRESH_MAX_AGE_MS = 8 * 24 * 60 * 60 * 1_000;
10
-
11
- export class CodexCredentialSourceError extends UsageError {
12
- constructor(
13
- public readonly reason: "malformed" | "unreadable",
14
- message: string,
15
- ) {
16
- super(reason === "malformed" ? "invalid-response" : "transient", message);
17
- this.name = "CodexCredentialSourceError";
18
- }
19
- }
20
-
21
- function getCodexDir(): string {
22
- if (process.env.CODEX_HOME) return process.env.CODEX_HOME;
23
- return join(homedir(), ".codex");
24
- }
25
-
26
- function readConfigToml(codexDir: string): string | undefined {
27
- const path = join(codexDir, "config.toml");
28
- try {
29
- const content = readFileSync(path, "utf8");
30
- for (const line of content.split(/\r?\n/)) {
31
- const match = line.match(/^chatgpt_base_url\s*=\s*"(.+)"\s*$/);
32
- if (match) return match[1];
33
- }
34
- } catch {
35
- // ignore
36
- }
37
- return undefined;
38
- }
39
-
40
- function decodeJwtClaims(token: string): { email?: string; sub?: string; org_id?: string } {
41
- try {
42
- const payload = token.split(".")[1];
43
- if (!payload) return {};
44
- const json = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as Record<string, unknown>;
45
- return {
46
- email: typeof json["email"] === "string" ? json["email"] : undefined,
47
- sub: typeof json["sub"] === "string" ? json["sub"] : undefined,
48
- org_id: typeof json["org_id"] === "string" ? json["org_id"] : undefined,
49
- };
50
- } catch {
51
- return {};
52
- }
53
- }
54
-
55
- function decodeJwtExpiry(token: string): number | undefined {
56
- try {
57
- const payload = token.split(".")[1];
58
- if (!payload) return undefined;
59
- const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as Record<string, unknown>;
60
- return typeof claims["exp"] === "number" && Number.isFinite(claims["exp"])
61
- ? claims["exp"] * 1_000
62
- : undefined;
63
- } catch {
64
- return undefined;
65
- }
66
- }
67
-
68
- function parseTimestamp(value: unknown): number | undefined {
69
- if (typeof value !== "string") return undefined;
70
- const timestamp = Date.parse(value);
71
- return Number.isNaN(timestamp) ? undefined : timestamp;
72
- }
73
-
74
- export function isCodexCredentialFresh(
75
- credentials: CodexOAuthCredentials,
76
- now = Date.now(),
77
- ): boolean {
78
- if (credentials.tokenExpiresAt !== undefined) {
79
- return credentials.tokenExpiresAt > now + NATIVE_EXPIRY_MARGIN_MS;
80
- }
81
- if (credentials.lastRefreshAt !== undefined) {
82
- return now - credentials.lastRefreshAt < NATIVE_REFRESH_MAX_AGE_MS;
83
- }
84
- return false;
85
- }
86
-
87
- /**
88
- * Extract access_token and related fields from Codex auth.json.
89
- * Codex CLI writes credentials in a nested "tokens" object, e.g.:
90
- * { "tokens": { "access_token": "...", "refresh_token": "...", "account_id": "..." } }
91
- * Some versions may write flat keys. This helper supports both formats.
92
- */
93
- function extractTokens(raw: Record<string, unknown>): {
94
- accessToken: string | undefined;
95
- accountId: string | undefined;
96
- refreshToken: string | undefined;
97
- } {
98
- const tokens =
99
- typeof raw["tokens"] === "object" && raw["tokens"] !== null
100
- ? (raw["tokens"] as Record<string, unknown>)
101
- : undefined;
102
-
103
- const accessToken =
104
- typeof tokens?.["access_token"] === "string"
105
- ? tokens["access_token"]
106
- : typeof raw["access_token"] === "string"
107
- ? raw["access_token"]
108
- : undefined;
109
-
110
- const accountId =
111
- typeof tokens?.["account_id"] === "string" ? tokens["account_id"] : undefined;
112
-
113
- const refreshToken =
114
- typeof raw["refresh_token"] === "string"
115
- ? raw["refresh_token"]
116
- : typeof tokens?.["refresh_token"] === "string"
117
- ? tokens["refresh_token"]
118
- : undefined;
119
-
120
- return { accessToken, accountId, refreshToken };
121
- }
122
-
123
- export class CodexAuthResolver implements TokenResolver<CodexOAuthCredentials> {
124
- constructor(private readonly configuredBaseUrl?: string) {}
125
-
126
- async resolve(): Promise<CodexOAuthCredentials | null> {
127
- const codexDir = getCodexDir();
128
- const authPath = join(codexDir, "auth.json");
129
- let content: string;
130
- try {
131
- content = readFileSync(authPath, "utf8");
132
- } catch (error) {
133
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
134
- throw new CodexCredentialSourceError("unreadable", "Codex auth.json could not be read");
135
- }
136
-
137
- try {
138
- const parsed = JSON.parse(content) as unknown;
139
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
140
- throw new Error("expected a JSON object");
141
- }
142
- const raw = parsed as Record<string, unknown>;
143
- const tokens = extractTokens(raw);
144
- if (!tokens.accessToken) return null;
145
-
146
- const claims = decodeJwtClaims(tokens.accessToken);
147
- const tokenExpiresAt = decodeJwtExpiry(tokens.accessToken) ?? parseTimestamp(raw["expires_at"]);
148
- const lastRefreshAt = parseTimestamp(raw["last_refresh"]);
149
- const chatgptBaseUrl = this.configuredBaseUrl ?? readConfigToml(codexDir);
150
-
151
- return {
152
- accessToken: tokens.accessToken,
153
- refreshToken: tokens.refreshToken,
154
- expiresAt: typeof raw["expires_at"] === "string" ? raw["expires_at"] : undefined,
155
- ...(tokenExpiresAt !== undefined ? { tokenExpiresAt } : {}),
156
- ...(lastRefreshAt !== undefined ? { lastRefreshAt } : {}),
157
- accountEmail: claims.email ?? null,
158
- accountId: tokens.accountId ?? claims.sub ?? claims.org_id ?? null,
159
- ...(chatgptBaseUrl ? { chatgptBaseUrl } : {}),
160
- };
161
- } catch (error) {
162
- if (error instanceof CodexCredentialSourceError) throw error;
163
- throw new CodexCredentialSourceError("malformed", "Codex auth.json is malformed");
164
- }
165
- }
166
-
167
- async isAvailable(): Promise<boolean> {
168
- try {
169
- return (await this.resolve()) !== null;
170
- } catch {
171
- return false;
172
- }
173
- }
174
- }