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