@steipete/oracle 0.18.0 → 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 (159) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +65 -63
  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 +119 -16
  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 +13 -0
  26. package/dist/src/browser/detect.js +1 -1
  27. package/dist/src/browser/index.js +444 -261
  28. package/dist/src/browser/modelDisplay.js +9 -0
  29. package/dist/src/browser/pageActions.js +2 -2
  30. package/dist/src/browser/profileState.js +114 -9
  31. package/dist/src/browser/projectSourcesRunner.js +83 -15
  32. package/dist/src/browser/prompt.js +272 -113
  33. package/dist/src/browser/providers/chatgptDomProvider.js +2 -0
  34. package/dist/src/browser/reattach.js +10 -3
  35. package/dist/src/browser/sessionRunner.js +71 -11
  36. package/dist/src/browser/tabLeaseRegistry.js +414 -38
  37. package/dist/src/browser/utils.js +20 -2
  38. package/dist/src/cli/browserConfig.js +3 -1
  39. package/dist/src/cli/browserDefaults.js +10 -0
  40. package/dist/src/cli/browserTabs.js +12 -29
  41. package/dist/src/cli/detachedSession.js +69 -0
  42. package/dist/src/cli/dryRun.js +58 -48
  43. package/dist/src/cli/harvestIntegrity.js +140 -0
  44. package/dist/src/cli/serveBrowserConfig.js +24 -0
  45. package/dist/src/cli/sessionDisplay.js +9 -2
  46. package/dist/src/cli/sessionRunner.js +91 -7
  47. package/dist/src/config.js +1 -0
  48. package/dist/src/gemini-web/client.js +67 -21
  49. package/dist/src/gemini-web/executor.js +4 -0
  50. package/dist/src/gemini-web/http.js +15 -0
  51. package/dist/src/gemini-web/models.js +15 -11
  52. package/dist/src/mcp/server.js +11 -14
  53. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  54. package/dist/src/mcp/tools/consult.js +84 -22
  55. package/dist/src/mcp/tools/projectSources.js +4 -6
  56. package/dist/src/mcp/tools/sessionResources.js +1 -1
  57. package/dist/src/mcp/tools/sessions.js +2 -2
  58. package/dist/src/mcp/tools/wait.js +182 -0
  59. package/dist/src/mcp/types.js +7 -0
  60. package/dist/src/oracle/errors.js +11 -0
  61. package/dist/src/oracle/providerRoutePlan.js +3 -2
  62. package/dist/src/remote/client.js +70 -6
  63. package/dist/src/remote/health.js +6 -2
  64. package/dist/src/remote/runSlots.js +83 -0
  65. package/dist/src/remote/server.js +345 -71
  66. package/dist/src/sessionManager.js +36 -1
  67. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  68. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  69. package/package.json +25 -23
  70. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  71. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  72. package/dist/bin/oracle.js +0 -683
  73. package/dist/docs-site/.nojekyll +0 -0
  74. package/dist/docs-site/CNAME +0 -1
  75. package/dist/docs-site/RELEASING.html +0 -410
  76. package/dist/docs-site/agents.html +0 -374
  77. package/dist/docs-site/anthropic.html +0 -368
  78. package/dist/docs-site/bridge.html +0 -400
  79. package/dist/docs-site/browser-mode.html +0 -588
  80. package/dist/docs-site/chromium-forks.html +0 -347
  81. package/dist/docs-site/cli-reference.html +0 -346
  82. package/dist/docs-site/configuration.html +0 -436
  83. package/dist/docs-site/favicon.svg +0 -14
  84. package/dist/docs-site/followup.html +0 -368
  85. package/dist/docs-site/gemini.html +0 -379
  86. package/dist/docs-site/grok.html +0 -325
  87. package/dist/docs-site/index.html +0 -360
  88. package/dist/docs-site/install.html +0 -335
  89. package/dist/docs-site/linux.html +0 -321
  90. package/dist/docs-site/llms.txt +0 -43
  91. package/dist/docs-site/manual-tests.html +0 -595
  92. package/dist/docs-site/mcp.html +0 -368
  93. package/dist/docs-site/multimodel.html +0 -364
  94. package/dist/docs-site/mythical-pro-agents.html +0 -360
  95. package/dist/docs-site/notifier.html +0 -338
  96. package/dist/docs-site/openai-endpoints.html +0 -387
  97. package/dist/docs-site/openrouter.html +0 -344
  98. package/dist/docs-site/quickstart.html +0 -369
  99. package/dist/docs-site/refactor/ux.html +0 -532
  100. package/dist/docs-site/sessions.html +0 -388
  101. package/dist/docs-site/social-card.png +0 -0
  102. package/dist/docs-site/social-card.svg +0 -79
  103. package/dist/docs-site/spec.html +0 -363
  104. package/dist/docs-site/testing.html +0 -320
  105. package/dist/docs-site/tui-debug.html +0 -326
  106. package/dist/docs-site/windows-work.html +0 -323
  107. package/dist/docs-site/windows.html +0 -320
  108. package/dist/markdansi/types/index.js +0 -4
  109. package/dist/oracle/bin/oracle-cli.js +0 -472
  110. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  111. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  112. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  113. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  114. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  115. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  116. package/dist/oracle/src/browser/config.js +0 -33
  117. package/dist/oracle/src/browser/constants.js +0 -40
  118. package/dist/oracle/src/browser/cookies.js +0 -210
  119. package/dist/oracle/src/browser/domDebug.js +0 -36
  120. package/dist/oracle/src/browser/index.js +0 -331
  121. package/dist/oracle/src/browser/pageActions.js +0 -5
  122. package/dist/oracle/src/browser/prompt.js +0 -88
  123. package/dist/oracle/src/browser/promptSummary.js +0 -20
  124. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  125. package/dist/oracle/src/browser/types.js +0 -1
  126. package/dist/oracle/src/browser/utils.js +0 -62
  127. package/dist/oracle/src/browserMode.js +0 -1
  128. package/dist/oracle/src/cli/browserConfig.js +0 -44
  129. package/dist/oracle/src/cli/dryRun.js +0 -59
  130. package/dist/oracle/src/cli/engine.js +0 -17
  131. package/dist/oracle/src/cli/errorUtils.js +0 -9
  132. package/dist/oracle/src/cli/help.js +0 -70
  133. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  134. package/dist/oracle/src/cli/options.js +0 -103
  135. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  136. package/dist/oracle/src/cli/rootAlias.js +0 -30
  137. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  138. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  139. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  140. package/dist/oracle/src/heartbeat.js +0 -43
  141. package/dist/oracle/src/oracle/client.js +0 -48
  142. package/dist/oracle/src/oracle/config.js +0 -29
  143. package/dist/oracle/src/oracle/errors.js +0 -101
  144. package/dist/oracle/src/oracle/files.js +0 -220
  145. package/dist/oracle/src/oracle/format.js +0 -33
  146. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  147. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  148. package/dist/oracle/src/oracle/request.js +0 -48
  149. package/dist/oracle/src/oracle/run.js +0 -444
  150. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  151. package/dist/oracle/src/oracle/types.js +0 -1
  152. package/dist/oracle/src/oracle.js +0 -9
  153. package/dist/oracle/src/sessionManager.js +0 -205
  154. package/dist/oracle/src/version.js +0 -39
  155. package/dist/scripts/chrome/browser-tools.js +0 -295
  156. package/dist/src/browser/chromeCookies.js +0 -312
  157. package/dist/src/browser/keytarShim.js +0 -56
  158. package/dist/src/browser/profileSync.js +0 -141
  159. package/dist/src/browser/windowsCookies.js +0 -219
