@steipete/oracle 0.16.0 → 0.17.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 (80) hide show
  1. package/README.md +72 -337
  2. package/dist/bin/oracle-cli.js +184 -55
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/docs-site/.nojekyll +0 -0
  5. package/dist/docs-site/CNAME +1 -0
  6. package/dist/docs-site/RELEASING.html +410 -0
  7. package/dist/docs-site/agents.html +374 -0
  8. package/dist/docs-site/anthropic.html +368 -0
  9. package/dist/docs-site/bridge.html +416 -0
  10. package/dist/docs-site/browser-mode.html +594 -0
  11. package/dist/docs-site/chromium-forks.html +347 -0
  12. package/dist/docs-site/cli-reference.html +346 -0
  13. package/dist/docs-site/configuration.html +462 -0
  14. package/dist/docs-site/favicon.svg +14 -0
  15. package/dist/docs-site/followup.html +375 -0
  16. package/dist/docs-site/gemini.html +383 -0
  17. package/dist/docs-site/grok.html +325 -0
  18. package/dist/docs-site/index.html +360 -0
  19. package/dist/docs-site/install.html +335 -0
  20. package/dist/docs-site/linux.html +321 -0
  21. package/dist/docs-site/llms.txt +43 -0
  22. package/dist/docs-site/manual-tests.html +596 -0
  23. package/dist/docs-site/mcp.html +391 -0
  24. package/dist/docs-site/multimodel.html +364 -0
  25. package/dist/docs-site/mythical-pro-agents.html +360 -0
  26. package/dist/docs-site/notifier.html +338 -0
  27. package/dist/docs-site/openai-endpoints.html +410 -0
  28. package/dist/docs-site/openrouter.html +344 -0
  29. package/dist/docs-site/quickstart.html +369 -0
  30. package/dist/docs-site/refactor/ux.html +532 -0
  31. package/dist/docs-site/sessions.html +389 -0
  32. package/dist/docs-site/social-card.png +0 -0
  33. package/dist/docs-site/social-card.svg +79 -0
  34. package/dist/docs-site/spec.html +363 -0
  35. package/dist/docs-site/testing.html +320 -0
  36. package/dist/docs-site/tui-debug.html +326 -0
  37. package/dist/docs-site/windows-work.html +324 -0
  38. package/dist/docs-site/windows.html +320 -0
  39. package/dist/scripts/test-browser.js +1 -25
  40. package/dist/src/browser/actions/modelSelection.js +36 -34
  41. package/dist/src/browser/actions/navigation.js +8 -3
  42. package/dist/src/browser/actions/thinkingTime.js +39 -3
  43. package/dist/src/browser/cdpLiveness.js +78 -0
  44. package/dist/src/browser/chromeLifecycle.js +85 -39
  45. package/dist/src/browser/conversationUrl.js +16 -0
  46. package/dist/src/browser/conversationUrlMonitor.js +2 -4
  47. package/dist/src/browser/cookies.js +6 -4
  48. package/dist/src/browser/index.js +181 -50
  49. package/dist/src/browser/liveTabs.js +2 -2
  50. package/dist/src/browser/modelDisplay.js +67 -0
  51. package/dist/src/browser/reattach.js +14 -1
  52. package/dist/src/browser/reattachHelpers.js +3 -5
  53. package/dist/src/browser/reattachability.js +2 -1
  54. package/dist/src/browser/recoverConversation.js +2 -1
  55. package/dist/src/browser/sessionRunner.js +9 -10
  56. package/dist/src/browser/tabLeaseRegistry.js +9 -3
  57. package/dist/src/browser/wslHost.js +50 -0
  58. package/dist/src/cli/browserConfig.js +31 -11
  59. package/dist/src/cli/browserDefaults.js +4 -1
  60. package/dist/src/cli/detach.js +21 -4
  61. package/dist/src/cli/dryRun.js +13 -2
  62. package/dist/src/cli/engine.js +2 -2
  63. package/dist/src/cli/options.js +4 -0
  64. package/dist/src/cli/sessionDisplay.js +60 -8
  65. package/dist/src/cli/sessionLifecycle.js +2 -1
  66. package/dist/src/cli/sessionRunner.js +159 -62
  67. package/dist/src/cli/sessionTable.js +5 -1
  68. package/dist/src/cli/tui/index.js +12 -4
  69. package/dist/src/duration.js +3 -0
  70. package/dist/src/gemini-web/client.js +19 -1
  71. package/dist/src/oracle/modelResolver.js +8 -1
  72. package/dist/src/oracle/request.js +9 -2
  73. package/dist/src/oracle/run.js +43 -3
  74. package/dist/src/remote/server.js +8 -0
  75. package/dist/src/sessionManager.js +41 -12
  76. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  77. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  78. package/package.json +16 -16
  79. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  80. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -0,0 +1,78 @@
