@scrylog/cli 0.2.0 → 0.3.1

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 (171) hide show
  1. package/README.md +46 -7
  2. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  3. package/dist/assets/highlights-ghv9g403.scm +205 -0
  4. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  5. package/dist/assets/highlights-r812a2qc.scm +150 -0
  6. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  7. package/dist/assets/injections-73j83es3.scm +27 -0
  8. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  9. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  10. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  12. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  13. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  14. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  15. package/dist/cli/assets/providers/SOURCES.md +21 -0
  16. package/dist/cli/assets/providers/claude-terminal.png +0 -0
  17. package/dist/cli/assets/providers/claude-terminal.svg +62 -0
  18. package/dist/cli/assets/providers/claude.png +0 -0
  19. package/dist/cli/assets/providers/copilot.png +0 -0
  20. package/dist/cli/assets/providers/copilot.svg +44 -0
  21. package/dist/cli/assets/providers/openai-terminal.png +0 -0
  22. package/dist/cli/assets/providers/openai-terminal.svg +62 -0
  23. package/dist/cli/assets/providers/openai.png +0 -0
  24. package/dist/cli/assets/providers/opencode-terminal.png +0 -0
  25. package/dist/cli/assets/providers/opencode-terminal.svg +62 -0
  26. package/dist/cli/assets/providers/opencode.png +0 -0
  27. package/dist/cli/assets/providers/shell.png +0 -0
  28. package/dist/cli/assets/providers/shell.svg +23 -0
  29. package/dist/cli/assets/providers/unknown.png +0 -0
  30. package/dist/cli/assets/providers/unknown.svg +23 -0
  31. package/dist/cli/index.js +499 -0
  32. package/dist/cli/index.js.map +61 -0
  33. package/dist/daemon/index.js +670 -0
  34. package/dist/daemon/index.js.map +1475 -0
  35. package/dist/hooks/claude.js +6 -0
  36. package/dist/hooks/claude.js.map +11 -0
  37. package/dist/hooks/codex.js +10 -0
  38. package/dist/hooks/codex.js.map +11 -0
  39. package/dist/mcp/index.js +100 -0
  40. package/dist/mcp/index.js.map +231 -0
  41. package/dist/oc/index.js +21 -0
  42. package/dist/oc/index.js.map +10 -0
  43. package/package.json +26 -20
  44. package/src/postinstall.cjs +11 -0
  45. package/src/api/docs/sessions.keys.ts +0 -6
  46. package/src/api/docs/sessions.ts +0 -81
  47. package/src/api/docs/system.keys.ts +0 -5
  48. package/src/api/docs/system.ts +0 -66
  49. package/src/api/docs/usage.keys.ts +0 -3
  50. package/src/api/docs/usage.ts +0 -24
  51. package/src/api/docs/webhooks.keys.ts +0 -5
  52. package/src/api/docs/webhooks.ts +0 -78
  53. package/src/api/index.ts +0 -18
  54. package/src/api/lib/auth.ts +0 -43
  55. package/src/api/lib/errors.ts +0 -28
  56. package/src/api/lib/route.ts +0 -18
  57. package/src/api/models/claude.ts +0 -4
  58. package/src/api/models/codex.ts +0 -4
  59. package/src/api/models/opencode.ts +0 -11
  60. package/src/api/routes/sessions.ts +0 -47
  61. package/src/api/routes/system.ts +0 -103
  62. package/src/api/routes/usage.ts +0 -15
  63. package/src/api/routes/webhooks/claude.ts +0 -141
  64. package/src/api/routes/webhooks/codex.ts +0 -175
  65. package/src/api/routes/webhooks/opencode.ts +0 -111
  66. package/src/api/routes.ts +0 -93
  67. package/src/api/schemas/common.ts +0 -21
  68. package/src/api/schemas/sessions.ts +0 -72
  69. package/src/api/schemas/usage.ts +0 -50
  70. package/src/api/schemas/webhooks/claude.ts +0 -32
  71. package/src/api/schemas/webhooks/codex.ts +0 -34
  72. package/src/api/schemas/webhooks/opencode.ts +0 -29
  73. package/src/api/services/claude-session.ts +0 -9
  74. package/src/api/services/codex-session.ts +0 -8
  75. package/src/api/services/opencode-session.ts +0 -73
  76. package/src/classifier.ts +0 -206
  77. package/src/cli/commands.ts +0 -632
  78. package/src/cli/prompt.ts +0 -44
  79. package/src/daemon/cli.ts +0 -27
  80. package/src/daemon/collectors/claude/README.md +0 -108
  81. package/src/daemon/collectors/claude/collector.ts +0 -423
  82. package/src/daemon/collectors/claude/discovery.ts +0 -148
  83. package/src/daemon/collectors/claude/hooks.ts +0 -181
  84. package/src/daemon/collectors/claude/index.ts +0 -8
  85. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  86. package/src/daemon/collectors/claude/transcript.ts +0 -246
  87. package/src/daemon/collectors/codex/README.md +0 -147
  88. package/src/daemon/collectors/codex/collector.ts +0 -661
  89. package/src/daemon/collectors/codex/discovery.ts +0 -104
  90. package/src/daemon/collectors/codex/hooks.ts +0 -179
  91. package/src/daemon/collectors/codex/index.ts +0 -7
  92. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  93. package/src/daemon/collectors/codex/transcript.ts +0 -299
  94. package/src/daemon/collectors/git.ts +0 -295
  95. package/src/daemon/collectors/opencode/README.md +0 -106
  96. package/src/daemon/collectors/opencode/collector.ts +0 -545
  97. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  98. package/src/daemon/collectors/opencode/index.ts +0 -5
  99. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  100. package/src/daemon/collectors/opencode-db.ts +0 -282
  101. package/src/daemon/collectors/process.ts +0 -201
  102. package/src/daemon/collectors/tmux/README.md +0 -128
  103. package/src/daemon/collectors/tmux/collector.ts +0 -472
  104. package/src/daemon/collectors/tmux/enrichment.ts +0 -241
  105. package/src/daemon/collectors/tmux/index.ts +0 -4
  106. package/src/daemon/collectors/tmux/parse.ts +0 -128
  107. package/src/daemon/collectors/usage.ts +0 -193
  108. package/src/daemon/index.ts +0 -207
  109. package/src/daemon/scheduler.ts +0 -86
  110. package/src/daemon/store.ts +0 -588
  111. package/src/detect.ts +0 -143
  112. package/src/hooks/claude.ts +0 -596
  113. package/src/hooks/codex.ts +0 -517
  114. package/src/legacy/classifier.ts +0 -215
  115. package/src/legacy/detect.ts +0 -143
  116. package/src/mcp/client-server.ts +0 -21
  117. package/src/mcp/http-handler.ts +0 -126
  118. package/src/mcp/index.ts +0 -61
  119. package/src/mcp/resources/config-current.ts +0 -19
  120. package/src/mcp/resources/index.ts +0 -11
  121. package/src/mcp/resources/sessions-active.ts +0 -19
  122. package/src/mcp/resources/sessions-all.ts +0 -19
  123. package/src/mcp/schemas/sessions.ts +0 -3
  124. package/src/mcp/server.ts +0 -17
  125. package/src/mcp/services/api-adapter.ts +0 -48
  126. package/src/mcp/services/data-source.ts +0 -22
  127. package/src/mcp/services/store-adapter.ts +0 -40
  128. package/src/mcp/tools/get-active-sessions.ts +0 -16
  129. package/src/mcp/tools/get-session-children.ts +0 -24
  130. package/src/mcp/tools/get-session-messages.ts +0 -24
  131. package/src/mcp/tools/get-session.ts +0 -23
  132. package/src/mcp/tools/index.ts +0 -15
  133. package/src/mcp/tools/list-sessions.ts +0 -16
  134. package/src/neo-blessed.d.ts +0 -4
  135. package/src/oc/index.ts +0 -302
  136. package/src/service/manager.ts +0 -284
  137. package/src/shared/config.ts +0 -478
  138. package/src/shared/ipc.ts +0 -80
  139. package/src/shared/logger.ts +0 -248
  140. package/src/shared/paths.ts +0 -73
  141. package/src/shared/platform.ts +0 -110
  142. package/src/shared/session-view.ts +0 -241
  143. package/src/shared/types.ts +0 -299
  144. package/src/shared/utils.ts +0 -339
  145. package/src/tui/app.ts +0 -920
  146. package/src/tui/client.ts +0 -149
  147. package/src/tui/index.ts +0 -217
  148. package/src/types.ts +0 -113
  149. package/src/usage/README.md +0 -323
  150. package/src/usage/api.ts +0 -99
  151. package/src/usage/client.ts +0 -3
  152. package/src/usage/clients/anthropic-oauth-usage.ts +0 -178
  153. package/src/usage/clients/anthropic-web-usage.ts +0 -245
  154. package/src/usage/clients/claude-cli-probe.ts +0 -187
  155. package/src/usage/clients/codex-oauth-usage.ts +0 -113
  156. package/src/usage/clients/opencode-go-usage.ts +0 -441
  157. package/src/usage/index.ts +0 -20
  158. package/src/usage/mappers.ts +0 -273
  159. package/src/usage/planner.ts +0 -54
  160. package/src/usage/providers/claude.ts +0 -113
  161. package/src/usage/providers/codex.ts +0 -34
  162. package/src/usage/providers/opencode-go.ts +0 -37
  163. package/src/usage/resolver.ts +0 -4
  164. package/src/usage/resolvers/browser-cookies.ts +0 -470
  165. package/src/usage/resolvers/claude-auth.ts +0 -108
  166. package/src/usage/resolvers/claude-cli.ts +0 -24
  167. package/src/usage/resolvers/claude-desktop-auth.ts +0 -160
  168. package/src/usage/resolvers/claude-web.ts +0 -16
  169. package/src/usage/resolvers/codex-auth.ts +0 -117
  170. package/src/usage/resolvers/opencode-go-web.ts +0 -14
  171. package/src/usage/types.ts +0 -190
