codex-chatgpt-control 0.1.0-alpha.1 → 0.3.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 (255) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +55 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +8 -0
  4. package/contracts/v1/fixtures/blocker-explanation-profiles.json +114 -0
  5. package/contracts/v1/fixtures/command-descriptors.json +224 -16
  6. package/contracts/v1/fixtures/doctor-bridge-upload.json +8 -0
  7. package/contracts/v1/fixtures/doctor-scenario-preflight.json +119 -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 +80 -7
  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 +7478 -1789
  30. package/dist/codex-chatgpt-control.bundle.mjs +8023 -2266
  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 +8 -0
  35. package/dist/src/backend/session.js +16 -0
  36. package/dist/src/browser/attach.js +128 -20
  37. package/dist/src/browser/clipboard.d.ts +11 -0
  38. package/dist/src/browser/clipboard.js +34 -7
  39. package/dist/src/browser/downloads.js +2 -1
  40. package/dist/src/browser/page-state.js +18 -5
  41. package/dist/src/client.d.ts +17 -1
  42. package/dist/src/client.js +89 -9
  43. package/dist/src/commands/artifacts.d.ts +5 -0
  44. package/dist/src/commands/artifacts.js +432 -0
  45. package/dist/src/commands/context.js +8 -3
  46. package/dist/src/commands/conversation.d.ts +15 -0
  47. package/dist/src/commands/conversation.js +44 -0
  48. package/dist/src/commands/deadline.d.ts +8 -0
  49. package/dist/src/commands/deadline.js +14 -0
  50. package/dist/src/commands/doctor.d.ts +10 -2
  51. package/dist/src/commands/doctor.js +249 -14
  52. package/dist/src/commands/files.d.ts +2 -1
  53. package/dist/src/commands/files.js +406 -18
  54. package/dist/src/commands/messages.d.ts +2 -1
  55. package/dist/src/commands/messages.js +430 -98
  56. package/dist/src/commands/modes.d.ts +4 -1
  57. package/dist/src/commands/modes.js +404 -37
  58. package/dist/src/commands/probes.d.ts +15 -0
  59. package/dist/src/commands/probes.js +64 -0
  60. package/dist/src/commands/project-sources.d.ts +10 -0
  61. package/dist/src/commands/project-sources.js +643 -0
  62. package/dist/src/commands/registry.js +108 -14
  63. package/dist/src/commands/reports.d.ts +9 -0
  64. package/dist/src/commands/reports.js +84 -7
  65. package/dist/src/commands/response-actions.js +33 -0
  66. package/dist/src/commands/sequence.js +14 -0
  67. package/dist/src/commands/threads.js +4 -25
  68. package/dist/src/commands/timeouts.d.ts +2 -0
  69. package/dist/src/commands/timeouts.js +18 -0
  70. package/dist/src/diagnostics/blockers.d.ts +54 -0
  71. package/dist/src/diagnostics/blockers.js +299 -0
  72. package/dist/src/dom/artifacts.d.ts +7 -0
  73. package/dist/src/dom/artifacts.js +198 -0
  74. package/dist/src/dom/generation-state.d.ts +15 -0
  75. package/dist/src/dom/generation-state.js +109 -0
  76. package/dist/src/dom/label-match.d.ts +3 -0
  77. package/dist/src/dom/label-match.js +25 -0
  78. package/dist/src/dom/locale/am.d.ts +34 -0
  79. package/dist/src/dom/locale/am.js +34 -0
  80. package/dist/src/dom/locale/ar.d.ts +35 -0
  81. package/dist/src/dom/locale/ar.js +35 -0
  82. package/dist/src/dom/locale/bg.d.ts +35 -0
  83. package/dist/src/dom/locale/bg.js +35 -0
  84. package/dist/src/dom/locale/bn.d.ts +35 -0
  85. package/dist/src/dom/locale/bn.js +35 -0
  86. package/dist/src/dom/locale/bs.d.ts +34 -0
  87. package/dist/src/dom/locale/bs.js +34 -0
  88. package/dist/src/dom/locale/ca.d.ts +34 -0
  89. package/dist/src/dom/locale/ca.js +34 -0
  90. package/dist/src/dom/locale/cs.d.ts +34 -0
  91. package/dist/src/dom/locale/cs.js +34 -0
  92. package/dist/src/dom/locale/da.d.ts +33 -0
  93. package/dist/src/dom/locale/da.js +33 -0
  94. package/dist/src/dom/locale/de.d.ts +36 -0
  95. package/dist/src/dom/locale/de.js +36 -0
  96. package/dist/src/dom/locale/el.d.ts +34 -0
  97. package/dist/src/dom/locale/el.js +34 -0
  98. package/dist/src/dom/locale/en.d.ts +64 -0
  99. package/dist/src/dom/locale/en.js +71 -0
  100. package/dist/src/dom/locale/es-419.d.ts +34 -0
  101. package/dist/src/dom/locale/es-419.js +34 -0
  102. package/dist/src/dom/locale/es-ES.d.ts +34 -0
  103. package/dist/src/dom/locale/es-ES.js +34 -0
  104. package/dist/src/dom/locale/et.d.ts +34 -0
  105. package/dist/src/dom/locale/et.js +34 -0
  106. package/dist/src/dom/locale/fa.d.ts +35 -0
  107. package/dist/src/dom/locale/fa.js +35 -0
  108. package/dist/src/dom/locale/fi.d.ts +34 -0
  109. package/dist/src/dom/locale/fi.js +34 -0
  110. package/dist/src/dom/locale/fr-CA.d.ts +35 -0
  111. package/dist/src/dom/locale/fr-CA.js +35 -0
  112. package/dist/src/dom/locale/fr-FR.d.ts +35 -0
  113. package/dist/src/dom/locale/fr-FR.js +35 -0
  114. package/dist/src/dom/locale/gu.d.ts +34 -0
  115. package/dist/src/dom/locale/gu.js +34 -0
  116. package/dist/src/dom/locale/hi.d.ts +34 -0
  117. package/dist/src/dom/locale/hi.js +34 -0
  118. package/dist/src/dom/locale/hr.d.ts +33 -0
  119. package/dist/src/dom/locale/hr.js +33 -0
  120. package/dist/src/dom/locale/hu.d.ts +34 -0
  121. package/dist/src/dom/locale/hu.js +34 -0
  122. package/dist/src/dom/locale/hy.d.ts +35 -0
  123. package/dist/src/dom/locale/hy.js +35 -0
  124. package/dist/src/dom/locale/id.d.ts +34 -0
  125. package/dist/src/dom/locale/id.js +34 -0
  126. package/dist/src/dom/locale/index.d.ts +83 -0
  127. package/dist/src/dom/locale/index.js +239 -0
  128. package/dist/src/dom/locale/is.d.ts +34 -0
  129. package/dist/src/dom/locale/is.js +34 -0
  130. package/dist/src/dom/locale/it.d.ts +33 -0
  131. package/dist/src/dom/locale/it.js +33 -0
  132. package/dist/src/dom/locale/ja.d.ts +33 -0
  133. package/dist/src/dom/locale/ja.js +33 -0
  134. package/dist/src/dom/locale/ka.d.ts +34 -0
  135. package/dist/src/dom/locale/ka.js +34 -0
  136. package/dist/src/dom/locale/kk.d.ts +34 -0
  137. package/dist/src/dom/locale/kk.js +34 -0
  138. package/dist/src/dom/locale/kn.d.ts +35 -0
  139. package/dist/src/dom/locale/kn.js +35 -0
  140. package/dist/src/dom/locale/ko.d.ts +34 -0
  141. package/dist/src/dom/locale/ko.js +34 -0
  142. package/dist/src/dom/locale/lt.d.ts +35 -0
  143. package/dist/src/dom/locale/lt.js +35 -0
  144. package/dist/src/dom/locale/lv.d.ts +34 -0
  145. package/dist/src/dom/locale/lv.js +34 -0
  146. package/dist/src/dom/locale/mk.d.ts +33 -0
  147. package/dist/src/dom/locale/mk.js +33 -0
  148. package/dist/src/dom/locale/ml.d.ts +35 -0
  149. package/dist/src/dom/locale/ml.js +35 -0
  150. package/dist/src/dom/locale/mn.d.ts +35 -0
  151. package/dist/src/dom/locale/mn.js +35 -0
  152. package/dist/src/dom/locale/mr.d.ts +35 -0
  153. package/dist/src/dom/locale/mr.js +35 -0
  154. package/dist/src/dom/locale/ms.d.ts +34 -0
  155. package/dist/src/dom/locale/ms.js +34 -0
  156. package/dist/src/dom/locale/my.d.ts +34 -0
  157. package/dist/src/dom/locale/my.js +34 -0
  158. package/dist/src/dom/locale/nb.d.ts +34 -0
  159. package/dist/src/dom/locale/nb.js +34 -0
  160. package/dist/src/dom/locale/nl.d.ts +34 -0
  161. package/dist/src/dom/locale/nl.js +34 -0
  162. package/dist/src/dom/locale/pa.d.ts +35 -0
  163. package/dist/src/dom/locale/pa.js +35 -0
  164. package/dist/src/dom/locale/pl.d.ts +34 -0
  165. package/dist/src/dom/locale/pl.js +34 -0
  166. package/dist/src/dom/locale/pt-BR.d.ts +34 -0
  167. package/dist/src/dom/locale/pt-BR.js +34 -0
  168. package/dist/src/dom/locale/pt-PT.d.ts +34 -0
  169. package/dist/src/dom/locale/pt-PT.js +34 -0
  170. package/dist/src/dom/locale/ro.d.ts +33 -0
  171. package/dist/src/dom/locale/ro.js +33 -0
  172. package/dist/src/dom/locale/ru.d.ts +31 -0
  173. package/dist/src/dom/locale/ru.js +31 -0
  174. package/dist/src/dom/locale/sk.d.ts +34 -0
  175. package/dist/src/dom/locale/sk.js +34 -0
  176. package/dist/src/dom/locale/sl.d.ts +34 -0
  177. package/dist/src/dom/locale/sl.js +34 -0
  178. package/dist/src/dom/locale/so.d.ts +34 -0
  179. package/dist/src/dom/locale/so.js +34 -0
  180. package/dist/src/dom/locale/sq.d.ts +34 -0
  181. package/dist/src/dom/locale/sq.js +34 -0
  182. package/dist/src/dom/locale/sr.d.ts +31 -0
  183. package/dist/src/dom/locale/sr.js +31 -0
  184. package/dist/src/dom/locale/sv.d.ts +34 -0
  185. package/dist/src/dom/locale/sv.js +34 -0
  186. package/dist/src/dom/locale/sw.d.ts +34 -0
  187. package/dist/src/dom/locale/sw.js +34 -0
  188. package/dist/src/dom/locale/ta.d.ts +35 -0
  189. package/dist/src/dom/locale/ta.js +35 -0
  190. package/dist/src/dom/locale/te.d.ts +35 -0
  191. package/dist/src/dom/locale/te.js +35 -0
  192. package/dist/src/dom/locale/th.d.ts +34 -0
  193. package/dist/src/dom/locale/th.js +34 -0
  194. package/dist/src/dom/locale/tl.d.ts +28 -0
  195. package/dist/src/dom/locale/tl.js +28 -0
  196. package/dist/src/dom/locale/tr.d.ts +34 -0
  197. package/dist/src/dom/locale/tr.js +34 -0
  198. package/dist/src/dom/locale/types.d.ts +52 -0
  199. package/dist/src/dom/locale/types.js +1 -0
  200. package/dist/src/dom/locale/uk.d.ts +34 -0
  201. package/dist/src/dom/locale/uk.js +34 -0
  202. package/dist/src/dom/locale/ur.d.ts +34 -0
  203. package/dist/src/dom/locale/ur.js +34 -0
  204. package/dist/src/dom/locale/vi.d.ts +34 -0
  205. package/dist/src/dom/locale/vi.js +34 -0
  206. package/dist/src/dom/locale/zh-HK.d.ts +35 -0
  207. package/dist/src/dom/locale/zh-HK.js +35 -0
  208. package/dist/src/dom/locale/zh-Hans.d.ts +35 -0
  209. package/dist/src/dom/locale/zh-Hans.js +35 -0
  210. package/dist/src/dom/locale/zh-TW.d.ts +36 -0
  211. package/dist/src/dom/locale/zh-TW.js +36 -0
  212. package/dist/src/dom/locale-labels.d.ts +26 -0
  213. package/dist/src/dom/locale-labels.js +26 -0
  214. package/dist/src/dom/menus.d.ts +6 -0
  215. package/dist/src/dom/menus.js +47 -10
  216. package/dist/src/dom/message-format.d.ts +2 -1
  217. package/dist/src/dom/message-format.js +67 -10
  218. package/dist/src/dom/messages.d.ts +2 -1
  219. package/dist/src/dom/messages.js +2 -4
  220. package/dist/src/dom/selectors.d.ts +1 -0
  221. package/dist/src/dom/selectors.js +42 -15
  222. package/dist/src/dom/wait-snapshot.d.ts +43 -0
  223. package/dist/src/dom/wait-snapshot.js +127 -0
  224. package/dist/src/index.d.ts +5 -0
  225. package/dist/src/index.js +5 -0
  226. package/dist/src/platform/local-paths.d.ts +4 -0
  227. package/dist/src/platform/local-paths.js +20 -0
  228. package/dist/src/runner/interruptions.js +15 -4
  229. package/dist/src/runner/responses.js +18 -1
  230. package/dist/src/runner/result.js +15 -0
  231. package/dist/src/runner/resume.js +1 -0
  232. package/dist/src/runner/types.d.ts +3 -0
  233. package/dist/src/safety/risk.d.ts +8 -0
  234. package/dist/src/safety/risk.js +8 -0
  235. package/dist/src/safety/untrusted-output.d.ts +73 -0
  236. package/dist/src/safety/untrusted-output.js +199 -0
  237. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +1 -0
  238. package/dist/src/scripts/apply-intelligence-locale-captures.js +270 -0
  239. package/dist/src/scripts/capture-intelligence-locales.d.ts +25 -0
  240. package/dist/src/scripts/capture-intelligence-locales.js +669 -0
  241. package/dist/src/scripts/check-surface-drift.d.ts +1 -0
  242. package/dist/src/scripts/check-surface-drift.js +11 -0
  243. package/dist/src/scripts/live-smoke/scenarios.js +84 -0
  244. package/dist/src/scripts/locale-capture/language-coverage.d.ts +10 -0
  245. package/dist/src/scripts/locale-capture/language-coverage.js +39 -0
  246. package/dist/src/testing/surface-drift.d.ts +66 -0
  247. package/dist/src/testing/surface-drift.js +363 -0
  248. package/dist/src/types.d.ts +229 -2
  249. package/package.json +3 -2
  250. package/references/backend-protocol.md +96 -2
  251. package/references/language-coverage.md +142 -0
  252. package/references/localization.md +257 -0
  253. package/references/python-parity.md +67 -3
  254. package/references/safety.md +2 -0
  255. package/references/troubleshooting.md +83 -2
