@steipete/oracle 0.17.2 → 0.18.0

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 (96) hide show
  1. package/dist/bin/oracle-cli.js +10 -1
  2. package/dist/bin/oracle-mcp.js +0 -0
  3. package/dist/bin/oracle.js +683 -0
  4. package/dist/docs-site/bridge.html +1 -17
  5. package/dist/docs-site/browser-mode.html +18 -26
  6. package/dist/docs-site/cli-reference.html +6 -6
  7. package/dist/docs-site/configuration.html +7 -33
  8. package/dist/docs-site/followup.html +9 -16
  9. package/dist/docs-site/gemini.html +6 -10
  10. package/dist/docs-site/index.html +7 -7
  11. package/dist/docs-site/install.html +1 -1
  12. package/dist/docs-site/manual-tests.html +2 -4
  13. package/dist/docs-site/mcp.html +5 -28
  14. package/dist/docs-site/mythical-pro-agents.html +10 -10
  15. package/dist/docs-site/openai-endpoints.html +1 -24
  16. package/dist/docs-site/quickstart.html +1 -1
  17. package/dist/docs-site/sessions.html +5 -6
  18. package/dist/docs-site/spec.html +1 -1
  19. package/dist/docs-site/windows-work.html +1 -2
  20. package/dist/docs-site/windows.html +1 -1
  21. package/dist/markdansi/types/index.js +4 -0
  22. package/dist/oracle/bin/oracle-cli.js +472 -0
  23. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  24. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  25. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  26. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  27. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  28. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  29. package/dist/oracle/src/browser/config.js +33 -0
  30. package/dist/oracle/src/browser/constants.js +40 -0
  31. package/dist/oracle/src/browser/cookies.js +210 -0
  32. package/dist/oracle/src/browser/domDebug.js +36 -0
  33. package/dist/oracle/src/browser/index.js +331 -0
  34. package/dist/oracle/src/browser/pageActions.js +5 -0
  35. package/dist/oracle/src/browser/prompt.js +88 -0
  36. package/dist/oracle/src/browser/promptSummary.js +20 -0
  37. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  38. package/dist/oracle/src/browser/types.js +1 -0
  39. package/dist/oracle/src/browser/utils.js +62 -0
  40. package/dist/oracle/src/browserMode.js +1 -0
  41. package/dist/oracle/src/cli/browserConfig.js +44 -0
  42. package/dist/oracle/src/cli/dryRun.js +59 -0
  43. package/dist/oracle/src/cli/engine.js +17 -0
  44. package/dist/oracle/src/cli/errorUtils.js +9 -0
  45. package/dist/oracle/src/cli/help.js +70 -0
  46. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  47. package/dist/oracle/src/cli/options.js +103 -0
  48. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  49. package/dist/oracle/src/cli/rootAlias.js +30 -0
  50. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  51. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  52. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  53. package/dist/oracle/src/heartbeat.js +43 -0
  54. package/dist/oracle/src/oracle/client.js +48 -0
  55. package/dist/oracle/src/oracle/config.js +29 -0
  56. package/dist/oracle/src/oracle/errors.js +101 -0
  57. package/dist/oracle/src/oracle/files.js +220 -0
  58. package/dist/oracle/src/oracle/format.js +33 -0
  59. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  60. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  61. package/dist/oracle/src/oracle/request.js +48 -0
  62. package/dist/oracle/src/oracle/run.js +444 -0
  63. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  64. package/dist/oracle/src/oracle/types.js +1 -0
  65. package/dist/oracle/src/oracle.js +9 -0
  66. package/dist/oracle/src/sessionManager.js +205 -0
  67. package/dist/oracle/src/version.js +39 -0
  68. package/dist/scripts/chrome/browser-tools.js +295 -0
  69. package/dist/src/browser/actions/assistantResponse.js +71 -23
  70. package/dist/src/browser/actions/thinkingTime.js +165 -9
  71. package/dist/src/browser/chromeCookies.js +312 -0
  72. package/dist/src/browser/config.js +2 -3
  73. package/dist/src/browser/index.js +7 -2
  74. package/dist/src/browser/keytarShim.js +56 -0
  75. package/dist/src/browser/policies.js +10 -2
  76. package/dist/src/browser/profileSync.js +141 -0
  77. package/dist/src/browser/projectSourcesRunner.js +6 -3
  78. package/dist/src/browser/reattach.js +50 -35
  79. package/dist/src/browser/windowsCookies.js +219 -0
  80. package/dist/src/cli/browserConfig.js +39 -10
  81. package/dist/src/cli/browserDefaults.js +9 -1
  82. package/dist/src/cli/dryRun.js +1 -0
  83. package/dist/src/cli/projectSources.js +10 -1
  84. package/dist/src/mcp/tools/consult.js +13 -4
  85. package/dist/src/remote/server.js +8 -3
  86. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  87. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  88. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  89. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  90. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  91. package/package.json +8 -8
  92. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  93. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  94. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  95. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  96. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
