@steipete/oracle 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +107 -49
  3. package/dist/bin/oracle-cli.js +551 -410
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/archiveConversation.js +224 -0
  18. package/dist/src/browser/actions/assistantResponse.js +175 -101
  19. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  20. package/dist/src/browser/actions/attachments.js +246 -150
  21. package/dist/src/browser/actions/deepResearch.js +662 -0
  22. package/dist/src/browser/actions/domEvents.js +2 -2
  23. package/dist/src/browser/actions/modelSelection.js +342 -119
  24. package/dist/src/browser/actions/navigation.js +183 -137
  25. package/dist/src/browser/actions/projectSources.js +491 -0
  26. package/dist/src/browser/actions/promptComposer.js +152 -91
  27. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  28. package/dist/src/browser/actions/thinkingStatus.js +391 -0
  29. package/dist/src/browser/actions/thinkingTime.js +207 -110
  30. package/dist/src/browser/artifacts.js +150 -0
  31. package/dist/src/browser/attachRunning.js +31 -0
  32. package/dist/src/browser/chatgptImages.js +315 -0
  33. package/dist/src/browser/chromeLifecycle.js +276 -63
  34. package/dist/src/browser/config.js +59 -16
  35. package/dist/src/browser/constants.js +25 -12
  36. package/dist/src/browser/controlPlan.js +81 -0
  37. package/dist/src/browser/cookies.js +19 -19
  38. package/dist/src/browser/detect.js +250 -77
  39. package/dist/src/browser/domDebug.js +50 -1
  40. package/dist/src/browser/index.js +1559 -692
  41. package/dist/src/browser/liveTabs.js +434 -0
  42. package/dist/src/browser/modelStrategy.js +1 -1
  43. package/dist/src/browser/pageActions.js +5 -5
  44. package/dist/src/browser/policies.js +16 -13
  45. package/dist/src/browser/profileState.js +127 -42
  46. package/dist/src/browser/projectSourcesRunner.js +366 -0
  47. package/dist/src/browser/prompt.js +72 -42
  48. package/dist/src/browser/promptSummary.js +5 -5
  49. package/dist/src/browser/providerDomFlow.js +1 -1
  50. package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
  51. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
  52. package/dist/src/browser/providers/index.js +2 -2
  53. package/dist/src/browser/reattach.js +178 -73
  54. package/dist/src/browser/reattachHelpers.js +32 -27
  55. package/dist/src/browser/sessionRunner.js +89 -25
  56. package/dist/src/browser/tabLeaseRegistry.js +182 -0
  57. package/dist/src/browser/utils.js +9 -9
  58. package/dist/src/browserMode.js +1 -1
  59. package/dist/src/cli/bridge/claudeConfig.js +24 -20
  60. package/dist/src/cli/bridge/client.js +28 -20
  61. package/dist/src/cli/bridge/codexConfig.js +16 -16
  62. package/dist/src/cli/bridge/doctor.js +47 -39
  63. package/dist/src/cli/bridge/host.js +58 -56
  64. package/dist/src/cli/browserConfig.js +102 -48
  65. package/dist/src/cli/browserDefaults.js +51 -26
  66. package/dist/src/cli/browserTabs.js +228 -0
  67. package/dist/src/cli/bundleWarnings.js +1 -1
  68. package/dist/src/cli/clipboard.js +11 -2
  69. package/dist/src/cli/detach.js +2 -2
  70. package/dist/src/cli/dryRun.js +62 -26
  71. package/dist/src/cli/duplicatePromptGuard.js +12 -4
  72. package/dist/src/cli/engine.js +9 -9
  73. package/dist/src/cli/errorUtils.js +1 -1
  74. package/dist/src/cli/fileSize.js +3 -3
  75. package/dist/src/cli/format.js +2 -2
  76. package/dist/src/cli/help.js +28 -28
  77. package/dist/src/cli/hiddenAliases.js +3 -3
  78. package/dist/src/cli/markdownBundle.js +7 -7
  79. package/dist/src/cli/markdownRenderer.js +15 -15
  80. package/dist/src/cli/notifier.js +77 -67
  81. package/dist/src/cli/options.js +131 -106
  82. package/dist/src/cli/oscUtils.js +1 -1
  83. package/dist/src/cli/projectSources.js +116 -0
  84. package/dist/src/cli/promptRequirement.js +2 -2
  85. package/dist/src/cli/renderOutput.js +1 -1
  86. package/dist/src/cli/rootAlias.js +1 -1
  87. package/dist/src/cli/runOptions.js +32 -28
  88. package/dist/src/cli/sessionCommand.js +82 -21
  89. package/dist/src/cli/sessionDisplay.js +213 -87
  90. package/dist/src/cli/sessionLineage.js +6 -2
  91. package/dist/src/cli/sessionRunner.js +149 -95
  92. package/dist/src/cli/sessionTable.js +26 -23
  93. package/dist/src/cli/stdin.js +22 -0
  94. package/dist/src/cli/tagline.js +121 -124
  95. package/dist/src/cli/tui/index.js +139 -128
  96. package/dist/src/cli/writeOutputPath.js +5 -5
  97. package/dist/src/config.js +7 -7
  98. package/dist/src/gemini-web/browserSessionManager.js +19 -15
  99. package/dist/src/gemini-web/client.js +76 -70
  100. package/dist/src/gemini-web/executionMode.js +6 -8
  101. package/dist/src/gemini-web/executor.js +98 -93
  102. package/dist/src/gemini-web/index.js +1 -1
  103. package/dist/src/mcp/consultPresets.js +19 -0
  104. package/dist/src/mcp/server.js +18 -12
  105. package/dist/src/mcp/tools/consult.js +246 -67
  106. package/dist/src/mcp/tools/projectSources.js +123 -0
  107. package/dist/src/mcp/tools/sessionResources.js +12 -12
  108. package/dist/src/mcp/tools/sessions.js +26 -17
  109. package/dist/src/mcp/types.js +12 -5
  110. package/dist/src/mcp/utils.js +21 -8
  111. package/dist/src/oracle/background.js +15 -15
  112. package/dist/src/oracle/claude.js +53 -25
  113. package/dist/src/oracle/client.js +50 -41
  114. package/dist/src/oracle/config.js +96 -66
  115. package/dist/src/oracle/errors.js +38 -38
  116. package/dist/src/oracle/files.js +55 -46
  117. package/dist/src/oracle/finishLine.js +10 -8
  118. package/dist/src/oracle/format.js +3 -3
  119. package/dist/src/oracle/gemini.js +37 -33
  120. package/dist/src/oracle/logging.js +7 -7
  121. package/dist/src/oracle/markdown.js +28 -28
  122. package/dist/src/oracle/modelResolver.js +16 -16
  123. package/dist/src/oracle/multiModelRunner.js +12 -12
  124. package/dist/src/oracle/oscProgress.js +8 -8
  125. package/dist/src/oracle/promptAssembly.js +6 -3
  126. package/dist/src/oracle/request.js +16 -13
  127. package/dist/src/oracle/run.js +160 -135
  128. package/dist/src/oracle/runUtils.js +8 -5
  129. package/dist/src/oracle/tokenEstimate.js +6 -6
  130. package/dist/src/oracle/tokenStats.js +5 -5
  131. package/dist/src/oracle/tokenStringifier.js +5 -5
  132. package/dist/src/oracle.js +12 -12
  133. package/dist/src/oracleHome.js +3 -3
  134. package/dist/src/projectSources/plan.js +27 -0
  135. package/dist/src/projectSources/url.js +23 -0
  136. package/dist/src/remote/client.js +25 -25
  137. package/dist/src/remote/health.js +20 -20
  138. package/dist/src/remote/remoteServiceConfig.js +9 -9
  139. package/dist/src/remote/server.js +129 -118
  140. package/dist/src/sessionManager.js +78 -75
  141. package/dist/src/sessionStore.js +3 -3
  142. package/dist/src/version.js +10 -10
  143. package/dist/vendor/oracle-notifier/README.md +2 -0
  144. package/package.json +67 -62
  145. package/vendor/oracle-notifier/README.md +2 -0
  146. package/dist/markdansi/types/index.js +0 -4
  147. package/dist/oracle/bin/oracle-cli.js +0 -472
  148. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  149. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  150. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  151. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  152. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  153. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  154. package/dist/oracle/src/browser/config.js +0 -33
  155. package/dist/oracle/src/browser/constants.js +0 -40
  156. package/dist/oracle/src/browser/cookies.js +0 -210
  157. package/dist/oracle/src/browser/domDebug.js +0 -36
  158. package/dist/oracle/src/browser/index.js +0 -331
  159. package/dist/oracle/src/browser/pageActions.js +0 -5
  160. package/dist/oracle/src/browser/prompt.js +0 -88
  161. package/dist/oracle/src/browser/promptSummary.js +0 -20
  162. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  163. package/dist/oracle/src/browser/utils.js +0 -62
  164. package/dist/oracle/src/browserMode.js +0 -1
  165. package/dist/oracle/src/cli/browserConfig.js +0 -44
  166. package/dist/oracle/src/cli/dryRun.js +0 -59
  167. package/dist/oracle/src/cli/engine.js +0 -17
  168. package/dist/oracle/src/cli/errorUtils.js +0 -9
  169. package/dist/oracle/src/cli/help.js +0 -70
  170. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  171. package/dist/oracle/src/cli/options.js +0 -103
  172. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  173. package/dist/oracle/src/cli/rootAlias.js +0 -30
  174. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  175. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  176. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  177. package/dist/oracle/src/heartbeat.js +0 -43
  178. package/dist/oracle/src/oracle/client.js +0 -48
  179. package/dist/oracle/src/oracle/config.js +0 -29
  180. package/dist/oracle/src/oracle/errors.js +0 -101
  181. package/dist/oracle/src/oracle/files.js +0 -220
  182. package/dist/oracle/src/oracle/format.js +0 -33
  183. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  184. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  185. package/dist/oracle/src/oracle/request.js +0 -48
  186. package/dist/oracle/src/oracle/run.js +0 -444
  187. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  188. package/dist/oracle/src/oracle/types.js +0 -1
  189. package/dist/oracle/src/oracle.js +0 -9
  190. package/dist/oracle/src/sessionManager.js +0 -205
  191. package/dist/oracle/src/version.js +0 -39
  192. package/dist/scripts/chrome/browser-tools.js +0 -295
  193. package/dist/src/browser/profileSync.js +0 -141
  194. /package/dist/{oracle/src/browser → src/projectSources}/types.js +0 -0