@@ -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,42 +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
- logBrowserCookieStrategy(browserConfig, log, "preview");
114
- logBrowserFileSummary(artifacts, log, "preview");
115
- if (previewMode === "json" || previewMode === "full") {
116
- const attachmentSummary = artifacts.attachments.map((attachment) => ({
117
- path: attachment.path,
118
- displayPath: attachment.displayPath,
119
- sizeBytes: attachment.sizeBytes,
120
- }));
121
- const previewPayload = {
108
+ try {
109
+ const suffix = buildTokenEstimateSuffix(artifacts);
110
+ const displayModel = formatBrowserModelTarget({
122
111
  model: runOptions.model,
123
- engine: "browser",
124
- composerText: artifacts.composerText,
125
- attachments: attachmentSummary,
126
- inlineFileCount: artifacts.inlineFileCount,
127
- bundled: artifacts.bundled,
128
- tokenEstimate: artifacts.estimatedInputTokens,
129
- browserFollowUps: runOptions.browserFollowUps ?? [],
130
- };
131
- log("");
132
- log(chalk.bold("Preview JSON"));
133
- 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
+ }
134
146
  }
135
- if (previewMode === "full") {
136
- log("");
137
- log(chalk.bold("Composer Text"));
138
- log(artifacts.composerText || chalk.dim("(empty prompt)"));
147
+ finally {
148
+ await cleanupGeneratedBrowserBundles(artifacts);
139
149
  }
140
150
  }
141
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
+ }
@@ -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
+ }
@@ -12,7 +12,7 @@ import { estimateTokenCount } from "../browser/utils.js";
12
12
  import { formatSessionTableHeader, formatSessionTableRow, resolveSessionCost, } from "./sessionTable.js";
13
13
  import { abbreviateResponseId, buildResponseOwnerIndex, resolveSessionLineage, } from "./sessionLineage.js";
14
14
  import { formatSessionExecutionLabel } from "./sessionLifecycle.js";
