@steipete/oracle 0.18.0 → 0.20.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 (169) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +76 -76
  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 +170 -45
  13. package/dist/src/browser/actions/modelSelection.js +91 -4
  14. package/dist/src/browser/actions/navigation.js +1 -1
  15. package/dist/src/browser/actions/promptComposer.js +364 -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 +156 -16
  19. package/dist/src/browser/actions/webSearch.js +96 -0
  20. package/dist/src/browser/attachRunning.js +46 -2
  21. package/dist/src/browser/attachmentValidation.js +53 -0
  22. package/dist/src/browser/cancellation.js +105 -0
  23. package/dist/src/browser/chatgptImages.js +2 -0
  24. package/dist/src/browser/chatgptThrottle.js +104 -0
  25. package/dist/src/browser/chromeLifecycle.js +272 -47
  26. package/dist/src/browser/config.js +14 -1
  27. package/dist/src/browser/detect.js +1 -1
  28. package/dist/src/browser/index.js +525 -274
  29. package/dist/src/browser/liveTabs.js +3 -4
  30. package/dist/src/browser/modelDisplay.js +9 -0
  31. package/dist/src/browser/pageActions.js +2 -2
  32. package/dist/src/browser/profileState.js +114 -9
  33. package/dist/src/browser/projectSourcesRunner.js +83 -15
  34. package/dist/src/browser/prompt.js +272 -113
  35. package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
  36. package/dist/src/browser/reattach.js +31 -4
  37. package/dist/src/browser/recoveryTarget.js +131 -0
  38. package/dist/src/browser/sessionRunner.js +73 -11
  39. package/dist/src/browser/tabLeaseRegistry.js +434 -38
  40. package/dist/src/browser/targetClaim.js +54 -0
  41. package/dist/src/browser/utils.js +20 -2
  42. package/dist/src/cli/browserConfig.js +36 -4
  43. package/dist/src/cli/browserDefaults.js +10 -0
  44. package/dist/src/cli/browserTabs.js +15 -29
  45. package/dist/src/cli/detachedSession.js +69 -0
  46. package/dist/src/cli/dryRun.js +58 -48
  47. package/dist/src/cli/harvestIntegrity.js +140 -0
  48. package/dist/src/cli/options.js +15 -0
  49. package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
  50. package/dist/src/cli/runOptions.js +19 -4
  51. package/dist/src/cli/serveBrowserConfig.js +24 -0
  52. package/dist/src/cli/sessionDisplay.js +13 -7
  53. package/dist/src/cli/sessionRunner.js +95 -12
  54. package/dist/src/config.js +1 -0
  55. package/dist/src/gemini-web/client.js +67 -21
  56. package/dist/src/gemini-web/executor.js +4 -0
  57. package/dist/src/gemini-web/http.js +15 -0
  58. package/dist/src/gemini-web/models.js +15 -11
  59. package/dist/src/mcp/server.js +11 -14
  60. package/dist/src/mcp/tools/chatgptImage.js +4 -4
  61. package/dist/src/mcp/tools/consult.js +86 -24
  62. package/dist/src/mcp/tools/projectSources.js +4 -6
  63. package/dist/src/mcp/tools/sessionResources.js +1 -1
  64. package/dist/src/mcp/tools/sessions.js +2 -2
  65. package/dist/src/mcp/tools/wait.js +182 -0
  66. package/dist/src/mcp/types.js +8 -1
  67. package/dist/src/oracle/config.js +14 -0
  68. package/dist/src/oracle/errors.js +11 -0
  69. package/dist/src/oracle/geminiModels.js +1 -0
  70. package/dist/src/oracle/providerRoutePlan.js +3 -2
  71. package/dist/src/oracle/run.js +27 -4
  72. package/dist/src/remote/client.js +73 -6
  73. package/dist/src/remote/health.js +6 -2
  74. package/dist/src/remote/runSlots.js +83 -0
  75. package/dist/src/remote/server.js +346 -71
  76. package/dist/src/sessionManager.js +36 -1
  77. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  78. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  79. package/package.json +25 -23
  80. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  81. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  82. package/dist/bin/oracle.js +0 -683
  83. package/dist/docs-site/.nojekyll +0 -0
  84. package/dist/docs-site/CNAME +0 -1
  85. package/dist/docs-site/RELEASING.html +0 -410
  86. package/dist/docs-site/agents.html +0 -374
  87. package/dist/docs-site/anthropic.html +0 -368
  88. package/dist/docs-site/bridge.html +0 -400
  89. package/dist/docs-site/browser-mode.html +0 -588
  90. package/dist/docs-site/chromium-forks.html +0 -347
  91. package/dist/docs-site/cli-reference.html +0 -346
  92. package/dist/docs-site/configuration.html +0 -436
  93. package/dist/docs-site/favicon.svg +0 -14
  94. package/dist/docs-site/followup.html +0 -368
  95. package/dist/docs-site/gemini.html +0 -379
  96. package/dist/docs-site/grok.html +0 -325
  97. package/dist/docs-site/index.html +0 -360
  98. package/dist/docs-site/install.html +0 -335
  99. package/dist/docs-site/linux.html +0 -321
  100. package/dist/docs-site/llms.txt +0 -43
  101. package/dist/docs-site/manual-tests.html +0 -595
  102. package/dist/docs-site/mcp.html +0 -368
  103. package/dist/docs-site/multimodel.html +0 -364
  104. package/dist/docs-site/mythical-pro-agents.html +0 -360
  105. package/dist/docs-site/notifier.html +0 -338
  106. package/dist/docs-site/openai-endpoints.html +0 -387
  107. package/dist/docs-site/openrouter.html +0 -344
  108. package/dist/docs-site/quickstart.html +0 -369
  109. package/dist/docs-site/refactor/ux.html +0 -532
  110. package/dist/docs-site/sessions.html +0 -388
  111. package/dist/docs-site/social-card.png +0 -0
  112. package/dist/docs-site/social-card.svg +0 -79
  113. package/dist/docs-site/spec.html +0 -363
  114. package/dist/docs-site/testing.html +0 -320
  115. package/dist/docs-site/tui-debug.html +0 -326
  116. package/dist/docs-site/windows-work.html +0 -323
  117. package/dist/docs-site/windows.html +0 -320
  118. package/dist/markdansi/types/index.js +0 -4
  119. package/dist/oracle/bin/oracle-cli.js +0 -472
  120. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  121. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  122. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  123. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  124. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  125. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  126. package/dist/oracle/src/browser/config.js +0 -33
  127. package/dist/oracle/src/browser/constants.js +0 -40
  128. package/dist/oracle/src/browser/cookies.js +0 -210
  129. package/dist/oracle/src/browser/domDebug.js +0 -36
  130. package/dist/oracle/src/browser/index.js +0 -331
  131. package/dist/oracle/src/browser/pageActions.js +0 -5
  132. package/dist/oracle/src/browser/prompt.js +0 -88
  133. package/dist/oracle/src/browser/promptSummary.js +0 -20
  134. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  135. package/dist/oracle/src/browser/types.js +0 -1
  136. package/dist/oracle/src/browser/utils.js +0 -62
  137. package/dist/oracle/src/browserMode.js +0 -1
  138. package/dist/oracle/src/cli/browserConfig.js +0 -44
  139. package/dist/oracle/src/cli/dryRun.js +0 -59
  140. package/dist/oracle/src/cli/engine.js +0 -17
  141. package/dist/oracle/src/cli/errorUtils.js +0 -9
  142. package/dist/oracle/src/cli/help.js +0 -70
  143. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  144. package/dist/oracle/src/cli/options.js +0 -103
  145. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  146. package/dist/oracle/src/cli/rootAlias.js +0 -30
  147. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  148. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  149. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  150. package/dist/oracle/src/heartbeat.js +0 -43
  151. package/dist/oracle/src/oracle/client.js +0 -48
  152. package/dist/oracle/src/oracle/config.js +0 -29
  153. package/dist/oracle/src/oracle/errors.js +0 -101
  154. package/dist/oracle/src/oracle/files.js +0 -220
  155. package/dist/oracle/src/oracle/format.js +0 -33
  156. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  157. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  158. package/dist/oracle/src/oracle/request.js +0 -48
  159. package/dist/oracle/src/oracle/run.js +0 -444
  160. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  161. package/dist/oracle/src/oracle/types.js +0 -1
  162. package/dist/oracle/src/oracle.js +0 -9
  163. package/dist/oracle/src/sessionManager.js +0 -205
  164. package/dist/oracle/src/version.js +0 -39
  165. package/dist/scripts/chrome/browser-tools.js +0 -295
  166. package/dist/src/browser/chromeCookies.js +0 -312
  167. package/dist/src/browser/keytarShim.js +0 -56
  168. package/dist/src/browser/profileSync.js +0 -141
  169. package/dist/src/browser/windowsCookies.js +0 -219
