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,33 @@
1
+ export function requireConfirmation(confirm, expected) {
2
+ if (confirm?.understood === true
3
+ && confirm.targetKind === expected.targetKind
4
+ && confirm.targetDisplayName === expected.targetDisplayName
5
+ && confirm.action === expected.action) {
6
+ return undefined;
7
+ }
8
+ return {
9
+ ok: false,
10
+ status: "needs_confirmation",
11
+ warnings: [],
12
+ blocker: {
13
+ kind: "confirmation",
14
+ message: `Confirmation required before ${expected.action} on ${expected.targetKind} "${expected.targetDisplayName}".`
15
+ },
16
+ context: { timestamp: new Date().toISOString() }
17
+ };
18
+ }
19
+ export function rejectNetworkCommand(command) {
20
+ if (!command.startsWith("network.")) {
21
+ return undefined;
22
+ }
23
+ return {
24
+ ok: false,
25
+ status: "unsupported",
26
+ warnings: [],
27
+ blocker: {
28
+ kind: "confirmation",
29
+ message: "Private ChatGPT network replay commands are intentionally unsupported."
30
+ },
31
+ context: { timestamp: new Date().toISOString() }
32
+ };
33
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext, PageLike } from "../types.js";
2
+ export declare function contextFromPage(page: PageLike | undefined, partial?: Partial<CommandContext>): Promise<CommandContext>;
@@ -0,0 +1,32 @@
1
+ import { countPageMessages } from "../dom/messages.js";
2
+ import { parseConversationId } from "../browser/page-state.js";
3
+ export async function contextFromPage(page, partial = {}) {
4
+ if (page === undefined) {
5
+ return { timestamp: new Date().toISOString(), ...partial };
6
+ }
7
+ const url = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => partial.url) : partial.url;
8
+ const title = typeof page.title === "function" ? await page.title().catch(() => undefined) : partial.title;
9
+ const turnCount = await countPageMessages(page).catch(() => partial.turnCount);
10
+ const assistantTurnCount = await countPageMessages(page, "assistant").catch(() => partial.assistantTurnCount);
11
+ const conversationId = url !== undefined ? parseConversationId(url) : partial.conversationId;
12
+ const context = {
13
+ timestamp: new Date().toISOString(),
14
+ ...partial
15
+ };
16
+ if (url !== undefined) {
17
+ context.url = url;
18
+ }
19
+ if (title !== undefined) {
20
+ context.title = title;
21
+ }
22
+ if (turnCount !== undefined) {
23
+ context.turnCount = turnCount;
24
+ }
25
+ if (assistantTurnCount !== undefined) {
26
+ context.assistantTurnCount = assistantTurnCount;
27
+ }
28
+ if (conversationId !== undefined) {
29
+ context.conversationId = conversationId;
30
+ }
31
+ return context;
32
+ }
@@ -0,0 +1,16 @@
1
+ import type { CommandResult, RuntimeEnv } from "../types.js";
2
+ export type DoctorCheckName = "bridge" | "login" | "upload" | "download" | "clipboard" | "modes" | "tools" | "selectors";
3
+ export type CapabilityStatus = "ok" | "blocked" | "unsupported" | "unknown";
4
+ export type CapabilityCheck = {
5
+ status: CapabilityStatus;
6
+ message: string;
7
+ remediation?: string[];
8
+ };
9
+ export type DoctorArgs = {
10
+ check?: DoctorCheckName[];
11
+ };
12
+ export type DoctorReport = {
13
+ ready: boolean;
14
+ checks: Partial<Record<DoctorCheckName, CapabilityCheck>>;
15
+ };
16
+ export declare function doctor(env: RuntimeEnv, args?: DoctorArgs): Promise<CommandResult<DoctorReport>>;
@@ -0,0 +1,120 @@
1
+ import { readSystemClipboard } from "../browser/clipboard.js";
2
+ import { readPageState } from "../browser/page-state.js";
3
+ import { BROWSER_BRIDGE_REMEDIATION, resultOk } from "../errors.js";
4
+ import { contextFromPage } from "./context.js";
5
+ import { bootstrap } from "./session.js";
6
+ const DEFAULT_CHECKS = ["bridge", "login", "upload", "download", "clipboard", "modes", "tools", "selectors"];
7
+ const UPLOAD_REMEDIATION = [
8
+ "Codex Settings > Computer Use > Chrome > Permissions > Uploads: set to Always allow, or add chatgpt.com to the allowed upload domains.",
9
+ "Chrome chrome://extensions > Codex extension > Details: enable Allow access to file URLs."
10
+ ];
11
+ export async function doctor(env, args = {}) {
12
+ const wanted = args.check ?? DEFAULT_CHECKS;
13
+ const checks = {};
14
+ const boot = await bootstrap(env, { preferExistingTab: true, timeoutMs: 30000 });
15
+ for (const check of wanted) {
16
+ switch (check) {
17
+ case "bridge":
18
+ checks.bridge = boot.ok
19
+ ? ok("Chrome bridge is available.")
20
+ : bridgeCheck(boot);
21
+ break;
22
+ case "login":
23
+ checks.login = await loginCheck(env, boot);
24
+ break;
25
+ case "upload":
26
+ checks.upload = uploadCheck(env);
27
+ break;
28
+ case "download":
29
+ checks.download = downloadCheck(env);
30
+ break;
31
+ case "clipboard":
32
+ checks.clipboard = await clipboardCheck();
33
+ break;
34
+ case "modes":
35
+ checks.modes = selectorCheck(env, "Mode/tool selection requires role/text selectors in the current ChatGPT page.");
36
+ break;
37
+ case "tools":
38
+ checks.tools = selectorCheck(env, "Tool selection requires role/text selectors in the current ChatGPT page.");
39
+ break;
40
+ case "selectors":
41
+ checks.selectors = selectorCheck(env, "Basic page selectors are available.");
42
+ break;
43
+ }
44
+ }
45
+ const ready = Object.values(checks).every(check => check?.status === "ok" || check?.status === "unknown");
46
+ return resultOk({ ready, checks }, await contextFromPage(env.page));
47
+ }
48
+ function bridgeCheck(boot) {
49
+ if (boot.blocker?.kind === "browser_bridge_unavailable") {
50
+ return blocked(boot.blocker.message, bridgeRemediation(boot));
51
+ }
52
+ if (boot.blocker?.kind === "login_required") {
53
+ return ok("Chrome bridge is available; ChatGPT login is required before browser-control commands can continue.");
54
+ }
55
+ if (boot.blocker !== undefined) {
56
+ return unknown(`Chrome bridge responded, but bootstrap is blocked by ${boot.blocker.kind}: ${boot.blocker.message}`);
57
+ }
58
+ return blocked(boot.error?.message ?? "Chrome bridge is unavailable.");
59
+ }
60
+ async function loginCheck(env, boot) {
61
+ if (!boot.ok && boot.blocker?.kind === "login_required") {
62
+ return blocked("ChatGPT login is required.", ["Ask the user to sign in to ChatGPT in Chrome, then retry."]);
63
+ }
64
+ if (env.page === undefined) {
65
+ return boot.ok ? ok("Bootstrap completed; login appears usable.") : blocked("Cannot determine login because bootstrap failed.");
66
+ }
67
+ const state = await readPageState(env.page).catch(() => undefined);
68
+ if (state?.blocker?.kind === "login_required") {
69
+ return blocked("ChatGPT login is required.", ["Ask the user to sign in to ChatGPT in Chrome, then retry."]);
70
+ }
71
+ return state?.signedIn === true ? ok("ChatGPT appears signed in.") : unknown("Could not prove signed-in state from the visible page.");
72
+ }
73
+ function uploadCheck(env) {
74
+ const page = env.page;
75
+ if (page === undefined) {
76
+ return unknown("Upload readiness requires a bootstrapped ChatGPT page.", UPLOAD_REMEDIATION);
77
+ }
78
+ if (typeof page.waitForEvent !== "function" && typeof page.evaluate !== "function") {
79
+ return blocked("The active browser page exposes no upload-capable file chooser or DOM fallback.", UPLOAD_REMEDIATION);
80
+ }
81
+ return unknown("Upload permissions can only be proven by a live attach attempt.", UPLOAD_REMEDIATION);
82
+ }
83
+ function downloadCheck(env) {
84
+ const page = env.page;
85
+ if (page === undefined)
86
+ return unknown("Download readiness requires a bootstrapped ChatGPT page.");
87
+ return typeof page.waitForEvent === "function"
88
+ ? ok("Browser download events are available.")
89
+ : unsupported("The active browser page does not expose download events.");
90
+ }
91
+ async function clipboardCheck() {
92
+ const value = await readSystemClipboard();
93
+ return value === undefined
94
+ ? unknown("System clipboard could not be read; response.copy will use DOM fallback if copy does not change.")
95
+ : ok("System clipboard can be read.");
96
+ }
97
+ function selectorCheck(env, message) {
98
+ const page = env.page;
99
+ if (page === undefined)
100
+ return unknown("Selector readiness requires a bootstrapped ChatGPT page.");
101
+ return typeof page.locator === "function" || typeof page.getByRole === "function"
102
+ ? ok(message)
103
+ : unsupported("The active page object does not expose locator or role selector helpers.");
104
+ }
105
+ function bridgeRemediation(boot) {
106
+ const remediation = boot.blocker?.remediation ?? BROWSER_BRIDGE_REMEDIATION;
107
+ return remediation.map(step => `${step.label}: ${step.instruction}`);
108
+ }
109
+ function ok(message) {
110
+ return { status: "ok", message };
111
+ }
112
+ function blocked(message, remediation) {
113
+ return remediation === undefined ? { status: "blocked", message } : { status: "blocked", message, remediation };
114
+ }
115
+ function unsupported(message, remediation) {
116
+ return remediation === undefined ? { status: "unsupported", message } : { status: "unsupported", message, remediation };
117
+ }
118
+ function unknown(message, remediation) {
119
+ return remediation === undefined ? { status: "unknown", message } : { status: "unknown", message, remediation };
120
+ }
@@ -0,0 +1,4 @@
1
+ import type { AttachedFile, AttachFilesArgs, AttachFilesData, CommandResult, DownloadedFile, DownloadLatestArgs, RuntimeEnv } from "../types.js";
2
+ export declare function validateAttachPaths(paths: string[]): Promise<AttachedFile[]>;
3
+ export declare function attachFiles(env: RuntimeEnv, args: AttachFilesArgs): Promise<CommandResult<AttachFilesData>>;
4
+ export declare function downloadLatestFile(env: RuntimeEnv, args: DownloadLatestArgs): Promise<CommandResult<DownloadedFile>>;
@@ -0,0 +1,314 @@
1
+ import { access, readFile, stat } from "node:fs/promises";
2
+ import { basename, resolve } from "node:path";
3
+ import { constants } from "node:fs";
4
+ import { waitForDownloadFromClick } from "../browser/downloads.js";
5
+ import { resultError, resultOk } from "../errors.js";
6
+ import { addFilesButton, cssSelectors, requiredLocator } from "../dom/selectors.js";
7
+ import { contextFromPage } from "./context.js";
8
+ import { bootstrap } from "./session.js";
9
+ const CODEX_UPLOAD_PERMISSION_FIX = "Codex Settings > Computer Use > Chrome > Permissions > Uploads: set to Always allow, or add chatgpt.com to the allowed upload domains.";
10
+ const CHROME_FILE_URL_PERMISSION_FIX = "Chrome chrome://extensions > Codex extension > Details: enable Allow access to file URLs.";
11
+ export async function validateAttachPaths(paths) {
12
+ const files = [];
13
+ for (const path of paths) {
14
+ if (!path.startsWith("/")) {
15
+ throw new Error(`File attachment path must be absolute: ${path}`);
16
+ }
17
+ const absolute = resolve(path);
18
+ await access(absolute, constants.R_OK);
19
+ const fileStat = await stat(absolute);
20
+ if (!fileStat.isFile()) {
21
+ throw new Error(`Attachment path is not a file: ${absolute}`);
22
+ }
23
+ files.push({
24
+ path: absolute,
25
+ name: basename(absolute),
26
+ bytes: fileStat.size
27
+ });
28
+ }
29
+ return files;
30
+ }
31
+ export async function attachFiles(env, args) {
32
+ const boot = await ensurePage(env);
33
+ if (!boot.ok) {
34
+ return boot;
35
+ }
36
+ const page = env.page;
37
+ try {
38
+ const files = await validateAttachPaths(args.paths);
39
+ await uploadFiles(page, files, args.timeoutMs ?? 30000);
40
+ await page.waitForTimeout?.(args.timeoutMs === undefined ? 1000 : Math.min(args.timeoutMs, 3000));
41
+ return resultOk({ files }, await contextFromPage(page));
42
+ }
43
+ catch (error) {
44
+ if (isUploadBridgeBlocker(error)) {
45
+ return {
46
+ ok: false,
47
+ status: "blocked",
48
+ warnings: [],
49
+ blocker: {
50
+ kind: "permission",
51
+ code: "upload_permission_required",
52
+ message: uploadPermissionMessage(error),
53
+ visibleText: uploadPermissionDetails(error),
54
+ remediation: uploadPermissionRemediation(),
55
+ resumable: true
56
+ },
57
+ context: await contextFromPage(page)
58
+ };
59
+ }
60
+ return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
61
+ }
62
+ }
63
+ async function uploadFiles(page, files, timeoutMs) {
64
+ const paths = files.map(file => file.path);
65
+ const errors = [];
66
+ const attempts = [
67
+ {
68
+ name: "visible-chatgpt-file-input",
69
+ run: async () => {
70
+ await clickFileChooserTarget(page, "#upload-files", paths, timeoutMs, { requireVisible: true });
71
+ }
72
+ },
73
+ {
74
+ name: "add-photos-files-menu-item",
75
+ run: async () => {
76
+ await clickChatGPTAddPhotosMenuItem(page, paths, timeoutMs);
77
+ }
78
+ },
79
+ {
80
+ name: "generic-add-files-button",
81
+ run: async () => {
82
+ await clickFileChooserLocator(page, addFilesButton(page), paths, timeoutMs);
83
+ }
84
+ },
85
+ {
86
+ name: "direct-file-input-set",
87
+ run: async () => {
88
+ await setHiddenFileInput(page, files);
89
+ }
90
+ }
91
+ ];
92
+ for (const attempt of attempts) {
93
+ try {
94
+ await attempt.run();
95
+ return;
96
+ }
97
+ catch (error) {
98
+ errors.push(`${attempt.name}: ${error instanceof Error ? error.message : String(error)}`);
99
+ }
100
+ }
101
+ throw new Error(`No ChatGPT upload path completed.\n${errors.join("\n")}`);
102
+ }
103
+ async function clickChatGPTAddPhotosMenuItem(page, paths, timeoutMs) {
104
+ const menuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: "Add photos & files" });
105
+ if (await locatorCount(menuItem) !== 1) {
106
+ const plusButton = requiredLocator(page, "#composer-plus-btn, button[aria-label='Add files and more']");
107
+ if (await locatorCount(plusButton) !== 1) {
108
+ throw new Error("ChatGPT Add files button was not uniquely available.");
109
+ }
110
+ await plusButton.click?.({ timeoutMs: Math.min(timeoutMs, 10000) });
111
+ await page.waitForTimeout?.(250);
112
+ }
113
+ const refreshedMenuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: "Add photos & files" });
114
+ await clickFileChooserLocator(page, refreshedMenuItem, paths, timeoutMs);
115
+ }
116
+ async function clickFileChooserTarget(page, selector, paths, timeoutMs, options = {}) {
117
+ const locator = requiredLocator(page, selector);
118
+ if (await locatorCount(locator) !== 1) {
119
+ throw new Error(`Upload target was not uniquely available: ${selector}`);
120
+ }
121
+ if (options.requireVisible === true && locator.isVisible !== undefined && !await locator.isVisible({ timeoutMs: 1000 })) {
122
+ throw new Error(`Upload target is hidden: ${selector}`);
123
+ }
124
+ await clickFileChooserLocator(page, locator, paths, timeoutMs);
125
+ }
126
+ async function clickFileChooserLocator(page, locator, paths, timeoutMs) {
127
+ if (locator === undefined) {
128
+ throw new Error("Upload locator was not available.");
129
+ }
130
+ if (typeof page.waitForEvent !== "function") {
131
+ throw new Error("The active browser page does not expose file chooser events.");
132
+ }
133
+ if (typeof locator.click !== "function") {
134
+ throw new Error("Upload locator does not expose click().");
135
+ }
136
+ const chooserPromise = waitForFileChooser(page, timeoutMs);
137
+ try {
138
+ await locator.click({ timeoutMs: Math.min(timeoutMs, 10000) });
139
+ }
140
+ catch (error) {
141
+ await chooserPromise.catch(() => undefined);
142
+ throw error;
143
+ }
144
+ const chooser = await chooserPromise;
145
+ await validateChooserMultiplicity(chooser, paths);
146
+ try {
147
+ await chooser.setFiles(paths);
148
+ }
149
+ catch (error) {
150
+ throw new Error(`fileChooser.setFiles failed. ${error instanceof Error ? error.message : String(error)}`);
151
+ }
152
+ }
153
+ async function waitForFileChooser(page, timeoutMs) {
154
+ const rawChooser = await page.waitForEvent?.("filechooser", {
155
+ timeout: timeoutMs,
156
+ timeoutMs
157
+ });
158
+ if (!isFileChooserLike(rawChooser)) {
159
+ throw new Error("File chooser event did not return a setFiles-capable chooser.");
160
+ }
161
+ return rawChooser;
162
+ }
163
+ async function validateChooserMultiplicity(chooser, paths) {
164
+ if (paths.length <= 1 || typeof chooser.isMultiple !== "function") {
165
+ return;
166
+ }
167
+ const isMultiple = await chooser.isMultiple();
168
+ if (!isMultiple) {
169
+ throw new Error("The active ChatGPT file chooser only accepts one file.");
170
+ }
171
+ }
172
+ function isFileChooserLike(value) {
173
+ return value !== null
174
+ && typeof value === "object"
175
+ && typeof value.setFiles === "function";
176
+ }
177
+ async function locatorCount(locator) {
178
+ if (locator === undefined || typeof locator.count !== "function") {
179
+ return 0;
180
+ }
181
+ return locator.count();
182
+ }
183
+ export async function downloadLatestFile(env, args) {
184
+ const boot = await ensurePage(env);
185
+ if (!boot.ok) {
186
+ return boot;
187
+ }
188
+ const page = env.page;
189
+ try {
190
+ const controls = requiredLocator(page, cssSelectors.downloadControls);
191
+ const count = await controls.count?.();
192
+ if (count === 0) {
193
+ return {
194
+ ok: false,
195
+ status: "unsupported",
196
+ warnings: [],
197
+ blocker: {
198
+ kind: "download_unavailable",
199
+ message: "No visible ChatGPT download control was found."
200
+ },
201
+ context: await contextFromPage(page)
202
+ };
203
+ }
204
+ const target = args.from === "visible_conversation" ? controls.last?.() ?? controls : controls.last?.() ?? controls;
205
+ const downloaded = await waitForDownloadFromClick(page, async () => {
206
+ await target.click?.();
207
+ }, args.destDir, args.timeoutMs ?? 120000);
208
+ return resultOk(downloaded, await contextFromPage(page));
209
+ }
210
+ catch (error) {
211
+ return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
212
+ }
213
+ }
214
+ async function setHiddenFileInput(page, files) {
215
+ if (page === undefined) {
216
+ throw new Error("No active page is available for file upload.");
217
+ }
218
+ const input = requiredLocator(page, cssSelectors.hiddenFileInputs).last?.() ?? requiredLocator(page, cssSelectors.hiddenFileInputs);
219
+ if (typeof input.setInputFiles !== "function") {
220
+ await setFilesViaDomDataTransfer(page, files);
221
+ return;
222
+ }
223
+ await input.setInputFiles(files.map(file => file.path));
224
+ }
225
+ async function ensurePage(env) {
226
+ if (env.page !== undefined) {
227
+ return resultOk({}, await contextFromPage(env.page));
228
+ }
229
+ return bootstrap(env, { preferExistingTab: true });
230
+ }
231
+ async function setFilesViaDomDataTransfer(page, files) {
232
+ const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
233
+ const maxInlineBytes = 25 * 1024 * 1024;
234
+ if (totalBytes > maxInlineBytes) {
235
+ throw new Error(`No file chooser or setInputFiles support is available for large uploads. ${CODEX_UPLOAD_PERMISSION_FIX} ${CHROME_FILE_URL_PERMISSION_FIX}`);
236
+ }
237
+ if (typeof page.evaluate !== "function") {
238
+ throw new Error(`No file chooser, setInputFiles, or page.evaluate support is available for file upload. ${CODEX_UPLOAD_PERMISSION_FIX} ${CHROME_FILE_URL_PERMISSION_FIX}`);
239
+ }
240
+ const payload = await Promise.all(files.map(async (file) => ({
241
+ name: file.name,
242
+ bytesBase64: (await readFile(file.path)).toString("base64"),
243
+ type: guessMimeType(file.name)
244
+ })));
245
+ await page.evaluate(async (payload) => {
246
+ const input = (document.querySelector("#upload-files") || document.querySelector("input[type='file']:not([accept='image/*'])") || document.querySelector("input[type='file']"));
247
+ if (!input) {
248
+ throw new Error("No ChatGPT file input found in the DOM.");
249
+ }
250
+ const dataTransfer = new DataTransfer();
251
+ for (const item of payload) {
252
+ const binary = atob(item.bytesBase64);
253
+ const bytes = new Uint8Array(binary.length);
254
+ for (let index = 0; index < binary.length; index += 1) {
255
+ bytes[index] = binary.charCodeAt(index);
256
+ }
257
+ dataTransfer.items.add(new File([bytes], item.name, { type: item.type }));
258
+ }
259
+ input.files = dataTransfer.files;
260
+ input.dispatchEvent(new Event("input", { bubbles: true }));
261
+ input.dispatchEvent(new Event("change", { bubbles: true }));
262
+ }, payload);
263
+ }
264
+ function guessMimeType(name) {
265
+ if (/\.txt$/i.test(name))
266
+ return "text/plain";
267
+ if (/\.pdf$/i.test(name))
268
+ return "application/pdf";
269
+ if (/\.csv$/i.test(name))
270
+ return "text/csv";
271
+ if (/\.json$/i.test(name))
272
+ return "application/json";
273
+ if (/\.md$/i.test(name))
274
+ return "text/markdown";
275
+ return "application/octet-stream";
276
+ }
277
+ function isUploadBridgeBlocker(error) {
278
+ const message = error instanceof Error ? error.message : String(error);
279
+ return /DataTransfer is not a constructor|No file chooser|setInputFiles|Allow access to file URLs|file upload|fileChooser\.setFiles failed|Not allowed|No ChatGPT upload path completed/i.test(message);
280
+ }
281
+ function uploadPermissionMessage(error) {
282
+ const message = error instanceof Error ? error.message : String(error);
283
+ if (/fileChooser\.setFiles failed|Not allowed/i.test(message)) {
284
+ return `ChatGPT's file chooser opened, but Chrome refused the local file handoff. Ask the user to enable both upload permission gates, then retry: ${CODEX_UPLOAD_PERMISSION_FIX} ${CHROME_FILE_URL_PERMISSION_FIX}`;
285
+ }
286
+ if (/Browser Use rejected|requested that files not be uploaded|upload files|permission denied|browser blocked/i.test(message)) {
287
+ return `Codex/Chrome upload permission is blocking file attachment. Ask the user to enable both upload permission gates, then retry: ${CODEX_UPLOAD_PERMISSION_FIX} ${CHROME_FILE_URL_PERMISSION_FIX}`;
288
+ }
289
+ return `File upload is not available until both upload permission gates are enabled. Ask the user to enable them, then retry: ${CODEX_UPLOAD_PERMISSION_FIX} ${CHROME_FILE_URL_PERMISSION_FIX}`;
290
+ }
291
+ function uploadPermissionDetails(error) {
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ return [
294
+ "Upload permission troubleshooting:",
295
+ `1. ${CODEX_UPLOAD_PERMISSION_FIX}`,
296
+ `2. ${CHROME_FILE_URL_PERMISSION_FIX}`,
297
+ "Observed failure:",
298
+ message
299
+ ].join("\n");
300
+ }
301
+ function uploadPermissionRemediation() {
302
+ return [
303
+ {
304
+ label: "Codex Chrome uploads",
305
+ instruction: CODEX_UPLOAD_PERMISSION_FIX,
306
+ userActionRequired: true
307
+ },
308
+ {
309
+ label: "Chrome file URLs",
310
+ instruction: CHROME_FILE_URL_PERMISSION_FIX,
311
+ userActionRequired: true
312
+ }
313
+ ];
314
+ }
@@ -0,0 +1,16 @@
1
+ import type { AskHelperArgs, AskInThreadArgs, AttachAskReadArgs, CommandResult, DownloadLatestArgs, PrecannedResponseArgs, RuntimeEnv, SearchOpenCopyArgs, SendAndWaitArgs, SequencePlan, TwoTurnExchangeArgs } from "../types.js";
2
+ export declare function planAsk(args: AskHelperArgs): SequencePlan;
3
+ export declare function planAskInThread(args: AskInThreadArgs): SequencePlan;
4
+ export declare function planAttachAskRead(args: AttachAskReadArgs): SequencePlan;
5
+ export declare function planDownloadLatestAttachment(args: DownloadLatestArgs): SequencePlan;
6
+ export declare function planSearchOpenCopyLatest(args: SearchOpenCopyArgs): SequencePlan;
7
+ export declare function planTwoTurnExchange(args: TwoTurnExchangeArgs): SequencePlan;
8
+ export declare function ask(args: AskHelperArgs, env?: RuntimeEnv): Promise<CommandResult<unknown>>;
9
+ export declare function askInThread(args: AskInThreadArgs, env?: RuntimeEnv): Promise<CommandResult<unknown>>;
10
+ export declare function findSwitchAskWaitRead(args: AskInThreadArgs, env?: RuntimeEnv): Promise<CommandResult<unknown>>;
11
+ export declare function sendAndWait(args: SendAndWaitArgs, env?: RuntimeEnv): Promise<CommandResult<unknown>>;
12
+ export declare function sendPrecannedResponse(args: PrecannedResponseArgs, env?: RuntimeEnv): Promise<CommandResult<unknown>>;
13
+ export declare function attachAskRead(args: AttachAskReadArgs, env?: RuntimeEnv): Promise<CommandResult<unknown>>;
14
+ export declare function downloadLatestAttachment(args: DownloadLatestArgs, env?: RuntimeEnv): Promise<CommandResult<unknown>>;
15
+ export declare function searchOpenCopyLatest(args: SearchOpenCopyArgs, env?: RuntimeEnv): Promise<CommandResult<unknown>>;
16
+ export declare function twoTurnExchange(args: TwoTurnExchangeArgs, env?: RuntimeEnv): Promise<CommandResult<unknown>>;
@@ -0,0 +1,128 @@
1
+ import { runSequence } from "./sequence.js";
2
+ export function planAsk(args) {
3
+ const steps = [
4
+ { id: "bootstrap", command: "session.bootstrap" }
5
+ ];
6
+ if (args.thread !== undefined) {
7
+ steps.push(...threadOpenSteps(args.thread));
8
+ }
9
+ steps.push({ id: "ask", command: "messages.ask", args: askStepArgs(args) });
10
+ return { name: "ask", steps };
11
+ }
12
+ export function planAskInThread(args) {
13
+ return {
14
+ name: "ask-in-thread",
15
+ policy: {
16
+ stopOnError: true,
17
+ returnPartial: true,
18
+ allowPromptResubmit: "only_if_no_matching_user_turn"
19
+ },
20
+ steps: [
21
+ { id: "bootstrap", command: "session.bootstrap" },
22
+ ...threadOpenSteps(args.thread),
23
+ { id: "ask", command: "messages.ask", args: askStepArgs(args) }
24
+ ]
25
+ };
26
+ }
27
+ export function planAttachAskRead(args) {
28
+ return {
29
+ name: "attach-ask-read",
30
+ policy: { stopOnError: true, returnPartial: true },
31
+ steps: [
32
+ { id: "bootstrap", command: "session.bootstrap" },
33
+ ...threadOpenSteps(args.thread),
34
+ { id: "attach", command: "files.attach", args: { paths: args.files } },
35
+ { id: "ask", command: "messages.ask", args: { text: args.text, wait: args.wait ?? true, read: args.read ?? true } }
36
+ ]
37
+ };
38
+ }
39
+ export function planDownloadLatestAttachment(args) {
40
+ return {
41
+ name: "download-latest-attachment",
42
+ steps: [
43
+ { id: "bootstrap", command: "session.bootstrap" },
44
+ { id: "download", command: "files.downloadLatest", args }
45
+ ]
46
+ };
47
+ }
48
+ export function planSearchOpenCopyLatest(args) {
49
+ return {
50
+ name: "search-open-copy-latest",
51
+ steps: [
52
+ { id: "bootstrap", command: "session.bootstrap" },
53
+ ...threadOpenSteps(args.thread),
54
+ { id: "copy", command: "response.copy", args: { which: "latest" } }
55
+ ]
56
+ };
57
+ }
58
+ export function planTwoTurnExchange(args) {
59
+ return {
60
+ name: "two-turn-exchange",
61
+ policy: { stopOnError: true, returnPartial: true },
62
+ steps: [
63
+ { id: "bootstrap", command: "session.bootstrap" },
64
+ ...threadOpenSteps(args.thread),
65
+ { id: "ask1", command: "messages.ask", args: { text: args.text, wait: true, read: true } },
66
+ { id: "ask2", command: "messages.ask", args: { text: args.followupText, wait: true, read: true } }
67
+ ]
68
+ };
69
+ }
70
+ export async function ask(args, env = {}) {
71
+ return runSequence(planAsk(args), env);
72
+ }
73
+ export async function askInThread(args, env = {}) {
74
+ return runSequence(planAskInThread(args), env);
75
+ }
76
+ export async function findSwitchAskWaitRead(args, env = {}) {
77
+ return askInThread(args, env);
78
+ }
79
+ export async function sendAndWait(args, env = {}) {
80
+ return runSequence({
81
+ name: "send-and-wait",
82
+ steps: [
83
+ { id: "bootstrap", command: "session.bootstrap" },
84
+ { id: "ask", command: "messages.ask", args: { text: args.text, wait: args.wait ?? true, read: true } }
85
+ ]
86
+ }, env);
87
+ }
88
+ export async function sendPrecannedResponse(args, env = {}) {
89
+ return askInThread(args, env);
90
+ }
91
+ export async function attachAskRead(args, env = {}) {
92
+ return runSequence(planAttachAskRead(args), env);
93
+ }
94
+ export async function downloadLatestAttachment(args, env = {}) {
95
+ return runSequence(planDownloadLatestAttachment(args), env);
96
+ }
97
+ export async function searchOpenCopyLatest(args, env = {}) {
98
+ return runSequence(planSearchOpenCopyLatest(args), env);
99
+ }
100
+ export async function twoTurnExchange(args, env = {}) {
101
+ return runSequence(planTwoTurnExchange(args), env);
102
+ }
103
+ function threadOpenSteps(thread) {
104
+ if (thread.url !== undefined) {
105
+ return [{ id: "open", command: "threads.open", args: { url: thread.url } }];
106
+ }
107
+ if (thread.conversationId !== undefined) {
108
+ return [{ id: "open", command: "threads.open", args: { conversationId: thread.conversationId } }];
109
+ }
110
+ const query = thread.query ?? thread.title;
111
+ if (query !== undefined) {
112
+ return [
113
+ { id: "find", command: "threads.search", args: { query, limit: 5 } },
114
+ { id: "open", command: "threads.open", args: { fromStep: "find", select: thread.title === undefined ? "first" : { title: thread.title } } }
115
+ ];
116
+ }
117
+ return [];
118
+ }
119
+ function askStepArgs(args) {
120
+ const askArgs = { text: args.text };
121
+ if (args.wait !== undefined) {
122
+ askArgs.wait = args.wait;
123
+ }
124
+ if (args.read !== undefined) {
125
+ askArgs.read = args.read;
126
+ }
127
+ return askArgs;
128
+ }