@steipete/oracle 0.17.3 → 0.19.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 (161) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +75 -64
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/archiveConversation.js +31 -7
  5. package/dist/src/browser/actions/assistantResponse.js +43 -1
  6. package/dist/src/browser/actions/attachmentContext.js +239 -0
  7. package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
  8. package/dist/src/browser/actions/attachmentEvidence.js +78 -0
  9. package/dist/src/browser/actions/attachmentProgress.js +41 -0
  10. package/dist/src/browser/actions/attachmentPrompt.js +130 -0
  11. package/dist/src/browser/actions/attachments.js +306 -355
  12. package/dist/src/browser/actions/deepResearch.js +2 -1
  13. package/dist/src/browser/actions/modelSelection.js +13 -3
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +361 -186
  16. package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
  17. package/dist/src/browser/actions/thinkingStatus.js +2 -5
  18. package/dist/src/browser/actions/thinkingTime.js +267 -15
  19. package/dist/src/browser/attachRunning.js +46 -2
  20. package/dist/src/browser/attachmentValidation.js +53 -0
  21. package/dist/src/browser/cancellation.js +105 -0
  22. package/dist/src/browser/chatgptImages.js +2 -0
  23. package/dist/src/browser/chatgptThrottle.js +104 -0
  24. package/dist/src/browser/chromeLifecycle.js +267 -43
  25. package/dist/src/browser/config.js +15 -3
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +447 -263
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/policies.js +6 -2
  31. package/dist/src/browser/profileState.js +114 -9
  32. package/dist/src/browser/projectSourcesRunner.js +88 -17
  33. package/dist/src/browser/prompt.js +272 -113
  34. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  35. package/dist/src/browser/reattach.js +14 -4
  36. package/dist/src/browser/sessionRunner.js +71 -11
  37. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  38. package/dist/src/browser/utils.js +20 -2
  39. package/dist/src/cli/browserConfig.js +40 -15
  40. package/dist/src/cli/browserDefaults.js +13 -0
  41. package/dist/src/cli/browserTabs.js +12 -29
  42. package/dist/src/cli/detachedSession.js +69 -0
  43. package/dist/src/cli/dryRun.js +58 -47
  44. package/dist/src/cli/harvestIntegrity.js +140 -0
  45. package/dist/src/cli/projectSources.js +5 -3
  46. package/dist/src/cli/serveBrowserConfig.js +24 -0
  47. package/dist/src/cli/sessionDisplay.js +9 -2
  48. package/dist/src/cli/sessionRunner.js +91 -7
  49. package/dist/src/config.js +1 -0
  50. package/dist/src/gemini-web/client.js +67 -21
  51. package/dist/src/gemini-web/executor.js +4 -0
  52. package/dist/src/gemini-web/http.js +15 -0
  53. package/dist/src/gemini-web/models.js +15 -11
  54. package/dist/src/mcp/server.js +11 -14
  55. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  56. package/dist/src/mcp/tools/consult.js +97 -26
  57. package/dist/src/mcp/tools/projectSources.js +4 -6
  58. package/dist/src/mcp/tools/sessionResources.js +1 -1
  59. package/dist/src/mcp/tools/sessions.js +2 -2
  60. package/dist/src/mcp/tools/wait.js +182 -0
  61. package/dist/src/mcp/types.js +7 -0
  62. package/dist/src/oracle/errors.js +11 -0
  63. package/dist/src/oracle/providerRoutePlan.js +3 -2
  64. package/dist/src/remote/client.js +70 -6
  65. package/dist/src/remote/health.js +6 -2
  66. package/dist/src/remote/runSlots.js +83 -0
  67. package/dist/src/remote/server.js +351 -72
  68. package/dist/src/sessionManager.js +36 -1
  69. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  70. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  71. package/package.json +25 -23
  72. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  73. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  74. package/dist/bin/oracle.js +0 -683
  75. package/dist/docs-site/.nojekyll +0 -0
  76. package/dist/docs-site/CNAME +0 -1
  77. package/dist/docs-site/RELEASING.html +0 -410
  78. package/dist/docs-site/agents.html +0 -374
  79. package/dist/docs-site/anthropic.html +0 -368
  80. package/dist/docs-site/bridge.html +0 -400
  81. package/dist/docs-site/browser-mode.html +0 -588
  82. package/dist/docs-site/chromium-forks.html +0 -347
  83. package/dist/docs-site/cli-reference.html +0 -346
  84. package/dist/docs-site/configuration.html +0 -436
  85. package/dist/docs-site/favicon.svg +0 -14
  86. package/dist/docs-site/followup.html +0 -368
  87. package/dist/docs-site/gemini.html +0 -379
  88. package/dist/docs-site/grok.html +0 -325
  89. package/dist/docs-site/index.html +0 -360
  90. package/dist/docs-site/install.html +0 -335
  91. package/dist/docs-site/linux.html +0 -321
  92. package/dist/docs-site/llms.txt +0 -43
  93. package/dist/docs-site/manual-tests.html +0 -595
  94. package/dist/docs-site/mcp.html +0 -368
  95. package/dist/docs-site/multimodel.html +0 -364
  96. package/dist/docs-site/mythical-pro-agents.html +0 -360
  97. package/dist/docs-site/notifier.html +0 -338
  98. package/dist/docs-site/openai-endpoints.html +0 -387
  99. package/dist/docs-site/openrouter.html +0 -344
  100. package/dist/docs-site/quickstart.html +0 -369
  101. package/dist/docs-site/refactor/ux.html +0 -532
  102. package/dist/docs-site/sessions.html +0 -388
  103. package/dist/docs-site/social-card.png +0 -0
  104. package/dist/docs-site/social-card.svg +0 -79
  105. package/dist/docs-site/spec.html +0 -363
  106. package/dist/docs-site/testing.html +0 -320
  107. package/dist/docs-site/tui-debug.html +0 -326
  108. package/dist/docs-site/windows-work.html +0 -323
  109. package/dist/docs-site/windows.html +0 -320
  110. package/dist/markdansi/types/index.js +0 -4
  111. package/dist/oracle/bin/oracle-cli.js +0 -472
  112. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  113. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  114. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  115. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  116. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  117. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  118. package/dist/oracle/src/browser/config.js +0 -33
  119. package/dist/oracle/src/browser/constants.js +0 -40
  120. package/dist/oracle/src/browser/cookies.js +0 -210
  121. package/dist/oracle/src/browser/domDebug.js +0 -36
  122. package/dist/oracle/src/browser/index.js +0 -331
  123. package/dist/oracle/src/browser/pageActions.js +0 -5
  124. package/dist/oracle/src/browser/prompt.js +0 -88
  125. package/dist/oracle/src/browser/promptSummary.js +0 -20
  126. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  127. package/dist/oracle/src/browser/types.js +0 -1
  128. package/dist/oracle/src/browser/utils.js +0 -62
  129. package/dist/oracle/src/browserMode.js +0 -1
  130. package/dist/oracle/src/cli/browserConfig.js +0 -44
  131. package/dist/oracle/src/cli/dryRun.js +0 -59
  132. package/dist/oracle/src/cli/engine.js +0 -17
  133. package/dist/oracle/src/cli/errorUtils.js +0 -9
  134. package/dist/oracle/src/cli/help.js +0 -70
  135. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  136. package/dist/oracle/src/cli/options.js +0 -103
  137. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  138. package/dist/oracle/src/cli/rootAlias.js +0 -30
  139. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  140. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  141. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  142. package/dist/oracle/src/heartbeat.js +0 -43
  143. package/dist/oracle/src/oracle/client.js +0 -48
  144. package/dist/oracle/src/oracle/config.js +0 -29
  145. package/dist/oracle/src/oracle/errors.js +0 -101
  146. package/dist/oracle/src/oracle/files.js +0 -220
  147. package/dist/oracle/src/oracle/format.js +0 -33
  148. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  149. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  150. package/dist/oracle/src/oracle/request.js +0 -48
  151. package/dist/oracle/src/oracle/run.js +0 -444
  152. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  153. package/dist/oracle/src/oracle/types.js +0 -1
  154. package/dist/oracle/src/oracle.js +0 -9
  155. package/dist/oracle/src/sessionManager.js +0 -205
  156. package/dist/oracle/src/version.js +0 -39
  157. package/dist/scripts/chrome/browser-tools.js +0 -295
  158. package/dist/src/browser/chromeCookies.js +0 -312
  159. package/dist/src/browser/keytarShim.js +0 -56
  160. package/dist/src/browser/profileSync.js +0 -141
  161. package/dist/src/browser/windowsCookies.js +0 -219