@@ -6,6 +6,7 @@ import { isKnownModel } from "../oracle/modelResolver.js";
6
6
  import { buildPromptMarkdown } from "../oracle/promptAssembly.js";
7
7
  import { buildAttachmentPlan } from "./policies.js";
8
8
  import { createStoredZip } from "./zipBundle.js";
9
+ import { buildAttachmentBasenameCollisionDetails, findAttachmentBasenameCollisions, formatAttachmentBasenameCollisionMessage, } from "./attachmentValidation.js";
9
10
  const DEFAULT_BROWSER_INLINE_CHAR_BUDGET = 60_000;
10
11
  const MAX_BROWSER_ATTACHMENTS = 10;
11
12
  const MAX_BROWSER_ZIP_BUNDLE_BYTES = 128 * 1024 * 1024;
@@ -82,70 +83,197 @@ export function isRawUploadFile(filePath) {
82
83
  const ext = path.extname(filePath).toLowerCase();
83
84
  return MEDIA_EXTENSIONS.has(ext) || ARCHIVE_EXTENSIONS.has(ext);
84
85
  }
86
+ const GENERATED_BUNDLE_DIR_PREFIX = "oracle-browser-bundle-";
87
+ const pendingFallbackBundles = new WeakMap();
85
88
  function formatSectionsForBundle(sections, options = {}) {
86
89
  return formatFileSections(sections, {
87
90
  lineNumbers: options.lineNumbers ?? true,
88
91
  trailingNewline: true,
89
92
  });
90
93
  }
