codex-chatgpt-control 0.2.0-alpha.1 → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/contracts/v1/fixtures/backend-capabilities.json +1 -0
  3. package/contracts/v1/fixtures/command-descriptors.json +33 -2
  4. package/contracts/v1/fixtures/doctor-scenario-preflight.json +12 -1
  5. package/contracts/v1/fixtures/help-root.json +1 -1
  6. package/contracts/v1/fixtures/reports-create-redacted.json +1 -1
  7. package/contracts/v1/parity-suite.json +8 -1
  8. package/dist/codex-chatgpt-control-backend.mjs +1199 -159
  9. package/dist/codex-chatgpt-control.bundle.mjs +1198 -156
  10. package/dist/src/backend/protocol.d.ts +1 -1
  11. package/dist/src/backend/protocol.js +1 -0
  12. package/dist/src/backend/session.js +2 -0
  13. package/dist/src/browser/clipboard.d.ts +11 -0
  14. package/dist/src/browser/clipboard.js +34 -7
  15. package/dist/src/browser/page-state.js +12 -2
  16. package/dist/src/client.d.ts +2 -1
  17. package/dist/src/client.js +3 -2
  18. package/dist/src/commands/context.js +3 -1
  19. package/dist/src/commands/conversation.d.ts +15 -0
  20. package/dist/src/commands/conversation.js +44 -0
  21. package/dist/src/commands/deadline.d.ts +8 -0
  22. package/dist/src/commands/deadline.js +14 -0
  23. package/dist/src/commands/doctor.js +9 -2
  24. package/dist/src/commands/files.js +62 -8
  25. package/dist/src/commands/messages.js +212 -72
  26. package/dist/src/commands/modes.d.ts +4 -1
  27. package/dist/src/commands/modes.js +215 -30
  28. package/dist/src/commands/probes.d.ts +15 -0
  29. package/dist/src/commands/probes.js +64 -0
  30. package/dist/src/commands/registry.js +34 -2
  31. package/dist/src/commands/reports.js +1 -1
  32. package/dist/src/commands/threads.js +2 -24
  33. package/dist/src/dom/generation-state.d.ts +7 -0
  34. package/dist/src/dom/generation-state.js +7 -1
  35. package/dist/src/dom/label-match.d.ts +3 -0
  36. package/dist/src/dom/label-match.js +25 -0
  37. package/dist/src/dom/locale/am.d.ts +6 -0
  38. package/dist/src/dom/locale/am.js +6 -0
  39. package/dist/src/dom/locale/ar.d.ts +7 -0
  40. package/dist/src/dom/locale/ar.js +7 -0
  41. package/dist/src/dom/locale/bg.d.ts +7 -0
  42. package/dist/src/dom/locale/bg.js +7 -0
  43. package/dist/src/dom/locale/bn.d.ts +7 -0
  44. package/dist/src/dom/locale/bn.js +7 -0
  45. package/dist/src/dom/locale/bs.d.ts +6 -0
  46. package/dist/src/dom/locale/bs.js +6 -0
  47. package/dist/src/dom/locale/ca.d.ts +6 -0
  48. package/dist/src/dom/locale/ca.js +6 -0
  49. package/dist/src/dom/locale/cs.d.ts +6 -0
  50. package/dist/src/dom/locale/cs.js +6 -0
  51. package/dist/src/dom/locale/da.d.ts +5 -0
  52. package/dist/src/dom/locale/da.js +5 -0
  53. package/dist/src/dom/locale/de.d.ts +6 -0
  54. package/dist/src/dom/locale/de.js +6 -0
  55. package/dist/src/dom/locale/el.d.ts +6 -0
  56. package/dist/src/dom/locale/el.js +6 -0
  57. package/dist/src/dom/locale/en.d.ts +14 -0
  58. package/dist/src/dom/locale/en.js +15 -0
  59. package/dist/src/dom/locale/es-419.d.ts +6 -0
  60. package/dist/src/dom/locale/es-419.js +6 -0
  61. package/dist/src/dom/locale/es-ES.d.ts +6 -0
  62. package/dist/src/dom/locale/es-ES.js +6 -0
  63. package/dist/src/dom/locale/et.d.ts +6 -0
  64. package/dist/src/dom/locale/et.js +6 -0
  65. package/dist/src/dom/locale/fa.d.ts +7 -0
  66. package/dist/src/dom/locale/fa.js +7 -0
  67. package/dist/src/dom/locale/fi.d.ts +6 -0
  68. package/dist/src/dom/locale/fi.js +6 -0
  69. package/dist/src/dom/locale/fr-CA.d.ts +6 -0
  70. package/dist/src/dom/locale/fr-CA.js +6 -0
  71. package/dist/src/dom/locale/fr-FR.d.ts +5 -0
  72. package/dist/src/dom/locale/fr-FR.js +5 -0
  73. package/dist/src/dom/locale/gu.d.ts +6 -0
  74. package/dist/src/dom/locale/gu.js +6 -0
  75. package/dist/src/dom/locale/hi.d.ts +6 -0
  76. package/dist/src/dom/locale/hi.js +6 -0
  77. package/dist/src/dom/locale/hr.d.ts +5 -0
  78. package/dist/src/dom/locale/hr.js +5 -0
  79. package/dist/src/dom/locale/hu.d.ts +6 -0
  80. package/dist/src/dom/locale/hu.js +6 -0
  81. package/dist/src/dom/locale/hy.d.ts +7 -0
  82. package/dist/src/dom/locale/hy.js +7 -0
  83. package/dist/src/dom/locale/id.d.ts +6 -0
  84. package/dist/src/dom/locale/id.js +6 -0
  85. package/dist/src/dom/locale/index.d.ts +5 -1
  86. package/dist/src/dom/locale/index.js +37 -0
  87. package/dist/src/dom/locale/is.d.ts +6 -0
  88. package/dist/src/dom/locale/is.js +6 -0
  89. package/dist/src/dom/locale/it.d.ts +6 -0
  90. package/dist/src/dom/locale/it.js +6 -0
  91. package/dist/src/dom/locale/ja.d.ts +6 -0
  92. package/dist/src/dom/locale/ja.js +6 -0
  93. package/dist/src/dom/locale/ka.d.ts +6 -0
  94. package/dist/src/dom/locale/ka.js +6 -0
  95. package/dist/src/dom/locale/kk.d.ts +6 -0
  96. package/dist/src/dom/locale/kk.js +6 -0
  97. package/dist/src/dom/locale/kn.d.ts +7 -0
  98. package/dist/src/dom/locale/kn.js +7 -0
  99. package/dist/src/dom/locale/ko.d.ts +6 -0
  100. package/dist/src/dom/locale/ko.js +6 -0
  101. package/dist/src/dom/locale/lt.d.ts +7 -0
  102. package/dist/src/dom/locale/lt.js +7 -0
  103. package/dist/src/dom/locale/lv.d.ts +6 -0
  104. package/dist/src/dom/locale/lv.js +6 -0
  105. package/dist/src/dom/locale/mk.d.ts +5 -0
  106. package/dist/src/dom/locale/mk.js +5 -0
  107. package/dist/src/dom/locale/ml.d.ts +7 -0
  108. package/dist/src/dom/locale/ml.js +7 -0
  109. package/dist/src/dom/locale/mn.d.ts +7 -0
  110. package/dist/src/dom/locale/mn.js +7 -0
  111. package/dist/src/dom/locale/mr.d.ts +7 -0
  112. package/dist/src/dom/locale/mr.js +7 -0
  113. package/dist/src/dom/locale/ms.d.ts +6 -0
  114. package/dist/src/dom/locale/ms.js +6 -0
  115. package/dist/src/dom/locale/my.d.ts +6 -0
  116. package/dist/src/dom/locale/my.js +6 -0
  117. package/dist/src/dom/locale/nb.d.ts +6 -0
  118. package/dist/src/dom/locale/nb.js +6 -0
  119. package/dist/src/dom/locale/nl.d.ts +6 -0
  120. package/dist/src/dom/locale/nl.js +6 -0
  121. package/dist/src/dom/locale/pa.d.ts +7 -0
  122. package/dist/src/dom/locale/pa.js +7 -0
  123. package/dist/src/dom/locale/pl.d.ts +6 -0
  124. package/dist/src/dom/locale/pl.js +6 -0
  125. package/dist/src/dom/locale/pt-BR.d.ts +6 -0
  126. package/dist/src/dom/locale/pt-BR.js +6 -0
  127. package/dist/src/dom/locale/pt-PT.d.ts +6 -0
  128. package/dist/src/dom/locale/pt-PT.js +6 -0
  129. package/dist/src/dom/locale/ro.d.ts +5 -0
  130. package/dist/src/dom/locale/ro.js +5 -0
  131. package/dist/src/dom/locale/ru.d.ts +3 -0
  132. package/dist/src/dom/locale/ru.js +3 -0
  133. package/dist/src/dom/locale/sk.d.ts +6 -0
  134. package/dist/src/dom/locale/sk.js +6 -0
  135. package/dist/src/dom/locale/sl.d.ts +6 -0
  136. package/dist/src/dom/locale/sl.js +6 -0
  137. package/dist/src/dom/locale/so.d.ts +6 -0
  138. package/dist/src/dom/locale/so.js +6 -0
  139. package/dist/src/dom/locale/sq.d.ts +6 -0
  140. package/dist/src/dom/locale/sq.js +6 -0
  141. package/dist/src/dom/locale/sr.d.ts +3 -0
  142. package/dist/src/dom/locale/sr.js +3 -0
  143. package/dist/src/dom/locale/sv.d.ts +6 -0
  144. package/dist/src/dom/locale/sv.js +6 -0
  145. package/dist/src/dom/locale/sw.d.ts +6 -0
  146. package/dist/src/dom/locale/sw.js +6 -0
  147. package/dist/src/dom/locale/ta.d.ts +7 -0
  148. package/dist/src/dom/locale/ta.js +7 -0
  149. package/dist/src/dom/locale/te.d.ts +7 -0
  150. package/dist/src/dom/locale/te.js +7 -0
  151. package/dist/src/dom/locale/th.d.ts +6 -0
  152. package/dist/src/dom/locale/th.js +6 -0
  153. package/dist/src/dom/locale/tr.d.ts +6 -0
  154. package/dist/src/dom/locale/tr.js +6 -0
  155. package/dist/src/dom/locale/types.d.ts +8 -1
  156. package/dist/src/dom/locale/uk.d.ts +6 -0
  157. package/dist/src/dom/locale/uk.js +6 -0
  158. package/dist/src/dom/locale/ur.d.ts +6 -0
  159. package/dist/src/dom/locale/ur.js +6 -0
  160. package/dist/src/dom/locale/vi.d.ts +6 -0
  161. package/dist/src/dom/locale/vi.js +6 -0
  162. package/dist/src/dom/locale/zh-HK.d.ts +7 -0
  163. package/dist/src/dom/locale/zh-HK.js +7 -0
  164. package/dist/src/dom/locale/zh-Hans.d.ts +7 -0
  165. package/dist/src/dom/locale/zh-Hans.js +7 -0
  166. package/dist/src/dom/locale/zh-TW.d.ts +7 -0
  167. package/dist/src/dom/locale/zh-TW.js +7 -0
  168. package/dist/src/dom/menus.js +13 -2
  169. package/dist/src/dom/wait-snapshot.d.ts +43 -0
  170. package/dist/src/dom/wait-snapshot.js +127 -0
  171. package/dist/src/safety/risk.d.ts +1 -0
  172. package/dist/src/safety/risk.js +1 -0
  173. package/dist/src/scripts/apply-intelligence-locale-captures.js +99 -13
  174. package/dist/src/types.d.ts +28 -0
  175. package/package.json +2 -2
  176. package/references/backend-protocol.md +6 -4
  177. package/references/localization.md +14 -4
  178. package/references/python-parity.md +5 -3
  179. package/references/troubleshooting.md +28 -5
