@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.
Files changed (52) hide show
  1. package/README.md +56 -11
  2. package/dist/bin/oracle-cli.js +104 -16
  3. package/dist/src/browser/actions/archiveConversation.js +236 -0
  4. package/dist/src/browser/actions/assistantResponse.js +26 -0
  5. package/dist/src/browser/actions/deepResearch.js +662 -0
  6. package/dist/src/browser/actions/modelSelection.js +86 -16
  7. package/dist/src/browser/actions/navigation.js +22 -0
  8. package/dist/src/browser/actions/projectSources.js +491 -0
  9. package/dist/src/browser/actions/promptComposer.js +52 -27
  10. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  11. package/dist/src/browser/artifacts.js +150 -0
  12. package/dist/src/browser/attachRunning.js +31 -0
  13. package/dist/src/browser/chatgptImages.js +315 -0
  14. package/dist/src/browser/chromeLifecycle.js +214 -3
  15. package/dist/src/browser/config.js +27 -9
  16. package/dist/src/browser/constants.js +8 -0
  17. package/dist/src/browser/controlPlan.js +81 -0
  18. package/dist/src/browser/detect.js +206 -33
  19. package/dist/src/browser/domDebug.js +49 -0
  20. package/dist/src/browser/index.js +1234 -479
  21. package/dist/src/browser/liveTabs.js +434 -0
  22. package/dist/src/browser/profileState.js +83 -3
  23. package/dist/src/browser/projectSourcesRunner.js +366 -0
  24. package/dist/src/browser/reattach.js +117 -45
  25. package/dist/src/browser/reattachHelpers.js +1 -1
  26. package/dist/src/browser/sessionRunner.js +53 -1
  27. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  28. package/dist/src/cli/bridge/claudeConfig.js +12 -8
  29. package/dist/src/cli/bridge/codexConfig.js +2 -2
  30. package/dist/src/cli/browserConfig.js +41 -8
  31. package/dist/src/cli/browserDefaults.js +31 -7
  32. package/dist/src/cli/browserTabs.js +228 -0
  33. package/dist/src/cli/dryRun.js +33 -1
  34. package/dist/src/cli/duplicatePromptGuard.js +10 -2
  35. package/dist/src/cli/help.js +1 -1
  36. package/dist/src/cli/options.js +4 -0
  37. package/dist/src/cli/projectSources.js +116 -0
  38. package/dist/src/cli/sessionCommand.js +51 -0
  39. package/dist/src/cli/sessionDisplay.js +121 -9
  40. package/dist/src/cli/sessionRunner.js +51 -7
  41. package/dist/src/mcp/consultPresets.js +19 -0
  42. package/dist/src/mcp/server.js +2 -0
  43. package/dist/src/mcp/tools/consult.js +201 -26
  44. package/dist/src/mcp/tools/projectSources.js +123 -0
  45. package/dist/src/mcp/types.js +11 -2
  46. package/dist/src/mcp/utils.js +6 -1
  47. package/dist/src/oracle/run.js +4 -1
  48. package/dist/src/projectSources/plan.js +27 -0
  49. package/dist/src/projectSources/types.js +1 -0
  50. package/dist/src/projectSources/url.js +23 -0
  51. package/dist/src/sessionManager.js +1 -0
  52. 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] ") && /fallback|retry/i.test(message);
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: process.env.ORACLE_HOME_DIR ?? path.join(os.homedir(), ".oracle-local"),
16
- browserProfileDir: process.env.ORACLE_BROWSER_PROFILE_DIR ??
17
- path.join(os.homedir(), ".oracle-local", "browser-profile"),
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.log("");
25
- console.log(chalk.dim("Tip: rerun with --print-token to include ORACLE_REMOTE_TOKEN in the snippet."));
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.log("");
20
- console.log(chalk.dim("Tip: rerun with --print-token to include ORACLE_REMOTE_TOKEN in the snippet."));
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, isTemporaryChatUrl, normalizeChatgptUrl, parseDuration, } from "../browserMode.js";
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 (isUnset("browserChromeProfile") && browser.chromeProfile !== undefined) {
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 (isUnset("browserCookiePath") && browser.chromeCookiePath !== undefined) {
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 (isUnset("browserKeepBrowser") && browser.keepBrowser !== undefined) {
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("browserManualLogin") && browser.manualLogin !== undefined) {
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 (isUnset("browserManualLoginProfileDir") && browser.manualLoginProfileDir !== undefined) {
101
+ if (!attachRunningRequested &&
102
+ isUnset("browserManualLoginProfileDir") &&
103
+ browser.manualLoginProfileDir !== undefined) {
80
104
  options.browserManualLoginProfileDir = browser.manualLoginProfileDir;
81
105
  }
82
106
  }