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,300 @@
1
+ import { parseConversationId, readPageState } from "../browser/page-state.js";
2
+ import { resultError, resultOk } from "../errors.js";
3
+ import { countPageMessages, readLatestMessageText } from "../dom/messages.js";
4
+ import { requiredLocator, searchChatsButton, searchChatsInput, newChatButton } from "../dom/selectors.js";
5
+ import { normalizeWhitespace, stripTags } from "../dom/visible-text.js";
6
+ import { contextFromPage } from "./context.js";
7
+ import { bootstrap } from "./session.js";
8
+ const CHATGPT_HOME = "https://chatgpt.com/";
9
+ export function extractThreadSearchResultsFromHtml(html) {
10
+ const anchors = html.matchAll(/<a\b(?<attrs>[^>]*\bhref=["'](?<href>\/c\/[^"']+)["'][^>]*)>(?<body>[\s\S]*?)<\/a>/gi);
11
+ const results = [];
12
+ for (const anchor of anchors) {
13
+ const href = anchor.groups?.href;
14
+ const body = anchor.groups?.body ?? "";
15
+ if (href === undefined) {
16
+ continue;
17
+ }
18
+ const lines = extractBlockTexts(body);
19
+ const fallback = normalizeWhitespace(stripTags(body));
20
+ const title = lines[0] ?? fallback;
21
+ if (title.length === 0) {
22
+ continue;
23
+ }
24
+ const result = { title, href };
25
+ const conversationId = parseConversationId(href);
26
+ if (conversationId !== undefined) {
27
+ result.conversationId = conversationId;
28
+ }
29
+ const snippet = lines.slice(1).join(" ");
30
+ if (snippet.length > 0) {
31
+ result.snippet = snippet;
32
+ }
33
+ results.push(result);
34
+ }
35
+ return dedupeResults(results);
36
+ }
37
+ export async function searchThreads(env, args) {
38
+ const boot = await ensurePage(env);
39
+ if (!boot.ok) {
40
+ return boot;
41
+ }
42
+ const page = env.page;
43
+ try {
44
+ const warnings = [];
45
+ try {
46
+ await openSearchUI(page);
47
+ await fillSearchQuery(page, args.query);
48
+ await page.waitForTimeout?.(350);
49
+ }
50
+ catch (error) {
51
+ warnings.push(`Search modal was not usable; fell back to visible sidebar links. ${error instanceof Error ? error.message : String(error)}`);
52
+ }
53
+ const results = filterResultsByQuery(await extractThreadSearchResultsFromPage(page), args.query);
54
+ const limited = results.slice(0, args.limit ?? results.length);
55
+ return resultOk({ query: args.query, results: limited }, await contextFromPage(page), warnings);
56
+ }
57
+ catch (error) {
58
+ return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
59
+ }
60
+ }
61
+ export async function newThread(env, args = {}) {
62
+ const boot = await ensurePage(env);
63
+ if (!boot.ok) {
64
+ return boot;
65
+ }
66
+ const page = env.page;
67
+ try {
68
+ try {
69
+ await newChatButton(page).click?.();
70
+ }
71
+ catch {
72
+ await page.goto?.(CHATGPT_HOME, { waitUntil: "domcontentloaded", timeout: args.timeoutMs ?? 30000 });
73
+ }
74
+ await page.waitForTimeout?.(500);
75
+ const state = await readPageState(page);
76
+ return resultOk(openThreadData(state.url, state.conversationId, state.title), await contextFromPage(page));
77
+ }
78
+ catch (error) {
79
+ return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
80
+ }
81
+ }
82
+ export async function openThread(env, args, previousResults) {
83
+ const boot = await ensurePage(env);
84
+ if (!boot.ok) {
85
+ return boot;
86
+ }
87
+ const page = env.page;
88
+ try {
89
+ const target = await resolveOpenTarget(env, args, previousResults);
90
+ if (target === undefined) {
91
+ return {
92
+ ok: false,
93
+ status: "not_found",
94
+ warnings: [],
95
+ blocker: {
96
+ kind: "not_found",
97
+ message: "No thread target could be resolved from the provided arguments."
98
+ },
99
+ context: await contextFromPage(page)
100
+ };
101
+ }
102
+ if (target.href !== undefined && target.href.startsWith("/")) {
103
+ await page.goto?.(new URL(target.href, CHATGPT_HOME).toString(), { waitUntil: "domcontentloaded", timeout: args.timeoutMs ?? 30000 });
104
+ }
105
+ else {
106
+ await page.goto?.(target.href ?? target.url, { waitUntil: "domcontentloaded", timeout: args.timeoutMs ?? 30000 });
107
+ }
108
+ await waitForThreadHydrated(page, args.timeoutMs ?? 30000, parseConversationId(target.url));
109
+ const state = await readPageState(page);
110
+ return resultOk(openThreadData(state.url, state.conversationId, state.title ?? target.title), await contextFromPage(page));
111
+ }
112
+ catch (error) {
113
+ return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
114
+ }
115
+ }
116
+ async function ensurePage(env) {
117
+ if (env.page !== undefined) {
118
+ return resultOk({}, await contextFromPage(env.page));
119
+ }
120
+ return bootstrap(env, { preferExistingTab: true });
121
+ }
122
+ async function resolveOpenTarget(env, args, previousResults) {
123
+ if (args.url !== undefined) {
124
+ return { url: args.url };
125
+ }
126
+ if (args.conversationId !== undefined) {
127
+ return { url: new URL(`/c/${args.conversationId}`, CHATGPT_HOME).toString() };
128
+ }
129
+ if (args.fromStep !== undefined && previousResults !== undefined) {
130
+ const previous = previousResults.get(args.fromStep);
131
+ const data = previous?.data;
132
+ const selected = selectSearchResult(data?.results ?? [], args.select ?? "first");
133
+ if (selected !== undefined) {
134
+ return { href: selected.href, url: new URL(selected.href, CHATGPT_HOME).toString(), title: selected.title };
135
+ }
136
+ }
137
+ if (args.title !== undefined) {
138
+ const search = await searchThreads(env, { query: args.title, limit: 10 });
139
+ const selected = selectSearchResult(search.data?.results ?? [], { title: args.title }) ?? search.data?.results[0];
140
+ if (selected !== undefined) {
141
+ return { href: selected.href, url: new URL(selected.href, CHATGPT_HOME).toString(), title: selected.title };
142
+ }
143
+ }
144
+ return undefined;
145
+ }
146
+ export function selectSearchResult(results, select = "first") {
147
+ if (select === "first") {
148
+ return results[0];
149
+ }
150
+ if (select !== undefined && "index" in select) {
151
+ return results[select.index];
152
+ }
153
+ if (select !== undefined && "title" in select) {
154
+ const wanted = normalizeForMatch(select.title);
155
+ return results.find(result => normalizeForMatch(result.title) === wanted)
156
+ ?? results.find(result => normalizeForMatch(result.title).includes(wanted));
157
+ }
158
+ return undefined;
159
+ }
160
+ async function extractThreadSearchResultsFromPage(page) {
161
+ if (page === undefined) {
162
+ return [];
163
+ }
164
+ if (typeof page.evaluate === "function") {
165
+ const raw = await page.evaluate(() => {
166
+ return Array.from(document.querySelectorAll("a[href^='/c/']"))
167
+ .map(anchor => ({
168
+ href: anchor.getAttribute("href") ?? "",
169
+ text: anchor.innerText ?? anchor.textContent ?? ""
170
+ }))
171
+ .filter(item => item.href.length > 0 && item.text.trim().length > 0);
172
+ });
173
+ return dedupeResults(raw.map(item => {
174
+ const lines = item.text.split(/\n+/).map(line => normalizeWhitespace(line)).filter(Boolean);
175
+ const result = {
176
+ title: lines[0] ?? normalizeWhitespace(item.text),
177
+ href: item.href
178
+ };
179
+ const conversationId = parseConversationId(item.href);
180
+ if (conversationId !== undefined) {
181
+ result.conversationId = conversationId;
182
+ }
183
+ const snippet = lines.slice(1).join(" ");
184
+ if (snippet.length > 0) {
185
+ result.snippet = snippet;
186
+ }
187
+ return result;
188
+ }));
189
+ }
190
+ if (typeof page.content === "function") {
191
+ return extractThreadSearchResultsFromHtml(await page.content());
192
+ }
193
+ return [];
194
+ }
195
+ function dedupeResults(results) {
196
+ const seen = new Set();
197
+ const deduped = [];
198
+ for (const result of results) {
199
+ const key = result.conversationId ?? result.href;
200
+ if (seen.has(key)) {
201
+ continue;
202
+ }
203
+ seen.add(key);
204
+ deduped.push(result);
205
+ }
206
+ return deduped;
207
+ }
208
+ function normalizeForMatch(text) {
209
+ return normalizeWhitespace(text).toLowerCase();
210
+ }
211
+ async function openSearchUI(page) {
212
+ try {
213
+ await searchChatsButton(page).click?.();
214
+ await page.waitForTimeout?.(250);
215
+ return;
216
+ }
217
+ catch {
218
+ // Fall through to DOM click.
219
+ }
220
+ if (typeof page.evaluate === "function") {
221
+ try {
222
+ await page.evaluate(() => {
223
+ const button = Array.from(document.querySelectorAll("button"))
224
+ .find(candidate => /Search chats/i.test(candidate.innerText ?? candidate.textContent ?? ""));
225
+ button?.click();
226
+ });
227
+ await page.waitForTimeout?.(250);
228
+ return;
229
+ }
230
+ catch {
231
+ // Fall through to keyboard shortcut.
232
+ }
233
+ }
234
+ await page.keyboard?.press?.("Meta+K");
235
+ await page.waitForTimeout?.(250);
236
+ }
237
+ async function fillSearchQuery(page, query) {
238
+ const attempts = [
239
+ async () => searchChatsInput(page).fill?.(query),
240
+ async () => page.getByRole?.("textbox", { name: "Search chats" }).fill?.(query),
241
+ async () => page.getByRole?.("textbox", { name: /Search chats/i }).fill?.(query),
242
+ async () => requiredLocator(page, "input[placeholder*='Search'], [role='dialog'] input").fill?.(query)
243
+ ];
244
+ let lastError;
245
+ for (const attempt of attempts) {
246
+ try {
247
+ await attempt();
248
+ return;
249
+ }
250
+ catch (error) {
251
+ lastError = error;
252
+ await page.keyboard?.press?.("Meta+K");
253
+ await page.waitForTimeout?.(250);
254
+ }
255
+ }
256
+ throw lastError instanceof Error ? lastError : new Error("Unable to fill ChatGPT search input.");
257
+ }
258
+ function openThreadData(url, conversationId, title) {
259
+ const data = { url };
260
+ if (conversationId !== undefined) {
261
+ data.conversationId = conversationId;
262
+ }
263
+ if (title !== undefined) {
264
+ data.title = title;
265
+ }
266
+ return data;
267
+ }
268
+ function extractBlockTexts(html) {
269
+ const chunks = Array.from(html.matchAll(/<(?:div|span|p|h[1-6])\b[^>]*>([\s\S]*?)<\/(?:div|span|p|h[1-6])>/gi))
270
+ .map(match => stripTags(match[1] ?? ""))
271
+ .filter(Boolean);
272
+ if (chunks.length > 0) {
273
+ return chunks;
274
+ }
275
+ const fallback = stripTags(html);
276
+ return fallback.length > 0 ? [fallback] : [];
277
+ }
278
+ function filterResultsByQuery(results, query) {
279
+ const wanted = normalizeForMatch(query);
280
+ return results.filter(result => {
281
+ const haystack = normalizeForMatch(`${result.title} ${result.snippet ?? ""}`);
282
+ return haystack.includes(wanted) || wanted.includes(normalizeForMatch(result.title));
283
+ });
284
+ }
285
+ async function waitForThreadHydrated(page, timeoutMs, expectedConversationId) {
286
+ const started = Date.now();
287
+ await page.waitForTimeout?.(1000);
288
+ while (Date.now() - started < timeoutMs) {
289
+ const url = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
290
+ const urlMatches = expectedConversationId === undefined || url.includes(expectedConversationId);
291
+ const count = await countPageMessages(page).catch(() => 0);
292
+ const latestAssistantText = await readLatestMessageText(page, "assistant").catch(() => undefined);
293
+ const title = typeof page.title === "function" ? await page.title().catch(() => "") : "";
294
+ if (urlMatches && ((latestAssistantText?.trim().length ?? 0) > 0 || (count > 0 && title.length > 0 && title !== "ChatGPT"))) {
295
+ await page.waitForTimeout?.(250);
296
+ return;
297
+ }
298
+ await page.waitForTimeout?.(500);
299
+ }
300
+ }
@@ -0,0 +1 @@
1
+ export declare function readFixture(name: string): Promise<string>;
@@ -0,0 +1,8 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ export async function readFixture(name) {
4
+ if (name.includes("..") || name.startsWith("/")) {
5
+ throw new Error(`Unsafe fixture name: ${name}`);
6
+ }
7
+ return readFile(resolve("tests/fixtures", name), "utf8");
8
+ }
@@ -0,0 +1,8 @@
1
+ import type { PageLike } from "../types.js";
2
+ export type MenuItem = {
3
+ label: string;
4
+ normalized: string;
5
+ };
6
+ export declare function extractMenuItemsFromText(text: string): MenuItem[];
7
+ export declare function enumerateVisibleMenuItems(page: PageLike): Promise<MenuItem[]>;
8
+ export declare function findUniqueMenuItem(items: MenuItem[], wanted: string): MenuItem | undefined;
@@ -0,0 +1,40 @@
1
+ import { normalizeLabel, normalizeWhitespace } from "./visible-text.js";
2
+ export function extractMenuItemsFromText(text) {
3
+ return text
4
+ .split(/\n| {2,}| • /)
5
+ .map(label => normalizeWhitespace(label))
6
+ .filter(Boolean)
7
+ .map(label => ({ label, normalized: normalizeLabel(label) }));
8
+ }
9
+ export async function enumerateVisibleMenuItems(page) {
10
+ if (typeof page.evaluate === "function") {
11
+ const labels = await page.evaluate(() => {
12
+ const roleItems = Array.from(document.querySelectorAll("[role='menuitem'], [role='menuitemradio'], [role='option']"))
13
+ .map(node => node.innerText ?? node.textContent ?? "")
14
+ .filter(Boolean);
15
+ if (roleItems.length > 0) {
16
+ return { labels: roleItems, split: false };
17
+ }
18
+ const menus = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper]"))
19
+ .map(node => node.innerText ?? node.textContent ?? "")
20
+ .filter(Boolean);
21
+ return { labels: menus, split: true };
22
+ });
23
+ return labels.split
24
+ ? labels.labels.flatMap(label => extractMenuItemsFromText(label))
25
+ : labels.labels
26
+ .map(label => normalizeWhitespace(label))
27
+ .filter(Boolean)
28
+ .map(label => ({ label, normalized: normalizeLabel(label) }));
29
+ }
30
+ return [];
31
+ }
32
+ export function findUniqueMenuItem(items, wanted) {
33
+ const normalized = normalizeLabel(wanted);
34
+ const exact = items.filter(item => item.normalized === normalized);
35
+ if (exact.length === 1) {
36
+ return exact[0];
37
+ }
38
+ const fuzzy = items.filter(item => item.normalized.includes(normalized));
39
+ return fuzzy.length === 1 ? fuzzy[0] : undefined;
40
+ }
@@ -0,0 +1,30 @@
1
+ import type { ResponseAction, ResponseBlock, ResponseBranchState, ResponseCaptureFidelity, ResponseCaptureSource, ResponseCitation, ResponseCodeBlock, ResponseFormat, ResponseTable } from "../types.js";
2
+ export type NormalizedResponseFormat = Exclude<ResponseFormat, "text">;
3
+ export type FormattedMessageContent = {
4
+ text: string;
5
+ format: NormalizedResponseFormat;
6
+ source?: ResponseCaptureSource;
7
+ fidelity?: ResponseCaptureFidelity;
8
+ warnings?: string[];
9
+ markdown?: string;
10
+ visibleText?: string;
11
+ normalizedText?: string;
12
+ html?: string;
13
+ blocks?: ResponseBlock[];
14
+ citations?: ResponseCitation[];
15
+ codeBlocks?: ResponseCodeBlock[];
16
+ tables?: ResponseTable[];
17
+ branch?: ResponseBranchState;
18
+ actions?: ResponseAction[];
19
+ thoughtDurationText?: string;
20
+ sourcesAvailable?: boolean;
21
+ };
22
+ export type RoleMessageHtml = {
23
+ role: "user" | "assistant";
24
+ html: string;
25
+ metadataHtml?: string;
26
+ };
27
+ export declare function normalizeResponseFormat(format: ResponseFormat | undefined): NormalizedResponseFormat;
28
+ export declare function extractRoleMessageHtml(html: string): RoleMessageHtml[];
29
+ export declare function formatMessageHtml(html: string, requestedFormat?: ResponseFormat | undefined, maxChars?: number, metadataHtml?: string): FormattedMessageContent;
30
+ export declare function formatClipboardMarkdown(text: string, maxChars?: number, requestedFormat?: ResponseFormat | undefined): FormattedMessageContent;