@@ -1,44 +1,200 @@
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 { createHash } from "node:crypto";
4
+ import path from "node:path";
5
+ import { downloadLatestArtifact, locatorCountWithTimeout } from "./artifacts.js";
4
6
  import { waitForDownloadFromClick } from "../browser/downloads.js";
5
7
  import { resultError, resultOk } from "../errors.js";
6
8
  import { addFilesButton, cssSelectors, requiredLocator } from "../dom/selectors.js";
9
+ import { localeLabels } from "../dom/locale-labels.js";
10
+ import { basenameForHostPath, isHostAbsolutePath, resolveForHostPath } from "../platform/local-paths.js";
7
11
  import { contextFromPage } from "./context.js";
8
12
  import { bootstrap } from "./session.js";
13
+ import { localGuardTimeout } from "./timeouts.js";
9
14
  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
15
  const CHROME_FILE_URL_PERMISSION_FIX = "Chrome chrome://extensions > Codex extension > Details: enable Allow access to file URLs.";
16
+ const DEFAULT_MAX_BYTES_PER_FILE = 512 * 1024 * 1024;
17
+ const DEFAULT_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
11
18
  export async function validateAttachPaths(paths) {
19
+ const result = await preflightFiles({}, { paths });
20
+ if (!result.ok || result.data === undefined) {
21
+ throw new Error(result.blocker?.message ?? result.error?.message ?? "File attachment preflight failed.");
22
+ }
23
+ return result.data.files.map(file => ({
24
+ path: file.path,
25
+ name: file.name,
26
+ bytes: file.bytes
27
+ }));
28
+ }
29
+ export async function preflightFiles(env, args) {
30
+ const maxBytesPerFile = args.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;
31
+ const maxTotalBytes = args.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
12
32
  const files = [];
13
- for (const path of paths) {
14
- if (!path.startsWith("/")) {
15
- throw new Error(`File attachment path must be absolute: ${path}`);
33
+ const warnings = [];
34
+ for (const [index, inputPath] of args.paths.entries()) {
35
+ const fieldPath = `paths[${index}]`;
36
+ if (!isHostAbsolutePath(inputPath)) {
37
+ return filePreflightBlocker({
38
+ env,
39
+ status: "blocked",
40
+ kind: "upload_failed",
41
+ code: "file_path_not_absolute",
42
+ fieldPath,
43
+ message: `File attachment path must be absolute for the backend host: ${inputPath}`
44
+ });
45
+ }
46
+ const absolute = resolveForHostPath(inputPath);
47
+ let fileStat;
48
+ try {
49
+ fileStat = await stat(absolute);
50
+ }
51
+ catch (error) {
52
+ if (isNodeError(error) && error.code === "ENOENT") {
53
+ return filePreflightBlocker({
54
+ env,
55
+ status: "not_found",
56
+ kind: "not_found",
57
+ code: "file_missing",
58
+ fieldPath,
59
+ message: `File attachment path does not exist: ${absolute}`
60
+ });
61
+ }
62
+ if (isNodeError(error) && (error.code === "EACCES" || error.code === "EPERM")) {
63
+ return filePreflightBlocker({
64
+ env,
65
+ status: "blocked",
66
+ kind: "permission",
67
+ code: "file_not_readable",
68
+ fieldPath,
69
+ message: `File attachment path is not readable: ${absolute}`
70
+ });
71
+ }
72
+ return resultError(error instanceof Error ? error : new Error(String(error)), filePreflightContext(env));
16
73
  }
17
- const absolute = resolve(path);
18
- await access(absolute, constants.R_OK);
19
- const fileStat = await stat(absolute);
20
74
  if (!fileStat.isFile()) {
21
- throw new Error(`Attachment path is not a file: ${absolute}`);
75
+ return filePreflightBlocker({
76
+ env,
77
+ status: "blocked",
78
+ kind: "upload_failed",
79
+ code: fileStat.isDirectory() ? "file_path_is_directory" : "file_path_not_file",
80
+ fieldPath,
81
+ message: `File attachment path is not a file: ${absolute}`
82
+ });
83
+ }
84
+ try {
85
+ await access(absolute, constants.R_OK);
86
+ }
87
+ catch (error) {
88
+ return filePreflightBlocker({
89
+ env,
90
+ status: "blocked",
91
+ kind: "permission",
92
+ code: "file_not_readable",
93
+ fieldPath,
94
+ message: `File attachment path is not readable: ${absolute}`
95
+ });
96
+ }
97
+ if (fileStat.size > maxBytesPerFile) {
98
+ return filePreflightBlocker({
99
+ env,
100
+ status: "blocked",
101
+ kind: "upload_failed",
102
+ code: "file_too_large",
103
+ fieldPath,
104
+ message: `File attachment exceeds the configured per-file preflight limit: ${absolute} (${fileStat.size}/${maxBytesPerFile} bytes)`
105
+ });
106
+ }
107
+ if (fileStat.size === 0) {
108
+ return filePreflightBlocker({
109
+ env,
110
+ status: "blocked",
111
+ kind: "upload_failed",
112
+ code: "file_empty",
113
+ fieldPath,
114
+ message: `File attachment path is zero bytes and ChatGPT rejects empty attachments: ${absolute}`
115
+ });
22
116
  }
23
- files.push({
24
- path: absolute,
25
- name: basename(absolute),
26
- bytes: fileStat.size
117
+ const metadata = await fileMetadata(absolute, fileStat.size, args.includeHashes === true);
118
+ files.push(metadata);
119
+ }
120
+ const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
121
+ if (totalBytes > maxTotalBytes) {
122
+ return filePreflightBlocker({
123
+ env,
124
+ status: "blocked",
125
+ kind: "upload_failed",
126
+ code: "file_total_bytes_exceeded",
127
+ fieldPath: "paths",
128
+ message: `File attachments exceed the configured total preflight limit: ${totalBytes}/${maxTotalBytes} bytes`
27
129
  });
28
130
  }
29
- return files;
131
+ collectFilePreflightWarnings(files, warnings);
132
+ return resultOk({ files, totalBytes }, filePreflightContext(env), warnings);
30
133
  }
31
134
  export async function attachFiles(env, args) {
135
+ const preflightArgs = { paths: args.paths };
136
+ if (args.includeDiagnostics === true && args.includeHashes !== undefined) {
137
+ preflightArgs.includeHashes = args.includeHashes;
138
+ }
139
+ const preflight = await preflightFiles(env, preflightArgs);
140
+ if (!preflight.ok || preflight.data === undefined) {
141
+ return preflight;
142
+ }
32
143
  const boot = await ensurePage(env);
33
144
  if (!boot.ok) {
34
145
  return boot;
35
146
  }
36
147
  const page = env.page;
37
148
  try {
38
- const files = await validateAttachPaths(args.paths);
149
+ const files = preflight.data.files.map(file => ({
150
+ path: file.path,
151
+ name: file.name,
152
+ bytes: file.bytes
153
+ }));
39
154
  await uploadFiles(page, files, args.timeoutMs ?? 30000);
155
+ const browserInput = args.includeDiagnostics === true
156
+ ? await readBrowserInputDiagnostic(page).catch(() => undefined)
157
+ : undefined;
40
158
  await page.waitForTimeout?.(args.timeoutMs === undefined ? 1000 : Math.min(args.timeoutMs, 3000));
41
- return resultOk({ files }, await contextFromPage(page));
159
+ const readiness = await waitForAttachedFilesReady(page, files, args.timeoutMs ?? 30000);
160
+ if (!readiness.ready) {
161
+ const blocker = {
162
+ kind: "upload_failed",
163
+ code: "attachment_processing",
164
+ message: "ChatGPT still appears to be processing the attached file, so the prompt was not submitted.",
165
+ remediation: [
166
+ {
167
+ label: "Wait for upload",
168
+ instruction: "Wait until the visible attachment finishes uploading or processing, then retry the askWithFiles call.",
169
+ userActionRequired: false
170
+ },
171
+ {
172
+ label: "Retry smaller file",
173
+ instruction: "If processing never finishes, retry with a smaller file or a different supported file type.",
174
+ userActionRequired: true
175
+ }
176
+ ],
177
+ resumable: true
178
+ };
179
+ if (readiness.processingText !== undefined) {
180
+ blocker.visibleText = readiness.processingText;
181
+ }
182
+ return {
183
+ ok: false,
184
+ status: "blocked",
185
+ warnings: [],
186
+ blocker,
187
+ context: await contextFromPage(page)
188
+ };
189
+ }
190
+ const data = { files };
191
+ if (args.includeDiagnostics === true) {
192
+ data.diagnostics = { preflight: preflight.data };
193
+ if (browserInput !== undefined) {
194
+ data.diagnostics.browserInput = browserInput;
195
+ }
196
+ }
197
+ return resultOk(data, await contextFromPage(page), preflight.warnings);
42
198
  }
43
199
  catch (error) {
44
200
  if (isUploadBridgeBlocker(error)) {
@@ -60,6 +216,186 @@ export async function attachFiles(env, args) {
60
216
  return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
61
217
  }
62
218
  }
219
+ function filePreflightBlocker(args) {
220
+ return {
221
+ ok: false,
222
+ status: args.status,
223
+ warnings: [],
224
+ blocker: {
225
+ kind: args.kind,
226
+ code: args.code,
227
+ fieldPath: args.fieldPath,
228
+ message: args.message,
229
+ resumable: true
230
+ },
231
+ context: filePreflightContext(args.env)
232
+ };
233
+ }
234
+ function filePreflightContext(env) {
235
+ return { timestamp: (env.now?.() ?? new Date()).toISOString() };
236
+ }
237
+ async function fileMetadata(absolute, bytes, includeHash = false) {
238
+ const extension = extensionForHostPath(absolute);
239
+ const { mimeType, category } = guessFileType(extension);
240
+ const metadata = {
241
+ path: absolute,
242
+ name: basenameForHostPath(absolute),
243
+ bytes,
244
+ extension,
245
+ mimeType,
246
+ category
247
+ };
248
+ if (includeHash) {
249
+ metadata.sha256 = createHash("sha256").update(await readFile(absolute)).digest("hex");
250
+ }
251
+ return metadata;
252
+ }
253
+ function extensionForHostPath(value) {
254
+ return process.platform === "win32"
255
+ ? path.win32.extname(value).toLowerCase()
256
+ : path.posix.extname(value).toLowerCase();
257
+ }
258
+ function collectFilePreflightWarnings(files, warnings) {
259
+ const byPath = new Map();
260
+ const byName = new Map();
261
+ for (const file of files) {
262
+ const pathCount = (byPath.get(file.path) ?? 0) + 1;
263
+ byPath.set(file.path, pathCount);
264
+ if (pathCount === 2) {
265
+ warnings.push(`Duplicate resolved file path requested: ${file.path}`);
266
+ }
267
+ const normalizedName = file.name.toLocaleLowerCase();
268
+ const nameCount = (byName.get(normalizedName) ?? 0) + 1;
269
+ byName.set(normalizedName, nameCount);
270
+ if (nameCount === 2) {
271
+ warnings.push(`Duplicate file basename requested: ${file.name}`);
272
+ }
273
+ }
274
+ }
275
+ function guessFileType(extension) {
276
+ switch (extension) {
277
+ case ".txt":
278
+ return { mimeType: "text/plain", category: "text" };
279
+ case ".md":
280
+ case ".markdown":
281
+ return { mimeType: "text/markdown", category: "text" };
282
+ case ".csv":
283
+ return { mimeType: "text/csv", category: "spreadsheet" };
284
+ case ".tsv":
285
+ return { mimeType: "text/tab-separated-values", category: "spreadsheet" };
286
+ case ".json":
287
+ return { mimeType: "application/json", category: "data" };
288
+ case ".jsonl":
289
+ case ".ndjson":
290
+ return { mimeType: "application/x-ndjson", category: "data" };
291
+ case ".pdf":
292
+ return { mimeType: "application/pdf", category: "document" };
293
+ case ".doc":
294
+ return { mimeType: "application/msword", category: "document" };
295
+ case ".docx":
296
+ return { mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", category: "document" };
297
+ case ".xls":
298
+ return { mimeType: "application/vnd.ms-excel", category: "spreadsheet" };
299
+ case ".xlsx":
300
+ return { mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", category: "spreadsheet" };
301
+ case ".png":
302
+ return { mimeType: "image/png", category: "image" };
303
+ case ".jpg":
304
+ case ".jpeg":
305
+ return { mimeType: "image/jpeg", category: "image" };
306
+ case ".gif":
307
+ return { mimeType: "image/gif", category: "image" };
308
+ case ".webp":
309
+ return { mimeType: "image/webp", category: "image" };
310
+ case ".svg":
311
+ return { mimeType: "image/svg+xml", category: "image" };
312
+ case ".mp3":
313
+ return { mimeType: "audio/mpeg", category: "audio" };
314
+ case ".wav":
315
+ return { mimeType: "audio/wav", category: "audio" };
316
+ case ".mp4":
317
+ return { mimeType: "video/mp4", category: "video" };
318
+ case ".mov":
319
+ return { mimeType: "video/quicktime", category: "video" };
320
+ case ".zip":
321
+ return { mimeType: "application/zip", category: "archive" };
322
+ case ".gz":
323
+ return { mimeType: "application/gzip", category: "archive" };
324
+ default:
325
+ return { mimeType: guessMimeType(extension), category: "unknown" };
326
+ }
327
+ }
328
+ function isNodeError(error) {
329
+ return error instanceof Error && "code" in error;
330
+ }
331
+ async function waitForAttachedFilesReady(page, files, timeoutMs) {
332
+ const started = Date.now();
333
+ let lastProcessingText;
334
+ while (Date.now() - started < timeoutMs) {
335
+ const snapshot = await readAttachmentReadiness(page, files).catch(() => undefined);
336
+ if (snapshot === undefined) {
337
+ return { ready: true };
338
+ }
339
+ const allNamesVisible = snapshot.files.length > 0 && snapshot.files.every(file => file.visible);
340
+ if (!snapshot.processing && allNamesVisible) {
341
+ return { ready: true };
342
+ }
343
+ if (!snapshot.processing && Date.now() - started >= Math.min(timeoutMs, 1000)) {
344
+ return { ready: true };
345
+ }
346
+ if (snapshot.processingText !== undefined) {
347
+ lastProcessingText = snapshot.processingText;
348
+ }
349
+ await page.waitForTimeout?.(250);
350
+ }
351
+ const blocked = { ready: false };
352
+ if (lastProcessingText !== undefined) {
353
+ blocked.processingText = lastProcessingText;
354
+ }
355
+ return blocked;
356
+ }
357
+ async function readAttachmentReadiness(page, files) {
358
+ if (typeof page.evaluate !== "function") {
359
+ return undefined;
360
+ }
361
+ return page.evaluate((fileNames) => {
362
+ const visibleText = document.body?.innerText ?? "";
363
+ const normalize = (value) => value.toLocaleLowerCase();
364
+ const normalizedVisibleText = normalize(visibleText);
365
+ const files = fileNames.map(name => ({
366
+ name,
367
+ visible: normalizedVisibleText.includes(normalize(name))
368
+ }));
369
+ const attachmentSelectors = [
370
+ "[data-testid*='attachment' i]",
371
+ "[data-testid*='file' i]",
372
+ "[aria-label*='attachment' i]",
373
+ "[aria-label*='upload' i]",
374
+ "[aria-label*='file' i]",
375
+ "[class*='attachment' i]",
376
+ "[class*='upload' i]",
377
+ "[class*='file' i]",
378
+ "[role='progressbar']"
379
+ ].join(", ");
380
+ const attachmentText = Array.from(document.querySelectorAll(attachmentSelectors))
381
+ .map(element => [
382
+ element.textContent ?? "",
383
+ element.getAttribute("aria-label") ?? "",
384
+ element.getAttribute("title") ?? ""
385
+ ].join(" "))
386
+ .join(" ");
387
+ const relevantText = attachmentText.length > 0 ? attachmentText : visibleText;
388
+ const processingMatch = /\b(uploading|processing|attaching|preparing|reading|scanning|analyzing)\b/i.exec(relevantText);
389
+ const snapshot = {
390
+ files,
391
+ processing: processingMatch !== null
392
+ };
393
+ if (processingMatch !== null) {
394
+ snapshot.processingText = relevantText.slice(0, 500);
395
+ }
396
+ return snapshot;
397
+ }, files.map(file => file.name));
398
+ }
63
399
  async function uploadFiles(page, files, timeoutMs) {
64
400
  const paths = files.map(file => file.path);
65
401
  const errors = [];
@@ -101,7 +437,10 @@ async function uploadFiles(page, files, timeoutMs) {
101
437
  throw new Error(`No ChatGPT upload path completed.\n${errors.join("\n")}`);
102
438
  }
103
439
  async function clickChatGPTAddPhotosMenuItem(page, paths, timeoutMs) {
104
- const menuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: "Add photos & files" });
440
+ // The `#composer-plus-btn` id is the language-agnostic primary; the aria-label and the
441
+ // menu-item text are locale-sensitive (menu text sourced from the locale registry).
442
+ const addPhotosFilesText = localeLabels.addPhotosFilesMenuItem[0];
443
+ const menuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: addPhotosFilesText });
105
444
  if (await locatorCount(menuItem) !== 1) {
106
445
  const plusButton = requiredLocator(page, "#composer-plus-btn, button[aria-label='Add files and more']");
107
446
  if (await locatorCount(plusButton) !== 1) {
@@ -110,7 +449,7 @@ async function clickChatGPTAddPhotosMenuItem(page, paths, timeoutMs) {
110
449
  await plusButton.click?.({ timeoutMs: Math.min(timeoutMs, 10000) });
111
450
  await page.waitForTimeout?.(250);
112
451
  }
113
- const refreshedMenuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: "Add photos & files" });
452
+ const refreshedMenuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: addPhotosFilesText });
114
453
  await clickFileChooserLocator(page, refreshedMenuItem, paths, timeoutMs);
115
454
  }
116
455
  async function clickFileChooserTarget(page, selector, paths, timeoutMs, options = {}) {
@@ -188,8 +527,29 @@ export async function downloadLatestFile(env, args) {
188
527
  const page = env.page;
189
528
  try {
190
529
  const controls = requiredLocator(page, cssSelectors.downloadControls);
191
- const count = await controls.count?.();
530
+ let count;
531
+ try {
532
+ count = await locatorCountWithTimeout(controls, localGuardTimeout(args.timeoutMs, 5000), "download_control_timeout");
533
+ }
534
+ catch (error) {
535
+ return {
536
+ ok: false,
537
+ status: "unsupported",
538
+ warnings: [],
539
+ blocker: {
540
+ kind: "download_unavailable",
541
+ code: "download_control_timeout",
542
+ message: `No visible ChatGPT download control could be counted before the local guard timeout: ${error instanceof Error ? error.message : String(error)}`,
543
+ resumable: true
544
+ },
545
+ context: await contextFromPage(page)
546
+ };
547
+ }
192
548
  if (count === 0) {
549
+ const artifactDownload = await downloadLatestArtifact(env, args);
550
+ if (artifactDownload.ok) {
551
+ return artifactDownload;
552
+ }
193
553
  return {
194
554
  ok: false,
195
555
  status: "unsupported",
@@ -222,6 +582,34 @@ async function setHiddenFileInput(page, files) {
222
582
  }
223
583
  await input.setInputFiles(files.map(file => file.path));
224
584
  }
585
+ async function readBrowserInputDiagnostic(page) {
586
+ if (typeof page.evaluate !== "function") {
587
+ return undefined;
588
+ }
589
+ return page.evaluate(() => {
590
+ const input = (document.querySelector("#upload-files")
591
+ || document.querySelector("input[type='file']:not([accept='image/*'])")
592
+ || document.querySelector("input[type='file']"));
593
+ if (!input) {
594
+ return { files: [] };
595
+ }
596
+ return {
597
+ files: Array.from(input.files ?? []).map(file => {
598
+ const diagnostic = {
599
+ name: file.name,
600
+ size: file.size
601
+ };
602
+ if (file.type.length > 0) {
603
+ diagnostic.type = file.type;
604
+ }
605
+ if (file.lastModified !== 0) {
606
+ diagnostic.lastModified = file.lastModified;
607
+ }
608
+ return diagnostic;
609
+ })
610
+ };
611
+ });
612
+ }
225
613
  async function ensurePage(env) {
226
614
  if (env.page !== undefined) {
227
615
  return resultOk({}, await contextFromPage(env.page));
@@ -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
  };