@@ -65,3 +65,12 @@ export function formatBrowserModelSelectionEvidence(evidence, model) {
65
65
  const verified = evidence.verified ? "yes" : "no";
66
66
  return `requestedKey=${requestedKey}; target=${target}; resolvedLabel=${resolvedLabel}; status=${evidence.status}; strategy=${strategy}; verified=${verified}; source=${evidence.source}; capturedAt=${evidence.capturedAt}`;
67
67
  }
68
+ /** Renders the observed effort selection independently of the model picker. */
69
+ export function formatBrowserThinkingSelectionEvidence(evidence) {
70
+ const resolvedLabel = cleanLabel(evidence.resolvedLabel) ?? "(none)";
71
+ const verified = evidence.verified ? "yes" : "no";
72
+ const failClosed = evidence.strictFailClosed ? "yes" : "no";
73
+ const targetKind = evidence.targetModelKind ?? "(none)";
74
+ const observedKind = evidence.observedModelKind ?? "(none)";
75
+ return `requestedLevel=${evidence.requestedLevel}; status=${evidence.status}; resolvedLabel=${resolvedLabel}; verified=${verified}; failClosed=${failClosed}; targetModelKind=${targetKind}; observedModelKind=${observedKind}; source=${evidence.source}; capturedAt=${evidence.capturedAt}`;
76
+ }
@@ -1,5 +1,5 @@
1
1
  export { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureChatMode, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, } from "./actions/navigation.js";