15
- import { formatBrowserModelSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
15
+ import { formatBrowserModelSelectionEvidence, formatBrowserThinkingSelectionEvidence, formatSessionBrowserModelWithRequestedKey, resolveSessionBrowserModelDisplayName, } from "../browser/modelDisplay.js";
16
16
  const isTty = () => Boolean(process.stdout.isTTY);
17
17
  const dim = (text) => (isTty() ? kleur.dim(text) : text);
18
18
  export const MAX_RENDER_BYTES = 200_000;
@@ -255,6 +255,7 @@ export async function attachSession(sessionId, options) {
255
255
  config: metadata.browser?.config,
256
256
  runtime,
257
257
  modelSelection: metadata.browser?.modelSelection,
258
+ thinkingSelection: metadata.browser?.thinkingSelection,
258
259
  warnings: metadata.browser?.warnings,
259
260
  },
260
261
  artifacts,
@@ -603,7 +604,9 @@ export function formatUserErrorMetadata(metadata) {
603
604
  }
604
605
  export function formatBrowserEvidence(metadata) {
605
606
  const browser = metadata.browser;
606
- if (!browser?.modelSelection && (!browser?.warnings || browser.warnings.length === 0)) {
607
+ if (!browser?.modelSelection &&
608
+ !browser?.thinkingSelection &&
609
+ (!browser?.warnings || browser.warnings.length === 0)) {
607
610
  return null;
608
611
  }
609
612
  const lines = [];
@@ -611,6 +614,10 @@ export function formatBrowserEvidence(metadata) {
611
614
  if (evidence) {
612
615
  lines.push(`model ${formatBrowserModelSelectionEvidence(evidence, metadata.model)}`);
613
616
  }
617
+ const thinkingEvidence = browser.thinkingSelection;
618
+ if (thinkingEvidence) {
619
+ lines.push(`effort ${formatBrowserThinkingSelectionEvidence(thinkingEvidence)}`);
620
+ }
614
621
  for (const warning of browser.warnings ?? []) {
615
622
  lines.push(`warning ${warning.code}: ${warning.message}`);
616
623
  }
@@ -1,4 +1,5 @@
1
1
  import kleur from "kleur";
2
+ import { constants as fsConstants } from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { runOracle, OracleResponseError, OracleTransportError, extractResponseMetadata, asOracleUserError, extractTextOutput, classifyProviderFailure, } from "../oracle.js";
@@ -23,6 +24,7 @@ import { cwd as getCwd } from "node:process";
23
24
  import { resumeBrowserSession } from "../browser/reattach.js";
24
25
  import { hasRecoverableChatGptConversation } from "../browser/reattachability.js";
25
26
  import { estimateTokenCount } from "../browser/utils.js";
27
+ import { computeFileSha256, sanitizeArtifactFilename } from "../browser/artifacts.js";
26
28
  import { formatElapsed } from "../oracle/format.js";
27
29
  import { formatBrowserReattachGuidance } from "./reattachGuidance.js";
28
30
  const isTty = process.stdout.isTTY;
@@ -78,7 +80,13 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
78
80
  cwd,
79
81
  log,
80
82
  }, runnerDeps);
81
- await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
83
+ const writtenOutputPath = await writeAssistantOutput(runOptions.writeOutputPath, result.answerText ?? "", log);
84
+ const outputArtifacts = await copyBrowserOutputArtifacts({
85
+ outputPath: writtenOutputPath,
86
+ savedFiles: runOptions.writeArtifacts ? result.savedFiles : undefined,
87
+ log,
88
+ });
89
+ const browserWarnings = [...(result.warnings ?? []), ...outputArtifacts.warnings];
82
90
  await sendSessionNotification({
83
91
  sessionId: sessionMeta.id,
84
92
  sessionName: sessionMeta.options?.slug ?? sessionMeta.id,
@@ -105,9 +113,10 @@ export async function performSessionRun({ sessionMeta, runOptions, mode, browser
105
113
  runtime: result.runtime,
106
114
  archive: result.archive,
107
115
  modelSelection: result.modelSelection,
108
- warnings: result.warnings,
116
+ thinkingSelection: result.thinkingSelection,
117
+ warnings: browserWarnings.length > 0 ? browserWarnings : undefined,
109
118
  },
110
- artifacts: mergeArtifacts(sessionMeta.artifacts, result.artifacts),
119
+ artifacts: mergeArtifacts(sessionMeta.artifacts, mergeArtifacts(result.artifacts, outputArtifacts.artifacts)),
111
120
  response: undefined,
112
121
  transport: undefined,
113
122
  error: undefined,
@@ -848,6 +857,81 @@ function resolveSessionPath(sessionDir, targetPath) {
848
857
  }
849
858
  return path.join(sessionDir, targetPath);
850
859
  }
860
+ async function copyBrowserOutputArtifacts(params) {
861
+ const artifacts = [];
862
+ const warnings = [];
863
+ if (!params.outputPath || !params.savedFiles?.length) {
864
+ return { artifacts, warnings };
865
+ }
866
+ const outputDir = path.dirname(params.outputPath);
867
+ for (const savedFile of params.savedFiles) {
868
+ const filename = sanitizeArtifactFilename(savedFile.filename ?? path.basename(savedFile.path), "artifact.bin");
869
+ let copied;
870
+ try {
871
+ copied = await copyFileWithoutOverwrite(savedFile.path, path.join(outputDir, filename));
872
+ const [stat, sha256] = await Promise.all([fs.stat(copied), computeFileSha256(copied)]);
873
+ if (savedFile.sizeBytes != null && stat.size !== savedFile.sizeBytes) {
874
+ throw new Error(`size mismatch (expected ${savedFile.sizeBytes}, received ${stat.size})`);
875
+ }
876
+ if (savedFile.sha256 && sha256 !== savedFile.sha256) {
877
+ throw new Error(`sha256 mismatch (expected ${savedFile.sha256}, received ${sha256})`);
878
+ }
879
+ const artifact = {
880
+ kind: "file",
881
+ path: copied,
882
+ label: `${savedFile.label ?? filename} (write-output)`,
883
+ mimeType: savedFile.mimeType,
884
+ sizeBytes: stat.size,
885
+ sourceUrl: savedFile.sourceUrl,
886
+ sha256,
887
+ validation: savedFile.validation,
888
+ transfer: { status: "not-needed" },
889
+ origin: { mode: "local" },
890
+ };
891
+ artifacts.push(artifact);
892
+ params.log(dim(`[browser] Saved write-output file artifact to ${copied} sha256=${sha256}`));
893
+ }
894
+ catch (error) {
895
+ if (copied) {
896
+ await fs.unlink(copied).catch(() => undefined);
897
+ }
898
+ const reason = error instanceof Error ? error.message : String(error);
899
+ const message = `Failed to copy saved file artifact ${savedFile.path} beside ${params.outputPath}: ${reason}`;
900
+ params.log(dim(`[browser] ${message}`));
901
+ warnings.push({
902
+ code: "browser-output-artifact-copy-failed",
903
+ severity: "warning",
904
+ message,
905
+ details: {
906
+ sourcePath: savedFile.path,
907
+ outputPath: params.outputPath,
908
+ sha256: savedFile.sha256,
909
+ },
910
+ });
911
+ }
912
+ }
913
+ return { artifacts, warnings };
914
+ }
915
+ async function copyFileWithoutOverwrite(sourcePath, baseTargetPath) {
916
+ const ext = path.extname(baseTargetPath);
917
+ const stem = ext ? path.basename(baseTargetPath, ext) : path.basename(baseTargetPath);
918
+ const dir = path.dirname(baseTargetPath);
919
+ let suffix = 1;
920
+ for (;;) {
921
+ const targetPath = suffix === 1 ? baseTargetPath : path.join(dir, `${stem}-${suffix}${ext}`);
922
+ try {
923
+ await fs.copyFile(sourcePath, targetPath, fsConstants.COPYFILE_EXCL);
924
+ return targetPath;
925
+ }
926
+ catch (error) {
927
+ if (isErrorCode(error, "EEXIST")) {
928
+ suffix += 1;
929
+ continue;
930
+ }
931
+ throw error;
932
+ }
933
+ }
934
+ }
851
935
  async function writeAssistantOutput(targetPath, content, log) {
852
936
  if (!targetPath)
853
937
  return;
@@ -1057,11 +1141,11 @@ export function deriveModelOutputPath(basePath, model) {
1057
1141
  const suffix = ext.length > 0 ? `${stem}.${model}${ext}` : `${stem}.${model}`;
1058
1142
  return path.join(dir, suffix);
1059
1143
  }
1144
+ function isErrorCode(error, expected) {
1145
+ return error instanceof Error && error.code === expected;
1146
+ }
1060
1147
  function isPermissionError(error) {
1061
- if (!(error instanceof Error))
1062
- return false;
1063
- const code = error.code;
1064
- return code === "EACCES" || code === "EPERM";
1148
+ return isErrorCode(error, "EACCES") || isErrorCode(error, "EPERM");
1065
1149
  }
1066
1150
  function buildFallbackPath(original) {
1067
1151
  const ext = path.extname(original);
@@ -135,6 +135,7 @@ function sanitizeProjectConfig(config) {
135
135
  "attachRunning",
136
136
  "timeoutMs",
137
137
  "inputTimeoutMs",
138
+ "approvalWaitMs",
138
139
  "attachmentTimeoutMs",
139
140
  "assistantRecheckDelayMs",
140
141
  "assistantRecheckTimeoutMs",