codex-chatgpt-control 0.3.0-alpha.1 → 0.5.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 (214) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +42 -2
  3. package/contracts/v1/fixtures/backend-capabilities.json +10 -0
  4. package/contracts/v1/fixtures/command-descriptors.json +257 -6
  5. package/contracts/v1/fixtures/describe-runner-run.json +5 -2
  6. package/contracts/v1/fixtures/doctor-scenario-preflight.json +34 -2
  7. package/contracts/v1/fixtures/help-root.json +1 -1
  8. package/contracts/v1/fixtures/output-json-parse-success.json +9 -0
  9. package/contracts/v1/fixtures/run-timeout-partial.json +15 -4
  10. package/contracts/v1/fixtures/stream-in-progress.ndjson +3 -0
  11. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
  12. package/contracts/v1/fixtures/surface-chat-legacy.json +35 -0
  13. package/contracts/v1/fixtures/surface-chat-simplified.json +37 -0
  14. package/contracts/v1/fixtures/surface-sidebar-false-positive.json +29 -0
  15. package/contracts/v1/fixtures/surface-work-advanced.json +45 -0
  16. package/contracts/v1/fixtures/surface-work-basic.json +38 -0
  17. package/contracts/v1/fixtures/workflow-ask-success.json +8 -0
  18. package/contracts/v1/manifest.json +32 -1
  19. package/contracts/v1/parity-suite.json +113 -0
  20. package/contracts/v1/schemas/backend-request.schema.json +15 -0
  21. package/contracts/v1/schemas/manifest.schema.json +6 -3
  22. package/contracts/v1/schemas/surface-profile.schema.json +166 -0
  23. package/dist/codex-chatgpt-control-backend.mjs +2045 -227
  24. package/dist/codex-chatgpt-control.bundle.mjs +3817 -1960
  25. package/dist/src/backend/client.d.ts +26 -1
  26. package/dist/src/backend/client.js +23 -1
  27. package/dist/src/backend/protocol.d.ts +1 -1
  28. package/dist/src/backend/protocol.js +10 -0
  29. package/dist/src/backend/session.js +20 -0
  30. package/dist/src/browser/attach.d.ts +1 -0
  31. package/dist/src/browser/attach.js +3 -3
  32. package/dist/src/browser/page-state.js +5 -2
  33. package/dist/src/client.d.ts +31 -1
  34. package/dist/src/client.js +69 -14
  35. package/dist/src/commands/artifacts.js +1 -7
  36. package/dist/src/commands/configuration.d.ts +15 -0
  37. package/dist/src/commands/configuration.js +674 -0
  38. package/dist/src/commands/doctor.js +38 -1
  39. package/dist/src/commands/experience.d.ts +12 -0
  40. package/dist/src/commands/experience.js +288 -0
  41. package/dist/src/commands/files.js +1 -7
  42. package/dist/src/commands/messages.d.ts +2 -1
  43. package/dist/src/commands/messages.js +138 -16
  44. package/dist/src/commands/modes.js +28 -14
  45. package/dist/src/commands/registry.js +69 -7
  46. package/dist/src/commands/response-actions.js +1 -7
  47. package/dist/src/commands/sequence.js +24 -1
  48. package/dist/src/commands/session.d.ts +2 -0
  49. package/dist/src/commands/session.js +50 -1
  50. package/dist/src/commands/threads.js +1 -7
  51. package/dist/src/commands/work.d.ts +6 -0
  52. package/dist/src/commands/work.js +338 -0
  53. package/dist/src/dom/locale/am.d.ts +2 -1
  54. package/dist/src/dom/locale/am.js +2 -1
  55. package/dist/src/dom/locale/ar.d.ts +2 -1
  56. package/dist/src/dom/locale/ar.js +2 -1
  57. package/dist/src/dom/locale/bg.d.ts +2 -1
  58. package/dist/src/dom/locale/bg.js +2 -1
  59. package/dist/src/dom/locale/bn.d.ts +2 -1
  60. package/dist/src/dom/locale/bn.js +2 -1
  61. package/dist/src/dom/locale/bs.d.ts +2 -1
  62. package/dist/src/dom/locale/bs.js +2 -1
  63. package/dist/src/dom/locale/ca.d.ts +2 -1
  64. package/dist/src/dom/locale/ca.js +2 -1
  65. package/dist/src/dom/locale/cs.d.ts +2 -1
  66. package/dist/src/dom/locale/cs.js +2 -1
  67. package/dist/src/dom/locale/da.d.ts +2 -1
  68. package/dist/src/dom/locale/da.js +2 -1
  69. package/dist/src/dom/locale/de.d.ts +4 -3
  70. package/dist/src/dom/locale/de.js +4 -3
  71. package/dist/src/dom/locale/el.d.ts +2 -1
  72. package/dist/src/dom/locale/el.js +2 -1
  73. package/dist/src/dom/locale/en.d.ts +27 -2
  74. package/dist/src/dom/locale/en.js +28 -2
  75. package/dist/src/dom/locale/es-419.d.ts +2 -1
  76. package/dist/src/dom/locale/es-419.js +2 -1
  77. package/dist/src/dom/locale/es-ES.d.ts +2 -1
  78. package/dist/src/dom/locale/es-ES.js +2 -1
  79. package/dist/src/dom/locale/et.d.ts +2 -1
  80. package/dist/src/dom/locale/et.js +2 -1
  81. package/dist/src/dom/locale/fa.d.ts +2 -1
  82. package/dist/src/dom/locale/fa.js +2 -1
  83. package/dist/src/dom/locale/fi.d.ts +2 -1
  84. package/dist/src/dom/locale/fi.js +2 -1
  85. package/dist/src/dom/locale/fr-CA.d.ts +2 -1
  86. package/dist/src/dom/locale/fr-CA.js +2 -1
  87. package/dist/src/dom/locale/fr-FR.d.ts +3 -3
  88. package/dist/src/dom/locale/fr-FR.js +3 -3
  89. package/dist/src/dom/locale/gu.d.ts +2 -1
  90. package/dist/src/dom/locale/gu.js +2 -1
  91. package/dist/src/dom/locale/hi.d.ts +2 -1
  92. package/dist/src/dom/locale/hi.js +2 -1
  93. package/dist/src/dom/locale/hr.d.ts +2 -1
  94. package/dist/src/dom/locale/hr.js +2 -1
  95. package/dist/src/dom/locale/hu.d.ts +2 -1
  96. package/dist/src/dom/locale/hu.js +2 -1
  97. package/dist/src/dom/locale/hy.d.ts +2 -1
  98. package/dist/src/dom/locale/hy.js +2 -1
  99. package/dist/src/dom/locale/id.d.ts +2 -1
  100. package/dist/src/dom/locale/id.js +2 -1
  101. package/dist/src/dom/locale/index.d.ts +17 -2
  102. package/dist/src/dom/locale/index.js +65 -0
  103. package/dist/src/dom/locale/is.d.ts +2 -1
  104. package/dist/src/dom/locale/is.js +2 -1
  105. package/dist/src/dom/locale/it.d.ts +2 -1
  106. package/dist/src/dom/locale/it.js +2 -1
  107. package/dist/src/dom/locale/ja.d.ts +2 -1
  108. package/dist/src/dom/locale/ja.js +2 -1
  109. package/dist/src/dom/locale/ka.d.ts +2 -1
  110. package/dist/src/dom/locale/ka.js +2 -1
  111. package/dist/src/dom/locale/kk.d.ts +2 -1
  112. package/dist/src/dom/locale/kk.js +2 -1
  113. package/dist/src/dom/locale/kn.d.ts +2 -1
  114. package/dist/src/dom/locale/kn.js +2 -1
  115. package/dist/src/dom/locale/ko.d.ts +2 -1
  116. package/dist/src/dom/locale/ko.js +2 -1
  117. package/dist/src/dom/locale/lt.d.ts +2 -1
  118. package/dist/src/dom/locale/lt.js +2 -1
  119. package/dist/src/dom/locale/lv.d.ts +2 -1
  120. package/dist/src/dom/locale/lv.js +2 -1
  121. package/dist/src/dom/locale/mk.d.ts +2 -1
  122. package/dist/src/dom/locale/mk.js +2 -1
  123. package/dist/src/dom/locale/ml.d.ts +2 -1
  124. package/dist/src/dom/locale/ml.js +2 -1
  125. package/dist/src/dom/locale/mn.d.ts +2 -1
  126. package/dist/src/dom/locale/mn.js +2 -1
  127. package/dist/src/dom/locale/mr.d.ts +2 -1
  128. package/dist/src/dom/locale/mr.js +2 -1
  129. package/dist/src/dom/locale/ms.d.ts +2 -1
  130. package/dist/src/dom/locale/ms.js +2 -1
  131. package/dist/src/dom/locale/my.d.ts +2 -1
  132. package/dist/src/dom/locale/my.js +2 -1
  133. package/dist/src/dom/locale/nb.d.ts +2 -1
  134. package/dist/src/dom/locale/nb.js +2 -1
  135. package/dist/src/dom/locale/nl.d.ts +2 -1
  136. package/dist/src/dom/locale/nl.js +2 -1
  137. package/dist/src/dom/locale/pa.d.ts +2 -1
  138. package/dist/src/dom/locale/pa.js +2 -1
  139. package/dist/src/dom/locale/pl.d.ts +2 -1
  140. package/dist/src/dom/locale/pl.js +2 -1
  141. package/dist/src/dom/locale/pt-BR.d.ts +2 -1
  142. package/dist/src/dom/locale/pt-BR.js +2 -1
  143. package/dist/src/dom/locale/pt-PT.d.ts +2 -1
  144. package/dist/src/dom/locale/pt-PT.js +2 -1
  145. package/dist/src/dom/locale/ro.d.ts +2 -1
  146. package/dist/src/dom/locale/ro.js +2 -1
  147. package/dist/src/dom/locale/ru.d.ts +2 -1
  148. package/dist/src/dom/locale/ru.js +2 -1
  149. package/dist/src/dom/locale/sk.d.ts +2 -1
  150. package/dist/src/dom/locale/sk.js +2 -1
  151. package/dist/src/dom/locale/sl.d.ts +2 -1
  152. package/dist/src/dom/locale/sl.js +2 -1
  153. package/dist/src/dom/locale/so.d.ts +2 -1
  154. package/dist/src/dom/locale/so.js +2 -1
  155. package/dist/src/dom/locale/sq.d.ts +2 -1
  156. package/dist/src/dom/locale/sq.js +2 -1
  157. package/dist/src/dom/locale/sr.d.ts +2 -1
  158. package/dist/src/dom/locale/sr.js +2 -1
  159. package/dist/src/dom/locale/sv.d.ts +2 -1
  160. package/dist/src/dom/locale/sv.js +2 -1
  161. package/dist/src/dom/locale/sw.d.ts +2 -1
  162. package/dist/src/dom/locale/sw.js +2 -1
  163. package/dist/src/dom/locale/ta.d.ts +2 -1
  164. package/dist/src/dom/locale/ta.js +2 -1
  165. package/dist/src/dom/locale/te.d.ts +2 -1
  166. package/dist/src/dom/locale/te.js +2 -1
  167. package/dist/src/dom/locale/th.d.ts +2 -1
  168. package/dist/src/dom/locale/th.js +2 -1
  169. package/dist/src/dom/locale/tl.d.ts +4 -5
  170. package/dist/src/dom/locale/tl.js +4 -5
  171. package/dist/src/dom/locale/tr.d.ts +2 -1
  172. package/dist/src/dom/locale/tr.js +2 -1
  173. package/dist/src/dom/locale/types.d.ts +18 -1
  174. package/dist/src/dom/locale/uk.d.ts +2 -1
  175. package/dist/src/dom/locale/uk.js +2 -1
  176. package/dist/src/dom/locale/ur.d.ts +2 -1
  177. package/dist/src/dom/locale/ur.js +2 -1
  178. package/dist/src/dom/locale/vi.d.ts +2 -1
  179. package/dist/src/dom/locale/vi.js +2 -1
  180. package/dist/src/dom/locale/zh-HK.d.ts +2 -1
  181. package/dist/src/dom/locale/zh-HK.js +4 -3
  182. package/dist/src/dom/locale/zh-Hans.d.ts +2 -1
  183. package/dist/src/dom/locale/zh-Hans.js +2 -1
  184. package/dist/src/dom/locale/zh-TW.d.ts +2 -1
  185. package/dist/src/dom/locale/zh-TW.js +2 -1
  186. package/dist/src/dom/selectors.js +6 -1
  187. package/dist/src/index.d.ts +3 -0
  188. package/dist/src/index.js +3 -0
  189. package/dist/src/runner/responses.d.ts +3 -1
  190. package/dist/src/runner/responses.js +16 -1
  191. package/dist/src/runner/result.js +129 -4
  192. package/dist/src/runner/stream.d.ts +1 -1
  193. package/dist/src/runner/stream.js +6 -0
  194. package/dist/src/runner/types.d.ts +35 -1
  195. package/dist/src/safety/risk.d.ts +10 -0
  196. package/dist/src/safety/risk.js +10 -0
  197. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +6 -0
  198. package/dist/src/scripts/apply-intelligence-locale-captures.js +53 -7
  199. package/dist/src/scripts/capture-intelligence-locales.d.ts +3 -0
  200. package/dist/src/scripts/capture-intelligence-locales.js +372 -3
  201. package/dist/src/scripts/capture-surface-profile.d.ts +23 -0
  202. package/dist/src/scripts/capture-surface-profile.js +304 -0
  203. package/dist/src/scripts/live-smoke/scenarios.js +34 -1
  204. package/dist/src/scripts/live-smoke.js +1 -0
  205. package/dist/src/types.d.ts +245 -2
  206. package/package.json +3 -2
  207. package/references/2026-07-16-chat-work-surfaces.md +87 -0
  208. package/references/agents-runner.md +13 -2
  209. package/references/backend-protocol.md +15 -5
  210. package/references/language-coverage.md +40 -3
  211. package/references/localization.md +34 -6
  212. package/references/python-parity.md +11 -1
  213. package/references/responses-adapter.md +12 -1
  214. package/references/troubleshooting.md +22 -2
