@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
@@ -5,6 +5,7 @@ import { normalizeThinkingTimeLevel } from "../oracle/thinkingTime.js";
5
5
  import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "../browser/constants.js";
6
6
  import { normalizeChatgptUrl } from "../browser/utils.js";
7
7
  import { parseDuration } from "../duration.js";
8
+ import { resolveBrowserApprovalWait } from "../browser/config.js";
8
9
  import { normalizeBrowserModelStrategy } from "../browser/modelStrategy.js";
9
10
  import { getOracleHomeDir } from "../oracleHome.js";
10
11
  const DEFAULT_BROWSER_TIMEOUT_MS = 1_200_000;
@@ -13,6 +14,12 @@ const DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS = 45_000;
13
14
  const DEFAULT_BROWSER_RECHECK_TIMEOUT_MS = 120_000;
14
15
  const DEFAULT_BROWSER_AUTO_REATTACH_TIMEOUT_MS = 120_000;
15
16
  const DEFAULT_CHROME_PROFILE = "Default";
17
+ const CURRENT_CHATGPT_PRO_ALIASES = new Set([
18
+ "gpt-5-pro",
19
+ "gpt-5.1-pro",
20
+ "gpt-5.2-pro",
21
+ "gpt-5.4-pro",
22
+ ]);
16
23
  // Ordered array: most specific models first to ensure correct selection.
17
24
  // The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