@@ -1,7 +1,7 @@
1
1
  export declare const BACKEND_REQUEST_SCHEMA_VERSION: "chatgpt.browser_control.backend_request.v1";
2
2
  export declare const BACKEND_RESPONSE_SCHEMA_VERSION: "chatgpt.browser_control.backend_response.v1";
3
3
  export declare const BACKEND_EVENT_SCHEMA_VERSION: "chatgpt.browser_control.backend_event.v1";
4
- export declare const backendCommands: readonly ["backend.version", "backend.health", "backend.capabilities", "runner.run", "runner.plan", "runner.stream", "responses.create", "ask", "askInThread", "askWithFiles", "askAndDownload", "runMessages", "openThread", "readLatest", "copyLatest", "downloadLatest", "runPlan", "doctor", "createReport", "reports.create", "reports.redact", "reports.summarize", "commands", "describe", "help", "session.bootstrap", "threads.new", "threads.search", "threads.open", "messages.compose", "messages.submit", "messages.ask", "messages.wait", "messages.readLatest", "messages.waitAndRead", "artifacts.listLatest", "artifacts.wait", "artifacts.downloadLatest", "files.preflight", "files.attach", "files.downloadLatest", "projects.sources.list", "projects.sources.planAdd", "projects.sources.add", "modes.set", "tools.select", "response.copy"];
4
+ export declare const backendCommands: readonly ["backend.version", "backend.health", "backend.capabilities", "runner.run", "runner.plan", "runner.stream", "responses.create", "ask", "askInThread", "askWithFiles", "askAndDownload", "runMessages", "openThread", "readLatest", "copyLatest", "downloadLatest", "runPlan", "doctor", "createReport", "reports.create", "reports.redact", "reports.summarize", "commands", "describe", "help", "session.bootstrap", "threads.new", "threads.search", "threads.open", "messages.compose", "messages.submit", "messages.ask", "messages.wait", "messages.readLatest", "messages.waitAndRead", "artifacts.listLatest", "artifacts.wait", "artifacts.downloadLatest", "files.preflight", "files.attach", "files.downloadLatest", "projects.sources.list", "projects.sources.planAdd", "projects.sources.add", "modes.set", "modes.get", "tools.select", "response.copy"];
5
5
  export type BackendCommand = typeof backendCommands[number];
