@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.
Files changed (52) hide show
  1. package/README.md +55 -10
  2. package/dist/bin/oracle-cli.js +104 -16
  3. package/dist/src/browser/actions/archiveConversation.js +224 -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 +78 -13
  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 +26 -2
  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 +1257 -485
  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 +40 -0
  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 +7 -0
  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 +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] ") && /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, }) {
@@ -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 (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
  }