@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.
- package/README.md +72 -337
- package/dist/bin/oracle-cli.js +184 -55
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +416 -0
- package/dist/docs-site/browser-mode.html +594 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +462 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +410 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +389 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +324 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/scripts/test-browser.js +1 -25
- package/dist/src/browser/actions/modelSelection.js +36 -34
- package/dist/src/browser/actions/navigation.js +8 -3
- package/dist/src/browser/actions/thinkingTime.js +39 -3
- package/dist/src/browser/cdpLiveness.js +78 -0
- package/dist/src/browser/chromeLifecycle.js +85 -39
- package/dist/src/browser/conversationUrl.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +2 -4
- package/dist/src/browser/cookies.js +6 -4
- package/dist/src/browser/index.js +181 -50
- package/dist/src/browser/liveTabs.js +2 -2
- package/dist/src/browser/modelDisplay.js +67 -0
- package/dist/src/browser/reattach.js +14 -1
- package/dist/src/browser/reattachHelpers.js +3 -5
- package/dist/src/browser/reattachability.js +2 -1
- package/dist/src/browser/recoverConversation.js +2 -1
- package/dist/src/browser/sessionRunner.js +9 -10
- package/dist/src/browser/tabLeaseRegistry.js +9 -3
- package/dist/src/browser/wslHost.js +50 -0
- package/dist/src/cli/browserConfig.js +31 -11
- package/dist/src/cli/browserDefaults.js +4 -1
- package/dist/src/cli/detach.js +21 -4
- package/dist/src/cli/dryRun.js +13 -2
- package/dist/src/cli/engine.js +2 -2
- package/dist/src/cli/options.js +4 -0
- package/dist/src/cli/sessionDisplay.js +60 -8
- package/dist/src/cli/sessionLifecycle.js +2 -1
- package/dist/src/cli/sessionRunner.js +159 -62
- package/dist/src/cli/sessionTable.js +5 -1
- package/dist/src/cli/tui/index.js +12 -4
- package/dist/src/duration.js +3 -0
- package/dist/src/gemini-web/client.js +19 -1
- package/dist/src/oracle/modelResolver.js +8 -1
- package/dist/src/oracle/request.js +9 -2
- package/dist/src/oracle/run.js +43 -3
- package/dist/src/remote/server.js +8 -0
- package/dist/src/sessionManager.js +41 -12
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +16 -16
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- 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 =
|
|
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
|
-
|
|
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 ||
|
|
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 &&
|
|
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
|
-
|
|
248
|
-
|
|
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
|
|
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);
|