6
6
  export type BackendRequest = {
7
7
  schemaVersion: typeof BACKEND_REQUEST_SCHEMA_VERSION;
@@ -47,6 +47,7 @@ export const backendCommands = [
47
47
  "projects.sources.planAdd",
48
48
  "projects.sources.add",
49
49
  "modes.set",
50
+ "modes.get",
50
51
  "tools.select",
51
52
  "response.copy"
52
53
  ];
@@ -161,6 +161,8 @@ async function dispatchBackendCommand(client, request) {
161
161
  return client.projects.sources.add(payload);
162
162
  case "modes.set":
163
163
  return client.modes.set(payload);
164
+ case "modes.get":
165
+ return client.modes.get(emptyToUndefined(payload));
164
166
  case "tools.select":
165
167
  return client.tools.select(payload);
166
168
  case "response.copy":
@@ -1,2 +1,13 @@
1
+ export type ClipboardReadCommand = {
2
+ command: string;
3
+ args: string[];
4
+ };
5
+ /**
6
+ * Ordered clipboard-read command candidates for a platform. The first command that
7
+ * succeeds wins; callers fall back to DOM extraction when none do. Linux ordering
8
+ * prefers Wayland's wl-paste only when a Wayland session is detectable, otherwise the
9
+ * X11 tools go first so plain X sessions do not pay a doomed wl-paste attempt.
10
+ */
11
+ export declare function clipboardReadCommandsForPlatform(platform: NodeJS.Platform, env?: Record<string, string | undefined>): ClipboardReadCommand[];
1
12
  export declare function readSystemClipboard(): Promise<string | undefined>;
2
13
  export declare function waitForClipboardChange(before: string | undefined, timeoutMs: number, pollMs?: number): Promise<string | undefined>;
@@ -1,17 +1,44 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  const execFileAsync = promisify(execFile);
4
- export async function readSystemClipboard() {
5
- if (typeof process === "undefined" || process.platform !== "darwin") {
6
- return undefined;
4
+ /**
5
+ * Ordered clipboard-read command candidates for a platform. The first command that
6
+ * succeeds wins; callers fall back to DOM extraction when none do. Linux ordering
7
+ * prefers Wayland's wl-paste only when a Wayland session is detectable, otherwise the
8
+ * X11 tools go first so plain X sessions do not pay a doomed wl-paste attempt.
9
+ */
10
+ export function clipboardReadCommandsForPlatform(platform, env = {}) {
11
+ if (platform === "darwin") {
12
+ return [{ command: "pbpaste", args: [] }];
13
+ }
14
+ if (platform === "win32") {
15
+ return [{ command: "powershell.exe", args: ["-NoProfile", "-Command", "Get-Clipboard -Raw"] }];
7
16
  }
8
- try {
9
- const { stdout } = await execFileAsync("pbpaste", [], { timeout: 2000, maxBuffer: 10 * 1024 * 1024 });
10
- return stdout;
17
+ if (platform === "linux") {
18
+ const waylandCommand = { command: "wl-paste", args: ["--no-newline"] };
19
+ const x11Commands = [
20
+ { command: "xclip", args: ["-selection", "clipboard", "-o"] },
21
+ { command: "xsel", args: ["--clipboard", "--output"] }
22
+ ];
23
+ const isWayland = typeof env.WAYLAND_DISPLAY === "string" && env.WAYLAND_DISPLAY.length > 0;
24
+ return isWayland ? [waylandCommand, ...x11Commands] : [...x11Commands, waylandCommand];
11
25
  }
12
- catch {
26
+ return [];
27
+ }
28
+ export async function readSystemClipboard() {
29
+ if (typeof process === "undefined") {
13
30
  return undefined;
14
31
  }
32
+ for (const { command, args } of clipboardReadCommandsForPlatform(process.platform, process.env)) {
33
+ try {
34
+ const { stdout } = await execFileAsync(command, args, { timeout: 2000, maxBuffer: 10 * 1024 * 1024 });
35
+ return stdout;
36
+ }
37
+ catch {
38
+ // Missing tool or empty clipboard: try the next candidate.
39
+ }
40
+ }
41
+ return undefined;
15
42
  }
16
43
  export async function waitForClipboardChange(before, timeoutMs, pollMs = 150) {
17
44
  const started = Date.now();
@@ -3,8 +3,18 @@ import { compactVisibleText } from "../safety/redaction.js";
3
3
  import { escapeRegExp, localeLabels } from "../dom/locale-labels.js";
4
4
  import { withTimeout } from "../commands/timeouts.js";
5
5
  export function parseConversationId(url) {
6
- const match = /\/c\/([A-Za-z0-9-]+)/.exec(url);
7
- return match?.[1];
6
+ let parsed;
7
+ try {
8
+ parsed = new URL(url, "https://chatgpt.com");
9
+ }
10
+ catch {
11
+ return undefined;
12
+ }
13
+ const segments = parsed.pathname.split("/").filter(Boolean);
14
+ if (segments[0] !== "c" || segments[1] === undefined || segments[1].length === 0) {
15
+ return undefined;
16
+ }
17
+ return segments[1];
8
18
  }
9
19
  export async function readPageState(page) {
10
20
  const rawUrl = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
@@ -1,4 +1,4 @@
1
- import type { AskArgs, ArtifactDownloadArgs, ArtifactWaitArgs, AttachFilesArgs, BootstrapArgs, CommandResult, CopyResponseArgs, DownloadLatestArgs, FilePreflightArgs, FilePreflightData, ListArtifactsArgs, NewThreadArgs, OpenThreadArgs, ProjectSourcesAddArgs, ProjectSourcesAddData, ProjectSourcesAddPlanData, ProjectSourcesListArgs, ProjectSourcesListData, ProjectSourcesPlanAddArgs, ReadLatestArgs, RuntimeEnv, SearchThreadsArgs, SelectToolArgs, SequencePlan, SetModeArgs, ThreadTarget, WaitArgs } from "./types.js";
1
+ import type { AskArgs, ArtifactDownloadArgs, ArtifactWaitArgs, AttachFilesArgs, BootstrapArgs, CommandResult, CopyResponseArgs, DownloadLatestArgs, FilePreflightArgs, FilePreflightData, GetModeArgs, GetModeData, ListArtifactsArgs, NewThreadArgs, OpenThreadArgs, ProjectSourcesAddArgs, ProjectSourcesAddData, ProjectSourcesAddPlanData, ProjectSourcesListArgs, ProjectSourcesListData, ProjectSourcesPlanAddArgs, ReadLatestArgs, RuntimeEnv, SearchThreadsArgs, SelectToolArgs, SequencePlan, SetModeArgs, ThreadTarget, WaitArgs } from "./types.js";
2
2
  import { type DoctorArgs, type DoctorReport } from "./commands/doctor.js";
3
3
  import { type RunReportData, type RunReportOptions } from "./commands/reports.js";
4
4
  import { type CommandDescriptor } from "./commands/registry.js";
@@ -171,6 +171,7 @@ export type ChatGPTClient = {
171
171
  };
172
172
  modes: {
173
173
  set(args: SetModeArgs): Promise<CommandResult<unknown>>;
174
+ get(args?: GetModeArgs): Promise<CommandResult<GetModeData>>;
174
175
  };
175
176
  tools: {
176
177
  select(args: SelectToolArgs): Promise<CommandResult<unknown>>;
@@ -3,7 +3,7 @@ import { attachFiles, downloadLatestFile, preflightFiles } from "./commands/file
3
3
  import { addProjectSources, buildProjectSourceAddPlan, listProjectSources } from "./commands/project-sources.js";
4
4
  import { doctor } from "./commands/doctor.js";
5
5
  import { askMessage, composeMessage, readLatest, submitMessage, waitAndRead, waitForMessage } from "./commands/messages.js";
6
- import { selectTool, setMode } from "./commands/modes.js";
6
+ import { getMode, selectTool, setMode } from "./commands/modes.js";
7
7
  import { createRunReport } from "./commands/reports.js";
8
8
  import { copyResponse } from "./commands/response-actions.js";
9
9
  import { commandDescriptors, describeCommand, helpText } from "./commands/registry.js";
@@ -91,7 +91,8 @@ export function createChatGPT(options = {}) {
91
91
  downloadLatest: args => downloadLatestArtifact(env, args)
92
92
  },
93
93
  modes: {
94
- set: args => setMode(env, args)
94
+ set: args => setMode(env, args),
95
+ get: args => getMode(env, args)
95
96
  },
96
97
  tools: {
97
98
  select: args => selectTool(env, args)
@@ -6,7 +6,9 @@ export async function contextFromPage(page, partial = {}) {
6
6
  return { timestamp: new Date().toISOString(), ...partial };
7
7
  }
8
8
  const url = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => partial.url) : partial.url;
9
- const title = typeof page.title === "function" ? await page.title().catch(() => undefined) : partial.title;
9
+ const title = typeof page.title === "function"
10
+ ? await withTimeout(page.title(), 1000, "Timed out while reading page title.").catch(() => partial.title)
11
+ : partial.title;
10
12
  const [turnCount, assistantTurnCount] = await Promise.all([
11
13
  withTimeout(countPageMessages(page), 1000, "Timed out while counting page messages.").catch(() => partial.turnCount),
12
14
  withTimeout(countPageMessages(page, "assistant"), 1000, "Timed out while counting assistant messages.").catch(() => partial.assistantTurnCount)
@@ -0,0 +1,15 @@
1
+ import type { PageLike } from "../types.js";
2
+ export type ConversationTarget = {
3
+ href?: string;
4
+ url: string;
5
+ };
6
+ export type EnsureConversationTargetOptions = {
7
+ timeoutMs: number;
8
+ };
9
+ export type EnsureConversationTargetResult = {
10
+ navigated: boolean;
11
+ targetUrl: string;
12
+ expectedConversationId?: string;
13
+ };
14
+ export declare function ensureConversationTarget(page: PageLike, target: ConversationTarget, options: EnsureConversationTargetOptions): Promise<EnsureConversationTargetResult>;
15
+ export declare function waitForConversationHydrated(page: PageLike, timeoutMs: number, expectedConversationId?: string): Promise<void>;
@@ -0,0 +1,44 @@
1
+ import { parseConversationId } from "../browser/page-state.js";
2
+ import { countPageMessages, readLatestMessageText } from "../dom/messages.js";
3
+ const CHATGPT_HOME = "https://chatgpt.com/";
4
+ export async function ensureConversationTarget(page, target, options) {
5
+ const targetUrl = absoluteConversationUrl(target);
6
+ const expectedConversationId = parseConversationId(targetUrl);
7
+ const currentUrl = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
8
+ if (expectedConversationId !== undefined
9
+ && parseConversationId(typeof currentUrl === "string" ? currentUrl : "") === expectedConversationId) {
10
+ await waitForConversationHydrated(page, options.timeoutMs, expectedConversationId);
11
+ return ensureResult(false, targetUrl, expectedConversationId);
12
+ }
13
+ await page.goto?.(targetUrl, { waitUntil: "domcontentloaded", timeout: options.timeoutMs });
14
+ await waitForConversationHydrated(page, options.timeoutMs, expectedConversationId);
15
+ return ensureResult(true, targetUrl, expectedConversationId);
16
+ }
17
+ export async function waitForConversationHydrated(page, timeoutMs, expectedConversationId) {
18
+ const started = Date.now();
19
+ do {
20
+ const url = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
21
+ const urlMatches = expectedConversationId === undefined || parseConversationId(typeof url === "string" ? url : "") === expectedConversationId;
22
+ const count = await countPageMessages(page).catch(() => 0);
23
+ const latestAssistantText = await readLatestMessageText(page, "assistant").catch(() => undefined);
24
+ const title = typeof page.title === "function" ? await page.title().catch(() => "") : "";
25
+ if (urlMatches && ((latestAssistantText?.trim().length ?? 0) > 0 || (count > 0 && title.length > 0 && title !== "ChatGPT"))) {
26
+ await page.waitForTimeout?.(250);
27
+ return;
28
+ }
29
+ await page.waitForTimeout?.(500);
30
+ } while (Date.now() - started < timeoutMs);
31
+ }
32
+ function absoluteConversationUrl(target) {
33
+ if (target.href !== undefined && target.href.startsWith("/")) {
34
+ return new URL(target.href, CHATGPT_HOME).toString();
35
+ }
36
+ return target.href ?? target.url;
37
+ }
38
+ function ensureResult(navigated, targetUrl, expectedConversationId) {
39
+ const result = { navigated, targetUrl };
40
+ if (expectedConversationId !== undefined) {
41
+ result.expectedConversationId = expectedConversationId;
42
+ }
43
+ return result;
44
+ }
@@ -0,0 +1,8 @@
1
+ export type Deadline = {
2
+ startedAtMs: number;
3
+ timeoutMs: number;
4
+ expiresAtMs: number;
5
+ };
6
+ export declare function createDeadline(timeoutMs: number, startedAtMs?: number): Deadline;
7
+ export declare function remainingMs(deadline: Deadline, nowMs?: number): number;
8
+ export declare function childTimeoutMs(deadline: Deadline, capMs: number, nowMs?: number): number;
@@ -0,0 +1,14 @@
1
+ export function createDeadline(timeoutMs, startedAtMs = Date.now()) {
2
+ const safeTimeoutMs = Math.max(0, timeoutMs);
3
+ return {
4
+ startedAtMs,
5
+ timeoutMs: safeTimeoutMs,
6
+ expiresAtMs: startedAtMs + safeTimeoutMs,
7
+ };
8
+ }
9
+ export function remainingMs(deadline, nowMs = Date.now()) {
10
+ return Math.max(0, deadline.expiresAtMs - nowMs);
11
+ }
12
+ export function childTimeoutMs(deadline, capMs, nowMs = Date.now()) {
13
+ return Math.max(0, Math.min(Math.max(0, capMs), remainingMs(deadline, nowMs)));
14
+ }
@@ -31,6 +31,7 @@ const REQUIRED_LOCALE_KEYS = [
31
31
  "rateLimitBlocker"
32
32
  ];
33
33
  const REQUIRED_TOOL_IDS = ["web_search", "deep_research", "create_image"];
34
+ const REQUIRED_MODE_OPTION_IDS = ["latest", "instant", "thinking", "extended", "medium", "high", "extraHigh", "pro"];
34
35
  export async function doctor(env, args = {}) {
35
36
  const wanted = args.check ?? DEFAULT_CHECKS;
36
37
  const checks = {};
@@ -229,23 +230,29 @@ async function filePreflightCheck(env, args) {
229
230
  function localizationCheck(env) {
230
231
  const requiredKeysMissing = REQUIRED_LOCALE_KEYS.filter(key => localeLabels[key].length === 0);
231
232
  const missingToolIds = REQUIRED_TOOL_IDS.filter(id => (localeLabels.tools[id]?.length ?? 0) === 0);
233
+ const missingModeOptionIds = REQUIRED_MODE_OPTION_IDS.filter(id => (localeLabels.modeOptions[id]?.length ?? 0) === 0);
232
234
  const toolIds = Object.keys(localeLabels.tools);
235
+ const modeOptionIds = Object.keys(localeLabels.modeOptions);
233
236
  const englishCanonicalPresent = localeLabels.composerTextbox[0] === "Chat with ChatGPT"
234
237
  && localeLabels.sendButton[0] === "Send prompt"
235
238
  && localeLabels.modeLabels.includes("Thinking")
239
+ && localeLabels.modeOptions.pro?.[0] === "Pro"
236
240
  && localeLabels.tools.web_search?.[0] === "Web search";
237
241
  const labelCandidateCount = REQUIRED_LOCALE_KEYS.reduce((total, key) => total + localeLabels[key].length, 0)
238
- + Object.values(localeLabels.tools).reduce((total, values) => total + values.length, 0);
242
+ + Object.values(localeLabels.tools).reduce((total, values) => total + values.length, 0)
243
+ + Object.values(localeLabels.modeOptions).reduce((total, values) => total + values.length, 0);
239
244
  const details = {
240
245
  englishCanonicalPresent,
241
246
  requiredKeysMissing,
242
247
  missingToolIds,
248
+ missingModeOptionIds,
243
249
  toolIds,
250
+ modeOptionIds,
244
251
  labelCandidateCount,
245
252
  pageAvailable: env.page !== undefined,
246
253
  runtimeSelectorCoverage: "registry_only_stage_2"
247
254
  };
248
- if (englishCanonicalPresent && requiredKeysMissing.length === 0 && missingToolIds.length === 0) {
255
+ if (englishCanonicalPresent && requiredKeysMissing.length === 0 && missingToolIds.length === 0 && missingModeOptionIds.length === 0) {
249
256
  return unknown("The locale registry is loaded; localized runtime selector coverage is registry-only in Stage 2 and not fully proven.", undefined, details);
250
257
  }
251
258
  return blocked("The locale registry is missing canonical labels required for selector fallback.", ["Update src/dom/locale-labels.ts or src/dom/locale/* with verified visible labels before relying on localized controls."], details, "selector_drift");
@@ -1,5 +1,6 @@
1
1
  import { access, readFile, stat } from "node:fs/promises";
2
2
  import { constants } from "node:fs";
3
+ import { createHash } from "node:crypto";
3
4
  import path from "node:path";
4
5
  import { downloadLatestArtifact, locatorCountWithTimeout } from "./artifacts.js";
5
6
  import { waitForDownloadFromClick } from "../browser/downloads.js";
@@ -103,7 +104,17 @@ export async function preflightFiles(env, args) {
103
104
  message: `File attachment exceeds the configured per-file preflight limit: ${absolute} (${fileStat.size}/${maxBytesPerFile} bytes)`
104
105
  });
105
106
  }
106
- const metadata = fileMetadata(absolute, fileStat.size);
107
+ if (fileStat.size === 0) {
108
+ return filePreflightBlocker({
109
+ env,
110
+ status: "blocked",
111
+ kind: "upload_failed",
112
+ code: "file_empty",
113
+ fieldPath,
114
+ message: `File attachment path is zero bytes and ChatGPT rejects empty attachments: ${absolute}`
115
+ });
116
+ }
117
+ const metadata = await fileMetadata(absolute, fileStat.size, args.includeHashes === true);
107
118
  files.push(metadata);
108
119
  }
109
120
  const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
@@ -121,7 +132,11 @@ export async function preflightFiles(env, args) {
121
132
  return resultOk({ files, totalBytes }, filePreflightContext(env), warnings);
122
133
  }
123
134
  export async function attachFiles(env, args) {
124
- const preflight = await preflightFiles(env, { paths: args.paths });
135
+ const preflightArgs = { paths: args.paths };
136
+ if (args.includeDiagnostics === true && args.includeHashes !== undefined) {
137
+ preflightArgs.includeHashes = args.includeHashes;
138
+ }
139
+ const preflight = await preflightFiles(env, preflightArgs);
125
140
  if (!preflight.ok || preflight.data === undefined) {
126
141
  return preflight;
127
142
  }
@@ -137,6 +152,9 @@ export async function attachFiles(env, args) {
137
152
  bytes: file.bytes
138
153
  }));
139
154
  await uploadFiles(page, files, args.timeoutMs ?? 30000);
155
+ const browserInput = args.includeDiagnostics === true
156
+ ? await readBrowserInputDiagnostic(page).catch(() => undefined)
157
+ : undefined;
140
158
  await page.waitForTimeout?.(args.timeoutMs === undefined ? 1000 : Math.min(args.timeoutMs, 3000));
141
159
  const readiness = await waitForAttachedFilesReady(page, files, args.timeoutMs ?? 30000);
142
160
  if (!readiness.ready) {
@@ -169,7 +187,14 @@ export async function attachFiles(env, args) {
169
187
  context: await contextFromPage(page)
170
188
  };
171
189
  }
172
- return resultOk({ files }, await contextFromPage(page), preflight.warnings);
190
+ const data = { files };
191
+ if (args.includeDiagnostics === true) {
192
+ data.diagnostics = { preflight: preflight.data };
193
+ if (browserInput !== undefined) {
194
+ data.diagnostics.browserInput = browserInput;
195
+ }
196
+ }
197
+ return resultOk(data, await contextFromPage(page), preflight.warnings);
173
198
  }
174
199
  catch (error) {
175
200
  if (isUploadBridgeBlocker(error)) {
@@ -209,10 +234,10 @@ function filePreflightBlocker(args) {
209
234
  function filePreflightContext(env) {
210
235
  return { timestamp: (env.now?.() ?? new Date()).toISOString() };
211
236
  }
212
- function fileMetadata(absolute, bytes) {
237
+ async function fileMetadata(absolute, bytes, includeHash = false) {
213
238
  const extension = extensionForHostPath(absolute);
214
239
  const { mimeType, category } = guessFileType(extension);
215
- return {
240
+ const metadata = {
216
241
  path: absolute,
217
242
  name: basenameForHostPath(absolute),
218
243
  bytes,
@@ -220,6 +245,10 @@ function fileMetadata(absolute, bytes) {
220
245
  mimeType,
221
246
  category
222
247
  };
248
+ if (includeHash) {
249
+ metadata.sha256 = createHash("sha256").update(await readFile(absolute)).digest("hex");
250
+ }
251
+ return metadata;
223
252
  }
224
253
  function extensionForHostPath(value) {
225
254
  return process.platform === "win32"
@@ -230,9 +259,6 @@ function collectFilePreflightWarnings(files, warnings) {
230
259
  const byPath = new Map();
231
260
  const byName = new Map();
232
261
  for (const file of files) {
233
- if (file.bytes === 0) {
234
- warnings.push(`Zero-byte file will be attached if ChatGPT accepts it: ${file.name}`);
235
- }
236
262
  const pathCount = (byPath.get(file.path) ?? 0) + 1;
237
263
  byPath.set(file.path, pathCount);
238
264
  if (pathCount === 2) {
@@ -556,6 +582,34 @@ async function setHiddenFileInput(page, files) {
556
582
  }
557
583
  await input.setInputFiles(files.map(file => file.path));
558
584
  }
585
+ async function readBrowserInputDiagnostic(page) {
586
+ if (typeof page.evaluate !== "function") {
587
+ return undefined;
588
+ }
589
+ return page.evaluate(() => {
590
+ const input = (document.querySelector("#upload-files")
591
+ || document.querySelector("input[type='file']:not([accept='image/*'])")
592
+ || document.querySelector("input[type='file']"));
593
+ if (!input) {
594
+ return { files: [] };
595
+ }
596
+ return {
597
+ files: Array.from(input.files ?? []).map(file => {
598
+ const diagnostic = {
599
+ name: file.name,
600
+ size: file.size
601
+ };
602
+ if (file.type.length > 0) {
603
+ diagnostic.type = file.type;
604
+ }
605
+ if (file.lastModified !== 0) {
606
+ diagnostic.lastModified = file.lastModified;
607
+ }
608
+ return diagnostic;
609
+ })
610
+ };
611
+ });
612
+ }
559
613
  async function ensurePage(env) {
560
614
  if (env.page !== undefined) {
561
615
  return resultOk({}, await contextFromPage(env.page));