1
+ import { listRemoteChromeTargets } from "./chromeLifecycle.js";
2
+ import { verifyDevToolsReachable } from "./profileState.js";
3
+ /**
4
+ * Probe whether Chrome's DevTools endpoint (and optionally a specific target)
5
+ * is still reachable after a CDP client WebSocket disconnect.
6
+ */
7
+ export async function probeChromeTargetLiveness(options) {
8
+ const host = options.host || "127.0.0.1";
9
+ const port = options.port;
10
+ if (!Number.isFinite(port) || port <= 0) {
11
+ return { endpointReachable: false, targetFound: null, error: "missing debug port" };
12
+ }
13
+ const verifyEndpoint = options.verifyEndpoint ?? verifyDevToolsReachable;
14
+ const endpoint = await verifyEndpoint({ host, port, attempts: 2, timeoutMs: 1500 });
15
+ if (!endpoint.ok) {
16
+ return {
17
+ endpointReachable: false,
18
+ targetFound: null,
19
+ error: endpoint.error,
20
+ };
21
+ }
22
+ const targetId = options.targetId?.trim();
23
+ if (!targetId) {
24
+ return { endpointReachable: true, targetFound: null };
25
+ }
26
+ try {
27
+ const listTargets = options.listTargets ?? listRemoteChromeTargets;
28
+ const targets = await listTargets({
29
+ host,
30
+ port,
31
+ browserWSEndpoint: options.browserWSEndpoint,
32
+ });
33
+ const match = targets.find((target) => {
34
+ const id = target.targetId ?? target.id;
35
+ return id === targetId;
36
+ });
37
+ if (!match) {
38
+ return { endpointReachable: true, targetFound: false };
39
+ }
40
+ return {
41
+ endpointReachable: true,
42
+ targetFound: true,
43
+ matchedUrl: typeof match.url === "string" ? match.url : undefined,
44
+ };
45
+ }
46
+ catch (error) {
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ // Endpoint answered /json/version; treat list failures as still recoverable.
49
+ return { endpointReachable: true, targetFound: null, error: message };
50
+ }
51
+ }
52
+ export function isRecoverableChromeDisconnect(liveness) {
53
+ if (!liveness.endpointReachable) {
54
+ return false;
55
+ }
56
+ // Confirmed live target → recoverable.
57
+ if (liveness.targetFound === true) {
58
+ return true;
59
+ }
60
+ // Confirmed missing target → not recoverable.
61
+ if (liveness.targetFound === false) {
62
+ return false;
63
+ }
64
+ // targetFound === null:
65
+ // - no target id was provided (endpoint-only check) → recoverable
66
+ // - target list failed after a specific id was requested (error set) → fail closed
67
+ return !liveness.error;
68
+ }
69
+ export function connectionLostUserMessage(options) {
70
+ if (options.recoverable) {
71
+ return options.remote
72
+ ? "Remote Chrome DevTools client disconnected before oracle finished; the browser target appears still alive."
73
+ : "Chrome DevTools client disconnected before oracle finished; the browser target appears still alive.";
74
+ }
75
+ return options.remote
76
+ ? "Remote Chrome connection lost before Oracle finished."
77
+ : "Chrome window closed before oracle finished. Please keep it open until completion.";
78
+ }
@@ -1,17 +1,14 @@
1
1
  import { rm } from "node:fs/promises";