18
25
  const BROWSER_MODEL_LABELS = [
@@ -52,11 +59,8 @@ export function normalizeChatGptModelForBrowser(model) {
52
59
  return normalized;
53
60
  }
54
61
  // Pro variants: resolve to the latest Pro model in ChatGPT.
55
- if (normalized === "gpt-5-pro" ||
56
- normalized === "gpt-5.1-pro" ||
57
- normalized === "gpt-5.2-pro" ||
58
- normalized === "gpt-5.4-pro") {
59
- return "gpt-5.5-pro";
62
+ if (isCurrentChatGptProAlias(normalized)) {
63
+ return "gpt-5.6-sol";
60
64
  }
61
65
  // Explicit model variants: keep as-is (they have their own browser labels)
62
66
  if (normalized === "gpt-5.2-thinking" || normalized === "gpt-5.2-instant") {
@@ -68,6 +72,18 @@ export function normalizeChatGptModelForBrowser(model) {
68
72
  }
69
73
  return model;
70
74
  }
75
+ export function isCurrentChatGptProAlias(model) {
76
+ return CURRENT_CHATGPT_PRO_ALIASES.has(model?.trim().toLowerCase() ?? "");
77
+ }
78
+ export function resolveDefaultBrowserThinkingTime({ model, requestedModel, modelStrategy, }) {
79
+ const strategy = normalizeBrowserModelStrategy(modelStrategy) ?? DEFAULT_MODEL_STRATEGY;
80
+ if (strategy !== "select")
81
+ return undefined;
82
+ const normalizedModel = normalizeChatGptModelForBrowser(model);
83
+ return isCurrentChatGptProAlias(requestedModel ?? model) || normalizedModel === "gpt-5.5-pro"
84
+ ? "pro"
85
+ : undefined;
86
+ }
71
87
  export async function buildBrowserConfig(options) {
72
88
  if (options.copyProfile && options.browserKeepBrowser) {
73
89
  throw new Error("--copy-profile cannot be combined with --browser-keep-browser: the copied profile is a throwaway that is deleted after the run, so it must not be retained.");
@@ -85,11 +101,14 @@ export async function buildBrowserConfig(options) {
85
101
  const normalizedOverride = desiredModelOverride?.toLowerCase() ?? "";
86
102
  const baseModel = options.model.toLowerCase();
87
103
  const isChatGptModel = baseModel.startsWith("gpt-") && !baseModel.includes("codex");
88
- const normalizedBrowserModel = normalizeChatGptModelForBrowser(options.model);
89
104
  const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
90
105
  const modelStrategy = normalizeBrowserModelStrategy(options.browserModelStrategy) ?? DEFAULT_MODEL_STRATEGY;
91
106
  const thinkingTime = normalizeThinkingTimeLevel(options.browserThinkingTime) ??
92
- (modelStrategy === "select" && normalizedBrowserModel === "gpt-5.5-pro" ? "pro" : undefined);
107
+ resolveDefaultBrowserThinkingTime({
108
+ model: options.model,
109
+ requestedModel: options.browserRequestedModel,
110
+ modelStrategy,
111
+ });
93
112
  assertBrowserModelAvailable(options.model, modelStrategy);
94
113
  const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
95
114
  let inline = await resolveInlineCookies({
@@ -99,7 +118,9 @@ export async function buildBrowserConfig(options) {
99
118
  envFile: process.env.ORACLE_BROWSER_COOKIES_FILE,
100
119
  cwd: process.cwd(),
101
120
  });
102
- if (inline?.source?.startsWith("home:") && options.browserNoCookieSync !== true) {
121
+ const chromeCookieSyncRequested = options.browserNoCookieSync !== true &&
122
+ (options.browserCookieSync === true || options.browserManualLoginCookieSync === true);
123
+ if (inline?.source?.startsWith("home:") && chromeCookieSyncRequested) {
103
124
  inline = undefined;
104
125
  }
105
126
  let remoteChrome;
@@ -133,6 +154,7 @@ export async function buildBrowserConfig(options) {
133
154
  inputTimeoutMs: options.browserInputTimeout
134
155
  ? parseBrowserDuration(options.browserInputTimeout, "--browser-input-timeout", DEFAULT_BROWSER_INPUT_TIMEOUT_MS)
135
156
  : undefined,
157
+ approvalWaitMs: resolveBrowserApprovalWait(options.browserApprovalWait),
136
158
  attachmentTimeoutMs: options.browserAttachmentTimeout
137
159
  ? parseBrowserDuration(options.browserAttachmentTimeout, "--browser-attachment-timeout", DEFAULT_BROWSER_ATTACHMENT_TIMEOUT_MS)
138
160
  : undefined,
@@ -161,11 +183,13 @@ export async function buildBrowserConfig(options) {
161
183
  cookieSyncWaitMs: options.browserCookieWait
162
184
  ? parseBrowserDuration(options.browserCookieWait, "--browser-cookie-wait", 0)
163
185
  : undefined,
164
- cookieSync: options.browserNoCookieSync
165
- ? false
166
- : options.browserManualLoginCookieSync === true
167
- ? true
168
- : undefined,
186
+ cookieSync: inline?.cookies?.length
187
+ ? true
188
+ : options.browserNoCookieSync
189
+ ? false
190
+ : options.browserCookieSync === true || options.browserManualLoginCookieSync === true
191
+ ? true
192
+ : undefined,
169
193
  cookieNames,
170
194
  inlineCookies: inline?.cookies,
171
195
  inlineCookiesSource: inline?.source ?? null,
@@ -173,7 +197,7 @@ export async function buildBrowserConfig(options) {
173
197
  keepBrowser: options.browserKeepBrowser ? true : undefined,
174
198
  manualLogin: options.browserManualLogin === undefined ? undefined : options.browserManualLogin,
175
199
  manualLoginProfileDir: options.browserManualLoginProfileDir ?? undefined,
176
- manualLoginCookieSync: options.browserManualLoginCookieSync,
200
+ manualLoginCookieSync: inline?.cookies?.length ? true : options.browserManualLoginCookieSync,
177
201
  copyProfileSource: options.copyProfile ?? undefined,
178
202
  hideWindow: options.browserHideWindow ? true : undefined,
179
203
  desiredModel,
@@ -206,6 +230,7 @@ function validateAttachRunningOptions(options, { attachRunning, hasInlineCookies
206
230
  const conflicts = [
207
231
  options.browserChromeProfile ? "--browser-chrome-profile" : null,
208
232
  options.browserCookiePath ? "--browser-cookie-path" : null,
233
+ options.browserCookieSync ? "--browser-cookie-sync" : null,
209
234
  options.browserNoCookieSync ? "--browser-no-cookie-sync" : null,
210
235
  options.browserHeadless ? "--browser-headless" : null,
211
236
  options.browserHideWindow ? "--browser-hide-window" : null,
@@ -268,7 +293,7 @@ export function resolveBrowserModelLabel(input, model) {
268
293
  }
269
294
  return trimmed;
270
295
  }
271
- function parseRemoteChromeTarget(raw) {
296
+ export function parseRemoteChromeTarget(raw) {
272
297
  const target = raw.trim();
273
298
  if (!target) {
274
299
  throw new Error("Invalid remote-chrome value: expected host:port but received an empty string.");
@@ -12,6 +12,13 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
12
12
  const attachRunningRequested = options.browserAttachRunning === true ||
13
13
  (isUnset("browserAttachRunning") && browser.attachRunning === true);
14
14
  const currentModelRequestedByCli = options.browserModelStrategy === "current" && getSource("browserModelStrategy") === "cli";
15
+ if (!options.copyProfile &&
16
+ isUnset("remoteChrome") &&
17
+ options.remoteChrome === undefined &&
18
+ browser.remoteChrome) {
19
+ const { host, port } = browser.remoteChrome;
20
+ options.remoteChrome = `${host.includes(":") && !host.startsWith("[") ? `[${host}]` : host}:${port}`;
21
+ }
15
22
  const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
16
23
  const cliChatgptSet = options.chatgptUrl !== undefined || options.browserUrl !== undefined;
17
24
  if (isUnset("chatgptUrl") && !cliChatgptSet && configuredChatgptUrl !== undefined) {
@@ -45,6 +52,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
45
52
  if (isUnset("browserInputTimeout") && typeof browser.inputTimeoutMs === "number") {
46
53
  options.browserInputTimeout = String(browser.inputTimeoutMs);
47
54
  }
55
+ if (isUnset("browserApprovalWait") && typeof browser.approvalWaitMs === "number") {
56
+ options.browserApprovalWait = String(browser.approvalWaitMs);
57
+ }
48
58
  if (isUnset("browserAttachmentTimeout") && typeof browser.attachmentTimeoutMs === "number") {
49
59
  options.browserAttachmentTimeout = String(browser.attachmentTimeoutMs);
50
60
  }
@@ -76,6 +86,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
76
86
  if (isUnset("browserCookieWait") && typeof browser.cookieSyncWaitMs === "number") {
77
87
  options.browserCookieWait = String(browser.cookieSyncWaitMs);
78
88
  }
89
+ if (!attachRunningRequested && isUnset("browserCookieSync") && browser.cookieSync !== undefined) {
90
+ options.browserCookieSync = browser.cookieSync;
91
+ }
79
92
  if (!attachRunningRequested && isUnset("browserHeadless") && browser.headless !== undefined) {
80
93
  options.browserHeadless = browser.headless;
81
94
  }
@@ -2,9 +2,10 @@ import fs from "node:fs/promises";
2
2
  import { createHash } from "node:crypto";
3
3
  import chalk from "chalk";
4
4
  import { sessionStore } from "../sessionStore.js";
5
- import { collectChatGptTabs, DEFAULT_REMOTE_CHROME_HOST, DEFAULT_REMOTE_CHROME_PORT, extractConversationIdFromUrl, formatBrowserTabState, harvestChatGptTab, sessionMatchesTab, } from "../browser/liveTabs.js";
5
+ import { collectChatGptTabs, DEFAULT_REMOTE_CHROME_HOST, DEFAULT_REMOTE_CHROME_PORT, formatBrowserTabState, harvestChatGptTab, sessionMatchesTab, } from "../browser/liveTabs.js";
6
6
  import { isRecoveredConversationHarvestReady, recoverConversationTab, } from "../browser/recoverConversation.js";
7
7
  import { resolveOutputPath } from "./writeOutputPath.js";
8
+ import { persistBrowserHarvest } from "./harvestIntegrity.js";
8
9
  const LIVE_POLL_MS = 2000;
9
10
  const DEFAULT_STALL_THRESHOLD_MS = 60_000;
10
11
  function isRecoverableMissingTabError(message) {
@@ -85,35 +86,17 @@ function resolveSessionTabRef(meta) {
85
86
  export function resolveSessionTabRefForTest(meta) {
86
87
  return resolveSessionTabRef(meta);
87
88
  }
88
- async function persistHarvest(sessionId, meta, harvested) {
89
- const hash = createHash("sha1")
90
- .update(harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "")
91
- .digest("hex");
92
- const browser = {
93
- ...(meta.browser ?? {}),
94
- harvest: {
95
- targetId: harvested.targetId,
96
- url: harvested.url,
97
- conversationId: harvested.conversationId ?? extractConversationIdFromUrl(harvested.url),
98
- harvestedAt: new Date().toISOString(),
99
- assistantHash: hash,
100
- state: harvested.state,
101
- stopExists: harvested.stopExists,
102
- sendExists: harvested.sendExists,
103
- assistantCount: harvested.assistantCount,
104
- currentModelLabel: harvested.currentModelLabel,
105
- lastAssistantSnippet: harvested.lastAssistantSnippet,
106
- },
107
- };
108
- await sessionStore.updateSession(sessionId, { browser });
109
- }
110
- function printHarvestSummary(sessionId, harvested) {
89
+ function printHarvestSummary(sessionId, harvested, integrity) {
111
90
  console.log(chalk.bold(`Session: ${sessionId}`));
112
91
  console.log(`Target: ${harvested.targetId}`);
113
92
  console.log(`State: ${formatBrowserTabState(harvested)}`);
114
93
  console.log(`Model: ${harvested.currentModelLabel || "(unknown)"}`);
115
94
  console.log(`URL: ${harvested.url}`);
116
95
  console.log(`Assistant turns: ${harvested.assistantCount}`);
96
+ console.log(`Capture identity: ${integrity.status}${integrity.explicitTarget ? " (explicit target)" : ""}`);
97
+ if (integrity.status === "mismatch") {
98
+ console.log(chalk.yellow("Explicit harvest target differs from the saved capture; original artifacts are unchanged."));
99
+ }
117
100
  console.log(`Signals: stop=${harvested.stopExists ? "yes" : "no"} send=${harvested.sendExists ? "yes" : "no"}`);
118
101
  if (harvested.lastUserSnippet) {
119
102
  console.log(`Last user: ${harvested.lastUserSnippet}`);
@@ -207,8 +190,8 @@ export async function harvestSessionBrowserOutput(sessionId, options = {}) {
207
190
  stallWindowMs: options.stallWindowMs,
208
191
  });
209
192
  }
210
- await persistHarvest(sessionId, meta, harvested);
211
- printHarvestSummary(sessionId, harvested);
193
+ const integrity = await persistBrowserHarvest(sessionId, harvested, Boolean(options.browserTabRef));
194
+ printHarvestSummary(sessionId, harvested, integrity);
212
195
  const output = harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "";
213
196
  if (options.writeOutputPath) {
214
197
  await maybeWriteHarvestOutput(options.writeOutputPath, meta.cwd ?? process.cwd(), output);
@@ -287,8 +270,8 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
287
270
  const statusLine = `[${new Date().toISOString()}] state=${harvested.state} stop=${harvested.stopExists ? "yes" : "no"} ` +
288
271
  `send=${harvested.sendExists ? "yes" : "no"} model=${harvested.currentModelLabel || "(unknown)"} ` +
289
272
  `snippet=${snippet(harvested.lastAssistantSnippet || fullText, 160)}`;
273
+ await persistBrowserHarvest(sessionId, harvested, Boolean(options.browserTabRef));
290
274
  console.log(statusLine);
291
- await persistHarvest(sessionId, meta, harvested);
292
275
  }
293
276
  const derivedState = harvested.stopExists
294
277
  ? Date.now() - unchangedSince >= stallThresholdMs
@@ -304,8 +287,8 @@ export async function liveTailSessionBrowserOutput(sessionId, options = {}) {
304
287
  ...harvested,
305
288
  state: derivedState,
306
289
  };
307
- await persistHarvest(sessionId, meta, finalHarvest);
308
- printHarvestSummary(sessionId, finalHarvest);
290
+ const integrity = await persistBrowserHarvest(sessionId, finalHarvest, Boolean(options.browserTabRef));
291
+ printHarvestSummary(sessionId, finalHarvest, integrity);
309
292
  const output = finalHarvest.lastAssistantMarkdown ?? finalHarvest.lastAssistantText ?? "";
310
293
  if (options.writeOutputPath) {
311
294
  await maybeWriteHarvestOutput(options.writeOutputPath, meta.cwd ?? process.cwd(), output);
@@ -0,0 +1,69 @@
1
+ import { spawn } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ export function resolveOracleCliEntrypoint(moduleUrl = import.meta.url) {
4
+ const extension = fileURLToPath(moduleUrl).endsWith(".ts") ? "ts" : "js";
5
+ return fileURLToPath(new URL(`../../bin/oracle-cli.${extension}`, moduleUrl));
6
+ }
7
+ export function buildDetachedSessionSpawnSpec({ sessionId, cliEntrypoint = resolveOracleCliEntrypoint(), env = process.env, nodeExecutable = process.execPath, }) {
8
+ return {
9
+ command: nodeExecutable,
10
+ args: ["--", cliEntrypoint, "--exec-session", sessionId],
11
+ options: {
12
+ detached: true,
13
+ stdio: ["pipe", "ignore", "ignore"],
14
+ env: {
15
+ ...env,
16
+ ORACLE_DETACHED_START_GATE: "1",
17
+ },
18
+ windowsHide: true,
19
+ },
20
+ };
21
+ }
22
+ export function launchDetachedSession({ sessionId, cliEntrypoint, env, nodeExecutable, prepare, spawnProcess = spawn, }) {
23
+ return new Promise((resolve, reject) => {
24
+ let child;
25
+ try {
26
+ const spec = buildDetachedSessionSpawnSpec({
27
+ sessionId,
28
+ cliEntrypoint,
29
+ env,
30
+ nodeExecutable,
31
+ });
32
+ child = spawnProcess(spec.command, spec.args, spec.options);
33
+ }
34
+ catch (error) {
35
+ reject(error);
36
+ return;
37
+ }
38
+ child.once("error", reject);
39
+ child.once("spawn", async () => {
40
+ if (child.pid === undefined) {
41
+ child.kill();
42
+ reject(new Error("Detached session worker started without a process ID."));
43
+ return;
44
+ }
45
+ try {
46
+ await prepare(child.pid);
47
+ if (!child.stdin) {
48
+ throw new Error("Detached session worker started without a writable start gate.");
49
+ }
50
+ const startGate = child.stdin;
51
+ await new Promise((resolveGate, rejectGate) => {
52
+ startGate.once("error", rejectGate);
53
+ startGate.end("ready\n", (error) => {
54
+ if (error)
55
+ rejectGate(error);
56
+ else
57
+ resolveGate();
58
+ });
59
+ });
60
+ child.unref();
61
+ resolve(child.pid);
62
+ }
63
+ catch (error) {
64
+ child.kill();
65
+ reject(error);
66
+ }
67
+ });
68
+ });
69
+ }
@@ -1,7 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { MODEL_CONFIGS, TOKENIZER_OPTIONS, DEFAULT_SYSTEM_PROMPT, buildPrompt, readFiles, getFileTokenStats, printFileTokenStats, } from "../oracle.js";
3
3
  import { isKnownModel } from "../oracle/modelResolver.js";
4
- import { assembleBrowserPrompt } from "../browser/prompt.js";
4
+ import { assembleBrowserPrompt, cleanupGeneratedBrowserBundles, } from "../browser/prompt.js";
5
5
  import { buildTokenEstimateSuffix, formatAttachmentLabel } from "../browser/promptSummary.js";
6
6
  import { buildCookiePlan } from "../browser/policies.js";
7
7
  import { describeBrowserControlPlan, formatBrowserControlPlan } from "../browser/controlPlan.js";
@@ -45,19 +45,24 @@ async function runBrowserDryRun({ runOptions, cwd, version, log, browserConfig,
45
45
  validateBrowserFollowUps(runOptions, browserConfig);
46
46
  const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
47
47
  const artifacts = await assemblePromptImpl(runOptions, { cwd });
48
- const suffix = buildTokenEstimateSuffix(artifacts);
49
- const displayModel = formatBrowserModelTarget({
50
- model: runOptions.model,
51
- desiredModel: browserConfig?.desiredModel,
52
- modelStrategy: browserConfig?.modelStrategy,
53
- });
54
- const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
55
- log(chalk.cyan(headerLine));
56
- logBrowserControlPlan(browserConfig, log, "dry-run");
57
- logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "dry-run");
58
- logBrowserCookieStrategy(browserConfig, log, "dry-run");
59
- logBrowserArchivePolicy(browserConfig, log, "dry-run");
60
- logBrowserFileSummary(artifacts, log, "dry-run");
48
+ try {
49
+ const suffix = buildTokenEstimateSuffix(artifacts);
50
+ const displayModel = formatBrowserModelTarget({
51
+ model: runOptions.model,
52
+ desiredModel: browserConfig?.desiredModel,
53
+ modelStrategy: browserConfig?.modelStrategy,
54
+ });
55
+ const headerLine = `[dry-run] Oracle (${version}) would launch browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
56
+ log(chalk.cyan(headerLine));
57
+ logBrowserControlPlan(browserConfig, log, "dry-run");
58
+ logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "dry-run");
59
+ logBrowserCookieStrategy(browserConfig, log, "dry-run");
60
+ logBrowserArchivePolicy(browserConfig, log, "dry-run");
61
+ logBrowserFileSummary(artifacts, log, "dry-run");
62
+ }
63
+ finally {
64
+ await cleanupGeneratedBrowserBundles(artifacts);
65
+ }
61
66
  }
62
67
  function logBrowserControlPlan(browserConfig, log, label) {
63
68
  const plan = describeBrowserControlPlan(browserConfig);
@@ -100,41 +105,47 @@ export async function runBrowserPreview({ runOptions, cwd, version, previewMode,
100
105
  validateBrowserFollowUps(runOptions, browserConfig);
101
106
  const assemblePromptImpl = deps.assembleBrowserPromptImpl ?? assembleBrowserPrompt;
102
107
  const artifacts = await assemblePromptImpl(runOptions, { cwd });
103
- const suffix = buildTokenEstimateSuffix(artifacts);
104
- const displayModel = formatBrowserModelTarget({
105
- model: runOptions.model,
106
- desiredModel: browserConfig?.desiredModel,
107
- modelStrategy: browserConfig?.modelStrategy,
108
- });
109
- const headerLine = `[preview] Oracle (${version}) browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
110
- log(chalk.cyan(headerLine));
111
- logBrowserControlPlan(browserConfig, log, "preview");
112
- logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "preview");
113
- logBrowserFileSummary(artifacts, log, "preview");
114
- if (previewMode === "json" || previewMode === "full") {
115
- const attachmentSummary = artifacts.attachments.map((attachment) => ({
116
- path: attachment.path,
117
- displayPath: attachment.displayPath,
118
- sizeBytes: attachment.sizeBytes,
119
- }));
120
- const previewPayload = {
108
+ try {
109
+ const suffix = buildTokenEstimateSuffix(artifacts);
110
+ const displayModel = formatBrowserModelTarget({
121
111
  model: runOptions.model,
122
- engine: "browser",
123
- composerText: artifacts.composerText,
124
- attachments: attachmentSummary,
125
- inlineFileCount: artifacts.inlineFileCount,
126
- bundled: artifacts.bundled,
127
- tokenEstimate: artifacts.estimatedInputTokens,
128
- browserFollowUps: runOptions.browserFollowUps ?? [],
129
- };
130
- log("");
131
- log(chalk.bold("Preview JSON"));
132
- log(JSON.stringify(previewPayload, null, 2));
112
+ desiredModel: browserConfig?.desiredModel,
113
+ modelStrategy: browserConfig?.modelStrategy,
114
+ });
115
+ const headerLine = `[preview] Oracle (${version}) browser mode (${displayModel}) with ~${artifacts.estimatedInputTokens.toLocaleString()} tokens${suffix}.`;
116
+ log(chalk.cyan(headerLine));
117
+ logBrowserControlPlan(browserConfig, log, "preview");
118
+ logBrowserFollowUpSummary(runOptions.browserFollowUps, log, "preview");
119
+ logBrowserCookieStrategy(browserConfig, log, "preview");
120
+ logBrowserFileSummary(artifacts, log, "preview");
121
+ if (previewMode === "json" || previewMode === "full") {
122
+ const attachmentSummary = artifacts.attachments.map((attachment) => ({
123
+ path: attachment.path,
124
+ displayPath: attachment.displayPath,
125
+ sizeBytes: attachment.sizeBytes,
126
+ }));
127
+ const previewPayload = {
128
+ model: runOptions.model,
129
+ engine: "browser",
130
+ composerText: artifacts.composerText,
131
+ attachments: attachmentSummary,
132
+ inlineFileCount: artifacts.inlineFileCount,
133
+ bundled: artifacts.bundled,
134
+ tokenEstimate: artifacts.estimatedInputTokens,
135
+ browserFollowUps: runOptions.browserFollowUps ?? [],
136
+ };
137
+ log("");
138
+ log(chalk.bold("Preview JSON"));
139
+ log(JSON.stringify(previewPayload, null, 2));
140
+ }
141
+ if (previewMode === "full") {
142
+ log("");
143
+ log(chalk.bold("Composer Text"));
144
+ log(artifacts.composerText || chalk.dim("(empty prompt)"));
145
+ }
133
146
  }
134
- if (previewMode === "full") {
135
- log("");
136
- log(chalk.bold("Composer Text"));
137
- log(artifacts.composerText || chalk.dim("(empty prompt)"));
147
+ finally {
148
+ await cleanupGeneratedBrowserBundles(artifacts);
138
149
  }
139
150
  }
140
151
  function logBrowserFollowUpSummary(followUps, log, label) {
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { sessionStore } from "../sessionStore.js";
6
+ import { extractConversationIdFromUrl } from "../browser/liveTabs.js";
7
+ import { BrowserAutomationError } from "../oracle/errors.js";
8
+ const INTEGRITY_WARNING = "browser-harvest-integrity";
9
+ const INTEGRITY_MESSAGE = "Harvest or saved conversation identities conflict. Original artifacts were preserved; inspect browser.harvest.integrity before using the answer.";
10
+ async function readTranscriptConversation(filename) {
11
+ let file;
12
+ try {
13
+ const stat = await fs.lstat(filename);
14
+ if (!stat.isFile() || stat.isSymbolicLink())
15
+ return undefined;
16
+ file = await fs.open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
17
+ if (!(await file.stat()).isFile())
18
+ return undefined;
19
+ const buffer = Buffer.alloc(2_048);
20
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
21
+ const match = buffer
22
+ .subarray(0, bytesRead)
23
+ .toString("utf8")
24
+ .match(/^# Oracle Browser Transcript\r?\n\r?\nConversation: ([^\r\n]+)\r?\n/);
25
+ return match ? extractConversationIdFromUrl(match[1]) : undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ finally {
31
+ await file?.close();
32
+ }
33
+ }
34
+ export async function persistBrowserHarvest(sessionId, harvested, explicitTarget = false) {
35
+ // Harvesting can take time; compare against the latest saved capture, not the initial read.
36
+ const meta = await sessionStore.readSession(sessionId);
37
+ if (!meta)
38
+ throw new Error(`No session found with ID ${sessionId}.`);
39
+ const captured = [];
40
+ const unverifiedSources = [];
41
+ const add = (source, id) => {
42
+ if (id)
43
+ captured.push({ source, conversationId: id });
44
+ };
45
+ const addUrl = (source, url, expectsConversation = true) => {
46
+ if (!url)
47
+ return;
48
+ const id = extractConversationIdFromUrl(url);
49
+ add(source, id);
50
+ if (!id && expectsConversation)
51
+ unverifiedSources.push(source);
52
+ };
53
+ add("runtime", meta.browser?.runtime?.conversationId);
54
+ addUrl("runtime-url", meta.browser?.runtime?.tabUrl);
55
+ addUrl("archive", meta.browser?.archive?.conversationUrl);
56
+ for (const [index, artifact] of (meta.artifacts ?? []).entries()) {
57
+ addUrl(`artifact:${index}`, artifact.sourceUrl, artifact.kind === "transcript" || artifact.kind === "deep-research-report");
58
+ }
59
+ const artifactDir = path.join((await sessionStore.getPaths(sessionId)).dir, "artifacts");
60
+ const directoryStat = await fs.lstat(artifactDir).catch(() => null);
61
+ if (directoryStat?.isDirectory() && !directoryStat.isSymbolicLink()) {
62
+ const transcripts = new Map([[path.join(artifactDir, "transcript.md"), false]]);
63
+ for (const artifact of meta.artifacts ?? []) {
64
+ if (artifact.kind !== "transcript")
65
+ continue;
66
+ const candidate = path.isAbsolute(artifact.path)
67
+ ? artifact.path
68
+ : path.resolve(artifactDir, "..", artifact.path);
69
+ // Stored paths must remain direct children of this session's artifact directory.
70
+ if (path.dirname(candidate) === artifactDir)
71
+ transcripts.set(candidate, true);
72
+ else
73
+ unverifiedSources.push("external-transcript-path");
74
+ }
75
+ for (const [filename, expected] of transcripts) {
76
+ const source = `transcript:${path.basename(filename)}`;
77
+ const id = await readTranscriptConversation(filename);
78
+ add(source, id);
79
+ if (!id && (expected || (await fs.lstat(filename).catch(() => null)))) {
80
+ unverifiedSources.push(source);
81
+ }
82
+ }
83
+ }
84
+ else if (directoryStat || meta.artifacts?.some((artifact) => artifact.kind === "transcript")) {
85
+ unverifiedSources.push("artifact-directory");
86
+ }
87
+ const observedConversationId = harvested.conversationId ?? extractConversationIdFromUrl(harvested.url);
88
+ const capturedIds = new Set(captured.map((entry) => entry.conversationId));
89
+ const previousIntegrity = meta.browser?.harvest?.integrity;
90
+ const mismatch = capturedIds.size > 1 ||
91
+ (observedConversationId
92
+ ? captured.some((entry) => entry.conversationId !== observedConversationId)
93
+ : previousIntegrity?.status === "mismatch");
94
+ const integrity = {
95
+ status: mismatch
96
+ ? "mismatch"
97
+ : observedConversationId && captured.length && unverifiedSources.length === 0
98
+ ? "matched"
99
+ : "unverified",
100
+ observedConversationId,
101
+ captured,
102
+ unverifiedSources,
103
+ explicitTarget,
104
+ previousHarvestConversationId: meta.browser?.harvest?.conversationId ?? previousIntegrity?.previousHarvestConversationId,
105
+ };
106
+ const warnings = (meta.browser?.warnings ?? []).filter((warning) => warning.code !== INTEGRITY_WARNING);
107
+ if (mismatch) {
108
+ warnings.push({ code: INTEGRITY_WARNING, severity: "warning", message: INTEGRITY_MESSAGE });
109
+ }
110
+ await sessionStore.updateSession(sessionId, {
111
+ browser: {
112
+ ...meta.browser,
113
+ warnings,
114
+ harvest: {
115
+ targetId: harvested.targetId,
116
+ url: harvested.url,
117
+ conversationId: observedConversationId,
118
+ harvestedAt: new Date().toISOString(),
119
+ assistantHash: createHash("sha1")
120
+ .update(harvested.lastAssistantMarkdown ?? harvested.lastAssistantText ?? "")
121
+ .digest("hex"),
122
+ state: harvested.state,
123
+ stopExists: harvested.stopExists,
124
+ sendExists: harvested.sendExists,
125
+ assistantCount: harvested.assistantCount,
126
+ currentModelLabel: harvested.currentModelLabel,
127
+ lastAssistantSnippet: harvested.lastAssistantSnippet,
128
+ integrity,
129
+ },
130
+ },
131
+ });
132
+ if (mismatch && !explicitTarget) {
133
+ throw new BrowserAutomationError(INTEGRITY_MESSAGE, {
134
+ stage: "harvest-integrity",
135
+ code: "conversation-identity-mismatch",
136
+ integrity,
137
+ });
138
+ }
139
+ return integrity;
140
+ }
@@ -74,9 +74,11 @@ export async function buildProjectSourcesBrowserConfig({ options, projectUrl, co
74
74
  const manualLoginCookieSync = flagConfig.manualLoginCookieSync ?? configuredBrowser.manualLoginCookieSync;
75
75
  const cookieSync = flagConfig.cookieSync === false
76
76
  ? false
77
- : manualLogin
78
- ? manualLoginCookieSync === true
79
- : configuredBrowser.cookieSync;
77
+ : flagConfig.cookieSync === true
78
+ ? true
79
+ : manualLogin
80
+ ? manualLoginCookieSync === true
81
+ : configuredBrowser.cookieSync === true;
80
82
  return {
81
83
  ...configuredBrowser,
82
84
  ...flagConfig,
@@ -0,0 +1,24 @@
1
+ import { resolveBrowserApprovalWait } from "../browser/config.js";
2
+ import { parseRemoteChromeTarget } from "./browserConfig.js";
3
+ import { applyBrowserDefaultsFromConfig } from "./browserDefaults.js";
4
+ export function buildServeBrowserConfig(options, config) {
5
+ const flags = {
6
+ browserAttachRunning: options.browserAttachRunning,
7
+ remoteChrome: options.remoteChrome,
8
+ browserApprovalWait: options.browserApprovalWait ??
9
+ (process.env.ORACLE_BROWSER_APPROVAL_WAIT?.trim() || undefined),
10
+ };
11
+ const browser = config.browser;
12
+ applyBrowserDefaultsFromConfig(flags, {
13
+ browser: {
14
+ attachRunning: browser?.attachRunning,
15
+ remoteChrome: browser?.remoteChrome,
16
+ approvalWaitMs: browser?.approvalWaitMs,
17
+ },
18
+ }, (key) => (flags[key] === undefined ? undefined : "cli"));
19
+ return {
20
+ attachRunning: flags.browserAttachRunning ?? false,
21
+ remoteChrome: flags.remoteChrome ? parseRemoteChromeTarget(flags.remoteChrome) : null,
22
+ approvalWaitMs: resolveBrowserApprovalWait(flags.browserApprovalWait),
23
+ };
24
+ }