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,299 @@
1
+ import { augmentCommandBlocker, resumeDecisionForBlocker } from "../runner/resume.js";
2
+ const PROFILES = {
3
+ browser_bridge_unavailable: {
4
+ title: "Browser bridge unavailable",
5
+ category: "environment",
6
+ severity: "blocked",
7
+ userActionRequired: false,
8
+ defaultRetryReason: "Retry only after changing the execution environment or bootstrapping the Codex Chrome bridge."
9
+ },
10
+ login_required: {
11
+ title: "Login required",
12
+ category: "auth",
13
+ severity: "action_required",
14
+ userActionRequired: true,
15
+ defaultRetryReason: "Retry after the user signs in to ChatGPT; do not auto-submit a prompt."
16
+ },
17
+ captcha: {
18
+ title: "Captcha or human verification required",
19
+ category: "auth",
20
+ severity: "action_required",
21
+ userActionRequired: true,
22
+ defaultRetryReason: "Retry after the user completes the visible verification; do not auto-submit a prompt."
23
+ },
24
+ rate_limit: {
25
+ title: "Rate limited",
26
+ category: "auth",
27
+ severity: "action_required",
28
+ userActionRequired: true,
29
+ defaultRetryReason: "Retry only after the usage window resets or the user selects a different safe path."
30
+ },
31
+ modal: {
32
+ title: "Modal is blocking the page",
33
+ category: "runtime",
34
+ severity: "action_required",
35
+ userActionRequired: true,
36
+ defaultRetryReason: "Retry after the blocking modal is dismissed or handled."
37
+ },
38
+ permission: {
39
+ title: "Permission required",
40
+ category: "permission",
41
+ severity: "action_required",
42
+ userActionRequired: true,
43
+ defaultRetryReason: "Retry after the reported permission setting changes and only if the command is safe to resume."
44
+ },
45
+ confirmation: {
46
+ title: "Confirmation required",
47
+ category: "user_confirmation",
48
+ severity: "action_required",
49
+ userActionRequired: true,
50
+ defaultRetryReason: "Retry only after the user approves the exact bounded action."
51
+ },
52
+ selector_drift: {
53
+ title: "Selector drift",
54
+ category: "ui_drift",
55
+ severity: "blocked",
56
+ userActionRequired: false,
57
+ defaultRetryReason: "Do not retry blindly; update selectors/localization or move the visible UI to a supported state."
58
+ },
59
+ artifact_unavailable: {
60
+ title: "Artifact unavailable",
61
+ category: "artifact",
62
+ severity: "warning",
63
+ userActionRequired: false,
64
+ defaultRetryReason: "Retry only after the artifact appears or the command can safely re-check without resubmitting a prompt."
65
+ },
66
+ artifact_selector_drift: {
67
+ title: "Artifact selector drift",
68
+ category: "ui_drift",
69
+ severity: "blocked",
70
+ userActionRequired: false,
71
+ defaultRetryReason: "Do not retry blindly; update artifact selectors before resuming."
72
+ },
73
+ artifact_download_unavailable: {
74
+ title: "Artifact download unavailable",
75
+ category: "download",
76
+ severity: "warning",
77
+ userActionRequired: false,
78
+ defaultRetryReason: "Retry only after a download control appears, or use a safe visible asset fallback if the caller requested it."
79
+ },
80
+ download_unavailable: {
81
+ title: "Download unavailable",
82
+ category: "download",
83
+ severity: "warning",
84
+ userActionRequired: false,
85
+ defaultRetryReason: "Retry only after a downloadable affordance appears; do not resubmit the prompt just to create one."
86
+ },
87
+ upload_failed: {
88
+ title: "Upload failed",
89
+ category: "upload",
90
+ severity: "action_required",
91
+ userActionRequired: true,
92
+ defaultRetryReason: "Retry after the upload blocker is fixed and only if the prompt has not already been submitted."
93
+ },
94
+ not_found: {
95
+ title: "Target not found",
96
+ category: "not_found",
97
+ severity: "warning",
98
+ userActionRequired: false,
99
+ defaultRetryReason: "Retry only after the target changes or the caller relaxes the targeting policy."
100
+ },
101
+ unknown: {
102
+ title: "Unknown blocker",
103
+ category: "unknown",
104
+ severity: "blocked",
105
+ userActionRequired: false,
106
+ defaultRetryReason: "Inspect the structured blocker and retry only after the cause is understood."
107
+ }
108
+ };
109
+ export function explainCommandBlocker(resultOrBlocker, options = {}) {
110
+ const result = isCommandResult(resultOrBlocker) ? resultOrBlocker : undefined;
111
+ const rawBlocker = result?.blocker ?? (isBlocker(resultOrBlocker) ? resultOrBlocker : undefined);
112
+ const blocker = rawBlocker === undefined ? undefined : augmentCommandBlocker(rawBlocker);
113
+ const context = explanationContext(result?.context ?? options.context, options.command);
114
+ if (blocker === undefined) {
115
+ const explanation = {
116
+ kind: "none",
117
+ title: "No blocker",
118
+ summary: "The command result does not include a browser-control blocker.",
119
+ severity: "info",
120
+ category: "unknown",
121
+ userActionRequired: false,
122
+ retry: { safe: false, reason: "There is no blocker-specific retry guidance." },
123
+ resume: { supported: false, reason: "This result has no resumable browser-control blocker." },
124
+ remediation: [],
125
+ nextCommands: options.nextCommands ?? []
126
+ };
127
+ if (context !== undefined)
128
+ explanation.context = context;
129
+ return { ...explanation, markdown: renderMarkdown(explanation) };
130
+ }
131
+ const profile = PROFILES[blocker.kind] ?? PROFILES.unknown;
132
+ const resume = toResumeGuidance(resumeDecisionForBlocker(blocker, options.stateId), options.command);
133
+ const retry = retryGuidance(blocker, profile, resume, options.command);
134
+ const remediation = blocker.remediation ?? profile.defaultRemediation ?? [];
135
+ const summary = summaryForBlocker(blocker);
136
+ const base = {
137
+ kind: blocker.kind,
138
+ title: profile.title,
139
+ summary,
140
+ severity: profile.severity,
141
+ category: categoryForBlocker(blocker, profile),
142
+ userActionRequired: profile.userActionRequired || remediation.some(step => step.userActionRequired),
143
+ retry,
144
+ resume,
145
+ remediation,
146
+ nextCommands: options.nextCommands ?? defaultNextCommands(blocker, options.command, resume)
147
+ };
148
+ if (blocker.code !== undefined)
149
+ base.code = blocker.code;
150
+ if (context !== undefined)
151
+ base.context = context;
152
+ if (blocker.candidates !== undefined)
153
+ base.candidates = blocker.candidates;
154
+ if (blocker.diagnostics !== undefined)
155
+ base.diagnostics = blocker.diagnostics;
156
+ return { ...base, markdown: renderMarkdown(base) };
157
+ }
158
+ function isCommandResult(value) {
159
+ return typeof value === "object"
160
+ && value !== null
161
+ && "ok" in value
162
+ && "status" in value
163
+ && "context" in value;
164
+ }
165
+ function isBlocker(value) {
166
+ return typeof value === "object"
167
+ && value !== null
168
+ && "kind" in value
169
+ && "message" in value;
170
+ }
171
+ function explanationContext(source, command) {
172
+ const context = {};
173
+ if (command !== undefined)
174
+ context.command = command;
175
+ if (source?.url !== undefined)
176
+ context.url = source.url;
177
+ if (source?.conversationId !== undefined)
178
+ context.conversationId = source.conversationId;
179
+ if (source?.tabId !== undefined)
180
+ context.tabId = source.tabId;
181
+ return Object.keys(context).length === 0 ? undefined : context;
182
+ }
183
+ function summaryForBlocker(blocker) {
184
+ const code = blocker.code === undefined ? "" : ` (${blocker.code})`;
185
+ return `${blocker.kind}${code}: ${blocker.message}`;
186
+ }
187
+ function categoryForBlocker(blocker, profile) {
188
+ if (blocker.kind === "not_found" && blocker.code?.startsWith("existing_tab_") === true) {
189
+ return "targeting";
190
+ }
191
+ return profile.category;
192
+ }
193
+ function retryGuidance(blocker, profile, resume, command) {
194
+ if (resume.supported) {
195
+ const guidance = {
196
+ safe: true,
197
+ when: retryWhen(blocker)
198
+ };
199
+ if (command !== undefined)
200
+ guidance.command = command;
201
+ return guidance;
202
+ }
203
+ return { safe: false, reason: profile.defaultRetryReason };
204
+ }
205
+ function retryWhen(blocker) {
206
+ switch (blocker.kind) {
207
+ case "permission":
208
+ case "upload_failed":
209
+ return "After the reported permission/upload issue is fixed and before any duplicate prompt submission.";
210
+ case "confirmation":
211
+ return "After the user approves the exact bounded action.";
212
+ case "download_unavailable":
213
+ case "artifact_download_unavailable":
214
+ return "After the download affordance appears without resubmitting the prompt.";
215
+ default:
216
+ return "After the blocker is resolved and the command remains safe to resume.";
217
+ }
218
+ }
219
+ function toResumeGuidance(decision, command) {
220
+ if (!decision.supported) {
221
+ return decision;
222
+ }
223
+ const supported = { supported: true };
224
+ if (decision.stateId !== undefined)
225
+ supported.stateId = decision.stateId;
226
+ if (command !== undefined)
227
+ supported.command = command;
228
+ return supported;
229
+ }
230
+ function defaultNextCommands(blocker, command, resume) {
231
+ if (blocker.kind === "browser_bridge_unavailable")
232
+ return ["session.bootstrap"];
233
+ if (blocker.kind === "not_found" && blocker.code?.startsWith("existing_tab_") === true) {
234
+ return command === undefined ? [] : [command];
235
+ }
236
+ if (resume.supported && command !== undefined)
237
+ return [command];
238
+ return [];
239
+ }
240
+ function renderMarkdown(explanation) {
241
+ const lines = [
242
+ `### ${explanation.title}`,
243
+ "",
244
+ explanation.summary,
245
+ "",
246
+ `- Kind: \`${explanation.kind}\``
247
+ ];
248
+ if (explanation.code !== undefined)
249
+ lines.push(`- Code: \`${explanation.code}\``);
250
+ lines.push(`- Category: \`${explanation.category}\``);
251
+ lines.push(`- Severity: \`${explanation.severity}\``);
252
+ if (explanation.context?.command !== undefined)
253
+ lines.push(`- Command: \`${explanation.context.command}\``);
254
+ if (explanation.context?.url !== undefined)
255
+ lines.push(`- URL: ${explanation.context.url}`);
256
+ if (explanation.context?.conversationId !== undefined)
257
+ lines.push(`- Conversation: \`${explanation.context.conversationId}\``);
258
+ if (explanation.context?.tabId !== undefined)
259
+ lines.push(`- Tab: \`${explanation.context.tabId}\``);
260
+ lines.push("");
261
+ if (explanation.retry.safe) {
262
+ lines.push(`Retry: safe only ${explanation.retry.when}`);
263
+ }
264
+ else {
265
+ lines.push(`Retry: ${explanation.retry.reason}`);
266
+ }
267
+ if (explanation.resume.supported) {
268
+ const state = explanation.resume.stateId === undefined ? "" : ` with state \`${explanation.resume.stateId}\``;
269
+ lines.push(`Resume: supported${state}.`);
270
+ }
271
+ else {
272
+ lines.push(`Resume: ${explanation.resume.reason}`);
273
+ }
274
+ if (explanation.remediation.length > 0) {
275
+ lines.push("", "Remediation:");
276
+ for (const step of explanation.remediation) {
277
+ lines.push(`- ${step.label}: ${step.instruction}`);
278
+ }
279
+ }
280
+ if ((explanation.candidates?.length ?? 0) > 0) {
281
+ lines.push("", "Candidates:");
282
+ for (const candidate of explanation.candidates ?? []) {
283
+ const role = candidate.role === undefined ? "" : ` (${candidate.role})`;
284
+ lines.push(`- ${candidate.label}${role}`);
285
+ }
286
+ }
287
+ const existingTab = explanation.diagnostics?.existingTab;
288
+ if (existingTab !== undefined) {
289
+ lines.push("", "Existing-tab diagnostics:");
290
+ lines.push(`- Target: \`${existingTab.requestedTarget.type}\``);
291
+ lines.push(`- Mismatch: \`${existingTab.mismatchReason}\``);
292
+ lines.push(`- User-open tab enumeration: \`${existingTab.userOpenTabsAvailable ? "available" : "unavailable"}\``);
293
+ lines.push(`- ChatGPT tabs seen: \`${existingTab.chatgptTabCount}\``);
294
+ for (const tab of existingTab.candidateTabs) {
295
+ lines.push(`- Candidate tab ${tab.id}: ${tab.title ?? "Untitled"} - ${tab.url ?? "unknown URL"}`);
296
+ }
297
+ }
298
+ return lines.join("\n");
299
+ }
@@ -0,0 +1,7 @@
1
+ import type { GeneratedArtifact, ListArtifactsArgs, PageLike } from "../types.js";
2
+ export declare function listPageArtifacts(page: PageLike, args?: ListArtifactsArgs): Promise<GeneratedArtifact[]>;
3
+ export declare function countPageArtifacts(page: PageLike, args?: ListArtifactsArgs): Promise<number>;
4
+ export declare function readLatestImageDataUrl(page: PageLike, timeoutMs: number | undefined): Promise<{
5
+ dataUrl: string;
6
+ alt?: string;
7
+ } | undefined>;
@@ -0,0 +1,198 @@
1
+ import { withTimeout, localGuardTimeout } from "../commands/timeouts.js";
2
+ export async function listPageArtifacts(page, args = {}) {
3
+ const timeoutMs = localGuardTimeout(args.timeoutMs, 5000);
4
+ let artifacts;
5
+ let evaluateError;
6
+ if (typeof page.evaluate === "function") {
7
+ artifacts = await withTimeout(page.evaluate(() => {
8
+ const images = Array.from(document.querySelectorAll("main img"));
9
+ return images
10
+ .map((image, index) => {
11
+ const rect = image.getBoundingClientRect();
12
+ const style = window.getComputedStyle(image);
13
+ const width = Math.round(rect.width || image.naturalWidth || image.width || 0);
14
+ const height = Math.round(rect.height || image.naturalHeight || image.height || 0);
15
+ const alt = image.getAttribute("alt") ?? undefined;
16
+ const src = image.currentSrc || image.src || undefined;
17
+ const ariaLabel = image.getAttribute("aria-label") ?? image.closest("[aria-label]")?.getAttribute("aria-label") ?? undefined;
18
+ const visible = width > 0
19
+ && height > 0
20
+ && style.display !== "none"
21
+ && style.visibility !== "hidden"
22
+ && Number(style.opacity || "1") > 0;
23
+ const likelyGenerated = visible
24
+ && !image.closest("nav, aside, header, footer, form, [contenteditable='true'], textarea")
25
+ && (width >= 96
26
+ || height >= 96
27
+ || /^data:image\//i.test(src ?? "")
28
+ || /^blob:/i.test(src ?? "")
29
+ || /\b(generated|image|photo|picture)\b/i.test(`${alt ?? ""} ${ariaLabel ?? ""}`));
30
+ if (!likelyGenerated)
31
+ return undefined;
32
+ const container = image.closest("figure, [data-testid*='image' i], [aria-label*='image' i], [role='group'], [data-testid^='conversation-turn']") ?? image.parentElement;
33
+ const scopedDownload = container?.querySelector("a[download], button[aria-label*='Download' i], a[aria-label*='Download' i]");
34
+ const globalDownload = document.querySelector("main button[aria-label*='Download image' i], main a[aria-label*='Download image' i]");
35
+ const turnNode = image.closest("[data-testid^='conversation-turn']");
36
+ const artifact = {
37
+ kind: "image",
38
+ index,
39
+ visible,
40
+ width,
41
+ height,
42
+ downloadAvailable: Boolean(scopedDownload ?? globalDownload),
43
+ selectorProvenance: "main generated image"
44
+ };
45
+ if (alt !== undefined)
46
+ artifact.alt = alt;
47
+ if (ariaLabel !== undefined)
48
+ artifact.ariaLabel = ariaLabel;
49
+ const safeSrc = safeArtifactSrc(src);
50
+ if (safeSrc !== undefined)
51
+ artifact.src = safeSrc;
52
+ const turnId = turnNode?.getAttribute("data-testid") ?? undefined;
53
+ if (turnId !== undefined)
54
+ artifact.turnId = turnId;
55
+ return artifact;
56
+ })
57
+ .filter((artifact) => artifact !== undefined);
58
+ }), timeoutMs, "Timed out while inspecting visible ChatGPT artifacts.").catch(error => {
59
+ evaluateError = error;
60
+ return undefined;
61
+ });
62
+ }
63
+ if (artifacts === undefined && typeof page.content !== "function" && evaluateError !== undefined) {
64
+ throw evaluateError;
65
+ }
66
+ const filtered = filterArtifacts(artifacts ?? await listArtifactsFromContent(page, timeoutMs), args);
67
+ return filtered.map((artifact, index) => ({ ...artifact, index }));
68
+ }
69
+ export async function countPageArtifacts(page, args = {}) {
70
+ return listPageArtifacts(page, args).then(artifacts => artifacts.length);
71
+ }
72
+ export async function readLatestImageDataUrl(page, timeoutMs) {
73
+ const guardMs = localGuardTimeout(timeoutMs, 5000);
74
+ if (typeof page.evaluate === "function") {
75
+ const fromDom = await withTimeout(page.evaluate(async () => {
76
+ const images = Array.from(document.querySelectorAll("main img"));
77
+ const candidates = images.filter(image => {
78
+ const rect = image.getBoundingClientRect();
79
+ const width = rect.width || image.naturalWidth || image.width || 0;
80
+ const height = rect.height || image.naturalHeight || image.height || 0;
81
+ const src = image.currentSrc || image.src || "";
82
+ const label = `${image.getAttribute("alt") ?? ""} ${image.closest("[aria-label]")?.getAttribute("aria-label") ?? ""}`;
83
+ return !image.closest("nav, aside, header, footer, form, [contenteditable='true'], textarea")
84
+ && (width >= 96 || height >= 96 || /^data:image\//i.test(src) || /^blob:/i.test(src) || /\b(generated|image|photo|picture)\b/i.test(label));
85
+ });
86
+ const image = candidates.at(-1);
87
+ if (image === undefined)
88
+ return undefined;
89
+ const src = image.currentSrc || image.src;
90
+ if (/^data:image\//i.test(src)) {
91
+ const alt = image.getAttribute("alt") ?? undefined;
92
+ return alt === undefined ? { dataUrl: src } : { dataUrl: src, alt };
93
+ }
94
+ if (/^(blob:|https?:)/i.test(src)) {
95
+ const response = await fetch(src);
96
+ const blob = await response.blob();
97
+ const dataUrl = await new Promise((resolve, reject) => {
98
+ const reader = new FileReader();
99
+ reader.onload = () => resolve(String(reader.result));
100
+ reader.onerror = () => reject(reader.error ?? new Error("FileReader failed."));
101
+ reader.readAsDataURL(blob);
102
+ });
103
+ const alt = image.getAttribute("alt") ?? undefined;
104
+ return alt === undefined ? { dataUrl } : { dataUrl, alt };
105
+ }
106
+ return undefined;
107
+ }), guardMs, "Timed out while reading the visible generated image source.").catch(() => undefined);
108
+ if (fromDom !== undefined)
109
+ return fromDom;
110
+ }
111
+ const html = await readContentWithTimeout(page, guardMs).catch(() => undefined);
112
+ if (html === undefined)
113
+ return undefined;
114
+ const artifact = parseArtifactsFromHtml(html).at(-1);
115
+ if (artifact?.src === undefined || !/^data:image\//i.test(artifact.src))
116
+ return undefined;
117
+ return artifact.alt === undefined
118
+ ? { dataUrl: artifact.src }
119
+ : { dataUrl: artifact.src, alt: artifact.alt };
120
+ }
121
+ async function listArtifactsFromContent(page, timeoutMs) {
122
+ const html = await readContentWithTimeout(page, timeoutMs).catch(() => undefined);
123
+ return html === undefined ? [] : parseArtifactsFromHtml(html);
124
+ }
125
+ function parseArtifactsFromHtml(html) {
126
+ const hasDownload = /<a\b[^>]*\sdownload(?:\s|=|>)/i.test(html)
127
+ || /\baria-label=["'][^"']*download[^"']*["']/i.test(html);
128
+ const artifacts = [];
129
+ const imagePattern = /<img\b[^>]*>/gi;
130
+ let match;
131
+ while ((match = imagePattern.exec(html)) !== null) {
132
+ const tag = match[0] ?? "";
133
+ const src = attr(tag, "src");
134
+ const alt = attr(tag, "alt");
135
+ const ariaLabel = attr(tag, "aria-label");
136
+ const width = numberAttr(tag, "width");
137
+ const height = numberAttr(tag, "height");
138
+ const label = `${alt ?? ""} ${ariaLabel ?? ""}`;
139
+ const likelyGenerated = (width ?? 0) >= 96
140
+ || (height ?? 0) >= 96
141
+ || /^data:image\//i.test(src ?? "")
142
+ || /^blob:/i.test(src ?? "")
143
+ || /\b(generated|image|photo|picture)\b/i.test(label);
144
+ if (!likelyGenerated)
145
+ continue;
146
+ const artifact = {
147
+ kind: "image",
148
+ index: artifacts.length,
149
+ visible: true,
150
+ downloadAvailable: hasDownload,
151
+ selectorProvenance: "main generated image"
152
+ };
153
+ const safeSrc = safeArtifactSrc(src);
154
+ if (safeSrc !== undefined)
155
+ artifact.src = safeSrc;
156
+ if (alt !== undefined)
157
+ artifact.alt = alt;
158
+ if (ariaLabel !== undefined)
159
+ artifact.ariaLabel = ariaLabel;
160
+ if (width !== undefined)
161
+ artifact.width = width;
162
+ if (height !== undefined)
163
+ artifact.height = height;
164
+ artifacts.push(artifact);
165
+ }
166
+ return artifacts;
167
+ }
168
+ function filterArtifacts(artifacts, args) {
169
+ const kind = args.kind ?? "image";
170
+ const max = args.max ?? artifacts.length;
171
+ return artifacts
172
+ .filter(artifact => artifact.kind === kind)
173
+ .slice(-max);
174
+ }
175
+ async function readContentWithTimeout(page, timeoutMs) {
176
+ if (typeof page.content !== "function")
177
+ return "";
178
+ return withTimeout(page.content(), timeoutMs, "Timed out while reading ChatGPT page content.");
179
+ }
180
+ function attr(tag, name) {
181
+ const match = new RegExp(`\\b${name}=(["'])(.*?)\\1`, "i").exec(tag);
182
+ return match?.[2];
183
+ }
184
+ function numberAttr(tag, name) {
185
+ const value = attr(tag, name);
186
+ if (value === undefined)
187
+ return undefined;
188
+ const parsed = Number(value);
189
+ return Number.isFinite(parsed) ? parsed : undefined;
190
+ }
191
+ function safeArtifactSrc(src) {
192
+ if (src === undefined)
193
+ return undefined;
194
+ if (/^https:\/\/chatgpt\.com\/backend-api\/estuary\/content\b/i.test(src)) {
195
+ return undefined;
196
+ }
197
+ return src;
198
+ }
@@ -0,0 +1,8 @@
1
+ import type { PageLike } from "../types.js";
2
+ export type AssistantGenerationState = {
3
+ active: boolean;
4
+ stopped: boolean;
5
+ signals: string[];
6
+ };
7
+ export declare function readAssistantGenerationState(page: PageLike): Promise<AssistantGenerationState>;
8
+ export declare function latestAssistantTurnHasResponseActions(page: PageLike): Promise<boolean>;
@@ -0,0 +1,103 @@
1
+ import { copyResponseButtons } from "./selectors.js";
2
+ import { localeLabels } from "./locale-labels.js";
3
+ const EMPTY_GENERATION_STATE = {
4
+ active: false,
5
+ stopped: false,
6
+ signals: []
7
+ };
8
+ export async function readAssistantGenerationState(page) {
9
+ if (typeof page.evaluate === "function") {
10
+ return page.evaluate((args) => {
11
+ const normalize = (value) => (value ?? "").trim().toLowerCase();
12
+ const isVisible = (element) => {
13
+ const style = window.getComputedStyle(element);
14
+ return style.display !== "none"
15
+ && style.visibility !== "hidden"
16
+ && style.opacity !== "0"
17
+ && element.getAttribute("aria-hidden") !== "true";
18
+ };
19
+ const visibleButtons = Array.from(document.querySelectorAll("button"))
20
+ .filter((button) => isVisible(button)
21
+ && button.disabled !== true
22
+ && button.getAttribute("aria-disabled") !== "true");
23
+ const buttonTexts = visibleButtons
24
+ .map(button => [
25
+ button.innerText,
26
+ button.textContent,
27
+ button.getAttribute("aria-label"),
28
+ button.getAttribute("title")
29
+ ].map(normalize).filter(Boolean).join(" "))
30
+ .filter(Boolean);
31
+ const bodyText = normalize(document.body?.innerText);
32
+ const haystacks = [bodyText, ...buttonTexts];
33
+ const matchingSignals = (phrases) => haystacks.flatMap(text => phrases
34
+ .map(phrase => phrase.toLowerCase())
35
+ .filter(phrase => text.includes(phrase)));
36
+ const activeSignals = matchingSignals(args.stop);
37
+ const stoppedSignals = matchingSignals(args.stopped);
38
+ return {
39
+ active: activeSignals.length > 0,
40
+ stopped: stoppedSignals.length > 0,
41
+ signals: [...new Set([...activeSignals, ...stoppedSignals, ...buttonTexts.filter(text => /stop|cancel|stopped|answering|thinking/i.test(text))])].slice(0, 5)
42
+ };
43
+ }, {
44
+ stop: [...localeLabels.stopControl],
45
+ stopped: [...localeLabels.stoppedAssistant]
46
+ }).catch(() => EMPTY_GENERATION_STATE);
47
+ }
48
+ if (typeof page.content === "function") {
49
+ const html = await page.content().catch(() => "");
50
+ return generationStateFromText(html);
51
+ }
52
+ return EMPTY_GENERATION_STATE;
53
+ }
54
+ export async function latestAssistantTurnHasResponseActions(page) {
55
+ if (typeof page.evaluate === "function") {
56
+ const scoped = await page.evaluate((phrases) => {
57
+ const turns = Array.from(document.querySelectorAll("[data-testid^='conversation-turn']"));
58
+ if (turns.length === 0)
59
+ return undefined;
60
+ const latestTurn = turns.reverse().find(turn => turn.querySelector("[data-message-author-role='assistant']") !== null);
61
+ if (latestTurn === undefined)
62
+ return false;
63
+ const actionText = Array.from(latestTurn.querySelectorAll("button"))
64
+ .map(button => [
65
+ button.innerText,
66
+ button.textContent,
67
+ button.getAttribute("aria-label"),
68
+ button.getAttribute("title")
69
+ ].filter(Boolean).join(" "))
70
+ .join(" ")
71
+ .toLowerCase();
72
+ return phrases.some(phrase => actionText.includes(phrase.toLowerCase()));
73
+ }, [...localeLabels.responseActions]).catch(() => undefined);
74
+ if (scoped !== undefined) {
75
+ return scoped;
76
+ }
77
+ }
78
+ try {
79
+ const copyButtons = copyResponseButtons(page);
80
+ const count = await copyButtons.count?.();
81
+ if (count !== undefined) {
82
+ return count > 0;
83
+ }
84
+ return await copyButtons.isVisible?.() === true;
85
+ }
86
+ catch {
87
+ if (typeof page.content === "function") {
88
+ const html = await page.content().catch(() => "");
89
+ return localeLabels.responseActions.some(phrase => html.toLowerCase().includes(phrase.toLowerCase()));
90
+ }
91
+ return true;
92
+ }
93
+ }
94
+ function generationStateFromText(text) {
95
+ const normalized = text.toLowerCase();
96
+ const activeSignals = localeLabels.stopControl.filter(phrase => normalized.includes(phrase.toLowerCase()));
97
+ const stoppedSignals = localeLabels.stoppedAssistant.filter(phrase => normalized.includes(phrase.toLowerCase()));
98
+ return {
99
+ active: activeSignals.length > 0,
100
+ stopped: stoppedSignals.length > 0,
101
+ signals: [...activeSignals, ...stoppedSignals].slice(0, 5)
102
+ };
103
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Amharic (am). Captured 2026-06-09 against a live chatgpt.com session
3
+ * (html lang=am, Google Translate confirmed off).
4
+ *
5
+ * Some non-Intelligence surfaces may still fall back to English + `selector_drift`.
6
+ *
7
+ * Intelligence picker labels updated 2026-06-10 from a visible ChatGPT Pro session.
8
+ */
9
+ export declare const am: {
10
+ composerTextbox: string[];
11
+ sendButton: string[];
12
+ searchChatsButton: string[];
13
+ searchChatsPlaceholder: string[];
14
+ newChat: string[];
15
+ addFilesButton: string[];
16
+ addFilesOpenerCandidates: string[];
17
+ addPhotosFilesMenuItem: string[];
18
+ copyResponse: string[];
19
+ modeLabels: string[];
20
+ modeOpenerExtra: string[];
21
+ tools: {
22
+ web_search: string[];
23
+ deep_research: string[];
24
+ create_image: string[];
25
+ };
26
+ signedInMarkers: string[];
27
+ responseActions: string[];
28
+ };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Amharic (am). Captured 2026-06-09 against a live chatgpt.com session
3
+ * (html lang=am, Google Translate confirmed off).
4
+ *
5
+ * Some non-Intelligence surfaces may still fall back to English + `selector_drift`.
6
+ *
7
+ * Intelligence picker labels updated 2026-06-10 from a visible ChatGPT Pro session.
8
+ */
9
+ export const am = {
10
+ composerTextbox: ["ከChatGPT ጋር ይወያዩ"],
11
+ sendButton: ["ጥያቄ ላክ"],
12
+ searchChatsButton: ["ውይይቶችን ፈልግ"],
13
+ searchChatsPlaceholder: ["ውይይቶችን ፈልግ..."],
14
+ newChat: ["አዲስ ውይይት"],
15
+ addFilesButton: ["ፋይሎችን ያክሉ እና ሌሎችም"],
16
+ addFilesOpenerCandidates: ["ፋይሎችን ያክሉ እና ሌሎችም"],
17
+ addPhotosFilesMenuItem: ["ፎቶዎችን እና ፋይሎችን ያክሉ"],
18
+ copyResponse: ["ምላሹን ይቅዱ"],
19
+ modeLabels: ["ፈጣን", "መካከለኛ", "ከፍተኛ", "እጅግ ከፍተኛ"],
20
+ modeOpenerExtra: ["ያዋቅሩ"],
21
+ tools: {
22
+ web_search: ["የድር ፍለጋ"],
23
+ deep_research: ["ጥልቅ ምርምር"],
24
+ create_image: ["ምስል ፍጠር"],
25
+ },
26
+ signedInMarkers: ["አዲስ ውይይት", "ውይይቶችን ፈልግ", "የቅርብ ጊዜዎች", "ላይብረሪ", "ፕሮጀክቶች", "ከChatGPT ጋር ይወያዩ"],
27
+ responseActions: ["ምላሹን ይቅዱ"],
28
+ };