codex-chatgpt-control 0.1.0-alpha.1 → 0.2.0-alpha.1

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 (242) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +55 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +7 -0
  4. package/contracts/v1/fixtures/blocker-explanation-profiles.json +114 -0
  5. package/contracts/v1/fixtures/command-descriptors.json +192 -15
  6. package/contracts/v1/fixtures/doctor-bridge-upload.json +8 -0
  7. package/contracts/v1/fixtures/doctor-scenario-preflight.json +108 -0
  8. package/contracts/v1/fixtures/existing-tab-diagnostics-blocker.json +57 -0
  9. package/contracts/v1/fixtures/files-preflight-success.json +32 -0
  10. package/contracts/v1/fixtures/help-root.json +1 -1
  11. package/contracts/v1/fixtures/output-json-parse-success.json +17 -2
  12. package/contracts/v1/fixtures/project-sources-plan-add.json +71 -0
  13. package/contracts/v1/fixtures/read-latest-clipped.json +31 -0
  14. package/contracts/v1/fixtures/reports-create-redacted.json +15 -0
  15. package/contracts/v1/fixtures/responses-basic-success.json +11 -0
  16. package/contracts/v1/fixtures/run-basic-success.json +15 -0
  17. package/contracts/v1/fixtures/run-browser-bridge-blocker.json +2 -2
  18. package/contracts/v1/fixtures/run-timeout-partial.json +2 -2
  19. package/contracts/v1/fixtures/runner-budget-blocker.json +1 -1
  20. package/contracts/v1/fixtures/stream-blocked.ndjson +1 -1
  21. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
  22. package/contracts/v1/manifest.json +30 -0
  23. package/contracts/v1/parity-suite.json +72 -6
  24. package/contracts/v1/schemas/backend-request.schema.json +3 -0
  25. package/contracts/v1/schemas/command-result.schema.json +48 -0
  26. package/contracts/v1/schemas/response.schema.json +28 -0
  27. package/contracts/v1/schemas/run-result.schema.json +84 -0
  28. package/contracts/v1/surface-drift-policy.json +210 -0
  29. package/dist/codex-chatgpt-control-backend.mjs +5137 -488
  30. package/dist/codex-chatgpt-control.bundle.mjs +5001 -286
  31. package/dist/src/backend/client.d.ts +6 -1
  32. package/dist/src/backend/client.js +5 -0
  33. package/dist/src/backend/protocol.d.ts +1 -1
  34. package/dist/src/backend/protocol.js +7 -0
  35. package/dist/src/backend/session.js +14 -0
  36. package/dist/src/browser/attach.js +128 -20
  37. package/dist/src/browser/downloads.js +2 -1
  38. package/dist/src/browser/page-state.js +6 -3
  39. package/dist/src/client.d.ts +16 -1
  40. package/dist/src/client.js +86 -7
  41. package/dist/src/commands/artifacts.d.ts +5 -0
  42. package/dist/src/commands/artifacts.js +432 -0
  43. package/dist/src/commands/context.js +5 -2
  44. package/dist/src/commands/doctor.d.ts +10 -2
  45. package/dist/src/commands/doctor.js +242 -14
  46. package/dist/src/commands/files.d.ts +2 -1
  47. package/dist/src/commands/files.js +352 -18
  48. package/dist/src/commands/messages.d.ts +2 -1
  49. package/dist/src/commands/messages.js +232 -40
  50. package/dist/src/commands/modes.js +202 -20
  51. package/dist/src/commands/project-sources.d.ts +10 -0
  52. package/dist/src/commands/project-sources.js +643 -0
  53. package/dist/src/commands/registry.js +76 -14
  54. package/dist/src/commands/reports.d.ts +9 -0
  55. package/dist/src/commands/reports.js +83 -6
  56. package/dist/src/commands/response-actions.js +33 -0
  57. package/dist/src/commands/sequence.js +14 -0
  58. package/dist/src/commands/threads.js +2 -1
  59. package/dist/src/commands/timeouts.d.ts +2 -0
  60. package/dist/src/commands/timeouts.js +18 -0
  61. package/dist/src/diagnostics/blockers.d.ts +54 -0
  62. package/dist/src/diagnostics/blockers.js +299 -0
  63. package/dist/src/dom/artifacts.d.ts +7 -0
  64. package/dist/src/dom/artifacts.js +198 -0
  65. package/dist/src/dom/generation-state.d.ts +8 -0
  66. package/dist/src/dom/generation-state.js +103 -0
  67. package/dist/src/dom/locale/am.d.ts +28 -0
  68. package/dist/src/dom/locale/am.js +28 -0
  69. package/dist/src/dom/locale/ar.d.ts +28 -0
  70. package/dist/src/dom/locale/ar.js +28 -0
  71. package/dist/src/dom/locale/bg.d.ts +28 -0
  72. package/dist/src/dom/locale/bg.js +28 -0
  73. package/dist/src/dom/locale/bn.d.ts +28 -0
  74. package/dist/src/dom/locale/bn.js +28 -0
  75. package/dist/src/dom/locale/bs.d.ts +28 -0
  76. package/dist/src/dom/locale/bs.js +28 -0
  77. package/dist/src/dom/locale/ca.d.ts +28 -0
  78. package/dist/src/dom/locale/ca.js +28 -0
  79. package/dist/src/dom/locale/cs.d.ts +28 -0
  80. package/dist/src/dom/locale/cs.js +28 -0
  81. package/dist/src/dom/locale/da.d.ts +28 -0
  82. package/dist/src/dom/locale/da.js +28 -0
  83. package/dist/src/dom/locale/de.d.ts +30 -0
  84. package/dist/src/dom/locale/de.js +30 -0
  85. package/dist/src/dom/locale/el.d.ts +28 -0
  86. package/dist/src/dom/locale/el.js +28 -0
  87. package/dist/src/dom/locale/en.d.ts +50 -0
  88. package/dist/src/dom/locale/en.js +56 -0
  89. package/dist/src/dom/locale/es-419.d.ts +28 -0
  90. package/dist/src/dom/locale/es-419.js +28 -0
  91. package/dist/src/dom/locale/es-ES.d.ts +28 -0
  92. package/dist/src/dom/locale/es-ES.js +28 -0
  93. package/dist/src/dom/locale/et.d.ts +28 -0
  94. package/dist/src/dom/locale/et.js +28 -0
  95. package/dist/src/dom/locale/fa.d.ts +28 -0
  96. package/dist/src/dom/locale/fa.js +28 -0
  97. package/dist/src/dom/locale/fi.d.ts +28 -0
  98. package/dist/src/dom/locale/fi.js +28 -0
  99. package/dist/src/dom/locale/fr-CA.d.ts +29 -0
  100. package/dist/src/dom/locale/fr-CA.js +29 -0
  101. package/dist/src/dom/locale/fr-FR.d.ts +30 -0
  102. package/dist/src/dom/locale/fr-FR.js +30 -0
  103. package/dist/src/dom/locale/gu.d.ts +28 -0
  104. package/dist/src/dom/locale/gu.js +28 -0
  105. package/dist/src/dom/locale/hi.d.ts +28 -0
  106. package/dist/src/dom/locale/hi.js +28 -0
  107. package/dist/src/dom/locale/hr.d.ts +28 -0
  108. package/dist/src/dom/locale/hr.js +28 -0
  109. package/dist/src/dom/locale/hu.d.ts +28 -0
  110. package/dist/src/dom/locale/hu.js +28 -0
  111. package/dist/src/dom/locale/hy.d.ts +28 -0
  112. package/dist/src/dom/locale/hy.js +28 -0
  113. package/dist/src/dom/locale/id.d.ts +28 -0
  114. package/dist/src/dom/locale/id.js +28 -0
  115. package/dist/src/dom/locale/index.d.ts +79 -0
  116. package/dist/src/dom/locale/index.js +202 -0
  117. package/dist/src/dom/locale/is.d.ts +28 -0
  118. package/dist/src/dom/locale/is.js +28 -0
  119. package/dist/src/dom/locale/it.d.ts +27 -0
  120. package/dist/src/dom/locale/it.js +27 -0
  121. package/dist/src/dom/locale/ja.d.ts +27 -0
  122. package/dist/src/dom/locale/ja.js +27 -0
  123. package/dist/src/dom/locale/ka.d.ts +28 -0
  124. package/dist/src/dom/locale/ka.js +28 -0
  125. package/dist/src/dom/locale/kk.d.ts +28 -0
  126. package/dist/src/dom/locale/kk.js +28 -0
  127. package/dist/src/dom/locale/kn.d.ts +28 -0
  128. package/dist/src/dom/locale/kn.js +28 -0
  129. package/dist/src/dom/locale/ko.d.ts +28 -0
  130. package/dist/src/dom/locale/ko.js +28 -0
  131. package/dist/src/dom/locale/lt.d.ts +28 -0
  132. package/dist/src/dom/locale/lt.js +28 -0
  133. package/dist/src/dom/locale/lv.d.ts +28 -0
  134. package/dist/src/dom/locale/lv.js +28 -0
  135. package/dist/src/dom/locale/mk.d.ts +28 -0
  136. package/dist/src/dom/locale/mk.js +28 -0
  137. package/dist/src/dom/locale/ml.d.ts +28 -0
  138. package/dist/src/dom/locale/ml.js +28 -0
  139. package/dist/src/dom/locale/mn.d.ts +28 -0
  140. package/dist/src/dom/locale/mn.js +28 -0
  141. package/dist/src/dom/locale/mr.d.ts +28 -0
  142. package/dist/src/dom/locale/mr.js +28 -0
  143. package/dist/src/dom/locale/ms.d.ts +28 -0
  144. package/dist/src/dom/locale/ms.js +28 -0
  145. package/dist/src/dom/locale/my.d.ts +28 -0
  146. package/dist/src/dom/locale/my.js +28 -0
  147. package/dist/src/dom/locale/nb.d.ts +28 -0
  148. package/dist/src/dom/locale/nb.js +28 -0
  149. package/dist/src/dom/locale/nl.d.ts +28 -0
  150. package/dist/src/dom/locale/nl.js +28 -0
  151. package/dist/src/dom/locale/pa.d.ts +28 -0
  152. package/dist/src/dom/locale/pa.js +28 -0
  153. package/dist/src/dom/locale/pl.d.ts +28 -0
  154. package/dist/src/dom/locale/pl.js +28 -0
  155. package/dist/src/dom/locale/pt-BR.d.ts +28 -0
  156. package/dist/src/dom/locale/pt-BR.js +28 -0
  157. package/dist/src/dom/locale/pt-PT.d.ts +28 -0
  158. package/dist/src/dom/locale/pt-PT.js +28 -0
  159. package/dist/src/dom/locale/ro.d.ts +28 -0
  160. package/dist/src/dom/locale/ro.js +28 -0
  161. package/dist/src/dom/locale/ru.d.ts +28 -0
  162. package/dist/src/dom/locale/ru.js +28 -0
  163. package/dist/src/dom/locale/sk.d.ts +28 -0
  164. package/dist/src/dom/locale/sk.js +28 -0
  165. package/dist/src/dom/locale/sl.d.ts +28 -0
  166. package/dist/src/dom/locale/sl.js +28 -0
  167. package/dist/src/dom/locale/so.d.ts +28 -0
  168. package/dist/src/dom/locale/so.js +28 -0
  169. package/dist/src/dom/locale/sq.d.ts +28 -0
  170. package/dist/src/dom/locale/sq.js +28 -0
  171. package/dist/src/dom/locale/sr.d.ts +28 -0
  172. package/dist/src/dom/locale/sr.js +28 -0
  173. package/dist/src/dom/locale/sv.d.ts +28 -0
  174. package/dist/src/dom/locale/sv.js +28 -0
  175. package/dist/src/dom/locale/sw.d.ts +28 -0
  176. package/dist/src/dom/locale/sw.js +28 -0
  177. package/dist/src/dom/locale/ta.d.ts +28 -0
  178. package/dist/src/dom/locale/ta.js +28 -0
  179. package/dist/src/dom/locale/te.d.ts +28 -0
  180. package/dist/src/dom/locale/te.js +28 -0
  181. package/dist/src/dom/locale/th.d.ts +28 -0
  182. package/dist/src/dom/locale/th.js +28 -0
  183. package/dist/src/dom/locale/tl.d.ts +28 -0
  184. package/dist/src/dom/locale/tl.js +28 -0
  185. package/dist/src/dom/locale/tr.d.ts +28 -0
  186. package/dist/src/dom/locale/tr.js +28 -0
  187. package/dist/src/dom/locale/types.d.ts +45 -0
  188. package/dist/src/dom/locale/types.js +1 -0
  189. package/dist/src/dom/locale/uk.d.ts +28 -0
  190. package/dist/src/dom/locale/uk.js +28 -0
  191. package/dist/src/dom/locale/ur.d.ts +28 -0
  192. package/dist/src/dom/locale/ur.js +28 -0
  193. package/dist/src/dom/locale/vi.d.ts +28 -0
  194. package/dist/src/dom/locale/vi.js +28 -0
  195. package/dist/src/dom/locale/zh-HK.d.ts +28 -0
  196. package/dist/src/dom/locale/zh-HK.js +28 -0
  197. package/dist/src/dom/locale/zh-Hans.d.ts +28 -0
  198. package/dist/src/dom/locale/zh-Hans.js +28 -0
  199. package/dist/src/dom/locale/zh-TW.d.ts +29 -0
  200. package/dist/src/dom/locale/zh-TW.js +29 -0
  201. package/dist/src/dom/locale-labels.d.ts +26 -0
  202. package/dist/src/dom/locale-labels.js +26 -0
  203. package/dist/src/dom/menus.d.ts +6 -0
  204. package/dist/src/dom/menus.js +34 -8
  205. package/dist/src/dom/message-format.d.ts +2 -1
  206. package/dist/src/dom/message-format.js +67 -10
  207. package/dist/src/dom/messages.d.ts +2 -1
  208. package/dist/src/dom/messages.js +2 -4
  209. package/dist/src/dom/selectors.d.ts +1 -0
  210. package/dist/src/dom/selectors.js +42 -15
  211. package/dist/src/index.d.ts +5 -0
  212. package/dist/src/index.js +5 -0
  213. package/dist/src/platform/local-paths.d.ts +4 -0
  214. package/dist/src/platform/local-paths.js +20 -0
  215. package/dist/src/runner/interruptions.js +15 -4
  216. package/dist/src/runner/responses.js +18 -1
  217. package/dist/src/runner/result.js +15 -0
  218. package/dist/src/runner/resume.js +1 -0
  219. package/dist/src/runner/types.d.ts +3 -0
  220. package/dist/src/safety/risk.d.ts +7 -0
  221. package/dist/src/safety/risk.js +7 -0
  222. package/dist/src/safety/untrusted-output.d.ts +73 -0
  223. package/dist/src/safety/untrusted-output.js +199 -0
  224. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +1 -0
  225. package/dist/src/scripts/apply-intelligence-locale-captures.js +184 -0
  226. package/dist/src/scripts/capture-intelligence-locales.d.ts +25 -0
  227. package/dist/src/scripts/capture-intelligence-locales.js +669 -0
  228. package/dist/src/scripts/check-surface-drift.d.ts +1 -0
  229. package/dist/src/scripts/check-surface-drift.js +11 -0
  230. package/dist/src/scripts/live-smoke/scenarios.js +84 -0
  231. package/dist/src/scripts/locale-capture/language-coverage.d.ts +10 -0
  232. package/dist/src/scripts/locale-capture/language-coverage.js +39 -0
  233. package/dist/src/testing/surface-drift.d.ts +66 -0
  234. package/dist/src/testing/surface-drift.js +363 -0
  235. package/dist/src/types.d.ts +201 -2
  236. package/package.json +2 -1
  237. package/references/backend-protocol.md +93 -1
  238. package/references/language-coverage.md +142 -0
  239. package/references/localization.md +247 -0
  240. package/references/python-parity.md +63 -1
  241. package/references/safety.md +2 -0
  242. package/references/troubleshooting.md +58 -0
