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
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PageLike } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* One-evaluate DOM snapshot for the messages.wait polling loop.
|
|
4
|
+
*
|
|
5
|
+
* The previous loop ran four separate DOM probes per poll and transferred the entire
|
|
6
|
+
* latest assistant text across the browser bridge every iteration, even though the loop
|
|
7
|
+
* only needs change detection until completion. This snapshot returns fixed-size text
|
|
8
|
+
* metadata (normalized length + hash + transient flag) plus generation state and
|
|
9
|
+
* response-action evidence in a single round trip, sampled atomically from the same DOM
|
|
10
|
+
* instant. The full text is fetched once, at loop exit, by the caller.
|
|
11
|
+
*/
|
|
12
|
+
export type WaitDomSnapshot = {
|
|
13
|
+
turnCount: number;
|
|
14
|
+
assistantTurnCount: number;
|
|
15
|
+
latestAssistantTurnIndex?: number;
|
|
16
|
+
text: WaitTextMetadata;
|
|
17
|
+
generation: {
|
|
18
|
+
active: boolean;
|
|
19
|
+
stopped: boolean;
|
|
20
|
+
signals: string[];
|
|
21
|
+
};
|
|
22
|
+
/** undefined means no conversation-turn markers were found; callers fall back to the copy-button locator. */
|
|
23
|
+
hasResponseActions?: boolean;
|
|
24
|
+
};
|
|
25
|
+
export type WaitTextMetadata = {
|
|
26
|
+
/** Length of the whitespace-normalized latest assistant text. */
|
|
27
|
+
length: number;
|
|
28
|
+
/** FNV-1a 32-bit hash (hex) of the whitespace-normalized latest assistant text. */
|
|
29
|
+
hash: string;
|
|
30
|
+
/** Whether the text is a transient placeholder such as "Thinking". */
|
|
31
|
+
transient: boolean;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* SDK-side twin of the in-page metadata computation. The transient check delegates to
|
|
35
|
+
* dom/messages.ts isTransientAssistantText — the ground truth used by isResponseComplete —
|
|
36
|
+
* so only the in-page copy below is a true duplicate. The evaluate callback inlines the
|
|
37
|
+
* same normalization, hash, and transient rules because serialized callbacks cannot close
|
|
38
|
+
* over imports; `wait-snapshot.test.ts` pins the in-page copy to this helper (and thereby,
|
|
39
|
+
* transitively, to the ground truth).
|
|
40
|
+
*/
|
|
41
|
+
export declare function waitTextMetadata(rawText: string | undefined): WaitTextMetadata;
|
|
42
|
+
export declare function fnv1a32Hex(text: string): string;
|
|
43
|
+
export declare function readWaitDomSnapshot(page: PageLike): Promise<WaitDomSnapshot | undefined>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { localeLabels } from "./locale-labels.js";
|
|
2
|
+
import { isTransientAssistantText } from "./messages.js";
|
|
3
|
+
import { normalizeWhitespace } from "./visible-text.js";
|
|
4
|
+
/**
|
|
5
|
+
* SDK-side twin of the in-page metadata computation. The transient check delegates to
|
|
6
|
+
* dom/messages.ts isTransientAssistantText — the ground truth used by isResponseComplete —
|
|
7
|
+
* so only the in-page copy below is a true duplicate. The evaluate callback inlines the
|
|
8
|
+
* same normalization, hash, and transient rules because serialized callbacks cannot close
|
|
9
|
+
* over imports; `wait-snapshot.test.ts` pins the in-page copy to this helper (and thereby,
|
|
10
|
+
* transitively, to the ground truth).
|
|
11
|
+
*/
|
|
12
|
+
export function waitTextMetadata(rawText) {
|
|
13
|
+
const normalized = normalizeWhitespace(rawText ?? "");
|
|
14
|
+
return {
|
|
15
|
+
length: normalized.length,
|
|
16
|
+
hash: fnv1a32Hex(normalized),
|
|
17
|
+
transient: isTransientAssistantText(normalized)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function fnv1a32Hex(text) {
|
|
21
|
+
let hash = 0x811c9dc5;
|
|
22
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
23
|
+
hash ^= text.charCodeAt(index);
|
|
24
|
+
hash = Math.imul(hash, 0x01000193);
|
|
25
|
+
}
|
|
26
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
27
|
+
}
|
|
28
|
+
export async function readWaitDomSnapshot(page) {
|
|
29
|
+
if (typeof page.evaluate !== "function") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return page.evaluate((args) => {
|
|
33
|
+
const __combinedWaitSnapshot = true;
|
|
34
|
+
void __combinedWaitSnapshot;
|
|
35
|
+
const normalizeWs = (value) => value.replace(/\s+/g, " ").trim();
|
|
36
|
+
const normalizeLower = (value) => (value ?? "").trim().toLowerCase();
|
|
37
|
+
// --- Progress: turn counts and latest assistant text metadata (no text transfer) ---
|
|
38
|
+
const nodes = Array.from(document.querySelectorAll("[data-message-author-role]"));
|
|
39
|
+
const assistantNodes = nodes.filter(node => node.getAttribute("data-message-author-role") === "assistant");
|
|
40
|
+
const latestAssistant = assistantNodes.at(-1);
|
|
41
|
+
const latestAssistantTurnIndex = latestAssistant === undefined ? undefined : nodes.indexOf(latestAssistant) + 1;
|
|
42
|
+
const normalizedText = normalizeWs(latestAssistant?.innerText ?? latestAssistant?.textContent ?? "");
|
|
43
|
+
let hash = 0x811c9dc5;
|
|
44
|
+
for (let index = 0; index < normalizedText.length; index += 1) {
|
|
45
|
+
hash ^= normalizedText.charCodeAt(index);
|
|
46
|
+
hash = Math.imul(hash, 0x01000193);
|
|
47
|
+
}
|
|
48
|
+
const textHash = (hash >>> 0).toString(16).padStart(8, "0");
|
|
49
|
+
const trimmedForTransient = normalizedText.replace(/[.。…]+$/g, "").trim().toLowerCase();
|
|
50
|
+
const transient = args.transient.some(phrase => trimmedForTransient === phrase.toLowerCase())
|
|
51
|
+
|| /^analyzing (?:the )?images?$/.test(trimmedForTransient)
|
|
52
|
+
|| /^processing (?:the )?images?$/.test(trimmedForTransient)
|
|
53
|
+
|| /^reading (?:the )?images?$/.test(trimmedForTransient);
|
|
54
|
+
// --- Generation state: mirrors dom/generation-state.ts readAssistantGenerationState ---
|
|
55
|
+
const isVisible = (element) => {
|
|
56
|
+
const style = window.getComputedStyle(element);
|
|
57
|
+
return style.display !== "none"
|
|
58
|
+
&& style.visibility !== "hidden"
|
|
59
|
+
&& style.opacity !== "0"
|
|
60
|
+
&& element.getAttribute("aria-hidden") !== "true";
|
|
61
|
+
};
|
|
62
|
+
const visibleButtons = Array.from(document.querySelectorAll("button"))
|
|
63
|
+
.filter((button) => isVisible(button)
|
|
64
|
+
&& button.disabled !== true
|
|
65
|
+
&& button.getAttribute("aria-disabled") !== "true");
|
|
66
|
+
const buttonTexts = visibleButtons
|
|
67
|
+
.map(button => [
|
|
68
|
+
button.innerText,
|
|
69
|
+
button.textContent,
|
|
70
|
+
button.getAttribute("aria-label"),
|
|
71
|
+
button.getAttribute("title")
|
|
72
|
+
].map(normalizeLower).filter(Boolean).join(" "))
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
const bodyText = normalizeLower(document.body?.innerText);
|
|
75
|
+
const haystacks = [bodyText, ...buttonTexts];
|
|
76
|
+
const matchingSignals = (phrases) => haystacks.flatMap(text => phrases
|
|
77
|
+
.map(phrase => phrase.toLowerCase())
|
|
78
|
+
.filter(phrase => text.includes(phrase)));
|
|
79
|
+
const activeSignals = matchingSignals(args.stop);
|
|
80
|
+
const stoppedSignals = matchingSignals(args.stopped);
|
|
81
|
+
const generation = {
|
|
82
|
+
active: activeSignals.length > 0,
|
|
83
|
+
stopped: stoppedSignals.length > 0,
|
|
84
|
+
signals: [...new Set([...activeSignals, ...stoppedSignals, ...buttonTexts.filter(text => /stop|cancel|stopped|answering|thinking/i.test(text))])].slice(0, 5)
|
|
85
|
+
};
|
|
86
|
+
// --- Response actions: mirrors dom/generation-state.ts latestAssistantTurnHasResponseActions ---
|
|
87
|
+
const turns = Array.from(document.querySelectorAll("[data-testid^='conversation-turn']"));
|
|
88
|
+
let hasResponseActions;
|
|
89
|
+
if (turns.length === 0) {
|
|
90
|
+
hasResponseActions = undefined;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const latestTurn = [...turns].reverse().find(turn => turn.querySelector("[data-message-author-role='assistant']") !== null);
|
|
94
|
+
if (latestTurn === undefined) {
|
|
95
|
+
hasResponseActions = false;
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const actionText = Array.from(latestTurn.querySelectorAll("button"))
|
|
99
|
+
.map(button => [
|
|
100
|
+
button.innerText,
|
|
101
|
+
button.textContent,
|
|
102
|
+
button.getAttribute("aria-label"),
|
|
103
|
+
button.getAttribute("title")
|
|
104
|
+
].filter(Boolean).join(" "))
|
|
105
|
+
.join(" ")
|
|
106
|
+
.toLowerCase();
|
|
107
|
+
hasResponseActions = args.actions.some(phrase => actionText.includes(phrase.toLowerCase()));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const snapshot = {
|
|
111
|
+
turnCount: nodes.length,
|
|
112
|
+
assistantTurnCount: assistantNodes.length,
|
|
113
|
+
text: { length: normalizedText.length, hash: textHash, transient },
|
|
114
|
+
generation
|
|
115
|
+
};
|
|
116
|
+
if (latestAssistantTurnIndex !== undefined)
|
|
117
|
+
snapshot.latestAssistantTurnIndex = latestAssistantTurnIndex;
|
|
118
|
+
if (hasResponseActions !== undefined)
|
|
119
|
+
snapshot.hasResponseActions = hasResponseActions;
|
|
120
|
+
return snapshot;
|
|
121
|
+
}, {
|
|
122
|
+
transient: [...localeLabels.transientAssistant],
|
|
123
|
+
stop: [...localeLabels.stopControl],
|
|
124
|
+
stopped: [...localeLabels.stoppedAssistant],
|
|
125
|
+
actions: [...localeLabels.responseActions]
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -21,6 +21,7 @@ export declare const commandRisk: {
|
|
|
21
21
|
readonly "projects.sources.add": "medium";
|
|
22
22
|
readonly "response.copy": "medium";
|
|
23
23
|
readonly "modes.set": "medium";
|
|
24
|
+
readonly "modes.get": "low";
|
|
24
25
|
readonly "tools.select": "medium";
|
|
25
26
|
readonly "threads.delete": "high";
|
|
26
27
|
readonly "threads.archive": "high";
|
package/dist/src/safety/risk.js
CHANGED
|
@@ -3,6 +3,14 @@ import { dirname, resolve } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { readLanguageCoverage } from "./locale-capture/language-coverage.js";
|
|
5
5
|
const ENGLISH_MODE_LABELS = new Set(["Latest", "Instant", "Thinking", "Extended", "Medium", "High", "Extra High", "Pro"]);
|
|
6
|
+
const INTELLIGENCE_MODE_OPTION_IDS = ["instant", "medium", "high", "extraHigh", "pro"];
|
|
7
|
+
const ENGLISH_INTELLIGENCE_MODE_OPTIONS = {
|
|
8
|
+
instant: "Instant",
|
|
9
|
+
medium: "Medium",
|
|
10
|
+
high: "High",
|
|
11
|
+
extraHigh: "Extra High",
|
|
12
|
+
pro: "Pro",
|
|
13
|
+
};
|
|
6
14
|
const UPDATE_NOTE = " * Intelligence picker labels updated 2026-06-10 from a visible ChatGPT Pro session.";
|
|
7
15
|
class ApplyUsageError extends Error {
|
|
8
16
|
exitCode;
|
|
@@ -45,12 +53,14 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
45
53
|
throw new ApplyUsageError(`Missing successful capture for ${language.bcp47}.`, 1);
|
|
46
54
|
}
|
|
47
55
|
const labels = observedNonEnglishLabels(record);
|
|
48
|
-
|
|
56
|
+
const modeOptions = observedNonEnglishModeOptions(record);
|
|
57
|
+
if (labels.length === 0 && Object.keys(modeOptions).length === 0)
|
|
49
58
|
continue;
|
|
50
59
|
planned.push({
|
|
51
60
|
locale: language.bcp47,
|
|
52
61
|
file: resolve(root, "src/dom/locale", `${language.bcp47}.ts`),
|
|
53
62
|
labels,
|
|
63
|
+
modeOptions,
|
|
54
64
|
});
|
|
55
65
|
}
|
|
56
66
|
if (!options.reviewed) {
|
|
@@ -62,10 +72,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
62
72
|
}
|
|
63
73
|
for (const change of planned) {
|
|
64
74
|
const before = await readFile(change.file, "utf8");
|
|
65
|
-
const after =
|
|
75
|
+
const after = mergeModeCapture(before, change.labels, change.modeOptions);
|
|
66
76
|
if (after !== before) {
|
|
67
77
|
await writeFile(change.file, after, "utf8");
|
|
68
|
-
console.log(`updated ${change.locale} labels=${change.labels.length}`);
|
|
78
|
+
console.log(`updated ${change.locale} labels=${change.labels.length} modeOptions=${Object.keys(change.modeOptions).length}`);
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
return 0;
|
|
@@ -93,18 +103,38 @@ function observedNonEnglishLabels(record) {
|
|
|
93
103
|
}
|
|
94
104
|
return labels;
|
|
95
105
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const line = ` modeLabels: [${merged.map(label => JSON.stringify(label)).join(", ")}],`;
|
|
101
|
-
if (/^\s*modeLabels:\s*\[[^\]]*\],/m.test(text)) {
|
|
102
|
-
return text.replace(/^\s*modeLabels:\s*\[[^\]]*\],/m, line);
|
|
106
|
+
function observedNonEnglishModeOptions(record) {
|
|
107
|
+
const labels = record.intelligenceLabels ?? [];
|
|
108
|
+
if (labels.length !== INTELLIGENCE_MODE_OPTION_IDS.length) {
|
|
109
|
+
throw new ApplyUsageError(`${record.requestedLocale} has ${labels.length} Intelligence labels; expected ${INTELLIGENCE_MODE_OPTION_IDS.length}.`, 1);
|
|
103
110
|
}
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
const modeOptions = {};
|
|
112
|
+
for (let index = 0; index < INTELLIGENCE_MODE_OPTION_IDS.length; index += 1) {
|
|
113
|
+
const id = INTELLIGENCE_MODE_OPTION_IDS[index];
|
|
114
|
+
const label = labels[index];
|
|
115
|
+
if (label !== ENGLISH_INTELLIGENCE_MODE_OPTIONS[id]) {
|
|
116
|
+
modeOptions[id] = [label];
|
|
117
|
+
}
|
|
106
118
|
}
|
|
107
|
-
return
|
|
119
|
+
return modeOptions;
|
|
120
|
+
}
|
|
121
|
+
function mergeModeCapture(source, labels, modeOptions) {
|
|
122
|
+
let text = updateComment(source);
|
|
123
|
+
if (labels.length > 0) {
|
|
124
|
+
const existing = parseExistingModeLabels(text);
|
|
125
|
+
const merged = dedupe([...existing, ...labels]);
|
|
126
|
+
const line = ` modeLabels: [${merged.map(label => JSON.stringify(label)).join(", ")}],`;
|
|
127
|
+
if (/^\s*modeLabels:\s*\[[^\]]*\],/m.test(text)) {
|
|
128
|
+
text = text.replace(/^\s*modeLabels:\s*\[[^\]]*\],/m, line);
|
|
129
|
+
}
|
|
130
|
+
else if (/^\s*copyResponse:\s*.*,\n/m.test(text)) {
|
|
131
|
+
text = text.replace(/^(\s*copyResponse:\s*.*,\n)/m, `$1${line}\n`);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
text = text.replace(/^export const \w+ = \{\n/m, match => `${match}${line}\n`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return mergeModeOptions(text, modeOptions);
|
|
108
138
|
}
|
|
109
139
|
function parseExistingModeLabels(source) {
|
|
110
140
|
const match = /^\s*modeLabels:\s*\[(?<body>[^\]]*)\],/m.exec(source);
|
|
@@ -117,6 +147,62 @@ function parseExistingModeLabels(source) {
|
|
|
117
147
|
}
|
|
118
148
|
return labels;
|
|
119
149
|
}
|
|
150
|
+
function mergeModeOptions(source, modeOptions) {
|
|
151
|
+
const existing = parseExistingModeOptions(source);
|
|
152
|
+
const merged = {};
|
|
153
|
+
for (const id of INTELLIGENCE_MODE_OPTION_IDS) {
|
|
154
|
+
const values = dedupe([...(existing[id] ?? []), ...(modeOptions[id] ?? [])]);
|
|
155
|
+
if (values.length > 0) {
|
|
156
|
+
merged[id] = values;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const block = formatModeOptions(merged);
|
|
160
|
+
if (block === undefined) {
|
|
161
|
+
return source;
|
|
162
|
+
}
|
|
163
|
+
if (/^\s*modeOptions:\s*\{[\s\S]*?^\s*\},\n/m.test(source)) {
|
|
164
|
+
return source.replace(/^\s*modeOptions:\s*\{[\s\S]*?^\s*\},\n/m, `${block}\n`);
|
|
165
|
+
}
|
|
166
|
+
if (/^\s*modeLabels:\s*\[[^\]]*\],\n/m.test(source)) {
|
|
167
|
+
return source.replace(/^(\s*modeLabels:\s*\[[^\]]*\],\n)/m, `$1${block}\n`);
|
|
168
|
+
}
|
|
169
|
+
return source.replace(/^export const \w+ = \{\n/m, match => `${match}${block}\n`);
|
|
170
|
+
}
|
|
171
|
+
function parseExistingModeOptions(source) {
|
|
172
|
+
const options = {};
|
|
173
|
+
const blockMatch = /^\s*modeOptions:\s*\{(?<body>[\s\S]*?)^\s*\},/m.exec(source);
|
|
174
|
+
const body = blockMatch?.groups?.body;
|
|
175
|
+
if (body === undefined)
|
|
176
|
+
return options;
|
|
177
|
+
for (const id of INTELLIGENCE_MODE_OPTION_IDS) {
|
|
178
|
+
const lineMatch = new RegExp(`^\\s*${id}:\\s*\\[(?<body>[^\\]]*)\\],`, "m").exec(body);
|
|
179
|
+
const lineBody = lineMatch?.groups?.body;
|
|
180
|
+
if (lineBody === undefined)
|
|
181
|
+
continue;
|
|
182
|
+
const values = [];
|
|
183
|
+
for (const stringMatch of lineBody.matchAll(/"((?:\\"|[^"])*)"/g)) {
|
|
184
|
+
values.push(JSON.parse(`"${stringMatch[1]}"`));
|
|
185
|
+
}
|
|
186
|
+
if (values.length > 0) {
|
|
187
|
+
options[id] = values;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return options;
|
|
191
|
+
}
|
|
192
|
+
function formatModeOptions(modeOptions) {
|
|
193
|
+
const lines = INTELLIGENCE_MODE_OPTION_IDS
|
|
194
|
+
.map(id => {
|
|
195
|
+
const values = modeOptions[id];
|
|
196
|
+
return values === undefined || values.length === 0
|
|
197
|
+
? undefined
|
|
198
|
+
: ` ${id}: [${values.map(value => JSON.stringify(value)).join(", ")}],`;
|
|
199
|
+
})
|
|
200
|
+
.filter((line) => line !== undefined);
|
|
201
|
+
if (lines.length === 0) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
return [" modeOptions: {", ...lines, " },"].join("\n");
|
|
205
|
+
}
|
|
120
206
|
function updateComment(source) {
|
|
121
207
|
let text = source.replace(/\n \* Omitted because they match English case-insensitively: `modeLabels`[\s\S]*?blocker copy\.\n/g, "\n * Some non-Intelligence surfaces may still fall back to English + `selector_drift`.\n");
|
|
122
208
|
if (!text.includes(UPDATE_NOTE)) {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -183,10 +183,14 @@ export type WaitArgs = {
|
|
|
183
183
|
stableMs?: number;
|
|
184
184
|
pollMs?: number;
|
|
185
185
|
mode?: "normal" | "deep_research";
|
|
186
|
+
responseContent?: "include" | "metadata";
|
|
186
187
|
};
|
|
187
188
|
export type WaitData = {
|
|
188
189
|
complete: boolean;
|
|
189
190
|
responseText?: string;
|
|
191
|
+
responseChars?: number;
|
|
192
|
+
responseSha256?: string;
|
|
193
|
+
responseContent?: "include" | "metadata";
|
|
190
194
|
assistantTurnCount: number;
|
|
191
195
|
elapsedMs: number;
|
|
192
196
|
};
|
|
@@ -291,6 +295,8 @@ export type AskReadData = {
|
|
|
291
295
|
export type AttachFilesArgs = {
|
|
292
296
|
paths: string[];
|
|
293
297
|
timeoutMs?: number;
|
|
298
|
+
includeDiagnostics?: boolean;
|
|
299
|
+
includeHashes?: boolean;
|
|
294
300
|
};
|
|
295
301
|
export type AttachedFile = {
|
|
296
302
|
path: string;
|
|
@@ -302,11 +308,13 @@ export type FilePreflightArgs = {
|
|
|
302
308
|
paths: string[];
|
|
303
309
|
maxBytesPerFile?: number;
|
|
304
310
|
maxTotalBytes?: number;
|
|
311
|
+
includeHashes?: boolean;
|
|
305
312
|
};
|
|
306
313
|
export type FilePreflightFile = AttachedFile & {
|
|
307
314
|
extension: string;
|
|
308
315
|
mimeType: string;
|
|
309
316
|
category: FileCategory;
|
|
317
|
+
sha256?: string;
|
|
310
318
|
};
|
|
311
319
|
export type FilePreflightData = {
|
|
312
320
|
files: FilePreflightFile[];
|
|
@@ -314,6 +322,20 @@ export type FilePreflightData = {
|
|
|
314
322
|
};
|
|
315
323
|
export type AttachFilesData = {
|
|
316
324
|
files: AttachedFile[];
|
|
325
|
+
diagnostics?: FileUploadDiagnostics;
|
|
326
|
+
};
|
|
327
|
+
export type BrowserInputFileDiagnostic = {
|
|
328
|
+
name: string;
|
|
329
|
+
size: number;
|
|
330
|
+
type?: string;
|
|
331
|
+
lastModified?: number;
|
|
332
|
+
};
|
|
333
|
+
export type BrowserInputDiagnostic = {
|
|
334
|
+
files: BrowserInputFileDiagnostic[];
|
|
335
|
+
};
|
|
336
|
+
export type FileUploadDiagnostics = {
|
|
337
|
+
preflight: FilePreflightData;
|
|
338
|
+
browserInput?: BrowserInputDiagnostic;
|
|
317
339
|
};
|
|
318
340
|
export type ProjectSourceStatus = "ready" | "processing" | "failed" | "unknown";
|
|
319
341
|
export type ProjectSource = {
|
|
@@ -460,6 +482,12 @@ export type SetModeArgs = {
|
|
|
460
482
|
version?: string;
|
|
461
483
|
timeoutMs?: number;
|
|
462
484
|
};
|
|
485
|
+
export type GetModeArgs = {
|
|
486
|
+
timeoutMs?: number;
|
|
487
|
+
};
|
|
488
|
+
export type GetModeData = {
|
|
489
|
+
modes: string[];
|
|
490
|
+
};
|
|
463
491
|
export type SelectToolArgs = {
|
|
464
492
|
tool: "web_search" | "deep_research" | "create_image" | string;
|
|
465
493
|
timeoutMs?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-chatgpt-control",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-alpha.1",
|
|
4
4
|
"description": "Unofficial SDK for Codex agents controlling visible ChatGPT web sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"devDependencies": {
|
|
75
75
|
"@types/node": "^24.13.1",
|
|
76
76
|
"ajv": "^8.17.1",
|
|
77
|
-
"esbuild": "^0.28.
|
|
77
|
+
"esbuild": "^0.28.1",
|
|
78
78
|
"tsx": "^4.20.0",
|
|
79
79
|
"typescript": "^5.8.0",
|
|
80
80
|
"vitest": "^4.1.8"
|
|
@@ -67,7 +67,7 @@ Current protocol error codes are:
|
|
|
67
67
|
|
|
68
68
|
Browser-control blockers are not protocol errors. They are normal command or runner results with `status: "blocked"`, `status: "partial"`, or `status: "needs_confirmation"` plus blocker/interruption details.
|
|
69
69
|
|
|
70
|
-
`status: "partial"` is the required result for incomplete response capture. A partial result may still include `output_text` and `data.responseText`, but consumers must treat that text as incomplete until a later wait confirms completion. Common causes are wait timeout after partial assistant text, active generation controls such as `Stop answering`, stopped-generation markers such as `Stopped thinking`, or a read fallback after the wait step could not confirm completion. Intentional capture clipping uses `data.captureLimit` plus warnings; it is separate from ChatGPT generation length.
|
|
70
|
+
`status: "partial"` is the required result for incomplete response capture. A partial result may still include `output_text` and `data.responseText`, but consumers must treat that text as incomplete until a later wait confirms completion. Common causes are wait timeout after partial assistant text, active generation controls such as `Stop answering`, stopped-generation markers such as `Stopped thinking`, or a read fallback after the wait step could not confirm completion. For status-only polling, `messages.wait` accepts `responseContent: "metadata"`; partial and completed wait results then omit assistant text and instead return compact metadata such as `data.responseChars` and `data.responseSha256`. Intentional capture clipping uses `data.captureLimit` plus warnings; it is separate from ChatGPT generation length.
|
|
71
71
|
|
|
72
72
|
## Streaming
|
|
73
73
|
|
|
@@ -112,7 +112,7 @@ The backend must support:
|
|
|
112
112
|
- diagnostics: `doctor`
|
|
113
113
|
- reports: `createReport`, `reports.create`, `reports.redact`, `reports.summarize`
|
|
114
114
|
- command discovery: `commands`, `describe`, `help`
|
|
115
|
-
- primitives: `session.bootstrap`, `threads.*`, `messages.*`, `artifacts.*`, `files.preflight`, `files.attach`, `files.downloadLatest`, `projects.sources.list`, `projects.sources.planAdd`, `projects.sources.add`, `modes.set`, `tools.select`, `response.copy`
|
|
115
|
+
- primitives: `session.bootstrap`, `threads.*`, `messages.*`, `artifacts.*`, `files.preflight`, `files.attach`, `files.downloadLatest`, `projects.sources.list`, `projects.sources.planAdd`, `projects.sources.add`, `modes.set`, `modes.get`, `tools.select`, `response.copy`
|
|
116
116
|
|
|
117
117
|
`doctor` returns a normal `CommandResult` whose `data.checks` map is extensible. Scenario checks such as `existing_tab`, `artifacts`, `file_preflight`, `localization`, and `reports` may add optional `code`, `blockerKind`, `nextCommand`, and JSON `details` fields to individual check entries while preserving the existing `status`, `message`, and `remediation` fields.
|
|
118
118
|
|
|
@@ -120,7 +120,9 @@ The backend must support:
|
|
|
120
120
|
|
|
121
121
|
Attachment paths are interpreted on the machine running the Node backend. Use an absolute path in that host operating system's native form. On macOS/Linux/WSL, use paths such as `/example/user/file.pdf`, `/home/you/file.pdf`, or `/mnt/c/example/user/file.pdf`. On Windows backend hosts, use fully qualified paths such as `C:\Users\you\file.pdf` or UNC paths such as `\\server\share\file.pdf`. Drive-relative paths like `C:Users\you\file.pdf`, root-relative paths like `\tmp\file.pdf`, and Windows-looking paths sent to a POSIX backend are rejected before filesystem access.
|
|
122
122
|
|
|
123
|
-
Use `files.preflight` for non-mutating local validation before browser upload workflows. It validates absolute paths, existence, readability, file-vs-directory status, configurable per-file and total byte limits, duplicate basenames, duplicate resolved paths, zero-byte files, and extension-based MIME/category guesses.
|
|
123
|
+
Use `files.preflight` for non-mutating local validation before browser upload workflows. It validates absolute paths, existence, readability, file-vs-directory status, configurable per-file and total byte limits, duplicate basenames, duplicate resolved paths, zero-byte files, and extension-based MIME/category guesses. Zero-byte files are blocked before browser interaction because ChatGPT rejects empty attachments. By default the command does not open ChatGPT, perform a live upload, read file contents for MIME detection, or return file-content fingerprints. Callers may pass `includeHashes: true` to include SHA-256 metadata for local diagnostics; file contents are never returned. `askWithFiles` and `files.attach` run the same preflight before upload attempts so obvious local file failures stop before browser interaction.
|
|
124
|
+
|
|
125
|
+
`files.attach` accepts `includeDiagnostics: true` to return metadata-only upload diagnostics in `data.diagnostics`: the preflight result plus the browser input's selected file names and sizes when the DOM exposes them. Pair `includeDiagnostics: true` with `includeHashes: true` when diagnosing whether a non-empty local file became an empty browser-side `File`; do not persist these diagnostics in public reports unless the user has approved content fingerprint metadata.
|
|
124
126
|
|
|
125
127
|
## Project Sources
|
|
126
128
|
|
|
@@ -213,7 +215,7 @@ Important: in Codex, `globalThis.agent` is not present until the Chrome plugin r
|
|
|
213
215
|
The live Chrome bootstrap is:
|
|
214
216
|
|
|
215
217
|
```js
|
|
216
|
-
const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/
|
|
218
|
+
const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/latest/scripts/browser-client.mjs");
|
|
217
219
|
await setupBrowserRuntime({ globals: globalThis });
|
|
218
220
|
globalThis.browser = await agent.browsers.get("extension");
|
|
219
221
|
```
|
|
@@ -77,7 +77,8 @@ Localized — lives in `src/dom/locale/en.ts` (English) and per-locale files, sa
|
|
|
77
77
|
| `projectSourcesTab` / `projectSourcesAddSource` / `projectSourcesUploadFiles` | Project Sources tab and append-add flow | visible tab/button/menu text |
|
|
78
78
|
| `copyResponse` | copy-response button | `aria-label` |
|
|
79
79
|
| `download` / `downloadImage` / `imageContainerHint` | download affordances | `aria-label` / container hint |
|
|
80
|
-
| `modeLabels` / `modeOpenerExtra` | model/effort switcher | visible button + menu text |
|
|
80
|
+
| `modeLabels` / `modeOpenerExtra` | model/effort switcher recognition and openers | visible button + menu text |
|
|
81
|
+
| `modeOptions.<semantic-id>` | selectable model/intelligence mode options | visible picker rows, keyed by stable ids such as `high`, `extraHigh`, and `pro` |
|
|
81
82
|
| `tools.web_search` / `tools.deep_research` / `tools.create_image` | tool menu items | visible menu text |
|
|
82
83
|
| `signedInMarkers` | signed-in detection | sidebar/shell words |
|
|
83
84
|
| `transientAssistant` | streaming placeholder filter | assistant streaming text ("Thinking", etc.) |
|
|
@@ -141,7 +142,13 @@ import type { LocaleContribution } from "./types.js";
|
|
|
141
142
|
|
|
142
143
|
export const de = {
|
|
143
144
|
sendButton: ["Nachricht senden"],
|
|
144
|
-
modeLabels: ["
|
|
145
|
+
modeLabels: ["Sofort", "Mittel", "Hoch", "Extra hoch"],
|
|
146
|
+
modeOptions: {
|
|
147
|
+
instant: ["Sofort"],
|
|
148
|
+
medium: ["Mittel"],
|
|
149
|
+
high: ["Hoch"],
|
|
150
|
+
extraHigh: ["Extra hoch"],
|
|
151
|
+
},
|
|
145
152
|
tools: {
|
|
146
153
|
web_search: ["Websuche"],
|
|
147
154
|
},
|
|
@@ -150,11 +157,14 @@ export const de = {
|
|
|
150
157
|
```
|
|
151
158
|
|
|
152
159
|
Leave the canonical English first (it comes from `en.ts`), and leave the API keys
|
|
153
|
-
(`web_search`,
|
|
160
|
+
(`web_search`, `modeOptions.pro`, `modeOptions.high`, and the other semantic ids)
|
|
161
|
+
unchanged.
|
|
154
162
|
|
|
155
163
|
Newer ChatGPT rollouts may expose `Medium`, `High`, `Extra High`, and `Pro`
|
|
156
164
|
under an `Intelligence` picker. Add localized equivalents only after observing
|
|
157
|
-
those exact labels in the target locale.
|
|
165
|
+
those exact labels in the target locale. Put broad picker/opening labels in
|
|
166
|
+
`modeLabels`, but put selectable labels under `modeOptions.<semantic-id>` so
|
|
167
|
+
short labels such as `Pro` cannot match unrelated rows like `Move to project`.
|
|
158
168
|
|
|
159
169
|
Then open [`src/dom/locale/index.ts`](../src/dom/locale/index.ts) and register the new
|
|
160
170
|
locale:
|
|
@@ -29,6 +29,8 @@ Wire fields stay TypeScript-compatible. Python exposes idiomatic aliases:
|
|
|
29
29
|
|
|
30
30
|
Incomplete response capture is also shared contract behavior. Python must preserve `status == "partial"`, `output_text`, warnings, and any nested `data.captureLimit` dictionaries exactly as the TypeScript backend returns them. `partial` is not a protocol error: callers should inspect `data.complete` and run another wait/read on the same thread when they need final output.
|
|
31
31
|
|
|
32
|
+
For long-answer polling, Python forwards `response_content="metadata"` to the shared wire field `responseContent: "metadata"` on `messages.wait`. The TypeScript backend then omits assistant text from wait results and returns compact metadata such as `data.responseChars` and `data.responseSha256`; Python must preserve those fields without trying to reconstruct omitted content.
|
|
33
|
+
|
|
32
34
|
Generated-image behavior stays owned by the TypeScript runtime. Python exposes
|
|
33
35
|
the same backend commands through `chatgpt.artifacts.list_latest(...)`,
|
|
34
36
|
`chatgpt.artifacts.wait(...)`, and `chatgpt.artifacts.download_latest(...)`.
|
|
@@ -49,7 +51,7 @@ the shared `blocker-explanation-profiles.json` and
|
|
|
49
51
|
|
|
50
52
|
Python does not reinterpret attachment paths. It sends the path string to the Node backend, and the backend validates the path against its own host operating system. Attachment paths must be absolute on the backend host. On macOS/Linux/WSL backends, use POSIX paths such as `/example/user/file.pdf` or `/mnt/c/example/user/file.pdf`. On Windows backends, use fully qualified paths such as `C:\Users\you\file.pdf` or UNC paths such as `\\server\share\file.pdf`. Drive-relative paths, root-relative paths, and Windows-looking paths sent to a POSIX backend are rejected before filesystem access.
|
|
51
53
|
|
|
52
|
-
Python exposes the backend-visible `files.preflight` command as `chatgpt.files.preflight(...)`. It returns the same `CommandResult` as TypeScript and can be decoded with `FilePreflightData` when callers want typed metadata. The command validates paths, readability, file-vs-directory status, size limits, duplicate basenames, duplicate resolved paths, zero-byte files, and extension-based MIME/category guesses without opening ChatGPT or reading file contents for MIME detection.
|
|
54
|
+
Python exposes the backend-visible `files.preflight` command as `chatgpt.files.preflight(...)`. It returns the same `CommandResult` as TypeScript and can be decoded with `FilePreflightData` when callers want typed metadata. The command validates paths, readability, file-vs-directory status, size limits, duplicate basenames, duplicate resolved paths, zero-byte files, and extension-based MIME/category guesses without opening ChatGPT or reading file contents for MIME detection. Zero-byte files are blocked before browser interaction. Optional `include_hashes=True` / wire `includeHashes: true` adds SHA-256 metadata to `FilePreflightFile.sha256` for local diagnostics; file contents are never returned.
|
|
53
55
|
|
|
54
56
|
## Project Sources
|
|
55
57
|
|
|
@@ -161,7 +163,7 @@ Python is a native SDK facade over the local backend protocol. The initial brows
|
|
|
161
163
|
|
|
162
164
|
- `dist/codex-chatgpt-control-backend.mjs` is the stdio backend bundle.
|
|
163
165
|
- `BackendClient` and `StdioBackendTransport` keep Python backend calls long-lived.
|
|
164
|
-
- `NodeSidecarTransport.run(...)` remains as a compatibility wrapper over backend `runner.run`.
|
|
166
|
+
- `NodeSidecarTransport.run(...)` remains as a compatibility wrapper over backend `runner.run`. By default each call spawns and tears down its own backend subprocess; use it as a context manager (or call `open()`/`close()`) to reuse one persistent backend process across calls in multi-command workflows. Transport-level failures close the persistent session; protocol-level errors keep it open.
|
|
165
167
|
- Ordinary-shell smoke passes when browser-required calls return structured `browser_bridge_unavailable`.
|
|
166
168
|
- Browser-bridge runtime smoke remains explicitly gated because it can operate a real ChatGPT session.
|
|
167
169
|
|
|
@@ -199,7 +201,7 @@ python scripts/live_smoke.py --mode browser-bridge
|
|
|
199
201
|
When the live backend is hosted inside the Codex Chrome plugin runtime, do not test bridge availability from a normal shell or an unbootstrapped Node REPL. First initialize the Chrome runtime:
|
|
200
202
|
|
|
201
203
|
```js
|
|
202
|
-
const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/
|
|
204
|
+
const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/latest/scripts/browser-client.mjs");
|
|
203
205
|
await setupBrowserRuntime({ globals: globalThis });
|
|
204
206
|
globalThis.browser = await agent.browsers.get("extension");
|
|
205
207
|
```
|
|
@@ -33,7 +33,7 @@ Use `chatgpt.explainBlocker(result)` or Python `explain_blocker(result)` when re
|
|
|
33
33
|
Do not conclude that Chrome or the extension is broken from a plain shell result, or from checking `globalThis.agent` before the Chrome plugin runtime is initialized. For a true Codex Chrome-plugin live run, bootstrap the runtime first:
|
|
34
34
|
|
|
35
35
|
```js
|
|
36
|
-
const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/
|
|
36
|
+
const { setupBrowserRuntime } = await import("/example/user/.codex/plugins/cache/openai-bundled/chrome/latest/scripts/browser-client.mjs");
|
|
37
37
|
await setupBrowserRuntime({ globals: globalThis });
|
|
38
38
|
globalThis.browser = await agent.browsers.get("extension");
|
|
39
39
|
```
|
|
@@ -90,9 +90,24 @@ Agent-facing remediation text should name both settings:
|
|
|
90
90
|
|
|
91
91
|
Attachment paths are validated against the backend host's operating system. If a Windows-looking path is rejected on macOS/Linux, do not retry with the same string. Convert it to the backend host's real path, for example `/mnt/c/example/user/file.pdf` for a WSL/Linux backend. Drive-relative paths like `C:Users\you\file.pdf`, root-relative paths like `\tmp\file.pdf`, and empty or relative paths are always rejected.
|
|
92
92
|
|
|
93
|
+
## Empty Or Stripped Attachments
|
|
94
|
+
|
|
95
|
+
Zero-byte files are blocked by `files.preflight` before browser upload because
|
|
96
|
+
ChatGPT rejects empty attachments with a generic help-center error. If a user
|
|
97
|
+
reports that manual upload works but automated upload fails, compare local and
|
|
98
|
+
browser-side metadata instead of guessing:
|
|
99
|
+
|
|
100
|
+
1. Run `files.preflight({ paths, includeHashes: true })` and inspect `bytes`
|
|
101
|
+
plus `sha256` for the backend-visible file.
|
|
102
|
+
2. Run `files.attach({ paths, includeDiagnostics: true, includeHashes: true })`
|
|
103
|
+
and inspect `data.diagnostics.browserInput.files[].size` when available.
|
|
104
|
+
3. If preflight `bytes` is zero, investigate the source path, generation race,
|
|
105
|
+
cloud placeholder, or host/container path mapping. If preflight bytes are
|
|
106
|
+
nonzero but browser-side size is zero, investigate the Chrome handoff.
|
|
107
|
+
|
|
93
108
|
## Clipboard Unavailable
|
|
94
109
|
|
|
95
|
-
`response.copy` falls back to DOM text extraction when the
|
|
110
|
+
`response.copy` falls back to DOM text extraction when the system clipboard does not change. Clipboard reads use `pbpaste` on macOS, PowerShell `Get-Clipboard` on Windows, and `xclip`/`xsel`/`wl-paste` on Linux; hosts without any of these tools always use the DOM fallback.
|
|
96
111
|
|
|
97
112
|
## Flattened Or Unreadable Response Capture
|
|
98
113
|
|
|
@@ -100,12 +115,20 @@ Use `readLatest({ format: "markdown" })`, `copyLatest()`, or the default SDK `re
|
|
|
100
115
|
|
|
101
116
|
## Long Responses Return `partial`
|
|
102
117
|
|
|
103
|
-
Long Pro, Thinking, Deep Research, or file-backed answers can take longer than the default wait window. Treat `status: "partial"` and `data.complete: false` as an incomplete capture even when `output_text` is non-empty. Re-run `messages.wait(...)` on the same thread
|
|
118
|
+
Long Pro, Thinking, Deep Research, or file-backed answers can take longer than the default wait window. Treat `status: "partial"` and `data.complete: false` as an incomplete capture even when `output_text` is non-empty. For repeated polling, prefer `messages.wait({ responseContent: "metadata", ... })` so Codex receives compact status metadata instead of the same growing partial answer body. Re-run `messages.wait(...)` on the same thread until completion is confirmed, then call `readLatest(...)` or `copyLatest(...)` once.
|
|
104
119
|
|
|
105
120
|
Recommended long-answer wait:
|
|
106
121
|
|
|
107
122
|
```ts
|
|
108
|
-
wait
|
|
123
|
+
await chatgpt.messages.wait({
|
|
124
|
+
timeoutMs: 45_000,
|
|
125
|
+
stableMs: 2_000,
|
|
126
|
+
pollMs: 1_000,
|
|
127
|
+
mode: "deep_research",
|
|
128
|
+
responseContent: "metadata"
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const final = await chatgpt.messages.readLatest({ format: "markdown" });
|
|
109
132
|
```
|
|
110
133
|
|
|
111
134
|
Active generation may appear as a visible or accessible-name control such as `Stop answering`, `Stop generating`, or `Stop streaming`. Stopped generation may appear as `Stopped thinking`. Treat all of those as incomplete states.
|
|
@@ -144,6 +167,6 @@ Doctor also supports opt-in scenario checks:
|
|
|
144
167
|
|
|
145
168
|
- `existing_tab`: claims only the requested already-open tab target by default and reports `existing_tab_not_found` / `existing_tab_ambiguous` diagnostics without opening a replacement tab unless `existingTab.ifMissing` explicitly allows that.
|
|
146
169
|
- `artifacts`: verifies current-page artifact selector/download/asset support without requesting generation.
|
|
147
|
-
- `file_preflight`: validates supplied local file paths without opening ChatGPT or attempting upload. It reports path count, total bytes, duplicate
|
|
170
|
+
- `file_preflight`: validates supplied local file paths without opening ChatGPT or attempting upload. It reports path count, total bytes, duplicate warnings, zero-byte blockers, and extension-based MIME/category metadata; fatal local file problems map to the same structured blockers as `files.preflight`.
|
|
148
171
|
- `localization`: checks locale-label registry readiness and English canonical labels without changing the ChatGPT account language; it is not yet proof of full localized selector coverage.
|
|
149
172
|
- `reports`: checks redacted-report policy and existing destination writability when possible without writing a report.
|