@steipete/oracle 0.10.0 → 0.11.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 +55 -10
- package/dist/bin/oracle-cli.js +104 -16
- package/dist/src/browser/actions/archiveConversation.js +224 -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 +78 -13
- 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 +26 -2
- 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 +1257 -485
- 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 +40 -0
- 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 +7 -0
- 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 +2 -1
|
@@ -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, }) {
|
|
@@ -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
|
|
@@ -92,6 +97,7 @@ export async function buildBrowserConfig(options) {
|
|
|
92
97
|
chromeProfile: options.browserChromeProfile ?? DEFAULT_CHROME_PROFILE,
|
|
93
98
|
chromePath: options.browserChromePath ?? null,
|
|
94
99
|
chromeCookiePath: options.browserCookiePath ?? null,
|
|
100
|
+
attachRunning,
|
|
95
101
|
url,
|
|
96
102
|
debugPort: selectBrowserPort(options),
|
|
97
103
|
timeoutMs: options.browserTimeout
|
|
@@ -112,6 +118,7 @@ export async function buildBrowserConfig(options) {
|
|
|
112
118
|
profileLockTimeoutMs: options.browserProfileLockTimeout
|
|
113
119
|
? parseDuration(options.browserProfileLockTimeout, 0)
|
|
114
120
|
: undefined,
|
|
121
|
+
maxConcurrentTabs: parseMaxConcurrentTabs(options.browserMaxConcurrentTabs),
|
|
115
122
|
autoReattachDelayMs: options.browserAutoReattachDelay
|
|
116
123
|
? parseDuration(options.browserAutoReattachDelay, 0)
|
|
117
124
|
: undefined,
|
|
@@ -139,9 +146,33 @@ export async function buildBrowserConfig(options) {
|
|
|
139
146
|
// Allow cookie failures by default so runs can continue without Chrome/Keychain secrets.
|
|
140
147
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
141
148
|
remoteChrome,
|
|
149
|
+
browserTabRef: options.browserTab ?? undefined,
|
|
142
150
|
thinkingTime: options.browserThinkingTime,
|
|
151
|
+
researchMode: options.browserResearch === "deep" ? "deep" : "off",
|
|
152
|
+
archiveConversations: options.browserArchive,
|
|
143
153
|
};
|
|
144
154
|
}
|
|
155
|
+
function validateAttachRunningOptions(options, { attachRunning, hasInlineCookies, }) {
|
|
156
|
+
if (!attachRunning) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const conflicts = [
|
|
160
|
+
options.browserChromeProfile ? "--browser-chrome-profile" : null,
|
|
161
|
+
options.browserCookiePath ? "--browser-cookie-path" : null,
|
|
162
|
+
options.browserNoCookieSync ? "--browser-no-cookie-sync" : null,
|
|
163
|
+
options.browserHideWindow ? "--browser-hide-window" : null,
|
|
164
|
+
options.browserKeepBrowser ? "--browser-keep-browser" : null,
|
|
165
|
+
options.browserManualLogin ? "--browser-manual-login" : null,
|
|
166
|
+
options.browserManualLoginProfileDir ? "--browser-manual-login-profile-dir" : null,
|
|
167
|
+
hasInlineCookies ? "--browser-inline-cookies/--browser-inline-cookies-file" : null,
|
|
168
|
+
options.browserPort != null || options.browserDebugPort != null
|
|
169
|
+
? "--browser-port/--browser-debug-port"
|
|
170
|
+
: null,
|
|
171
|
+
].filter((value) => Boolean(value));
|
|
172
|
+
if (conflicts.length > 0) {
|
|
173
|
+
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.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
145
176
|
function selectBrowserPort(options) {
|
|
146
177
|
const candidate = options.browserPort ?? options.browserDebugPort;
|
|
147
178
|
if (candidate === undefined || candidate === null)
|
|
@@ -151,6 +182,15 @@ function selectBrowserPort(options) {
|
|
|
151
182
|
}
|
|
152
183
|
return candidate;
|
|
153
184
|
}
|
|
185
|
+
function parseMaxConcurrentTabs(raw) {
|
|
186
|
+
if (!raw)
|
|
187
|
+
return undefined;
|
|
188
|
+
const value = Number.parseInt(raw, 10);
|
|
189
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
190
|
+
throw new Error(`Invalid browser max concurrent tabs: ${raw}. Expected a positive integer.`);
|
|
191
|
+
}
|
|
192
|
+
return Math.trunc(value);
|
|
193
|
+
}
|
|
154
194
|
export function mapModelToBrowserLabel(model) {
|
|
155
195
|
const normalized = normalizeChatGptModelForBrowser(model);
|
|
156
196
|
// 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
|
}
|