codex-chatgpt-control 0.1.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 (179) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +54 -0
  4. package/contracts/v1/fixtures/backend-capabilities.json +54 -0
  5. package/contracts/v1/fixtures/backend-error-event-missing-stream-input.json +10 -0
  6. package/contracts/v1/fixtures/backend-error-missing-run-input.json +10 -0
  7. package/contracts/v1/fixtures/backend-runner-plan-request.json +12 -0
  8. package/contracts/v1/fixtures/backend-version.json +10 -0
  9. package/contracts/v1/fixtures/command-descriptors.json +803 -0
  10. package/contracts/v1/fixtures/describe-runner-run.json +31 -0
  11. package/contracts/v1/fixtures/doctor-bridge-upload.json +34 -0
  12. package/contracts/v1/fixtures/help-root.json +6 -0
  13. package/contracts/v1/fixtures/named-plan-two-turn.json +39 -0
  14. package/contracts/v1/fixtures/output-json-parse-success.json +115 -0
  15. package/contracts/v1/fixtures/primitive-bootstrap-blocker.json +43 -0
  16. package/contracts/v1/fixtures/report-redaction-default.json +16 -0
  17. package/contracts/v1/fixtures/reports-create-redacted.json +16 -0
  18. package/contracts/v1/fixtures/reports-summarize-redacted.json +26 -0
  19. package/contracts/v1/fixtures/responses-basic-success.json +27 -0
  20. package/contracts/v1/fixtures/responses-hidden-instructions-unsupported.json +22 -0
  21. package/contracts/v1/fixtures/responses-unknown-field-unsupported.json +22 -0
  22. package/contracts/v1/fixtures/responses-unsupported-previous-response-id.json +22 -0
  23. package/contracts/v1/fixtures/responses-unsupported-temperature.json +22 -0
  24. package/contracts/v1/fixtures/run-basic-success.json +36 -0
  25. package/contracts/v1/fixtures/run-browser-bridge-blocker.json +180 -0
  26. package/contracts/v1/fixtures/run-file-attach-success.json +63 -0
  27. package/contracts/v1/fixtures/run-report-redacted.json +52 -0
  28. package/contracts/v1/fixtures/run-selector-drift-blocker.json +60 -0
  29. package/contracts/v1/fixtures/run-timeout-partial.json +81 -0
  30. package/contracts/v1/fixtures/run-two-turn-success.json +83 -0
  31. package/contracts/v1/fixtures/run-upload-permission-blocker.json +56 -0
  32. package/contracts/v1/fixtures/runner-budget-blocker.json +106 -0
  33. package/contracts/v1/fixtures/runner-full-agent-config.json +50 -0
  34. package/contracts/v1/fixtures/runner-input-items-and-files-plan.json +53 -0
  35. package/contracts/v1/fixtures/runner-metadata-only-plan.json +31 -0
  36. package/contracts/v1/fixtures/runner-visible-prefix-plan.json +28 -0
  37. package/contracts/v1/fixtures/runner-visible-setup-plan.json +37 -0
  38. package/contracts/v1/fixtures/stream-basic.ndjson +3 -0
  39. package/contracts/v1/fixtures/stream-blocked.ndjson +2 -0
  40. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +3 -0
  41. package/contracts/v1/fixtures/workflow-ask-success.json +67 -0
  42. package/contracts/v1/manifest.json +210 -0
  43. package/contracts/v1/parity-suite.json +635 -0
  44. package/contracts/v1/schemas/agent.schema.json +52 -0
  45. package/contracts/v1/schemas/backend-event.schema.json +74 -0
  46. package/contracts/v1/schemas/backend-request.schema.json +59 -0
  47. package/contracts/v1/schemas/backend-response.schema.json +42 -0
  48. package/contracts/v1/schemas/capabilities.schema.json +33 -0
  49. package/contracts/v1/schemas/command-descriptor.schema.json +31 -0
  50. package/contracts/v1/schemas/command-result.schema.json +99 -0
  51. package/contracts/v1/schemas/manifest.schema.json +69 -0
  52. package/contracts/v1/schemas/response.schema.json +51 -0
  53. package/contracts/v1/schemas/run-result.schema.json +138 -0
  54. package/contracts/v1/schemas/sequence-plan.schema.json +44 -0
  55. package/contracts/v1/schemas/stream-event.schema.json +25 -0
  56. package/dist/codex-chatgpt-control-backend.mjs +5284 -0
  57. package/dist/codex-chatgpt-control.bundle.mjs +6013 -0
  58. package/dist/src/backend/client.d.ts +129 -0
  59. package/dist/src/backend/client.js +415 -0
  60. package/dist/src/backend/protocol.d.ts +74 -0
  61. package/dist/src/backend/protocol.js +128 -0
  62. package/dist/src/backend/session.d.ts +11 -0
  63. package/dist/src/backend/session.js +223 -0
  64. package/dist/src/backend/stdio-server.d.ts +9 -0
  65. package/dist/src/backend/stdio-server.js +88 -0
  66. package/dist/src/browser/attach.d.ts +8 -0
  67. package/dist/src/browser/attach.js +456 -0
  68. package/dist/src/browser/clipboard.d.ts +2 -0
  69. package/dist/src/browser/clipboard.js +26 -0
  70. package/dist/src/browser/downloads.d.ts +7 -0
  71. package/dist/src/browser/downloads.js +29 -0
  72. package/dist/src/browser/page-state.d.ts +17 -0
  73. package/dist/src/browser/page-state.js +66 -0
  74. package/dist/src/client.d.ts +167 -0
  75. package/dist/src/client.js +666 -0
  76. package/dist/src/commands/confirmations.d.ts +9 -0
  77. package/dist/src/commands/confirmations.js +33 -0
  78. package/dist/src/commands/context.d.ts +2 -0
  79. package/dist/src/commands/context.js +32 -0
  80. package/dist/src/commands/doctor.d.ts +16 -0
  81. package/dist/src/commands/doctor.js +120 -0
  82. package/dist/src/commands/files.d.ts +4 -0
  83. package/dist/src/commands/files.js +314 -0
  84. package/dist/src/commands/helpers.d.ts +16 -0
  85. package/dist/src/commands/helpers.js +128 -0
  86. package/dist/src/commands/messages.d.ts +16 -0
  87. package/dist/src/commands/messages.js +538 -0
  88. package/dist/src/commands/modes.d.ts +9 -0
  89. package/dist/src/commands/modes.js +323 -0
  90. package/dist/src/commands/output.d.ts +5 -0
  91. package/dist/src/commands/output.js +31 -0
  92. package/dist/src/commands/registry.d.ts +17 -0
  93. package/dist/src/commands/registry.js +275 -0
  94. package/dist/src/commands/reports.d.ts +14 -0
  95. package/dist/src/commands/reports.js +47 -0
  96. package/dist/src/commands/response-actions.d.ts +2 -0
  97. package/dist/src/commands/response-actions.js +141 -0
  98. package/dist/src/commands/sequence.d.ts +9 -0
  99. package/dist/src/commands/sequence.js +228 -0
  100. package/dist/src/commands/session.d.ts +2 -0
  101. package/dist/src/commands/session.js +25 -0
  102. package/dist/src/commands/threads.d.ts +6 -0
  103. package/dist/src/commands/threads.js +300 -0
  104. package/dist/src/dom/fixtures.d.ts +1 -0
  105. package/dist/src/dom/fixtures.js +8 -0
  106. package/dist/src/dom/menus.d.ts +8 -0
  107. package/dist/src/dom/menus.js +40 -0
  108. package/dist/src/dom/message-format.d.ts +30 -0
  109. package/dist/src/dom/message-format.js +593 -0
  110. package/dist/src/dom/messages.d.ts +40 -0
  111. package/dist/src/dom/messages.js +127 -0
  112. package/dist/src/dom/selectors.d.ts +20 -0
  113. package/dist/src/dom/selectors.js +72 -0
  114. package/dist/src/dom/visible-text.d.ts +5 -0
  115. package/dist/src/dom/visible-text.js +27 -0
  116. package/dist/src/errors.d.ts +32 -0
  117. package/dist/src/errors.js +127 -0
  118. package/dist/src/index.d.ts +34 -0
  119. package/dist/src/index.js +34 -0
  120. package/dist/src/logger.d.ts +15 -0
  121. package/dist/src/logger.js +25 -0
  122. package/dist/src/runner/agent.d.ts +2 -0
  123. package/dist/src/runner/agent.js +17 -0
  124. package/dist/src/runner/index.d.ts +7 -0
  125. package/dist/src/runner/index.js +7 -0
  126. package/dist/src/runner/interruptions.d.ts +3 -0
  127. package/dist/src/runner/interruptions.js +65 -0
  128. package/dist/src/runner/responses.d.ts +30 -0
  129. package/dist/src/runner/responses.js +171 -0
  130. package/dist/src/runner/result.d.ts +3 -0
  131. package/dist/src/runner/result.js +128 -0
  132. package/dist/src/runner/resume.d.ts +11 -0
  133. package/dist/src/runner/resume.js +26 -0
  134. package/dist/src/runner/stream.d.ts +13 -0
  135. package/dist/src/runner/stream.js +66 -0
  136. package/dist/src/runner/types.d.ts +264 -0
  137. package/dist/src/runner/types.js +1 -0
  138. package/dist/src/safety/blockers.d.ts +7 -0
  139. package/dist/src/safety/blockers.js +60 -0
  140. package/dist/src/safety/redaction.d.ts +2 -0
  141. package/dist/src/safety/redaction.js +18 -0
  142. package/dist/src/safety/report-redaction.d.ts +8 -0
  143. package/dist/src/safety/report-redaction.js +74 -0
  144. package/dist/src/safety/risk.d.ts +26 -0
  145. package/dist/src/safety/risk.js +28 -0
  146. package/dist/src/scripts/backend-server.d.ts +2 -0
  147. package/dist/src/scripts/backend-server.js +7 -0
  148. package/dist/src/scripts/check-parity-suite.d.ts +1 -0
  149. package/dist/src/scripts/check-parity-suite.js +10 -0
  150. package/dist/src/scripts/continue-thread.d.ts +27 -0
  151. package/dist/src/scripts/continue-thread.js +388 -0
  152. package/dist/src/scripts/live-smoke/harness.d.ts +11 -0
  153. package/dist/src/scripts/live-smoke/harness.js +151 -0
  154. package/dist/src/scripts/live-smoke/scenarios.d.ts +3 -0
  155. package/dist/src/scripts/live-smoke/scenarios.js +648 -0
  156. package/dist/src/scripts/live-smoke/types.d.ts +53 -0
  157. package/dist/src/scripts/live-smoke/types.js +1 -0
  158. package/dist/src/scripts/live-smoke-module.d.ts +3 -0
  159. package/dist/src/scripts/live-smoke-module.js +2 -0
  160. package/dist/src/scripts/live-smoke.d.ts +1 -0
  161. package/dist/src/scripts/live-smoke.js +54 -0
  162. package/dist/src/scripts/smoke-attach-file.d.ts +1 -0
  163. package/dist/src/scripts/smoke-attach-file.js +39 -0
  164. package/dist/src/scripts/smoke-hi.d.ts +1 -0
  165. package/dist/src/scripts/smoke-hi.js +25 -0
  166. package/dist/src/scripts/smoke-search-open-read.d.ts +1 -0
  167. package/dist/src/scripts/smoke-search-open-read.js +41 -0
  168. package/dist/src/testing/parity-suite.d.ts +12 -0
  169. package/dist/src/testing/parity-suite.js +213 -0
  170. package/dist/src/types.d.ts +495 -0
  171. package/dist/src/types.js +1 -0
  172. package/package.json +81 -0
  173. package/references/agents-runner.md +32 -0
  174. package/references/backend-protocol.md +238 -0
  175. package/references/python-parity.md +168 -0
  176. package/references/responses-adapter.md +28 -0
  177. package/references/safety.md +14 -0
  178. package/references/streaming.md +15 -0
  179. package/references/troubleshooting.md +91 -0