2
2
  export { ensureModelSelection } from "./actions/modelSelection.js";
3
3
  export { submitPrompt, clearPromptComposer } from "./actions/promptComposer.js";
4
- export { clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, buildUserTurnAttachmentExpressionForTest, } from "./actions/attachments.js";
5
- export { waitForAssistantResponse, readAssistantSnapshot, captureAssistantMarkdown, buildAssistantExtractorForTest, buildAssistantSnapshotExpressionForTest, buildConversationDebugExpressionForTest, buildMarkdownFallbackExtractorForTest, buildCopyExpressionForTest, } from "./actions/assistantResponse.js";
4
+ export { assertComposerPlusStayedInPlace, captureComposerNavigationUrl, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, buildUserTurnAttachmentExpressionForTest, } from "./actions/attachments.js";
5
+ export { waitForAssistantResponse, readAssistantSnapshot, captureAssistantMarkdown, buildAssistantExtractorForTest, buildAssistantSnapshotExpressionForTest, buildConversationDebugExpressionForTest, buildMarkdownFallbackExtractorForTest, buildCopyExpressionForTest, isRetryAssistantUiErrorText, } from "./actions/assistantResponse.js";
@@ -24,6 +24,7 @@ export function buildAttachmentPlan(sections, { inlineFiles, bundleRequested, ma
24
24
  shouldBundle,
25
25
  };
26
26
  }
27
+ export const CHROME_COOKIE_SYNC_WARNING = "Warning: copying cookies from a live Chrome profile can invalidate that browser's ChatGPT session when tokens rotate. Prefer --browser-manual-login or inline cookies when possible.";
27
28
  export function shouldSyncBrowserCookies(config, { manualLogin, profileIsPreSigned = manualLogin, }) {
28
29
  const explicitManualLoginSync = manualLogin && config.manualLoginCookieSync === true;
29
30
  return config.cookieSync && (!profileIsPreSigned || explicitManualLoginSync);
@@ -36,8 +37,11 @@ export function buildCookiePlan(config) {
36
37
  description: `Cookies: inline payload (${config.inlineCookies.length}) via ${source}.`,
37
38
  };
38
39
  }