91
- function resolveBrowserBundleFormat(format, sources) {
94
+ function resolveBrowserBundleFormat(format, { hasRawUploadFiles }) {
92
95
  if (format !== "auto") {
93
96
  return format;
94
97
  }
95
- return sources.hasRawUploadFiles ? "zip" : "text";
98
+ return hasRawUploadFiles ? "zip" : "text";
96
99
  }
97
- function shouldWriteBrowserBundle(format, { attachmentCount, bundleRequested, textSourceCount, textPlanShouldBundle, }) {
98
- if (format === "zip") {
99
- return (textPlanShouldBundle ||
100
- (bundleRequested && attachmentCount > 0) ||
101
- attachmentCount > MAX_BROWSER_ATTACHMENTS);
100
+ function resolveBrowserBundleScope(format, { bundleRequested, rawAttachmentCount, textAttachmentCount, }) {
101
+ const attachmentCount = textAttachmentCount + rawAttachmentCount;
102
+ if (attachmentCount === 0) {
103
+ return "none";
104
+ }
105
+ if (format === "text") {
106
+ if (textAttachmentCount === 0) {
107
+ return "none";
108
+ }
109
+ const shouldBundleText = bundleRequested || textAttachmentCount > 1 || attachmentCount > MAX_BROWSER_ATTACHMENTS;
110
+ return shouldBundleText ? "text-only" : "none";
111
+ }
112
+ if (bundleRequested) {
113
+ return "all";
102
114
  }
103
- return textSourceCount > 0 && (textPlanShouldBundle || attachmentCount > MAX_BROWSER_ATTACHMENTS);
115
+ // Preserve native uploads for images, PDFs, archives, and other raw inputs.
116
+ // Multiple text/source files benefit from a real filesystem tree, so bundle
117
+ // those into one ZIP while leaving any native attachments alongside it.
118
+ if (textAttachmentCount > 1) {
119
+ return rawAttachmentCount + 1 <= MAX_BROWSER_ATTACHMENTS ? "text-only" : "all";
120
+ }
121
+ return attachmentCount > MAX_BROWSER_ATTACHMENTS ? "all" : "none";
122
+ }
123
+ function appendZipBundleInstruction(composerText, originalCount, bundlePath) {
124
+ const fileLabel = originalCount === 1 ? "file" : "files";
125
+ const instruction = [
126
+ `The attached \`${path.basename(bundlePath)}\` contains ${originalCount} selected ${fileLabel} with relative paths preserved.`,
127
+ "Extract it into a temporary directory, then inspect the resulting file tree with filesystem and search tools before answering.",
128
+ ].join(" ");
129
+ return [composerText, instruction].filter(Boolean).join("\n\n").trim();
130
+ }
131
+ function assertAttachmentCount(attachmentCount, format) {
132
+ if (attachmentCount <= MAX_BROWSER_ATTACHMENTS)
133
+ return;
134
+ throw new Error(`Browser upload has ${attachmentCount} attachments after applying bundle format "${format}". Use --browser-bundle-format auto or zip to stay within the ${MAX_BROWSER_ATTACHMENTS}-attachment limit.`);
104
135
  }
105
- function assertAttachmentCount(attachments, format) {
106
- if (attachments.length <= MAX_BROWSER_ATTACHMENTS)
136
+ function assertUniqueAttachmentBasenames(attachments, cwd) {
137
+ const collisions = findAttachmentBasenameCollisions(attachments);
138
+ if (collisions.length === 0)
107
139
  return;
108
- throw new Error(`Browser upload has ${attachments.length} attachments after applying bundle format "${format}". Use --browser-bundle-format auto or zip to stay within the ${MAX_BROWSER_ATTACHMENTS}-attachment limit.`);
140
+ const details = buildAttachmentBasenameCollisionDetails(collisions, (attachment) => path.isAbsolute(attachment.path) ? attachment.path : path.resolve(cwd, attachment.path));
141
+ throw new FileValidationError(formatAttachmentBasenameCollisionMessage("Browser upload", details.collisions), { ...details });
142
+ }
143
+ async function applyWrittenBundle({ sections, sources, format, scope, rawUploadAttachments, composerText, bundleParentDir, }) {
144
+ const nativeAttachments = scope === "text-only" ? rawUploadAttachments : [];
145
+ assertAttachmentCount(1 + nativeAttachments.length, format);
146
+ assertUniqueAttachmentBasenames(nativeAttachments, process.cwd());
147
+ const reservedNames = new Set(nativeAttachments.map((a) => path.basename(a.path).toLowerCase()));
148
+ let bundleName = `attachments-bundle.${format === "zip" ? "zip" : "txt"}`;
149
+ for (let suffix = 2; reservedNames.has(bundleName.toLowerCase()); suffix += 1) {
150
+ bundleName = `attachments-bundle-${suffix}.${format === "zip" ? "zip" : "txt"}`;
151
+ }
152
+ const writtenBundle = await writeBrowserBundle(sections, sources, format, bundleName, bundleParentDir);
153
+ const attachments = [writtenBundle.attachment];
154
+ if (scope === "text-only") {
155
+ attachments.push(...rawUploadAttachments);
156
+ }
157
+ assertAttachmentCount(attachments.length, format);
158
+ return {
159
+ attachments,
160
+ bundled: writtenBundle.metadata,
161
+ composerText: format === "zip"
162
+ ? appendZipBundleInstruction(composerText, writtenBundle.metadata.originalCount, writtenBundle.metadata.bundlePath)
163
+ : composerText,
164
+ tokenEstimateText: writtenBundle.tokenEstimateText,
165
+ };
109
166
  }
110
- async function writeBrowserBundle(sections, sources, format) {
111
- const bundleDir = await fs.mkdtemp(path.join(os.tmpdir(), "oracle-browser-bundle-"));
112
- const tokenEstimateText = formatSectionsForBundle(sections, {
113
- lineNumbers: format === "text",
167
+ function generatedBundleDirectory(attachment) {
168
+ if (!attachment.generatedBundle)
169
+ return null;
170
+ const dir = path.dirname(attachment.path);
171
+ return path.basename(dir).startsWith(GENERATED_BUNDLE_DIR_PREFIX) ? dir : null;
172
+ }
173
+ export function listGeneratedBrowserBundleDirs(artifacts) {
174
+ const dirs = new Set();
175
+ for (const attachment of [...artifacts.attachments, ...(artifacts.fallback?.attachments ?? [])]) {
176
+ const dir = generatedBundleDirectory(attachment);
177
+ if (dir)
178
+ dirs.add(dir);
179
+ }
180
+ return [...dirs];
181
+ }
182
+ export async function cleanupGeneratedBrowserBundles(artifacts) {
183
+ await Promise.all(listGeneratedBrowserBundleDirs(artifacts).map((dir) => fs.rm(dir, { recursive: true, force: true })));
184
+ }
185
+ export async function materializeBrowserFallback(artifacts) {
186
+ const pending = pendingFallbackBundles.get(artifacts);
187
+ if (!pending || !artifacts.fallback) {
188
+ return artifacts.fallback ?? null;
189
+ }
190
+ pendingFallbackBundles.delete(artifacts);
191
+ const applied = await applyWrittenBundle({
192
+ sections: pending.sections,
193
+ sources: pending.scope === "all" ? pending.allSources : pending.textSources,
194
+ format: pending.format,
195
+ scope: pending.scope,
196
+ rawUploadAttachments: pending.rawUploadAttachments,
197
+ composerText: artifacts.fallback.composerText,
114
198
  });
199
+ artifacts.fallback.composerText = applied.composerText;
200
+ artifacts.fallback.attachments = applied.attachments;
201
+ artifacts.fallback.bundled = applied.bundled;
202
+ artifacts.fallback.pendingBundle = null;
203
+ return artifacts.fallback;
204
+ }
205
+ export async function materializeStagedFallbackBundle({ composerText, attachments, format, scope, bundleParentDir, }) {
206
+ const textAttachments = attachments.filter((attachment) => !isRawUploadFile(attachment.path));
207
+ const rawAttachments = attachments.filter((attachment) => isRawUploadFile(attachment.path));
208
+ const textSources = textAttachments.map((attachment) => ({
209
+ absolutePath: attachment.path,
210
+ displayPath: attachment.displayPath,
211
+ sizeBytes: attachment.sizeBytes ?? 0,
212
+ }));
213
+ const allSources = [
214
+ ...textSources,
215
+ ...rawAttachments.map((attachment) => ({
216
+ absolutePath: attachment.path,
217
+ displayPath: attachment.displayPath,
218
+ sizeBytes: attachment.sizeBytes ?? 0,
219
+ })),
220
+ ];
221
+ const sections = await Promise.all(textAttachments.map(async (attachment, index) => {
222
+ const content = await fs.readFile(attachment.path, "utf8");
223
+ return {
224
+ index: index + 1,
225
+ absolutePath: attachment.path,
226
+ displayPath: attachment.displayPath,
227
+ content,
228
+ sectionText: "",
229
+ };
230
+ }));
231
+ return applyWrittenBundle({
232
+ sections,
233
+ sources: scope === "all" ? allSources : textSources,
234
+ format,
235
+ scope,
236
+ rawUploadAttachments: rawAttachments,
237
+ composerText,
238
+ bundleParentDir,
239
+ });
240
+ }
241
+ async function writeBrowserBundle(sections, sources, format, bundleName, bundleParentDir = os.tmpdir()) {
242
+ const tokenEstimateText = formatSectionsForBundle(sections, { lineNumbers: format === "text" });
243
+ let content = tokenEstimateText;
115
244
  if (format === "zip") {
116
245
  const totalSourceBytes = sources.reduce((total, source) => total + source.sizeBytes, 0);
117
246
  if (totalSourceBytes > MAX_BROWSER_ZIP_BUNDLE_BYTES) {
118
247
  throw new Error(`Browser ZIP bundle inputs exceed the ${MAX_BROWSER_ZIP_BUNDLE_BYTES}-byte in-memory limit.`);
119
248
  }
120
- const bundlePath = path.join(bundleDir, "attachments-bundle.zip");
121
- const buffer = createStoredZip(await Promise.all(sources.map(async (source) => ({
249
+ content = createStoredZip(await Promise.all(sources.map(async (source) => ({
122
250
  path: source.displayPath,
123
251
  content: await fs.readFile(source.absolutePath),
124
252
  }))));
125
- await fs.writeFile(bundlePath, buffer);
253
+ }
254
+ const bundleDir = await fs.mkdtemp(path.join(bundleParentDir, GENERATED_BUNDLE_DIR_PREFIX));
255
+ const bundlePath = path.join(bundleDir, bundleName);
256
+ try {
257
+ await fs.writeFile(bundlePath, content);
126
258
  return {
127
259
  attachment: {
128
260
  path: bundlePath,
129
261
  displayPath: bundlePath,
130
- sizeBytes: buffer.length,
262
+ sizeBytes: Buffer.byteLength(content),
131
263
  generatedBundle: true,
132
264
  },
133
- metadata: { originalCount: sources.length, bundlePath, format },
265
+ metadata: {
266
+ originalCount: format === "zip" ? sources.length : sections.length,
267
+ bundlePath,
268
+ format,
269
+ },
134
270
  tokenEstimateText,
135
271
  };
136
272
  }
137
- const bundlePath = path.join(bundleDir, "attachments-bundle.txt");
138
- await fs.writeFile(bundlePath, tokenEstimateText, "utf8");
139
- return {
140
- attachment: {
141
- path: bundlePath,
142
- displayPath: bundlePath,
143
- sizeBytes: Buffer.byteLength(tokenEstimateText, "utf8"),
144
- generatedBundle: true,
145
- },
146
- metadata: { originalCount: sections.length, bundlePath, format },
147
- tokenEstimateText,
148
- };
273
+ catch (error) {
274
+ await fs.rm(bundleDir, { recursive: true, force: true });
275
+ throw error;
276
+ }
149
277
  }
150
278
  export async function assembleBrowserPrompt(runOptions, deps = {}) {
151
279
  const cwd = deps.cwd ?? process.cwd();
@@ -206,10 +334,10 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
206
334
  .filter(Boolean)
207
335
  .join("\n\n")
208
336
  .trim();
209
- const selectedPlan = attachmentsPolicy === "always"
210
- ? uploadPlan
211
- : attachmentsPolicy === "never"
212
- ? inlinePlan
337
+ const selectedPlan = attachmentsPolicy === "never"
338
+ ? inlinePlan
339
+ : attachmentsPolicy === "always" || bundleRequested
340
+ ? uploadPlan
213
341
  : inlineComposerText.length <= DEFAULT_BROWSER_INLINE_CHAR_BUDGET || sections.length === 0
214
342
  ? inlinePlan
215
343
  : uploadPlan;
@@ -228,13 +356,13 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
228
356
  const resolvedBundleFormat = resolveBrowserBundleFormat(bundleFormat, {
229
357
  hasRawUploadFiles: rawUploadAttachments.length > 0,
230
358
  });
231
- const shouldBundle = shouldWriteBrowserBundle(resolvedBundleFormat, {
232
- attachmentCount: attachments.length,
359
+ const bundleScope = resolveBrowserBundleScope(resolvedBundleFormat, {
233
360
  bundleRequested,
234
- textSourceCount: textBundleSources.length,
235
- textPlanShouldBundle: selectedPlan.shouldBundle,
361
+ rawAttachmentCount: rawUploadAttachments.length,
362
+ textAttachmentCount: selectedPlan.attachments.length,
236
363
  });
237
- const composerText = (!shouldBundle && selectedPlan.inlineBlock
364
+ const shouldBundle = bundleScope !== "none";
365
+ let composerText = (!shouldBundle && selectedPlan.inlineBlock
238
366
  ? [...baseComposerSections, selectedPlan.inlineBlock]
239
367
  : baseComposerSections)
240
368
  .filter(Boolean)
@@ -242,85 +370,116 @@ export async function assembleBrowserPrompt(runOptions, deps = {}) {
242
370
  .trim();
243
371
  let bundleText = null;
244
372
  let bundled = null;
245
- if (shouldBundle) {
246
- const writtenBundle = await writeBrowserBundle(sections, resolvedBundleFormat === "zip" ? allBundleSources : textBundleSources, resolvedBundleFormat);
373
+ if (bundleScope !== "none") {
374
+ const writtenBundle = await applyWrittenBundle({
375
+ sections,
376
+ sources: bundleScope === "all" ? allBundleSources : textBundleSources,
377
+ format: resolvedBundleFormat,
378
+ scope: bundleScope,
379
+ rawUploadAttachments,
380
+ composerText,
381
+ });
247
382
  bundleText = writtenBundle.tokenEstimateText;
248
383
  attachments.length = 0;
249
- attachments.push(writtenBundle.attachment);
250
- if (resolvedBundleFormat === "text") {
251
- attachments.push(...rawUploadAttachments);
252
- }
253
- bundled = writtenBundle.metadata;
384
+ attachments.push(...writtenBundle.attachments);
385
+ bundled = writtenBundle.bundled;
386
+ composerText = writtenBundle.composerText;
254
387
  }
255
- assertAttachmentCount(attachments, resolvedBundleFormat);
256
- const inlineFileCount = shouldBundle ? 0 : selectedPlan.inlineFileCount;
257
- const modelConfig = isKnownModel(runOptions.model)
258
- ? MODEL_CONFIGS[runOptions.model]
259
- : MODEL_CONFIGS["gpt-5.1"];
260
- const tokenizer = deps.tokenizeImpl ?? modelConfig.tokenizer;
261
- const tokenizerUserContent = inlineFileCount > 0 && selectedPlan.inlineBlock
262
- ? [userPrompt, selectedPlan.inlineBlock]
388
+ else {
389
+ assertAttachmentCount(attachments.length, resolvedBundleFormat);
390
+ }
391
+ try {
392
+ assertUniqueAttachmentBasenames(attachments, cwd);
393
+ const inlineFileCount = shouldBundle ? 0 : selectedPlan.inlineFileCount;
394
+ const modelConfig = isKnownModel(runOptions.model)
395
+ ? MODEL_CONFIGS[runOptions.model]
396
+ : MODEL_CONFIGS["gpt-5.1"];
397
+ const tokenizer = deps.tokenizeImpl ?? modelConfig.tokenizer;
398
+ const tokenizerUserSections = [userPrompt];
399
+ if (inlineFileCount > 0 && selectedPlan.inlineBlock) {
400
+ tokenizerUserSections.push(selectedPlan.inlineBlock);
401
+ }
402
+ if (shouldBundle && resolvedBundleFormat === "zip" && bundled) {
403
+ tokenizerUserSections.push(appendZipBundleInstruction("", bundled.originalCount, bundled.bundlePath));
404
+ }
405
+ const tokenizerUserContent = tokenizerUserSections
263
406
  .filter((value) => Boolean(value?.trim()))
264
407
  .join("\n\n")
265
- .trim()
266
- : userPrompt;
267
- const tokenizerMessages = [
268
- systemPrompt ? { role: "system", content: systemPrompt } : null,
269
- tokenizerUserContent ? { role: "user", content: tokenizerUserContent } : null,
270
- ].filter(Boolean);
271
- let estimatedInputTokens = tokenizer(tokenizerMessages.length > 0 ? tokenizerMessages : [{ role: "user", content: "" }], TOKENIZER_OPTIONS);
272
- const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(selectedPlan.inlineBlock);
273
- if (!tokenEstimateIncludesInlineFiles && sections.length > 0) {
274
- const attachmentText = bundleText ?? formatFileSections(sections, { lineNumbers: false });
275
- const attachmentTokens = tokenizer([{ role: "user", content: attachmentText }], TOKENIZER_OPTIONS);
276
- estimatedInputTokens += attachmentTokens;
277
- }
278
- let fallback = null;
279
- if (attachmentsPolicy === "auto" && selectedPlan.mode === "inline" && sections.length > 0) {
280
- const fallbackComposerText = baseComposerSections.join("\n\n").trim();
281
- const fallbackAttachments = [...uploadPlan.attachments, ...rawUploadAttachments];
282
- let fallbackBundled = null;
283
- const fallbackBundleFormat = resolveBrowserBundleFormat(bundleFormat, {
284
- hasRawUploadFiles: rawUploadAttachments.length > 0,
285
- });
286
- const fallbackShouldBundle = shouldWriteBrowserBundle(fallbackBundleFormat, {
287
- attachmentCount: fallbackAttachments.length,
288
- bundleRequested,
289
- textSourceCount: textBundleSources.length,
290
- textPlanShouldBundle: uploadPlan.shouldBundle,
291
- });
292
- if (fallbackShouldBundle) {
293
- const writtenBundle = await writeBrowserBundle(sections, fallbackBundleFormat === "zip" ? allBundleSources : textBundleSources, fallbackBundleFormat);
294
- fallbackAttachments.length = 0;
295
- fallbackAttachments.push(writtenBundle.attachment);
296
- if (fallbackBundleFormat === "text") {
297
- fallbackAttachments.push(...rawUploadAttachments);
408
+ .trim();
409
+ const tokenizerMessages = [
410
+ systemPrompt ? { role: "system", content: systemPrompt } : null,
411
+ tokenizerUserContent ? { role: "user", content: tokenizerUserContent } : null,
412
+ ].filter(Boolean);
413
+ let estimatedInputTokens = tokenizer(tokenizerMessages.length > 0 ? tokenizerMessages : [{ role: "user", content: "" }], TOKENIZER_OPTIONS);
414
+ const tokenEstimateIncludesInlineFiles = inlineFileCount > 0 && Boolean(selectedPlan.inlineBlock);
415
+ if (!tokenEstimateIncludesInlineFiles && sections.length > 0) {
416
+ const attachmentText = bundleText ?? formatFileSections(sections, { lineNumbers: false });
417
+ const attachmentTokens = tokenizer([{ role: "user", content: attachmentText }], TOKENIZER_OPTIONS);
418
+ estimatedInputTokens += attachmentTokens;
419
+ }
420
+ let fallback = null;
421
+ let pendingFallback;
422
+ if (attachmentsPolicy === "auto" && selectedPlan.mode === "inline" && sections.length > 0) {
423
+ const fallbackComposerText = baseComposerSections.join("\n\n").trim();
424
+ const fallbackAttachments = [...uploadPlan.attachments, ...rawUploadAttachments];
425
+ const fallbackBundleFormat = resolveBrowserBundleFormat(bundleFormat, {
426
+ hasRawUploadFiles: rawUploadAttachments.length > 0,
427
+ });
428
+ const fallbackBundleScope = resolveBrowserBundleScope(fallbackBundleFormat, {
429
+ bundleRequested,
430
+ rawAttachmentCount: rawUploadAttachments.length,
431
+ textAttachmentCount: uploadPlan.attachments.length,
432
+ });
433
+ fallback = {
434
+ composerText: fallbackComposerText,
435
+ attachments: fallbackAttachments,
436
+ bundled: null,
437
+ pendingBundle: fallbackBundleScope === "none"
438
+ ? null
439
+ : { format: fallbackBundleFormat, scope: fallbackBundleScope },
440
+ };
441
+ if (fallbackBundleScope !== "none") {
442
+ pendingFallback = {
443
+ format: fallbackBundleFormat,
444
+ scope: fallbackBundleScope,
445
+ sections,
446
+ textSources: textBundleSources,
447
+ allSources: allBundleSources,
448
+ rawUploadAttachments,
449
+ };
450
+ }
451
+ else {
452
+ assertAttachmentCount(fallbackAttachments.length, fallbackBundleFormat);
298
453
  }
299
- fallbackBundled = writtenBundle.metadata;
300
454
  }
301
- assertAttachmentCount(fallbackAttachments, fallbackBundleFormat);
302
- fallback = {
303
- composerText: fallbackComposerText,
304
- attachments: fallbackAttachments,
305
- bundled: fallbackBundled,
455
+ const artifacts = {
456
+ markdown,
457
+ composerText,
458
+ estimatedInputTokens,
459
+ attachments,
460
+ inlineFileCount,
461
+ tokenEstimateIncludesInlineFiles,
462
+ attachmentsPolicy,
463
+ attachmentMode: shouldBundle
464
+ ? "bundle"
465
+ : attachments.length > 0
466
+ ? "upload"
467
+ : selectedPlan.mode === "bundle"
468
+ ? "inline"
469
+ : selectedPlan.mode,
470
+ fallback,
471
+ bundled,
306
472
  };
473
+ if (pendingFallback) {
474
+ pendingFallbackBundles.set(artifacts, pendingFallback);
475
+ }
476
+ return artifacts;
477
+ }
478
+ catch (error) {
479
+ await Promise.all(attachments
480
+ .map(generatedBundleDirectory)
481
+ .filter((dir) => dir !== null)
482
+ .map((dir) => fs.rm(dir, { recursive: true, force: true })));
483
+ throw error;
307
484
  }
308
- return {
309
- markdown,
310
- composerText,
311
- estimatedInputTokens,
312
- attachments,
313
- inlineFileCount,
314
- tokenEstimateIncludesInlineFiles,
315
- attachmentsPolicy,
316
- attachmentMode: shouldBundle
317
- ? "bundle"
318
- : attachments.length > 0
319
- ? "upload"
320
- : selectedPlan.mode === "bundle"
321
- ? "inline"
322
- : selectedPlan.mode,
323
- fallback,
324
- bundled,
325
- };
326
485
  }
@@ -20,11 +20,14 @@ async function submitPromptViaAdapter(ctx) {
20
20
  const committedTurns = await submitPrompt({
21
21
  runtime: state.runtime,
22
22
  input: state.input,
23
+ page: state.page,
23
24
  attachmentNames: state.attachmentNames ?? [],
25
+ attachmentNavigationUrl: state.attachmentNavigationUrl,
24
26
  baselineTurns: state.baselineTurns ?? undefined,
25
27
  inputTimeoutMs: state.inputTimeoutMs ?? undefined,
26
28
  attachmentTimeoutMs: state.attachmentTimeoutMs ?? undefined,
27
29
  onPromptSubmitted: state.onPromptSubmitted,
30
+ webSearch: state.webSearch,
28
31
  }, ctx.prompt, state.logger);
29
32
  state.committedTurns =
30
33
  typeof committedTurns === "number" && Number.isFinite(committedTurns) ? committedTurns : null;
@@ -3,8 +3,8 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { mkdtemp, mkdir, rm } from "node:fs/promises";
5
5
  import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, } from "./pageActions.js";
6
- import { launchChrome, connectToChrome, positionChromeWindowOffscreen, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
7
- import { resolveBrowserConfig } from "./config.js";
6
+ import { launchChrome, connectToChrome, positionChromeWindowOffscreen, positionChromeWindowOnscreen, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
7
+ import { resolveBrowserApprovalWait, resolveBrowserConfig } from "./config.js";
8
8
  import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
9
9
  import { CHATGPT_URL } from "./constants.js";
10
10
  import { buildConversationTurnListExpression } from "./conversationTurns.js";
@@ -31,11 +31,14 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
31
31
  const host = liveRuntime.chromeHost ?? "127.0.0.1";
32
32
  const port = liveRuntime.chromePort ?? inferPortFromBrowserWSEndpoint(liveRuntime.chromeBrowserWSEndpoint);
33
33
  const browserWSEndpoint = liveRuntime.chromeBrowserWSEndpoint ?? undefined;
34
+ const approvalWaitMs = resolveBrowserApprovalWait(config?.approvalWaitMs);
34
35
  const listTargets = deps.listTargets ??
35
36
  (async () => (await listRemoteChromeTargets({
36
37
  host,
37
38
  port: port ?? 9222,
38
39
  browserWSEndpoint,
40
+ approvalWaitMs,
41
+ logger,
39
42
  })));
40
43
  const targetList = (await listTargets());
41
44
  const target = pickTarget(targetList, liveRuntime);
@@ -44,6 +47,7 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
44
47
  browserWSEndpoint,
45
48
  targetId: target?.targetId ?? target?.id,
46
49
  closeTargetOnDispose: false,
50
+ approvalWaitMs,
47
51
  })
48
52
  : await (async () => {
49
53
  const client = (await (deps.connect ?? ((options) => CDP(options)))(browserWSEndpoint
@@ -62,6 +66,19 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
62
66
  closeAttachedConnection = () => connection.close();
63
67
  const client = connection.client;
64
68
  const { Runtime, DOM, Page } = client;
69
+ const captureIdentity = async () => {
70
+ const targetId = target?.targetId ?? target?.id;
71
+ if (!targetId || !port)
72
+ return undefined;
73
+ const { result } = await withTimeout(Runtime.evaluate({ expression: "location.href", returnByValue: true }), 2_000, "Recovery target identity unavailable");
74
+ return {
75
+ host,
76
+ port,
77
+ targetId,
78
+ browserWSEndpoint,
79
+ conversationId: extractConversationIdFromUrl(typeof result?.value === "string" ? result.value : ""),
80
+ };
81
+ };
65
82
  if (Runtime?.enable) {
66
83
  await Runtime.enable();
67
84
  }
@@ -113,10 +130,12 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
113
130
  const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
114
131
  requireScopedTargetOwner: true,
115
132
  }), timeoutMs + 5_000, "Reattach Deep Research response timed out");
133
+ const captureTarget = await captureIdentity().catch(() => undefined);
116
134
  await closeAttached();
117
135
  return {
118
136
  answerText: researchResult.text,
119
137
  answerMarkdown: researchResult.text,
138
+ captureTarget,
120
139
  };
121
140
  }
122
141
  const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
@@ -124,8 +143,13 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
124
143
  const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
125
144
  const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
126
145
  const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
146
+ const captureTarget = await captureIdentity().catch(() => undefined);
127
147
  await closeAttached();
128
- return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
148
+ return {
149
+ answerText: aligned.answerText,
150
+ answerMarkdown: aligned.answerMarkdown,
151
+ captureTarget,
152
+ };
129
153
  }
130
154
  catch (error) {
131
155
  await closeAttached();
@@ -216,7 +240,10 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
216
240
  await DOM.enable();
217
241
  }
218
242
  if (!resolved.headless && resolved.hideWindow) {
219
- await positionChromeWindowOffscreen(client, logger);
243
+ await positionChromeWindowOffscreen(client, userDataDir, logger);
244
+ }
245
+ else if (!resolved.headless) {
246
+ await positionChromeWindowOnscreen(client, userDataDir, logger);
220
247
  }
221
248
  let appliedCookies = 0;
222
249
  if (shouldSyncBrowserCookies(resolved, { manualLogin })) {