@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.
- package/README.md +46 -7
- package/dist/assets/highlights-eq9cgrbb.scm +604 -0
- package/dist/assets/highlights-ghv9g403.scm +205 -0
- package/dist/assets/highlights-hk7bwhj4.scm +284 -0
- package/dist/assets/highlights-r812a2qc.scm +150 -0
- package/dist/assets/highlights-x6tmsnaa.scm +115 -0
- package/dist/assets/injections-73j83es3.scm +27 -0
- package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
- package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
- package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/dist/cli/assets/providers/SOURCES.md +21 -0
- package/dist/cli/assets/providers/claude-terminal.png +0 -0
- package/dist/cli/assets/providers/claude-terminal.svg +62 -0
- package/dist/cli/assets/providers/claude.png +0 -0
- package/dist/cli/assets/providers/copilot.png +0 -0
- package/dist/cli/assets/providers/copilot.svg +44 -0
- package/dist/cli/assets/providers/openai-terminal.png +0 -0
- package/dist/cli/assets/providers/openai-terminal.svg +62 -0
- package/dist/cli/assets/providers/openai.png +0 -0
- package/dist/cli/assets/providers/opencode-terminal.png +0 -0
- package/dist/cli/assets/providers/opencode-terminal.svg +62 -0
- package/dist/cli/assets/providers/opencode.png +0 -0
- package/dist/cli/assets/providers/shell.png +0 -0
- package/dist/cli/assets/providers/shell.svg +23 -0
- package/dist/cli/assets/providers/unknown.png +0 -0
- package/dist/cli/assets/providers/unknown.svg +23 -0
- package/dist/cli/index.js +499 -0
- package/dist/cli/index.js.map +61 -0
- package/dist/daemon/index.js +670 -0
- package/dist/daemon/index.js.map +1475 -0
- package/dist/hooks/claude.js +6 -0
- package/dist/hooks/claude.js.map +11 -0
- package/dist/hooks/codex.js +10 -0
- package/dist/hooks/codex.js.map +11 -0
- package/dist/mcp/index.js +100 -0
- package/dist/mcp/index.js.map +231 -0
- package/dist/oc/index.js +21 -0
- package/dist/oc/index.js.map +10 -0
- package/package.json +26 -20
- package/src/postinstall.cjs +11 -0
- package/src/api/docs/sessions.keys.ts +0 -6
- package/src/api/docs/sessions.ts +0 -81
- package/src/api/docs/system.keys.ts +0 -5
- package/src/api/docs/system.ts +0 -66
- package/src/api/docs/usage.keys.ts +0 -3
- package/src/api/docs/usage.ts +0 -24
- package/src/api/docs/webhooks.keys.ts +0 -5
- package/src/api/docs/webhooks.ts +0 -78
- package/src/api/index.ts +0 -18
- package/src/api/lib/auth.ts +0 -43
- package/src/api/lib/errors.ts +0 -28
- package/src/api/lib/route.ts +0 -18
- package/src/api/models/claude.ts +0 -4
- package/src/api/models/codex.ts +0 -4
- package/src/api/models/opencode.ts +0 -11
- package/src/api/routes/sessions.ts +0 -47
- package/src/api/routes/system.ts +0 -103
- package/src/api/routes/usage.ts +0 -15
- package/src/api/routes/webhooks/claude.ts +0 -141
- package/src/api/routes/webhooks/codex.ts +0 -175
- package/src/api/routes/webhooks/opencode.ts +0 -111
- package/src/api/routes.ts +0 -93
- package/src/api/schemas/common.ts +0 -21
- package/src/api/schemas/sessions.ts +0 -72
- package/src/api/schemas/usage.ts +0 -50
- package/src/api/schemas/webhooks/claude.ts +0 -32
- package/src/api/schemas/webhooks/codex.ts +0 -34
- package/src/api/schemas/webhooks/opencode.ts +0 -29
- package/src/api/services/claude-session.ts +0 -9
- package/src/api/services/codex-session.ts +0 -8
- package/src/api/services/opencode-session.ts +0 -73
- package/src/classifier.ts +0 -206
- package/src/cli/commands.ts +0 -632
- package/src/cli/prompt.ts +0 -44
- package/src/daemon/cli.ts +0 -27
- package/src/daemon/collectors/claude/README.md +0 -108
- package/src/daemon/collectors/claude/collector.ts +0 -423
- package/src/daemon/collectors/claude/discovery.ts +0 -148
- package/src/daemon/collectors/claude/hooks.ts +0 -181
- package/src/daemon/collectors/claude/index.ts +0 -8
- package/src/daemon/collectors/claude/jsonl.ts +0 -85
- package/src/daemon/collectors/claude/transcript.ts +0 -246
- package/src/daemon/collectors/codex/README.md +0 -147
- package/src/daemon/collectors/codex/collector.ts +0 -661
- package/src/daemon/collectors/codex/discovery.ts +0 -104
- package/src/daemon/collectors/codex/hooks.ts +0 -179
- package/src/daemon/collectors/codex/index.ts +0 -7
- package/src/daemon/collectors/codex/jsonl.ts +0 -108
- package/src/daemon/collectors/codex/transcript.ts +0 -299
- package/src/daemon/collectors/git.ts +0 -295
- package/src/daemon/collectors/opencode/README.md +0 -106
- package/src/daemon/collectors/opencode/collector.ts +0 -545
- package/src/daemon/collectors/opencode/discovery.ts +0 -257
- package/src/daemon/collectors/opencode/index.ts +0 -5
- package/src/daemon/collectors/opencode/session-builder.ts +0 -59
- package/src/daemon/collectors/opencode-db.ts +0 -282
- package/src/daemon/collectors/process.ts +0 -201
- package/src/daemon/collectors/tmux/README.md +0 -128
- package/src/daemon/collectors/tmux/collector.ts +0 -472
- package/src/daemon/collectors/tmux/enrichment.ts +0 -241
- package/src/daemon/collectors/tmux/index.ts +0 -4
- package/src/daemon/collectors/tmux/parse.ts +0 -128
- package/src/daemon/collectors/usage.ts +0 -193
- package/src/daemon/index.ts +0 -207
- package/src/daemon/scheduler.ts +0 -86
- package/src/daemon/store.ts +0 -588
- package/src/detect.ts +0 -143
- package/src/hooks/claude.ts +0 -596
- package/src/hooks/codex.ts +0 -517
- package/src/legacy/classifier.ts +0 -215
- package/src/legacy/detect.ts +0 -143
- package/src/mcp/client-server.ts +0 -21
- package/src/mcp/http-handler.ts +0 -126
- package/src/mcp/index.ts +0 -61
- package/src/mcp/resources/config-current.ts +0 -19
- package/src/mcp/resources/index.ts +0 -11
- package/src/mcp/resources/sessions-active.ts +0 -19
- package/src/mcp/resources/sessions-all.ts +0 -19
- package/src/mcp/schemas/sessions.ts +0 -3
- package/src/mcp/server.ts +0 -17
- package/src/mcp/services/api-adapter.ts +0 -48
- package/src/mcp/services/data-source.ts +0 -22
- package/src/mcp/services/store-adapter.ts +0 -40
- package/src/mcp/tools/get-active-sessions.ts +0 -16
- package/src/mcp/tools/get-session-children.ts +0 -24
- package/src/mcp/tools/get-session-messages.ts +0 -24
- package/src/mcp/tools/get-session.ts +0 -23
- package/src/mcp/tools/index.ts +0 -15
- package/src/mcp/tools/list-sessions.ts +0 -16
- package/src/neo-blessed.d.ts +0 -4
- package/src/oc/index.ts +0 -302
- package/src/service/manager.ts +0 -284
- package/src/shared/config.ts +0 -478
- package/src/shared/ipc.ts +0 -80
- package/src/shared/logger.ts +0 -248
- package/src/shared/paths.ts +0 -73
- package/src/shared/platform.ts +0 -110
- package/src/shared/session-view.ts +0 -241
- package/src/shared/types.ts +0 -299
- package/src/shared/utils.ts +0 -339
- package/src/tui/app.ts +0 -920
- package/src/tui/client.ts +0 -149
- package/src/tui/index.ts +0 -217
- package/src/types.ts +0 -113
- package/src/usage/README.md +0 -323
- package/src/usage/api.ts +0 -99
- package/src/usage/client.ts +0 -3
- package/src/usage/clients/anthropic-oauth-usage.ts +0 -178
- package/src/usage/clients/anthropic-web-usage.ts +0 -245
- package/src/usage/clients/claude-cli-probe.ts +0 -187
- package/src/usage/clients/codex-oauth-usage.ts +0 -113
- package/src/usage/clients/opencode-go-usage.ts +0 -441
- package/src/usage/index.ts +0 -20
- package/src/usage/mappers.ts +0 -273
- package/src/usage/planner.ts +0 -54
- package/src/usage/providers/claude.ts +0 -113
- package/src/usage/providers/codex.ts +0 -34
- package/src/usage/providers/opencode-go.ts +0 -37
- package/src/usage/resolver.ts +0 -4
- package/src/usage/resolvers/browser-cookies.ts +0 -470
- package/src/usage/resolvers/claude-auth.ts +0 -108
- package/src/usage/resolvers/claude-cli.ts +0 -24
- package/src/usage/resolvers/claude-desktop-auth.ts +0 -160
- package/src/usage/resolvers/claude-web.ts +0 -16
- package/src/usage/resolvers/codex-auth.ts +0 -117
- package/src/usage/resolvers/opencode-go-web.ts +0 -14
- 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
|
-
}
|