codex-chatgpt-control 0.1.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.
- package/CHANGELOG.md +14 -0
- package/README.md +55 -0
- package/contracts/v1/fixtures/backend-capabilities.json +8 -0
- package/contracts/v1/fixtures/blocker-explanation-profiles.json +114 -0
- package/contracts/v1/fixtures/command-descriptors.json +224 -16
- package/contracts/v1/fixtures/doctor-bridge-upload.json +8 -0
- package/contracts/v1/fixtures/doctor-scenario-preflight.json +119 -0
- package/contracts/v1/fixtures/existing-tab-diagnostics-blocker.json +57 -0
- package/contracts/v1/fixtures/files-preflight-success.json +32 -0
- package/contracts/v1/fixtures/help-root.json +1 -1
- package/contracts/v1/fixtures/output-json-parse-success.json +17 -2
- package/contracts/v1/fixtures/project-sources-plan-add.json +71 -0
- package/contracts/v1/fixtures/read-latest-clipped.json +31 -0
- package/contracts/v1/fixtures/reports-create-redacted.json +15 -0
- package/contracts/v1/fixtures/responses-basic-success.json +11 -0
- package/contracts/v1/fixtures/run-basic-success.json +15 -0
- package/contracts/v1/fixtures/run-browser-bridge-blocker.json +2 -2
- package/contracts/v1/fixtures/run-timeout-partial.json +2 -2
- package/contracts/v1/fixtures/runner-budget-blocker.json +1 -1
- package/contracts/v1/fixtures/stream-blocked.ndjson +1 -1
- package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
- package/contracts/v1/manifest.json +30 -0
- package/contracts/v1/parity-suite.json +80 -7
- package/contracts/v1/schemas/backend-request.schema.json +3 -0
- package/contracts/v1/schemas/command-result.schema.json +48 -0
- package/contracts/v1/schemas/response.schema.json +28 -0
- package/contracts/v1/schemas/run-result.schema.json +84 -0
- package/contracts/v1/surface-drift-policy.json +210 -0
- package/dist/codex-chatgpt-control-backend.mjs +7478 -1789
- package/dist/codex-chatgpt-control.bundle.mjs +8023 -2266
- package/dist/src/backend/client.d.ts +6 -1
- package/dist/src/backend/client.js +5 -0
- package/dist/src/backend/protocol.d.ts +1 -1
- package/dist/src/backend/protocol.js +8 -0
- package/dist/src/backend/session.js +16 -0
- package/dist/src/browser/attach.js +128 -20
- package/dist/src/browser/clipboard.d.ts +11 -0
- package/dist/src/browser/clipboard.js +34 -7
- package/dist/src/browser/downloads.js +2 -1
- package/dist/src/browser/page-state.js +18 -5
- package/dist/src/client.d.ts +17 -1
- package/dist/src/client.js +89 -9
- package/dist/src/commands/artifacts.d.ts +5 -0
- package/dist/src/commands/artifacts.js +432 -0
- package/dist/src/commands/context.js +8 -3
- package/dist/src/commands/conversation.d.ts +15 -0
- package/dist/src/commands/conversation.js +44 -0
- package/dist/src/commands/deadline.d.ts +8 -0
- package/dist/src/commands/deadline.js +14 -0
- package/dist/src/commands/doctor.d.ts +10 -2
- package/dist/src/commands/doctor.js +249 -14
- package/dist/src/commands/files.d.ts +2 -1
- package/dist/src/commands/files.js +406 -18
- package/dist/src/commands/messages.d.ts +2 -1
- package/dist/src/commands/messages.js +430 -98
- package/dist/src/commands/modes.d.ts +4 -1
- package/dist/src/commands/modes.js +404 -37
- package/dist/src/commands/probes.d.ts +15 -0
- package/dist/src/commands/probes.js +64 -0
- package/dist/src/commands/project-sources.d.ts +10 -0
- package/dist/src/commands/project-sources.js +643 -0
- package/dist/src/commands/registry.js +108 -14
- package/dist/src/commands/reports.d.ts +9 -0
- package/dist/src/commands/reports.js +84 -7
- package/dist/src/commands/response-actions.js +33 -0
- package/dist/src/commands/sequence.js +14 -0
- package/dist/src/commands/threads.js +4 -25
- package/dist/src/commands/timeouts.d.ts +2 -0
- package/dist/src/commands/timeouts.js +18 -0
- package/dist/src/diagnostics/blockers.d.ts +54 -0
- package/dist/src/diagnostics/blockers.js +299 -0
- package/dist/src/dom/artifacts.d.ts +7 -0
- package/dist/src/dom/artifacts.js +198 -0
- package/dist/src/dom/generation-state.d.ts +15 -0
- package/dist/src/dom/generation-state.js +109 -0
- package/dist/src/dom/label-match.d.ts +3 -0
- package/dist/src/dom/label-match.js +25 -0
- package/dist/src/dom/locale/am.d.ts +34 -0
- package/dist/src/dom/locale/am.js +34 -0
- package/dist/src/dom/locale/ar.d.ts +35 -0
- package/dist/src/dom/locale/ar.js +35 -0
- package/dist/src/dom/locale/bg.d.ts +35 -0
- package/dist/src/dom/locale/bg.js +35 -0
- package/dist/src/dom/locale/bn.d.ts +35 -0
- package/dist/src/dom/locale/bn.js +35 -0
- package/dist/src/dom/locale/bs.d.ts +34 -0
- package/dist/src/dom/locale/bs.js +34 -0
- package/dist/src/dom/locale/ca.d.ts +34 -0
- package/dist/src/dom/locale/ca.js +34 -0
- package/dist/src/dom/locale/cs.d.ts +34 -0
- package/dist/src/dom/locale/cs.js +34 -0
- package/dist/src/dom/locale/da.d.ts +33 -0
- package/dist/src/dom/locale/da.js +33 -0
- package/dist/src/dom/locale/de.d.ts +36 -0
- package/dist/src/dom/locale/de.js +36 -0
- package/dist/src/dom/locale/el.d.ts +34 -0
- package/dist/src/dom/locale/el.js +34 -0
- package/dist/src/dom/locale/en.d.ts +64 -0
- package/dist/src/dom/locale/en.js +71 -0
- package/dist/src/dom/locale/es-419.d.ts +34 -0
- package/dist/src/dom/locale/es-419.js +34 -0
- package/dist/src/dom/locale/es-ES.d.ts +34 -0
- package/dist/src/dom/locale/es-ES.js +34 -0
- package/dist/src/dom/locale/et.d.ts +34 -0
- package/dist/src/dom/locale/et.js +34 -0
- package/dist/src/dom/locale/fa.d.ts +35 -0
- package/dist/src/dom/locale/fa.js +35 -0
- package/dist/src/dom/locale/fi.d.ts +34 -0
- package/dist/src/dom/locale/fi.js +34 -0
- package/dist/src/dom/locale/fr-CA.d.ts +35 -0
- package/dist/src/dom/locale/fr-CA.js +35 -0
- package/dist/src/dom/locale/fr-FR.d.ts +35 -0
- package/dist/src/dom/locale/fr-FR.js +35 -0
- package/dist/src/dom/locale/gu.d.ts +34 -0
- package/dist/src/dom/locale/gu.js +34 -0
- package/dist/src/dom/locale/hi.d.ts +34 -0
- package/dist/src/dom/locale/hi.js +34 -0
- package/dist/src/dom/locale/hr.d.ts +33 -0
- package/dist/src/dom/locale/hr.js +33 -0
- package/dist/src/dom/locale/hu.d.ts +34 -0
- package/dist/src/dom/locale/hu.js +34 -0
- package/dist/src/dom/locale/hy.d.ts +35 -0
- package/dist/src/dom/locale/hy.js +35 -0
- package/dist/src/dom/locale/id.d.ts +34 -0
- package/dist/src/dom/locale/id.js +34 -0
- package/dist/src/dom/locale/index.d.ts +83 -0
- package/dist/src/dom/locale/index.js +239 -0
- package/dist/src/dom/locale/is.d.ts +34 -0
- package/dist/src/dom/locale/is.js +34 -0
- package/dist/src/dom/locale/it.d.ts +33 -0
- package/dist/src/dom/locale/it.js +33 -0
- package/dist/src/dom/locale/ja.d.ts +33 -0
- package/dist/src/dom/locale/ja.js +33 -0
- package/dist/src/dom/locale/ka.d.ts +34 -0
- package/dist/src/dom/locale/ka.js +34 -0
- package/dist/src/dom/locale/kk.d.ts +34 -0
- package/dist/src/dom/locale/kk.js +34 -0
- package/dist/src/dom/locale/kn.d.ts +35 -0
- package/dist/src/dom/locale/kn.js +35 -0
- package/dist/src/dom/locale/ko.d.ts +34 -0
- package/dist/src/dom/locale/ko.js +34 -0
- package/dist/src/dom/locale/lt.d.ts +35 -0
- package/dist/src/dom/locale/lt.js +35 -0
- package/dist/src/dom/locale/lv.d.ts +34 -0
- package/dist/src/dom/locale/lv.js +34 -0
- package/dist/src/dom/locale/mk.d.ts +33 -0
- package/dist/src/dom/locale/mk.js +33 -0
- package/dist/src/dom/locale/ml.d.ts +35 -0
- package/dist/src/dom/locale/ml.js +35 -0
- package/dist/src/dom/locale/mn.d.ts +35 -0
- package/dist/src/dom/locale/mn.js +35 -0
- package/dist/src/dom/locale/mr.d.ts +35 -0
- package/dist/src/dom/locale/mr.js +35 -0
- package/dist/src/dom/locale/ms.d.ts +34 -0
- package/dist/src/dom/locale/ms.js +34 -0
- package/dist/src/dom/locale/my.d.ts +34 -0
- package/dist/src/dom/locale/my.js +34 -0
- package/dist/src/dom/locale/nb.d.ts +34 -0
- package/dist/src/dom/locale/nb.js +34 -0
- package/dist/src/dom/locale/nl.d.ts +34 -0
- package/dist/src/dom/locale/nl.js +34 -0
- package/dist/src/dom/locale/pa.d.ts +35 -0
- package/dist/src/dom/locale/pa.js +35 -0
- package/dist/src/dom/locale/pl.d.ts +34 -0
- package/dist/src/dom/locale/pl.js +34 -0
- package/dist/src/dom/locale/pt-BR.d.ts +34 -0
- package/dist/src/dom/locale/pt-BR.js +34 -0
- package/dist/src/dom/locale/pt-PT.d.ts +34 -0
- package/dist/src/dom/locale/pt-PT.js +34 -0
- package/dist/src/dom/locale/ro.d.ts +33 -0
- package/dist/src/dom/locale/ro.js +33 -0
- package/dist/src/dom/locale/ru.d.ts +31 -0
- package/dist/src/dom/locale/ru.js +31 -0
- package/dist/src/dom/locale/sk.d.ts +34 -0
- package/dist/src/dom/locale/sk.js +34 -0
- package/dist/src/dom/locale/sl.d.ts +34 -0
- package/dist/src/dom/locale/sl.js +34 -0
- package/dist/src/dom/locale/so.d.ts +34 -0
- package/dist/src/dom/locale/so.js +34 -0
- package/dist/src/dom/locale/sq.d.ts +34 -0
- package/dist/src/dom/locale/sq.js +34 -0
- package/dist/src/dom/locale/sr.d.ts +31 -0
- package/dist/src/dom/locale/sr.js +31 -0
- package/dist/src/dom/locale/sv.d.ts +34 -0
- package/dist/src/dom/locale/sv.js +34 -0
- package/dist/src/dom/locale/sw.d.ts +34 -0
- package/dist/src/dom/locale/sw.js +34 -0
- package/dist/src/dom/locale/ta.d.ts +35 -0
- package/dist/src/dom/locale/ta.js +35 -0
- package/dist/src/dom/locale/te.d.ts +35 -0
- package/dist/src/dom/locale/te.js +35 -0
- package/dist/src/dom/locale/th.d.ts +34 -0
- package/dist/src/dom/locale/th.js +34 -0
- package/dist/src/dom/locale/tl.d.ts +28 -0
- package/dist/src/dom/locale/tl.js +28 -0
- package/dist/src/dom/locale/tr.d.ts +34 -0
- package/dist/src/dom/locale/tr.js +34 -0
- package/dist/src/dom/locale/types.d.ts +52 -0
- package/dist/src/dom/locale/types.js +1 -0
- package/dist/src/dom/locale/uk.d.ts +34 -0
- package/dist/src/dom/locale/uk.js +34 -0
- package/dist/src/dom/locale/ur.d.ts +34 -0
- package/dist/src/dom/locale/ur.js +34 -0
- package/dist/src/dom/locale/vi.d.ts +34 -0
- package/dist/src/dom/locale/vi.js +34 -0
- package/dist/src/dom/locale/zh-HK.d.ts +35 -0
- package/dist/src/dom/locale/zh-HK.js +35 -0
- package/dist/src/dom/locale/zh-Hans.d.ts +35 -0
- package/dist/src/dom/locale/zh-Hans.js +35 -0
- package/dist/src/dom/locale/zh-TW.d.ts +36 -0
- package/dist/src/dom/locale/zh-TW.js +36 -0
- package/dist/src/dom/locale-labels.d.ts +26 -0
- package/dist/src/dom/locale-labels.js +26 -0
- package/dist/src/dom/menus.d.ts +6 -0
- package/dist/src/dom/menus.js +47 -10
- package/dist/src/dom/message-format.d.ts +2 -1
- package/dist/src/dom/message-format.js +67 -10
- package/dist/src/dom/messages.d.ts +2 -1
- package/dist/src/dom/messages.js +2 -4
- package/dist/src/dom/selectors.d.ts +1 -0
- package/dist/src/dom/selectors.js +42 -15
- package/dist/src/dom/wait-snapshot.d.ts +43 -0
- package/dist/src/dom/wait-snapshot.js +127 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +5 -0
- package/dist/src/platform/local-paths.d.ts +4 -0
- package/dist/src/platform/local-paths.js +20 -0
- package/dist/src/runner/interruptions.js +15 -4
- package/dist/src/runner/responses.js +18 -1
- package/dist/src/runner/result.js +15 -0
- package/dist/src/runner/resume.js +1 -0
- package/dist/src/runner/types.d.ts +3 -0
- package/dist/src/safety/risk.d.ts +8 -0
- package/dist/src/safety/risk.js +8 -0
- package/dist/src/safety/untrusted-output.d.ts +73 -0
- package/dist/src/safety/untrusted-output.js +199 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +1 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.js +270 -0
- package/dist/src/scripts/capture-intelligence-locales.d.ts +25 -0
- package/dist/src/scripts/capture-intelligence-locales.js +669 -0
- package/dist/src/scripts/check-surface-drift.d.ts +1 -0
- package/dist/src/scripts/check-surface-drift.js +11 -0
- package/dist/src/scripts/live-smoke/scenarios.js +84 -0
- package/dist/src/scripts/locale-capture/language-coverage.d.ts +10 -0
- package/dist/src/scripts/locale-capture/language-coverage.js +39 -0
- package/dist/src/testing/surface-drift.d.ts +66 -0
- package/dist/src/testing/surface-drift.js +363 -0
- package/dist/src/types.d.ts +229 -2
- package/package.json +3 -2
- package/references/backend-protocol.md +96 -2
- package/references/language-coverage.md +142 -0
- package/references/localization.md +257 -0
- package/references/python-parity.md +67 -3
- package/references/safety.md +2 -0
- package/references/troubleshooting.md +83 -2
|
@@ -1,44 +1,200 @@
|
|
|
1
1
|
import { access, readFile, stat } from "node:fs/promises";
|
|
2
|
-
import { basename, resolve } from "node:path";
|
|
3
2
|
import { constants } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { downloadLatestArtifact, locatorCountWithTimeout } from "./artifacts.js";
|
|
4
6
|
import { waitForDownloadFromClick } from "../browser/downloads.js";
|
|
5
7
|
import { resultError, resultOk } from "../errors.js";
|
|
6
8
|
import { addFilesButton, cssSelectors, requiredLocator } from "../dom/selectors.js";
|
|
9
|
+
import { localeLabels } from "../dom/locale-labels.js";
|
|
10
|
+
import { basenameForHostPath, isHostAbsolutePath, resolveForHostPath } from "../platform/local-paths.js";
|
|
7
11
|
import { contextFromPage } from "./context.js";
|
|
8
12
|
import { bootstrap } from "./session.js";
|
|
13
|
+
import { localGuardTimeout } from "./timeouts.js";
|
|
9
14
|
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
15
|
const CHROME_FILE_URL_PERMISSION_FIX = "Chrome chrome://extensions > Codex extension > Details: enable Allow access to file URLs.";
|
|
16
|
+
const DEFAULT_MAX_BYTES_PER_FILE = 512 * 1024 * 1024;
|
|
17
|
+
const DEFAULT_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
|
|
11
18
|
export async function validateAttachPaths(paths) {
|
|
19
|
+
const result = await preflightFiles({}, { paths });
|
|
20
|
+
if (!result.ok || result.data === undefined) {
|
|
21
|
+
throw new Error(result.blocker?.message ?? result.error?.message ?? "File attachment preflight failed.");
|
|
22
|
+
}
|
|
23
|
+
return result.data.files.map(file => ({
|
|
24
|
+
path: file.path,
|
|
25
|
+
name: file.name,
|
|
26
|
+
bytes: file.bytes
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
export async function preflightFiles(env, args) {
|
|
30
|
+
const maxBytesPerFile = args.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;
|
|
31
|
+
const maxTotalBytes = args.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
12
32
|
const files = [];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
33
|
+
const warnings = [];
|
|
34
|
+
for (const [index, inputPath] of args.paths.entries()) {
|
|
35
|
+
const fieldPath = `paths[${index}]`;
|
|
36
|
+
if (!isHostAbsolutePath(inputPath)) {
|
|
37
|
+
return filePreflightBlocker({
|
|
38
|
+
env,
|
|
39
|
+
status: "blocked",
|
|
40
|
+
kind: "upload_failed",
|
|
41
|
+
code: "file_path_not_absolute",
|
|
42
|
+
fieldPath,
|
|
43
|
+
message: `File attachment path must be absolute for the backend host: ${inputPath}`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const absolute = resolveForHostPath(inputPath);
|
|
47
|
+
let fileStat;
|
|
48
|
+
try {
|
|
49
|
+
fileStat = await stat(absolute);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
53
|
+
return filePreflightBlocker({
|
|
54
|
+
env,
|
|
55
|
+
status: "not_found",
|
|
56
|
+
kind: "not_found",
|
|
57
|
+
code: "file_missing",
|
|
58
|
+
fieldPath,
|
|
59
|
+
message: `File attachment path does not exist: ${absolute}`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (isNodeError(error) && (error.code === "EACCES" || error.code === "EPERM")) {
|
|
63
|
+
return filePreflightBlocker({
|
|
64
|
+
env,
|
|
65
|
+
status: "blocked",
|
|
66
|
+
kind: "permission",
|
|
67
|
+
code: "file_not_readable",
|
|
68
|
+
fieldPath,
|
|
69
|
+
message: `File attachment path is not readable: ${absolute}`
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return resultError(error instanceof Error ? error : new Error(String(error)), filePreflightContext(env));
|
|
16
73
|
}
|
|
17
|
-
const absolute = resolve(path);
|
|
18
|
-
await access(absolute, constants.R_OK);
|
|
19
|
-
const fileStat = await stat(absolute);
|
|
20
74
|
if (!fileStat.isFile()) {
|
|
21
|
-
|
|
75
|
+
return filePreflightBlocker({
|
|
76
|
+
env,
|
|
77
|
+
status: "blocked",
|
|
78
|
+
kind: "upload_failed",
|
|
79
|
+
code: fileStat.isDirectory() ? "file_path_is_directory" : "file_path_not_file",
|
|
80
|
+
fieldPath,
|
|
81
|
+
message: `File attachment path is not a file: ${absolute}`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
await access(absolute, constants.R_OK);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return filePreflightBlocker({
|
|
89
|
+
env,
|
|
90
|
+
status: "blocked",
|
|
91
|
+
kind: "permission",
|
|
92
|
+
code: "file_not_readable",
|
|
93
|
+
fieldPath,
|
|
94
|
+
message: `File attachment path is not readable: ${absolute}`
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (fileStat.size > maxBytesPerFile) {
|
|
98
|
+
return filePreflightBlocker({
|
|
99
|
+
env,
|
|
100
|
+
status: "blocked",
|
|
101
|
+
kind: "upload_failed",
|
|
102
|
+
code: "file_too_large",
|
|
103
|
+
fieldPath,
|
|
104
|
+
message: `File attachment exceeds the configured per-file preflight limit: ${absolute} (${fileStat.size}/${maxBytesPerFile} bytes)`
|
|
105
|
+
});
|
|
106
|
+
}
|
|
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
|
+
});
|
|
22
116
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
117
|
+
const metadata = await fileMetadata(absolute, fileStat.size, args.includeHashes === true);
|
|
118
|
+
files.push(metadata);
|
|
119
|
+
}
|
|
120
|
+
const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
121
|
+
if (totalBytes > maxTotalBytes) {
|
|
122
|
+
return filePreflightBlocker({
|
|
123
|
+
env,
|
|
124
|
+
status: "blocked",
|
|
125
|
+
kind: "upload_failed",
|
|
126
|
+
code: "file_total_bytes_exceeded",
|
|
127
|
+
fieldPath: "paths",
|
|
128
|
+
message: `File attachments exceed the configured total preflight limit: ${totalBytes}/${maxTotalBytes} bytes`
|
|
27
129
|
});
|
|
28
130
|
}
|
|
29
|
-
|
|
131
|
+
collectFilePreflightWarnings(files, warnings);
|
|
132
|
+
return resultOk({ files, totalBytes }, filePreflightContext(env), warnings);
|
|
30
133
|
}
|
|
31
134
|
export async function attachFiles(env, args) {
|
|
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);
|
|
140
|
+
if (!preflight.ok || preflight.data === undefined) {
|
|
141
|
+
return preflight;
|
|
142
|
+
}
|
|
32
143
|
const boot = await ensurePage(env);
|
|
33
144
|
if (!boot.ok) {
|
|
34
145
|
return boot;
|
|
35
146
|
}
|
|
36
147
|
const page = env.page;
|
|
37
148
|
try {
|
|
38
|
-
const files =
|
|
149
|
+
const files = preflight.data.files.map(file => ({
|
|
150
|
+
path: file.path,
|
|
151
|
+
name: file.name,
|
|
152
|
+
bytes: file.bytes
|
|
153
|
+
}));
|
|
39
154
|
await uploadFiles(page, files, args.timeoutMs ?? 30000);
|
|
155
|
+
const browserInput = args.includeDiagnostics === true
|
|
156
|
+
? await readBrowserInputDiagnostic(page).catch(() => undefined)
|
|
157
|
+
: undefined;
|
|
40
158
|
await page.waitForTimeout?.(args.timeoutMs === undefined ? 1000 : Math.min(args.timeoutMs, 3000));
|
|
41
|
-
|
|
159
|
+
const readiness = await waitForAttachedFilesReady(page, files, args.timeoutMs ?? 30000);
|
|
160
|
+
if (!readiness.ready) {
|
|
161
|
+
const blocker = {
|
|
162
|
+
kind: "upload_failed",
|
|
163
|
+
code: "attachment_processing",
|
|
164
|
+
message: "ChatGPT still appears to be processing the attached file, so the prompt was not submitted.",
|
|
165
|
+
remediation: [
|
|
166
|
+
{
|
|
167
|
+
label: "Wait for upload",
|
|
168
|
+
instruction: "Wait until the visible attachment finishes uploading or processing, then retry the askWithFiles call.",
|
|
169
|
+
userActionRequired: false
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
label: "Retry smaller file",
|
|
173
|
+
instruction: "If processing never finishes, retry with a smaller file or a different supported file type.",
|
|
174
|
+
userActionRequired: true
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
resumable: true
|
|
178
|
+
};
|
|
179
|
+
if (readiness.processingText !== undefined) {
|
|
180
|
+
blocker.visibleText = readiness.processingText;
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
status: "blocked",
|
|
185
|
+
warnings: [],
|
|
186
|
+
blocker,
|
|
187
|
+
context: await contextFromPage(page)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
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);
|
|
42
198
|
}
|
|
43
199
|
catch (error) {
|
|
44
200
|
if (isUploadBridgeBlocker(error)) {
|
|
@@ -60,6 +216,186 @@ export async function attachFiles(env, args) {
|
|
|
60
216
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
61
217
|
}
|
|
62
218
|
}
|
|
219
|
+
function filePreflightBlocker(args) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
status: args.status,
|
|
223
|
+
warnings: [],
|
|
224
|
+
blocker: {
|
|
225
|
+
kind: args.kind,
|
|
226
|
+
code: args.code,
|
|
227
|
+
fieldPath: args.fieldPath,
|
|
228
|
+
message: args.message,
|
|
229
|
+
resumable: true
|
|
230
|
+
},
|
|
231
|
+
context: filePreflightContext(args.env)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function filePreflightContext(env) {
|
|
235
|
+
return { timestamp: (env.now?.() ?? new Date()).toISOString() };
|
|
236
|
+
}
|
|
237
|
+
async function fileMetadata(absolute, bytes, includeHash = false) {
|
|
238
|
+
const extension = extensionForHostPath(absolute);
|
|
239
|
+
const { mimeType, category } = guessFileType(extension);
|
|
240
|
+
const metadata = {
|
|
241
|
+
path: absolute,
|
|
242
|
+
name: basenameForHostPath(absolute),
|
|
243
|
+
bytes,
|
|
244
|
+
extension,
|
|
245
|
+
mimeType,
|
|
246
|
+
category
|
|
247
|
+
};
|
|
248
|
+
if (includeHash) {
|
|
249
|
+
metadata.sha256 = createHash("sha256").update(await readFile(absolute)).digest("hex");
|
|
250
|
+
}
|
|
251
|
+
return metadata;
|
|
252
|
+
}
|
|
253
|
+
function extensionForHostPath(value) {
|
|
254
|
+
return process.platform === "win32"
|
|
255
|
+
? path.win32.extname(value).toLowerCase()
|
|
256
|
+
: path.posix.extname(value).toLowerCase();
|
|
257
|
+
}
|
|
258
|
+
function collectFilePreflightWarnings(files, warnings) {
|
|
259
|
+
const byPath = new Map();
|
|
260
|
+
const byName = new Map();
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
const pathCount = (byPath.get(file.path) ?? 0) + 1;
|
|
263
|
+
byPath.set(file.path, pathCount);
|
|
264
|
+
if (pathCount === 2) {
|
|
265
|
+
warnings.push(`Duplicate resolved file path requested: ${file.path}`);
|
|
266
|
+
}
|
|
267
|
+
const normalizedName = file.name.toLocaleLowerCase();
|
|
268
|
+
const nameCount = (byName.get(normalizedName) ?? 0) + 1;
|
|
269
|
+
byName.set(normalizedName, nameCount);
|
|
270
|
+
if (nameCount === 2) {
|
|
271
|
+
warnings.push(`Duplicate file basename requested: ${file.name}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function guessFileType(extension) {
|
|
276
|
+
switch (extension) {
|
|
277
|
+
case ".txt":
|
|
278
|
+
return { mimeType: "text/plain", category: "text" };
|
|
279
|
+
case ".md":
|
|
280
|
+
case ".markdown":
|
|
281
|
+
return { mimeType: "text/markdown", category: "text" };
|
|
282
|
+
case ".csv":
|
|
283
|
+
return { mimeType: "text/csv", category: "spreadsheet" };
|
|
284
|
+
case ".tsv":
|
|
285
|
+
return { mimeType: "text/tab-separated-values", category: "spreadsheet" };
|
|
286
|
+
case ".json":
|
|
287
|
+
return { mimeType: "application/json", category: "data" };
|
|
288
|
+
case ".jsonl":
|
|
289
|
+
case ".ndjson":
|
|
290
|
+
return { mimeType: "application/x-ndjson", category: "data" };
|
|
291
|
+
case ".pdf":
|
|
292
|
+
return { mimeType: "application/pdf", category: "document" };
|
|
293
|
+
case ".doc":
|
|
294
|
+
return { mimeType: "application/msword", category: "document" };
|
|
295
|
+
case ".docx":
|
|
296
|
+
return { mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", category: "document" };
|
|
297
|
+
case ".xls":
|
|
298
|
+
return { mimeType: "application/vnd.ms-excel", category: "spreadsheet" };
|
|
299
|
+
case ".xlsx":
|
|
300
|
+
return { mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", category: "spreadsheet" };
|
|
301
|
+
case ".png":
|
|
302
|
+
return { mimeType: "image/png", category: "image" };
|
|
303
|
+
case ".jpg":
|
|
304
|
+
case ".jpeg":
|
|
305
|
+
return { mimeType: "image/jpeg", category: "image" };
|
|
306
|
+
case ".gif":
|
|
307
|
+
return { mimeType: "image/gif", category: "image" };
|
|
308
|
+
case ".webp":
|
|
309
|
+
return { mimeType: "image/webp", category: "image" };
|
|
310
|
+
case ".svg":
|
|
311
|
+
return { mimeType: "image/svg+xml", category: "image" };
|
|
312
|
+
case ".mp3":
|
|
313
|
+
return { mimeType: "audio/mpeg", category: "audio" };
|
|
314
|
+
case ".wav":
|
|
315
|
+
return { mimeType: "audio/wav", category: "audio" };
|
|
316
|
+
case ".mp4":
|
|
317
|
+
return { mimeType: "video/mp4", category: "video" };
|
|
318
|
+
case ".mov":
|
|
319
|
+
return { mimeType: "video/quicktime", category: "video" };
|
|
320
|
+
case ".zip":
|
|
321
|
+
return { mimeType: "application/zip", category: "archive" };
|
|
322
|
+
case ".gz":
|
|
323
|
+
return { mimeType: "application/gzip", category: "archive" };
|
|
324
|
+
default:
|
|
325
|
+
return { mimeType: guessMimeType(extension), category: "unknown" };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
function isNodeError(error) {
|
|
329
|
+
return error instanceof Error && "code" in error;
|
|
330
|
+
}
|
|
331
|
+
async function waitForAttachedFilesReady(page, files, timeoutMs) {
|
|
332
|
+
const started = Date.now();
|
|
333
|
+
let lastProcessingText;
|
|
334
|
+
while (Date.now() - started < timeoutMs) {
|
|
335
|
+
const snapshot = await readAttachmentReadiness(page, files).catch(() => undefined);
|
|
336
|
+
if (snapshot === undefined) {
|
|
337
|
+
return { ready: true };
|
|
338
|
+
}
|
|
339
|
+
const allNamesVisible = snapshot.files.length > 0 && snapshot.files.every(file => file.visible);
|
|
340
|
+
if (!snapshot.processing && allNamesVisible) {
|
|
341
|
+
return { ready: true };
|
|
342
|
+
}
|
|
343
|
+
if (!snapshot.processing && Date.now() - started >= Math.min(timeoutMs, 1000)) {
|
|
344
|
+
return { ready: true };
|
|
345
|
+
}
|
|
346
|
+
if (snapshot.processingText !== undefined) {
|
|
347
|
+
lastProcessingText = snapshot.processingText;
|
|
348
|
+
}
|
|
349
|
+
await page.waitForTimeout?.(250);
|
|
350
|
+
}
|
|
351
|
+
const blocked = { ready: false };
|
|
352
|
+
if (lastProcessingText !== undefined) {
|
|
353
|
+
blocked.processingText = lastProcessingText;
|
|
354
|
+
}
|
|
355
|
+
return blocked;
|
|
356
|
+
}
|
|
357
|
+
async function readAttachmentReadiness(page, files) {
|
|
358
|
+
if (typeof page.evaluate !== "function") {
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
return page.evaluate((fileNames) => {
|
|
362
|
+
const visibleText = document.body?.innerText ?? "";
|
|
363
|
+
const normalize = (value) => value.toLocaleLowerCase();
|
|
364
|
+
const normalizedVisibleText = normalize(visibleText);
|
|
365
|
+
const files = fileNames.map(name => ({
|
|
366
|
+
name,
|
|
367
|
+
visible: normalizedVisibleText.includes(normalize(name))
|
|
368
|
+
}));
|
|
369
|
+
const attachmentSelectors = [
|
|
370
|
+
"[data-testid*='attachment' i]",
|
|
371
|
+
"[data-testid*='file' i]",
|
|
372
|
+
"[aria-label*='attachment' i]",
|
|
373
|
+
"[aria-label*='upload' i]",
|
|
374
|
+
"[aria-label*='file' i]",
|
|
375
|
+
"[class*='attachment' i]",
|
|
376
|
+
"[class*='upload' i]",
|
|
377
|
+
"[class*='file' i]",
|
|
378
|
+
"[role='progressbar']"
|
|
379
|
+
].join(", ");
|
|
380
|
+
const attachmentText = Array.from(document.querySelectorAll(attachmentSelectors))
|
|
381
|
+
.map(element => [
|
|
382
|
+
element.textContent ?? "",
|
|
383
|
+
element.getAttribute("aria-label") ?? "",
|
|
384
|
+
element.getAttribute("title") ?? ""
|
|
385
|
+
].join(" "))
|
|
386
|
+
.join(" ");
|
|
387
|
+
const relevantText = attachmentText.length > 0 ? attachmentText : visibleText;
|
|
388
|
+
const processingMatch = /\b(uploading|processing|attaching|preparing|reading|scanning|analyzing)\b/i.exec(relevantText);
|
|
389
|
+
const snapshot = {
|
|
390
|
+
files,
|
|
391
|
+
processing: processingMatch !== null
|
|
392
|
+
};
|
|
393
|
+
if (processingMatch !== null) {
|
|
394
|
+
snapshot.processingText = relevantText.slice(0, 500);
|
|
395
|
+
}
|
|
396
|
+
return snapshot;
|
|
397
|
+
}, files.map(file => file.name));
|
|
398
|
+
}
|
|
63
399
|
async function uploadFiles(page, files, timeoutMs) {
|
|
64
400
|
const paths = files.map(file => file.path);
|
|
65
401
|
const errors = [];
|
|
@@ -101,7 +437,10 @@ async function uploadFiles(page, files, timeoutMs) {
|
|
|
101
437
|
throw new Error(`No ChatGPT upload path completed.\n${errors.join("\n")}`);
|
|
102
438
|
}
|
|
103
439
|
async function clickChatGPTAddPhotosMenuItem(page, paths, timeoutMs) {
|
|
104
|
-
|
|
440
|
+
// The `#composer-plus-btn` id is the language-agnostic primary; the aria-label and the
|
|
441
|
+
// menu-item text are locale-sensitive (menu text sourced from the locale registry).
|
|
442
|
+
const addPhotosFilesText = localeLabels.addPhotosFilesMenuItem[0];
|
|
443
|
+
const menuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: addPhotosFilesText });
|
|
105
444
|
if (await locatorCount(menuItem) !== 1) {
|
|
106
445
|
const plusButton = requiredLocator(page, "#composer-plus-btn, button[aria-label='Add files and more']");
|
|
107
446
|
if (await locatorCount(plusButton) !== 1) {
|
|
@@ -110,7 +449,7 @@ async function clickChatGPTAddPhotosMenuItem(page, paths, timeoutMs) {
|
|
|
110
449
|
await plusButton.click?.({ timeoutMs: Math.min(timeoutMs, 10000) });
|
|
111
450
|
await page.waitForTimeout?.(250);
|
|
112
451
|
}
|
|
113
|
-
const refreshedMenuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText:
|
|
452
|
+
const refreshedMenuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: addPhotosFilesText });
|
|
114
453
|
await clickFileChooserLocator(page, refreshedMenuItem, paths, timeoutMs);
|
|
115
454
|
}
|
|
116
455
|
async function clickFileChooserTarget(page, selector, paths, timeoutMs, options = {}) {
|
|
@@ -188,8 +527,29 @@ export async function downloadLatestFile(env, args) {
|
|
|
188
527
|
const page = env.page;
|
|
189
528
|
try {
|
|
190
529
|
const controls = requiredLocator(page, cssSelectors.downloadControls);
|
|
191
|
-
|
|
530
|
+
let count;
|
|
531
|
+
try {
|
|
532
|
+
count = await locatorCountWithTimeout(controls, localGuardTimeout(args.timeoutMs, 5000), "download_control_timeout");
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
return {
|
|
536
|
+
ok: false,
|
|
537
|
+
status: "unsupported",
|
|
538
|
+
warnings: [],
|
|
539
|
+
blocker: {
|
|
540
|
+
kind: "download_unavailable",
|
|
541
|
+
code: "download_control_timeout",
|
|
542
|
+
message: `No visible ChatGPT download control could be counted before the local guard timeout: ${error instanceof Error ? error.message : String(error)}`,
|
|
543
|
+
resumable: true
|
|
544
|
+
},
|
|
545
|
+
context: await contextFromPage(page)
|
|
546
|
+
};
|
|
547
|
+
}
|
|
192
548
|
if (count === 0) {
|
|
549
|
+
const artifactDownload = await downloadLatestArtifact(env, args);
|
|
550
|
+
if (artifactDownload.ok) {
|
|
551
|
+
return artifactDownload;
|
|
552
|
+
}
|
|
193
553
|
return {
|
|
194
554
|
ok: false,
|
|
195
555
|
status: "unsupported",
|
|
@@ -222,6 +582,34 @@ async function setHiddenFileInput(page, files) {
|
|
|
222
582
|
}
|
|
223
583
|
await input.setInputFiles(files.map(file => file.path));
|
|
224
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
|
+
}
|
|
225
613
|
async function ensurePage(env) {
|
|
226
614
|
if (env.page !== undefined) {
|
|
227
615
|
return resultOk({}, await contextFromPage(env.page));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { type AssistantGenerationState } from "../dom/generation-state.js";
|
|
1
2
|
import type { AskArgs, AskReadData, CommandResult, ComposeArgs, ComposeData, ReadLatestArgs, ReadLatestData, RuntimeEnv, SubmitArgs, SubmitData, WaitAndReadArgs, WaitArgs, WaitData } from "../types.js";
|
|
2
3
|
export type CompletionSnapshot = {
|
|
3
4
|
textStableForMs: number;
|
|
4
5
|
stableMs: number;
|
|
5
|
-
|
|
6
|
+
generation: AssistantGenerationState;
|
|
6
7
|
hasResponseActions: boolean;
|
|
7
8
|
latestText: string;
|
|
8
9
|
};
|