codex-chatgpt-control 0.1.0-alpha.1 → 0.2.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 +6 -0
- package/README.md +55 -0
- package/contracts/v1/fixtures/backend-capabilities.json +7 -0
- package/contracts/v1/fixtures/blocker-explanation-profiles.json +114 -0
- package/contracts/v1/fixtures/command-descriptors.json +192 -15
- package/contracts/v1/fixtures/doctor-bridge-upload.json +8 -0
- package/contracts/v1/fixtures/doctor-scenario-preflight.json +108 -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 +72 -6
- 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 +5137 -488
- package/dist/codex-chatgpt-control.bundle.mjs +5001 -286
- 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 +7 -0
- package/dist/src/backend/session.js +14 -0
- package/dist/src/browser/attach.js +128 -20
- package/dist/src/browser/downloads.js +2 -1
- package/dist/src/browser/page-state.js +6 -3
- package/dist/src/client.d.ts +16 -1
- package/dist/src/client.js +86 -7
- package/dist/src/commands/artifacts.d.ts +5 -0
- package/dist/src/commands/artifacts.js +432 -0
- package/dist/src/commands/context.js +5 -2
- package/dist/src/commands/doctor.d.ts +10 -2
- package/dist/src/commands/doctor.js +242 -14
- package/dist/src/commands/files.d.ts +2 -1
- package/dist/src/commands/files.js +352 -18
- package/dist/src/commands/messages.d.ts +2 -1
- package/dist/src/commands/messages.js +232 -40
- package/dist/src/commands/modes.js +202 -20
- 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 +76 -14
- package/dist/src/commands/reports.d.ts +9 -0
- package/dist/src/commands/reports.js +83 -6
- package/dist/src/commands/response-actions.js +33 -0
- package/dist/src/commands/sequence.js +14 -0
- package/dist/src/commands/threads.js +2 -1
- 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 +8 -0
- package/dist/src/dom/generation-state.js +103 -0
- package/dist/src/dom/locale/am.d.ts +28 -0
- package/dist/src/dom/locale/am.js +28 -0
- package/dist/src/dom/locale/ar.d.ts +28 -0
- package/dist/src/dom/locale/ar.js +28 -0
- package/dist/src/dom/locale/bg.d.ts +28 -0
- package/dist/src/dom/locale/bg.js +28 -0
- package/dist/src/dom/locale/bn.d.ts +28 -0
- package/dist/src/dom/locale/bn.js +28 -0
- package/dist/src/dom/locale/bs.d.ts +28 -0
- package/dist/src/dom/locale/bs.js +28 -0
- package/dist/src/dom/locale/ca.d.ts +28 -0
- package/dist/src/dom/locale/ca.js +28 -0
- package/dist/src/dom/locale/cs.d.ts +28 -0
- package/dist/src/dom/locale/cs.js +28 -0
- package/dist/src/dom/locale/da.d.ts +28 -0
- package/dist/src/dom/locale/da.js +28 -0
- package/dist/src/dom/locale/de.d.ts +30 -0
- package/dist/src/dom/locale/de.js +30 -0
- package/dist/src/dom/locale/el.d.ts +28 -0
- package/dist/src/dom/locale/el.js +28 -0
- package/dist/src/dom/locale/en.d.ts +50 -0
- package/dist/src/dom/locale/en.js +56 -0
- package/dist/src/dom/locale/es-419.d.ts +28 -0
- package/dist/src/dom/locale/es-419.js +28 -0
- package/dist/src/dom/locale/es-ES.d.ts +28 -0
- package/dist/src/dom/locale/es-ES.js +28 -0
- package/dist/src/dom/locale/et.d.ts +28 -0
- package/dist/src/dom/locale/et.js +28 -0
- package/dist/src/dom/locale/fa.d.ts +28 -0
- package/dist/src/dom/locale/fa.js +28 -0
- package/dist/src/dom/locale/fi.d.ts +28 -0
- package/dist/src/dom/locale/fi.js +28 -0
- package/dist/src/dom/locale/fr-CA.d.ts +29 -0
- package/dist/src/dom/locale/fr-CA.js +29 -0
- package/dist/src/dom/locale/fr-FR.d.ts +30 -0
- package/dist/src/dom/locale/fr-FR.js +30 -0
- package/dist/src/dom/locale/gu.d.ts +28 -0
- package/dist/src/dom/locale/gu.js +28 -0
- package/dist/src/dom/locale/hi.d.ts +28 -0
- package/dist/src/dom/locale/hi.js +28 -0
- package/dist/src/dom/locale/hr.d.ts +28 -0
- package/dist/src/dom/locale/hr.js +28 -0
- package/dist/src/dom/locale/hu.d.ts +28 -0
- package/dist/src/dom/locale/hu.js +28 -0
- package/dist/src/dom/locale/hy.d.ts +28 -0
- package/dist/src/dom/locale/hy.js +28 -0
- package/dist/src/dom/locale/id.d.ts +28 -0
- package/dist/src/dom/locale/id.js +28 -0
- package/dist/src/dom/locale/index.d.ts +79 -0
- package/dist/src/dom/locale/index.js +202 -0
- package/dist/src/dom/locale/is.d.ts +28 -0
- package/dist/src/dom/locale/is.js +28 -0
- package/dist/src/dom/locale/it.d.ts +27 -0
- package/dist/src/dom/locale/it.js +27 -0
- package/dist/src/dom/locale/ja.d.ts +27 -0
- package/dist/src/dom/locale/ja.js +27 -0
- package/dist/src/dom/locale/ka.d.ts +28 -0
- package/dist/src/dom/locale/ka.js +28 -0
- package/dist/src/dom/locale/kk.d.ts +28 -0
- package/dist/src/dom/locale/kk.js +28 -0
- package/dist/src/dom/locale/kn.d.ts +28 -0
- package/dist/src/dom/locale/kn.js +28 -0
- package/dist/src/dom/locale/ko.d.ts +28 -0
- package/dist/src/dom/locale/ko.js +28 -0
- package/dist/src/dom/locale/lt.d.ts +28 -0
- package/dist/src/dom/locale/lt.js +28 -0
- package/dist/src/dom/locale/lv.d.ts +28 -0
- package/dist/src/dom/locale/lv.js +28 -0
- package/dist/src/dom/locale/mk.d.ts +28 -0
- package/dist/src/dom/locale/mk.js +28 -0
- package/dist/src/dom/locale/ml.d.ts +28 -0
- package/dist/src/dom/locale/ml.js +28 -0
- package/dist/src/dom/locale/mn.d.ts +28 -0
- package/dist/src/dom/locale/mn.js +28 -0
- package/dist/src/dom/locale/mr.d.ts +28 -0
- package/dist/src/dom/locale/mr.js +28 -0
- package/dist/src/dom/locale/ms.d.ts +28 -0
- package/dist/src/dom/locale/ms.js +28 -0
- package/dist/src/dom/locale/my.d.ts +28 -0
- package/dist/src/dom/locale/my.js +28 -0
- package/dist/src/dom/locale/nb.d.ts +28 -0
- package/dist/src/dom/locale/nb.js +28 -0
- package/dist/src/dom/locale/nl.d.ts +28 -0
- package/dist/src/dom/locale/nl.js +28 -0
- package/dist/src/dom/locale/pa.d.ts +28 -0
- package/dist/src/dom/locale/pa.js +28 -0
- package/dist/src/dom/locale/pl.d.ts +28 -0
- package/dist/src/dom/locale/pl.js +28 -0
- package/dist/src/dom/locale/pt-BR.d.ts +28 -0
- package/dist/src/dom/locale/pt-BR.js +28 -0
- package/dist/src/dom/locale/pt-PT.d.ts +28 -0
- package/dist/src/dom/locale/pt-PT.js +28 -0
- package/dist/src/dom/locale/ro.d.ts +28 -0
- package/dist/src/dom/locale/ro.js +28 -0
- package/dist/src/dom/locale/ru.d.ts +28 -0
- package/dist/src/dom/locale/ru.js +28 -0
- package/dist/src/dom/locale/sk.d.ts +28 -0
- package/dist/src/dom/locale/sk.js +28 -0
- package/dist/src/dom/locale/sl.d.ts +28 -0
- package/dist/src/dom/locale/sl.js +28 -0
- package/dist/src/dom/locale/so.d.ts +28 -0
- package/dist/src/dom/locale/so.js +28 -0
- package/dist/src/dom/locale/sq.d.ts +28 -0
- package/dist/src/dom/locale/sq.js +28 -0
- package/dist/src/dom/locale/sr.d.ts +28 -0
- package/dist/src/dom/locale/sr.js +28 -0
- package/dist/src/dom/locale/sv.d.ts +28 -0
- package/dist/src/dom/locale/sv.js +28 -0
- package/dist/src/dom/locale/sw.d.ts +28 -0
- package/dist/src/dom/locale/sw.js +28 -0
- package/dist/src/dom/locale/ta.d.ts +28 -0
- package/dist/src/dom/locale/ta.js +28 -0
- package/dist/src/dom/locale/te.d.ts +28 -0
- package/dist/src/dom/locale/te.js +28 -0
- package/dist/src/dom/locale/th.d.ts +28 -0
- package/dist/src/dom/locale/th.js +28 -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 +28 -0
- package/dist/src/dom/locale/tr.js +28 -0
- package/dist/src/dom/locale/types.d.ts +45 -0
- package/dist/src/dom/locale/types.js +1 -0
- package/dist/src/dom/locale/uk.d.ts +28 -0
- package/dist/src/dom/locale/uk.js +28 -0
- package/dist/src/dom/locale/ur.d.ts +28 -0
- package/dist/src/dom/locale/ur.js +28 -0
- package/dist/src/dom/locale/vi.d.ts +28 -0
- package/dist/src/dom/locale/vi.js +28 -0
- package/dist/src/dom/locale/zh-HK.d.ts +28 -0
- package/dist/src/dom/locale/zh-HK.js +28 -0
- package/dist/src/dom/locale/zh-Hans.d.ts +28 -0
- package/dist/src/dom/locale/zh-Hans.js +28 -0
- package/dist/src/dom/locale/zh-TW.d.ts +29 -0
- package/dist/src/dom/locale/zh-TW.js +29 -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 +34 -8
- 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/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 +7 -0
- package/dist/src/safety/risk.js +7 -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 +184 -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 +201 -2
- package/package.json +2 -1
- package/references/backend-protocol.md +93 -1
- package/references/language-coverage.md +142 -0
- package/references/localization.md +247 -0
- package/references/python-parity.md +63 -1
- package/references/safety.md +2 -0
- package/references/troubleshooting.md +58 -0
|
@@ -1,44 +1,175 @@
|
|
|
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 path from "node:path";
|
|
4
|
+
import { downloadLatestArtifact, locatorCountWithTimeout } from "./artifacts.js";
|
|
4
5
|
import { waitForDownloadFromClick } from "../browser/downloads.js";
|
|
5
6
|
import { resultError, resultOk } from "../errors.js";
|
|
6
7
|
import { addFilesButton, cssSelectors, requiredLocator } from "../dom/selectors.js";
|
|
8
|
+
import { localeLabels } from "../dom/locale-labels.js";
|
|
9
|
+
import { basenameForHostPath, isHostAbsolutePath, resolveForHostPath } from "../platform/local-paths.js";
|
|
7
10
|
import { contextFromPage } from "./context.js";
|
|
8
11
|
import { bootstrap } from "./session.js";
|
|
12
|
+
import { localGuardTimeout } from "./timeouts.js";
|
|
9
13
|
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
14
|
const CHROME_FILE_URL_PERMISSION_FIX = "Chrome chrome://extensions > Codex extension > Details: enable Allow access to file URLs.";
|
|
15
|
+
const DEFAULT_MAX_BYTES_PER_FILE = 512 * 1024 * 1024;
|
|
16
|
+
const DEFAULT_MAX_TOTAL_BYTES = 2 * 1024 * 1024 * 1024;
|
|
11
17
|
export async function validateAttachPaths(paths) {
|
|
18
|
+
const result = await preflightFiles({}, { paths });
|
|
19
|
+
if (!result.ok || result.data === undefined) {
|
|
20
|
+
throw new Error(result.blocker?.message ?? result.error?.message ?? "File attachment preflight failed.");
|
|
21
|
+
}
|
|
22
|
+
return result.data.files.map(file => ({
|
|
23
|
+
path: file.path,
|
|
24
|
+
name: file.name,
|
|
25
|
+
bytes: file.bytes
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
export async function preflightFiles(env, args) {
|
|
29
|
+
const maxBytesPerFile = args.maxBytesPerFile ?? DEFAULT_MAX_BYTES_PER_FILE;
|
|
30
|
+
const maxTotalBytes = args.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
12
31
|
const files = [];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
32
|
+
const warnings = [];
|
|
33
|
+
for (const [index, inputPath] of args.paths.entries()) {
|
|
34
|
+
const fieldPath = `paths[${index}]`;
|
|
35
|
+
if (!isHostAbsolutePath(inputPath)) {
|
|
36
|
+
return filePreflightBlocker({
|
|
37
|
+
env,
|
|
38
|
+
status: "blocked",
|
|
39
|
+
kind: "upload_failed",
|
|
40
|
+
code: "file_path_not_absolute",
|
|
41
|
+
fieldPath,
|
|
42
|
+
message: `File attachment path must be absolute for the backend host: ${inputPath}`
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const absolute = resolveForHostPath(inputPath);
|
|
46
|
+
let fileStat;
|
|
47
|
+
try {
|
|
48
|
+
fileStat = await stat(absolute);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
52
|
+
return filePreflightBlocker({
|
|
53
|
+
env,
|
|
54
|
+
status: "not_found",
|
|
55
|
+
kind: "not_found",
|
|
56
|
+
code: "file_missing",
|
|
57
|
+
fieldPath,
|
|
58
|
+
message: `File attachment path does not exist: ${absolute}`
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (isNodeError(error) && (error.code === "EACCES" || error.code === "EPERM")) {
|
|
62
|
+
return filePreflightBlocker({
|
|
63
|
+
env,
|
|
64
|
+
status: "blocked",
|
|
65
|
+
kind: "permission",
|
|
66
|
+
code: "file_not_readable",
|
|
67
|
+
fieldPath,
|
|
68
|
+
message: `File attachment path is not readable: ${absolute}`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return resultError(error instanceof Error ? error : new Error(String(error)), filePreflightContext(env));
|
|
16
72
|
}
|
|
17
|
-
const absolute = resolve(path);
|
|
18
|
-
await access(absolute, constants.R_OK);
|
|
19
|
-
const fileStat = await stat(absolute);
|
|
20
73
|
if (!fileStat.isFile()) {
|
|
21
|
-
|
|
74
|
+
return filePreflightBlocker({
|
|
75
|
+
env,
|
|
76
|
+
status: "blocked",
|
|
77
|
+
kind: "upload_failed",
|
|
78
|
+
code: fileStat.isDirectory() ? "file_path_is_directory" : "file_path_not_file",
|
|
79
|
+
fieldPath,
|
|
80
|
+
message: `File attachment path is not a file: ${absolute}`
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
await access(absolute, constants.R_OK);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return filePreflightBlocker({
|
|
88
|
+
env,
|
|
89
|
+
status: "blocked",
|
|
90
|
+
kind: "permission",
|
|
91
|
+
code: "file_not_readable",
|
|
92
|
+
fieldPath,
|
|
93
|
+
message: `File attachment path is not readable: ${absolute}`
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (fileStat.size > maxBytesPerFile) {
|
|
97
|
+
return filePreflightBlocker({
|
|
98
|
+
env,
|
|
99
|
+
status: "blocked",
|
|
100
|
+
kind: "upload_failed",
|
|
101
|
+
code: "file_too_large",
|
|
102
|
+
fieldPath,
|
|
103
|
+
message: `File attachment exceeds the configured per-file preflight limit: ${absolute} (${fileStat.size}/${maxBytesPerFile} bytes)`
|
|
104
|
+
});
|
|
22
105
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
106
|
+
const metadata = fileMetadata(absolute, fileStat.size);
|
|
107
|
+
files.push(metadata);
|
|
108
|
+
}
|
|
109
|
+
const totalBytes = files.reduce((sum, file) => sum + file.bytes, 0);
|
|
110
|
+
if (totalBytes > maxTotalBytes) {
|
|
111
|
+
return filePreflightBlocker({
|
|
112
|
+
env,
|
|
113
|
+
status: "blocked",
|
|
114
|
+
kind: "upload_failed",
|
|
115
|
+
code: "file_total_bytes_exceeded",
|
|
116
|
+
fieldPath: "paths",
|
|
117
|
+
message: `File attachments exceed the configured total preflight limit: ${totalBytes}/${maxTotalBytes} bytes`
|
|
27
118
|
});
|
|
28
119
|
}
|
|
29
|
-
|
|
120
|
+
collectFilePreflightWarnings(files, warnings);
|
|
121
|
+
return resultOk({ files, totalBytes }, filePreflightContext(env), warnings);
|
|
30
122
|
}
|
|
31
123
|
export async function attachFiles(env, args) {
|
|
124
|
+
const preflight = await preflightFiles(env, { paths: args.paths });
|
|
125
|
+
if (!preflight.ok || preflight.data === undefined) {
|
|
126
|
+
return preflight;
|
|
127
|
+
}
|
|
32
128
|
const boot = await ensurePage(env);
|
|
33
129
|
if (!boot.ok) {
|
|
34
130
|
return boot;
|
|
35
131
|
}
|
|
36
132
|
const page = env.page;
|
|
37
133
|
try {
|
|
38
|
-
const files =
|
|
134
|
+
const files = preflight.data.files.map(file => ({
|
|
135
|
+
path: file.path,
|
|
136
|
+
name: file.name,
|
|
137
|
+
bytes: file.bytes
|
|
138
|
+
}));
|
|
39
139
|
await uploadFiles(page, files, args.timeoutMs ?? 30000);
|
|
40
140
|
await page.waitForTimeout?.(args.timeoutMs === undefined ? 1000 : Math.min(args.timeoutMs, 3000));
|
|
41
|
-
|
|
141
|
+
const readiness = await waitForAttachedFilesReady(page, files, args.timeoutMs ?? 30000);
|
|
142
|
+
if (!readiness.ready) {
|
|
143
|
+
const blocker = {
|
|
144
|
+
kind: "upload_failed",
|
|
145
|
+
code: "attachment_processing",
|
|
146
|
+
message: "ChatGPT still appears to be processing the attached file, so the prompt was not submitted.",
|
|
147
|
+
remediation: [
|
|
148
|
+
{
|
|
149
|
+
label: "Wait for upload",
|
|
150
|
+
instruction: "Wait until the visible attachment finishes uploading or processing, then retry the askWithFiles call.",
|
|
151
|
+
userActionRequired: false
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
label: "Retry smaller file",
|
|
155
|
+
instruction: "If processing never finishes, retry with a smaller file or a different supported file type.",
|
|
156
|
+
userActionRequired: true
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
resumable: true
|
|
160
|
+
};
|
|
161
|
+
if (readiness.processingText !== undefined) {
|
|
162
|
+
blocker.visibleText = readiness.processingText;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
status: "blocked",
|
|
167
|
+
warnings: [],
|
|
168
|
+
blocker,
|
|
169
|
+
context: await contextFromPage(page)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return resultOk({ files }, await contextFromPage(page), preflight.warnings);
|
|
42
173
|
}
|
|
43
174
|
catch (error) {
|
|
44
175
|
if (isUploadBridgeBlocker(error)) {
|
|
@@ -60,6 +191,185 @@ export async function attachFiles(env, args) {
|
|
|
60
191
|
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
61
192
|
}
|
|
62
193
|
}
|
|
194
|
+
function filePreflightBlocker(args) {
|
|
195
|
+
return {
|
|
196
|
+
ok: false,
|
|
197
|
+
status: args.status,
|
|
198
|
+
warnings: [],
|
|
199
|
+
blocker: {
|
|
200
|
+
kind: args.kind,
|
|
201
|
+
code: args.code,
|
|
202
|
+
fieldPath: args.fieldPath,
|
|
203
|
+
message: args.message,
|
|
204
|
+
resumable: true
|
|
205
|
+
},
|
|
206
|
+
context: filePreflightContext(args.env)
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function filePreflightContext(env) {
|
|
210
|
+
return { timestamp: (env.now?.() ?? new Date()).toISOString() };
|
|
211
|
+
}
|
|
212
|
+
function fileMetadata(absolute, bytes) {
|
|
213
|
+
const extension = extensionForHostPath(absolute);
|
|
214
|
+
const { mimeType, category } = guessFileType(extension);
|
|
215
|
+
return {
|
|
216
|
+
path: absolute,
|
|
217
|
+
name: basenameForHostPath(absolute),
|
|
218
|
+
bytes,
|
|
219
|
+
extension,
|
|
220
|
+
mimeType,
|
|
221
|
+
category
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function extensionForHostPath(value) {
|
|
225
|
+
return process.platform === "win32"
|
|
226
|
+
? path.win32.extname(value).toLowerCase()
|
|
227
|
+
: path.posix.extname(value).toLowerCase();
|
|
228
|
+
}
|
|
229
|
+
function collectFilePreflightWarnings(files, warnings) {
|
|
230
|
+
const byPath = new Map();
|
|
231
|
+
const byName = new Map();
|
|
232
|
+
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
|
+
const pathCount = (byPath.get(file.path) ?? 0) + 1;
|
|
237
|
+
byPath.set(file.path, pathCount);
|
|
238
|
+
if (pathCount === 2) {
|
|
239
|
+
warnings.push(`Duplicate resolved file path requested: ${file.path}`);
|
|
240
|
+
}
|
|
241
|
+
const normalizedName = file.name.toLocaleLowerCase();
|
|
242
|
+
const nameCount = (byName.get(normalizedName) ?? 0) + 1;
|
|
243
|
+
byName.set(normalizedName, nameCount);
|
|
244
|
+
if (nameCount === 2) {
|
|
245
|
+
warnings.push(`Duplicate file basename requested: ${file.name}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function guessFileType(extension) {
|
|
250
|
+
switch (extension) {
|
|
251
|
+
case ".txt":
|
|
252
|
+
return { mimeType: "text/plain", category: "text" };
|
|
253
|
+
case ".md":
|
|
254
|
+
case ".markdown":
|
|
255
|
+
return { mimeType: "text/markdown", category: "text" };
|
|
256
|
+
case ".csv":
|
|
257
|
+
return { mimeType: "text/csv", category: "spreadsheet" };
|
|
258
|
+
case ".tsv":
|
|
259
|
+
return { mimeType: "text/tab-separated-values", category: "spreadsheet" };
|
|
260
|
+
case ".json":
|
|
261
|
+
return { mimeType: "application/json", category: "data" };
|
|
262
|
+
case ".jsonl":
|
|
263
|
+
case ".ndjson":
|
|
264
|
+
return { mimeType: "application/x-ndjson", category: "data" };
|
|
265
|
+
case ".pdf":
|
|
266
|
+
return { mimeType: "application/pdf", category: "document" };
|
|
267
|
+
case ".doc":
|
|
268
|
+
return { mimeType: "application/msword", category: "document" };
|
|
269
|
+
case ".docx":
|
|
270
|
+
return { mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", category: "document" };
|
|
271
|
+
case ".xls":
|
|
272
|
+
return { mimeType: "application/vnd.ms-excel", category: "spreadsheet" };
|
|
273
|
+
case ".xlsx":
|
|
274
|
+
return { mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", category: "spreadsheet" };
|
|
275
|
+
case ".png":
|
|
276
|
+
return { mimeType: "image/png", category: "image" };
|
|
277
|
+
case ".jpg":
|
|
278
|
+
case ".jpeg":
|
|
279
|
+
return { mimeType: "image/jpeg", category: "image" };
|
|
280
|
+
case ".gif":
|
|
281
|
+
return { mimeType: "image/gif", category: "image" };
|
|
282
|
+
case ".webp":
|
|
283
|
+
return { mimeType: "image/webp", category: "image" };
|
|
284
|
+
case ".svg":
|
|
285
|
+
return { mimeType: "image/svg+xml", category: "image" };
|
|
286
|
+
case ".mp3":
|
|
287
|
+
return { mimeType: "audio/mpeg", category: "audio" };
|
|
288
|
+
case ".wav":
|
|
289
|
+
return { mimeType: "audio/wav", category: "audio" };
|
|
290
|
+
case ".mp4":
|
|
291
|
+
return { mimeType: "video/mp4", category: "video" };
|
|
292
|
+
case ".mov":
|
|
293
|
+
return { mimeType: "video/quicktime", category: "video" };
|
|
294
|
+
case ".zip":
|
|
295
|
+
return { mimeType: "application/zip", category: "archive" };
|
|
296
|
+
case ".gz":
|
|
297
|
+
return { mimeType: "application/gzip", category: "archive" };
|
|
298
|
+
default:
|
|
299
|
+
return { mimeType: guessMimeType(extension), category: "unknown" };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function isNodeError(error) {
|
|
303
|
+
return error instanceof Error && "code" in error;
|
|
304
|
+
}
|
|
305
|
+
async function waitForAttachedFilesReady(page, files, timeoutMs) {
|
|
306
|
+
const started = Date.now();
|
|
307
|
+
let lastProcessingText;
|
|
308
|
+
while (Date.now() - started < timeoutMs) {
|
|
309
|
+
const snapshot = await readAttachmentReadiness(page, files).catch(() => undefined);
|
|
310
|
+
if (snapshot === undefined) {
|
|
311
|
+
return { ready: true };
|
|
312
|
+
}
|
|
313
|
+
const allNamesVisible = snapshot.files.length > 0 && snapshot.files.every(file => file.visible);
|
|
314
|
+
if (!snapshot.processing && allNamesVisible) {
|
|
315
|
+
return { ready: true };
|
|
316
|
+
}
|
|
317
|
+
if (!snapshot.processing && Date.now() - started >= Math.min(timeoutMs, 1000)) {
|
|
318
|
+
return { ready: true };
|
|
319
|
+
}
|
|
320
|
+
if (snapshot.processingText !== undefined) {
|
|
321
|
+
lastProcessingText = snapshot.processingText;
|
|
322
|
+
}
|
|
323
|
+
await page.waitForTimeout?.(250);
|
|
324
|
+
}
|
|
325
|
+
const blocked = { ready: false };
|
|
326
|
+
if (lastProcessingText !== undefined) {
|
|
327
|
+
blocked.processingText = lastProcessingText;
|
|
328
|
+
}
|
|
329
|
+
return blocked;
|
|
330
|
+
}
|
|
331
|
+
async function readAttachmentReadiness(page, files) {
|
|
332
|
+
if (typeof page.evaluate !== "function") {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
return page.evaluate((fileNames) => {
|
|
336
|
+
const visibleText = document.body?.innerText ?? "";
|
|
337
|
+
const normalize = (value) => value.toLocaleLowerCase();
|
|
338
|
+
const normalizedVisibleText = normalize(visibleText);
|
|
339
|
+
const files = fileNames.map(name => ({
|
|
340
|
+
name,
|
|
341
|
+
visible: normalizedVisibleText.includes(normalize(name))
|
|
342
|
+
}));
|
|
343
|
+
const attachmentSelectors = [
|
|
344
|
+
"[data-testid*='attachment' i]",
|
|
345
|
+
"[data-testid*='file' i]",
|
|
346
|
+
"[aria-label*='attachment' i]",
|
|
347
|
+
"[aria-label*='upload' i]",
|
|
348
|
+
"[aria-label*='file' i]",
|
|
349
|
+
"[class*='attachment' i]",
|
|
350
|
+
"[class*='upload' i]",
|
|
351
|
+
"[class*='file' i]",
|
|
352
|
+
"[role='progressbar']"
|
|
353
|
+
].join(", ");
|
|
354
|
+
const attachmentText = Array.from(document.querySelectorAll(attachmentSelectors))
|
|
355
|
+
.map(element => [
|
|
356
|
+
element.textContent ?? "",
|
|
357
|
+
element.getAttribute("aria-label") ?? "",
|
|
358
|
+
element.getAttribute("title") ?? ""
|
|
359
|
+
].join(" "))
|
|
360
|
+
.join(" ");
|
|
361
|
+
const relevantText = attachmentText.length > 0 ? attachmentText : visibleText;
|
|
362
|
+
const processingMatch = /\b(uploading|processing|attaching|preparing|reading|scanning|analyzing)\b/i.exec(relevantText);
|
|
363
|
+
const snapshot = {
|
|
364
|
+
files,
|
|
365
|
+
processing: processingMatch !== null
|
|
366
|
+
};
|
|
367
|
+
if (processingMatch !== null) {
|
|
368
|
+
snapshot.processingText = relevantText.slice(0, 500);
|
|
369
|
+
}
|
|
370
|
+
return snapshot;
|
|
371
|
+
}, files.map(file => file.name));
|
|
372
|
+
}
|
|
63
373
|
async function uploadFiles(page, files, timeoutMs) {
|
|
64
374
|
const paths = files.map(file => file.path);
|
|
65
375
|
const errors = [];
|
|
@@ -101,7 +411,10 @@ async function uploadFiles(page, files, timeoutMs) {
|
|
|
101
411
|
throw new Error(`No ChatGPT upload path completed.\n${errors.join("\n")}`);
|
|
102
412
|
}
|
|
103
413
|
async function clickChatGPTAddPhotosMenuItem(page, paths, timeoutMs) {
|
|
104
|
-
|
|
414
|
+
// The `#composer-plus-btn` id is the language-agnostic primary; the aria-label and the
|
|
415
|
+
// menu-item text are locale-sensitive (menu text sourced from the locale registry).
|
|
416
|
+
const addPhotosFilesText = localeLabels.addPhotosFilesMenuItem[0];
|
|
417
|
+
const menuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: addPhotosFilesText });
|
|
105
418
|
if (await locatorCount(menuItem) !== 1) {
|
|
106
419
|
const plusButton = requiredLocator(page, "#composer-plus-btn, button[aria-label='Add files and more']");
|
|
107
420
|
if (await locatorCount(plusButton) !== 1) {
|
|
@@ -110,7 +423,7 @@ async function clickChatGPTAddPhotosMenuItem(page, paths, timeoutMs) {
|
|
|
110
423
|
await plusButton.click?.({ timeoutMs: Math.min(timeoutMs, 10000) });
|
|
111
424
|
await page.waitForTimeout?.(250);
|
|
112
425
|
}
|
|
113
|
-
const refreshedMenuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText:
|
|
426
|
+
const refreshedMenuItem = requiredLocator(page, "div[role='menuitem']").filter?.({ hasText: addPhotosFilesText });
|
|
114
427
|
await clickFileChooserLocator(page, refreshedMenuItem, paths, timeoutMs);
|
|
115
428
|
}
|
|
116
429
|
async function clickFileChooserTarget(page, selector, paths, timeoutMs, options = {}) {
|
|
@@ -188,8 +501,29 @@ export async function downloadLatestFile(env, args) {
|
|
|
188
501
|
const page = env.page;
|
|
189
502
|
try {
|
|
190
503
|
const controls = requiredLocator(page, cssSelectors.downloadControls);
|
|
191
|
-
|
|
504
|
+
let count;
|
|
505
|
+
try {
|
|
506
|
+
count = await locatorCountWithTimeout(controls, localGuardTimeout(args.timeoutMs, 5000), "download_control_timeout");
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
return {
|
|
510
|
+
ok: false,
|
|
511
|
+
status: "unsupported",
|
|
512
|
+
warnings: [],
|
|
513
|
+
blocker: {
|
|
514
|
+
kind: "download_unavailable",
|
|
515
|
+
code: "download_control_timeout",
|
|
516
|
+
message: `No visible ChatGPT download control could be counted before the local guard timeout: ${error instanceof Error ? error.message : String(error)}`,
|
|
517
|
+
resumable: true
|
|
518
|
+
},
|
|
519
|
+
context: await contextFromPage(page)
|
|
520
|
+
};
|
|
521
|
+
}
|
|
192
522
|
if (count === 0) {
|
|
523
|
+
const artifactDownload = await downloadLatestArtifact(env, args);
|
|
524
|
+
if (artifactDownload.ok) {
|
|
525
|
+
return artifactDownload;
|
|
526
|
+
}
|
|
193
527
|
return {
|
|
194
528
|
ok: false,
|
|
195
529
|
status: "unsupported",
|
|
@@ -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
|
};
|