@@ -0,0 +1,56 @@
1
+ const defaultLabels = [
2
+ { service: 'Chrome Safe Storage', account: 'Chrome' },
3
+ { service: 'Chromium Safe Storage', account: 'Chromium' },
4
+ { service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
5
+ { service: 'Brave Safe Storage', account: 'Brave' },
6
+ { service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
7
+ ];
8
+ function loadEnvLabels() {
9
+ const raw = process.env.ORACLE_KEYCHAIN_LABELS;
10
+ if (!raw)
11
+ return [];
12
+ try {
13
+ const parsed = JSON.parse(raw);
14
+ if (Array.isArray(parsed)) {
15
+ return parsed
16
+ .map((entry) => (entry && typeof entry === 'object' ? entry : null))
17
+ .filter((entry) => Boolean(entry?.service && entry?.account));
18
+ }
19
+ }
20
+ catch {
21
+ // ignore invalid env payload
22
+ }
23
+ return [];
24
+ }
25
+ const fallbackLabels = [...loadEnvLabels(), ...defaultLabels];
26
+ const disableKeytar = process.env.ORACLE_DISABLE_KEYTAR === '1' || process.env.CI === 'true';
27
+ let keytar;
28
+ if (disableKeytar) {
29
+ keytar = {
30
+ getPassword: async () => null,
31
+ setPassword: async () => undefined,
32
+ deletePassword: async () => false,
33
+ };
34
+ }
35
+ else {
36
+ const keytarModule = await import('keytar');
37
+ keytar = (keytarModule.default ?? keytarModule);
38
+ const originalGetPassword = keytar.getPassword.bind(keytar);
39
+ keytar.getPassword = async (service, account) => {
40
+ const primary = await originalGetPassword(service, account);
41
+ if (primary) {
42
+ return primary;
43
+ }
44
+ for (const label of fallbackLabels) {
45
+ if (label.service === service && label.account === account) {
46
+ continue; // already tried
47
+ }
48
+ const value = await originalGetPassword(label.service, label.account);
49
+ if (value) {
50
+ return value;
51
+ }
52
+ }
53
+ return null;
54
+ };
55
+ }
56
+ export default keytar;
@@ -24,6 +24,11 @@ export function buildAttachmentPlan(sections, { inlineFiles, bundleRequested, ma
24
24
  shouldBundle,
25
25
  };
26
26
  }
27
+ export const CHROME_COOKIE_SYNC_WARNING = "Warning: copying cookies from a live Chrome profile can invalidate that browser's ChatGPT session when tokens rotate. Prefer --browser-manual-login or inline cookies when possible.";
28
+ export function shouldSyncBrowserCookies(config, { manualLogin, profileIsPreSigned = manualLogin, }) {
29
+ const explicitManualLoginSync = manualLogin && config.manualLoginCookieSync === true;
30
+ return config.cookieSync && (!profileIsPreSigned || explicitManualLoginSync);
31
+ }
27
32
  export function buildCookiePlan(config) {
28
33
  if (config?.inlineCookies && config.inlineCookies.length > 0) {
29
34
  const source = config.inlineCookiesSource ?? "inline";
@@ -32,8 +37,11 @@ export function buildCookiePlan(config) {
32
37
  description: `Cookies: inline payload (${config.inlineCookies.length}) via ${source}.`,
33
38
  };
34
39
  }
35
- if (config?.cookieSync === false) {
36
- return { type: "disabled", description: "Cookies: sync disabled (--browser-no-cookie-sync)." };
40
+ if (config?.cookieSync !== true) {
41
+ return {
42
+ type: "disabled",
43
+ description: "Cookies: Chrome copy disabled (use --browser-manual-login, inline cookies, or --browser-cookie-sync).",
44
+ };
37
45
  }
38
46
  const allowlist = config?.cookieNames && config.cookieNames.length > 0
39
47
  ? config.cookieNames.join(", ")
@@ -0,0 +1,141 @@
1
+ import { mkdir, rm, cp as copyDir } from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+ import path from 'node:path';
5
+ import { defaultProfileRoot, expandPath, looksLikePath } from './chromeCookies.js';
6
+ const DIR_EXCLUDES = [
7
+ 'Cache',
8
+ 'Code Cache',
9
+ 'GPUCache',
10
+ 'Service Worker',
11
+ 'Crashpad',
12
+ 'BrowserMetrics*',
13
+ 'GrShaderCache',
14
+ 'ShaderCache',
15
+ 'OptimizationGuide',
16
+ ];
17
+ const FILE_EXCLUDES = [
18
+ 'SingletonLock',
19
+ 'SingletonSocket',
20
+ 'SingletonCookie',
21
+ '*.lock',
22
+ 'lockfile',
23
+ 'Lock',
24
+ '*.tmp',
25
+ 'DevToolsActivePort',
26
+ path.join('Default', 'DevToolsActivePort'),
27
+ path.join('Sessions', '*'),
28
+ 'Current Session',
29
+ 'Current Tabs',
30
+ 'Last Session',
31
+ 'Last Tabs',
32
+ ];
33
+ export async function syncChromeProfile(options) {
34
+ const { targetDir } = options;
35
+ await mkdir(targetDir, { recursive: true });
36
+ const { sourceDir, profileName } = await resolveProfileSource(options.profile, options.explicitPath);
37
+ const logger = options.logger;
38
+ if (!existsSync(sourceDir)) {
39
+ throw new Error(`Chrome profile not found at ${sourceDir}. Log in once in Chrome, then retry.`);
40
+ }
41
+ // Clean any stale DevTools ports/locks in the target before copying.
42
+ await rm(targetDir, { recursive: true, force: true }).catch(() => undefined);
43
+ await mkdir(targetDir, { recursive: true });
44
+ const result = process.platform === 'win32'
45
+ ? await copyWithRobocopy(sourceDir, targetDir, logger)
46
+ : await copyWithRsync(sourceDir, targetDir, logger);
47
+ // Remove lock files in the copied profile to avoid "already running" errors.
48
+ await removeLocks(targetDir);
49
+ return {
50
+ source: sourceDir,
51
+ profileName,
52
+ method: result.method,
53
+ status: result.copied ? 'copied' : 'skipped',
54
+ };
55
+ }
56
+ async function copyWithRsync(sourceDir, targetDir, logger) {
57
+ const rsyncArgs = [
58
+ '-a',
59
+ '--delete',
60
+ ...DIR_EXCLUDES.flatMap((entry) => ['--exclude', entry]),
61
+ ...FILE_EXCLUDES.flatMap((entry) => ['--exclude', entry]),
62
+ `${sourceDir}/`,
63
+ `${targetDir}/`,
64
+ ];
65
+ const attempt = spawnSync('rsync', rsyncArgs, { stdio: 'pipe' });
66
+ if (!attempt.error && (attempt.status ?? 0) === 0) {
67
+ return { copied: true, method: 'rsync' };
68
+ }
69
+ logger?.('rsync unavailable or failed; falling back to Node copy');
70
+ await copyDirWithFilter(sourceDir, targetDir);
71
+ return copyWithNodeFs();
72
+ }
73
+ async function copyWithRobocopy(sourceDir, targetDir, logger) {
74
+ const args = [sourceDir, targetDir, '/MIR', '/NFL', '/NDL', '/NJH', '/NJS', '/NP', '/Z'];
75
+ if (DIR_EXCLUDES.length) {
76
+ args.push('/XD', ...DIR_EXCLUDES);
77
+ }
78
+ if (FILE_EXCLUDES.length) {
79
+ args.push('/XF', ...FILE_EXCLUDES);
80
+ }
81
+ const attempt = spawnSync('robocopy', args, { stdio: 'pipe' });
82
+ const exitCode = attempt.status ?? 0;
83
+ // Robocopy treats 0-7 as success/partial success; >=8 is failure.
84
+ if (!attempt.error && exitCode < 8) {
85
+ return { copied: true, method: 'robocopy' };
86
+ }
87
+ logger?.('robocopy failed; falling back to Node copy');
88
+ await copyDirWithFilter(sourceDir, targetDir);
89
+ return copyWithNodeFs();
90
+ }
91
+ function copyWithNodeFs() {
92
+ return { copied: true, method: 'node' };
93
+ }
94
+ function shouldExclude(relativePath) {
95
+ const normalized = relativePath.replace(/\\/g, '/');
96
+ return DIR_EXCLUDES.some((entry) => normalized === entry || normalized.startsWith(`${entry}/`)) ||
97
+ FILE_EXCLUDES.some((entry) => {
98
+ if (entry.endsWith('*')) {
99
+ return normalized.startsWith(entry.slice(0, -1));
100
+ }
101
+ if (entry.includes('*')) {
102
+ // simple glob support for BrowserMetrics*
103
+ const prefix = entry.replace('*', '');
104
+ return normalized.startsWith(prefix);
105
+ }
106
+ return path.basename(normalized) === entry || normalized.endsWith(`/${entry}`);
107
+ });
108
+ }
109
+ async function removeLocks(targetDir) {
110
+ const lockNames = ['SingletonLock', 'SingletonSocket', 'SingletonCookie', 'DevToolsActivePort'];
111
+ for (const lock of lockNames) {
112
+ await rm(path.join(targetDir, lock), { force: true }).catch(() => undefined);
113
+ await rm(path.join(targetDir, 'Default', lock), { force: true }).catch(() => undefined);
114
+ }
115
+ }
116
+ async function resolveProfileSource(profile, explicitPath) {
117
+ const profileName = profile?.trim() ? profile.trim() : 'Default';
118
+ if (explicitPath?.trim()) {
119
+ const resolved = expandPath(explicitPath.trim());
120
+ if (resolved.toLowerCase().endsWith('cookies')) {
121
+ return { sourceDir: path.dirname(resolved), profileName };
122
+ }
123
+ return { sourceDir: resolved, profileName };
124
+ }
125
+ if (looksLikePath(profileName)) {
126
+ return { sourceDir: expandPath(profileName), profileName };
127
+ }
128
+ const baseRoot = await defaultProfileRoot();
129
+ return { sourceDir: path.join(baseRoot, profileName), profileName };
130
+ }
131
+ async function copyDirWithFilter(sourceDir, targetDir) {
132
+ await copyDir(sourceDir, targetDir, {
133
+ recursive: true,
134
+ filter: async (source) => {
135
+ const rel = path.relative(sourceDir, source);
136
+ if (!rel)
137
+ return true;
138
+ return !shouldExclude(rel);
139
+ },
140
+ });
141
+ }
@@ -13,6 +13,7 @@ import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, form
13
13
  import { openProjectSourcesTab, uploadProjectSources, waitForProjectSourcesReady, waitForProjectSourcesListSettled, } from "./actions/projectSources.js";
14
14
  import { normalizeProjectSourcesUrl } from "../projectSources/url.js";
15
15
  import { buildProjectSourcesUploadPlan, diffAddedProjectSources } from "../projectSources/plan.js";
16
+ import { CHROME_COOKIE_SYNC_WARNING, shouldSyncBrowserCookies } from "./policies.js";
16
17
  export async function runBrowserProjectSources(request) {
17
18
  const startedAt = Date.now();
18
19
  const logger = ((message) => request.log?.(message));
@@ -248,14 +249,16 @@ export async function runBrowserProjectSources(request) {
248
249
  }
249
250
  }
250
251
  async function applyProjectSourcesCookies({ config, network, manualLogin, logger, }) {
251
- const manualLoginCookieSync = manualLogin && Boolean(config.manualLoginCookieSync);
252
- const cookieSyncEnabled = config.cookieSync && (!manualLogin || manualLoginCookieSync);
252
+ const cookieSyncEnabled = shouldSyncBrowserCookies(config, { manualLogin });
253
253
  if (!cookieSyncEnabled) {
254
254
  logger(manualLogin
255
255
  ? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
256
- : "Skipping Chrome cookie sync (--browser-no-cookie-sync)");
256
+ : "Skipping Chrome cookie copy (disabled by default; use --browser-cookie-sync to opt in).");
257
257
  return 0;
258
258
  }
259
+ if (!config.inlineCookies) {
260
+ logger(CHROME_COOKIE_SYNC_WARNING);
261
+ }
259
262
  const cookieCount = await syncCookies(network, config.url, config.chromeProfile, logger, {
260
263
  allowErrors: config.allowCookieErrors ?? false,
261
264
  filterNames: config.cookieNames ?? undefined,
@@ -12,6 +12,7 @@ import { cleanupStaleProfileState } from "./profileState.js";
12
12
  import { readDevToolsActivePortInfo } from "./detect.js";
13
13
  import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
14
14
  import { waitForDeepResearchCompletion } from "./actions/deepResearch.js";
15
+ import { CHROME_COOKIE_SYNC_WARNING, shouldSyncBrowserCookies } from "./policies.js";
15
16
  export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
16
17
  const recoverSession = deps.recoverSession ??
17
18
  (async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
@@ -176,9 +177,37 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
176
177
  if (manualLogin) {
177
178
  await mkdir(userDataDir, { recursive: true });
178
179
  }
179
- const chrome = await launchChrome(resolved, userDataDir, logger);
180
- const chromeHost = chrome.host ?? "127.0.0.1";
181
- const client = await connectToChrome(chrome.port, logger, chromeHost);
180
+ const launch = deps.launchChrome ?? launchChrome;
181
+ const connectToLaunchedChrome = deps.connectToChrome ?? connectToChrome;
182
+ const chrome = await launch(resolved, userDataDir, logger);
183
+ const chromeHost = chrome && typeof chrome === "object" && "host" in chrome && typeof chrome.host === "string"
184
+ ? chrome.host
185
+ : "127.0.0.1";
186
+ const client = await connectToLaunchedChrome(chrome.port, logger, chromeHost);
187
+ const cleanup = async () => {
188
+ if (client && typeof client.close === "function") {
189
+ try {
190
+ await client.close();
191
+ }
192
+ catch {
193
+ // ignore
194
+ }
195
+ }
196
+ if (!resolved.keepBrowser) {
197
+ try {
198
+ await chrome.kill();
199
+ }
200
+ catch {
201
+ // ignore
202
+ }
203
+ if (manualLogin) {
204
+ await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
205
+ }
206
+ else {
207
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
208
+ }
209
+ }
210
+ };
182
211
  const { Network, Page, Runtime, DOM, Target } = client;
183
212
  if (Runtime?.enable) {
184
213
  await Runtime.enable();
@@ -190,14 +219,24 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
190
219
  await positionChromeWindowOffscreen(client, logger);
191
220
  }
192
221
  let appliedCookies = 0;
193
- if (!manualLogin && resolved.cookieSync) {
194
- appliedCookies = await syncCookies(Network, resolved.url, resolved.chromeProfile, logger, {
195
- allowErrors: resolved.allowCookieErrors,
196
- filterNames: resolved.cookieNames ?? undefined,
197
- inlineCookies: resolved.inlineCookies ?? undefined,
198
- cookiePath: resolved.chromeCookiePath ?? undefined,
199
- waitMs: resolved.cookieSyncWaitMs ?? 0,
200
- });
222
+ if (shouldSyncBrowserCookies(resolved, { manualLogin })) {
223
+ if (!resolved.inlineCookies) {
224
+ logger(CHROME_COOKIE_SYNC_WARNING);
225
+ }
226
+ const sync = deps.syncCookies ?? syncCookies;
227
+ try {
228
+ appliedCookies = await sync(Network, resolved.url, resolved.chromeProfile, logger, {
229
+ allowErrors: resolved.allowCookieErrors,
230
+ filterNames: resolved.cookieNames ?? undefined,
231
+ inlineCookies: resolved.inlineCookies ?? undefined,
232
+ cookiePath: resolved.chromeCookiePath ?? undefined,
233
+ waitMs: resolved.cookieSyncWaitMs ?? 0,
234
+ });
235
+ }
236
+ catch (error) {
237
+ await cleanup();
238
+ throw error;
239
+ }
201
240
  }
202
241
  await clearStaleChatGptConversationCookies(Network, Target, logger, {
203
242
  preserveConversationIds: [
@@ -242,30 +281,6 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
242
281
  const waitForResponse = deps.waitForAssistantResponse ?? waitForAssistantResponse;
243
282
  const captureMarkdown = deps.captureAssistantMarkdown ?? captureAssistantMarkdown;
244
283
  const timeoutMs = resolved.timeoutMs ?? 120_000;
245
- const cleanup = async () => {
246
- if (client && typeof client.close === "function") {
247
- try {
248
- await client.close();
249
- }
250
- catch {
251
- // ignore
252
- }
253
- }
254
- if (!resolved.keepBrowser) {
255
- try {
256
- await chrome.kill();
257
- }
258
- catch {
259
- // ignore
260
- }
261
- if (manualLogin) {
262
- await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "never" }).catch(() => undefined);
263
- }
264
- else {
265
- await rm(userDataDir, { recursive: true, force: true }).catch(() => undefined);
266
- }
267
- }
268
- };
269
284
  const minTurnIndex = (await readPromptPreviewTurnIndex(Runtime, deps.promptPreview)) ??
270
285
  (deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
271
286
  if (resolved.researchMode === "deep") {
@@ -0,0 +1,219 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import { existsSync, promises as fsp } from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import sqlite3 from 'sqlite3';
7
+ import { createRequire } from 'node:module';
8
+ import { spawnSync } from 'node:child_process';
9
+ let cachedUnprotect = null;
10
+ function getUnprotectData() {
11
+ if (cachedUnprotect) {
12
+ return cachedUnprotect;
13
+ }
14
+ try {
15
+ // win-dpapi is CommonJS; require it explicitly and support both named/default shapes.
16
+ const dpapiModule = createRequire(import.meta.url)('win-dpapi');
17
+ const unprotect = dpapiModule?.Dpapi?.unprotectData ??
18
+ dpapiModule?.default?.unprotectData ??
19
+ ((_) => {
20
+ throw new Error('win-dpapi unprotectData not available');
21
+ });
22
+ cachedUnprotect = unprotect;
23
+ return unprotect;
24
+ }
25
+ catch (error) {
26
+ if (process.platform !== 'win32') {
27
+ // On macOS/Linux we don't need DPAPI; return a function that makes misuse obvious.
28
+ cachedUnprotect = () => {
29
+ throw new Error('win-dpapi is unavailable on non-Windows platforms');
30
+ };
31
+ return cachedUnprotect;
32
+ }
33
+ throw error;
34
+ }
35
+ }
36
+ export async function loadWindowsCookies(dbPath, filterNames) {
37
+ if (process.platform !== 'win32') {
38
+ throw new Error('loadWindowsCookies is only supported on Windows');
39
+ }
40
+ const localStatePath = await locateLocalState(dbPath);
41
+ const cookiesCopy = await copyLockedFile(dbPath, 'Cookies'); // name the copy exactly "Cookies"
42
+ const cookiesDirForFallback = await ensureCookiesDirForFallback(cookiesCopy);
43
+ const localStateCopy = await copyLockedFile(localStatePath, 'Local State');
44
+ const aesKey = await extractWindowsAesKey(localStateCopy);
45
+ const rows = await readChromeCookiesDb(cookiesCopy, filterNames);
46
+ const cookies = [];
47
+ for (const row of rows) {
48
+ const enc = row.encrypted_value && row.encrypted_value.length > 0 ? row.encrypted_value : Buffer.from(row.value ?? '', 'utf8');
49
+ const decrypted = enc.length > 0 ? decryptCookie(enc, aesKey) : '';
50
+ cookies.push({
51
+ name: row.name,
52
+ value: decrypted,
53
+ domain: row.host_key?.startsWith('.') ? row.host_key.slice(1) : row.host_key,
54
+ path: row.path ?? '/',
55
+ expires: normalizeExpiration(row.expires_utc),
56
+ secure: Boolean(row.is_secure),
57
+ httpOnly: Boolean(row.is_httponly),
58
+ });
59
+ }
60
+ if (process.env.ORACLE_DEBUG_COOKIES === '1') {
61
+ // eslint-disable-next-line no-console
62
+ console.log(`[cookies] windows decrypt decoded ${cookies.length} cookies from ${cookiesCopy} (fallback dir ${cookiesDirForFallback})`);
63
+ }
64
+ return cookies.filter((c) => c.value);
65
+ }
66
+ function decryptCookie(value, aesKey) {
67
+ const prefix = value.slice(0, 3).toString();
68
+ // Chrome prefixes AES-GCM encrypted cookies with version markers like v10/v11/v20; treat all v** the same.
69
+ if (/^v\d{2}$/u.test(prefix)) {
70
+ const iv = value.slice(3, 15);
71
+ const tag = value.slice(value.length - 16);
72
+ const data = value.slice(15, value.length - 16);
73
+ const decipher = crypto.createDecipheriv('aes-256-gcm', aesKey, iv);
74
+ decipher.setAuthTag(tag);
75
+ const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
76
+ return decrypted.toString('utf8');
77
+ }
78
+ const unprotectData = getUnprotectData();
79
+ const unprotected = unprotectData(value, undefined, 'CurrentUser');
80
+ return Buffer.from(unprotected).toString('utf8');
81
+ }
82
+ async function locateLocalState(dbPath) {
83
+ // Prefer sibling Local State near the profile path; fall back to default location.
84
+ const guess = path.resolve(path.join(path.dirname(dbPath), '..', 'Local State'));
85
+ if (existsSync(guess))
86
+ return guess;
87
+ const localState = path.join(os.homedir(), 'AppData', 'Local', 'Google', 'Chrome', 'User Data', 'Local State');
88
+ if (existsSync(localState))
89
+ return localState;
90
+ throw new Error('Chrome Local State not found for AES key');
91
+ }
92
+ async function extractWindowsAesKey(localStatePath) {
93
+ const raw = await fs.readFile(localStatePath, 'utf8');
94
+ const state = JSON.parse(raw);
95
+ const encKeyB64 = state?.os_crypt?.encrypted_key;
96
+ if (!encKeyB64)
97
+ throw new Error('encrypted_key missing in Local State');
98
+ const encKey = Buffer.from(encKeyB64, 'base64');
99
+ const dpapiBlob = encKey.slice(5); // strip "DPAPI"
100
+ const unprotectData = getUnprotectData();
101
+ const unprotected = unprotectData(dpapiBlob, undefined, 'CurrentUser');
102
+ return Buffer.from(unprotected);
103
+ }
104
+ async function readChromeCookiesDb(dbPath, filterNames) {
105
+ const db = await openSqlite(dbPath);
106
+ const placeholders = filterNames && filterNames.size > 0 ? `AND name IN (${Array.from(filterNames).map(() => '?').join(',')})` : '';
107
+ const params = filterNames ? Array.from(filterNames) : [];
108
+ const sql = `SELECT name,value,encrypted_value,host_key,path,expires_utc,is_secure,is_httponly FROM cookies WHERE host_key LIKE '%chatgpt.com%' ${placeholders}`;
109
+ try {
110
+ return await allSqlite(db, sql, params);
111
+ }
112
+ finally {
113
+ db.close();
114
+ }
115
+ }
116
+ function openSqlite(dbPath) {
117
+ return new Promise((resolve, reject) => {
118
+ const db = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
119
+ if (err)
120
+ reject(err);
121
+ else
122
+ resolve(db);
123
+ });
124
+ });
125
+ }
126
+ async function copyLockedFile(sourcePath, targetName) {
127
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookie-'));
128
+ const tempPath = targetName ? path.join(tempDir, targetName) : path.join(tempDir, path.basename(sourcePath));
129
+ const psScript = `
130
+ $src = '${sourcePath.replace(/'/g, "''")}'
131
+ $dst = '${tempPath.replace(/'/g, "''")}'
132
+ try {
133
+ $fs = [System.IO.File]::Open($src, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
134
+ $bytes = New-Object byte[] $fs.Length
135
+ $null = $fs.Read($bytes, 0, $fs.Length)
136
+ $fs.Close()
137
+ [System.IO.File]::WriteAllBytes($dst, $bytes)
138
+ exit 0
139
+ } catch {
140
+ exit 1
141
+ }
142
+ `;
143
+ const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
144
+ const result = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], {
145
+ timeout: 15000,
146
+ encoding: 'utf8',
147
+ });
148
+ if (result.status !== 0) {
149
+ // Fallback: best-effort normal copy
150
+ await fs.copyFile(sourcePath, tempPath);
151
+ }
152
+ if (process.env.ORACLE_DEBUG_COOKIES === '1') {
153
+ // eslint-disable-next-line no-console
154
+ console.log(`[cookies] copied locked file ${sourcePath} -> ${tempPath}`);
155
+ }
156
+ return tempPath;
157
+ }
158
+ // Create a directory containing a file named "Cookies" for chrome-cookies-secure fallback
159
+ export async function ensureCookiesDirForFallback(cookieFile) {
160
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookies-secure-'));
161
+ const target = path.join(dir, 'Cookies');
162
+ await fs.copyFile(cookieFile, target);
163
+ return dir;
164
+ }
165
+ function allSqlite(db, sql, params) {
166
+ return new Promise((resolve, reject) => {
167
+ db.all(sql, params, (err, rows) => {
168
+ if (err)
169
+ reject(err);
170
+ else
171
+ resolve((rows ?? []));
172
+ });
173
+ });
174
+ }
175
+ function normalizeExpiration(expires) {
176
+ if (!expires || Number.isNaN(expires)) {
177
+ return undefined;
178
+ }
179
+ const value = Number(expires);
180
+ if (value <= 0)
181
+ return undefined;
182
+ if (value > 1_000_000_000_000) {
183
+ return Math.round(value / 1_000_000 - 11644473600);
184
+ }
185
+ if (value > 1_000_000_000) {
186
+ return Math.round(value / 1000);
187
+ }
188
+ return Math.round(value);
189
+ }
190
+ export async function materializeCookieFile(sourcePath) {
191
+ if (process.platform !== 'win32')
192
+ return sourcePath;
193
+ const resolved = await resolveDirectoryCandidate(sourcePath);
194
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'oracle-cookies-'));
195
+ const tempPath = path.join(tempDir, 'Cookies');
196
+ await fs.copyFile(resolved, tempPath);
197
+ return tempPath;
198
+ }
199
+ async function resolveDirectoryCandidate(inputPath) {
200
+ const stat = await fs.stat(inputPath).catch(() => null);
201
+ if (!stat?.isDirectory())
202
+ return inputPath;
203
+ const network = path.join(inputPath, 'Network', 'Cookies');
204
+ const direct = path.join(inputPath, 'Cookies');
205
+ if (await fileExists(network))
206
+ return network;
207
+ if (await fileExists(direct))
208
+ return direct;
209
+ return inputPath;
210
+ }
211
+ async function fileExists(candidate) {
212
+ try {
213
+ const stat = await fsp.stat(candidate);
214
+ return stat.isFile();
215
+ }
216
+ catch {
217
+ return false;
218
+ }
219
+ }