@steipete/oracle 0.12.1 → 0.14.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 -61
- package/dist/bin/oracle-cli.js +77 -68
- 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 +400 -0
- package/dist/docs-site/browser-mode.html +593 -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 +452 -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 +387 -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 +388 -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 +323 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/src/browser/actions/assistantResponse.js +91 -9
- package/dist/src/browser/actions/deepResearch.js +180 -52
- package/dist/src/browser/actions/modelSelection.js +111 -24
- package/dist/src/browser/actions/navigation.js +358 -67
- package/dist/src/browser/actions/promptComposer.js +241 -46
- package/dist/src/browser/actions/thinkingTime.js +308 -89
- package/dist/src/browser/artifacts.js +19 -0
- package/dist/src/browser/chatgptFiles.js +797 -0
- package/dist/src/browser/chatgptImages.js +130 -3
- package/dist/src/browser/chromeLifecycle.js +4 -0
- package/dist/src/browser/config.js +4 -0
- package/dist/src/browser/index.js +353 -41
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/policies.js +2 -6
- package/dist/src/browser/projectSourcesRunner.js +14 -2
- package/dist/src/browser/prompt.js +11 -15
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattach.js +6 -2
- package/dist/src/browser/reattachability.js +22 -10
- package/dist/src/browser/recoverConversation.js +73 -0
- package/dist/src/browser/sessionRunner.js +4 -1
- package/dist/src/cli/bridge/doctor.js +7 -2
- package/dist/src/cli/browserConfig.js +13 -2
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +129 -54
- package/dist/src/cli/engine.js +6 -2
- package/dist/src/cli/followup.js +72 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +20 -0
- package/dist/src/cli/reattachGuidance.js +8 -0
- package/dist/src/cli/runOptions.js +10 -31
- package/dist/src/cli/sessionCommand.js +4 -0
- package/dist/src/cli/sessionDisplay.js +17 -3
- package/dist/src/cli/sessionLineage.js +7 -4
- package/dist/src/cli/sessionRunner.js +16 -1
- package/dist/src/config.js +164 -9
- package/dist/src/gemini-web/client.js +5 -10
- package/dist/src/gemini-web/executor.js +1 -24
- package/dist/src/gemini-web/models.js +83 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/chatgptImage.js +127 -0
- package/dist/src/mcp/tools/consult.js +261 -167
- package/dist/src/mcp/types.js +2 -0
- package/dist/src/mcp/utils.js +87 -1
- package/dist/src/oracle/config.js +27 -1
- package/dist/src/oracle/files.js +4 -6
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/markdown.js +36 -3
- package/dist/src/oracle/modelResolver.js +21 -13
- package/dist/src/oracle/promptAssembly.js +2 -4
- package/dist/src/oracle/request.js +3 -5
- package/dist/src/oracle/tokenStats.js +5 -1
- package/dist/src/oracle.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- 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 +20 -29
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, installJavaScriptDialogAutoDismissal, } from "./actions/navigation.js";
|
|
1
|
+
export { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, } from "./actions/navigation.js";
|
|
2
2
|
export { ensureModelSelection } from "./actions/modelSelection.js";
|
|
3
3
|
export { submitPrompt, clearPromptComposer } from "./actions/promptComposer.js";
|
|
4
4
|
export { clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, buildUserTurnAttachmentExpressionForTest, } from "./actions/attachments.js";
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatFileSections } from "../oracle/markdown.js";
|
|
2
2
|
export function buildAttachmentPlan(sections, { inlineFiles, bundleRequested, maxAttachments = 10, }) {
|
|
3
3
|
if (inlineFiles) {
|
|
4
|
-
const
|
|
5
|
-
sections.forEach((section) => {
|
|
6
|
-
inlineLines.push(formatFileSection(section.displayPath, section.content).trimEnd(), "");
|
|
7
|
-
});
|
|
8
|
-
const inlineBlock = inlineLines.join("\n").trim();
|
|
4
|
+
const inlineBlock = formatFileSections(sections);
|
|
9
5
|
return {
|
|
10
6
|
mode: "inline",
|
|
11
7
|
inlineBlock,
|
|
@@ -347,11 +347,20 @@ async function maybeReuseProjectSourcesChrome(userDataDir, logger, options = {})
|
|
|
347
347
|
if (!port) {
|
|
348
348
|
const discovered = await findRunningChromeDebugTargetForProfile(userDataDir);
|
|
349
349
|
if (!discovered) {
|
|
350
|
+
if (pid) {
|
|
351
|
+
logger(`No reachable Chrome DevTools target found for ${userDataDir}; clearing stale profile state before launching new Chrome.`);
|
|
352
|
+
await cleanupStaleProfileState(userDataDir, logger, {
|
|
353
|
+
lockRemovalMode: "if_oracle_pid_dead",
|
|
354
|
+
});
|
|
355
|
+
}
|
|
350
356
|
return null;
|
|
351
357
|
}
|
|
352
|
-
const probe = await verifyDevToolsReachable({ port: discovered.port });
|
|
358
|
+
const probe = await (options.probe ?? verifyDevToolsReachable)({ port: discovered.port });
|
|
353
359
|
if (!probe.ok) {
|
|
354
360
|
logger(`Discovered Chrome for ${userDataDir} on port ${discovered.port} but it was unreachable (${probe.error}); launching new Chrome.`);
|
|
361
|
+
await cleanupStaleProfileState(userDataDir, logger, {
|
|
362
|
+
lockRemovalMode: "if_oracle_pid_dead",
|
|
363
|
+
});
|
|
355
364
|
return null;
|
|
356
365
|
}
|
|
357
366
|
await writeDevToolsActivePort(userDataDir, discovered.port);
|
|
@@ -361,7 +370,7 @@ async function maybeReuseProjectSourcesChrome(userDataDir, logger, options = {})
|
|
|
361
370
|
logger(`Discovered running Chrome for ${userDataDir}; reusing (DevTools port ${port}, pid ${pid})`);
|
|
362
371
|
return { port, pid, kill: async () => { }, process: undefined };
|
|
363
372
|
}
|
|
364
|
-
const probe = await verifyDevToolsReachable({ port });
|
|
373
|
+
const probe = await (options.probe ?? verifyDevToolsReachable)({ port });
|
|
365
374
|
if (!probe.ok) {
|
|
366
375
|
logger(`Recorded Chrome DevTools port ${port} is stale (${probe.error}); launching new Chrome.`);
|
|
367
376
|
await cleanupStaleProfileState(userDataDir, logger, { lockRemovalMode: "if_oracle_pid_dead" });
|
|
@@ -375,3 +384,6 @@ async function maybeReuseProjectSourcesChrome(userDataDir, logger, options = {})
|
|
|
375
384
|
process: undefined,
|
|
376
385
|
};
|
|
377
386
|
}
|
|
387
|
+
export async function maybeReuseProjectSourcesChromeForTest(userDataDir, logger, options = {}) {
|
|
388
|
+
return maybeReuseProjectSourcesChrome(userDataDir, logger, options);
|
|
389
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS,
|
|
4
|
+
import { readFiles, createFileSections, MODEL_CONFIGS, TOKENIZER_OPTIONS, formatFileSections, } from "../oracle.js";
|
|
5
5
|
import { isKnownModel } from "../oracle/modelResolver.js";
|
|
6
6
|
import { buildPromptMarkdown } from "../oracle/promptAssembly.js";
|
|
7
7
|
import { buildAttachmentPlan } from "./policies.js";
|
|
@@ -35,20 +35,17 @@ export function isMediaFile(filePath) {
|
|
|
35
35
|
const ext = path.extname(filePath).toLowerCase();
|
|
36
36
|
return MEDIA_EXTENSIONS.has(ext);
|
|
37
37
|
}
|
|
38
|
-
function formatSectionsForBundle(sections) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
bundleLines.push("");
|
|
38
|
+
function formatSectionsForBundle(sections, options = {}) {
|
|
39
|
+
return formatFileSections(sections, {
|
|
40
|
+
lineNumbers: options.lineNumbers ?? true,
|
|
41
|
+
trailingNewline: true,
|
|
43
42
|
});
|
|
44
|
-
return `${bundleLines
|
|
45
|
-
.join("\n")
|
|
46
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
47
|
-
.trimEnd()}\n`;
|
|
48
43
|
}
|
|
49
44
|
async function writeBrowserBundle(sections, format) {
|
|
50
45
|
const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
|
|
51
|
-
const tokenEstimateText = formatSectionsForBundle(sections
|
|
46
|
+
const tokenEstimateText = formatSectionsForBundle(sections, {
|
|
47
|
+
lineNumbers: format === "text",
|
|
48
|
+
});
|
|
52
49
|
if (format === "zip") {
|
|
53
50
|
const bundlePath = path.join(bundleDir, "attachments-bundle.zip");
|
|
54
51
|
const buffer = createStoredZip(sections.map((section) => ({
|
|
@@ -61,6 +58,7 @@ async function writeBrowserBundle(sections, format) {
|
|
|
61
58
|
path: bundlePath,
|
|
62
59
|
displayPath: bundlePath,
|
|
63
60
|
sizeBytes: buffer.length,
|
|
61
|
+
generatedBundle: true,
|
|
64
62
|
},
|
|
65
63
|
metadata: { originalCount: sections.length, bundlePath, format },
|
|
66
64
|
tokenEstimateText,
|
|
@@ -73,6 +71,7 @@ async function writeBrowserBundle(sections, format) {
|
|
|
73
71
|
path: bundlePath,
|
|
74
72
|
displayPath: bundlePath,
|
|
75
73
|
sizeBytes: Buffer.byteLength(tokenEstimateText, "utf8"),
|
|
74
|
+
generatedBundle: true,
|
|
76
75
|
},
|
|
77
76
|
metadata: { originalCount: sections.length, bundlePath, format },
|
|
78
77
|
tokenEstimateText,
|
|
@@ -161,10 +160,7 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
|
|
|
161
160
|
let estimatedInputTokens = tokenizer(tokenizerMessages.length > 0 ? tokenizerMessages : [{ role: "user", content: "" }], TOKENIZER_OPTIONS);
|
|
162
161
|
const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(selectedPlan.inlineBlock);
|
|
163
162
|
if (!tokenEstimateIncludesInlineFiles && sections.length > 0) {
|
|
164
|
-
const attachmentText = bundleText ??
|
|
165
|
-
sections
|
|
166
|
-
.map((section) => formatFileSection(section.displayPath, section.content).trimEnd())
|
|
167
|
-
.join("\n\n");
|
|
163
|
+
const attachmentText = bundleText ?? formatFileSections(sections, { lineNumbers: false });
|
|
168
164
|
const attachmentTokens = tokenizer([{ role: "user", content: attachmentText }], TOKENIZER_OPTIONS);
|
|
169
165
|
estimatedInputTokens += attachmentTokens;
|
|
170
166
|
}
|
|
@@ -23,6 +23,7 @@ async function submitPromptViaAdapter(ctx) {
|
|
|
23
23
|
attachmentNames: state.attachmentNames ?? [],
|
|
24
24
|
baselineTurns: state.baselineTurns ?? undefined,
|
|
25
25
|
inputTimeoutMs: state.inputTimeoutMs ?? undefined,
|
|
26
|
+
attachmentTimeoutMs: state.attachmentTimeoutMs ?? undefined,
|
|
26
27
|
onPromptSubmitted: state.onPromptSubmitted,
|
|
27
28
|
}, ctx.prompt, state.logger);
|
|
28
29
|
state.committedTurns =
|
|
@@ -94,7 +94,9 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
94
94
|
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
95
95
|
if (config?.researchMode === "deep") {
|
|
96
96
|
const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
|
|
97
|
-
const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client
|
|
97
|
+
const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
98
|
+
requireScopedTargetOwner: true,
|
|
99
|
+
}), timeoutMs + 5_000, "Reattach Deep Research response timed out");
|
|
98
100
|
await connection.close().catch(() => undefined);
|
|
99
101
|
return {
|
|
100
102
|
answerText: researchResult.text,
|
|
@@ -239,7 +241,9 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
239
241
|
(deps.promptPreview ? null : await readConversationTurnIndex(Runtime, logger));
|
|
240
242
|
if (resolved.researchMode === "deep") {
|
|
241
243
|
const waitForDeepResearch = deps.waitForDeepResearchCompletion ?? waitForDeepResearchCompletion;
|
|
242
|
-
const researchResult = await waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client
|
|
244
|
+
const researchResult = await waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
245
|
+
requireScopedTargetOwner: true,
|
|
246
|
+
});
|
|
243
247
|
await cleanup();
|
|
244
248
|
return {
|
|
245
249
|
answerText: researchResult.text,
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
if (!
|
|
1
|
+
/**
|
|
2
|
+
* True when the URL points at a specific ChatGPT conversation (`/c/<id>`) on
|
|
3
|
+
* chatgpt.com or chat.openai.com. Rejects home, project shell, and external
|
|
4
|
+
* URLs — anything else would be unsafe to auto-reopen in a persistent
|
|
5
|
+
* signed-in browser profile.
|
|
6
|
+
*/
|
|
7
|
+
export function isRecoverableChatGptConversationUrl(candidate) {
|
|
8
|
+
const trimmed = candidate?.trim();
|
|
9
|
+
if (!trimmed) {
|
|
10
10
|
return false;
|
|
11
11
|
}
|
|
12
12
|
try {
|
|
13
|
-
const url = new URL(
|
|
13
|
+
const url = new URL(trimmed);
|
|
14
|
+
if (url.protocol !== "https:" || url.port) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
14
17
|
if (url.hostname !== "chatgpt.com" && url.hostname !== "chat.openai.com") {
|
|
15
18
|
return false;
|
|
16
19
|
}
|
|
@@ -20,3 +23,12 @@ export function hasRecoverableChatGptConversation(runtime) {
|
|
|
20
23
|
return false;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
26
|
+
export function hasRecoverableChatGptConversation(runtime) {
|
|
27
|
+
if (!runtime) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (runtime.conversationId?.trim()) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return isRecoverableChatGptConversationUrl(runtime.tabUrl);
|
|
34
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { launch } from "chrome-launcher";
|
|
2
|
+
import { defaultManualLoginProfileDir } from "./manualLoginProfile.js";
|
|
3
|
+
import { isRecoverableChatGptConversationUrl } from "./reattachability.js";
|
|
4
|
+
const DEFAULT_HYDRATION_DELAY_MS = 3_000;
|
|
5
|
+
/**
|
|
6
|
+
* Picks the URL to navigate the recovered Chrome tab to.
|
|
7
|
+
*
|
|
8
|
+
* Preference order matches `resolveSessionTabRef`: `harvest.url` (post-harvest,
|
|
9
|
+
* always a ChatGPT conversation URL when present) wins over `runtime.tabUrl`
|
|
10
|
+
* (the URL the original run last navigated to, which can be stale).
|
|
11
|
+
*
|
|
12
|
+
* Both candidates are gated by `isRecoverableChatGptConversationUrl` so a stale
|
|
13
|
+
* home / project shell URL or an unrelated external URL stored in metadata
|
|
14
|
+
* cannot navigate the persistent signed-in profile to the wrong page.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveRecoveryUrl(meta) {
|
|
17
|
+
const harvest = meta?.browser?.harvest ?? {};
|
|
18
|
+
const runtime = meta?.browser?.runtime ?? {};
|
|
19
|
+
for (const candidate of [harvest.url, runtime.tabUrl]) {
|
|
20
|
+
if (isRecoverableChatGptConversationUrl(candidate)) {
|
|
21
|
+
return candidate;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function resolveProfileDir(meta) {
|
|
27
|
+
const fromMeta = meta?.browser?.config?.manualLoginProfileDir;
|
|
28
|
+
if (typeof fromMeta === "string" && fromMeta.length > 0) {
|
|
29
|
+
return fromMeta;
|
|
30
|
+
}
|
|
31
|
+
return defaultManualLoginProfileDir();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Re-open a previously-harvested ChatGPT conversation by relaunching Chrome
|
|
35
|
+
* with the session's persistent profile and navigating to the saved tab URL.
|
|
36
|
+
*
|
|
37
|
+
* Used as a fallback when `harvestChatGptTab` can find no live tab matching the
|
|
38
|
+
* stored target (common after the original CLI run exits and closes its
|
|
39
|
+
* browser). ChatGPT preserves attachments + history at the conversation URL,
|
|
40
|
+
* so harvesting against the relaunched tab returns the original message + any
|
|
41
|
+
* assistant response that completed after the original run gave up.
|
|
42
|
+
*/
|
|
43
|
+
export async function recoverConversationTab(meta, logger, options = {}) {
|
|
44
|
+
const url = resolveRecoveryUrl(meta);
|
|
45
|
+
if (!url) {
|
|
46
|
+
throw new Error("Cannot recover conversation: session metadata has no recoverable ChatGPT conversation URL " +
|
|
47
|
+
"(expected browser.harvest.url or browser.runtime.tabUrl to be a chatgpt.com/c/<id> URL).");
|
|
48
|
+
}
|
|
49
|
+
const userDataDir = resolveProfileDir(meta);
|
|
50
|
+
logger(`[browser] Recovery: relaunching Chrome with profile ${userDataDir} and navigating to ${url}`);
|
|
51
|
+
const chrome = await launch({
|
|
52
|
+
chromeFlags: [
|
|
53
|
+
"--no-first-run",
|
|
54
|
+
"--no-default-browser-check",
|
|
55
|
+
"--disable-features=AutomationControlled,TranslateUI",
|
|
56
|
+
"--disable-sync",
|
|
57
|
+
"--password-store=basic",
|
|
58
|
+
"--use-mock-keychain",
|
|
59
|
+
"--lang=en-US",
|
|
60
|
+
url,
|
|
61
|
+
],
|
|
62
|
+
userDataDir,
|
|
63
|
+
handleSIGINT: false,
|
|
64
|
+
});
|
|
65
|
+
const host = "127.0.0.1";
|
|
66
|
+
const port = chrome.port;
|
|
67
|
+
const hydrationDelayMs = options.hydrationDelayMs ?? DEFAULT_HYDRATION_DELAY_MS;
|
|
68
|
+
if (hydrationDelayMs > 0) {
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, hydrationDelayMs));
|
|
70
|
+
}
|
|
71
|
+
logger(`[browser] Recovery: Chrome listening on ${host}:${port}; tab loaded.`);
|
|
72
|
+
return { host, port, url, chrome };
|
|
73
|
+
}
|
|
@@ -105,6 +105,9 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
105
105
|
log(chalk.dim("Chrome automation does not stream output; this may take a minute..."));
|
|
106
106
|
}
|
|
107
107
|
const persistRuntimeHint = deps.persistRuntimeHint ?? (() => { });
|
|
108
|
+
const executionBrowserConfig = runOptions.browserResumeConversationUrl
|
|
109
|
+
? { ...browserConfig, resumeConversationUrl: runOptions.browserResumeConversationUrl }
|
|
110
|
+
: browserConfig;
|
|
108
111
|
let browserResult;
|
|
109
112
|
try {
|
|
110
113
|
browserResult = await executeBrowser({
|
|
@@ -116,7 +119,7 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
116
119
|
attachments: promptArtifacts.fallback.attachments,
|
|
117
120
|
}
|
|
118
121
|
: undefined,
|
|
119
|
-
config:
|
|
122
|
+
config: executionBrowserConfig,
|
|
120
123
|
log: automationLogger,
|
|
121
124
|
heartbeatIntervalMs: runOptions.heartbeatIntervalMs,
|
|
122
125
|
verbose: runOptions.verbose,
|
|
@@ -7,8 +7,9 @@ import { checkTcpConnection, checkRemoteHealth } from "../../remote/health.js";
|
|
|
7
7
|
import { detectChromeBinary, detectChromeCookieDb } from "../../browser/detect.js";
|
|
8
8
|
import { formatCodexMcpSnippet } from "./codexConfig.js";
|
|
9
9
|
export async function runBridgeDoctor(_options) {
|
|
10
|
-
const { config: userConfig, path: configPath, loaded } = await loadUserConfig();
|
|
10
|
+
const { config: userConfig, path: configPath, paths: configPaths, loaded: userConfigLoaded, } = await loadUserConfig();
|
|
11
11
|
const version = getCliVersion();
|
|
12
|
+
const projectConfigPaths = configPaths.filter((entry) => entry !== configPath);
|
|
12
13
|
const resolvedRemote = resolveRemoteServiceConfig({
|
|
13
14
|
cliHost: undefined,
|
|
14
15
|
cliToken: undefined,
|
|
@@ -22,7 +23,11 @@ export async function runBridgeDoctor(_options) {
|
|
|
22
23
|
lines.push(chalk.dim(`OS: ${process.platform} ${os.release()} (${process.arch})`));
|
|
23
24
|
lines.push(chalk.dim(`Node: ${process.version}`));
|
|
24
25
|
lines.push(chalk.dim(`Oracle: ${version}`));
|
|
25
|
-
lines.push(chalk.dim(`Config: ${
|
|
26
|
+
lines.push(chalk.dim(`Config: ${userConfigLoaded ? configPath : `${configPath} (missing)`}`));
|
|
27
|
+
if (projectConfigPaths.length > 0) {
|
|
28
|
+
const label = projectConfigPaths.length === 1 ? "Project config" : "Project configs";
|
|
29
|
+
lines.push(chalk.dim(`${label}: ${projectConfigPaths.join(", ")}`));
|
|
30
|
+
}
|
|
26
31
|
if (userConfig.engine) {
|
|
27
32
|
lines.push(chalk.dim(`Default engine: ${userConfig.engine}`));
|
|
28
33
|
}
|
|
@@ -7,6 +7,7 @@ import { normalizeBrowserModelStrategy } from "../browser/modelStrategy.js";
|
|
|
7
7
|
import { getOracleHomeDir } from "../oracleHome.js";
|
|
8
8
|
const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
|
|
9
9
|
const DEFAULT_BROWSER_INPUT_TIMEOUT_MS = 60_000;
|
|
10
|
+
const DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS = 45_000;
|
|
10
11
|
const DEFAULT_BROWSER_RECHECK_TIMEOUT_MS = 120_000;
|
|
11
12
|
const DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS = 120_000;
|
|
12
13
|
const DEFAULT_CHROME_PROFILE = "Default";
|
|
@@ -15,6 +16,7 @@ const DEFAULT_CHROME_PROFILE = "Default";
|
|
|
15
16
|
const BROWSER_MODEL_LABELS = [
|
|
16
17
|
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
17
18
|
["gpt-5.5-pro", "Pro"],
|
|
19
|
+
["gpt-5.5-instant", "GPT-5.5 Instant"],
|
|
18
20
|
["gpt-5.5", "Thinking 5.5"],
|
|
19
21
|
["gpt-5.4-pro", "Pro"],
|
|
20
22
|
["gpt-5.2-thinking", "GPT-5.2 Thinking"],
|
|
@@ -26,7 +28,10 @@ const BROWSER_MODEL_LABELS = [
|
|
|
26
28
|
["gpt-5.4", "Thinking 5.4"],
|
|
27
29
|
["gpt-5.2", "GPT-5.2"], // Selects "Auto" in ChatGPT UI
|
|
28
30
|
["gpt-5.1", "GPT-5.2"], // Legacy alias → Auto
|
|
29
|
-
["gemini-3-
|
|
31
|
+
["gemini-3.1-flash-lite", "Gemini 3.1 Flash-Lite"],
|
|
32
|
+
["gemini-3.5-flash", "Gemini 3.5 Flash"],
|
|
33
|
+
["gemini-3.1-pro", "Gemini 3.1 Pro"],
|
|
34
|
+
["gemini-3-pro", "Gemini 3.1 Pro"],
|
|
30
35
|
["gemini-3-pro-deep-think", "gemini-3-deep-think"],
|
|
31
36
|
];
|
|
32
37
|
export function normalizeChatGptModelForBrowser(model) {
|
|
@@ -34,7 +39,10 @@ export function normalizeChatGptModelForBrowser(model) {
|
|
|
34
39
|
if (!normalized.startsWith("gpt-") || normalized.includes("codex")) {
|
|
35
40
|
return model;
|
|
36
41
|
}
|
|
37
|
-
if (normalized === "gpt-5.5-pro" ||
|
|
42
|
+
if (normalized === "gpt-5.5-pro" ||
|
|
43
|
+
normalized === "gpt-5.5-instant" ||
|
|
44
|
+
normalized === "gpt-5.5" ||
|
|
45
|
+
normalized === "gpt-5.4") {
|
|
38
46
|
return normalized;
|
|
39
47
|
}
|
|
40
48
|
// Pro variants: resolve to the latest Pro model in ChatGPT.
|
|
@@ -101,6 +109,9 @@ export async function buildBrowserConfig(options) {
|
|
|
101
109
|
inputTimeoutMs: options.browserInputTimeout
|
|
102
110
|
? parseDuration(options.browserInputTimeout, DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
|
|
103
111
|
: undefined,
|
|
112
|
+
attachmentTimeoutMs: options.browserAttachmentTimeout
|
|
113
|
+
? parseDuration(options.browserAttachmentTimeout, DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS)
|
|
114
|
+
: undefined,
|
|
104
115
|
assistantRecheckDelayMs: options.browserRecheckDelay
|
|
105
116
|
? parseDuration(options.browserRecheckDelay, 0)
|
|
106
117
|
: undefined,
|
|
@@ -43,6 +43,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
43
43
|
if (isUnset("browserInputTimeout") && typeof browser.inputTimeoutMs === "number") {
|
|
44
44
|
options.browserInputTimeout = String(browser.inputTimeoutMs);
|
|
45
45
|
}
|
|
46
|
+
if (isUnset("browserAttachmentTimeout") && typeof browser.attachmentTimeoutMs === "number") {
|
|
47
|
+
options.browserAttachmentTimeout = String(browser.attachmentTimeoutMs);
|
|
48
|
+
}
|
|
46
49
|
if (isUnset("browserRecheckDelay") && typeof browser.assistantRecheckDelayMs === "number") {
|
|
47
50
|
options.browserRecheckDelay = String(browser.assistantRecheckDelayMs);
|
|
48
51
|
}
|
|
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { sessionStore } from "../sessionStore.js";
|
|
5
5
|
import { collectChatGptTabs, DEFAULT_REMOTE_CHROME_HOST, DEFAULT_REMOTE_CHROME_PORT, extractConversationIdFromUrl, formatBrowserTabState, harvestChatGptTab, sessionMatchesTab, } from "../browser/liveTabs.js";
|
|
6
|
+
import { recoverConversationTab } from "../browser/recoverConversation.js";
|
|
6
7
|
import { resolveOutputPath } from "./writeOutputPath.js";
|
|
7
8
|
const LIVE_POLL_MS = 2000;
|
|
8
9
|
const DEFAULT_STALL_THRESHOLD_MS = 60_000;
|
|
@@ -149,80 +150,154 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
|
|
|
149
150
|
if (!meta) {
|
|
150
151
|
throw new Error(`No session found with ID ${sessionId}.`);
|
|
151
152
|
}
|
|
152
|
-
const
|
|
153
|
+
const initialEndpoint = sessionBrowserEndpoint(meta) ?? {
|
|
153
154
|
host: DEFAULT_REMOTE_CHROME_HOST,
|
|
154
155
|
port: DEFAULT_REMOTE_CHROME_PORT,
|
|
155
156
|
};
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
157
|
+
const ref = options.browserTabRef ?? resolveSessionTabRef(meta);
|
|
158
|
+
const recoverIfMissing = options.recoverIfMissing !== false;
|
|
159
|
+
let recoveredChrome = null;
|
|
160
|
+
try {
|
|
161
|
+
let harvested;
|
|
162
|
+
try {
|
|
163
|
+
harvested = await harvestChatGptTab({
|
|
164
|
+
host: initialEndpoint.host,
|
|
165
|
+
port: initialEndpoint.port,
|
|
166
|
+
ref,
|
|
167
|
+
stallWindowMs: options.stallWindowMs,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
172
|
+
const isMissingTabError = message.includes("No ChatGPT tab matched") ||
|
|
173
|
+
message.includes("ECONNREFUSED") ||
|
|
174
|
+
message.includes("Could not connect");
|
|
175
|
+
if (!isMissingTabError || !recoverIfMissing) {
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
console.log(chalk.yellow(`No live ChatGPT tab matched session "${sessionId}". Attempting recovery by reopening the saved conversation URL.`));
|
|
179
|
+
const recovered = await recoverConversationTab(meta, (line) => console.log(line));
|
|
180
|
+
recoveredChrome = recovered.chrome;
|
|
181
|
+
harvested = await harvestChatGptTab({
|
|
182
|
+
host: recovered.host,
|
|
183
|
+
port: recovered.port,
|
|
184
|
+
ref: recovered.url,
|
|
185
|
+
stallWindowMs: options.stallWindowMs,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
await persistHarvest(sessionId, meta, harvested);
|
|
189
|
+
printHarvestSummary(sessionId, harvested);
|
|
190
|
+
const output = harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "";
|
|
191
|
+
if (options.writeOutputPath) {
|
|
192
|
+
await maybeWriteHarvestOutput(options.writeOutputPath, meta.cwd ?? process.cwd(), output);
|
|
193
|
+
}
|
|
194
|
+
if (!options.quietOutput && output) {
|
|
195
|
+
process.stdout.write(`${output}${output.endsWith("\n") ? "" : "\n"}`);
|
|
196
|
+
}
|
|
197
|
+
return harvested;
|
|
167
198
|
}
|
|
168
|
-
|
|
169
|
-
|
|
199
|
+
finally {
|
|
200
|
+
if (recoveredChrome && options.closeAfterRecover) {
|
|
201
|
+
try {
|
|
202
|
+
recoveredChrome.kill();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// best-effort cleanup
|
|
206
|
+
}
|
|
207
|
+
}
|
|
170
208
|
}
|
|
171
|
-
return harvested;
|
|
172
209
|
}
|
|
173
210
|
export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
|
|
174
211
|
const meta = await sessionStore.readSession(sessionId);
|
|
175
212
|
if (!meta) {
|
|
176
213
|
throw new Error(`No session found with ID ${sessionId}.`);
|
|
177
214
|
}
|
|
178
|
-
|
|
215
|
+
let endpoint = sessionBrowserEndpoint(meta) ?? {
|
|
179
216
|
host: DEFAULT_REMOTE_CHROME_HOST,
|
|
180
217
|
port: DEFAULT_REMOTE_CHROME_PORT,
|
|
181
218
|
};
|
|
182
|
-
|
|
219
|
+
let browserTabRef = options.browserTabRef ?? resolveSessionTabRef(meta);
|
|
220
|
+
const recoverIfMissing = options.recoverIfMissing !== false;
|
|
221
|
+
let recoveredChrome = null;
|
|
183
222
|
const stallThresholdMs = options.stallThresholdMs ?? DEFAULT_STALL_THRESHOLD_MS;
|
|
184
223
|
let lastHash = null;
|
|
185
224
|
let unchangedSince = Date.now();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
225
|
+
try {
|
|
226
|
+
// Probe once to see if the live tab is still alive; recover if not.
|
|
227
|
+
try {
|
|
228
|
+
await harvestChatGptTab({
|
|
229
|
+
host: endpoint.host,
|
|
230
|
+
port: endpoint.port,
|
|
231
|
+
ref: browserTabRef,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
236
|
+
const isMissingTabError = message.includes("No ChatGPT tab matched") ||
|
|
237
|
+
message.includes("ECONNREFUSED") ||
|
|
238
|
+
message.includes("Could not connect");
|
|
239
|
+
if (!isMissingTabError || !recoverIfMissing) {
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
console.log(chalk.yellow(`No live ChatGPT tab matched session "${sessionId}". Attempting recovery by reopening the saved conversation URL.`));
|
|
243
|
+
const recovered = await recoverConversationTab(meta, (line) => console.log(line));
|
|
244
|
+
recoveredChrome = recovered.chrome;
|
|
245
|
+
endpoint = { host: recovered.host, port: recovered.port };
|
|
246
|
+
browserTabRef = recovered.url;
|
|
202
247
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
248
|
+
while (true) {
|
|
249
|
+
const harvested = await harvestChatGptTab({
|
|
250
|
+
host: endpoint.host,
|
|
251
|
+
port: endpoint.port,
|
|
252
|
+
ref: browserTabRef,
|
|
253
|
+
});
|
|
254
|
+
const fullText = harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "";
|
|
255
|
+
const hash = createHash("sha1").update(fullText).digest("hex");
|
|
256
|
+
if (hash !== lastHash) {
|
|
257
|
+
lastHash = hash;
|
|
258
|
+
unchangedSince = Date.now();
|
|
259
|
+
const statusLine = `[${new Date().toISOString()}] state=${harvested.state} stop=${harvested.stopExists ? "yes" : "no"} ` +
|
|
260
|
+
`send=${harvested.sendExists ? "yes" : "no"} model=${harvested.currentModelLabel || "(unknown)"} ` +
|
|
261
|
+
`snippet=${snippet(harvested.lastAssistantSnippet || fullText, 160)}`;
|
|
262
|
+
console.log(statusLine);
|
|
263
|
+
await persistHarvest(sessionId, meta, harvested);
|
|
264
|
+
}
|
|
265
|
+
const derivedState = harvested.stopExists
|
|
266
|
+
? Date.now() - unchangedSince >= stallThresholdMs
|
|
267
|
+
? "stalled"
|
|
268
|
+
: "running"
|
|
269
|
+
: harvested.authenticated
|
|
270
|
+
? "completed"
|
|
271
|
+
: "detached";
|
|
272
|
+
if (derivedState === "completed" ||
|
|
273
|
+
derivedState === "stalled" ||
|
|
274
|
+
derivedState === "detached") {
|
|
275
|
+
const finalHarvest = {
|
|
276
|
+
...harvested,
|
|
277
|
+
state: derivedState,
|
|
278
|
+
};
|
|
279
|
+
await persistHarvest(sessionId, meta, finalHarvest);
|
|
280
|
+
printHarvestSummary(sessionId, finalHarvest);
|
|
281
|
+
const output = finalHarvest.lastAssistantMarkdown ?? finalHarvest.lastAssistantText ?? "";
|
|
282
|
+
if (options.writeOutputPath) {
|
|
283
|
+
await maybeWriteHarvestOutput(options.writeOutputPath, meta.cwd ?? process.cwd(), output);
|
|
284
|
+
}
|
|
285
|
+
if (output) {
|
|
286
|
+
process.stdout.write(`${output}${output.endsWith("\n") ? "" : "\n"}`);
|
|
287
|
+
}
|
|
288
|
+
return finalHarvest;
|
|
289
|
+
}
|
|
290
|
+
await new Promise((resolve) => setTimeout(resolve, LIVE_POLL_MS));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
if (recoveredChrome && options.closeAfterRecover) {
|
|
295
|
+
try {
|
|
296
|
+
recoveredChrome.kill();
|
|
220
297
|
}
|
|
221
|
-
|
|
222
|
-
|
|
298
|
+
catch {
|
|
299
|
+
// best-effort cleanup
|
|
223
300
|
}
|
|
224
|
-
return finalHarvest;
|
|
225
301
|
}
|
|
226
|
-
await new Promise((resolve) => setTimeout(resolve, LIVE_POLL_MS));
|
|
227
302
|
}
|
|
228
303
|
}
|
package/dist/src/cli/engine.js
CHANGED
|
@@ -14,9 +14,10 @@ export function defaultWaitPreference(model, engine) {
|
|
|
14
14
|
* 2) Explicit --engine value.
|
|
15
15
|
* 3) Explicit API provider routing flags force API.
|
|
16
16
|
* 4) ORACLE_ENGINE environment override (api|browser).
|
|
17
|
-
* 5)
|
|
17
|
+
* 5) Config engine value.
|
|
18
|
+
* 6) API environment decides: api when set, otherwise browser.
|
|
18
19
|
*/
|
|
19
|
-
export function resolveEngine({ engine, browserFlag, apiProviderRequested, env, }) {
|
|
20
|
+
export function resolveEngine({ engine, configEngine, browserFlag, apiProviderRequested, env, }) {
|
|
20
21
|
if (browserFlag) {
|
|
21
22
|
return "browser";
|
|
22
23
|
}
|
|
@@ -30,6 +31,9 @@ export function resolveEngine({ engine, browserFlag, apiProviderRequested, env,
|
|
|
30
31
|
if (envEngine) {
|
|
31
32
|
return envEngine;
|
|
32
33
|
}
|
|
34
|
+
if (configEngine) {
|
|
35
|
+
return configEngine;
|
|
36
|
+
}
|
|
33
37
|
return hasApiEnvironment(env) ? "api" : "browser";
|
|
34
38
|
}
|
|
35
39
|
function hasApiEnvironment(env) {
|