@@ -0,0 +1,315 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { ASSISTANT_ROLE_SELECTOR, CONVERSATION_TURN_SELECTOR } from "./constants.js";
4
+ import { delay } from "./utils.js";
5
+ import { readAssistantSnapshot } from "./pageActions.js";
6
+ import { getOracleHomeDir } from "../oracleHome.js";
7
+ import { resolveSessionArtifactsDir } from "./artifacts.js";
8
+ const GENERATED_IMAGE_WAIT_MIN_MS = 15_000;
9
+ const GENERATED_IMAGE_WAIT_MAX_MS = 15 * 60_000;
10
+ function extractFileId(url) {
11
+ try {
12
+ return new URL(url).searchParams.get("id") ?? undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ function dedupeImages(images) {
19
+ const best = new Map();
20
+ for (const image of images) {
21
+ const key = image.fileId ?? image.url;
22
+ const currentArea = (image.width ?? 0) * (image.height ?? 0);
23
+ const existing = best.get(key);
24
+ const existingArea = existing ? (existing.width ?? 0) * (existing.height ?? 0) : -1;
25
+ if (!existing || currentArea >= existingArea) {
26
+ best.set(key, image);
27
+ }
28
+ }
29
+ return [...best.values()];
30
+ }
31
+ function buildAssistantImageExpression(minTurnIndex) {
32
+ const minTurnLiteral = typeof minTurnIndex === "number" && Number.isFinite(minTurnIndex) && minTurnIndex >= 0
33
+ ? Math.floor(minTurnIndex)
34
+ : -1;
35
+ const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
36
+ const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
37
+ return `(() => {
38
+ const MIN_TURN_INDEX = ${minTurnLiteral};
39
+ const CONVERSATION_SELECTOR = ${conversationLiteral};
40
+ const ASSISTANT_SELECTOR = ${assistantLiteral};
41
+ const isGeneratedImage = (img) => {
42
+ const url = img?.src || '';
43
+ if (!url.includes('/backend-api/estuary/content?id=file_')) return false;
44
+ const alt = String(img.alt || '').toLowerCase();
45
+ if (alt.includes('generated image')) return true;
46
+ let node = img;
47
+ while (node instanceof HTMLElement) {
48
+ if (String(node.id || '').startsWith('image-')) return true;
49
+ if (String(node.className || '').includes('imagegen-image')) return true;
50
+ node = node.parentElement;
51
+ }
52
+ return false;
53
+ };
54
+ const serializeImages = (root) =>
55
+ Array.from(root.querySelectorAll('img')).filter(isGeneratedImage).map((img) => ({
56
+ url: img.src || '',
57
+ alt: img.alt || '',
58
+ width: img.naturalWidth || 0,
59
+ height: img.naturalHeight || 0,
60
+ }));
61
+ const isAssistantTurn = (node) => {
62
+ if (!(node instanceof HTMLElement)) return false;
63
+ const turnAttr = (node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
64
+ if (turnAttr === 'assistant') return true;
65
+ const role = (node.getAttribute('data-message-author-role') || node.dataset?.messageAuthorRole || '').toLowerCase();
66
+ if (role === 'assistant') return true;
67
+ const testId = (node.getAttribute('data-testid') || '').toLowerCase();
68
+ if (testId.includes('assistant')) return true;
69
+ return Boolean(node.querySelector(ASSISTANT_SELECTOR) || node.querySelector('[data-testid*="assistant"]'));
70
+ };
71
+ const turns = Array.from(document.querySelectorAll(CONVERSATION_SELECTOR));
72
+ for (let index = turns.length - 1; index >= 0; index -= 1) {
73
+ const turn = turns[index];
74
+ if (!isAssistantTurn(turn)) continue;
75
+ if (MIN_TURN_INDEX >= 0 && index < MIN_TURN_INDEX) continue;
76
+ const messageRoot = turn.querySelector(ASSISTANT_SELECTOR) || turn;
77
+ const images = serializeImages(messageRoot);
78
+ if (images.length > 0) return images;
79
+ }
80
+ const boundary =
81
+ MIN_TURN_INDEX > 0 && turns.length > 0
82
+ ? turns[Math.min(MIN_TURN_INDEX - 1, turns.length - 1)]
83
+ : null;
84
+ return Array.from(document.querySelectorAll('img'))
85
+ .filter(isGeneratedImage)
86
+ .filter((img) => {
87
+ if (!boundary) return true;
88
+ return Boolean(boundary.compareDocumentPosition(img) & Node.DOCUMENT_POSITION_FOLLOWING);
89
+ })
90
+ .map((img) => ({
91
+ url: img.src || '',
92
+ alt: img.alt || '',
93
+ width: img.naturalWidth || 0,
94
+ height: img.naturalHeight || 0,
95
+ }));
96
+ })()`;
97
+ }
98
+ export async function readAssistantGeneratedImages(Runtime, minTurnIndex) {
99
+ const { result } = await Runtime.evaluate({
100
+ expression: buildAssistantImageExpression(minTurnIndex),
101
+ returnByValue: true,
102
+ });
103
+ const raw = Array.isArray(result?.value) ? result.value : [];
104
+ const normalized = raw
105
+ .map((item) => ({
106
+ url: typeof item?.url === "string" ? item.url : "",
107
+ alt: typeof item?.alt === "string" ? item.alt : undefined,
108
+ width: typeof item?.width === "number" ? item.width : undefined,
109
+ height: typeof item?.height === "number" ? item.height : undefined,
110
+ fileId: typeof item?.url === "string" ? extractFileId(item.url) : undefined,
111
+ }))
112
+ .filter((item) => item.url.length > 0);
113
+ return dedupeImages(normalized);
114
+ }
115
+ async function readAssistantGeneratedImagesWithFallback(Runtime, minTurnIndex) {
116
+ const filteredImages = await readAssistantGeneratedImages(Runtime, minTurnIndex ?? undefined).catch(() => []);
117
+ if (filteredImages.length > 0 ||
118
+ typeof minTurnIndex !== "number" ||
119
+ !Number.isFinite(minTurnIndex)) {
120
+ return filteredImages;
121
+ }
122
+ const [fallbackImages, fallbackSnapshot] = await Promise.all([
123
+ readAssistantGeneratedImages(Runtime).catch(() => []),
124
+ readAssistantSnapshot(Runtime).catch(() => null),
125
+ ]);
126
+ const fallbackTurnIndex = typeof fallbackSnapshot?.turnIndex === "number" ? fallbackSnapshot.turnIndex : null;
127
+ const nearBoundary = fallbackTurnIndex !== null && fallbackTurnIndex + 1 >= Math.floor(minTurnIndex);
128
+ return fallbackImages.length > 0 && nearBoundary ? fallbackImages : [];
129
+ }
130
+ function resolveGeneratedImageWaitTimeoutMs(waitTimeoutMs) {
131
+ const requestedTimeout = typeof waitTimeoutMs === "number" && Number.isFinite(waitTimeoutMs)
132
+ ? waitTimeoutMs
133
+ : GENERATED_IMAGE_WAIT_MAX_MS;
134
+ return Math.max(GENERATED_IMAGE_WAIT_MIN_MS, Math.min(requestedTimeout, GENERATED_IMAGE_WAIT_MAX_MS));
135
+ }
136
+ export function resolveGeneratedImageWaitTimeoutMsForTest(waitTimeoutMs) {
137
+ return resolveGeneratedImageWaitTimeoutMs(waitTimeoutMs);
138
+ }
139
+ function contentTypeToExtension(contentType) {
140
+ const value = String(contentType ?? "").toLowerCase();
141
+ if (value.includes("png"))
142
+ return "png";
143
+ if (value.includes("jpeg") || value.includes("jpg"))
144
+ return "jpg";
145
+ if (value.includes("webp"))
146
+ return "webp";
147
+ if (value.includes("gif"))
148
+ return "gif";
149
+ if (value.includes("svg"))
150
+ return "svg";
151
+ return "bin";
152
+ }
153
+ function resolveSiblingImagePath(basePath, index, extension) {
154
+ const ext = path.extname(basePath);
155
+ const dir = path.dirname(basePath);
156
+ const stem = ext ? path.basename(basePath, ext) : path.basename(basePath);
157
+ if (index === 0) {
158
+ return ext ? basePath : path.join(dir, `${stem}.${extension}`);
159
+ }
160
+ const suffix = ext ? `${stem}.${index + 1}${ext}` : `${stem}.${index + 1}.${extension}`;
161
+ return path.join(dir, suffix);
162
+ }
163
+ function sanitizeGeneratedImageStem(value) {
164
+ return value
165
+ .replace(/[^a-zA-Z0-9_-]+/g, "-")
166
+ .replace(/-+/g, "-")
167
+ .replace(/^-|-$/g, "")
168
+ .slice(0, 48);
169
+ }
170
+ function resolveDefaultGeneratedImagePath(images, sessionId) {
171
+ const primary = images[0];
172
+ const stemSource = primary?.fileId || primary?.alt || primary?.url || `generated-${Date.now().toString(36)}`;
173
+ const stem = sanitizeGeneratedImageStem(stemSource) || `generated-${Date.now().toString(36)}`;
174
+ const baseDir = sessionId
175
+ ? resolveSessionArtifactsDir(sessionId)
176
+ : path.join(getOracleHomeDir(), ".temp");
177
+ return path.join(baseDir, `${stem}.png`);
178
+ }
179
+ async function buildCookieHeader(Network) {
180
+ const response = await Network.getCookies({ urls: ["https://chatgpt.com/"] });
181
+ return (response.cookies ?? [])
182
+ .filter((cookie) => cookie.name && typeof cookie.value === "string")
183
+ .map((cookie) => `${cookie.name}=${cookie.value}`)
184
+ .join("; ");
185
+ }
186
+ export async function saveChatGptGeneratedImages(params) {
187
+ const { Network, images, outputPath, logger } = params;
188
+ if (!images.length)
189
+ return { saved: false, imageCount: 0, savedImages: [], errors: [] };
190
+ const cookieHeader = await buildCookieHeader(Network);
191
+ if (!cookieHeader) {
192
+ return {
193
+ saved: false,
194
+ imageCount: images.length,
195
+ savedImages: [],
196
+ errors: ["Missing ChatGPT cookies for image download."],
197
+ };
198
+ }
199
+ const savedImages = [];
200
+ const errors = [];
201
+ await fs.mkdir(path.dirname(path.resolve(outputPath)), { recursive: true });
202
+ for (let index = 0; index < images.length; index += 1) {
203
+ const image = images[index];
204
+ try {
205
+ const response = await fetch(image.url, {
206
+ headers: {
207
+ cookie: cookieHeader,
208
+ "user-agent": "Mozilla/5.0",
209
+ },
210
+ redirect: "follow",
211
+ });
212
+ if (!response.ok) {
213
+ throw new Error(`download failed: ${response.status} ${response.statusText}`);
214
+ }
215
+ const contentType = response.headers.get("content-type");
216
+ const extension = contentTypeToExtension(contentType);
217
+ const targetPath = resolveSiblingImagePath(path.resolve(outputPath), index, extension);
218
+ const buffer = Buffer.from(await response.arrayBuffer());
219
+ await fs.writeFile(targetPath, buffer);
220
+ savedImages.push({
221
+ kind: "image",
222
+ path: targetPath,
223
+ label: index === 0 ? "Generated image" : `Generated image ${index + 1}`,
224
+ mimeType: contentType ?? undefined,
225
+ sizeBytes: buffer.length,
226
+ sourceUrl: image.url,
227
+ url: image.url,
228
+ finalUrl: response.url,
229
+ alt: image.alt,
230
+ width: image.width,
231
+ height: image.height,
232
+ fileId: image.fileId,
233
+ });
234
+ }
235
+ catch (error) {
236
+ const message = error instanceof Error ? error.message : String(error);
237
+ errors.push(`${image.fileId ?? image.url}: ${message}`);
238
+ logger?.(`[browser] Failed to save generated image ${index + 1}/${images.length}: ${message}`);
239
+ }
240
+ }
241
+ return {
242
+ saved: savedImages.length > 0,
243
+ imageCount: images.length,
244
+ savedImages,
245
+ errors,
246
+ };
247
+ }
248
+ export async function collectGeneratedImageArtifacts(params) {
249
+ const explicitTargetPath = params.generateImagePath ?? params.outputPath;
250
+ let generatedImages = await readAssistantGeneratedImagesWithFallback(params.Runtime, params.minTurnIndex ?? undefined);
251
+ let latestAnswerText = params.answerText;
252
+ if (explicitTargetPath && generatedImages.length === 0) {
253
+ const deadline = Date.now() + resolveGeneratedImageWaitTimeoutMs(params.waitTimeoutMs);
254
+ while (Date.now() < deadline) {
255
+ await delay(1500);
256
+ generatedImages = await readAssistantGeneratedImagesWithFallback(params.Runtime, params.minTurnIndex ?? undefined);
257
+ if (generatedImages.length > 0) {
258
+ break;
259
+ }
260
+ const latestSnapshot = await readAssistantSnapshot(params.Runtime, params.minTurnIndex ?? undefined).catch(() => null);
261
+ const snapshotText = typeof latestSnapshot?.text === "string" ? latestSnapshot.text.trim() : "";
262
+ if (snapshotText) {
263
+ latestAnswerText = snapshotText;
264
+ }
265
+ }
266
+ }
267
+ const imageCount = generatedImages.length;
268
+ if (explicitTargetPath && imageCount === 0) {
269
+ throw new Error(`No images generated. Response text:\n${latestAnswerText || "(empty response)"}`);
270
+ }
271
+ if (imageCount === 0) {
272
+ return {
273
+ generatedImages,
274
+ savedImages: [],
275
+ imageCount,
276
+ markdownSuffix: "",
277
+ answerText: latestAnswerText,
278
+ };
279
+ }
280
+ const targetPath = explicitTargetPath ?? resolveDefaultGeneratedImagePath(generatedImages, params.sessionId);
281
+ if (!explicitTargetPath) {
282
+ params.logger?.(`[browser] Auto-saving generated images to ${targetPath}`);
283
+ }
284
+ const saved = await saveChatGptGeneratedImages({
285
+ Network: params.Network,
286
+ images: generatedImages,
287
+ outputPath: targetPath,
288
+ logger: params.logger,
289
+ });
290
+ if (!saved.saved) {
291
+ const detail = saved.errors.length > 0 ? `\n${saved.errors.join("\n")}` : "";
292
+ if (explicitTargetPath) {
293
+ throw new Error(`No images generated. Response text:\n${latestAnswerText || "(empty response)"}${detail}`);
294
+ }
295
+ params.logger?.(`[browser] Auto-save for generated images failed; returning metadata only.${detail}`);
296
+ return {
297
+ generatedImages,
298
+ savedImages: [],
299
+ imageCount,
300
+ markdownSuffix: `\n\n*Generated ${imageCount} image(s).*`,
301
+ answerText: latestAnswerText,
302
+ };
303
+ }
304
+ const primaryPath = saved.savedImages[0]?.path ?? targetPath;
305
+ const suffix = saved.savedImages.length > 1
306
+ ? `\n\n*Generated ${saved.imageCount} image(s). Saved ${saved.savedImages.length} file(s) starting at: ${primaryPath}*`
307
+ : `\n\n*Generated ${saved.imageCount} image(s). Saved to: ${primaryPath}*`;
308
+ return {
309
+ generatedImages,
310
+ savedImages: saved.savedImages,
311
+ imageCount: saved.imageCount,
312
+ markdownSuffix: suffix,
313
+ answerText: latestAnswerText,
314
+ };
315
+ }