@@ -1,470 +0,0 @@
1
- import * as child_process from "node:child_process";
2
- import { existsSync, readdirSync, readFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { createDecipheriv, pbkdf2Sync } from "node:crypto";
5
- import { join } from "node:path";
6
- import { Database } from "bun:sqlite";
7
- import { IS_MAC, IS_WINDOWS } from "../../shared/platform";
8
-
9
- export interface BrowserCookie {
10
- name: string;
11
- value: string;
12
- }
13
-
14
- // ---------------------------------------------------------------------------
15
- // Browser catalog types
16
- // ---------------------------------------------------------------------------
17
-
18
- interface ChromiumProfileEntry {
19
- /** Unique browser ID, e.g. "chrome", "brave", "claude-desktop" */
20
- id: string;
21
- /** Relative path under ~/Library/Application Support/ on macOS */
22
- macOSPath: string;
23
- /** Relative path under ~/.config/ on Linux */
24
- linuxPath: string;
25
- /** Relative path under %APPDATA%/ on Windows */
26
- windowsPath: string;
27
- /** macOS Keychain service label, e.g. "Chrome Safe Storage" */
28
- keychainService?: string;
29
- /** macOS Keychain account label, e.g. "Chrome" */
30
- keychainAccount?: string;
31
- }
32
-
33
- interface GeckoProfileEntry {
34
- /** Unique browser ID, e.g. "firefox", "zen" */
35
- id: string;
36
- /**
37
- * Profiles parent folder name.
38
- * macOS: ~/Library/Application Support/<profilesFolder>/Profiles
39
- * Linux: ~/.mozilla/<profilesFolder>/Profiles
40
- */
41
- profilesFolder: string;
42
- }
43
-
44
- // ---------------------------------------------------------------------------
45
- // Browser catalogs
46
- // ---------------------------------------------------------------------------
47
-
48
- export const CHROMIUM_PROFILES: ChromiumProfileEntry[] = [
49
- {
50
- id: "chrome",
51
- macOSPath: "Google/Chrome",
52
- linuxPath: "google-chrome",
53
- windowsPath: "Google/Chrome/User Data",
54
- keychainService: "Chrome Safe Storage",
55
- keychainAccount: "Chrome",
56
- },
57
- {
58
- id: "edge",
59
- macOSPath: "Microsoft Edge",
60
- linuxPath: "microsoft-edge",
61
- windowsPath: "Microsoft/Edge/User Data",
62
- keychainService: "Microsoft Edge Safe Storage",
63
- keychainAccount: "Microsoft Edge",
64
- },
65
- {
66
- id: "brave",
67
- macOSPath: "BraveSoftware/Brave-Browser",
68
- linuxPath: "BraveSoftware/Brave-Browser",
69
- windowsPath: "BraveSoftware/Brave-Browser/User Data",
70
- keychainService: "Brave Safe Storage",
71
- keychainAccount: "Brave",
72
- },
73
- {
74
- id: "arc",
75
- macOSPath: "Arc/User Data",
76
- linuxPath: "Arc",
77
- windowsPath: "Arc/User Data",
78
- keychainService: "Arc Safe Storage",
79
- keychainAccount: "Arc",
80
- },
81
- {
82
- id: "vivaldi",
83
- macOSPath: "Vivaldi",
84
- linuxPath: "vivaldi",
85
- windowsPath: "Vivaldi/User Data",
86
- keychainService: "Vivaldi Safe Storage",
87
- keychainAccount: "Vivaldi",
88
- },
89
- {
90
- id: "chromium",
91
- macOSPath: "Chromium",
92
- linuxPath: "chromium",
93
- windowsPath: "Chromium/User Data",
94
- keychainService: "Chromium Safe Storage",
95
- keychainAccount: "Chromium",
96
- },
97
- {
98
- id: "yandex",
99
- macOSPath: "Yandex/YandexBrowser",
100
- linuxPath: "yandex-browser",
101
- windowsPath: "Yandex/YandexBrowser/User Data",
102
- keychainService: "Yandex Safe Storage",
103
- keychainAccount: "Yandex",
104
- },
105
- {
106
- // Claude Desktop app (Electron/Chromium) — macOS and Windows only
107
- id: "claude-desktop",
108
- macOSPath: "Claude",
109
- linuxPath: "", // not available on Linux
110
- windowsPath: "Anthropic/Claude",
111
- keychainService: "Claude Safe Storage",
112
- keychainAccount: "Claude",
113
- },
114
- ];
115
-
116
- export const GECKO_PROFILES: GeckoProfileEntry[] = [
117
- { id: "firefox", profilesFolder: "Firefox" },
118
- { id: "zen", profilesFolder: "zen" },
119
- ];
120
-
121
- const SAFARI_COOKIE_PATH = join(homedir(), "Library", "Cookies", "Cookies.binarycookies");
122
-
123
- // ---------------------------------------------------------------------------
124
- // Platform root resolution
125
- // ---------------------------------------------------------------------------
126
-
127
- function chromiumRootPath(entry: ChromiumProfileEntry): string {
128
- const home = homedir();
129
- if (IS_MAC) {
130
- if (!entry.macOSPath) return "";
131
- return join(home, "Library", "Application Support", entry.macOSPath);
132
- }
133
- if (IS_WINDOWS) {
134
- const appData = process.env.APPDATA ?? join(home, "AppData", "Roaming");
135
- if (!entry.windowsPath) return "";
136
- return join(appData, entry.windowsPath);
137
- }
138
- // Linux
139
- if (!entry.linuxPath) return "";
140
- return join(home, ".config", entry.linuxPath);
141
- }
142
-
143
- function geckoProfilesRoot(entry: GeckoProfileEntry): string {
144
- const home = homedir();
145
- if (IS_MAC) {
146
- return join(home, "Library", "Application Support", entry.profilesFolder, "Profiles");
147
- }
148
- if (IS_WINDOWS) {
149
- // Firefox on Windows is not in scope; skip
150
- return "";
151
- }
152
- // Linux
153
- return join(home, ".mozilla", entry.profilesFolder, "Profiles");
154
- }
155
-
156
- // ---------------------------------------------------------------------------
157
- // Chromium helpers
158
- // ---------------------------------------------------------------------------
159
-
160
- /**
161
- * Derives the Chromium AES-128-CBC key for a given Keychain service label.
162
- * macOS: reads from Keychain via `security` then applies PBKDF2.
163
- * Linux/Windows: uses the fixed "peanuts" PBKDF2 key (no Keychain).
164
- */
165
- export function getChromiumSafeStorageKey(service: string, _account: string): Buffer | null {
166
- try {
167
- if (IS_MAC) {
168
- const password = child_process.execSync(`security find-generic-password -s "${service}" -w`, {
169
- encoding: "utf8",
170
- stdio: ["pipe", "pipe", "pipe"],
171
- }).trim();
172
- if (!password) return null;
173
- return pbkdf2Sync(password, "saltysalt", 1003, 16, "sha1");
174
- }
175
-
176
- if (IS_WINDOWS) {
177
- // DPAPI not implemented; fall back to peanuts key (partial compatibility)
178
- return pbkdf2Sync("peanuts", "saltysalt", 1003, 16, "sha1");
179
- }
180
-
181
- // Linux
182
- return pbkdf2Sync("peanuts", "saltysalt", 1003, 16, "sha1");
183
- } catch {
184
- return null;
185
- }
186
- }
187
-
188
- /**
189
- * Returns all profile subdirectory paths under rootPath that match the
190
- * Chromium naming conventions: Default, Profile *, user-*.
191
- */
192
- export function discoverChromiumProfiles(rootPath: string): string[] {
193
- if (!existsSync(rootPath)) return [];
194
- let entries: string[];
195
- try {
196
- entries = readdirSync(rootPath, { withFileTypes: true })
197
- .filter((d) => d.isDirectory())
198
- .map((d) => d.name);
199
- } catch {
200
- return [];
201
- }
202
- return entries
203
- .filter((name) => name === "Default" || /^Profile \d+$/.test(name) || /^user-/.test(name))
204
- .map((name) => join(rootPath, name));
205
- }
206
-
207
- /**
208
- * Returns the two candidate cookie DB paths for a Chromium profile directory.
209
- * The caller is responsible for checking existence before opening.
210
- */
211
- export function chromiumCookieDbPaths(profilePath: string): string[] {
212
- return [join(profilePath, "Cookies"), join(profilePath, "Network", "Cookies")];
213
- }
214
-
215
- /**
216
- * Internal: decrypt AES-128-CBC safeStorage payload, returning raw bytes after
217
- * PKCS#7 un-padding. Returns `null` on decryption failure. Callers that need
218
- * the Chrome v24+ 32-byte domain-hash strip must apply it themselves.
219
- */
220
- function decryptSafeStorageRaw(buf: Buffer, key: Buffer): Buffer | null {
221
- if (buf.length < 3) return null;
222
- const prefix = buf.slice(0, 3).toString("ascii");
223
- if (prefix !== "v10" && prefix !== "v11") {
224
- return buf; // plain text (older versions)
225
- }
226
- const iv = Buffer.alloc(16, 32); // space character (0x20) repeated 16 times
227
- try {
228
- const decipher = createDecipheriv("aes-128-cbc", key, iv);
229
- let decrypted = Buffer.concat([decipher.update(buf.slice(3)), decipher.final()]);
230
- // Remove PKCS#7 padding
231
- const padLen = decrypted[decrypted.length - 1];
232
- if (padLen > 0 && padLen <= 16) {
233
- decrypted = decrypted.slice(0, decrypted.length - padLen);
234
- }
235
- return decrypted;
236
- } catch {
237
- return null;
238
- }
239
- }
240
-
241
- /**
242
- * Decrypt a Chromium/Electron safeStorage blob (AES-128-CBC, v10/v11 prefix)
243
- * and return the result as a UTF-8 string.
244
- *
245
- * Used for Electron `safeStorage`-encrypted config fields (e.g. `oauth:tokenCache`
246
- * in Claude Desktop's `config.json`). Does **not** strip the Chrome v24+ 32-byte
247
- * domain-hash prefix — use `decryptChromiumCookie` for cookie DB values.
248
- */
249
- export function decryptSafeStorageBlob(buf: Buffer, key: Buffer): string | null {
250
- const raw = decryptSafeStorageRaw(buf, key);
251
- return raw !== null ? raw.toString("utf8") : null;
252
- }
253
-
254
- function decryptChromiumCookie(encrypted: Buffer, key: Buffer, dbVersion = 0): string | null {
255
- const raw = decryptSafeStorageRaw(encrypted, key);
256
- if (raw === null) return null;
257
- // Chrome v24+ prepends a SHA256 hash of the domain (32 bytes) to encrypted cookie values
258
- if (encrypted.length >= 3) {
259
- const prefix = encrypted.slice(0, 3).toString("ascii");
260
- if ((prefix === "v10" || prefix === "v11") && dbVersion >= 24 && raw.length >= 32) {
261
- return raw.slice(32).toString("utf8");
262
- }
263
- }
264
- return raw.toString("utf8");
265
- }
266
-
267
- export function readChromiumCookies(entry: ChromiumProfileEntry, domain: string): BrowserCookie[] {
268
- const cookies: BrowserCookie[] = [];
269
- const rootPath = chromiumRootPath(entry);
270
- if (!rootPath || !existsSync(rootPath)) return cookies;
271
-
272
- // Resolve decryption key only if we found a root (avoids Keychain prompts for absent browsers)
273
- const key = getChromiumSafeStorageKey(
274
- entry.keychainService ?? "",
275
- entry.keychainAccount ?? ""
276
- );
277
- if (!key) return cookies;
278
-
279
- const profiles = discoverChromiumProfiles(rootPath);
280
- for (const profilePath of profiles) {
281
- for (const dbPath of chromiumCookieDbPaths(profilePath)) {
282
- if (!existsSync(dbPath)) continue;
283
- let db: Database | undefined;
284
- try {
285
- db = new Database(dbPath, { readonly: true });
286
-
287
- // Chrome v24+ prepends a SHA256 hash (32 bytes) to encrypted cookie values
288
- let dbVersion = 0;
289
- try {
290
- const versionStmt = db.query("SELECT value FROM meta WHERE key = 'version'");
291
- const versionRow = versionStmt.get() as { value: number } | null;
292
- if (versionRow) dbVersion = Number(versionRow.value);
293
- } catch {
294
- // meta table may not exist in older DBs
295
- }
296
-
297
- const stmt = db.query(
298
- "SELECT name, encrypted_value, host_key FROM cookies WHERE host_key LIKE ? AND expires_utc > ?"
299
- );
300
- const now = (Date.now() + 11644473600000) * 1000; // Chrome epoch offset
301
- const rows = stmt.all(`%${domain}%`, now) as Array<{
302
- name: string;
303
- encrypted_value: Uint8Array;
304
- host_key: string;
305
- }>;
306
- for (const row of rows) {
307
- const value = decryptChromiumCookie(Buffer.from(row.encrypted_value), key, dbVersion);
308
- if (value !== null) {
309
- cookies.push({ name: row.name, value });
310
- }
311
- }
312
- } catch {
313
- // Ignore permission errors, locked DB (SQLITE_BUSY), etc.
314
- } finally {
315
- db?.close();
316
- }
317
- }
318
- }
319
-
320
- return cookies;
321
- }
322
-
323
- // ---------------------------------------------------------------------------
324
- // Gecko (Firefox/Zen) support
325
- // ---------------------------------------------------------------------------
326
-
327
- export function readGeckoCookies(entry: GeckoProfileEntry, domain: string): BrowserCookie[] {
328
- const cookies: BrowserCookie[] = [];
329
- const profilesRoot = geckoProfilesRoot(entry);
330
- if (!profilesRoot || !existsSync(profilesRoot)) return cookies;
331
-
332
- let profileNames: string[];
333
- try {
334
- profileNames = readdirSync(profilesRoot, { withFileTypes: true })
335
- .filter((d) => d.isDirectory() && d.name.includes(".default"))
336
- .map((d) => d.name)
337
- // Prefer .default-release over .default
338
- .sort((a, b) => {
339
- const aScore = a.endsWith(".default-release") ? 1 : 0;
340
- const bScore = b.endsWith(".default-release") ? 1 : 0;
341
- return bScore - aScore;
342
- });
343
- } catch {
344
- return cookies;
345
- }
346
-
347
- for (const profileName of profileNames) {
348
- const dbPath = join(profilesRoot, profileName, "cookies.sqlite");
349
- if (!existsSync(dbPath)) continue;
350
- let db: Database | undefined;
351
- try {
352
- db = new Database(dbPath, { readonly: true });
353
- const stmt = db.query("SELECT name, value FROM moz_cookies WHERE host LIKE ?");
354
- const rows = stmt.all(`%${domain}%`) as Array<{ name: string; value: string }>;
355
- for (const row of rows) {
356
- cookies.push({ name: row.name, value: row.value });
357
- }
358
- if (cookies.length > 0) break; // first-profile-wins within a browser
359
- } catch {
360
- // Ignore locked or corrupt DB
361
- } finally {
362
- db?.close();
363
- }
364
- }
365
-
366
- return cookies;
367
- }
368
-
369
- // ---------------------------------------------------------------------------
370
- // Safari (macOS-only, unchanged)
371
- // ---------------------------------------------------------------------------
372
-
373
- function readSafariCookies(domain: string): BrowserCookie[] {
374
- const cookies: BrowserCookie[] = [];
375
- if (process.platform !== "darwin") return cookies;
376
- if (!existsSync(SAFARI_COOKIE_PATH)) return cookies;
377
-
378
- try {
379
- const data = readFileSync(SAFARI_COOKIE_PATH);
380
- // Simple binarycookies parser
381
- // Header: 4 bytes magic "cook", 4 bytes page count, then pages
382
- if (data.length < 8 || data.toString("ascii", 0, 4) !== "cook") return cookies;
383
-
384
- const numPages = data.readUInt32BE(4);
385
- let offset = 8;
386
- const pageSizes: number[] = [];
387
- for (let i = 0; i < numPages; i++) {
388
- pageSizes.push(data.readUInt32BE(offset));
389
- offset += 4;
390
- }
391
-
392
- for (const pageSize of pageSizes) {
393
- const pageEnd = offset + pageSize;
394
- if (pageEnd > data.length) break;
395
- // Page header: "page" + 4 bytes count
396
- if (data.toString("ascii", offset, offset + 4) !== "page") {
397
- offset += pageSize;
398
- continue;
399
- }
400
- const numCookies = data.readUInt32BE(offset + 4);
401
- let cookieOffset = offset + 8;
402
- for (let i = 0; i < numCookies; i++) {
403
- // Each cookie record: size (4) + unknown (4) + flags (4) + hasPort (4)
404
- // + port (4) + urlOffset (4) + nameOffset (4) + pathOffset (4) + valueOffset (4)
405
- // + tail (8) = 40 bytes header, then strings
406
- const size = data.readUInt32BE(cookieOffset);
407
- const urlOffset = data.readUInt32BE(cookieOffset + 20);
408
- const nameOffset = data.readUInt32BE(cookieOffset + 24);
409
- const pathOffset = data.readUInt32BE(cookieOffset + 28);
410
- const valueOffset = data.readUInt32BE(cookieOffset + 32);
411
-
412
- const url = data.toString("utf8", cookieOffset + urlOffset, cookieOffset + nameOffset - 1);
413
- const name = data.toString("utf8", cookieOffset + nameOffset, cookieOffset + pathOffset - 1);
414
- const value = data.toString("utf8", cookieOffset + valueOffset, cookieOffset + size);
415
-
416
- if (url.includes(domain)) {
417
- cookies.push({ name, value });
418
- }
419
-
420
- cookieOffset += size;
421
- if (cookieOffset > pageEnd) break;
422
- }
423
- offset += pageSize;
424
- }
425
- } catch {
426
- // Ignore errors
427
- }
428
-
429
- return cookies;
430
- }
431
-
432
- // ---------------------------------------------------------------------------
433
- // Public API
434
- // ---------------------------------------------------------------------------
435
-
436
- /**
437
- * Extract cookies for the given domain from all available browser sources.
438
- * Sources are tried in order that minimises Keychain prompts:
439
- * macOS: Gecko (Firefox/Zen) → Safari → Chromium catalog
440
- * Linux: Gecko → Chromium catalog
441
- * Windows: Gecko (limited) → Chromium catalog
442
- *
443
- * Returns the first source that yields ≥1 matching cookie; subsequent sources
444
- * are skipped (first-source-wins).
445
- */
446
- export function extractCookies(domain: string): BrowserCookie[] {
447
- // 1. Gecko sources first — no Keychain prompt, cheap read
448
- for (const entry of GECKO_PROFILES) {
449
- const cookies = readGeckoCookies(entry, domain);
450
- if (cookies.length > 0) return cookies;
451
- }
452
-
453
- // 2. Safari (macOS only) — no Keychain prompt
454
- if (IS_MAC) {
455
- const safari = readSafariCookies(domain);
456
- if (safari.length > 0) return safari;
457
- }
458
-
459
- // 3. Chromium catalog — may trigger Keychain prompts on macOS
460
- for (const entry of CHROMIUM_PROFILES) {
461
- const cookies = readChromiumCookies(entry, domain);
462
- if (cookies.length > 0) return cookies;
463
- }
464
-
465
- return [];
466
- }
467
-
468
- export function buildCookieHeader(cookies: BrowserCookie[]): string {
469
- return cookies.map((c) => `${c.name}=${c.value}`).join("; ");
470
- }
@@ -1,108 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import { join } from "node:path";
5
- import type { TokenResolver } from "../resolver";
6
- import type { ClaudeOAuthCredentials } from "../types";
7
- import { IS_MAC } from "../../shared/platform";
8
- import { ClaudeDesktopAuthResolver } from "./claude-desktop-auth";
9
-
10
- export class ClaudeAuthResolver implements TokenResolver<ClaudeOAuthCredentials> {
11
- private readonly debug = (message: string, meta?: Record<string, unknown>) => {
12
- // Avoid coupling to logger; stderr debug line is acceptable for resolver diagnostics.
13
- if (process.env.SCRYLOG_DEBUG_AUTH) {
14
- process.stderr.write(`[scrylog:claude-auth] ${message}${meta ? ` ${JSON.stringify(meta)}` : ""}\n`);
15
- }
16
- };
17
-
18
- async resolve(): Promise<ClaudeOAuthCredentials | null> {
19
- // 1. macOS Keychain is the primary store on macOS.
20
- if (IS_MAC) {
21
- const services = ["claude-api-oauth", "Claude Code-credentials"];
22
- for (const service of services) {
23
- const creds = this.tryService(service);
24
- if (creds) return creds;
25
- }
26
- } else {
27
- this.debug("Keychain not available on this platform", { platform: process.platform });
28
- }
29
-
30
- // 2. Claude Desktop app local token cache (macOS / Windows only).
31
- // Reads the Electron safeStorage-encrypted `oauth:tokenCache` from
32
- // ~/Library/Application Support/Claude/config.json (macOS) or
33
- // %APPDATA%/Anthropic/Claude/config.json (Windows). This covers the
34
- // common case where the user has Claude Desktop but not Claude Code CLI.
35
- const desktopCreds = await new ClaudeDesktopAuthResolver().resolve();
36
- if (desktopCreds) return desktopCreds;
37
-
38
- // 3. Linux/WSL/Windows: Claude Code stores credentials in ~/.claude/.credentials.json
39
- const fileCreds = this.tryCredentialsFile();
40
- if (fileCreds) return fileCreds;
41
-
42
- // 4. Universal env-var fallback
43
- const envToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
44
- if (envToken) {
45
- return { accessToken: envToken };
46
- }
47
-
48
- return null;
49
- }
50
-
51
- async isAvailable(): Promise<boolean> {
52
- return (await this.resolve()) !== null;
53
- }
54
-
55
- private tryService(service: string): ClaudeOAuthCredentials | null {
56
- try {
57
- const output = execSync(
58
- `security find-generic-password -s "${service}" -w`,
59
- { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }
60
- ).trim();
61
- if (!output) return null;
62
- return this.parseCredentialsJson(output);
63
- } catch (error) {
64
- this.debug("Keychain lookup failed", { service, error: error instanceof Error ? error.message : String(error) });
65
- return null;
66
- }
67
- }
68
-
69
- private tryCredentialsFile(): ClaudeOAuthCredentials | null {
70
- // On macOS, Keychain is authoritative; skip the file unless explicitly requested.
71
- if (IS_MAC && !process.env.CLAUDE_CODE_USE_CREDENTIALS_FILE) return null;
72
-
73
- const claudeDir = process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
74
- const path = join(claudeDir, ".credentials.json");
75
- if (!existsSync(path)) return null;
76
-
77
- try {
78
- const raw = readFileSync(path, "utf8");
79
- return this.parseCredentialsJson(raw);
80
- } catch (error) {
81
- this.debug("Failed to read credentials file", { path, error: error instanceof Error ? error.message : String(error) });
82
- return null;
83
- }
84
- }
85
-
86
- private parseCredentialsJson(input: string): ClaudeOAuthCredentials | null {
87
- try {
88
- const parsed = JSON.parse(input) as Record<string, unknown>;
89
- // Handle nested structure: { claudeAiOauth: { accessToken, ... } }
90
- const inner = parsed.claudeAiOauth && typeof parsed.claudeAiOauth === "object"
91
- ? parsed.claudeAiOauth as Record<string, unknown>
92
- : parsed;
93
- const accessToken = inner.accessToken ?? inner.access_token;
94
- if (!accessToken) return null;
95
- return {
96
- accessToken: String(accessToken),
97
- refreshToken: inner.refreshToken ? String(inner.refreshToken) : undefined,
98
- expiresAt: inner.expiresAt ? String(inner.expiresAt) : undefined,
99
- scopes: Array.isArray(inner.scopes) ? inner.scopes.map(String) : undefined,
100
- rateLimitTier: inner.rateLimitTier ? String(inner.rateLimitTier) : undefined,
101
- subscriptionType: inner.subscriptionType ? String(inner.subscriptionType) : undefined,
102
- };
103
- } catch {
104
- // Not JSON — treat the raw string as an access token.
105
- return { accessToken: input.trim() };
106
- }
107
- }
108
- }
@@ -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
- }