@@ -0,0 +1,323 @@
1
+ import { resultError, resultOk } from "../errors.js";
2
+ import { enumerateVisibleMenuItems, findUniqueMenuItem } from "../dom/menus.js";
3
+ import { normalizeLabel, normalizeWhitespace } from "../dom/visible-text.js";
4
+ import { contextFromPage } from "./context.js";
5
+ import { bootstrap } from "./session.js";
6
+ const DEFAULT_MODE_EFFORT = "Thinking";
7
+ const CURRENT_MODE_LABELS = ["Latest", "Instant", "Thinking", "Extended", "Pro"];
8
+ const MODE_OPENER_LABELS = [...CURRENT_MODE_LABELS.filter(label => label !== "Pro"), "Configure"];
9
+ export async function setMode(env, args) {
10
+ const boot = await ensurePage(env);
11
+ if (!boot.ok) {
12
+ return boot;
13
+ }
14
+ const page = env.page;
15
+ try {
16
+ const requested = requestedModeLabels(args);
17
+ const opened = await waitForModeMenu(page, requested, args.timeoutMs ?? 30000);
18
+ if (opened.alreadySelected.length === requested.length) {
19
+ return resultOk({ selected: opened.alreadySelected, candidates: opened.modeButtons }, await contextFromPage(page));
20
+ }
21
+ if (!opened.opened) {
22
+ return selectorDrift(page, "No unique ChatGPT mode menu opener was found.");
23
+ }
24
+ await page.waitForTimeout?.(250);
25
+ const candidates = await enumerateVisibleMenuItems(page);
26
+ const selected = [];
27
+ for (const item of requested) {
28
+ const match = findUniqueMenuItem(candidates, item);
29
+ if (match === undefined) {
30
+ const candidateLabels = candidates.map(candidate => candidate.label);
31
+ return {
32
+ ok: false,
33
+ status: "unsupported",
34
+ warnings: [],
35
+ blocker: selectorDriftBlocker(`Mode option "${item}" was not found or was ambiguous.`, candidateLabels),
36
+ context: await contextFromPage(page)
37
+ };
38
+ }
39
+ if (!await clickMenuItem(page, match.label)) {
40
+ return selectorDrift(page, `Mode option "${match.label}" was visible but could not be clicked.`, candidates.map(candidate => candidate.label));
41
+ }
42
+ selected.push(match.label);
43
+ }
44
+ return resultOk({ selected, candidates: candidates.map(candidate => candidate.label) }, await contextFromPage(page));
45
+ }
46
+ catch (error) {
47
+ return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
48
+ }
49
+ }
50
+ async function waitForModeMenu(page, requested, timeoutMs) {
51
+ const deadline = Date.now() + timeoutMs;
52
+ let modeButtons = [];
53
+ do {
54
+ modeButtons = await visibleModeButtonLabelList(page);
55
+ const alreadySelected = findAlreadySelectedModes(modeButtons, requested);
56
+ if (alreadySelected.length === requested.length) {
57
+ return { opened: false, alreadySelected, modeButtons };
58
+ }
59
+ const openMenuItems = await enumerateVisibleMenuItems(page);
60
+ if (looksLikeModeMenu(openMenuItems.map(item => item.label))) {
61
+ return { opened: true, alreadySelected: [], modeButtons };
62
+ }
63
+ if (await clickModeOpener(page, modeButtons)) {
64
+ return { opened: true, alreadySelected: [], modeButtons };
65
+ }
66
+ if (Date.now() >= deadline) {
67
+ break;
68
+ }
69
+ await page.waitForTimeout?.(250);
70
+ } while (true);
71
+ return { opened: false, alreadySelected: [], modeButtons };
72
+ }
73
+ export async function selectTool(env, args) {
74
+ const boot = await ensurePage(env);
75
+ if (!boot.ok) {
76
+ return boot;
77
+ }
78
+ const page = env.page;
79
+ try {
80
+ const opened = await clickFirstUniqueButton(page, ["Add files and more", "Add files", "Add photos"]);
81
+ if (!opened) {
82
+ return selectorDrift(page, "No unique ChatGPT tool menu opener was found.");
83
+ }
84
+ await page.waitForTimeout?.(250);
85
+ const candidates = await enumerateVisibleMenuItems(page);
86
+ const wanted = toolLabel(args.tool);
87
+ const match = findUniqueMenuItem(candidates, wanted);
88
+ if (match === undefined) {
89
+ const candidateLabels = candidates.map(candidate => candidate.label);
90
+ return {
91
+ ok: false,
92
+ status: "unsupported",
93
+ warnings: [],
94
+ blocker: selectorDriftBlocker(`Tool "${wanted}" was not found or was ambiguous.`, candidateLabels),
95
+ context: await contextFromPage(page)
96
+ };
97
+ }
98
+ if (!await clickMenuItem(page, match.label)) {
99
+ return selectorDrift(page, `Tool "${match.label}" was visible but could not be clicked.`, candidates.map(candidate => candidate.label));
100
+ }
101
+ return resultOk({ selected: match.label, candidates: candidates.map(candidate => candidate.label) }, await contextFromPage(page));
102
+ }
103
+ catch (error) {
104
+ return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
105
+ }
106
+ }
107
+ async function ensurePage(env) {
108
+ if (env.page !== undefined) {
109
+ return resultOk({}, await contextFromPage(env.page));
110
+ }
111
+ return bootstrap(env, { preferExistingTab: true });
112
+ }
113
+ async function clickFirstUniqueButton(page, labels) {
114
+ for (const label of labels) {
115
+ const roleLocator = page.getByRole?.("button", { name: label, exact: true });
116
+ if (await clickIfUnique(roleLocator)) {
117
+ return true;
118
+ }
119
+ const textLocator = page.locator?.("button, [role='button']")?.filter?.({ hasText: label });
120
+ if (await clickIfUnique(textLocator)) {
121
+ return true;
122
+ }
123
+ }
124
+ return false;
125
+ }
126
+ async function clickModeOpener(page, modeButtons) {
127
+ if (await clickFirstUniqueButton(page, modeButtons)) {
128
+ return true;
129
+ }
130
+ return clickFirstUniqueButton(page, MODE_OPENER_LABELS);
131
+ }
132
+ function looksLikeModeMenu(labels) {
133
+ return labels.some(label => {
134
+ const normalized = normalizeLabel(label);
135
+ return CURRENT_MODE_LABELS.some(modeLabel => visibleLabelMatches(normalized, normalizeLabel(modeLabel)));
136
+ });
137
+ }
138
+ async function clickMenuItem(page, label) {
139
+ if (await clickModelSwitcherMenuItem(page, label)) {
140
+ return true;
141
+ }
142
+ if (await clickMenuItemByDom(page, label)) {
143
+ return true;
144
+ }
145
+ const roleLocator = page.locator?.("[role='menuitem'], [role='menuitemradio'], [role='option']")?.filter?.({ hasText: label });
146
+ if (await clickIfUnique(roleLocator)) {
147
+ return true;
148
+ }
149
+ const textLocator = page.getByText?.(label, { exact: true });
150
+ return clickIfUnique(textLocator);
151
+ }
152
+ async function clickModelSwitcherMenuItem(page, label) {
153
+ if (typeof page.evaluate !== "function" || typeof page.locator !== "function") {
154
+ return false;
155
+ }
156
+ const testId = await page.evaluate((wanted) => {
157
+ const normalizedWanted = wanted.replace(/\s+/g, " ").trim().toLowerCase();
158
+ const candidates = Array.from(document.querySelectorAll("[data-testid^='model-switcher-']"));
159
+ const matches = candidates
160
+ .filter(node => {
161
+ const element = node;
162
+ const candidateTestId = element.getAttribute("data-testid") ?? "";
163
+ if (candidateTestId.endsWith("-effort"))
164
+ return false;
165
+ const text = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim().toLowerCase();
166
+ return text === normalizedWanted;
167
+ })
168
+ .map(node => node.getAttribute("data-testid"))
169
+ .filter((value) => value !== null);
170
+ return matches.length === 1 ? matches[0] : undefined;
171
+ }, label).catch(() => undefined);
172
+ if (testId === undefined) {
173
+ return false;
174
+ }
175
+ return clickIfUnique(page.locator(`[data-testid="${escapeAttributeValue(testId)}"]`));
176
+ }
177
+ async function clickMenuItemByDom(page, label) {
178
+ if (typeof page.evaluate !== "function") {
179
+ return false;
180
+ }
181
+ return page.evaluate((wanted) => {
182
+ const normalizedWanted = wanted.replace(/\s+/g, " ").trim().toLowerCase();
183
+ const candidates = Array.from(document.querySelectorAll("[role='menuitem'], [role='menuitemradio'], [role='option']"));
184
+ const matches = candidates.filter(node => {
185
+ const element = node;
186
+ const text = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim().toLowerCase();
187
+ return text === normalizedWanted;
188
+ });
189
+ if (matches.length !== 1)
190
+ return false;
191
+ matches[0].click();
192
+ return true;
193
+ }, label).catch(() => false);
194
+ }
195
+ async function clickIfUnique(locator) {
196
+ if (locator === undefined || typeof locator.count !== "function" || typeof locator.click !== "function") {
197
+ return false;
198
+ }
199
+ const count = await locator.count().catch(() => 0);
200
+ if (count !== 1) {
201
+ return false;
202
+ }
203
+ await locator.click();
204
+ return true;
205
+ }
206
+ function toolLabel(tool) {
207
+ switch (tool) {
208
+ case "web_search":
209
+ return "Web search";
210
+ case "deep_research":
211
+ return "Deep research";
212
+ case "create_image":
213
+ return "Create image";
214
+ default:
215
+ return tool;
216
+ }
217
+ }
218
+ function requestedModeLabels(args) {
219
+ const requested = [args.model, args.effort].filter((value) => value !== undefined);
220
+ return requested.length > 0 ? requested : [DEFAULT_MODE_EFFORT];
221
+ }
222
+ function findUniqueVisibleLabel(labels, wanted) {
223
+ const normalized = normalizeLabel(wanted);
224
+ const exact = labels.filter(label => normalizeLabel(label) === normalized);
225
+ if (exact.length === 1) {
226
+ return exact[0];
227
+ }
228
+ const fuzzy = labels.filter(label => visibleLabelMatches(normalizeLabel(label), normalized));
229
+ return fuzzy.length === 1 ? fuzzy[0] : undefined;
230
+ }
231
+ function visibleLabelMatches(label, wanted) {
232
+ if (wanted.length <= 3) {
233
+ return new RegExp(`(^|[^a-z0-9])${escapeRegExp(wanted)}([^a-z0-9]|$)`, "i").test(label);
234
+ }
235
+ return label.includes(wanted);
236
+ }
237
+ function escapeRegExp(value) {
238
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
239
+ }
240
+ function escapeAttributeValue(value) {
241
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
242
+ }
243
+ function findAlreadySelectedModes(visibleButtons, requested) {
244
+ return requested
245
+ .map(label => findUniqueVisibleLabel(visibleButtons, label))
246
+ .filter((label) => label !== undefined);
247
+ }
248
+ async function selectorDrift(page, message, candidates) {
249
+ const visibleText = candidates?.join("\n") ?? await visibleButtonLabels(page);
250
+ return {
251
+ ok: false,
252
+ status: "unsupported",
253
+ warnings: [],
254
+ blocker: selectorDriftBlocker(message, candidates, visibleText),
255
+ context: await contextFromPage(page)
256
+ };
257
+ }
258
+ function selectorDriftBlocker(message, candidates, visibleText = candidates?.join("\n") ?? "") {
259
+ const candidateLabels = candidates ?? visibleText.split("\n").map(label => label.trim()).filter(Boolean).slice(0, 30);
260
+ const blocker = {
261
+ kind: "selector_drift",
262
+ code: "visible_candidate_not_found",
263
+ message,
264
+ visibleText,
265
+ resumable: false
266
+ };
267
+ if (candidateLabels.length > 0) {
268
+ blocker.candidates = candidateLabels.map(label => ({ label }));
269
+ }
270
+ return blocker;
271
+ }
272
+ async function visibleButtonLabels(page) {
273
+ return (await visibleButtonLabelList(page)).join("\n");
274
+ }
275
+ async function visibleButtonLabelList(page) {
276
+ if (typeof page.evaluate !== "function") {
277
+ return [];
278
+ }
279
+ return page.evaluate(() => {
280
+ return Array.from(document.querySelectorAll("button, [role='button']"))
281
+ .map(node => {
282
+ const element = node;
283
+ return element.getAttribute("aria-label") ?? element.innerText ?? element.textContent ?? "";
284
+ })
285
+ .map(text => text.trim())
286
+ .filter(Boolean)
287
+ .slice(0, 30);
288
+ }).then(labels => labels.map(normalizeWhitespace)).catch(() => []);
289
+ }
290
+ async function visibleModeButtonLabelList(page) {
291
+ if (typeof page.evaluate !== "function") {
292
+ return [];
293
+ }
294
+ return page.evaluate((modeLabels) => {
295
+ const normalizedModeLabels = modeLabels.map(label => label.toLowerCase());
296
+ const tokenMatches = (text, token) => {
297
+ if (token.length <= 3) {
298
+ return new RegExp(`(^|[^a-z0-9])${token}([^a-z0-9]|$)`, "i").test(text);
299
+ }
300
+ return text.includes(token);
301
+ };
302
+ return Array.from(document.querySelectorAll("button, [role='button']"))
303
+ .map(node => {
304
+ const element = node;
305
+ const visibleText = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim();
306
+ const ariaLabel = (element.getAttribute("aria-label") ?? "").replace(/\s+/g, " ").trim();
307
+ const label = visibleText.length > 0 ? visibleText : ariaLabel;
308
+ const testId = element.getAttribute("data-testid") ?? "";
309
+ if (testId === "accounts-profile-button")
310
+ return "";
311
+ if (/open profile menu/i.test(label))
312
+ return "";
313
+ if (visibleText.length === 0 && /feedback|conversation options|dismiss/i.test(ariaLabel))
314
+ return "";
315
+ const normalized = label.toLowerCase();
316
+ if (!normalizedModeLabels.some(modeLabel => tokenMatches(normalized, modeLabel)))
317
+ return "";
318
+ return label;
319
+ })
320
+ .filter(Boolean)
321
+ .slice(0, 30);
322
+ }, CURRENT_MODE_LABELS).then(labels => labels.map(normalizeWhitespace)).catch(() => []);
323
+ }
@@ -0,0 +1,5 @@
1
+ export declare function commandOutputText(data: unknown): string | undefined;
2
+ export declare function withCommandOutputText<T extends {
3
+ data?: unknown;
4
+ output_text?: string;
5
+ }>(result: T): T;
@@ -0,0 +1,31 @@
1
+ export function commandOutputText(data) {
2
+ if (!isRecord(data))
3
+ return undefined;
4
+ const responseText = data.responseText;
5
+ if (typeof responseText === "string")
6
+ return responseText;
7
+ const role = data.role;
8
+ const text = data.text;
9
+ if (typeof text === "string" && role !== "user")
10
+ return text;
11
+ const markdown = data.markdown;
12
+ if (typeof markdown === "string")
13
+ return markdown;
14
+ for (const [key, value] of Object.entries(data)) {
15
+ if (key === "prompt" || key === "input")
16
+ continue;
17
+ const nested = commandOutputText(value);
18
+ if (nested !== undefined)
19
+ return nested;
20
+ }
21
+ return undefined;
22
+ }
23
+ export function withCommandOutputText(result) {
24
+ if (result.output_text !== undefined)
25
+ return result;
26
+ const outputText = commandOutputText(result.data);
27
+ return outputText === undefined ? result : { ...result, output_text: outputText };
28
+ }
29
+ function isRecord(value) {
30
+ return typeof value === "object" && value !== null && !Array.isArray(value);
31
+ }
@@ -0,0 +1,17 @@
1
+ import { type RiskLevel } from "../safety/risk.js";
2
+ export type CommandLayer = "workflow" | "primitive" | "diagnostic" | "report";
3
+ export type CommandDescriptor = {
4
+ name: string;
5
+ layer: CommandLayer;
6
+ summary: string;
7
+ risk: RiskLevel;
8
+ defaultTimeoutMs?: number;
9
+ args: Record<string, string>;
10
+ defaults: Record<string, unknown>;
11
+ retryPolicy: string;
12
+ blockers: string[];
13
+ examples: string[];
14
+ };
15
+ export declare function commandDescriptors(): CommandDescriptor[];
16
+ export declare function describeCommand(name: string): CommandDescriptor | undefined;
17
+ export declare function helpText(topic?: string): string;
@@ -0,0 +1,275 @@
1
+ import { riskForCommand } from "../safety/risk.js";
2
+ const descriptors = [
3
+ workflow("ask", "Ask ChatGPT in a new or selected thread, optionally with files, wait/read, downloads, and reports.", [
4
+ `await chatgpt.ask({ prompt: "reply with the word hi", wait: true, read: true });`
5
+ ]),
6
+ workflow("askInThread", "Open or claim an existing thread by URL, conversation id, title, or search query, then ask and read.", [
7
+ `await chatgpt.askInThread({ thread: { type: "search", query: "Naming macOS Utility" }, prompt: "Continue." });`,
8
+ `await chatgpt.askInThread({ thread: { type: "url", url: "https://chatgpt.com/c/<conversation-id>" }, existingTab: true, prompt: "Continue." });`
9
+ ]),
10
+ workflow("askWithFiles", "Attach absolute local file paths, optionally set mode, ask, wait, and read.", [
11
+ `await chatgpt.askWithFiles({ thread: { type: "url", url: "https://chatgpt.com/c/<conversation-id>" }, existingTab: true, mode: { effort: "Thinking" }, files: ["/absolute/path/brief.md"], prompt: "Summarize this.", wait: true, read: { format: "markdown" } });`
12
+ ]),
13
+ workflow("askAndDownload", "Ask ChatGPT to produce a visible downloadable output and save the latest exposed file.", [
14
+ `await chatgpt.askAndDownload({ prompt: "Create a CSV.", download: { destDir: "/tmp/out" }, wait: true });`
15
+ ]),
16
+ workflow("runMessages", "Run sequential prompts where later prompts can use earlier step data.", [
17
+ `await chatgpt.runMessages({ messages: [{ id: "first", prompt: "alpha" }, { id: "second", prompt: "beta" }] });`
18
+ ]),
19
+ workflow("runner.run", "Agents-style facade: run a visible ChatGPT browser-control agent against input, files, thread, existing-tab, mode, and response options.", [
20
+ `const agent = chatgpt.agent({ name: "reviewer", instructions: "Review deeply." }); await chatgpt.runner.run(agent, { input: "Review this.", thread: { type: "new" } });`,
21
+ `await chatgpt.runner.run(agent, { input: "Continue.", thread: { type: "url", url: "https://chatgpt.com/c/<conversation-id>" }, existingTab: true });`
22
+ ]),
23
+ workflow("responses.create", "Narrow Responses-shaped adapter over the visible ChatGPT browser-control runner; rejects unsupported API-only fields before prompt submission.", [
24
+ `await chatgpt.responses.create({ input: "Summarize.", thread: { type: "current" }, text: { format: "markdown" }, stream: false });`
25
+ ]),
26
+ workflow("copyLatest", "Copy or DOM-read the latest assistant response with Markdown-first fidelity.", [
27
+ `await chatgpt.copyLatest({ prefer: "clipboard" });`
28
+ ]),
29
+ workflow("runPlan", "Execute an inline SequencePlan or named macro through the existing sequence engine.", [
30
+ `await chatgpt.runPlan({ name: "new-ask-read", input: { prompt: "hi" } });`
31
+ ]),
32
+ workflow("new-ask-read", "Named macro: open a new thread, ask, wait, and read Markdown.", [
33
+ `await chatgpt.runPlan({ name: "new-ask-read", input: { prompt: "hi" } });`
34
+ ]),
35
+ workflow("find-open-ask-read", "Named macro: search history, open the first match, ask, wait, and read Markdown.", [
36
+ `await chatgpt.runPlan({ name: "find-open-ask-read", input: { query: "SDK Design Proposal", prompt: "Continue." } });`
37
+ ]),
38
+ workflow("find-open-copy-latest", "Named macro: search history, open the first match, and copy/read the latest response.", [
39
+ `await chatgpt.runPlan({ name: "find-open-copy-latest", input: { query: "SDK Design Proposal" } });`
40
+ ]),
41
+ workflow("attach-ask-read", "Named macro: open a new thread, attach files, ask, wait, and read Markdown.", [
42
+ `await chatgpt.runPlan({ name: "attach-ask-read", input: { files: ["/absolute/path.md"], prompt: "Summarize." } });`
43
+ ]),
44
+ workflow("ask-and-download", "Named macro: ask in a new thread and download the latest file affordance.", [
45
+ `await chatgpt.runPlan({ name: "ask-and-download", input: { prompt: "Create a CSV.", destDir: "/tmp/out" } });`
46
+ ]),
47
+ workflow("two-turn", "Named macro: run two sequential prompts in a new thread.", [
48
+ `await chatgpt.runPlan({ name: "two-turn", input: { first: "alpha", second: "beta" } });`
49
+ ]),
50
+ diagnostic("doctor-upload", "Named macro: preflight bridge, login, and upload permission remediation.", [
51
+ `await chatgpt.runPlan({ name: "doctor-upload" });`
52
+ ]),
53
+ report("redacted-run-report", "Named macro: create a redacted report for a supplied CommandResult.", [
54
+ `await chatgpt.runPlan({ name: "redacted-run-report", input: { result } });`
55
+ ]),
56
+ diagnostic("doctor", "Preflight browser bridge, login, upload, download, clipboard, mode, and tool readiness.", [
57
+ `await chatgpt.doctor({ check: ["bridge", "login", "upload"] });`
58
+ ]),
59
+ report("createReport", "Write a durable redacted run report for a command result.", [
60
+ `await chatgpt.createReport(result, { destDir: "/tmp/reports" });`
61
+ ]),
62
+ primitive("session.bootstrap", "Attach to ChatGPT in Chrome and detect login/blocker state.", 30000),
63
+ primitive("threads.new", "Open a new ChatGPT thread.", 30000),
64
+ primitive("threads.search", "Search visible ChatGPT history by query.", 30000),
65
+ primitive("threads.open", "Open a thread by URL, conversation id, title, or search result.", 30000),
66
+ primitive("messages.compose", "Fill the composer without submitting.", 30000),
67
+ primitive("messages.submit", "Submit the current composer contents.", 30000),
68
+ primitive("messages.ask", "Compose, submit, optionally wait, and optionally read.", 120000),
69
+ primitive("messages.wait", "Wait for the latest assistant response to stabilize.", 120000),
70
+ primitive("messages.readLatest", "Read the latest message as Markdown, normalized text, blocks, or HTML.", 30000),
71
+ primitive("messages.waitAndRead", "Wait for completion and read the latest message.", 120000),
72
+ primitive("files.attach", "Attach absolute local file paths through visible ChatGPT upload controls.", 180000),
73
+ primitive("files.downloadLatest", "Download the latest visible ChatGPT file affordance.", 120000),
74
+ primitive("response.copy", "Click Copy response and return clipboard Markdown, with DOM fallback.", 5000),
75
+ primitive("modes.set", "Select a visible model or effort candidate when unambiguous.", 30000),
76
+ primitive("tools.select", "Select a visible ChatGPT tool when unambiguous.", 30000)
77
+ ];
78
+ export function commandDescriptors() {
79
+ return descriptors.map(cloneDescriptor);
80
+ }
81
+ export function describeCommand(name) {
82
+ const descriptor = descriptors.find(item => item.name === name);
83
+ if (descriptor === undefined)
84
+ return undefined;
85
+ return cloneDescriptor(descriptor);
86
+ }
87
+ export function helpText(topic) {
88
+ if (topic !== undefined) {
89
+ const descriptor = describeCommand(topic);
90
+ if (descriptor === undefined)
91
+ return `No ChatGPT browser-control command is registered as "${topic}".`;
92
+ return [
93
+ `${descriptor.name} (${descriptor.layer}, ${descriptor.risk} risk)`,
94
+ descriptor.summary,
95
+ descriptor.defaultTimeoutMs === undefined ? undefined : `Default timeout: ${descriptor.defaultTimeoutMs} ms`,
96
+ Object.keys(descriptor.args).length === 0 ? undefined : `Args: ${Object.entries(descriptor.args).map(([name, description]) => `${name} (${description})`).join(", ")}`,
97
+ Object.keys(descriptor.defaults).length === 0 ? undefined : `Defaults: ${JSON.stringify(descriptor.defaults)}`,
98
+ `Retry policy: ${descriptor.retryPolicy}`,
99
+ descriptor.blockers.length === 0 ? undefined : `Blockers: ${descriptor.blockers.join(", ")}`,
100
+ descriptor.examples.length === 0 ? undefined : `Example: ${descriptor.examples[0]}`
101
+ ].filter((line) => line !== undefined).join("\n");
102
+ }
103
+ const grouped = groupByLayer(descriptors);
104
+ return [
105
+ "ChatGPT browser-control SDK commands",
106
+ "",
107
+ ...["workflow", "diagnostic", "report", "primitive"]
108
+ .flatMap(layer => [
109
+ `${layer}:`,
110
+ ...(grouped[layer] ?? []).map(descriptor => `- ${descriptor.name}: ${descriptor.summary}`)
111
+ ])
112
+ ].join("\n");
113
+ }
114
+ function workflow(name, summary, examples) {
115
+ return {
116
+ name,
117
+ layer: "workflow",
118
+ summary,
119
+ risk: "medium",
120
+ defaultTimeoutMs: 120000,
121
+ args: workflowArgs(name),
122
+ defaults: workflowDefaults(name),
123
+ retryPolicy: "Return structured CommandResult failures; do not resubmit prompts unless the sequence policy permits unmatched-turn recovery.",
124
+ blockers: commonBlockers(),
125
+ examples
126
+ };
127
+ }
128
+ function diagnostic(name, summary, examples) {
129
+ return {
130
+ name,
131
+ layer: "diagnostic",
132
+ summary,
133
+ risk: "low",
134
+ defaultTimeoutMs: 30000,
135
+ args: diagnosticArgs(name),
136
+ defaults: {},
137
+ retryPolicy: "Return structured readiness checks; retry only after the reported blocker or permission setting changes.",
138
+ blockers: ["browser_bridge_unavailable", "login_required", "selector_drift"],
139
+ examples
140
+ };
141
+ }
142
+ function report(name, summary, examples) {
143
+ return {
144
+ name,
145
+ layer: "report",
146
+ summary,
147
+ risk: "low",
148
+ defaultTimeoutMs: 5000,
149
+ args: reportArgs(name),
150
+ defaults: { includeContent: false, maxPreviewChars: 240 },
151
+ retryPolicy: "Do not retry blindly; preserve redaction defaults and report filesystem errors as CommandResult failures.",
152
+ blockers: ["permission"],
153
+ examples
154
+ };
155
+ }
156
+ function primitive(name, summary, defaultTimeoutMs) {
157
+ return {
158
+ name,
159
+ layer: "primitive",
160
+ summary,
161
+ risk: riskForCommand(name),
162
+ defaultTimeoutMs,
163
+ args: primitiveArgs(name),
164
+ defaults: {},
165
+ retryPolicy: "Return structured CommandResult failures; retry only when the blocker is recoverable and no duplicate prompt will be submitted.",
166
+ blockers: primitiveBlockers(name),
167
+ examples: primitiveExamples(name)
168
+ };
169
+ }
170
+ function workflowArgs(name) {
171
+ if (name === "find-open-copy-latest")
172
+ return { query: "history search query" };
173
+ if (name === "find-open-ask-read")
174
+ return { query: "history search query", prompt: "message to send" };
175
+ if (name === "attach-ask-read")
176
+ return { files: "absolute local file paths", prompt: "message to send" };
177
+ if (name === "ask-and-download")
178
+ return { prompt: "message to send", destDir: "download destination directory" };
179
+ if (name === "two-turn")
180
+ return { first: "first message", second: "second message" };
181
+ if (name === "new-ask-read")
182
+ return { prompt: "message to send" };
183
+ if (name === "askWithFiles") {
184
+ return {
185
+ files: "absolute local file paths to attach before submitting",
186
+ prompt: "message to send after files are attached",
187
+ thread: "optional thread selector",
188
+ existingTab: "true or explicit policy to claim a user-open Chrome tab instead of opening a replacement",
189
+ mode: "optional visible mode selection, e.g. { effort: \"Thinking\" }",
190
+ wait: "true or wait options; defaults to true",
191
+ read: "true or read options such as { format: \"markdown\" }; defaults to Markdown",
192
+ report: "optional redacted report settings"
193
+ };
194
+ }
195
+ return {
196
+ prompt: "message to send or workflow-specific input",
197
+ thread: "optional thread selector",
198
+ existingTab: "true or explicit policy to claim a user-open Chrome tab instead of opening a replacement",
199
+ report: "optional redacted report settings"
200
+ };
201
+ }
202
+ function workflowDefaults(name) {
203
+ if (name === "copyLatest" || name === "find-open-copy-latest")
204
+ return { prefer: "clipboard", format: "markdown" };
205
+ if (name === "runPlan")
206
+ return {};
207
+ return { wait: true, read: { format: "markdown" } };
208
+ }
209
+ function diagnosticArgs(name) {
210
+ if (name === "doctor-upload")
211
+ return {};
212
+ return { check: "optional list of readiness checks" };
213
+ }
214
+ function reportArgs(name) {
215
+ if (name === "redacted-run-report")
216
+ return { result: "CommandResult to persist" };
217
+ return { result: "CommandResult to persist", destDir: "optional report directory" };
218
+ }
219
+ function primitiveArgs(name) {
220
+ if (name === "messages.readLatest")
221
+ return { role: "assistant or user", format: "markdown, normalized_text, visible_text, html, blocks, or all" };
222
+ if (name === "response.copy")
223
+ return { prefer: "clipboard or dom", format: "markdown, normalized_text, visible_text, html, blocks, or all" };
224
+ if (name.startsWith("threads.search"))
225
+ return { query: "history search query" };
226
+ if (name.startsWith("files.attach"))
227
+ return { paths: "absolute local file paths" };
228
+ if (name === "modes.set") {
229
+ return {
230
+ effort: "visible effort label such as Thinking or Extended",
231
+ model: "visible model label such as Instant, Pro, or another available model",
232
+ timeoutMs: "optional timeout for opening and selecting the visible mode menu"
233
+ };
234
+ }
235
+ return {};
236
+ }
237
+ function primitiveExamples(name) {
238
+ if (name === "modes.set") {
239
+ return [
240
+ `await chatgpt.modes.set({ effort: "Thinking" });`,
241
+ `await chatgpt.askWithFiles({ mode: { effort: "Thinking" }, files: ["/absolute/path.jpg"], prompt: "Describe this image.", wait: true });`
242
+ ];
243
+ }
244
+ if (name === "files.attach") {
245
+ return [`await chatgpt.files.attach({ paths: ["/absolute/path.jpg"] });`];
246
+ }
247
+ return [];
248
+ }
249
+ function primitiveBlockers(name) {
250
+ if (name.startsWith("files.attach"))
251
+ return ["browser_bridge_unavailable", "login_required", "permission", "upload_failed"];
252
+ if (name.startsWith("files.download"))
253
+ return ["browser_bridge_unavailable", "login_required", "download_unavailable"];
254
+ if (name.startsWith("modes.") || name.startsWith("tools."))
255
+ return ["browser_bridge_unavailable", "login_required", "selector_drift"];
256
+ return commonBlockers();
257
+ }
258
+ function commonBlockers() {
259
+ return ["browser_bridge_unavailable", "login_required", "captcha", "rate_limit", "selector_drift"];
260
+ }
261
+ function groupByLayer(items) {
262
+ return items.reduce((grouped, item) => {
263
+ grouped[item.layer].push(item);
264
+ return grouped;
265
+ }, { workflow: [], primitive: [], diagnostic: [], report: [] });
266
+ }
267
+ function cloneDescriptor(descriptor) {
268
+ return {
269
+ ...descriptor,
270
+ args: { ...descriptor.args },
271
+ defaults: { ...descriptor.defaults },
272
+ blockers: [...descriptor.blockers],
273
+ examples: [...descriptor.examples]
274
+ };
275
+ }
@@ -0,0 +1,14 @@
1
+ import type { CommandResult, RuntimeEnv } from "../types.js";
2
+ export type RunReportOptions = {
3
+ enabled?: boolean;
4
+ destDir?: string;
5
+ basename?: string;
6
+ includeContent?: boolean;
7
+ maxPreviewChars?: number;
8
+ };
9
+ export type RunReportData = {
10
+ path: string;
11
+ bytes: number;
12
+ includeContent: boolean;
13
+ };
14
+ export declare function createRunReport(env: RuntimeEnv, result: CommandResult<unknown>, options?: RunReportOptions): Promise<CommandResult<RunReportData>>;