2
- import { readFileSync } from "node:fs";
3
- import os from "node:os";
4
2
  import net from "node:net";
5
3
  import CDP from "chrome-remote-interface";
6
4
  import { launch, Launcher } from "chrome-launcher";
7
5
  import { cleanupStaleProfileState } from "./profileState.js";
8
6
  import { delay } from "./utils.js";
7
+ import { isWsl, resolveWslChromeLaunchRoute } from "./wslHost.js";
9
8
  export async function launchChrome(config, userDataDir, logger) {
10
- const connectHost = resolveRemoteDebugHost();
11
- const debugBindAddress = connectHost && connectHost !== "127.0.0.1" ? "0.0.0.0" : connectHost;
9
+ const { connectHost, debugBindAddress, usePatchedLauncher } = resolveWslChromeLaunchRoute();
12
10
  const debugPort = config.debugPort ?? parseDebugPortEnv();
13
11
  const chromeFlags = buildChromeFlags(config.headless ?? false, debugBindAddress, config.hideWindow ?? false);
14
- const usePatchedLauncher = Boolean(connectHost && connectHost !== "127.0.0.1");
15
12
  // copy-profile reuses a copied signed-in profile whose cookies are
16
13
  // Keychain-encrypted, so it must launch with the real Keychain (not mocked):
17
14
  // strip the keychain-mocking flags from both chrome-launcher's defaults and
@@ -406,13 +403,79 @@ export async function closeTab(port, targetId, logger, host) {
406
403
  const effectiveHost = host ?? "127.0.0.1";
407
404
  try {
408
405
  await CDP.Close({ host: effectiveHost, port, id: targetId });
409
- logger(`Closed isolated browser tab (target=${targetId})`);
406
+ for (let attempt = 0; attempt < 40; attempt += 1) {
407
+ await delay(25);
408
+ let targets;
409
+ try {
410
+ targets = (await CDP.List({ host: effectiveHost, port }));
411
+ }
412
+ catch {
413
+ continue;
414
+ }
415
+ if (!targets.some((target) => (target.targetId ?? target.id) === targetId)) {
416
+ logger(`Closed isolated browser tab (target=${targetId})`);
417
+ return true;
418
+ }
419
+ }
420
+ logger(`Browser tab close was not confirmed (target=${targetId})`);
421
+ return false;
410
422
  }
411
423
  catch (error) {
424
+ try {
425
+ const targets = (await CDP.List({ host: effectiveHost, port }));
426
+ if (!targets.some((target) => (target.targetId ?? target.id) === targetId)) {
427
+ logger(`Closed isolated browser tab (target=${targetId})`);
428
+ return true;
429
+ }
430
+ }
431
+ catch {
432
+ // Preserve the original close error below.
433
+ }
412
434
  const message = error instanceof Error ? error.message : String(error);
413
435
  logger(`Failed to close browser tab ${targetId}: ${message}`);
436
+ return false;
414
437
  }
415
438
  }
439
+ export async function createChromePageTarget(port, logger, host) {
440
+ const effectiveHost = host ?? "127.0.0.1";
441
+ try {
442
+ const created = (await CDP.New({
443
+ host: effectiveHost,
444
+ port,
445
+ url: "about:blank",
446
+ }));
447
+ const createdTargetId = created.targetId ?? created.id;
448
+ if (!createdTargetId) {
449
+ logger("Failed to create a replacement Chrome tab.");
450
+ return undefined;
451
+ }
452
+ logger(`Opened replacement Chrome tab (target=${createdTargetId})`);
453
+ return createdTargetId;
454
+ }
455
+ catch (error) {
456
+ const message = error instanceof Error ? error.message : String(error);
457
+ logger(`Failed to create a replacement Chrome tab: ${message}`);
458
+ return undefined;
459
+ }
460
+ }
461
+ export async function ensureChromePageTargetAfterClose(port, closingTargetId, logger, host) {
462
+ const effectiveHost = host ?? "127.0.0.1";
463
+ try {
464
+ const targets = (await CDP.List({ host: effectiveHost, port }));
465
+ const existingPageTargetId = targets
466
+ .filter((target) => target.type === "page")
467
+ .map((target) => target.targetId ?? target.id)
468
+ .find((targetId) => Boolean(targetId) && targetId !== closingTargetId);
469
+ if (existingPageTargetId) {
470
+ return existingPageTargetId;
471
+ }
472
+ }
473
+ catch (error) {
474
+ const message = error instanceof Error ? error.message : String(error);
475
+ logger(`Failed to inspect Chrome tabs before closing ${closingTargetId}: ${message}`);
476
+ }
477
+ return await createChromePageTarget(port, logger, host);
478
+ }
416
479
  export async function closeBlankChromeTabs(port, logger, host, options) {
417
480
  const effectiveHost = host ?? "127.0.0.1";
418
481
  const excluded = new Set([...(options?.excludeTargetIds ?? [])].filter((targetId) => typeof targetId === "string" && targetId.length > 0));
@@ -425,10 +488,20 @@ export async function closeBlankChromeTabs(port, logger, host, options) {
425
488
  logger(`Failed to inspect blank Chrome tabs: ${message}`);
426
489
  return;
427
490
  }
491
+ const preservedBlankTargetId = options?.preserveOneBlank
492
+ ? targets
493
+ .filter(isBlankPageTarget)
494
+ .map((target) => target.targetId ?? target.id)
495
+ .filter((targetId) => Boolean(targetId))
496
+ .sort()[0]
497
+ : undefined;
428
498
  let closed = 0;
429
499
  for (const target of targets) {
430
500
  const targetId = target.targetId ?? target.id;
431
- if (!targetId || excluded.has(targetId) || !isBlankPageTarget(target)) {
501
+ if (!targetId ||
502
+ targetId === preservedBlankTargetId ||
503
+ excluded.has(targetId) ||
504
+ !isBlankPageTarget(target)) {
432
505
  continue;
433
506
  }
434
507
  try {
@@ -487,6 +560,11 @@ function buildChromeFlags(headless, debugBindAddress, hideWindow = false) {
487
560
  // off-screen avoids desktop disruption while preserving normal rendering.
488
561
  flags.push("--window-position=-32000,-32000");
489
562
  }
563
+ // Opt-in only: container/CI Chromium often cannot use the sandbox. Callers must
564
+ // set ORACLE_CHROME_NO_SANDBOX=1 explicitly (never default this on).
565
+ if (process.env.ORACLE_CHROME_NO_SANDBOX === "1") {
566
+ flags.push("--no-sandbox", "--disable-dev-shm-usage");
567
+ }
490
568
  return flags;
491
569
  }
492
570
  export function buildChromeFlagsForTest(headless, debugBindAddress, hideWindow = false) {
@@ -514,38 +592,6 @@ function parseDebugPortEnv() {
514
592
  }
515
593
  return value;
516
594
  }
517
- function resolveRemoteDebugHost() {
518
- const override = process.env.ORACLE_BROWSER_REMOTE_DEBUG_HOST?.trim() || process.env.WSL_HOST_IP?.trim();
519
- if (override) {
520
- return override;
521
- }
522
- if (!isWsl()) {
523
- return null;
524
- }
525
- try {
526
- const resolv = readFileSync("/etc/resolv.conf", "utf8");
527
- for (const line of resolv.split("\n")) {
528
- const match = line.match(/^nameserver\s+([0-9.]+)/);
529
- if (match?.[1]) {
530
- return match[1];
531
- }
532
- }
533
- }
534
- catch {
535
- // ignore; fall back to localhost
536
- }
537
- return null;
538
- }
539
- function isWsl() {
540
- if (process.platform !== "linux") {
541
- return false;
542
- }
543
- if (process.env.WSL_DISTRO_NAME) {
544
- return true;
545
- }
546
- const release = os.release();
547
- return release.toLowerCase().includes("microsoft");
548
- }
549
595
  async function launchWithCustomHost({ chromeFlags, chromePath, userDataDir, host, requestedPort, ignoreDefaultFlags, }) {
550
596
  const launcher = new Launcher({
551
597
  chromePath: chromePath ?? undefined,
@@ -0,0 +1,16 @@
1
+ const CONVERSATION_ID_PATH = /\/c\/([a-zA-Z0-9-]+)(?=[/?#]|$)/;
2
+ /**
3
+ * Extract a durable ChatGPT conversation id from a URL.
4
+ *
5
+ * ChatGPT can briefly expose client-created routes such as `/c/WEB:<request-id>`
6
+ * before replacing them with the persisted conversation URL. Those transient
7
+ * routes must not be used to scope assistant-response capture or reattachment.
8
+ */
9
+ export function extractStableConversationIdFromUrl(url) {
10
+ if (!url)
11
+ return undefined;
12
+ return url.match(CONVERSATION_ID_PATH)?.[1];
13
+ }
14
+ export function isStableConversationUrl(url) {
15
+ return extractStableConversationIdFromUrl(url) !== undefined;
16
+ }
@@ -1,4 +1,5 @@
1
1
  import { delay } from "./utils.js";
2
+ import { isStableConversationUrl } from "./conversationUrl.js";
2
3
  export function createConversationUrlMonitor(options) {
3
4
  const pollIntervalMs = options.pollIntervalMs ?? 250;
4
5
  const wait = options.wait ?? delay;
@@ -14,7 +15,7 @@ export function createConversationUrlMonitor(options) {
14
15
  if (stopped) {
15
16
  return false;
16
17
  }
17
- if (url && isConversationUrl(url)) {
18
+ if (url && isStableConversationUrl(url)) {
18
19
  options.logger(`[browser] conversation url (${label}) = ${url}`);
19
20
  const persist = options.persistUrl(url);
20
21
  activePersists.add(persist);
@@ -59,6 +60,3 @@ export function createConversationUrlMonitor(options) {
59
60
  },
60
61
  };
61
62
  }
62
- function isConversationUrl(url) {
63
- return /\/c\/[a-z0-9-]+/i.test(url);
64
- }
@@ -244,12 +244,14 @@ function normalizeExpiration(expires) {
244
244
  if (value <= 0) {
245
245
  return undefined;
246
246
  }
247
- if (value > 1_000_000_000_000) {
248
- // Learned: Chrome may store WebKit microseconds since 1601; convert to Unix seconds.
247
+ // Units by magnitude (do not treat Unix seconds ~1.7e9 as milliseconds):
248
+ // - >= 1e15: Chrome/WebKit FILETIME microseconds since 1601
249
+ // - >= 1e12: Unix milliseconds
250
+ // - else: Unix seconds (sweet-cookie / Chromium Cookie.expires)
251
+ if (value >= 1_000_000_000_000_000) {
249
252
  return Math.round(value / 1_000_000 - 11644473600);
250
253
  }
251
- if (value > 1_000_000_000) {
252
- // Likely milliseconds; normalize to seconds for CDP.
254
+ if (value >= 1_000_000_000_000) {
253
255
  return Math.round(value / 1000);
254
256
  }
255
257
  return Math.round(value);