@@ -0,0 +1,304 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { attachChatGPTBrowser } from "../browser/attach.js";
4
+ import { inspectConfiguration } from "../commands/configuration.js";
5
+ import { detectExperienceFromSnapshot, readSurfaceSnapshot } from "../commands/experience.js";
6
+ import { BROWSER_BRIDGE_REMEDIATION, BROWSER_BRIDGE_UNAVAILABLE_MESSAGE, toCommandResult } from "../errors.js";
7
+ const CHATGPT_HOME = "https://chatgpt.com/";
8
+ const DEFAULT_PROVENANCE = "Read-only sanitized capture from visible ChatGPT controls; review before committing.";
9
+ class UsageError extends Error {
10
+ exitCode;
11
+ constructor(message, exitCode = 2) {
12
+ super(message);
13
+ this.exitCode = exitCode;
14
+ this.name = "UsageError";
15
+ }
16
+ }
17
+ const USAGE = [
18
+ "Usage:",
19
+ " npm run capture:surface-profile -- --id work-basic-en",
20
+ " npm run capture:surface-profile -- --id chat-simplified-de --locale de-DE --out ../../outputs/surface-profiles/chat-simplified-de.json",
21
+ "",
22
+ "Options:",
23
+ " --id Required normalized profile id.",
24
+ " --out Draft JSON path. Defaults under ../../outputs/surface-profiles/.",
25
+ " --locale Known BCP47 locale; otherwise reads documentElement.lang.",
26
+ " --region Normalized supplied region group. Default: not-recorded.",
27
+ " --account-scope Normalized supplied account group. Default: not-recorded.",
28
+ " --plan-scope Normalized supplied plan group. Default: not-recorded.",
29
+ " --workspace-scope Normalized supplied workspace group. Default: not-recorded.",
30
+ " --support-state current|compatibility|unverified|retired. Default: unverified.",
31
+ " --provenance Non-private provenance note.",
32
+ " --tab-id Claim an exact already-open ChatGPT tab.",
33
+ " --if-missing block|open|create. Default: block.",
34
+ "",
35
+ "The capture is read-only with respect to ChatGPT configuration. It opens and",
36
+ "closes visible menus, writes a sanitized draft locally, and never records",
37
+ "prompts, responses, sidebar titles, account names, cookies, or network data."
38
+ ].join("\n");
39
+ export async function main(argv = process.argv.slice(2), runtime = globalThis) {
40
+ let options;
41
+ try {
42
+ options = parseArgs(argv);
43
+ }
44
+ catch (error) {
45
+ if (error instanceof UsageError) {
46
+ console.log(error.message);
47
+ return error.exitCode;
48
+ }
49
+ throw error;
50
+ }
51
+ if ((runtime.agent === undefined || runtime.agent === null) && runtime.browser === undefined) {
52
+ console.log(JSON.stringify({
53
+ ok: false,
54
+ status: "blocked",
55
+ blocker: {
56
+ kind: "browser_bridge_unavailable",
57
+ code: "codex_chrome_bridge_unavailable",
58
+ message: BROWSER_BRIDGE_UNAVAILABLE_MESSAGE,
59
+ remediation: BROWSER_BRIDGE_REMEDIATION
60
+ }
61
+ }, null, 2));
62
+ return 2;
63
+ }
64
+ try {
65
+ const env = {};
66
+ if (runtime.agent !== undefined && runtime.agent !== null)
67
+ env.agent = runtime.agent;
68
+ if (runtime.browser !== undefined)
69
+ env.browser = runtime.browser;
70
+ const attached = await attachChatGPTBrowser(env, {
71
+ existingTab: {
72
+ target: options.tabId === undefined
73
+ ? { type: "selected", host: "chatgpt" }
74
+ : { type: "tabId", tabId: options.tabId },
75
+ ifMissing: options.ifMissing,
76
+ ifMultiple: "first",
77
+ requireChatGPT: true
78
+ },
79
+ url: CHATGPT_HOME
80
+ });
81
+ env.page = attached.page;
82
+ const snapshot = await readSurfaceSnapshot(attached.page);
83
+ const detected = detectExperienceFromSnapshot(snapshot);
84
+ const inspectionResult = await inspectConfiguration(env, { includeOptions: true });
85
+ if (!inspectionResult.ok || inspectionResult.data === undefined) {
86
+ console.log(JSON.stringify(inspectionResult, null, 2));
87
+ return 1;
88
+ }
89
+ const locale = options.locale ?? await readDocumentLocale(attached.page) ?? "und";
90
+ const profile = buildSurfaceProfileDraft(options, locale, snapshot, detected, inspectionResult.data);
91
+ await mkdir(dirname(options.out), { recursive: true });
92
+ await writeFile(options.out, `${JSON.stringify(profile, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
93
+ console.log(JSON.stringify({
94
+ ok: true,
95
+ status: "ok",
96
+ path: options.out,
97
+ id: profile.id,
98
+ experience: profile.expected.experience,
99
+ selectorProfile: profile.expected.selectorProfile,
100
+ supportState: profile.supportState
101
+ }, null, 2));
102
+ return 0;
103
+ }
104
+ catch (error) {
105
+ console.log(JSON.stringify(toCommandResult(error), null, 2));
106
+ return 1;
107
+ }
108
+ }
109
+ export function buildSurfaceProfileDraft(options, locale, snapshot, detected, inspection, observedAt = new Date().toISOString().slice(0, 10)) {
110
+ const activeEntries = Object.entries(inspection.active)
111
+ .filter((entry) => typeof entry[1] === "string");
112
+ const axisRows = inspection.experience === "work"
113
+ ? activeEntries.map(([axis, value]) => ({
114
+ axis,
115
+ label: `${titleCase(axis)} ${value}`,
116
+ value
117
+ }))
118
+ : [];
119
+ const openerLabel = inspection.experience === "chat"
120
+ ? inspection.active.intelligence ?? inspection.active.effort
121
+ : undefined;
122
+ const menuItems = Object.values(inspection.options)
123
+ .flatMap(optionsForAxis => optionsForAxis ?? [])
124
+ .map(option => ({
125
+ label: option.label,
126
+ normalized: option.id,
127
+ role: option.hasSubmenu === true ? "menuitem" : "menuitemradio",
128
+ checked: option.selected,
129
+ ...(option.hasSubmenu === undefined ? {} : { hasPopup: option.hasSubmenu })
130
+ }));
131
+ const safeComposerLabels = dedupeBounded(detected.evidence
132
+ .filter(item => item.source === "composer")
133
+ .map(item => item.label), 16);
134
+ const safeMainControls = dedupeBounded([
135
+ ...axisRows.map(row => row.label),
136
+ ...(openerLabel === undefined ? [] : [openerLabel]),
137
+ ...menuItems.map(item => item.label),
138
+ ...(inspection.selectorProfile === "work_advanced_v1" ? ["Advanced"] : [])
139
+ ], 80);
140
+ return {
141
+ schemaVersion: "chatgpt.browser_control.surface_profile.v1",
142
+ id: options.id,
143
+ observedAt,
144
+ provenance: options.provenance,
145
+ locale,
146
+ region: options.region,
147
+ accountScope: options.accountScope,
148
+ planScope: options.planScope,
149
+ workspaceScope: options.workspaceScope,
150
+ supportState: options.supportState,
151
+ snapshot: {
152
+ url: sanitizeChatGPTUrl(snapshot.url),
153
+ composerLabels: safeComposerLabels,
154
+ mainControls: safeMainControls,
155
+ mainText: detected.evidence
156
+ .filter(item => item.source === "heading")
157
+ .map(item => item.label)
158
+ .join(" ")
159
+ .slice(0, 2000)
160
+ },
161
+ panel: {
162
+ ...(openerLabel === undefined ? {} : { openerLabel }),
163
+ axisRows,
164
+ advancedVisible: inspection.selectorProfile === "work_advanced_v1"
165
+ },
166
+ menuItems,
167
+ expected: {
168
+ experience: detected.experience,
169
+ selectorProfile: inspection.selectorProfile,
170
+ availableAxes: inspection.availableAxes,
171
+ active: inspection.active
172
+ }
173
+ };
174
+ }
175
+ export function parseArgs(argv) {
176
+ let id;
177
+ let out;
178
+ let locale;
179
+ let region = "not-recorded";
180
+ let accountScope = "not-recorded";
181
+ let planScope = "not-recorded";
182
+ let workspaceScope = "not-recorded";
183
+ let supportState = "unverified";
184
+ let provenance = DEFAULT_PROVENANCE;
185
+ let tabId;
186
+ let ifMissing = "block";
187
+ for (let index = 0; index < argv.length; index += 1) {
188
+ const arg = argv[index];
189
+ if (arg === undefined)
190
+ continue;
191
+ switch (arg) {
192
+ case "--help":
193
+ case "-h":
194
+ throw new UsageError(USAGE, 0);
195
+ case "--id":
196
+ id = requiredValue(argv, ++index, arg);
197
+ break;
198
+ case "--out":
199
+ out = requiredValue(argv, ++index, arg);
200
+ break;
201
+ case "--locale":
202
+ locale = requiredValue(argv, ++index, arg);
203
+ break;
204
+ case "--region":
205
+ region = requiredValue(argv, ++index, arg);
206
+ break;
207
+ case "--account-scope":
208
+ accountScope = requiredValue(argv, ++index, arg);
209
+ break;
210
+ case "--plan-scope":
211
+ planScope = requiredValue(argv, ++index, arg);
212
+ break;
213
+ case "--workspace-scope":
214
+ workspaceScope = requiredValue(argv, ++index, arg);
215
+ break;
216
+ case "--support-state":
217
+ supportState = parseSupportState(requiredValue(argv, ++index, arg));
218
+ break;
219
+ case "--provenance":
220
+ provenance = requiredValue(argv, ++index, arg);
221
+ break;
222
+ case "--tab-id":
223
+ tabId = requiredValue(argv, ++index, arg);
224
+ break;
225
+ case "--if-missing":
226
+ ifMissing = parseIfMissing(requiredValue(argv, ++index, arg));
227
+ break;
228
+ default:
229
+ throw new UsageError(`Unknown argument: ${arg}\n\n${USAGE}`);
230
+ }
231
+ }
232
+ if (id === undefined) {
233
+ throw new UsageError(`--id is required.\n\n${USAGE}`);
234
+ }
235
+ for (const [field, value] of Object.entries({
236
+ id,
237
+ region,
238
+ accountScope,
239
+ planScope,
240
+ workspaceScope
241
+ })) {
242
+ assertNormalizedSlug(value, `--${field.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`)}`);
243
+ }
244
+ const defaultOut = resolve(process.cwd(), "..", "..", "outputs", "surface-profiles", `${new Date().toISOString().slice(0, 10)}-${id}.json`);
245
+ return {
246
+ id,
247
+ out: resolve(out ?? defaultOut),
248
+ ...(locale === undefined ? {} : { locale }),
249
+ region,
250
+ accountScope,
251
+ planScope,
252
+ workspaceScope,
253
+ supportState,
254
+ provenance,
255
+ ...(tabId === undefined ? {} : { tabId }),
256
+ ifMissing
257
+ };
258
+ }
259
+ async function readDocumentLocale(page) {
260
+ if (page.evaluate === undefined)
261
+ return undefined;
262
+ return page.evaluate(() => document.documentElement.lang || undefined).catch(() => undefined);
263
+ }
264
+ function sanitizeChatGPTUrl(value) {
265
+ const url = new URL(value || CHATGPT_HOME, CHATGPT_HOME);
266
+ url.search = "";
267
+ url.hash = "";
268
+ if (/^\/c\/[^/]+/.test(url.pathname)) {
269
+ url.pathname = url.pathname.replace(/^\/c\/[^/]+/, "/c/sanitized");
270
+ }
271
+ return `${url.origin}${url.pathname}`;
272
+ }
273
+ function titleCase(value) {
274
+ return value.length === 0 ? value : `${value[0].toUpperCase()}${value.slice(1)}`;
275
+ }
276
+ function dedupeBounded(values, limit) {
277
+ return Array.from(new Set(values.map(value => value.trim()).filter(Boolean))).slice(0, limit);
278
+ }
279
+ function requiredValue(argv, index, flag) {
280
+ const value = argv[index];
281
+ if (value === undefined || value.startsWith("--")) {
282
+ throw new UsageError(`${flag} requires a value.`);
283
+ }
284
+ return value;
285
+ }
286
+ function parseSupportState(value) {
287
+ if (["current", "compatibility", "unverified", "retired"].includes(value)) {
288
+ return value;
289
+ }
290
+ throw new UsageError(`Invalid --support-state: ${value}`);
291
+ }
292
+ function parseIfMissing(value) {
293
+ if (value === "block" || value === "open" || value === "create")
294
+ return value;
295
+ throw new UsageError(`Invalid --if-missing: ${value}`);
296
+ }
297
+ function assertNormalizedSlug(value, flag) {
298
+ if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(value)) {
299
+ throw new UsageError(`${flag} must be a normalized lowercase slug without private names.`);
300
+ }
301
+ }
302
+ if (import.meta.url === `file://${process.argv[1]}`) {
303
+ process.exitCode = await main();
304
+ }
@@ -1,7 +1,7 @@
1
1
  import { mkdtemp, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { attachAskRead, attachFiles, askMessage, bootstrap, composeMessage, createChatGPT, copyResponse, downloadLatestAttachment, newThread, openThread, readLatest, runSequence, searchThreads, selectTool, setMode, submitMessage, twoTurnExchange, waitAndRead, waitForMessage } from "../../index.js";
4
+ import { attachAskRead, attachFiles, askMessage, bootstrap, composeMessage, createChatGPT, copyResponse, downloadLatestAttachment, messageStatus, newThread, openThread, readLatest, runSequence, searchThreads, selectTool, setMode, submitMessage, twoTurnExchange, waitAndRead, waitForMessage } from "../../index.js";
5
5
  import { readAssistantGenerationState } from "../../dom/generation-state.js";
6
6
  import { contextEnvFlag, contextEnvText } from "./harness.js";
7
7
  export const requiredScenarios = [
@@ -496,6 +496,39 @@ export const optionalScenarios = [
496
496
  ? pass(meta, waited, details)
497
497
  : fail(meta, waited, details);
498
498
  }),
499
+ scenario("running-status-detection", false, context => contextEnvFlag(context, "CHATGPT_E2E_RUNNING_STATUS"), async (context, meta) => {
500
+ const env = await bootNewThread(context, meta);
501
+ if ("status" in env)
502
+ return env;
503
+ let status;
504
+ try {
505
+ const asked = await askMessage(env, {
506
+ text: [
507
+ "Live running-status stress test. Write exactly 500 numbered items.",
508
+ "Each item should be a complete sentence. Continue until item 500."
509
+ ].join("\n"),
510
+ wait: false,
511
+ read: false
512
+ });
513
+ if (!asked.ok)
514
+ return fail(meta, asked);
515
+ await waitForGenerationSignal(env, 30000);
516
+ status = await messageStatus(env, { maxPreviewChars: 400 });
517
+ const data = status.data;
518
+ const details = {
519
+ completionState: data?.completionState,
520
+ generationActive: data?.generationActive,
521
+ latestAssistantTextLength: data?.latestAssistantTextLength,
522
+ status: status.status
523
+ };
524
+ return status.ok && (data?.completionState === "generating" || data?.generationActive === true)
525
+ ? pass(meta, status, details)
526
+ : fail(meta, status, details);
527
+ }
528
+ finally {
529
+ await stopGenerationIfVisible(env);
530
+ }
531
+ }),
499
532
  scenario("download-generated-file", false, context => contextEnvFlag(context, "CHATGPT_E2E_DOWNLOAD"), async (context, meta) => {
500
533
  const env = await bootNewThread(context, meta);
501
534
  if ("status" in env)
@@ -44,6 +44,7 @@ else {
44
44
  CHATGPT_E2E_DOWNLOAD: envText("CHATGPT_E2E_DOWNLOAD"),
45
45
  CHATGPT_E2E_LOGIN_PROFILE: envText("CHATGPT_E2E_LOGIN_PROFILE"),
46
46
  CHATGPT_E2E_MODE_LABEL: envText("CHATGPT_E2E_MODE_LABEL"),
47
+ CHATGPT_E2E_RUNNING_STATUS: envText("CHATGPT_E2E_RUNNING_STATUS"),
47
48
  CHATGPT_E2E_STREAM: envText("CHATGPT_E2E_STREAM"),
48
49
  CHATGPT_E2E_UPLOAD_PERMISSION_MANUAL: envText("CHATGPT_E2E_UPLOAD_PERMISSION_MANUAL"),
49
50
  CHATGPT_E2E_WEB_SEARCH: envText("CHATGPT_E2E_WEB_SEARCH")
@@ -1,4 +1,6 @@
1
1
  export type CommandStatus = "ok" | "partial" | "timeout" | "blocked" | "needs_confirmation" | "not_found" | "unsupported" | "error";
2
+ export type SubmissionState = "not_submitted" | "submitted" | "submitted_unconfirmed" | "submitted_generating";
3
+ export type CompletionState = "complete" | "generating" | "stopped" | "partial" | "unknown";
2
4
  export type BlockerKind = "browser_bridge_unavailable" | "login_required" | "captcha" | "rate_limit" | "modal" | "permission" | "confirmation" | "selector_drift" | "artifact_unavailable" | "artifact_selector_drift" | "artifact_download_unavailable" | "download_unavailable" | "upload_failed" | "not_found" | "unknown";
3
5
  export type CommandContext = {
4
6
  url?: string;
@@ -6,6 +8,8 @@ export type CommandContext = {
6
8
  title?: string;
7
9
  turnCount?: number;
8
10
  assistantTurnCount?: number;
11
+ experience?: ChatGPTExperience;
12
+ selectorProfile?: SurfaceSelectorProfile;
9
13
  browserName?: string;
10
14
  tabId?: string;
11
15
  timestamp: string;
@@ -174,6 +178,10 @@ export type SubmitData = {
174
178
  submitted: boolean;
175
179
  userTurnText?: string;
176
180
  turnCount?: number;
181
+ submissionState?: SubmissionState;
182
+ completionState?: CompletionState;
183
+ generationActive?: boolean;
184
+ generationSignals?: string[];
177
185
  };
178
186
  export type WaitArgs = {
179
187
  afterTurnCount?: number;
@@ -193,6 +201,9 @@ export type WaitData = {
193
201
  responseContent?: "include" | "metadata";
194
202
  assistantTurnCount: number;
195
203
  elapsedMs: number;
204
+ completionState?: CompletionState;
205
+ generationActive?: boolean;
206
+ generationSignals?: string[];
196
207
  };
197
208
  export type ResponseFormat = "markdown" | "text" | "normalized_text" | "visible_text" | "html" | "blocks" | "all";
198
209
  export type ResponseCitation = {
@@ -277,10 +288,14 @@ export type ReadLatestData = {
277
288
  actions?: ResponseAction[];
278
289
  thoughtDurationText?: string;
279
290
  sourcesAvailable?: boolean;
291
+ completionState?: CompletionState;
292
+ generationActive?: boolean;
293
+ generationSignals?: string[];
280
294
  };
281
295
  export type WaitAndReadArgs = WaitArgs & ReadLatestArgs;
282
296
  export type AskArgs = {
283
- text: string;
297
+ text?: string;
298
+ prompt?: string;
284
299
  wait?: boolean | WaitArgs;
285
300
  read?: boolean | ReadLatestArgs;
286
301
  timeoutMs?: number;
@@ -291,6 +306,24 @@ export type AskReadData = {
291
306
  complete?: boolean;
292
307
  conversationId?: string;
293
308
  title?: string;
309
+ submissionState?: SubmissionState;
310
+ completionState?: CompletionState;
311
+ generationActive?: boolean;
312
+ generationSignals?: string[];
313
+ };
314
+ export type MessageStatusArgs = {
315
+ maxPreviewChars?: number;
316
+ };
317
+ export type MessageStatusData = {
318
+ turnCount?: number;
319
+ assistantTurnCount: number;
320
+ latestAssistantTurnIndex?: number;
321
+ latestAssistantText?: string;
322
+ latestAssistantPreview?: string;
323
+ latestAssistantTextLength?: number;
324
+ completionState: CompletionState;
325
+ generationActive: boolean;
326
+ generationSignals: string[];
294
327
  };
295
328
  export type AttachFilesArgs = {
296
329
  paths: string[];
@@ -488,6 +521,172 @@ export type GetModeArgs = {
488
521
  export type GetModeData = {
489
522
  modes: string[];
490
523
  };
524
+ export type ChatGPTExperience = "chat" | "work" | "unknown";
525
+ export type SurfaceSelectorProfile = "chat_legacy_v1" | "chat_simplified_v1" | "work_basic_v1" | "work_advanced_v1" | "unknown";
526
+ export type SurfaceProfileSupportState = "current" | "compatibility" | "unverified" | "retired";
527
+ export type SurfaceProfileObservation = {
528
+ observedAt: string;
529
+ provenance: string;
530
+ locale: string;
531
+ region: string;
532
+ accountScope: string;
533
+ planScope: string;
534
+ workspaceScope: string;
535
+ supportState: SurfaceProfileSupportState;
536
+ };
537
+ export type ExperienceConfidence = "high" | "medium" | "low";
538
+ export type ExperienceEvidence = {
539
+ source: "url" | "composer" | "control" | "menu" | "heading";
540
+ label: string;
541
+ };
542
+ export type DetectExperienceArgs = {
543
+ timeoutMs?: number;
544
+ };
545
+ export type DetectExperienceData = {
546
+ experience: ChatGPTExperience;
547
+ selectorProfile: SurfaceSelectorProfile;
548
+ confidence: ExperienceConfidence;
549
+ evidence: ExperienceEvidence[];
550
+ };
551
+ export type OpenExperienceArgs = {
552
+ experience: Exclude<ChatGPTExperience, "unknown">;
553
+ timeoutMs?: number;
554
+ };
555
+ export type OpenExperienceData = {
556
+ experience: Exclude<ChatGPTExperience, "unknown">;
557
+ previousExperience: ChatGPTExperience;
558
+ changed: boolean;
559
+ selectorProfile: SurfaceSelectorProfile;
560
+ };
561
+ export type ConfigurationAxis = "model" | "intelligence" | "effort" | "speed" | "modelVersion";
562
+ export type SurfaceProfileFixture = SurfaceProfileObservation & {
563
+ schemaVersion: "chatgpt.browser_control.surface_profile.v1";
564
+ id: string;
565
+ snapshot: {
566
+ url: string;
567
+ composerLabels: string[];
568
+ mainControls: string[];
569
+ mainText: string;
570
+ };
571
+ panel: {
572
+ openerLabel?: string;
573
+ axisRows: Array<{
574
+ axis: ConfigurationAxis;
575
+ label: string;
576
+ value?: string;
577
+ }>;
578
+ advancedVisible: boolean;
579
+ };
580
+ menuItems: Array<{
581
+ label: string;
582
+ normalized: string;
583
+ role?: string;
584
+ checked?: boolean;
585
+ expanded?: boolean;
586
+ hasPopup?: boolean;
587
+ testId?: string;
588
+ ariaLabel?: string;
589
+ }>;
590
+ expected: {
591
+ experience: ChatGPTExperience;
592
+ selectorProfile: SurfaceSelectorProfile;
593
+ availableAxes: ConfigurationAxis[];
594
+ active: Partial<Record<ConfigurationAxis, string>>;
595
+ };
596
+ };
597
+ export type ConfigurationOption = {
598
+ id: string;
599
+ label: string;
600
+ selected: boolean;
601
+ disabled?: boolean;
602
+ description?: string;
603
+ hasSubmenu?: boolean;
604
+ };
605
+ export type ConfigurationSelection = {
606
+ model?: string;
607
+ intelligence?: string;
608
+ effort?: string;
609
+ speed?: string;
610
+ modelVersion?: string;
611
+ /** Backward-compatible alias for modelVersion. */
612
+ version?: string;
613
+ };
614
+ export type InspectConfigurationArgs = {
615
+ experience?: Exclude<ChatGPTExperience, "unknown">;
616
+ includeOptions?: boolean;
617
+ timeoutMs?: number;
618
+ };
619
+ export type ConfigurationInspectionData = {
620
+ experience: ChatGPTExperience;
621
+ selectorProfile: SurfaceSelectorProfile;
622
+ availableAxes: ConfigurationAxis[];
623
+ active: Partial<Record<ConfigurationAxis, string>>;
624
+ options: Partial<Record<ConfigurationAxis, ConfigurationOption[]>>;
625
+ verified: boolean;
626
+ evidence: ExperienceEvidence[];
627
+ };
628
+ export type ApplyConfigurationArgs = {
629
+ experience?: Exclude<ChatGPTExperience, "unknown">;
630
+ desired: ConfigurationSelection;
631
+ strict?: boolean;
632
+ timeoutMs?: number;
633
+ };
634
+ export type AppliedConfigurationSelection = {
635
+ axis: ConfigurationAxis;
636
+ requested: string;
637
+ selected: string;
638
+ };
639
+ export type ApplyConfigurationData = {
640
+ requested: ConfigurationSelection;
641
+ selected: AppliedConfigurationSelection[];
642
+ before: ConfigurationInspectionData;
643
+ after: ConfigurationInspectionData;
644
+ verified: boolean;
645
+ };
646
+ export type WorkTaskRef = {
647
+ url?: string;
648
+ conversationId?: string;
649
+ title?: string;
650
+ baselineTurnCount?: number;
651
+ baselineAssistantTurnCount?: number;
652
+ };
653
+ export type StartWorkArgs = {
654
+ prompt: string;
655
+ /** Start from a blank Work task when possible. Defaults to true. */
656
+ newTask?: boolean;
657
+ files?: string[];
658
+ configuration?: ConfigurationSelection;
659
+ wait?: boolean | WaitArgs;
660
+ read?: boolean | ReadLatestArgs;
661
+ timeoutMs?: number;
662
+ };
663
+ export type StartWorkData = {
664
+ task: WorkTaskRef;
665
+ submitted: SubmitData;
666
+ configuration?: ApplyConfigurationData;
667
+ wait?: WaitData;
668
+ response?: ReadLatestData;
669
+ };
670
+ export type WorkStatusArgs = MessageStatusArgs & {
671
+ includeArtifacts?: boolean;
672
+ };
673
+ export type WorkStatusData = {
674
+ experience: "work";
675
+ task: WorkTaskRef;
676
+ message: MessageStatusData;
677
+ artifacts?: ArtifactListData;
678
+ };
679
+ export type WorkWaitArgs = WaitArgs;
680
+ export type WorkWaitData = WaitData;
681
+ export type SteerWorkArgs = {
682
+ prompt: string;
683
+ wait?: boolean | WaitArgs;
684
+ read?: boolean | ReadLatestArgs;
685
+ timeoutMs?: number;
686
+ };
687
+ export type SteerWorkData = AskReadData;
688
+ export type ReadWorkLatestArgs = ReadLatestArgs;
689
+ export type ReadWorkLatestData = ReadLatestData;
491
690
  export type SelectToolArgs = {
492
691
  tool: "web_search" | "deep_research" | "create_image" | string;
493
692
  timeoutMs?: number;
@@ -496,6 +695,42 @@ export type SequenceStep = {
496
695
  id: string;
497
696
  command: "session.bootstrap";
498
697
  args?: BootstrapArgs;
698
+ } | {
699
+ id: string;
700
+ command: "experience.detect";
701
+ args?: DetectExperienceArgs;
702
+ } | {
703
+ id: string;
704
+ command: "experience.open";
705
+ args: OpenExperienceArgs;
706
+ } | {
707
+ id: string;
708
+ command: "configuration.inspect";
709
+ args?: InspectConfigurationArgs;
710
+ } | {
711
+ id: string;
712
+ command: "configuration.apply";
713
+ args: ApplyConfigurationArgs;
714
+ } | {
715
+ id: string;
716
+ command: "work.start";
717
+ args: StartWorkArgs;
718
+ } | {
719
+ id: string;
720
+ command: "work.status";
721
+ args?: WorkStatusArgs;
722
+ } | {
723
+ id: string;
724
+ command: "work.wait";
725
+ args?: WorkWaitArgs;
726
+ } | {
727
+ id: string;
728
+ command: "work.steer";
729
+ args: SteerWorkArgs;
730
+ } | {
731
+ id: string;
732
+ command: "work.readLatest";
733
+ args?: ReadWorkLatestArgs;
499
734
  } | {
500
735
  id: string;
501
736
  command: "threads.search";
@@ -528,6 +763,10 @@ export type SequenceStep = {
528
763
  id: string;
529
764
  command: "messages.readLatest";
530
765
  args?: ReadLatestArgs;
766
+ } | {
767
+ id: string;
768
+ command: "messages.status";
769
+ args?: MessageStatusArgs;
531
770
  } | {
532
771
  id: string;
533
772
  command: "messages.waitAndRead";
@@ -599,6 +838,7 @@ export type RuntimeEnv = {
599
838
  page?: PageLike;
600
839
  clipboard?: ClipboardLike;
601
840
  now?: () => Date;
841
+ expectedTabId?: string;
602
842
  };
603
843
  export type ClipboardLike = {
604
844
  read: () => Promise<string>;
@@ -656,6 +896,8 @@ export type WaitForEventOptions = {
656
896
  timeoutMs?: number;
657
897
  };
658
898
  export type PageLike = {
899
+ id?: string;
900
+ tabId?: string;
659
901
  url?: () => string | Promise<string>;
660
902
  goto?: (url: string, options?: unknown) => Promise<unknown>;
661
903
  title?: () => Promise<string>;
@@ -694,7 +936,8 @@ export type PageLike = {
694
936
  [key: string]: unknown;
695
937
  };
696
938
  };
697
- export type AskHelperArgs = AskArgs & {
939
+ export type AskHelperArgs = Omit<AskArgs, "text" | "prompt"> & {
940
+ text: string;
698
941
  thread?: ThreadTarget;
699
942
  };
700
943
  export type AskInThreadArgs = AskHelperArgs & {