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
@@ -1,44 +1,175 @@
1
1
  import { access, readFile, stat } from "node:fs/promises";
2
- import { basename, resolve } from "node:path";
3
2
  import { constants } from "node:fs";
3
+ import path from "node:path";
4
+ import { downloadLatestArtifact, locatorCountWithTimeout } from "./artifacts.js";
4
5
  import { waitForDownloadFromClick } from "../browser/downloads.js";
5
6
  import { resultError, resultOk } from "../errors.js";
6
7
  import { addFilesButton, cssSelectors, requiredLocator } from "../dom/selectors.js";
8
+ import { localeLabels } from "../dom/locale-labels.js";
9
+ import { basenameForHostPath, isHostAbsolutePath, resolveForHostPath } from "../platform/local-paths.js";
7
10
  import { contextFromPage } from "./context.js";
8
11
  import { bootstrap } from "./session.js";
12
+ import { localGuardTimeout } from "./timeouts.js";
9
13
  const CODEX_UPLOAD_PERMISSION_FIX = "Codex Settings > Computer Use > Chrome > Permissions > Uploads: set to Always allow, or add chatgpt.com to the allowed upload domains.";
10
14
  const CHROME_FILE_URL_PERMISSION_FIX = "Chrome chrome://extensions > Codex extension > Details: enable Allow access to file URLs.";
