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
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { attachChatGPTBrowser } from "../browser/attach.js";
|
|
6
|
+
import { BROWSER_BRIDGE_REMEDIATION, BROWSER_BRIDGE_UNAVAILABLE_MESSAGE } from "../errors.js";
|
|
7
|
+
import { localeLabels } from "../dom/locale-labels.js";
|
|
8
|
+
import { nonEnglishLanguages, readLanguageCoverage } from "./locale-capture/language-coverage.js";
|
|
9
|
+
const SCHEMA_VERSION = "chatgpt.browser_control.intelligence_locale_capture.v1";
|
|
10
|
+
const CHATGPT_HOME = "https://chatgpt.com/";
|
|
11
|
+
const DEFAULT_SWITCH_TIMEOUT_MS = 15_000;
|
|
12
|
+
const DEFAULT_SETTLE_MS = 1_500;
|
|
13
|
+
const DEFAULT_LIMIT = 25;
|
|
14
|
+
class CaptureUsageError extends Error {
|
|
15
|
+
exitCode;
|
|
16
|
+
constructor(message, exitCode = 2) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.exitCode = exitCode;
|
|
19
|
+
this.name = "CaptureUsageError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const USAGE = [
|
|
23
|
+
"Usage:",
|
|
24
|
+
" npm run capture:intelligence-locales:queue",
|
|
25
|
+
" npm run capture:intelligence-locales -- --locale de --native Deutsch --out ../../outputs/intelligence-locale-captures/2026-06-10-intelligence-picker.jsonl",
|
|
26
|
+
" npm run capture:intelligence-locales -- --auto-switch --limit 25",
|
|
27
|
+
" npm run capture:intelligence-locales -- --auto-switch --all --if-missing open",
|
|
28
|
+
" npm run capture:intelligence-locales -- --auto-switch --locales de,fr-FR,pt-BR",
|
|
29
|
+
"",
|
|
30
|
+
"Options:",
|
|
31
|
+
" --print-queue Print the language queue from references/language-coverage.md.",
|
|
32
|
+
" --locale BCP47 locale id for a one-shot capture.",
|
|
33
|
+
" --native Exact Settings language option text for a one-shot capture.",
|
|
34
|
+
" --out JSONL output path. Defaults to ../../outputs/intelligence-locale-captures/<today>-intelligence-picker.jsonl.",
|
|
35
|
+
" --auto-switch Change ChatGPT Settings -> General -> Language before each capture.",
|
|
36
|
+
" --all Sweep every non-English language from references/language-coverage.md.",
|
|
37
|
+
" --limit Number of non-English languages to sweep. Default: 25 with --auto-switch.",
|
|
38
|
+
" --locales Comma-separated BCP47 ids to sweep instead of first --limit languages.",
|
|
39
|
+
" --open-version-submenu Capture GPT-* model version submenu labels. Default: true.",
|
|
40
|
+
" --no-open-version-submenu Do not open the model-version submenu.",
|
|
41
|
+
" --restore Restore the initially selected language after a sweep. Default with --auto-switch.",
|
|
42
|
+
" --no-restore Leave ChatGPT on the last swept language.",
|
|
43
|
+
" --settle-ms Wait after language switches and menu opens. Default: 1500.",
|
|
44
|
+
" --switch-timeout-ms Wait for rendered html lang after a language switch. Default: 15000.",
|
|
45
|
+
" --if-missing block|open|create. Default: open with --auto-switch, otherwise block.",
|
|
46
|
+
" --tab-id Claim an exact ChatGPT tab id instead of the selected ChatGPT tab.",
|
|
47
|
+
" --coverage-path Path to language-coverage.md."
|
|
48
|
+
].join("\n");
|
|
49
|
+
export async function main(argv = process.argv.slice(2), runtime = globalThis) {
|
|
50
|
+
let options;
|
|
51
|
+
try {
|
|
52
|
+
options = parseArgs(argv);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof CaptureUsageError) {
|
|
56
|
+
console.log(error.message);
|
|
57
|
+
return error.exitCode;
|
|
58
|
+
}
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
const languages = await readLanguageCoverage(options.coveragePath);
|
|
62
|
+
if (options.printQueue) {
|
|
63
|
+
printQueue(nonEnglishLanguages(languages));
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
if ((runtime.agent === undefined || runtime.agent === null) && runtime.browser === undefined) {
|
|
67
|
+
console.log(JSON.stringify({
|
|
68
|
+
ok: false,
|
|
69
|
+
status: "blocked",
|
|
70
|
+
blocker: {
|
|
71
|
+
kind: "browser_bridge_unavailable",
|
|
72
|
+
code: "codex_chrome_bridge_unavailable",
|
|
73
|
+
message: BROWSER_BRIDGE_UNAVAILABLE_MESSAGE,
|
|
74
|
+
remediation: BROWSER_BRIDGE_REMEDIATION
|
|
75
|
+
}
|
|
76
|
+
}, null, 2));
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
const initialLanguage = options.autoSwitch
|
|
80
|
+
? await attachCapturePage(runtime, options).then(page => readSelectedLanguage(page)).catch(() => undefined)
|
|
81
|
+
: undefined;
|
|
82
|
+
const sweepLanguages = resolveSweepLanguages(options, languages);
|
|
83
|
+
const records = [];
|
|
84
|
+
try {
|
|
85
|
+
for (const language of sweepLanguages) {
|
|
86
|
+
const page = await attachCapturePage(runtime, options);
|
|
87
|
+
const record = await captureOne(page, language, options);
|
|
88
|
+
records.push(record);
|
|
89
|
+
await appendRecord(options.out, record);
|
|
90
|
+
printCaptureRecord(record, options.out);
|
|
91
|
+
const recentRecords = records.slice(-3);
|
|
92
|
+
if (recentRecords.length === 3 && recentRecords.every(previous => previous.status === "blocked")) {
|
|
93
|
+
console.error("Stopping after three consecutive blocked locale captures.");
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
if (options.autoSwitch && options.restore && initialLanguage !== undefined) {
|
|
100
|
+
await attachCapturePage(runtime, options).then(page => restoreLanguage(page, initialLanguage, options)).catch(error => {
|
|
101
|
+
console.error(`Unable to restore initial language ${initialLanguage}: ${error instanceof Error ? error.message : String(error)}`);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return records.some(record => record.status === "blocked") ? 1 : 0;
|
|
106
|
+
}
|
|
107
|
+
export function parseArgs(argv) {
|
|
108
|
+
let locale;
|
|
109
|
+
let nativeName;
|
|
110
|
+
let out;
|
|
111
|
+
let printQueue = false;
|
|
112
|
+
let autoSwitch = false;
|
|
113
|
+
let all = false;
|
|
114
|
+
let limit;
|
|
115
|
+
let locales;
|
|
116
|
+
let openVersionSubmenu = true;
|
|
117
|
+
let restore;
|
|
118
|
+
let settleMs = DEFAULT_SETTLE_MS;
|
|
119
|
+
let switchTimeoutMs = DEFAULT_SWITCH_TIMEOUT_MS;
|
|
120
|
+
let coveragePath;
|
|
121
|
+
let ifMissing;
|
|
122
|
+
let tabId;
|
|
123
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
124
|
+
const arg = argv[index];
|
|
125
|
+
if (arg === undefined)
|
|
126
|
+
continue;
|
|
127
|
+
switch (arg) {
|
|
128
|
+
case "--help":
|
|
129
|
+
case "-h":
|
|
130
|
+
throw new CaptureUsageError(USAGE, 0);
|
|
131
|
+
case "--print-queue":
|
|
132
|
+
printQueue = true;
|
|
133
|
+
break;
|
|
134
|
+
case "--locale":
|
|
135
|
+
locale = requiredValue(argv, ++index, arg);
|
|
136
|
+
break;
|
|
137
|
+
case "--native":
|
|
138
|
+
nativeName = requiredValue(argv, ++index, arg);
|
|
139
|
+
break;
|
|
140
|
+
case "--out":
|
|
141
|
+
out = requiredValue(argv, ++index, arg);
|
|
142
|
+
break;
|
|
143
|
+
case "--auto-switch":
|
|
144
|
+
autoSwitch = true;
|
|
145
|
+
break;
|
|
146
|
+
case "--all":
|
|
147
|
+
all = true;
|
|
148
|
+
break;
|
|
149
|
+
case "--limit":
|
|
150
|
+
limit = parsePositiveInteger(requiredValue(argv, ++index, arg), arg);
|
|
151
|
+
break;
|
|
152
|
+
case "--locales":
|
|
153
|
+
locales = requiredValue(argv, ++index, arg).split(",").map(value => value.trim()).filter(Boolean);
|
|
154
|
+
break;
|
|
155
|
+
case "--open-version-submenu":
|
|
156
|
+
openVersionSubmenu = true;
|
|
157
|
+
break;
|
|
158
|
+
case "--no-open-version-submenu":
|
|
159
|
+
openVersionSubmenu = false;
|
|
160
|
+
break;
|
|
161
|
+
case "--restore":
|
|
162
|
+
restore = true;
|
|
163
|
+
break;
|
|
164
|
+
case "--no-restore":
|
|
165
|
+
restore = false;
|
|
166
|
+
break;
|
|
167
|
+
case "--settle-ms":
|
|
168
|
+
settleMs = parsePositiveInteger(requiredValue(argv, ++index, arg), arg);
|
|
169
|
+
break;
|
|
170
|
+
case "--switch-timeout-ms":
|
|
171
|
+
switchTimeoutMs = parsePositiveInteger(requiredValue(argv, ++index, arg), arg);
|
|
172
|
+
break;
|
|
173
|
+
case "--if-missing":
|
|
174
|
+
ifMissing = parseIfMissing(requiredValue(argv, ++index, arg));
|
|
175
|
+
break;
|
|
176
|
+
case "--tab-id":
|
|
177
|
+
tabId = requiredValue(argv, ++index, arg);
|
|
178
|
+
break;
|
|
179
|
+
case "--coverage-path":
|
|
180
|
+
coveragePath = requiredValue(argv, ++index, arg);
|
|
181
|
+
break;
|
|
182
|
+
default:
|
|
183
|
+
throw new CaptureUsageError(`Unknown option: ${arg}\n\n${USAGE}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!printQueue && !autoSwitch && (locale === undefined || nativeName === undefined)) {
|
|
187
|
+
throw new CaptureUsageError(`One-shot capture requires --locale and --native unless --auto-switch is used.\n\n${USAGE}`);
|
|
188
|
+
}
|
|
189
|
+
const root = packageRoot();
|
|
190
|
+
return {
|
|
191
|
+
locale,
|
|
192
|
+
nativeName,
|
|
193
|
+
out: resolve(root, out ?? defaultOutputPath()),
|
|
194
|
+
printQueue,
|
|
195
|
+
autoSwitch,
|
|
196
|
+
all,
|
|
197
|
+
limit,
|
|
198
|
+
locales,
|
|
199
|
+
openVersionSubmenu,
|
|
200
|
+
restore: restore ?? autoSwitch,
|
|
201
|
+
settleMs,
|
|
202
|
+
switchTimeoutMs,
|
|
203
|
+
coveragePath: resolve(root, coveragePath ?? "references/language-coverage.md"),
|
|
204
|
+
ifMissing: ifMissing ?? (autoSwitch ? "open" : "block"),
|
|
205
|
+
tabId
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function attachCapturePage(runtime, options) {
|
|
209
|
+
const runtimeEnv = {};
|
|
210
|
+
if (runtime.agent !== undefined && runtime.agent !== null)
|
|
211
|
+
runtimeEnv.agent = runtime.agent;
|
|
212
|
+
if (runtime.browser !== undefined)
|
|
213
|
+
runtimeEnv.browser = runtime.browser;
|
|
214
|
+
const attached = await attachChatGPTBrowser(runtimeEnv, {
|
|
215
|
+
existingTab: {
|
|
216
|
+
target: options.tabId === undefined
|
|
217
|
+
? { type: "selected", host: "chatgpt" }
|
|
218
|
+
: { type: "tabId", tabId: options.tabId },
|
|
219
|
+
ifMissing: options.ifMissing,
|
|
220
|
+
ifMultiple: "first",
|
|
221
|
+
requireChatGPT: true
|
|
222
|
+
},
|
|
223
|
+
url: CHATGPT_HOME
|
|
224
|
+
});
|
|
225
|
+
return attached.page;
|
|
226
|
+
}
|
|
227
|
+
function resolveSweepLanguages(options, languages) {
|
|
228
|
+
if (options.autoSwitch) {
|
|
229
|
+
const nonEnglish = nonEnglishLanguages(languages);
|
|
230
|
+
if (options.locales !== undefined) {
|
|
231
|
+
return options.locales.map(locale => {
|
|
232
|
+
const language = languages.find(candidate => candidate.bcp47.toLowerCase() === locale.toLowerCase());
|
|
233
|
+
if (language === undefined) {
|
|
234
|
+
throw new CaptureUsageError(`Locale ${locale} was not found in language coverage.`);
|
|
235
|
+
}
|
|
236
|
+
return language;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (options.all) {
|
|
240
|
+
return nonEnglish;
|
|
241
|
+
}
|
|
242
|
+
return nonEnglish.slice(0, options.limit ?? DEFAULT_LIMIT);
|
|
243
|
+
}
|
|
244
|
+
return [{
|
|
245
|
+
language: options.locale,
|
|
246
|
+
nativeName: options.nativeName,
|
|
247
|
+
bcp47: options.locale,
|
|
248
|
+
speakers: "",
|
|
249
|
+
status: ""
|
|
250
|
+
}];
|
|
251
|
+
}
|
|
252
|
+
async function captureOne(page, language, options) {
|
|
253
|
+
const warnings = [];
|
|
254
|
+
try {
|
|
255
|
+
if (options.autoSwitch) {
|
|
256
|
+
await switchLanguage(page, language, options);
|
|
257
|
+
const proof = await renderedProof(page);
|
|
258
|
+
if (!htmlLangMatches(proof.htmlLang, language.bcp47)) {
|
|
259
|
+
return blockedRecord(language, proof, warnings, "rendered_locale_mismatch", `Rendered html lang ${proof.htmlLang || "unknown"} did not match requested ${language.bcp47}.`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
await closeSettingsIfOpen(page);
|
|
263
|
+
const picker = await captureIntelligencePicker(page, options);
|
|
264
|
+
return {
|
|
265
|
+
schemaVersion: SCHEMA_VERSION,
|
|
266
|
+
status: "ok",
|
|
267
|
+
capturedAt: new Date().toISOString(),
|
|
268
|
+
requestedLocale: language.bcp47,
|
|
269
|
+
requestedNativeName: language.nativeName,
|
|
270
|
+
htmlLang: picker.htmlLang,
|
|
271
|
+
url: normalizeChatGPTUrl(picker.url),
|
|
272
|
+
menuHeading: picker.menuHeading,
|
|
273
|
+
intelligenceLabels: picker.intelligenceLabels,
|
|
274
|
+
selectedIntelligenceLabel: picker.selectedIntelligenceLabel,
|
|
275
|
+
versionFamilyLabels: picker.versionFamilyLabels,
|
|
276
|
+
modelVersionLabels: picker.modelVersionLabels,
|
|
277
|
+
warnings
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
const proof = await renderedProof(page).catch(() => ({}));
|
|
282
|
+
return blockedRecord(language, proof, warnings, "capture_failed", error instanceof Error ? error.message : String(error));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function switchLanguage(page, language, options) {
|
|
286
|
+
await openSettings(page, options);
|
|
287
|
+
await openLanguageCombobox(page);
|
|
288
|
+
await clickOptionExact(page, language.nativeName);
|
|
289
|
+
await waitForRenderedLanguage(page, language.bcp47, options.switchTimeoutMs).catch(async () => {
|
|
290
|
+
await wait(options.settleMs);
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
async function restoreLanguage(page, selectedLanguageText, options) {
|
|
294
|
+
await openSettings(page, options);
|
|
295
|
+
await openLanguageCombobox(page);
|
|
296
|
+
await clickOptionExact(page, selectedLanguageText);
|
|
297
|
+
await wait(options.settleMs);
|
|
298
|
+
await closeSettingsIfOpen(page);
|
|
299
|
+
}
|
|
300
|
+
async function openSettings(page, options) {
|
|
301
|
+
if (await isSettingsOpen(page))
|
|
302
|
+
return;
|
|
303
|
+
await closeFloatingMenus(page);
|
|
304
|
+
const profile = page.locator?.("[data-testid=\"accounts-profile-button\"]")?.last?.();
|
|
305
|
+
if (profile?.click === undefined) {
|
|
306
|
+
throw new Error("Profile menu button was not available.");
|
|
307
|
+
}
|
|
308
|
+
await profile.click();
|
|
309
|
+
await wait(options.settleMs);
|
|
310
|
+
const settings = page.locator?.("[data-testid=\"settings-menu-item\"]")?.last?.();
|
|
311
|
+
if (settings?.click === undefined) {
|
|
312
|
+
throw new Error("Settings menu item was not available.");
|
|
313
|
+
}
|
|
314
|
+
await settings.click();
|
|
315
|
+
await wait(options.settleMs);
|
|
316
|
+
if (!await isSettingsOpen(page)) {
|
|
317
|
+
throw new Error("Settings modal did not open.");
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function isSettingsOpen(page) {
|
|
321
|
+
return page.evaluate?.(() => document.querySelector("[role='dialog']") !== null
|
|
322
|
+
&& (location.hash === "#settings"
|
|
323
|
+
|| document.querySelector("[data-testid='close-button']") !== null
|
|
324
|
+
|| document.querySelectorAll("button[role='combobox']").length >= 3)) ?? false;
|
|
325
|
+
}
|
|
326
|
+
async function openLanguageCombobox(page) {
|
|
327
|
+
const combo = page.locator?.("button[role=\"combobox\"]")?.nth?.(3);
|
|
328
|
+
if (combo?.click === undefined) {
|
|
329
|
+
throw new Error("Language combobox was not available.");
|
|
330
|
+
}
|
|
331
|
+
await combo.click();
|
|
332
|
+
await wait(500);
|
|
333
|
+
}
|
|
334
|
+
async function clickOptionExact(page, label) {
|
|
335
|
+
const option = page.getByRole?.("option", { name: label, exact: true });
|
|
336
|
+
if (option?.click === undefined) {
|
|
337
|
+
throw new Error(`Language option ${label} was not available.`);
|
|
338
|
+
}
|
|
339
|
+
await option.click();
|
|
340
|
+
}
|
|
341
|
+
async function readSelectedLanguage(page) {
|
|
342
|
+
await openSettings(page, { settleMs: DEFAULT_SETTLE_MS });
|
|
343
|
+
const labels = await page.evaluate?.(() => Array.from(document.querySelectorAll("button[role='combobox']")).map(button => (button.textContent ?? "").replace(/\s+/g, " ").trim()));
|
|
344
|
+
return labels?.[3];
|
|
345
|
+
}
|
|
346
|
+
async function closeSettingsIfOpen(page) {
|
|
347
|
+
if (!await isSettingsOpen(page))
|
|
348
|
+
return;
|
|
349
|
+
const closePoints = await page.evaluate?.(() => Array.from(document.querySelectorAll("[data-testid='close-button']"))
|
|
350
|
+
.map(element => {
|
|
351
|
+
const rect = element.getBoundingClientRect();
|
|
352
|
+
return {
|
|
353
|
+
x: rect.left + rect.width / 2,
|
|
354
|
+
y: rect.top + rect.height / 2,
|
|
355
|
+
width: rect.width,
|
|
356
|
+
height: rect.height,
|
|
357
|
+
};
|
|
358
|
+
})
|
|
359
|
+
.filter(point => point.width > 0 && point.height > 0)
|
|
360
|
+
.sort((left, right) => right.x - left.x)) ?? [];
|
|
361
|
+
for (const point of closePoints) {
|
|
362
|
+
await clickPagePoint(page, point).catch(() => undefined);
|
|
363
|
+
await wait(500);
|
|
364
|
+
if (!await isSettingsOpen(page))
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const buttons = page.locator?.("[data-testid=\"close-button\"]");
|
|
368
|
+
const count = await buttons?.count?.().catch(() => 0) ?? 0;
|
|
369
|
+
for (let index = count - 1; index >= 0; index -= 1) {
|
|
370
|
+
await buttons?.nth?.(index)?.click?.().catch(() => undefined);
|
|
371
|
+
await wait(500);
|
|
372
|
+
if (!await isSettingsOpen(page))
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
await page.keyboard?.press?.("Escape").catch(() => undefined);
|
|
376
|
+
await wait(500);
|
|
377
|
+
if (await isSettingsOpen(page)) {
|
|
378
|
+
throw new Error("Settings modal did not close.");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async function closeFloatingMenus(page) {
|
|
382
|
+
await page.locator?.("body")?.click?.({ position: { x: 12, y: 12 } }).catch(() => undefined);
|
|
383
|
+
await wait(250);
|
|
384
|
+
}
|
|
385
|
+
async function captureIntelligencePicker(page, options) {
|
|
386
|
+
await closeFloatingMenus(page);
|
|
387
|
+
await openPicker(page);
|
|
388
|
+
await wait(options.settleMs);
|
|
389
|
+
const first = await readPickerState(page);
|
|
390
|
+
let modelVersionLabels = [];
|
|
391
|
+
if (options.openVersionSubmenu && first.versionFamilyLabels.length > 0) {
|
|
392
|
+
await openVersionSubmenu(page);
|
|
393
|
+
await wait(options.settleMs);
|
|
394
|
+
modelVersionLabels = (await readPickerState(page)).modelVersionLabels;
|
|
395
|
+
}
|
|
396
|
+
return { ...first, modelVersionLabels };
|
|
397
|
+
}
|
|
398
|
+
async function openPicker(page) {
|
|
399
|
+
const proCandidates = [
|
|
400
|
+
page.locator?.("form button.__composer-pill")?.last?.(),
|
|
401
|
+
page.locator?.("button.__composer-pill")?.last?.(),
|
|
402
|
+
page.getByRole?.("button", { name: /^Pro$/ })?.last?.(),
|
|
403
|
+
page.locator?.("button")?.filter?.({ hasText: /^Pro$/ })?.last?.(),
|
|
404
|
+
page.locator?.("button")?.filter?.({ hasText: /^\s*Pro\s*$/ })?.last?.(),
|
|
405
|
+
];
|
|
406
|
+
for (const proButton of proCandidates) {
|
|
407
|
+
if (proButton?.click === undefined)
|
|
408
|
+
continue;
|
|
409
|
+
await proButton.click().catch(() => undefined);
|
|
410
|
+
if (await pickerIsOpen(page))
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
await clickStructuralPickerCandidate(page).catch(() => undefined);
|
|
414
|
+
if (await pickerIsOpen(page))
|
|
415
|
+
return;
|
|
416
|
+
for (const label of localeLabels.modeLabels) {
|
|
417
|
+
const button = page.getByRole?.("button", { name: new RegExp(`^${escapeRegExp(label)}$`, "i") })?.last?.();
|
|
418
|
+
if (button?.click === undefined)
|
|
419
|
+
continue;
|
|
420
|
+
await button.click().catch(() => undefined);
|
|
421
|
+
if (await pickerIsOpen(page))
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
throw new Error("Unable to open Intelligence picker.");
|
|
425
|
+
}
|
|
426
|
+
async function clickStructuralPickerCandidate(page) {
|
|
427
|
+
const point = await page.evaluate?.(() => {
|
|
428
|
+
let targetPoint;
|
|
429
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
430
|
+
const considerButton = (button, formScoped) => {
|
|
431
|
+
const rect = button.getBoundingClientRect();
|
|
432
|
+
const text = (button.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
433
|
+
if (text.length === 0 || text.length > 32)
|
|
434
|
+
return;
|
|
435
|
+
if (rect.width < 24 || rect.height < 24 || rect.width > 220 || rect.height > 64)
|
|
436
|
+
return;
|
|
437
|
+
if (rect.bottom < window.innerHeight * 0.45)
|
|
438
|
+
return;
|
|
439
|
+
const aria = button.getAttribute("aria-label") ?? "";
|
|
440
|
+
const testId = button.getAttribute("data-testid") ?? "";
|
|
441
|
+
if (/composer-plus|send|microphone|dictat|voice|audio/i.test(`${aria} ${testId}`))
|
|
442
|
+
return;
|
|
443
|
+
const score = (formScoped ? 1000 : 0) + rect.bottom + rect.right / 10;
|
|
444
|
+
if (score > bestScore) {
|
|
445
|
+
bestScore = score;
|
|
446
|
+
targetPoint = {
|
|
447
|
+
x: rect.left + rect.width / 2,
|
|
448
|
+
y: rect.top + rect.height / 2,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
document.querySelectorAll("form button").forEach(button => considerButton(button, true));
|
|
453
|
+
if (targetPoint === undefined) {
|
|
454
|
+
document.querySelectorAll("button").forEach(button => considerButton(button, false));
|
|
455
|
+
}
|
|
456
|
+
return targetPoint;
|
|
457
|
+
});
|
|
458
|
+
if (point === undefined)
|
|
459
|
+
return false;
|
|
460
|
+
await clickPagePoint(page, point);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
async function clickPagePoint(page, point) {
|
|
464
|
+
const pageWithMouse = page;
|
|
465
|
+
if (pageWithMouse.mouse?.click !== undefined) {
|
|
466
|
+
await pageWithMouse.mouse.click(point.x, point.y);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const body = page.locator?.("body");
|
|
470
|
+
if (body?.click === undefined) {
|
|
471
|
+
throw new Error("Page does not expose pointer click support.");
|
|
472
|
+
}
|
|
473
|
+
await body.click({ position: { x: point.x, y: point.y } });
|
|
474
|
+
}
|
|
475
|
+
async function pickerIsOpen(page) {
|
|
476
|
+
await wait(300);
|
|
477
|
+
return page.evaluate?.(() => document.querySelector("[data-testid='composer-intelligence-picker-content']") !== null
|
|
478
|
+
|| document.querySelector("[role='menuitemradio']") !== null) ?? false;
|
|
479
|
+
}
|
|
480
|
+
async function readPickerState(page) {
|
|
481
|
+
return await page.evaluate?.(() => {
|
|
482
|
+
const menu = document.querySelector("[data-testid='composer-intelligence-picker-content']")
|
|
483
|
+
?? Array.from(document.querySelectorAll("[role='menu']")).find(candidate => candidate.querySelector("[role='menuitemradio']"))
|
|
484
|
+
?? document;
|
|
485
|
+
const menuTextLines = (menu.textContent ?? "").split(/\n/).map(line => line.trim()).filter(Boolean);
|
|
486
|
+
const items = Array.from(document.querySelectorAll("[role='menuitemradio'], [role='menuitem']")).map((element) => {
|
|
487
|
+
const label = (element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
488
|
+
const role = element.getAttribute("role") ?? undefined;
|
|
489
|
+
return {
|
|
490
|
+
label,
|
|
491
|
+
role,
|
|
492
|
+
checked: element.getAttribute("aria-checked") === "true",
|
|
493
|
+
expanded: element.getAttribute("aria-expanded") === "true",
|
|
494
|
+
hasPopup: element.getAttribute("aria-haspopup") ?? undefined,
|
|
495
|
+
testId: element.getAttribute("data-testid") ?? undefined,
|
|
496
|
+
ariaLabel: element.getAttribute("aria-label") ?? undefined,
|
|
497
|
+
};
|
|
498
|
+
}).filter(item => item.label.length > 0 || item.ariaLabel !== undefined || item.testId !== undefined);
|
|
499
|
+
const radioLabels = items
|
|
500
|
+
.filter(item => item.role === "menuitemradio" && item.label.length > 0)
|
|
501
|
+
.map(item => item.label);
|
|
502
|
+
const versionLabelPattern = /^(?:o\d+|\d+(?:\.\d+)?)$/i;
|
|
503
|
+
const modelVersionLabels = radioLabels.filter(label => versionLabelPattern.test(label));
|
|
504
|
+
const intelligenceLabels = radioLabels.filter(label => !versionLabelPattern.test(label));
|
|
505
|
+
const selectedIntelligenceLabel = items.find(item => item.role === "menuitemradio"
|
|
506
|
+
&& item.checked
|
|
507
|
+
&& item.label.length > 0
|
|
508
|
+
&& !versionLabelPattern.test(item.label))?.label;
|
|
509
|
+
const versionFamilyLabels = items
|
|
510
|
+
.filter(item => item.role === "menuitem" && /^GPT[\s-]/i.test(item.label))
|
|
511
|
+
.map(item => item.label);
|
|
512
|
+
const result = {
|
|
513
|
+
htmlLang: document.documentElement.lang,
|
|
514
|
+
url: location.href,
|
|
515
|
+
intelligenceLabels,
|
|
516
|
+
versionFamilyLabels,
|
|
517
|
+
modelVersionLabels,
|
|
518
|
+
items,
|
|
519
|
+
};
|
|
520
|
+
const heading = menuTextLines.find(line => !radioLabels.includes(line) && !/^GPT[\s-]/i.test(line));
|
|
521
|
+
if (heading !== undefined)
|
|
522
|
+
result.menuHeading = heading;
|
|
523
|
+
if (selectedIntelligenceLabel !== undefined)
|
|
524
|
+
result.selectedIntelligenceLabel = selectedIntelligenceLabel;
|
|
525
|
+
return result;
|
|
526
|
+
}) ?? {
|
|
527
|
+
htmlLang: "",
|
|
528
|
+
url: "",
|
|
529
|
+
intelligenceLabels: [],
|
|
530
|
+
versionFamilyLabels: [],
|
|
531
|
+
modelVersionLabels: []
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
async function openVersionSubmenu(page) {
|
|
535
|
+
const gptCandidates = [
|
|
536
|
+
page.getByRole?.("menuitem", { name: /^GPT[\s-]/i })?.last?.(),
|
|
537
|
+
page.locator?.("[role='menuitem']")?.filter?.({ hasText: /^GPT[\s-]/i })?.last?.(),
|
|
538
|
+
];
|
|
539
|
+
for (const gptMenu of gptCandidates) {
|
|
540
|
+
if (gptMenu?.click === undefined)
|
|
541
|
+
continue;
|
|
542
|
+
await gptMenu.click().catch(() => undefined);
|
|
543
|
+
if ((await readPickerState(page)).modelVersionLabels.length > 0)
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async function renderedProof(page) {
|
|
548
|
+
return page.evaluate?.(() => ({
|
|
549
|
+
htmlLang: document.documentElement.lang,
|
|
550
|
+
url: location.href
|
|
551
|
+
})) ?? {};
|
|
552
|
+
}
|
|
553
|
+
async function waitForRenderedLanguage(page, bcp47, timeoutMs) {
|
|
554
|
+
const deadline = Date.now() + timeoutMs;
|
|
555
|
+
for (;;) {
|
|
556
|
+
const proof = await renderedProof(page);
|
|
557
|
+
if (htmlLangMatches(proof.htmlLang, bcp47))
|
|
558
|
+
return;
|
|
559
|
+
if (Date.now() >= deadline) {
|
|
560
|
+
throw new Error(`Timed out waiting for rendered language ${bcp47}; html lang is ${proof.htmlLang ?? "unknown"}.`);
|
|
561
|
+
}
|
|
562
|
+
await wait(500);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function blockedRecord(language, proof, warnings, code, message) {
|
|
566
|
+
const record = {
|
|
567
|
+
schemaVersion: SCHEMA_VERSION,
|
|
568
|
+
status: "blocked",
|
|
569
|
+
capturedAt: new Date().toISOString(),
|
|
570
|
+
requestedLocale: language.bcp47,
|
|
571
|
+
requestedNativeName: language.nativeName,
|
|
572
|
+
warnings,
|
|
573
|
+
blocker: {
|
|
574
|
+
kind: "selector_drift",
|
|
575
|
+
code,
|
|
576
|
+
message
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
if (proof.htmlLang !== undefined)
|
|
580
|
+
record.htmlLang = proof.htmlLang;
|
|
581
|
+
if (proof.url !== undefined)
|
|
582
|
+
record.url = normalizeChatGPTUrl(proof.url);
|
|
583
|
+
return record;
|
|
584
|
+
}
|
|
585
|
+
async function appendRecord(path, record) {
|
|
586
|
+
await mkdir(dirname(path), { recursive: true });
|
|
587
|
+
await appendFile(path, `${JSON.stringify(record)}\n`, "utf8");
|
|
588
|
+
}
|
|
589
|
+
function printCaptureRecord(record, out) {
|
|
590
|
+
if (record.status === "ok") {
|
|
591
|
+
console.log(`captured ${record.requestedLocale} htmlLang=${record.htmlLang ?? "unknown"} intelligence=${record.intelligenceLabels?.length ?? 0} versions=${record.modelVersionLabels?.length ?? 0} out=${out}`);
|
|
592
|
+
}
|
|
593
|
+
else {
|
|
594
|
+
console.log(`blocked ${record.requestedLocale} htmlLang=${record.htmlLang ?? "unknown"} code=${record.blocker?.code ?? "unknown"} out=${out}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
function printQueue(languages) {
|
|
598
|
+
for (const language of languages) {
|
|
599
|
+
console.log(`${language.bcp47.padEnd(8)} ${language.nativeName}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
function htmlLangMatches(htmlLang, bcp47) {
|
|
603
|
+
if (htmlLang === undefined || htmlLang.length === 0)
|
|
604
|
+
return false;
|
|
605
|
+
const actual = htmlLang.toLowerCase();
|
|
606
|
+
const expected = bcp47.toLowerCase();
|
|
607
|
+
if (actual === expected)
|
|
608
|
+
return true;
|
|
609
|
+
if (expected === "zh-hans")
|
|
610
|
+
return actual === "zh-cn" || actual.includes("hans");
|
|
611
|
+
if (expected === "zh-hk")
|
|
612
|
+
return actual.includes("hk");
|
|
613
|
+
if (expected === "zh-tw")
|
|
614
|
+
return actual.includes("tw");
|
|
615
|
+
const [base] = expected.split("-");
|
|
616
|
+
return base !== undefined && actual === base || (base !== undefined && actual.startsWith(`${base}-`));
|
|
617
|
+
}
|
|
618
|
+
function normalizeChatGPTUrl(url) {
|
|
619
|
+
try {
|
|
620
|
+
const parsed = new URL(url);
|
|
621
|
+
if (!/chatgpt\.com$/.test(parsed.hostname))
|
|
622
|
+
return parsed.origin;
|
|
623
|
+
return `${parsed.origin}${parsed.pathname}${parsed.hash === "#settings" ? "#settings" : ""}`;
|
|
624
|
+
}
|
|
625
|
+
catch {
|
|
626
|
+
return url.slice(0, 120);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function requiredValue(argv, index, flag) {
|
|
630
|
+
const value = argv[index];
|
|
631
|
+
if (value === undefined || value.startsWith("--")) {
|
|
632
|
+
throw new CaptureUsageError(`${flag} requires a value.\n\n${USAGE}`);
|
|
633
|
+
}
|
|
634
|
+
return value;
|
|
635
|
+
}
|
|
636
|
+
function parsePositiveInteger(value, flag) {
|
|
637
|
+
const parsed = Number.parseInt(value, 10);
|
|
638
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
639
|
+
throw new CaptureUsageError(`${flag} must be a positive integer.`);
|
|
640
|
+
}
|
|
641
|
+
return parsed;
|
|
642
|
+
}
|
|
643
|
+
function parseIfMissing(value) {
|
|
644
|
+
if (value === "block" || value === "open" || value === "create")
|
|
645
|
+
return value;
|
|
646
|
+
throw new CaptureUsageError("--if-missing must be one of: block, open, create.");
|
|
647
|
+
}
|
|
648
|
+
function defaultOutputPath() {
|
|
649
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
650
|
+
return `../../outputs/intelligence-locale-captures/${date}-intelligence-picker.jsonl`;
|
|
651
|
+
}
|
|
652
|
+
function packageRoot() {
|
|
653
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
654
|
+
for (const candidate of [resolve(scriptDir, "../.."), resolve(scriptDir, "../../..")]) {
|
|
655
|
+
if (existsSync(resolve(candidate, "package.json")))
|
|
656
|
+
return candidate;
|
|
657
|
+
}
|
|
658
|
+
return resolve(scriptDir, "../..");
|
|
659
|
+
}
|
|
660
|
+
function wait(ms) {
|
|
661
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
662
|
+
}
|
|
663
|
+
function escapeRegExp(value) {
|
|
664
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
665
|
+
}
|
|
666
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
667
|
+
const exitCode = await main();
|
|
668
|
+
process.exitCode = exitCode;
|
|
669
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { validateSurfaceDrift } from "../testing/surface-drift.js";
|
|
2
|
+
const report = validateSurfaceDrift();
|
|
3
|
+
console.log([
|
|
4
|
+
"Validated docs/surface drift gate:",
|
|
5
|
+
`- ${report.commandCount} backend commands`,
|
|
6
|
+
`- ${report.descriptorCount} command descriptors`,
|
|
7
|
+
`- ${report.blockerKindCount} blocker kinds`,
|
|
8
|
+
`- ${report.pythonCommandCount} Python-covered commands`,
|
|
9
|
+
`- ${report.generatedDocsChecked} generated blocker doc sections`,
|
|
10
|
+
`- ${report.docAnchorsChecked} doc anchors`
|
|
11
|
+
].join("\n"));
|