@steipete/oracle 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +56 -11
- package/dist/bin/oracle-cli.js +104 -16
- package/dist/src/browser/actions/archiveConversation.js +236 -0
- package/dist/src/browser/actions/assistantResponse.js +26 -0
- package/dist/src/browser/actions/deepResearch.js +662 -0
- package/dist/src/browser/actions/modelSelection.js +86 -16
- package/dist/src/browser/actions/navigation.js +22 -0
- package/dist/src/browser/actions/projectSources.js +491 -0
- package/dist/src/browser/actions/promptComposer.js +52 -27
- package/dist/src/browser/actions/thinkingStatus.js +391 -0
- package/dist/src/browser/artifacts.js +150 -0
- package/dist/src/browser/attachRunning.js +31 -0
- package/dist/src/browser/chatgptImages.js +315 -0
- package/dist/src/browser/chromeLifecycle.js +214 -3
- package/dist/src/browser/config.js +27 -9
- package/dist/src/browser/constants.js +8 -0
- package/dist/src/browser/controlPlan.js +81 -0
- package/dist/src/browser/detect.js +206 -33
- package/dist/src/browser/domDebug.js +49 -0
- package/dist/src/browser/index.js +1234 -479
- package/dist/src/browser/liveTabs.js +434 -0
- package/dist/src/browser/profileState.js +83 -3
- package/dist/src/browser/projectSourcesRunner.js +366 -0
- package/dist/src/browser/reattach.js +117 -45
- package/dist/src/browser/reattachHelpers.js +1 -1
- package/dist/src/browser/sessionRunner.js +53 -1
- package/dist/src/browser/tabLeaseRegistry.js +182 -0
- package/dist/src/cli/bridge/claudeConfig.js +12 -8
- package/dist/src/cli/bridge/codexConfig.js +2 -2
- package/dist/src/cli/browserConfig.js +41 -8
- package/dist/src/cli/browserDefaults.js +31 -7
- package/dist/src/cli/browserTabs.js +228 -0
- package/dist/src/cli/dryRun.js +33 -1
- package/dist/src/cli/duplicatePromptGuard.js +10 -2
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +4 -0
- package/dist/src/cli/projectSources.js +116 -0
- package/dist/src/cli/sessionCommand.js +51 -0
- package/dist/src/cli/sessionDisplay.js +121 -9
- package/dist/src/cli/sessionRunner.js +51 -7
- package/dist/src/mcp/consultPresets.js +19 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/consult.js +201 -26
- package/dist/src/mcp/tools/projectSources.js +123 -0
- package/dist/src/mcp/types.js +11 -2
- package/dist/src/mcp/utils.js +6 -1
- package/dist/src/oracle/run.js +4 -1
- package/dist/src/projectSources/plan.js +27 -0
- package/dist/src/projectSources/types.js +1 -0
- package/dist/src/projectSources/url.js +23 -0
- package/dist/src/sessionManager.js +1 -0
- package/package.json +7 -6
|
@@ -4,6 +4,7 @@ import { formatFinishLine } from "../oracle/finishLine.js";
|
|
|
4
4
|
import { runBrowserMode } from "../browserMode.js";
|
|
5
5
|
import { assembleBrowserPrompt } from "./prompt.js";
|
|
6
6
|
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
7
|
+
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
7
8
|
export async function runBrowserSessionExecution({ runOptions, browserConfig, cwd, log }, deps = {}) {
|
|
8
9
|
const assemblePrompt = deps.assemblePrompt ?? assembleBrowserPrompt;
|
|
9
10
|
const executeBrowser = deps.executeBrowser ?? runBrowserMode;
|
|
@@ -35,7 +36,8 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
35
36
|
const automationLogger = ((message) => {
|
|
36
37
|
if (typeof message !== "string")
|
|
37
38
|
return;
|
|
38
|
-
const shouldAlwaysPrint = message.startsWith("[browser] ") &&
|
|
39
|
+
const shouldAlwaysPrint = message.startsWith("[browser] ") &&
|
|
40
|
+
/archive|fallback|follow-up|retry|thinking|waiting for chatgpt|browser slot|browser control|browser guidance/i.test(message);
|
|
39
41
|
if (!runOptions.verbose && !shouldAlwaysPrint)
|
|
40
42
|
return;
|
|
41
43
|
log(message);
|
|
@@ -63,6 +65,10 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
63
65
|
log: automationLogger,
|
|
64
66
|
heartbeatIntervalMs: runOptions.heartbeatIntervalMs,
|
|
65
67
|
verbose: runOptions.verbose,
|
|
68
|
+
sessionId: runOptions.sessionId,
|
|
69
|
+
generateImagePath: runOptions.generateImage,
|
|
70
|
+
outputPath: runOptions.outputPath,
|
|
71
|
+
followUpPrompts: runOptions.browserFollowUps,
|
|
66
72
|
runtimeHintCb: async (runtime) => {
|
|
67
73
|
await persistRuntimeHint({
|
|
68
74
|
...runtime,
|
|
@@ -84,6 +90,15 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
84
90
|
log("");
|
|
85
91
|
}
|
|
86
92
|
const answerText = browserResult.answerMarkdown || browserResult.answerText || "";
|
|
93
|
+
const savedArtifacts = await ensureSessionArtifacts({
|
|
94
|
+
sessionId: runOptions.sessionId,
|
|
95
|
+
prompt: promptArtifacts.composerText,
|
|
96
|
+
answerMarkdown: answerText,
|
|
97
|
+
conversationUrl: browserResult.tabUrl,
|
|
98
|
+
browserConfig,
|
|
99
|
+
existingArtifacts: browserResult.artifacts,
|
|
100
|
+
logger: automationLogger,
|
|
101
|
+
});
|
|
87
102
|
const usage = {
|
|
88
103
|
inputTokens: promptArtifacts.estimatedInputTokens,
|
|
89
104
|
outputTokens: browserResult.answerTokens,
|
|
@@ -120,12 +135,49 @@ export async function runBrowserSessionExecution({ runOptions, browserConfig, cw
|
|
|
120
135
|
usage,
|
|
121
136
|
elapsedMs: browserResult.tookMs,
|
|
122
137
|
runtime: {
|
|
138
|
+
browserTransport: browserResult.browserTransport,
|
|
123
139
|
chromePid: browserResult.chromePid,
|
|
124
140
|
chromePort: browserResult.chromePort,
|
|
125
141
|
chromeHost: browserResult.chromeHost,
|
|
142
|
+
chromeBrowserWSEndpoint: browserResult.chromeBrowserWSEndpoint,
|
|
143
|
+
chromeProfileRoot: browserResult.chromeProfileRoot,
|
|
126
144
|
userDataDir: browserResult.userDataDir,
|
|
145
|
+
chromeTargetId: browserResult.chromeTargetId,
|
|
146
|
+
tabUrl: browserResult.tabUrl,
|
|
147
|
+
conversationId: browserResult.conversationId,
|
|
127
148
|
controllerPid: browserResult.controllerPid ?? process.pid,
|
|
128
149
|
},
|
|
150
|
+
archive: browserResult.archive,
|
|
129
151
|
answerText,
|
|
152
|
+
artifacts: savedArtifacts,
|
|
130
153
|
};
|
|
131
154
|
}
|
|
155
|
+
export async function ensureSessionArtifacts(params) {
|
|
156
|
+
if (!params.sessionId || !params.answerMarkdown.trim()) {
|
|
157
|
+
return params.existingArtifacts;
|
|
158
|
+
}
|
|
159
|
+
let artifacts = params.existingArtifacts;
|
|
160
|
+
const hasReport = artifacts?.some((artifact) => artifact.kind === "deep-research-report");
|
|
161
|
+
if (params.browserConfig.researchMode === "deep" && !hasReport) {
|
|
162
|
+
const report = await saveDeepResearchReportArtifact({
|
|
163
|
+
sessionId: params.sessionId,
|
|
164
|
+
reportMarkdown: params.answerMarkdown,
|
|
165
|
+
conversationUrl: params.conversationUrl,
|
|
166
|
+
logger: params.logger,
|
|
167
|
+
}).catch(() => null);
|
|
168
|
+
artifacts = appendArtifacts(artifacts, [report]);
|
|
169
|
+
}
|
|
170
|
+
const hasTranscript = artifacts?.some((artifact) => artifact.kind === "transcript");
|
|
171
|
+
if (!hasTranscript) {
|
|
172
|
+
const transcript = await saveBrowserTranscriptArtifact({
|
|
173
|
+
sessionId: params.sessionId,
|
|
174
|
+
prompt: params.prompt,
|
|
175
|
+
answerMarkdown: params.answerMarkdown,
|
|
176
|
+
conversationUrl: params.conversationUrl,
|
|
177
|
+
artifacts,
|
|
178
|
+
logger: params.logger,
|
|
179
|
+
}).catch(() => null);
|
|
180
|
+
artifacts = appendArtifacts(artifacts, [transcript]);
|
|
181
|
+
}
|
|
182
|
+
return artifacts;
|
|
183
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { isProcessAlive } from "./profileState.js";
|
|
5
|
+
import { delay } from "./utils.js";
|
|
6
|
+
export const DEFAULT_MAX_CONCURRENT_CHATGPT_TABS = 3;
|
|
7
|
+
const REGISTRY_FILENAME = "oracle-tab-leases.json";
|
|
8
|
+
const REGISTRY_LOCK_DIRNAME = "oracle-tab-leases.lock";
|
|
9
|
+
const DEFAULT_POLL_MS = 1000;
|
|
10
|
+
const DEFAULT_STALE_MS = 6 * 60 * 60 * 1000;
|
|
11
|
+
const REGISTRY_LOCK_TIMEOUT_MS = 10_000;
|
|
12
|
+
export function normalizeMaxConcurrentTabs(value) {
|
|
13
|
+
if (value === undefined || value === null) {
|
|
14
|
+
return DEFAULT_MAX_CONCURRENT_CHATGPT_TABS;
|
|
15
|
+
}
|
|
16
|
+
const numeric = typeof value === "string" ? Number.parseInt(value, 10) : Number(value);
|
|
17
|
+
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
18
|
+
return DEFAULT_MAX_CONCURRENT_CHATGPT_TABS;
|
|
19
|
+
}
|
|
20
|
+
return Math.max(1, Math.trunc(numeric));
|
|
21
|
+
}
|
|
22
|
+
export async function acquireBrowserTabLease(profileDir, options, deps = {}) {
|
|
23
|
+
const maxConcurrentTabs = normalizeMaxConcurrentTabs(options.maxConcurrentTabs);
|
|
24
|
+
const pollMs = Math.max(50, options.pollMs ?? DEFAULT_POLL_MS);
|
|
25
|
+
const timeoutMs = Math.max(0, options.timeoutMs ?? 0);
|
|
26
|
+
const staleMs = Math.max(60_000, options.staleMs ?? DEFAULT_STALE_MS);
|
|
27
|
+
const now = deps.now ?? Date.now;
|
|
28
|
+
const pid = deps.pid ?? process.pid;
|
|
29
|
+
const leaseId = randomUUID();
|
|
30
|
+
const startedAt = now();
|
|
31
|
+
let warned = false;
|
|
32
|
+
let lastHeartbeatAt = 0;
|
|
33
|
+
for (;;) {
|
|
34
|
+
const acquired = await withRegistryLock(profileDir, async () => {
|
|
35
|
+
const registry = await readRegistry(profileDir);
|
|
36
|
+
const active = pruneStaleLeases(registry.leases, {
|
|
37
|
+
nowMs: now(),
|
|
38
|
+
staleMs,
|
|
39
|
+
isProcessAlive: deps.isProcessAlive ?? isProcessAlive,
|
|
40
|
+
});
|
|
41
|
+
if (active.length >= maxConcurrentTabs) {
|
|
42
|
+
if (active.length !== registry.leases.length) {
|
|
43
|
+
await writeRegistry(profileDir, { version: 1, leases: active });
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const timestamp = new Date(now()).toISOString();
|
|
48
|
+
const lease = {
|
|
49
|
+
id: leaseId,
|
|
50
|
+
pid,
|
|
51
|
+
sessionId: options.sessionId,
|
|
52
|
+
chromeHost: options.chromeHost,
|
|
53
|
+
chromePort: options.chromePort,
|
|
54
|
+
createdAt: timestamp,
|
|
55
|
+
updatedAt: timestamp,
|
|
56
|
+
};
|
|
57
|
+
await writeRegistry(profileDir, { version: 1, leases: [...active, lease] });
|
|
58
|
+
return lease;
|
|
59
|
+
});
|
|
60
|
+
if (acquired) {
|
|
61
|
+
options.logger?.(`[browser] Acquired ChatGPT browser slot ${leaseId.slice(0, 8)} (${maxConcurrentTabs} max).`);
|
|
62
|
+
return {
|
|
63
|
+
id: leaseId,
|
|
64
|
+
release: async () => releaseBrowserTabLease(profileDir, leaseId, options.logger),
|
|
65
|
+
update: async (patch) => updateBrowserTabLease(profileDir, leaseId, patch),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const elapsed = now() - startedAt;
|
|
69
|
+
if (!warned || now() - lastHeartbeatAt >= 30_000) {
|
|
70
|
+
options.logger?.(`[browser] Waiting for ChatGPT browser slot (${maxConcurrentTabs} max, ${Math.round(elapsed / 1000)}s elapsed).`);
|
|
71
|
+
warned = true;
|
|
72
|
+
lastHeartbeatAt = now();
|
|
73
|
+
}
|
|
74
|
+
if (timeoutMs > 0 && elapsed >= timeoutMs) {
|
|
75
|
+
throw new Error(`Timed out waiting for ChatGPT browser slot after ${Math.round(elapsed / 1000)}s (${maxConcurrentTabs} max).`);
|
|
76
|
+
}
|
|
77
|
+
await delay(timeoutMs > 0 ? Math.min(pollMs, timeoutMs - elapsed) : pollMs);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export async function updateBrowserTabLease(profileDir, leaseId, patch) {
|
|
81
|
+
await withRegistryLock(profileDir, async () => {
|
|
82
|
+
const registry = await readRegistry(profileDir);
|
|
83
|
+
const leases = registry.leases.map((lease) => lease.id === leaseId
|
|
84
|
+
? { ...lease, ...patch, id: lease.id, updatedAt: new Date().toISOString() }
|
|
85
|
+
: lease);
|
|
86
|
+
await writeRegistry(profileDir, { version: 1, leases });
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export async function releaseBrowserTabLease(profileDir, leaseId, logger) {
|
|
90
|
+
await withRegistryLock(profileDir, async () => {
|
|
91
|
+
const registry = await readRegistry(profileDir);
|
|
92
|
+
const leases = registry.leases.filter((lease) => lease.id !== leaseId);
|
|
93
|
+
await writeRegistry(profileDir, { version: 1, leases });
|
|
94
|
+
}).catch(() => undefined);
|
|
95
|
+
logger?.(`[browser] Released ChatGPT browser slot ${leaseId.slice(0, 8)}.`);
|
|
96
|
+
}
|
|
97
|
+
export async function hasOtherActiveBrowserTabLeases(profileDir, leaseId, options = {}) {
|
|
98
|
+
const now = options.now ?? Date.now;
|
|
99
|
+
const staleMs = Math.max(60_000, options.staleMs ?? DEFAULT_STALE_MS);
|
|
100
|
+
return withRegistryLock(profileDir, async () => {
|
|
101
|
+
const registry = await readRegistry(profileDir);
|
|
102
|
+
const active = pruneStaleLeases(registry.leases, {
|
|
103
|
+
nowMs: now(),
|
|
104
|
+
staleMs,
|
|
105
|
+
isProcessAlive: options.isProcessAlive ?? isProcessAlive,
|
|
106
|
+
});
|
|
107
|
+
if (active.length !== registry.leases.length) {
|
|
108
|
+
await writeRegistry(profileDir, { version: 1, leases: active });
|
|
109
|
+
}
|
|
110
|
+
return active.some((lease) => lease.id !== leaseId);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
async function withRegistryLock(profileDir, callback) {
|
|
114
|
+
const lockDir = path.join(profileDir, REGISTRY_LOCK_DIRNAME);
|
|
115
|
+
const startedAt = Date.now();
|
|
116
|
+
for (;;) {
|
|
117
|
+
try {
|
|
118
|
+
await mkdir(lockDir, { recursive: false });
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code !== "EEXIST") {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
if (Date.now() - startedAt > REGISTRY_LOCK_TIMEOUT_MS) {
|
|
126
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => undefined);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
await delay(50);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return await callback();
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await rm(lockDir, { recursive: true, force: true }).catch(() => undefined);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function readRegistry(profileDir) {
|
|
140
|
+
try {
|
|
141
|
+
const raw = await readFile(registryPath(profileDir), "utf8");
|
|
142
|
+
const parsed = JSON.parse(raw);
|
|
143
|
+
if (!Array.isArray(parsed.leases)) {
|
|
144
|
+
return { version: 1, leases: [] };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
version: 1,
|
|
148
|
+
leases: parsed.leases.filter(isLeaseRecord),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return { version: 1, leases: [] };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function writeRegistry(profileDir, registry) {
|
|
156
|
+
await mkdir(profileDir, { recursive: true });
|
|
157
|
+
await writeFile(registryPath(profileDir), `${JSON.stringify(registry, null, 2)}\n`, "utf8");
|
|
158
|
+
}
|
|
159
|
+
function registryPath(profileDir) {
|
|
160
|
+
return path.join(profileDir, REGISTRY_FILENAME);
|
|
161
|
+
}
|
|
162
|
+
function pruneStaleLeases(leases, options) {
|
|
163
|
+
return leases.filter((lease) => {
|
|
164
|
+
if (!options.isProcessAlive(lease.pid)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const updatedAt = Date.parse(lease.updatedAt);
|
|
168
|
+
if (Number.isFinite(updatedAt) && options.nowMs - updatedAt > options.staleMs) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function isLeaseRecord(value) {
|
|
175
|
+
if (!value || typeof value !== "object")
|
|
176
|
+
return false;
|
|
177
|
+
const record = value;
|
|
178
|
+
return (typeof record.id === "string" &&
|
|
179
|
+
typeof record.pid === "number" &&
|
|
180
|
+
typeof record.createdAt === "string" &&
|
|
181
|
+
typeof record.updatedAt === "string");
|
|
182
|
+
}
|
|
@@ -12,20 +12,24 @@ export async function runBridgeClaudeConfig(options) {
|
|
|
12
12
|
env: process.env,
|
|
13
13
|
});
|
|
14
14
|
const snippet = formatClaudeMcpConfig({
|
|
15
|
-
oracleHomeDir:
|
|
16
|
-
|
|
17
|
-
path.join(os.homedir(), ".oracle
|
|
15
|
+
oracleHomeDir: options.oracleHomeDir ??
|
|
16
|
+
process.env.ORACLE_HOME_DIR ??
|
|
17
|
+
path.join(os.homedir(), options.localBrowser ? ".oracle" : ".oracle-local"),
|
|
18
|
+
browserProfileDir: options.browserProfileDir ??
|
|
19
|
+
process.env.ORACLE_BROWSER_PROFILE_DIR ??
|
|
20
|
+
path.join(os.homedir(), options.localBrowser ? ".oracle" : ".oracle-local", "browser-profile"),
|
|
18
21
|
remoteHost: resolved.host,
|
|
19
22
|
remoteToken: resolved.token,
|
|
20
23
|
includeToken: Boolean(options.printToken),
|
|
24
|
+
localBrowser: Boolean(options.localBrowser),
|
|
21
25
|
});
|
|
22
26
|
console.log(snippet);
|
|
23
|
-
if (!options.printToken) {
|
|
24
|
-
console.
|
|
25
|
-
console.
|
|
27
|
+
if (!options.printToken && !options.localBrowser) {
|
|
28
|
+
console.error("");
|
|
29
|
+
console.error(chalk.dim("Tip: rerun with --print-token to include ORACLE_REMOTE_TOKEN in the snippet."));
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
|
-
export function formatClaudeMcpConfig({ oracleHomeDir, browserProfileDir, remoteHost, remoteToken, includeToken, }) {
|
|
32
|
+
export function formatClaudeMcpConfig({ oracleHomeDir, browserProfileDir, remoteHost, remoteToken, includeToken, localBrowser = false, }) {
|
|
29
33
|
const env = {};
|
|
30
34
|
// biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
|
|
31
35
|
env["ORACLE_ENGINE"] = "browser";
|
|
@@ -33,7 +37,7 @@ export function formatClaudeMcpConfig({ oracleHomeDir, browserProfileDir, remote
|
|
|
33
37
|
env["ORACLE_HOME_DIR"] = oracleHomeDir;
|
|
34
38
|
// biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
|
|
35
39
|
env["ORACLE_BROWSER_PROFILE_DIR"] = browserProfileDir;
|
|
36
|
-
if (remoteHost) {
|
|
40
|
+
if (remoteHost && !localBrowser) {
|
|
37
41
|
// biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
|
|
38
42
|
env["ORACLE_REMOTE_HOST"] = remoteHost;
|
|
39
43
|
// biome-ignore lint/complexity/useLiteralKeys: env vars are uppercase and include underscores.
|
|
@@ -16,8 +16,8 @@ export async function runBridgeCodexConfig(options) {
|
|
|
16
16
|
});
|
|
17
17
|
console.log(snippet);
|
|
18
18
|
if (!options.printToken) {
|
|
19
|
-
console.
|
|
20
|
-
console.
|
|
19
|
+
console.error("");
|
|
20
|
+
console.error(chalk.dim("Tip: rerun with --print-token to include ORACLE_REMOTE_TOKEN in the snippet."));
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
export function formatCodexMcpSnippet({ remoteHost, remoteToken, includeToken, }) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET,
|
|
3
|
+
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET, normalizeChatgptUrl, parseDuration, } from "../browserMode.js";
|
|
4
4
|
import { normalizeBrowserModelStrategy } from "../browser/modelStrategy.js";
|
|
5
5
|
import { getOracleHomeDir } from "../oracleHome.js";
|
|
6
6
|
const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
|
|
@@ -74,6 +74,11 @@ export async function buildBrowserConfig(options) {
|
|
|
74
74
|
if (options.remoteChrome) {
|
|
75
75
|
remoteChrome = parseRemoteChromeTarget(options.remoteChrome);
|
|
76
76
|
}
|
|
77
|
+
const attachRunning = options.browserAttachRunning === true;
|
|
78
|
+
validateAttachRunningOptions(options, {
|
|
79
|
+
attachRunning,
|
|
80
|
+
hasInlineCookies: Boolean(inline?.cookies),
|
|
81
|
+
});
|
|
77
82
|
const rawUrl = options.chatgptUrl ?? options.browserUrl;
|
|
78
83
|
const url = rawUrl ? normalizeChatgptUrl(rawUrl, CHATGPT_URL) : undefined;
|
|
79
84
|
const desiredModel = isChatGptModel
|
|
@@ -81,17 +86,11 @@ export async function buildBrowserConfig(options) {
|
|
|
81
86
|
: shouldUseOverride
|
|
82
87
|
? desiredModelOverride
|
|
83
88
|
: mapModelToBrowserLabel(options.model);
|
|
84
|
-
if (modelStrategy === "select" &&
|
|
85
|
-
url &&
|
|
86
|
-
isTemporaryChatUrl(url) &&
|
|
87
|
-
/\bpro\b/i.test(desiredModel ?? "")) {
|
|
88
|
-
throw new Error("Temporary Chat mode does not expose Pro models in the ChatGPT model picker. " +
|
|
89
|
-
'Remove "temporary-chat=true" from --chatgpt-url (or omit --chatgpt-url), or use a non-Pro model (e.g. --model gpt-5.2).');
|
|
90
|
-
}
|
|
91
89
|
return {
|
|
92
90
|
chromeProfile: options.browserChromeProfile ?? DEFAULT_CHROME_PROFILE,
|
|
93
91
|
chromePath: options.browserChromePath ?? null,
|
|
94
92
|
chromeCookiePath: options.browserCookiePath ?? null,
|
|
93
|
+
attachRunning,
|
|
95
94
|
url,
|
|
96
95
|
debugPort: selectBrowserPort(options),
|
|
97
96
|
timeoutMs: options.browserTimeout
|
|
@@ -112,6 +111,7 @@ export async function buildBrowserConfig(options) {
|
|
|
112
111
|
profileLockTimeoutMs: options.browserProfileLockTimeout
|
|
113
112
|
? parseDuration(options.browserProfileLockTimeout, 0)
|
|
114
113
|
: undefined,
|
|
114
|
+
maxConcurrentTabs: parseMaxConcurrentTabs(options.browserMaxConcurrentTabs),
|
|
115
115
|
autoReattachDelayMs: options.browserAutoReattachDelay
|
|
116
116
|
? parseDuration(options.browserAutoReattachDelay, 0)
|
|
117
117
|
: undefined,
|
|
@@ -139,9 +139,33 @@ export async function buildBrowserConfig(options) {
|
|
|
139
139
|
// Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
|
|
140
140
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
141
141
|
remoteChrome,
|
|
142
|
+
browserTabRef: options.browserTab ?? undefined,
|
|
142
143
|
thinkingTime: options.browserThinkingTime,
|
|
144
|
+
researchMode: options.browserResearch === "deep" ? "deep" : "off",
|
|
145
|
+
archiveConversations: options.browserArchive,
|
|
143
146
|
};
|
|
144
147
|
}
|
|
148
|
+
function validateAttachRunningOptions(options, { attachRunning, hasInlineCookies, }) {
|
|
149
|
+
if (!attachRunning) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const conflicts = [
|
|
153
|
+
options.browserChromeProfile ? "--browser-chrome-profile" : null,
|
|
154
|
+
options.browserCookiePath ? "--browser-cookie-path" : null,
|
|
155
|
+
options.browserNoCookieSync ? "--browser-no-cookie-sync" : null,
|
|
156
|
+
options.browserHideWindow ? "--browser-hide-window" : null,
|
|
157
|
+
options.browserKeepBrowser ? "--browser-keep-browser" : null,
|
|
158
|
+
options.browserManualLogin ? "--browser-manual-login" : null,
|
|
159
|
+
options.browserManualLoginProfileDir ? "--browser-manual-login-profile-dir" : null,
|
|
160
|
+
hasInlineCookies ? "--browser-inline-cookies/--browser-inline-cookies-file" : null,
|
|
161
|
+
options.browserPort != null || options.browserDebugPort != null
|
|
162
|
+
? "--browser-port/--browser-debug-port"
|
|
163
|
+
: null,
|
|
164
|
+
].filter((value) => Boolean(value));
|
|
165
|
+
if (conflicts.length > 0) {
|
|
166
|
+
throw new Error(`--browser-attach-running cannot be combined with ${conflicts.join(", ")} because attach mode reuses an already-running browser instead of launching and configuring its own Chrome instance.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
145
169
|
function selectBrowserPort(options) {
|
|
146
170
|
const candidate = options.browserPort ?? options.browserDebugPort;
|
|
147
171
|
if (candidate === undefined || candidate === null)
|
|
@@ -151,6 +175,15 @@ function selectBrowserPort(options) {
|
|
|
151
175
|
}
|
|
152
176
|
return candidate;
|
|
153
177
|
}
|
|
178
|
+
function parseMaxConcurrentTabs(raw) {
|
|
179
|
+
if (!raw)
|
|
180
|
+
return undefined;
|
|
181
|
+
const value = Number.parseInt(raw, 10);
|
|
182
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
183
|
+
throw new Error(`Invalid browser max concurrent tabs: ${raw}. Expected a positive integer.`);
|
|
184
|
+
}
|
|
185
|
+
return Math.trunc(value);
|
|
186
|
+
}
|
|
154
187
|
export function mapModelToBrowserLabel(model) {
|
|
155
188
|
const normalized = normalizeChatGptModelForBrowser(model);
|
|
156
189
|
// Iterate ordered array to find first match (most specific first)
|
|
@@ -7,27 +7,36 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
7
7
|
const source = getSource(key);
|
|
8
8
|
return source === undefined || source === "default";
|
|
9
9
|
};
|
|
10
|
+
const attachRunningRequested = options.browserAttachRunning === true ||
|
|
11
|
+
(isUnset("browserAttachRunning") && browser.attachRunning === true);
|
|
10
12
|
const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
|
|
11
13
|
const cliChatgptSet = options.chatgptUrl !== undefined || options.browserUrl !== undefined;
|
|
12
14
|
if (isUnset("chatgptUrl") && !cliChatgptSet && configuredChatgptUrl !== undefined) {
|
|
13
15
|
options.chatgptUrl = normalizeChatgptUrl(configuredChatgptUrl ?? "", CHATGPT_URL);
|
|
14
16
|
}
|
|
15
|
-
if (
|
|
17
|
+
if (!attachRunningRequested &&
|
|
18
|
+
isUnset("browserChromeProfile") &&
|
|
19
|
+
browser.chromeProfile !== undefined) {
|
|
16
20
|
options.browserChromeProfile = browser.chromeProfile ?? undefined;
|
|
17
21
|
}
|
|
18
22
|
if (isUnset("browserChromePath") && browser.chromePath !== undefined) {
|
|
19
23
|
options.browserChromePath = browser.chromePath ?? undefined;
|
|
20
24
|
}
|
|
21
|
-
if (
|
|
25
|
+
if (!attachRunningRequested &&
|
|
26
|
+
isUnset("browserCookiePath") &&
|
|
27
|
+
browser.chromeCookiePath !== undefined) {
|
|
22
28
|
options.browserCookiePath = browser.chromeCookiePath ?? undefined;
|
|
23
29
|
}
|
|
30
|
+
if (isUnset("browserAttachRunning") && browser.attachRunning !== undefined) {
|
|
31
|
+
options.browserAttachRunning = browser.attachRunning;
|
|
32
|
+
}
|
|
24
33
|
if (isUnset("browserUrl") && options.browserUrl === undefined && browser.url !== undefined) {
|
|
25
34
|
options.browserUrl = browser.url;
|
|
26
35
|
}
|
|
27
36
|
if (isUnset("browserTimeout") && typeof browser.timeoutMs === "number") {
|
|
28
37
|
options.browserTimeout = String(browser.timeoutMs);
|
|
29
38
|
}
|
|
30
|
-
if (isUnset("browserPort") && typeof browser.debugPort === "number") {
|
|
39
|
+
if (!attachRunningRequested && isUnset("browserPort") && typeof browser.debugPort === "number") {
|
|
31
40
|
options.browserPort = browser.debugPort;
|
|
32
41
|
}
|
|
33
42
|
if (isUnset("browserInputTimeout") && typeof browser.inputTimeoutMs === "number") {
|
|
@@ -45,6 +54,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
45
54
|
if (isUnset("browserProfileLockTimeout") && typeof browser.profileLockTimeoutMs === "number") {
|
|
46
55
|
options.browserProfileLockTimeout = String(browser.profileLockTimeoutMs);
|
|
47
56
|
}
|
|
57
|
+
if (isUnset("browserMaxConcurrentTabs") && typeof browser.maxConcurrentTabs === "number") {
|
|
58
|
+
options.browserMaxConcurrentTabs = String(browser.maxConcurrentTabs);
|
|
59
|
+
}
|
|
48
60
|
if (isUnset("browserAutoReattachDelay") && typeof browser.autoReattachDelayMs === "number") {
|
|
49
61
|
options.browserAutoReattachDelay = String(browser.autoReattachDelayMs);
|
|
50
62
|
}
|
|
@@ -61,10 +73,12 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
61
73
|
if (isUnset("browserHeadless") && browser.headless !== undefined) {
|
|
62
74
|
options.browserHeadless = browser.headless;
|
|
63
75
|
}
|
|
64
|
-
if (isUnset("browserHideWindow") && browser.hideWindow !== undefined) {
|
|
76
|
+
if (!attachRunningRequested && isUnset("browserHideWindow") && browser.hideWindow !== undefined) {
|
|
65
77
|
options.browserHideWindow = browser.hideWindow;
|
|
66
78
|
}
|
|
67
|
-
if (
|
|
79
|
+
if (!attachRunningRequested &&
|
|
80
|
+
isUnset("browserKeepBrowser") &&
|
|
81
|
+
browser.keepBrowser !== undefined) {
|
|
68
82
|
options.browserKeepBrowser = browser.keepBrowser;
|
|
69
83
|
}
|
|
70
84
|
if (isUnset("browserModelStrategy") && browser.modelStrategy !== undefined) {
|
|
@@ -73,10 +87,20 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
73
87
|
if (isUnset("browserThinkingTime") && browser.thinkingTime !== undefined) {
|
|
74
88
|
options.browserThinkingTime = browser.thinkingTime;
|
|
75
89
|
}
|
|
76
|
-
if (isUnset("
|
|
90
|
+
if (isUnset("browserResearch") && browser.researchMode !== undefined) {
|
|
91
|
+
options.browserResearch = browser.researchMode;
|
|
92
|
+
}
|
|
93
|
+
if (isUnset("browserArchive") && browser.archiveConversations !== undefined) {
|
|
94
|
+
options.browserArchive = browser.archiveConversations;
|
|
95
|
+
}
|
|
96
|
+
if (!attachRunningRequested &&
|
|
97
|
+
isUnset("browserManualLogin") &&
|
|
98
|
+
browser.manualLogin !== undefined) {
|
|
77
99
|
options.browserManualLogin = browser.manualLogin;
|
|
78
100
|
}
|
|
79
|
-
if (
|
|
101
|
+
if (!attachRunningRequested &&
|
|
102
|
+
isUnset("browserManualLoginProfileDir") &&
|
|
103
|
+
browser.manualLoginProfileDir !== undefined) {
|
|
80
104
|
options.browserManualLoginProfileDir = browser.manualLoginProfileDir;
|
|
81
105
|
}
|
|
82
106
|
}
|