15
+ const DEFAULT_MAX_BYTES_PER_FILE = 512 * 1024 * 1024;
16
+ const DEFAULT_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
11
17
  export async function validateAttachPaths(paths) {
18
+ const result = await preflightFiles({}, { paths });
19
+ if (!result.ok || result.data === undefined) {
20
+ throw new Error(result.blocker?.message ?? result.error?.message ?? "File attachment preflight failed.");
21
+ }
22
+ return result.data.files.map(file => ({
23
+ path: file.path,
24
+ name: file.name,
25
+ bytes: file.bytes
26
+ }));
27
+ }
28
+ export async function preflightFiles(env, args) {
29
+ const maxBytesPerFile = args.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;
30
+ const maxTotalBytes = args.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
12
31
  const files = [];
13
- for (const path of paths) {
14
- if (!path.startsWith("/")) {
15
- throw new Error(`File attachment path must be absolute: ${path}`);
32
+ const warnings = [];
33
+ for (const [index, inputPath] of args.paths.entries()) {
34
+ const fieldPath = `paths[${index}]`;
35
+ if (!isHostAbsolutePath(inputPath)) {
36
+ return filePreflightBlocker({
37
+ env,
38
+ status: "blocked",
39
+ kind: "upload_failed",
40
+ code: "file_path_not_absolute",
41
+ fieldPath,
42
+ message: `File attachment path must be absolute for the backend host: ${inputPath}`
43
+ });
44
+ }
45
+ const absolute = resolveForHostPath(inputPath);
46
+ let fileStat;
47
+ try {
48
+ fileStat = await stat(absolute);
49
+ }
50
+ catch (error) {
51
+ if (isNodeError(error) && error.code === "ENOENT") {
52
+ return filePreflightBlocker({
53
+ env,
54
+ status: "not_found",
55
+ kind: "not_found",
56
+ code: "file_missing",
57
+ fieldPath,
58
+ message: `File attachment path does not exist: ${absolute}`
59
+ });
60
+ }
61
+ if (isNodeError(error) && (error.code === "EACCES" || error.code === "EPERM")) {
62
+ return filePreflightBlocker({
63
+ env,
64
+ status: "blocked",
65
+ kind: "permission",
66
+ code: "file_not_readable",
67
+ fieldPath,
68
+ message: `File attachment path is not readable: ${absolute}`
69
+ });
70
+ }
71
+ return resultError(error instanceof Error ? error : new Error(String(error)), filePreflightContext(env));
16
72
  }
17
- const absolute = resolve(path);
18
- await access(absolute, constants.R_OK);
19
- const fileStat = await stat(absolute);
20
73
  if (!fileStat.isFile()) {
21
- throw new Error(`Attachment path is not a file: ${absolute}`);
74
+ return filePreflightBlocker({
75
+ env,
76
+ status: "blocked",
77
+ kind: "upload_failed",
78
+ code: fileStat.isDirectory() ? "file_path_is_directory" : "file_path_not_file",
79
+ fieldPath,
80
+ message: `File attachment path is not a file: ${absolute}`
81
+ });
82
+ }
83
+ try {
84
+ await access(absolute, constants.R_OK);
85
+ }
86
+ catch (error) {
87
+ return filePreflightBlocker({
88
+ env,
89
+ status: "blocked",
90
+ kind: "permission",
91
+ code: "file_not_readable",
92
+ fieldPath,
93
+ message: `File attachment path is not readable: ${absolute}`
94
+ });
95
+ }
96
+ if (fileStat.size > maxBytesPerFile) {
97
+ return filePreflightBlocker({
98
+ env,
99
+ status: "blocked",
100
+ kind: "upload_failed",
101
+ code: "file_too_large",
102
+ fieldPath,
103
+ message: `File attachment exceeds the configured per-file preflight limit: ${absolute} (${fileStat.size}/${maxBytesPerFile} bytes)`
104
+ });
22
105
  }
23
- files.push({
24
- path: absolute,
25
- name: basename(absolute),
26
- bytes: fileStat.size
106
+ const metadata = fileMetadata(absolute, fileStat.size);
107
+ files.push(metadata);
108
+ }
109
+ const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
110
+ if (totalBytes > maxTotalBytes) {
111
+ return filePreflightBlocker({
112
+ env,
113
+ status: "blocked",
114
+ kind: "upload_failed",
115
+ code: "file_total_bytes_exceeded",
116
+ fieldPath: "paths",
117
+ message: `File attachments exceed the configured total preflight limit: ${totalBytes}/${maxTotalBytes} bytes`
27
118
  });
28
119
  }
29
- return files;
120
+ collectFilePreflightWarnings(files, warnings);
121
+ return resultOk({ files, totalBytes }, filePreflightContext(env), warnings);
30
122
  }
31
123
  export async function attachFiles(env, args) {
124
+ const preflight = await preflightFiles(env, { paths: args.paths });
125
+ if (!preflight.ok || preflight.data === undefined) {
126
+ return preflight;
127
+ }
32
128
  const boot = await ensurePage(env);
33
129
  if (!boot.ok) {
34
130
  return boot;
35
131
  }
36
132
  const page = env.page;
37
133
  try {
38
- const files = await validateAttachPaths(args.paths);
134
+ const files = preflight.data.files.map(file => ({
135
+ path: file.path,
136
+ name: file.name,
137
+ bytes: file.bytes
138
+ }));
39
139
  await uploadFiles(page, files, args.timeoutMs ?? 30000);
40
140
  await page.waitForTimeout?.(args.timeoutMs === undefined ? 1000 : Math.min(args.timeoutMs, 3000));
41
- return resultOk({ files }, await contextFromPage(page));
141
+ const readiness = await waitForAttachedFilesReady(page, files, args.timeoutMs ?? 30000);
142
+ if (!readiness.ready) {
143
+ const blocker = {
144
+ kind: "upload_failed",
145
+ code: "attachment_processing",
146
+ message: "ChatGPT still appears to be processing the attached file, so the prompt was not submitted.",
147
+ remediation: [
148
+ {
149
+ label: "Wait for upload",
150
+ instruction: "Wait until the visible attachment finishes uploading or processing, then retry the askWithFiles call.",
151
+ userActionRequired: false
152
+ },
153
+ {
154
+ label: "Retry smaller file",
155
+ instruction: "If processing never finishes, retry with a smaller file or a different supported file type.",
156
+ userActionRequired: true
157
+ }
158
+ ],
159
+ resumable: true
160
+ };
161
+ if (readiness.processingText !== undefined) {
162
+ blocker.visibleText = readiness.processingText;
163
+ }
164
+ return {
165
+ ok: false,
166
+ status: "blocked",
167
+ warnings: [],
168
+ blocker,
169
+ context: await contextFromPage(page)
170
+ };
171
+ }
172
+ return resultOk({ files }, await contextFromPage(page), preflight.warnings);
42
173
  }
43
174
  catch (error) {
44
175
  if (isUploadBridgeBlocker(error)) {
@@ -60,6 +191,185 @@ export async function attachFiles(env, args) {
60
191
  return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
61
192
  }
62
193
  }
194
+ function filePreflightBlocker(args) {
195
+ return {
196
+ ok: false,
197
+ status: args.status,
198
+ warnings: [],
199
+ blocker: {
200
+ kind: args.kind,
201
+ code: args.code,
202
+ fieldPath: args.fieldPath,
203
+ message: args.message,
204
+ resumable: true
205
+ },
206
+ context: filePreflightContext(args.env)
207
+ };
208
+ }
209
+ function filePreflightContext(env) {
210
+ return { timestamp: (env.now?.() ?? new Date()).toISOString() };
211
+ }
212
+ function fileMetadata(absolute, bytes) {
213
+ const extension = extensionForHostPath(absolute);
214
+ const { mimeType, category } = guessFileType(extension);
215
+ return {
216
+ path: absolute,
217
+ name: basenameForHostPath(absolute),
218
+ bytes,
219
+ extension,
220
+ mimeType,
221
+ category
222
+ };
223
+ }
224
+ function extensionForHostPath(value) {
225
+ return process.platform === "win32"
226
+ ? path.win32.extname(value).toLowerCase()
227
+ : path.posix.extname(value).toLowerCase();
228
+ }
229
+ function collectFilePreflightWarnings(files, warnings) {
230
+ const byPath = new Map();
231
+ const byName = new Map();
232
+ for (const file of files) {
233
+ if (file.bytes === 0) {
234
+ warnings.push(`Zero-byte file will be attached if ChatGPT accepts it: ${file.name}`);
235
+ }
236
+ const pathCount = (byPath.get(file.path) ?? 0) + 1;
237
+ byPath.set(file.path, pathCount);
238
+ if (pathCount === 2) {
239
+ warnings.push(`Duplicate resolved file path requested: ${file.path}`);
240
+ }
241
+ const normalizedName = file.name.toLocaleLowerCase();
242
+ const nameCount = (byName.get(normalizedName) ?? 0) + 1;
243
+ byName.set(normalizedName, nameCount);
244
+ if (nameCount === 2) {
245
+ warnings.push(`Duplicate file basename requested: ${file.name}`);
246
+ }
247
+ }
248
+ }
249
+ function guessFileType(extension) {
250
+ switch (extension) {
251
+ case ".txt":
252
+ return { mimeType: "text/plain", category: "text" };
253
+ case ".md":
254
+ case ".markdown":
255
+ return { mimeType: "text/markdown", category: "text" };
256
+ case ".csv":
257
+ return { mimeType: "text/csv", category: "spreadsheet" };
258
+ case ".tsv":
259
+ return { mimeType: "text/tab-separated-values", category: "spreadsheet" };
260
+ case ".json":
261
+ return { mimeType: "application/json", category: "data" };
262
+ case ".jsonl":
263
+ case ".ndjson":
264
+ return { mimeType: "application/x-ndjson", category: "data" };
265
+ case ".pdf":
266
+ return { mimeType: "application/pdf", category: "document" };
267
+ case ".doc":
268
+ return { mimeType: "application/msword", category: "document" };
269
+ case ".docx":
270
+ return { mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", category: "document" };
271
+ case ".xls":
272
+ return { mimeType: "application/vnd.ms-excel", category: "spreadsheet" };
273
+ case ".xlsx":
274
+ return { mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", category: "spreadsheet" };
275
+ case ".png":
276
+ return { mimeType: "image/png", category: "image" };
277
+ case ".jpg":
278
+ case ".jpeg":
279
+ return { mimeType: "image/jpeg", category: "image" };
280
+ case ".gif":
281
+ return { mimeType: "image/gif", category: "image" };
282
+ case ".webp":
283
+ return { mimeType: "image/webp", category: "image" };
284
+ case ".svg":
285
+ return { mimeType: "image/svg+xml", category: "image" };
286
+ case ".mp3":
287
+ return { mimeType: "audio/mpeg", category: "audio" };
288
+ case ".wav":
289
+ return { mimeType: "audio/wav", category: "audio" };
290
+ case ".mp4":
291
+ return { mimeType: "video/mp4", category: "video" };
292
+ case ".mov":
293
+ return { mimeType: "video/quicktime", category: "video" };
294
+ case ".zip":
295
+ return { mimeType: "application/zip", category: "archive" };
296
+ case ".gz":
297
+ return { mimeType: "application/gzip", category: "archive" };
298
+ default:
299
+ return { mimeType: guessMimeType(extension), category: "unknown" };
300
+ }
301
+ }
302
+ function isNodeError(error) {
303
+ return error instanceof Error && "code" in error;
304
+ }
305
+ async function waitForAttachedFilesReady(page, files, timeoutMs) {
306
+ const started = Date.now();
307
+ let lastProcessingText;
308
+ while (Date.now() - started < timeoutMs) {
309
+ const snapshot = await readAttachmentReadiness(page, files).catch(() => undefined);
310
+ if (snapshot === undefined) {
311
+ return { ready: true };
312
+ }
313
+ const allNamesVisible = snapshot.files.length > 0 && snapshot.files.every(file => file.visible);
314
+ if (!snapshot.processing && allNamesVisible) {
315
+ return { ready: true };
316
+ }
317
+ if (!snapshot.processing && Date.now() - started >= Math.min(timeoutMs, 1000)) {
318
+ return { ready: true };
319
+ }
320
+ if (snapshot.processingText !== undefined) {
321
+ lastProcessingText = snapshot.processingText;
322
+ }
323
+ await page.waitForTimeout?.(250);
324
+ }
325
+ const blocked = { ready: false };
326
+ if (lastProcessingText !== undefined) {
327
+ blocked.processingText = lastProcessingText;
328
+ }
329
+ return blocked;
330
+ }
331
+ async function readAttachmentReadiness(page, files) {
332
+ if (typeof page.evaluate !== "function") {
333
+ return undefined;
334
+ }
335
+ return page.evaluate((fileNames) => {
336
+ const visibleText = document.body?.innerText ?? "";
337
+ const normalize = (value) => value.toLocaleLowerCase();
338
+ const normalizedVisibleText = normalize(visibleText);
339
+ const files = fileNames.map(name => ({
340
+ name,
341
+ visible: normalizedVisibleText.includes(normalize(name))
342
+ }));
343
+ const attachmentSelectors = [
344
+ "[data-testid*='attachment' i]",
345
+ "[data-testid*='file' i]",
346
+ "[aria-label*='attachment' i]",
347
+ "[aria-label*='upload' i]",
348
+ "[aria-label*='file' i]",
349
+ "[class*='attachment' i]",
350
+ "[class*='upload' i]",
351
+ "[class*='file' i]",
352
+ "[role='progressbar']"
353
+ ].join(", ");
354
+ const attachmentText = Array.from(document.querySelectorAll(attachmentSelectors))
355
+ .map(element => [
356
+ element.textContent ?? "",
357
+ element.getAttribute("aria-label") ?? "",
358
+ element.getAttribute("title") ?? ""
359
+ ].join(" "))
360
+ .join(" ");
361
+ const relevantText = attachmentText.length > 0 ? attachmentText : visibleText;
362
+ const processingMatch = /\b(uploading|processing|attaching|preparing|reading|scanning|analyzing)\b/i.exec(relevantText);
363
+ const snapshot = {
364
+ files,
365
+ processing: processingMatch !== null
366
+ };
367
+ if (processingMatch !== null) {
368
+ snapshot.processingText = relevantText.slice(0, 500);
369
+ }
370
+ return snapshot;
371
+ }, files.map(file => file.name));
372
+ }
63
373
  async function uploadFiles(page, files, timeoutMs) {
64
374
  const paths = files.map(file => file.path);
65
375
  const errors = [];
@@ -101,7 +411,10 @@ async function uploadFiles(page, files, timeoutMs) {
101
411
  throw new Error(`No ChatGPT upload path completed.\n${errors.join("\n")}`);
102
412
  }
103
413
  async function clickChatGPTAddPhotosMenuItem(page, paths, timeoutMs) {
104
- const menuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: "Add photos & files" });
414
+ // The `#composer-plus-btn` id is the language-agnostic primary; the aria-label and the
415
+ // menu-item text are locale-sensitive (menu text sourced from the locale registry).
416
+ const addPhotosFilesText = localeLabels.addPhotosFilesMenuItem[0];
417
+ const menuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: addPhotosFilesText });
105
418
  if (await locatorCount(menuItem) !== 1) {
106
419
  const plusButton = requiredLocator(page, "#composer-plus-btn, button[aria-label='Add files and more']");
107
420
  if (await locatorCount(plusButton) !== 1) {
@@ -110,7 +423,7 @@ async function clickChatGPTAddPhotosMenuItem(page, paths, timeoutMs) {
110
423
  await plusButton.click?.({ timeoutMs: Math.min(timeoutMs, 10000) });
111
424
  await page.waitForTimeout?.(250);
112
425
  }
113
- const refreshedMenuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: "Add photos & files" });
426
+ const refreshedMenuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: addPhotosFilesText });
114
427
  await clickFileChooserLocator(page, refreshedMenuItem, paths, timeoutMs);
115
428
  }
116
429
  async function clickFileChooserTarget(page, selector, paths, timeoutMs, options = {}) {
@@ -188,8 +501,29 @@ export async function downloadLatestFile(env, args) {
188
501
  const page = env.page;
189
502
  try {
190
503
  const controls = requiredLocator(page, cssSelectors.downloadControls);
191
- const count = await controls.count?.();
504
+ let count;
505
+ try {
506
+ count = await locatorCountWithTimeout(controls, localGuardTimeout(args.timeoutMs, 5000), "download_control_timeout");
507
+ }
508
+ catch (error) {
509
+ return {
510
+ ok: false,
511
+ status: "unsupported",
512
+ warnings: [],
513
+ blocker: {
514
+ kind: "download_unavailable",
515
+ code: "download_control_timeout",
516
+ message: `No visible ChatGPT download control could be counted before the local guard timeout: ${error instanceof Error ? error.message : String(error)}`,
517
+ resumable: true
518
+ },
519
+ context: await contextFromPage(page)
520
+ };
521
+ }
192
522
  if (count === 0) {
523
+ const artifactDownload = await downloadLatestArtifact(env, args);
524
+ if (artifactDownload.ok) {
525
+ return artifactDownload;
526
+ }
193
527
  return {
194
528
  ok: false,
195
529
  status: "unsupported",
@@ -1,8 +1,9 @@
1
+ import { type AssistantGenerationState } from "../dom/generation-state.js";
1
2
  import type { AskArgs, AskReadData, CommandResult, ComposeArgs, ComposeData, ReadLatestArgs, ReadLatestData, RuntimeEnv, SubmitArgs, SubmitData, WaitAndReadArgs, WaitArgs, WaitData } from "../types.js";
2
3
  export type CompletionSnapshot = {
3
4
  textStableForMs: number;
4
5
  stableMs: number;
5
- hasStopButton: boolean;
6
+ generation: AssistantGenerationState;
6
7
  hasResponseActions: boolean;
7
8
  latestText: string;
8
9
  };