39
- if (config?.cookieSync === false) {
40
- return { type: "disabled", description: "Cookies: sync disabled (--browser-no-cookie-sync)." };
40
+ if (config?.cookieSync !== true) {
41
+ return {
42
+ type: "disabled",
43
+ description: "Cookies: Chrome copy disabled (use --browser-manual-login, inline cookies, or --browser-cookie-sync).",
44
+ };
41
45
  }
42
46
  const allowlist = config?.cookieNames && config.cookieNames.length > 0
43
47
  ? config.cookieNames.join(", ")
@@ -12,6 +12,7 @@ const DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS = [
12
12
  const CHROME_PID_FILENAME = "chrome.pid";
13
13
  const ORACLE_PROFILE_LOCK_FILENAME = "oracle-automation.lock";
14
14
  const execFileAsync = promisify(execFile);
15
+ let ownProcessStartTime;
15
16
  export function getDevToolsActivePortPaths(userDataDir) {
16
17
  return DEVTOOLS_ACTIVE_PORT_RELATIVE_PATHS.map((relative) => path.join(userDataDir, relative));
17
18
  }
@@ -95,7 +96,7 @@ function findChromeDebugTargetForProfileFromProcessList(processList, userDataDir
95
96
  continue;
96
97
  if (!lower.includes("chrome") && !lower.includes("chromium"))
97
98
  continue;
98
- if (!lower.includes("user-data-dir") || !command.includes(userDataDir))
99
+ if (!isChromeCommandForUserDataDir(command, userDataDir))
99
100
  continue;
100
101
  const portMatch = command.match(/--remote-debugging-port(?:=|\s+)(\d+)/);
101
102
  const port = Number.parseInt(portMatch?.[1] ?? "", 10);
@@ -119,11 +120,31 @@ export async function terminateRecordedChromeForProfile(userDataDir, logger) {
119
120
  return false;
120
121
  }
121
122
  try {
122
- process.kill(pid, "SIGTERM");
123
+ if (process.platform === "win32") {
124
+ await execFileAsync("taskkill.exe", ["/PID", String(pid), "/T", "/F"], {
125
+ maxBuffer: 1024 * 1024,
126
+ windowsHide: true,
127
+ timeout: 5000,
128
+ });
129
+ }
130
+ else {
131
+ process.kill(pid, "SIGTERM");
132
+ }
133
+ for (let attempt = 0; attempt < 30 && isProcessAlive(pid); attempt += 1) {
134
+ await delay(100);
135
+ }
136
+ if (isProcessAlive(pid)) {
137
+ logger?.(`Shared manual-login Chrome pid ${pid} remained alive after termination.`);
138
+ return false;
139
+ }
123
140
  logger?.(`Terminated shared manual-login Chrome pid ${pid}`);
124
141
  return true;
125
142
  }
126
143
  catch (error) {
144
+ if (!isProcessAlive(pid)) {
145
+ logger?.(`Terminated shared manual-login Chrome pid ${pid}`);
146
+ return true;
147
+ }
127
148
  const message = error instanceof Error ? error.message : String(error);
128
149
  logger?.(`Failed to terminate shared manual-login Chrome pid ${pid}: ${message}`);
129
150
  return false;
@@ -132,10 +153,49 @@ export async function terminateRecordedChromeForProfile(userDataDir, logger) {
132
153
  function isChromeCommandForUserDataDir(command, userDataDir) {
133
154
  if (!command)
134
155
  return false;
135
- const lower = command.toLowerCase();
136
- return ((lower.includes("chrome") || lower.includes("chromium")) &&
137
- lower.includes("user-data-dir") &&
138
- command.includes(userDataDir));
156
+ // ps flattens POSIX argv: an unquoted profile may contain spaces, so its
157
+ // boundary is the next Chrome switch/startup URL or the end, not the first whitespace.
158
+ const matches = [
159
+ ...command.matchAll(/(?:^|\s)(?:"--user-data-dir=([^"]*)"|--user-data-dir(?:=|\s+)(?:"([^"]*)"|'([^']*)'|([^\r\n]+?)(?=\s+(?:"?--|about:blank(?:\s|$))|\s*$)))/giu),
160
+ ];
161
+ if (matches.length !== 1)
162
+ return false;
163
+ const match = matches[0];
164
+ const commandProfile = match[1] ?? match[2] ?? match[3] ?? match[4];
165
+ if (!commandProfile || !startsWithChromeExecutable(command))
166
+ return false;
167
+ return (normalizeProfilePathForComparison(commandProfile) ===
168
+ normalizeProfilePathForComparison(userDataDir));
169
+ }
170
+ function startsWithChromeExecutable(command) {
171
+ const names = /^(?:google chrome(?: canary| beta| dev| for testing)?|google-chrome(?:-stable|-beta|-dev|-unstable)?|chrome|chromium|chromium-browser|chrome-headless-shell)(?:\.exe)?$/iu;
172
+ const trimmed = command.trimStart();
173
+ const quoted = trimmed.match(/^(?:"([^"]+)"|'([^']+)')(?=\s|$)/u);
174
+ let executable;
175
+ if (quoted) {
176
+ executable = quoted[1] ?? quoted[2];
177
+ }
178
+ else {
179
+ const optionsStart = trimmed.search(/\s+"?--/u);
180
+ executable = optionsStart < 0 ? trimmed : trimmed.slice(0, optionsStart);
181
+ // macOS ps leaves the standard Google Chrome bundle path unquoted. Other
182
+ // whitespace is ambiguous with argv entries; never mistake a later argument
183
+ // named chrome for the executable that owns this PID.
184
+ if (/\s/u.test(executable.replace(/google chrome(?: canary| beta| dev| for testing)?(?:\.app)?/giu, "chrome")))
185
+ return false;
186
+ }
187
+ return names.test(executable.split(/[\\/]/u).at(-1) ?? "");
188
+ }
189
+ function normalizeProfilePathForComparison(value) {
190
+ let normalized = path.normalize(path.resolve(value));
191
+ if (process.platform === "win32") {
192
+ normalized = normalized.replace(/^\\\\\?\\/u, "").toLowerCase();
193
+ }
194
+ const root = path.parse(normalized).root;
195
+ while (normalized.length > root.length && /[\\/]$/u.test(normalized)) {
196
+ normalized = normalized.slice(0, -1);
197
+ }
198
+ return normalized;
139
199
  }
140
200
  export function isChromeCommandForUserDataDirForTest(command, userDataDir) {
141
201
  return isChromeCommandForUserDataDir(command, userDataDir);
@@ -158,6 +218,39 @@ export function isProcessAlive(pid) {
158
218
  return false;
159
219
  }
160
220
  }
221
+ export async function readProcessStartTimeMs(pid) {
222
+ if (!Number.isFinite(pid) || pid <= 0)
223
+ return null;
224
+ if (Math.trunc(pid) === process.pid) {
225
+ // Use the same OS identity as peer controllers, not wall time minus uptime.
226
+ // Cache our own PID only: it cannot be reused during this process's lifetime.
227
+ return (ownProcessStartTime ??= queryProcessStartTimeMs(process.pid));
228
+ }
229
+ return queryProcessStartTimeMs(pid);
230
+ }
231
+ async function queryProcessStartTimeMs(pid) {
232
+ try {
233
+ const executable = process.platform === "win32" ? "powershell.exe" : "ps";
234
+ const args = process.platform === "win32"
235
+ ? [
236
+ "-NoProfile",
237
+ "-NonInteractive",
238
+ "-Command",
239
+ `$process = Get-Process -Id ${Math.trunc(pid)} -ErrorAction Stop; $process.StartTime.ToUniversalTime().ToString('o')`,
240
+ ]
241
+ : ["-p", String(Math.trunc(pid)), "-o", "lstart="];
242
+ const { stdout } = await execFileAsync(executable, args, {
243
+ maxBuffer: 1024 * 1024,
244
+ windowsHide: true,
245
+ timeout: 5000,
246
+ });
247
+ const startedAt = Date.parse(String(stdout ?? "").trim());
248
+ return Number.isFinite(startedAt) ? startedAt : null;
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ }
161
254
  function parseProfileRunLock(payload) {
162
255
  if (!payload)
163
256
  return null;
@@ -186,6 +279,7 @@ export async function acquireProfileRunLock(userDataDir, options) {
186
279
  const startedAt = Date.now();
187
280
  let warned = false;
188
281
  for (;;) {
282
+ options.signal?.throwIfAborted();
189
283
  try {
190
284
  const payload = {
191
285
  pid: process.pid,
@@ -210,7 +304,7 @@ export async function acquireProfileRunLock(userDataDir, options) {
210
304
  let existing = parseProfileRunLock(await readFile(lockPath, "utf8").catch(() => null));
211
305
  if (!existing) {
212
306
  // Likely partial write / corruption; re-read once, then delete (user preference: delete unreadable lockfiles).
213
- await delay(200);
307
+ await delay(200, options.signal);
214
308
  existing = parseProfileRunLock(await readFile(lockPath, "utf8").catch(() => null));
215
309
  if (!existing) {
216
310
  options.logger?.("Oracle profile lock unreadable; deleting lockfile.");
@@ -231,7 +325,7 @@ export async function acquireProfileRunLock(userDataDir, options) {
231
325
  if (elapsed >= timeoutMs) {
232
326
  throw new Error(`Oracle profile lock still held by pid ${existing.pid} after ${Math.round(elapsed / 1000)}s`);
233
327
  }
234
- await delay(Math.min(pollMs, timeoutMs - elapsed));
328
+ await delay(Math.min(pollMs, timeoutMs - elapsed), options.signal);
235
329
  }
236
330
  }
237
331
  }
@@ -353,8 +447,19 @@ async function isChromeUsingUserDataDir(userDataDir) {
353
447
  }
354
448
  async function readProcessCommand(pid) {
355
449
  try {
356
- const { stdout } = await execFileAsync("ps", ["-p", String(Math.trunc(pid)), "-o", "command="], {
450
+ const executable = process.platform === "win32" ? "powershell.exe" : "ps";
451
+ const args = process.platform === "win32"
452
+ ? [
453
+ "-NoProfile",
454
+ "-NonInteractive",
455
+ "-Command",
456
+ `(Get-CimInstance Win32_Process -Filter 'ProcessId = ${Math.trunc(pid)}').CommandLine`,
457
+ ]
458
+ : ["-p", String(Math.trunc(pid)), "-o", "command="];
459
+ const { stdout } = await execFileAsync(executable, args, {
357
460
  maxBuffer: 1024 * 1024,
461
+ windowsHide: true,
462
+ timeout: 5000,
358
463
  });
359
464
  const command = String(stdout ?? "").trim();
360
465
  return command || null;
@@ -1,19 +1,72 @@
1
1
  import { mkdtemp, mkdir, rm } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import { closeTab, connectWithNewTab, launchChrome, positionChromeWindowOffscreen, registerTerminationHooks, } from "./chromeLifecycle.js";
4
+ import { closeTab, connectWithNewTab, launchChrome, positionChromeWindowOffscreen, positionChromeWindowOnscreen, registerTerminationHooks, } from "./chromeLifecycle.js";
5
5
  import { resolveBrowserConfig } from "./config.js";
6
6
  import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
7
7
  import { installJavaScriptDialogAutoDismissal, navigateToChatGPT, ensureLoggedIn, } from "./pageActions.js";
8
- import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
9
- import { acquireProfileRunLock, cleanupStaleProfileState, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
8
+ import { acquireBrowserTabLease } from "./tabLeaseRegistry.js";
9
+ import { acquireProfileRunLock, cleanupStaleProfileState, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, terminateRecordedChromeForProfile, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
10
10
  import { CHATGPT_URL } from "./constants.js";
11
11
  import { delay } from "./utils.js";
12
12
  import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
13
13
  import { openProjectSourcesTab, uploadProjectSources, waitForProjectSourcesReady, waitForProjectSourcesListSettled, } from "./actions/projectSources.js";
14
14
  import { normalizeProjectSourcesUrl } from "../projectSources/url.js";
15
15
  import { buildProjectSourcesUploadPlan, diffAddedProjectSources } from "../projectSources/plan.js";
16
- import { shouldSyncBrowserCookies } from "./policies.js";
16
+ import { CHROME_COOKIE_SYNC_WARNING, shouldSyncBrowserCookies } from "./policies.js";
17
+ async function releaseProjectSourcesBrowserTabLease(options) {
18
+ let decisionObserved = false;
19
+ let keepBrowserOpen = false;
20
+ let terminationHandled = false;
21
+ let otherLeasesRemain = false;
22
+ let releaseError;
23
+ try {
24
+ await options.lease.release({
25
+ onRelease: async ({ isLastLease }) => {
26
+ decisionObserved = true;
27
+ if (!isLastLease) {
28
+ keepBrowserOpen = true;
29
+ otherLeasesRemain = true;
30
+ return;
31
+ }
32
+ if (!options.terminateSharedChrome)
33
+ return;
34
+ options.logger(`[browser] ChatGPT browser slot ${options.lease.id.slice(0, 8)} is final; ` +
35
+ `terminating shared Chrome (session=project-sources; controllerPid=${process.pid}; ` +
36
+ `chromePid=${options.chromePid ?? "unknown"}; chromePort=${options.chromePort ?? "unknown"}).`);
37
+ const terminated = await options.terminateSharedChrome().catch(() => false);
38
+ if (terminated) {
39
+ terminationHandled = true;
40
+ }
41
+ else {
42
+ keepBrowserOpen = true;
43
+ options.logger("[browser] Could not verify shared Chrome termination; leaving it available for reuse.");
44
+ }
45
+ },
46
+ });
47
+ }
48
+ catch (error) {
49
+ releaseError = error instanceof Error ? error : new Error(String(error));
50
+ if (!terminationHandled)
51
+ keepBrowserOpen = true;
52
+ options.logger(`[browser] Failed to release the ChatGPT browser slot registry lock; restart Oracle/Codex MCP before another browser run: ${releaseError.message}`);
53
+ }
54
+ if (!decisionObserved) {
55
+ keepBrowserOpen = true;
56
+ options.logger("[browser] Could not verify final ChatGPT tab lease; leaving shared Chrome running.");
57
+ }
58
+ else if (otherLeasesRemain) {
59
+ options.logger(`[browser] Other ChatGPT tab leases still active; leaving shared Chrome running; ` +
60
+ `browser slot ${options.lease.id.slice(0, 8)} is non-final ` +
61
+ `(session=project-sources; controllerPid=${process.pid}; ` +
62
+ `chromePid=${options.chromePid ?? "unknown"}; chromePort=${options.chromePort ?? "unknown"}).`);
63
+ }
64
+ return {
65
+ keepBrowserOpen,
66
+ terminationHandled,
67
+ ...(releaseError ? { releaseError } : {}),
68
+ };
69
+ }
17
70
  export async function runBrowserProjectSources(request) {
18
71
  const startedAt = Date.now();
19
72
  const logger = ((message) => request.log?.(message));
@@ -96,6 +149,7 @@ export async function runBrowserProjectSources(request) {
96
149
  removeTerminationHooks = registerTerminationHooks(chrome, userDataDir, effectiveKeepBrowser, logger, {
97
150
  isInFlight: () => !completed,
98
151
  preserveUserDataDir: manualLogin,
152
+ preserveSharedChromeOnSignal: manualLogin,
99
153
  });
100
154
  const strictTabIsolation = Boolean(manualLogin && reusedChrome);
101
155
  const devtoolsRetries = manualLogin ? 6 : 0;
@@ -128,7 +182,10 @@ export async function runBrowserProjectSources(request) {
128
182
  }
129
183
  await Promise.all(domainEnablers);
130
184
  if (!config.headless && config.hideWindow) {
131
- await positionChromeWindowOffscreen(client, logger);
185
+ await positionChromeWindowOffscreen(client, userDataDir, logger);
186
+ }
187
+ else if (!config.headless) {
188
+ await positionChromeWindowOnscreen(client, userDataDir, logger);
132
189
  }
133
190
  removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
134
191
  if (!manualLogin) {
@@ -189,7 +246,8 @@ export async function runBrowserProjectSources(request) {
189
246
  }
190
247
  let keepBrowserOpen = effectiveKeepBrowser;
191
248
  let cleanupProfileLock = null;
192
- let terminatedRecordedChrome = false;
249
+ let browserTerminationHandledByLease = false;
250
+ let tabLeaseReleaseError;
193
251
  if (!keepBrowserOpen && manualLogin && tabLease) {
194
252
  const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
195
253
  if (cleanupLockTimeoutMs > 0) {
@@ -199,24 +257,29 @@ export async function runBrowserProjectSources(request) {
199
257
  sessionId: "project-sources",
200
258
  }).catch(() => null);
201
259
  }
202
- keepBrowserOpen = await hasOtherActiveBrowserTabLeases(userDataDir, tabLease.id).catch(() => false);
203
- if (keepBrowserOpen) {
204
- logger("[browser] Other ChatGPT tab leases still active; leaving shared Chrome running.");
205
- }
206
- else if (reusedChrome && !connectionClosedUnexpectedly) {
207
- keepBrowserOpen = true;
208
- logger("[browser] Reused shared Chrome; leaving browser process running.");
209
- }
210
260
  }
211
261
  if (tabLease) {
212
262
  const handle = tabLease;
213
263
  tabLease = null;
214
- await handle.release().catch(() => undefined);
264
+ const chromeHandle = chrome;
265
+ const terminateSharedChrome = !keepBrowserOpen && manualLogin && chromeHandle && !connectionClosedUnexpectedly
266
+ ? async () => terminateRecordedChromeForProfile(userDataDir, logger).catch(() => false)
267
+ : undefined;
268
+ const releaseResult = await releaseProjectSourcesBrowserTabLease({
269
+ lease: handle,
270
+ terminateSharedChrome,
271
+ chromePid: chromeHandle?.pid,
272
+ chromePort: chromeHandle?.port,
273
+ logger,
274
+ });
275
+ keepBrowserOpen ||= releaseResult.keepBrowserOpen;
276
+ browserTerminationHandledByLease = releaseResult.terminationHandled;
277
+ tabLeaseReleaseError = releaseResult.releaseError;
215
278
  }
216
279
  if (!keepBrowserOpen && chrome) {
217
280
  if (!connectionClosedUnexpectedly) {
218
281
  try {
219
- if (!terminatedRecordedChrome) {
282
+ if (!browserTerminationHandledByLease) {
220
283
  await chrome.kill();
221
284
  }
222
285
  }
@@ -246,6 +309,10 @@ export async function runBrowserProjectSources(request) {
246
309
  if (cleanupProfileLock) {
247
310
  await cleanupProfileLock.release().catch(() => undefined);
248
311
  }
312
+ if (tabLeaseReleaseError) {
313
+ // oxlint-disable-next-line eslint/no-unsafe-finally -- This cleanup failure must override a successful Project Sources result or a live MCP owner can remain locked.
314
+ throw new Error("Failed to release the ChatGPT browser slot registry lock; restart Oracle/Codex MCP before another browser run.", { cause: tabLeaseReleaseError });
315
+ }
249
316
  }
250
317
  }
251
318
  async function applyProjectSourcesCookies({ config, network, manualLogin, logger, }) {
@@ -253,9 +320,12 @@ async function applyProjectSourcesCookies({ config, network, manualLogin, logger
253
320
  if (!cookieSyncEnabled) {
254
321
  logger(manualLogin
255
322
  ? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
256
- : "Skipping Chrome cookie sync (--browser-no-cookie-sync)");
323
+ : "Skipping Chrome cookie copy (disabled by default; use --browser-cookie-sync to opt in).");
257
324
  return 0;
258
325
  }
326
+ if (!config.inlineCookies) {
327
+ logger(CHROME_COOKIE_SYNC_WARNING);
328
+ }
259
329
  const cookieCount = await syncCookies(network, config.url, config.chromeProfile, logger, {
260
330
  allowErrors: config.allowCookieErrors ?? false,
261
331
  filterNames: config.cookieNames ?? undefined,
@@ -389,3 +459,4 @@ async function maybeReuseProjectSourcesChrome(userDataDir, logger, options = {})
389
459
  export async function maybeReuseProjectSourcesChromeForTest(userDataDir, logger, options = {}) {
390
460
  return maybeReuseProjectSourcesChrome(userDataDir, logger, options);
391
461
  }
462
+ export const releaseProjectSourcesBrowserTabLeaseForTest = releaseProjectSourcesBrowserTabLease;