@@ -0,0 +1,432 @@
1
+ import { copyFile, mkdir, stat, writeFile } from "node:fs/promises";
2
+ import { basename, join, resolve } from "node:path";
3
+ import { waitForDownloadFromClick } from "../browser/downloads.js";
4
+ import { readPageState } from "../browser/page-state.js";
5
+ import { listPageArtifacts, readLatestImageDataUrl } from "../dom/artifacts.js";
6
+ import { cssSelectors, requiredLocator } from "../dom/selectors.js";
7
+ import { localeLabels } from "../dom/locale-labels.js";
8
+ import { resultOk } from "../errors.js";
9
+ import { contextFromPage } from "./context.js";
10
+ import { bootstrap } from "./session.js";
11
+ import { localGuardTimeout, withTimeout } from "./timeouts.js";
12
+ export async function listLatestArtifacts(env, args = {}) {
13
+ const boot = await ensurePage(env);
14
+ if (!boot.ok) {
15
+ return boot;
16
+ }
17
+ const page = env.page;
18
+ try {
19
+ const artifacts = await listPageArtifactsWithBridgeFallback(env, page, args);
20
+ return resultOk(artifactListData(artifacts), await contextFromPage(page));
21
+ }
22
+ catch (error) {
23
+ return artifactSelectorBlocker(error, await contextFromPage(page));
24
+ }
25
+ }
26
+ export async function waitForArtifact(env, args = {}) {
27
+ const boot = await ensurePage(env);
28
+ if (!boot.ok) {
29
+ return boot;
30
+ }
31
+ const page = env.page;
32
+ const timeoutMs = args.timeoutMs ?? 120000;
33
+ const stableMs = args.stableMs ?? 1000;
34
+ const pollMs = args.pollMs ?? 750;
35
+ const started = Date.now();
36
+ const afterArtifactCount = args.afterArtifactCount ?? 0;
37
+ let lastSignature = "";
38
+ let lastChangedAt = Date.now();
39
+ let latestArtifacts = [];
40
+ while (Date.now() - started < timeoutMs) {
41
+ const state = await withTimeout(readPageState(page), localGuardTimeout(timeoutMs, 5000), "Timed out while reading ChatGPT page state.").catch(() => undefined);
42
+ if (state?.blocker !== undefined && state.blocker.kind !== "modal") {
43
+ return {
44
+ ok: false,
45
+ status: "blocked",
46
+ warnings: [],
47
+ blocker: state.blocker,
48
+ context: await contextFromPage(page)
49
+ };
50
+ }
51
+ try {
52
+ latestArtifacts = await listPageArtifactsWithBridgeFallback(env, page, args);
53
+ }
54
+ catch (error) {
55
+ return artifactSelectorBlocker(error, await contextFromPage(page));
56
+ }
57
+ const latest = latestArtifacts.at(-1);
58
+ const signature = JSON.stringify({
59
+ count: latestArtifacts.length,
60
+ src: latest?.src,
61
+ width: latest?.width,
62
+ height: latest?.height,
63
+ downloadAvailable: latest?.downloadAvailable
64
+ });
65
+ if (signature !== lastSignature) {
66
+ lastSignature = signature;
67
+ lastChangedAt = Date.now();
68
+ }
69
+ const targetReached = latestArtifacts.length > afterArtifactCount
70
+ && latest !== undefined
71
+ && (args.requireDownload !== true || latest.downloadAvailable);
72
+ if (targetReached && Date.now() - lastChangedAt >= stableMs && !await hasStopControl(page, timeoutMs)) {
73
+ return resultOk({
74
+ complete: true,
75
+ count: latestArtifacts.length,
76
+ latest,
77
+ elapsedMs: Date.now() - started
78
+ }, await contextFromPage(page));
79
+ }
80
+ await sleep(page, pollMs);
81
+ }
82
+ const data = {
83
+ complete: false,
84
+ count: latestArtifacts.length,
85
+ elapsedMs: Date.now() - started
86
+ };
87
+ const latest = latestArtifacts.at(-1);
88
+ if (latest !== undefined)
89
+ data.latest = latest;
90
+ return {
91
+ ok: false,
92
+ status: "timeout",
93
+ data,
94
+ warnings: [],
95
+ blocker: {
96
+ kind: "artifact_unavailable",
97
+ code: args.requireDownload === true ? "artifact_download_not_ready" : "artifact_not_ready",
98
+ message: args.requireDownload === true
99
+ ? "No generated artifact with a visible download affordance appeared before the timeout."
100
+ : "No generated artifact appeared before the timeout.",
101
+ resumable: true
102
+ },
103
+ context: await contextFromPage(page)
104
+ };
105
+ }
106
+ export async function downloadLatestArtifact(env, args) {
107
+ const boot = await ensurePage(env);
108
+ if (!boot.ok) {
109
+ return boot;
110
+ }
111
+ const page = env.page;
112
+ const timeoutMs = args.timeoutMs ?? 120000;
113
+ if (args.prefer !== "visible_image_source") {
114
+ const byDownload = await tryDownloadControl(page, args, timeoutMs);
115
+ if (byDownload.ok || args.prefer === "download_control") {
116
+ return byDownload;
117
+ }
118
+ }
119
+ try {
120
+ const byImageSource = await saveLatestVisibleImageSource(page, args.destDir, timeoutMs);
121
+ if (byImageSource !== undefined) {
122
+ return resultOk(byImageSource, await contextFromPage(page));
123
+ }
124
+ }
125
+ catch (error) {
126
+ return artifactDownloadBlocker(error, await contextFromPage(page));
127
+ }
128
+ try {
129
+ const byPageAssets = await saveLatestPageAssetImage(env, page, args.destDir, timeoutMs);
130
+ if (byPageAssets !== undefined) {
131
+ return resultOk(byPageAssets, await contextFromPage(page));
132
+ }
133
+ }
134
+ catch (error) {
135
+ return artifactDownloadBlocker(error, await contextFromPage(page));
136
+ }
137
+ return artifactDownloadBlocker(new Error("No visible generated image source was available to save."), await contextFromPage(page));
138
+ }
139
+ export async function locatorCountWithTimeout(locator, timeoutMs, code) {
140
+ if (locator === undefined || typeof locator.count !== "function") {
141
+ return 0;
142
+ }
143
+ return withTimeout(locator.count(), timeoutMs, `${code}: locator count did not complete before the local guard timeout.`);
144
+ }
145
+ async function tryDownloadControl(page, args, timeoutMs) {
146
+ try {
147
+ const controls = requiredLocator(page, cssSelectors.generatedArtifactDownloadControls);
148
+ const count = await locatorCountWithTimeout(controls, localGuardTimeout(timeoutMs, 5000), "artifact_download_control_timeout");
149
+ if (count === 0) {
150
+ return artifactDownloadBlocker(new Error("No visible generated-image download control was found."), await contextFromPage(page));
151
+ }
152
+ const target = controls.last?.() ?? controls;
153
+ const downloaded = await waitForDownloadFromClick(page, async () => {
154
+ await target.click?.({ timeoutMs: localGuardTimeout(timeoutMs, 10000) });
155
+ }, args.destDir, timeoutMs);
156
+ return resultOk(downloaded, await contextFromPage(page));
157
+ }
158
+ catch (error) {
159
+ return artifactDownloadBlocker(error, await contextFromPage(page));
160
+ }
161
+ }
162
+ async function saveLatestVisibleImageSource(page, destDir, timeoutMs) {
163
+ const source = await readLatestImageDataUrl(page, timeoutMs);
164
+ if (source === undefined)
165
+ return undefined;
166
+ const parsed = parseDataUrl(source.dataUrl);
167
+ if (parsed === undefined)
168
+ return undefined;
169
+ const absoluteDest = resolve(destDir);
170
+ await mkdir(absoluteDest, { recursive: true });
171
+ const suggestedFilename = `generated-image-${Date.now()}.${extensionForMime(parsed.mimeType)}`;
172
+ const path = join(absoluteDest, suggestedFilename);
173
+ await writeFile(path, parsed.bytes);
174
+ const saved = await stat(path);
175
+ if (saved.size <= 0) {
176
+ throw new Error(`Generated image artifact file is empty: ${path}`);
177
+ }
178
+ return { path, suggestedFilename, bytes: saved.size };
179
+ }
180
+ async function listPageArtifactsWithBridgeFallback(env, page, args) {
181
+ try {
182
+ const artifacts = await listPageArtifacts(page, args);
183
+ if (artifacts.length > 0) {
184
+ return artifacts;
185
+ }
186
+ const fromAssets = await listPageAssetArtifacts(env, page, args, args.timeoutMs).catch(() => []);
187
+ return fromAssets.length > 0 ? fromAssets : artifacts;
188
+ }
189
+ catch (error) {
190
+ const fromAssets = await listPageAssetArtifacts(env, page, args, args.timeoutMs).catch(() => []);
191
+ if (fromAssets.length > 0) {
192
+ return fromAssets;
193
+ }
194
+ throw error;
195
+ }
196
+ }
197
+ async function listPageAssetArtifacts(env, page, args, timeoutMs) {
198
+ const inventory = await readPageAssetsInventory(page, timeoutMs).catch(() => undefined)
199
+ ?? await withTemporaryBridgeOwnedPage(env, page, timeoutMs, async (freshPage) => {
200
+ return await readPageAssetsInventory(freshPage, timeoutMs).catch(() => undefined);
201
+ });
202
+ if (inventory === undefined)
203
+ return [];
204
+ const artifacts = inventory.assets
205
+ .filter(asset => asset.kind === "image")
206
+ .filter(asset => !isInlineSvgAsset(asset) && isLikelyRasterImageAsset(asset))
207
+ .map((asset, index) => {
208
+ const artifact = {
209
+ kind: "image",
210
+ index,
211
+ visible: true,
212
+ downloadAvailable: true,
213
+ selectorProvenance: "pageAssets image inventory"
214
+ };
215
+ const src = safeArtifactSrc(asset.url);
216
+ if (src !== undefined)
217
+ artifact.src = src;
218
+ return artifact;
219
+ });
220
+ const max = args.max ?? artifacts.length;
221
+ return artifacts
222
+ .filter(artifact => artifact.kind === (args.kind ?? "image"))
223
+ .slice(-max)
224
+ .map((artifact, index) => ({ ...artifact, index }));
225
+ }
226
+ async function saveLatestPageAssetImage(env, page, destDir, timeoutMs) {
227
+ return await saveLatestPageAssetImageFromPage(page, destDir, timeoutMs).catch(() => undefined)
228
+ ?? await withTemporaryBridgeOwnedPage(env, page, timeoutMs, async (freshPage) => {
229
+ return await saveLatestPageAssetImageFromPage(freshPage, destDir, timeoutMs).catch(() => undefined);
230
+ });
231
+ }
232
+ async function saveLatestPageAssetImageFromPage(page, destDir, timeoutMs) {
233
+ const capability = await getPageAssetsCapability(page);
234
+ if (capability === undefined)
235
+ return undefined;
236
+ const inventory = await withTimeout(capability.list(), localGuardTimeout(timeoutMs, 15000), "Timed out while listing page assets for generated image download.");
237
+ const candidateIds = inventory.assets
238
+ .filter(asset => asset.kind === "image")
239
+ .filter(asset => !isInlineSvgAsset(asset) && isLikelyRasterImageAsset(asset))
240
+ .map(asset => asset.id);
241
+ if (candidateIds.length === 0)
242
+ return undefined;
243
+ const bundled = await withTimeout(capability.bundle({ assetIds: candidateIds, inventoryId: inventory.id, kinds: ["image"] }), localGuardTimeout(timeoutMs, 30000), "Timed out while bundling generated image page asset.");
244
+ const asset = bundled.assets
245
+ .filter(item => !isInlineSvgAsset(item) && isLikelyRasterImageAsset(item))
246
+ .at(-1);
247
+ if (asset === undefined)
248
+ return undefined;
249
+ const absoluteDest = resolve(destDir);
250
+ await mkdir(absoluteDest, { recursive: true });
251
+ const suggestedFilename = `generated-image-${Date.now()}.${extensionForMime(asset.contentType ?? "image/png")}`;
252
+ const path = join(absoluteDest, suggestedFilename);
253
+ await copyFile(asset.path, path);
254
+ const saved = await stat(path);
255
+ if (saved.size <= 0) {
256
+ throw new Error(`Generated image artifact file is empty: ${path}`);
257
+ }
258
+ return { path, suggestedFilename, bytes: saved.size };
259
+ }
260
+ async function readPageAssetsInventory(page, timeoutMs) {
261
+ const capability = await getPageAssetsCapability(page);
262
+ if (capability === undefined)
263
+ return undefined;
264
+ return await withTimeout(capability.list(), localGuardTimeout(timeoutMs, 15000), "Timed out while listing page assets for generated artifacts.");
265
+ }
266
+ async function getPageAssetsCapability(page) {
267
+ const capabilities = page.capabilities;
268
+ const get = capabilities?.get;
269
+ if (typeof get !== "function")
270
+ return undefined;
271
+ const capability = await get.call(capabilities, "pageAssets");
272
+ if (!isPageAssetsCapability(capability))
273
+ return undefined;
274
+ return capability;
275
+ }
276
+ async function withTemporaryBridgeOwnedPage(env, currentPage, timeoutMs, callback) {
277
+ const url = await currentPageUrl(currentPage);
278
+ if (url === undefined || !/^https:\/\/chatgpt\.com\/c\//i.test(url))
279
+ return undefined;
280
+ const freshPage = await openTemporaryPage(env, url, timeoutMs);
281
+ if (freshPage === undefined)
282
+ return undefined;
283
+ try {
284
+ await settlePage(freshPage, localGuardTimeout(timeoutMs, 5000));
285
+ return await callback(freshPage);
286
+ }
287
+ finally {
288
+ await closeTemporaryPage(freshPage).catch(() => undefined);
289
+ }
290
+ }
291
+ async function openTemporaryPage(env, url, timeoutMs) {
292
+ const browser = env.browser;
293
+ if (browser === undefined)
294
+ return undefined;
295
+ let page;
296
+ if (typeof browser.tabs?.create === "function") {
297
+ page = await Promise.resolve(browser.tabs.create.call(browser.tabs, url));
298
+ }
299
+ else if (typeof browser.tabs?.new === "function") {
300
+ page = await Promise.resolve(browser.tabs.new.call(browser.tabs));
301
+ if (typeof page?.goto === "function") {
302
+ await withTimeout(page.goto(url), localGuardTimeout(timeoutMs, 20000), "Timed out while opening generated image conversation in a temporary bridge tab.").catch(() => undefined);
303
+ }
304
+ }
305
+ else if (typeof browser.newPage === "function") {
306
+ page = await Promise.resolve(browser.newPage.call(browser));
307
+ if (typeof page?.goto === "function") {
308
+ await withTimeout(page.goto(url), localGuardTimeout(timeoutMs, 20000), "Timed out while opening generated image conversation in a temporary bridge page.").catch(() => undefined);
309
+ }
310
+ }
311
+ return page;
312
+ }
313
+ async function settlePage(page, timeoutMs) {
314
+ const waitForTimeout = page.waitForTimeout ?? page.playwright?.waitForTimeout;
315
+ if (typeof waitForTimeout !== "function")
316
+ return;
317
+ await withTimeout(waitForTimeout.call(page.waitForTimeout === waitForTimeout ? page : page.playwright, Math.min(timeoutMs, 5000)), timeoutMs, "Timed out while waiting for temporary bridge tab to settle.").catch(() => undefined);
318
+ }
319
+ async function closeTemporaryPage(page) {
320
+ if (typeof page.close === "function") {
321
+ await page.close();
322
+ }
323
+ }
324
+ async function currentPageUrl(page) {
325
+ const value = await Promise.resolve(page.url?.()).catch(() => undefined);
326
+ return typeof value === "string" && value.length > 0 ? value : undefined;
327
+ }
328
+ function isPageAssetsCapability(value) {
329
+ return typeof value === "object"
330
+ && value !== null
331
+ && typeof value.list === "function"
332
+ && typeof value.bundle === "function";
333
+ }
334
+ function isLikelyRasterImageAsset(asset) {
335
+ const contentType = asset.contentType ?? "";
336
+ if (/^image\/(png|jpe?g|webp|gif|avif)$/i.test(contentType))
337
+ return true;
338
+ const name = asset.name ?? basename(asset.path ?? "");
339
+ const url = asset.url ?? "";
340
+ return /\.(png|jpe?g|webp|gif|avif)(?:$|[?#])/i.test(name)
341
+ || /\.(png|jpe?g|webp|gif|avif)(?:$|[?#])/i.test(url)
342
+ || (contentType === "" && !isInlineSvgAsset(asset));
343
+ }
344
+ function isInlineSvgAsset(asset) {
345
+ return /^inline-svg:/i.test(asset.url ?? "")
346
+ || /svg/i.test(asset.contentType ?? "")
347
+ || /\.svg(?:$|[?#])/i.test(asset.name ?? "")
348
+ || /\.svg(?:$|[?#])/i.test(asset.path ?? "");
349
+ }
350
+ function safeArtifactSrc(src) {
351
+ if (src === undefined)
352
+ return undefined;
353
+ if (/^https:\/\/chatgpt\.com\/backend-api\/estuary\/content\b/i.test(src)) {
354
+ return undefined;
355
+ }
356
+ return src;
357
+ }
358
+ function parseDataUrl(dataUrl) {
359
+ const match = /^data:([^;,]+);base64,(.*)$/i.exec(dataUrl);
360
+ if (match === null || match[1] === undefined || match[2] === undefined)
361
+ return undefined;
362
+ return { mimeType: match[1], bytes: Buffer.from(match[2], "base64") };
363
+ }
364
+ function extensionForMime(mimeType) {
365
+ if (/jpeg|jpg/i.test(mimeType))
366
+ return "jpg";
367
+ if (/webp/i.test(mimeType))
368
+ return "webp";
369
+ if (/gif/i.test(mimeType))
370
+ return "gif";
371
+ return "png";
372
+ }
373
+ function artifactListData(artifacts) {
374
+ const data = {
375
+ count: artifacts.length,
376
+ artifacts
377
+ };
378
+ const latest = artifacts.at(-1);
379
+ if (latest !== undefined)
380
+ data.latest = latest;
381
+ return data;
382
+ }
383
+ function artifactSelectorBlocker(error, context) {
384
+ return {
385
+ ok: false,
386
+ status: "blocked",
387
+ warnings: [],
388
+ blocker: {
389
+ kind: "artifact_selector_drift",
390
+ code: "artifact_dom_timeout",
391
+ message: `Generated artifact detection could not inspect the ChatGPT page: ${error instanceof Error ? error.message : String(error)}`,
392
+ resumable: true
393
+ },
394
+ context
395
+ };
396
+ }
397
+ function artifactDownloadBlocker(error, context) {
398
+ return {
399
+ ok: false,
400
+ status: "unsupported",
401
+ warnings: [],
402
+ blocker: {
403
+ kind: "artifact_download_unavailable",
404
+ code: "artifact_download_unavailable",
405
+ message: `No downloadable generated artifact could be saved from the visible ChatGPT page: ${error instanceof Error ? error.message : String(error)}`,
406
+ resumable: true
407
+ },
408
+ context
409
+ };
410
+ }
411
+ async function ensurePage(env) {
412
+ if (env.page !== undefined) {
413
+ return resultOk({}, await contextFromPage(env.page));
414
+ }
415
+ return bootstrap(env, { preferExistingTab: true });
416
+ }
417
+ async function hasStopControl(page, timeoutMs) {
418
+ if (typeof page.evaluate !== "function")
419
+ return false;
420
+ return withTimeout(page.evaluate((phrases) => {
421
+ const text = document.body?.innerText ?? "";
422
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
423
+ return phrases.some(phrase => new RegExp(`\\b${escape(phrase)}\\b`, "i").test(text));
424
+ }, [...localeLabels.stopControl]), localGuardTimeout(timeoutMs, 2000), "Timed out while checking ChatGPT stop controls.").catch(() => false);
425
+ }
426
+ async function sleep(page, ms) {
427
+ if (typeof page.waitForTimeout === "function") {
428
+ await page.waitForTimeout(ms);
429
+ return;
430
+ }
431
+ await new Promise(resolve => setTimeout(resolve, ms));
432
+ }
@@ -1,13 +1,16 @@
1
1
  import { countPageMessages } from "../dom/messages.js";
2
2
  import { parseConversationId } from "../browser/page-state.js";
3
+ import { withTimeout } from "./timeouts.js";
3
4
  export async function contextFromPage(page, partial = {}) {
4
5
  if (page === undefined) {
5
6
  return { timestamp: new Date().toISOString(), ...partial };
6
7
  }
7
8
  const url = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => partial.url) : partial.url;
8
9
  const title = typeof page.title === "function" ? await page.title().catch(() => undefined) : partial.title;
9
- const turnCount = await countPageMessages(page).catch(() => partial.turnCount);
10
- const assistantTurnCount = await countPageMessages(page, "assistant").catch(() => partial.assistantTurnCount);
10
+ const [turnCount, assistantTurnCount] = await Promise.all([
11
+ withTimeout(countPageMessages(page), 1000, "Timed out while counting page messages.").catch(() => partial.turnCount),
12
+ withTimeout(countPageMessages(page, "assistant"), 1000, "Timed out while counting assistant messages.").catch(() => partial.assistantTurnCount)
13
+ ]);
11
14
  const conversationId = url !== undefined ? parseConversationId(url) : partial.conversationId;
12
15
  const context = {
13
16
  timestamp: new Date().toISOString(),
@@ -1,13 +1,21 @@
1
- import type { CommandResult, RuntimeEnv } from "../types.js";
2
- export type DoctorCheckName = "bridge" | "login" | "upload" | "download" | "clipboard" | "modes" | "tools" | "selectors";
1
+ import type { BootstrapArgs, CommandResult, RuntimeEnv } from "../types.js";
2
+ import type { RunReportOptions } from "./reports.js";
3
+ export type DoctorCheckName = "bridge" | "login" | "upload" | "download" | "clipboard" | "modes" | "tools" | "selectors" | "existing_tab" | "artifacts" | "file_preflight" | "localization" | "reports";
3
4
  export type CapabilityStatus = "ok" | "blocked" | "unsupported" | "unknown";
4
5
  export type CapabilityCheck = {
5
6
  status: CapabilityStatus;
6
7
  message: string;
7
8
  remediation?: string[];
9
+ code?: string;
10
+ blockerKind?: string;
11
+ nextCommand?: string;
12
+ details?: Record<string, unknown>;
8
13
  };
9
14
  export type DoctorArgs = {
10
15
  check?: DoctorCheckName[];
16
+ existingTab?: BootstrapArgs["existingTab"];
17
+ files?: string[];
18
+ report?: RunReportOptions;
11
19
  };
12
20
  export type DoctorReport = {
13
21
  ready: boolean;