codex-chatgpt-control 0.1.0-alpha.1 → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +55 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +8 -0
  4. package/contracts/v1/fixtures/blocker-explanation-profiles.json +114 -0
  5. package/contracts/v1/fixtures/command-descriptors.json +224 -16
  6. package/contracts/v1/fixtures/doctor-bridge-upload.json +8 -0
  7. package/contracts/v1/fixtures/doctor-scenario-preflight.json +119 -0
  8. package/contracts/v1/fixtures/existing-tab-diagnostics-blocker.json +57 -0
  9. package/contracts/v1/fixtures/files-preflight-success.json +32 -0
  10. package/contracts/v1/fixtures/help-root.json +1 -1
  11. package/contracts/v1/fixtures/output-json-parse-success.json +17 -2
  12. package/contracts/v1/fixtures/project-sources-plan-add.json +71 -0
  13. package/contracts/v1/fixtures/read-latest-clipped.json +31 -0
  14. package/contracts/v1/fixtures/reports-create-redacted.json +15 -0
  15. package/contracts/v1/fixtures/responses-basic-success.json +11 -0
  16. package/contracts/v1/fixtures/run-basic-success.json +15 -0
  17. package/contracts/v1/fixtures/run-browser-bridge-blocker.json +2 -2
  18. package/contracts/v1/fixtures/run-timeout-partial.json +2 -2
  19. package/contracts/v1/fixtures/runner-budget-blocker.json +1 -1
  20. package/contracts/v1/fixtures/stream-blocked.ndjson +1 -1
  21. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
  22. package/contracts/v1/manifest.json +30 -0
  23. package/contracts/v1/parity-suite.json +80 -7
  24. package/contracts/v1/schemas/backend-request.schema.json +3 -0
  25. package/contracts/v1/schemas/command-result.schema.json +48 -0
  26. package/contracts/v1/schemas/response.schema.json +28 -0
  27. package/contracts/v1/schemas/run-result.schema.json +84 -0
  28. package/contracts/v1/surface-drift-policy.json +210 -0
  29. package/dist/codex-chatgpt-control-backend.mjs +7478 -1789
  30. package/dist/codex-chatgpt-control.bundle.mjs +8023 -2266
  31. package/dist/src/backend/client.d.ts +6 -1
  32. package/dist/src/backend/client.js +5 -0
  33. package/dist/src/backend/protocol.d.ts +1 -1
  34. package/dist/src/backend/protocol.js +8 -0
  35. package/dist/src/backend/session.js +16 -0
  36. package/dist/src/browser/attach.js +128 -20
  37. package/dist/src/browser/clipboard.d.ts +11 -0
  38. package/dist/src/browser/clipboard.js +34 -7
  39. package/dist/src/browser/downloads.js +2 -1
  40. package/dist/src/browser/page-state.js +18 -5
  41. package/dist/src/client.d.ts +17 -1
  42. package/dist/src/client.js +89 -9
  43. package/dist/src/commands/artifacts.d.ts +5 -0
  44. package/dist/src/commands/artifacts.js +432 -0
  45. package/dist/src/commands/context.js +8 -3
  46. package/dist/src/commands/conversation.d.ts +15 -0
  47. package/dist/src/commands/conversation.js +44 -0
  48. package/dist/src/commands/deadline.d.ts +8 -0
  49. package/dist/src/commands/deadline.js +14 -0
  50. package/dist/src/commands/doctor.d.ts +10 -2
  51. package/dist/src/commands/doctor.js +249 -14
  52. package/dist/src/commands/files.d.ts +2 -1
  53. package/dist/src/commands/files.js +406 -18
  54. package/dist/src/commands/messages.d.ts +2 -1
  55. package/dist/src/commands/messages.js +430 -98
  56. package/dist/src/commands/modes.d.ts +4 -1
  57. package/dist/src/commands/modes.js +404 -37
  58. package/dist/src/commands/probes.d.ts +15 -0
  59. package/dist/src/commands/probes.js +64 -0
  60. package/dist/src/commands/project-sources.d.ts +10 -0
  61. package/dist/src/commands/project-sources.js +643 -0
  62. package/dist/src/commands/registry.js +108 -14
  63. package/dist/src/commands/reports.d.ts +9 -0
  64. package/dist/src/commands/reports.js +84 -7
  65. package/dist/src/commands/response-actions.js +33 -0
  66. package/dist/src/commands/sequence.js +14 -0
  67. package/dist/src/commands/threads.js +4 -25
  68. package/dist/src/commands/timeouts.d.ts +2 -0
  69. package/dist/src/commands/timeouts.js +18 -0
  70. package/dist/src/diagnostics/blockers.d.ts +54 -0
  71. package/dist/src/diagnostics/blockers.js +299 -0
  72. package/dist/src/dom/artifacts.d.ts +7 -0
  73. package/dist/src/dom/artifacts.js +198 -0
  74. package/dist/src/dom/generation-state.d.ts +15 -0
  75. package/dist/src/dom/generation-state.js +109 -0
  76. package/dist/src/dom/label-match.d.ts +3 -0
  77. package/dist/src/dom/label-match.js +25 -0
  78. package/dist/src/dom/locale/am.d.ts +34 -0
  79. package/dist/src/dom/locale/am.js +34 -0
  80. package/dist/src/dom/locale/ar.d.ts +35 -0
  81. package/dist/src/dom/locale/ar.js +35 -0
  82. package/dist/src/dom/locale/bg.d.ts +35 -0
  83. package/dist/src/dom/locale/bg.js +35 -0
  84. package/dist/src/dom/locale/bn.d.ts +35 -0
  85. package/dist/src/dom/locale/bn.js +35 -0
  86. package/dist/src/dom/locale/bs.d.ts +34 -0
  87. package/dist/src/dom/locale/bs.js +34 -0
  88. package/dist/src/dom/locale/ca.d.ts +34 -0
  89. package/dist/src/dom/locale/ca.js +34 -0
  90. package/dist/src/dom/locale/cs.d.ts +34 -0
  91. package/dist/src/dom/locale/cs.js +34 -0
  92. package/dist/src/dom/locale/da.d.ts +33 -0
  93. package/dist/src/dom/locale/da.js +33 -0
  94. package/dist/src/dom/locale/de.d.ts +36 -0
  95. package/dist/src/dom/locale/de.js +36 -0
  96. package/dist/src/dom/locale/el.d.ts +34 -0
  97. package/dist/src/dom/locale/el.js +34 -0
  98. package/dist/src/dom/locale/en.d.ts +64 -0
  99. package/dist/src/dom/locale/en.js +71 -0
  100. package/dist/src/dom/locale/es-419.d.ts +34 -0
  101. package/dist/src/dom/locale/es-419.js +34 -0
  102. package/dist/src/dom/locale/es-ES.d.ts +34 -0
  103. package/dist/src/dom/locale/es-ES.js +34 -0
  104. package/dist/src/dom/locale/et.d.ts +34 -0
  105. package/dist/src/dom/locale/et.js +34 -0
  106. package/dist/src/dom/locale/fa.d.ts +35 -0
  107. package/dist/src/dom/locale/fa.js +35 -0
  108. package/dist/src/dom/locale/fi.d.ts +34 -0
  109. package/dist/src/dom/locale/fi.js +34 -0
  110. package/dist/src/dom/locale/fr-CA.d.ts +35 -0
  111. package/dist/src/dom/locale/fr-CA.js +35 -0
  112. package/dist/src/dom/locale/fr-FR.d.ts +35 -0
  113. package/dist/src/dom/locale/fr-FR.js +35 -0
  114. package/dist/src/dom/locale/gu.d.ts +34 -0
  115. package/dist/src/dom/locale/gu.js +34 -0
  116. package/dist/src/dom/locale/hi.d.ts +34 -0
  117. package/dist/src/dom/locale/hi.js +34 -0
  118. package/dist/src/dom/locale/hr.d.ts +33 -0
  119. package/dist/src/dom/locale/hr.js +33 -0
  120. package/dist/src/dom/locale/hu.d.ts +34 -0
  121. package/dist/src/dom/locale/hu.js +34 -0
  122. package/dist/src/dom/locale/hy.d.ts +35 -0
  123. package/dist/src/dom/locale/hy.js +35 -0
  124. package/dist/src/dom/locale/id.d.ts +34 -0
  125. package/dist/src/dom/locale/id.js +34 -0
  126. package/dist/src/dom/locale/index.d.ts +83 -0
  127. package/dist/src/dom/locale/index.js +239 -0
  128. package/dist/src/dom/locale/is.d.ts +34 -0
  129. package/dist/src/dom/locale/is.js +34 -0
  130. package/dist/src/dom/locale/it.d.ts +33 -0
  131. package/dist/src/dom/locale/it.js +33 -0
  132. package/dist/src/dom/locale/ja.d.ts +33 -0
  133. package/dist/src/dom/locale/ja.js +33 -0
  134. package/dist/src/dom/locale/ka.d.ts +34 -0
  135. package/dist/src/dom/locale/ka.js +34 -0
  136. package/dist/src/dom/locale/kk.d.ts +34 -0
  137. package/dist/src/dom/locale/kk.js +34 -0
  138. package/dist/src/dom/locale/kn.d.ts +35 -0
  139. package/dist/src/dom/locale/kn.js +35 -0
  140. package/dist/src/dom/locale/ko.d.ts +34 -0
  141. package/dist/src/dom/locale/ko.js +34 -0
  142. package/dist/src/dom/locale/lt.d.ts +35 -0
  143. package/dist/src/dom/locale/lt.js +35 -0
  144. package/dist/src/dom/locale/lv.d.ts +34 -0
  145. package/dist/src/dom/locale/lv.js +34 -0
  146. package/dist/src/dom/locale/mk.d.ts +33 -0
  147. package/dist/src/dom/locale/mk.js +33 -0
  148. package/dist/src/dom/locale/ml.d.ts +35 -0
  149. package/dist/src/dom/locale/ml.js +35 -0
  150. package/dist/src/dom/locale/mn.d.ts +35 -0
  151. package/dist/src/dom/locale/mn.js +35 -0
  152. package/dist/src/dom/locale/mr.d.ts +35 -0
  153. package/dist/src/dom/locale/mr.js +35 -0
  154. package/dist/src/dom/locale/ms.d.ts +34 -0
  155. package/dist/src/dom/locale/ms.js +34 -0
  156. package/dist/src/dom/locale/my.d.ts +34 -0
  157. package/dist/src/dom/locale/my.js +34 -0
  158. package/dist/src/dom/locale/nb.d.ts +34 -0
  159. package/dist/src/dom/locale/nb.js +34 -0
  160. package/dist/src/dom/locale/nl.d.ts +34 -0
  161. package/dist/src/dom/locale/nl.js +34 -0
  162. package/dist/src/dom/locale/pa.d.ts +35 -0
  163. package/dist/src/dom/locale/pa.js +35 -0
  164. package/dist/src/dom/locale/pl.d.ts +34 -0
  165. package/dist/src/dom/locale/pl.js +34 -0
  166. package/dist/src/dom/locale/pt-BR.d.ts +34 -0
  167. package/dist/src/dom/locale/pt-BR.js +34 -0
  168. package/dist/src/dom/locale/pt-PT.d.ts +34 -0
  169. package/dist/src/dom/locale/pt-PT.js +34 -0
  170. package/dist/src/dom/locale/ro.d.ts +33 -0
  171. package/dist/src/dom/locale/ro.js +33 -0
  172. package/dist/src/dom/locale/ru.d.ts +31 -0
  173. package/dist/src/dom/locale/ru.js +31 -0
  174. package/dist/src/dom/locale/sk.d.ts +34 -0
  175. package/dist/src/dom/locale/sk.js +34 -0
  176. package/dist/src/dom/locale/sl.d.ts +34 -0
  177. package/dist/src/dom/locale/sl.js +34 -0
  178. package/dist/src/dom/locale/so.d.ts +34 -0
  179. package/dist/src/dom/locale/so.js +34 -0
  180. package/dist/src/dom/locale/sq.d.ts +34 -0
  181. package/dist/src/dom/locale/sq.js +34 -0
  182. package/dist/src/dom/locale/sr.d.ts +31 -0
  183. package/dist/src/dom/locale/sr.js +31 -0
  184. package/dist/src/dom/locale/sv.d.ts +34 -0
  185. package/dist/src/dom/locale/sv.js +34 -0
  186. package/dist/src/dom/locale/sw.d.ts +34 -0
  187. package/dist/src/dom/locale/sw.js +34 -0
  188. package/dist/src/dom/locale/ta.d.ts +35 -0
  189. package/dist/src/dom/locale/ta.js +35 -0
  190. package/dist/src/dom/locale/te.d.ts +35 -0
  191. package/dist/src/dom/locale/te.js +35 -0
  192. package/dist/src/dom/locale/th.d.ts +34 -0
  193. package/dist/src/dom/locale/th.js +34 -0
  194. package/dist/src/dom/locale/tl.d.ts +28 -0
  195. package/dist/src/dom/locale/tl.js +28 -0
  196. package/dist/src/dom/locale/tr.d.ts +34 -0
  197. package/dist/src/dom/locale/tr.js +34 -0
  198. package/dist/src/dom/locale/types.d.ts +52 -0
  199. package/dist/src/dom/locale/types.js +1 -0
  200. package/dist/src/dom/locale/uk.d.ts +34 -0
  201. package/dist/src/dom/locale/uk.js +34 -0
  202. package/dist/src/dom/locale/ur.d.ts +34 -0
  203. package/dist/src/dom/locale/ur.js +34 -0
  204. package/dist/src/dom/locale/vi.d.ts +34 -0
  205. package/dist/src/dom/locale/vi.js +34 -0
  206. package/dist/src/dom/locale/zh-HK.d.ts +35 -0
  207. package/dist/src/dom/locale/zh-HK.js +35 -0
  208. package/dist/src/dom/locale/zh-Hans.d.ts +35 -0
  209. package/dist/src/dom/locale/zh-Hans.js +35 -0
  210. package/dist/src/dom/locale/zh-TW.d.ts +36 -0
  211. package/dist/src/dom/locale/zh-TW.js +36 -0
  212. package/dist/src/dom/locale-labels.d.ts +26 -0
  213. package/dist/src/dom/locale-labels.js +26 -0
  214. package/dist/src/dom/menus.d.ts +6 -0
  215. package/dist/src/dom/menus.js +47 -10
  216. package/dist/src/dom/message-format.d.ts +2 -1
  217. package/dist/src/dom/message-format.js +67 -10
  218. package/dist/src/dom/messages.d.ts +2 -1
  219. package/dist/src/dom/messages.js +2 -4
  220. package/dist/src/dom/selectors.d.ts +1 -0
  221. package/dist/src/dom/selectors.js +42 -15
  222. package/dist/src/dom/wait-snapshot.d.ts +43 -0
  223. package/dist/src/dom/wait-snapshot.js +127 -0
  224. package/dist/src/index.d.ts +5 -0
  225. package/dist/src/index.js +5 -0
  226. package/dist/src/platform/local-paths.d.ts +4 -0
  227. package/dist/src/platform/local-paths.js +20 -0
  228. package/dist/src/runner/interruptions.js +15 -4
  229. package/dist/src/runner/responses.js +18 -1
  230. package/dist/src/runner/result.js +15 -0
  231. package/dist/src/runner/resume.js +1 -0
  232. package/dist/src/runner/types.d.ts +3 -0
  233. package/dist/src/safety/risk.d.ts +8 -0
  234. package/dist/src/safety/risk.js +8 -0
  235. package/dist/src/safety/untrusted-output.d.ts +73 -0
  236. package/dist/src/safety/untrusted-output.js +199 -0
  237. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +1 -0
  238. package/dist/src/scripts/apply-intelligence-locale-captures.js +270 -0
  239. package/dist/src/scripts/capture-intelligence-locales.d.ts +25 -0
  240. package/dist/src/scripts/capture-intelligence-locales.js +669 -0
  241. package/dist/src/scripts/check-surface-drift.d.ts +1 -0
  242. package/dist/src/scripts/check-surface-drift.js +11 -0
  243. package/dist/src/scripts/live-smoke/scenarios.js +84 -0
  244. package/dist/src/scripts/locale-capture/language-coverage.d.ts +10 -0
  245. package/dist/src/scripts/locale-capture/language-coverage.js +39 -0
  246. package/dist/src/testing/surface-drift.d.ts +66 -0
  247. package/dist/src/testing/surface-drift.js +363 -0
  248. package/dist/src/types.d.ts +229 -2
  249. package/package.json +3 -2
  250. package/references/backend-protocol.md +96 -2
  251. package/references/language-coverage.md +142 -0
  252. package/references/localization.md +257 -0
  253. package/references/python-parity.md +67 -3
  254. package/references/safety.md +2 -0
  255. package/references/troubleshooting.md +83 -2
@@ -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
+ }
@@ -1,13 +1,21 @@
1
- import type { CommandResult, RuntimeEnv } from "../types.js";
2
- export type DoctorCheckName = "bridge" | "login" | "upload" | "download" | "clipboard" | "modes" | "tools" | "selectors";
1
+ import type { BootstrapArgs, CommandResult, RuntimeEnv } from "../types.js";
2
+ import type { RunReportOptions } from "./reports.js";
3
+ export type DoctorCheckName = "bridge" | "login" | "upload" | "download" | "clipboard" | "modes" | "tools" | "selectors" | "existing_tab" | "artifacts" | "file_preflight" | "localization" | "reports";
3
4
  export type CapabilityStatus = "ok" | "blocked" | "unsupported" | "unknown";
4
5
  export type CapabilityCheck = {
5
6
  status: CapabilityStatus;
6
7
  message: string;
7
8
  remediation?: string[];
9
+ code?: string;
10
+ blockerKind?: string;
11
+ nextCommand?: string;
12
+ details?: Record<string, unknown>;
8
13
  };
9
14
  export type DoctorArgs = {
10
15
  check?: DoctorCheckName[];
16
+ existingTab?: BootstrapArgs["existingTab"];
17
+ files?: string[];
18
+ report?: RunReportOptions;
11
19
  };
12
20
  export type DoctorReport = {
13
21
  ready: boolean;
@@ -1,21 +1,51 @@
1
+ import { constants } from "node:fs";
2
+ import { access, stat } from "node:fs/promises";
1
3
  import { readSystemClipboard } from "../browser/clipboard.js";
2
4
  import { readPageState } from "../browser/page-state.js";
5
+ import { localeLabels } from "../dom/locale-labels.js";
3
6
  import { BROWSER_BRIDGE_REMEDIATION, resultOk } from "../errors.js";
7
+ import { explainCommandBlocker } from "../diagnostics/blockers.js";
8
+ import { preflightFiles } from "./files.js";
4
9
  import { contextFromPage } from "./context.js";
5
10
  import { bootstrap } from "./session.js";
6
11
  const DEFAULT_CHECKS = ["bridge", "login", "upload", "download", "clipboard", "modes", "tools", "selectors"];
12
+ const BOOTSTRAP_CHECKS = new Set(["bridge", "login", "upload", "download", "modes", "tools", "selectors"]);
7
13
  const UPLOAD_REMEDIATION = [
8
14
  "Codex Settings > Computer Use > Chrome > Permissions > Uploads: set to Always allow, or add chatgpt.com to the allowed upload domains.",
9
15
  "Chrome chrome://extensions > Codex extension > Details: enable Allow access to file URLs."
10
16
  ];
17
+ const REQUIRED_LOCALE_KEYS = [
18
+ "composerTextbox",
19
+ "sendButton",
20
+ "searchChatsButton",
21
+ "searchChatsPlaceholder",
22
+ "newChat",
23
+ "addFilesButton",
24
+ "addPhotosFilesMenuItem",
25
+ "copyResponse",
26
+ "download",
27
+ "modeLabels",
28
+ "signedInMarkers",
29
+ "loginBlocker",
30
+ "captchaBlocker",
31
+ "rateLimitBlocker"
32
+ ];
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"];
11
35
  export async function doctor(env, args = {}) {
12
36
  const wanted = args.check ?? DEFAULT_CHECKS;
13
37
  const checks = {};
14
- const boot = await bootstrap(env, { preferExistingTab: true, timeoutMs: 30000 });
38
+ const wantsExistingTab = wanted.includes("existing_tab");
39
+ const existingTab = wantsExistingTab ? normalizeDoctorExistingTab(args.existingTab) : undefined;
40
+ const boot = wantsExistingTab || wanted.some(check => BOOTSTRAP_CHECKS.has(check))
41
+ ? await bootstrap(env, existingTab === undefined
42
+ ? { preferExistingTab: true, timeoutMs: 30000 }
43
+ : { existingTab, preferExistingTab: false, timeoutMs: 30000 })
44
+ : undefined;
15
45
  for (const check of wanted) {
16
46
  switch (check) {
17
47
  case "bridge":
18
- checks.bridge = boot.ok
48
+ checks.bridge = boot?.ok
19
49
  ? ok("Chrome bridge is available.")
20
50
  : bridgeCheck(boot);
21
51
  break;
@@ -40,14 +70,34 @@ export async function doctor(env, args = {}) {
40
70
  case "selectors":
41
71
  checks.selectors = selectorCheck(env, "Basic page selectors are available.");
42
72
  break;
73
+ case "existing_tab":
74
+ checks.existing_tab = existingTab === undefined || boot === undefined
75
+ ? blocked("Existing-tab readiness was requested, but bootstrap was not initialized.")
76
+ : existingTabCheck(existingTab, boot);
77
+ break;
78
+ case "artifacts":
79
+ checks.artifacts = artifactsCheck(env);
80
+ break;
81
+ case "file_preflight":
82
+ checks.file_preflight = await filePreflightCheck(env, args);
83
+ break;
84
+ case "localization":
85
+ checks.localization = localizationCheck(env);
86
+ break;
87
+ case "reports":
88
+ checks.reports = await reportsCheck(args.report);
89
+ break;
43
90
  }
44
91
  }
45
92
  const ready = Object.values(checks).every(check => check?.status === "ok" || check?.status === "unknown");
46
93
  return resultOk({ ready, checks }, await contextFromPage(env.page));
47
94
  }
48
95
  function bridgeCheck(boot) {
96
+ if (boot === undefined) {
97
+ return unknown("Bridge readiness was not requested.");
98
+ }
49
99
  if (boot.blocker?.kind === "browser_bridge_unavailable") {
50
- return blocked(boot.blocker.message, bridgeRemediation(boot));
100
+ return withBlockerDetails(blocked(boot.blocker.message, bridgeRemediation(boot)), boot, "session.bootstrap");
51
101
  }
52
102
  if (boot.blocker?.kind === "login_required") {
53
103
  return ok("Chrome bridge is available; ChatGPT login is required before browser-control commands can continue.");
@@ -58,11 +108,11 @@ function bridgeCheck(boot) {
58
108
  return blocked(boot.error?.message ?? "Chrome bridge is unavailable.");
59
109
  }
60
110
  async function loginCheck(env, boot) {
61
- if (!boot.ok && boot.blocker?.kind === "login_required") {
62
- return blocked("ChatGPT login is required.", ["Ask the user to sign in to ChatGPT in Chrome, then retry."]);
111
+ if (boot !== undefined && !boot.ok && boot.blocker?.kind === "login_required") {
112
+ return withBlockerDetails(blocked("ChatGPT login is required.", ["Ask the user to sign in to ChatGPT in Chrome, then retry."]), boot, "session.bootstrap");
63
113
  }
64
114
  if (env.page === undefined) {
65
- return boot.ok ? ok("Bootstrap completed; login appears usable.") : blocked("Cannot determine login because bootstrap failed.");
115
+ return boot?.ok ? ok("Bootstrap completed; login appears usable.") : blocked("Cannot determine login because bootstrap failed.");
66
116
  }
67
117
  const state = await readPageState(env.page).catch(() => undefined);
68
118
  if (state?.blocker?.kind === "login_required") {
@@ -102,19 +152,204 @@ function selectorCheck(env, message) {
102
152
  ? ok(message)
103
153
  : unsupported("The active page object does not expose locator or role selector helpers.");
104
154
  }
155
+ function existingTabCheck(existingTab, boot) {
156
+ if (boot.ok) {
157
+ return ok("Existing ChatGPT tab target can be claimed.", {
158
+ target: existingTab.target,
159
+ tabId: boot.context.tabId,
160
+ url: boot.context.url,
161
+ conversationId: boot.context.conversationId
162
+ });
163
+ }
164
+ return withBlockerDetails(blocked(boot.blocker?.message ?? boot.error?.message ?? "Existing ChatGPT tab target could not be claimed."), boot, "session.bootstrap");
165
+ }
166
+ function normalizeDoctorExistingTab(existingTab) {
167
+ if (existingTab !== undefined && existingTab !== true && existingTab !== false) {
168
+ return {
169
+ requireChatGPT: true,
170
+ ifMissing: "block",
171
+ ifMultiple: existingTab.target?.type === "selected" ? "first" : "block",
172
+ ...existingTab
173
+ };
174
+ }
175
+ return {
176
+ target: { type: "selected", host: "chatgpt" },
177
+ ifMissing: "block",
178
+ ifMultiple: "block",
179
+ requireChatGPT: true
180
+ };
181
+ }
182
+ function artifactsCheck(env) {
183
+ const page = env.page;
184
+ if (page === undefined) {
185
+ return unknown("Artifact readiness requires an already bootstrapped ChatGPT page.", undefined, {
186
+ pageAvailable: false
187
+ }, "session.bootstrap");
188
+ }
189
+ const selectorsAvailable = typeof page.locator === "function" || typeof page.getByRole === "function";
190
+ const downloadEventsAvailable = typeof page.waitForEvent === "function";
191
+ const domEvaluateAvailable = typeof page.evaluate === "function";
192
+ const pageAssetsAvailable = typeof page.capabilities?.get === "function";
193
+ const details = {
194
+ pageAvailable: true,
195
+ selectorsAvailable,
196
+ downloadEventsAvailable,
197
+ domEvaluateAvailable,
198
+ pageAssetsAvailable
199
+ };
200
+ if (selectorsAvailable && (downloadEventsAvailable || domEvaluateAvailable || pageAssetsAvailable)) {
201
+ return ok("Artifact primitives can inspect the current page without requesting generation.", details);
202
+ }
203
+ return unknown("Artifact primitives need selector support plus download, DOM, or page-assets support to prove readiness.", undefined, details);
204
+ }
205
+ async function filePreflightCheck(env, args) {
206
+ const paths = args.files ?? [];
207
+ const result = await preflightFiles(env, { paths });
208
+ const pathCount = paths.length;
209
+ if (result.ok && result.data !== undefined) {
210
+ return ok(pathCount === 0
211
+ ? "No file paths were supplied; file preflight has no local files to validate."
212
+ : "File preflight completed without blocking local file issues.", {
213
+ pathCount,
214
+ totalBytes: result.data.totalBytes,
215
+ warnings: result.warnings,
216
+ files: result.data.files.map(file => ({
217
+ name: file.name,
218
+ bytes: file.bytes,
219
+ extension: file.extension,
220
+ mimeType: file.mimeType,
221
+ category: file.category
222
+ }))
223
+ });
224
+ }
225
+ return withBlockerDetails(blocked(result.blocker?.message ?? result.error?.message ?? "File preflight failed.", result.blocker?.remediation?.map(step => `${step.label}: ${step.instruction}`), {
226
+ pathCount,
227
+ warnings: result.warnings
228
+ }), result, "files.preflight");
229
+ }
230
+ function localizationCheck(env) {
231
+ const requiredKeysMissing = REQUIRED_LOCALE_KEYS.filter(key => localeLabels[key].length === 0);
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);
234
+ const toolIds = Object.keys(localeLabels.tools);
235
+ const modeOptionIds = Object.keys(localeLabels.modeOptions);
236
+ const englishCanonicalPresent = localeLabels.composerTextbox[0] === "Chat with ChatGPT"
237
+ && localeLabels.sendButton[0] === "Send prompt"
238
+ && localeLabels.modeLabels.includes("Thinking")
239
+ && localeLabels.modeOptions.pro?.[0] === "Pro"
240
+ && localeLabels.tools.web_search?.[0] === "Web search";
241
+ const labelCandidateCount = REQUIRED_LOCALE_KEYS.reduce((total, key) => total + localeLabels[key].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);
244
+ const details = {
245
+ englishCanonicalPresent,
246
+ requiredKeysMissing,
247
+ missingToolIds,
248
+ missingModeOptionIds,
249
+ toolIds,
250
+ modeOptionIds,
251
+ labelCandidateCount,
252
+ pageAvailable: env.page !== undefined,
253
+ runtimeSelectorCoverage: "registry_only_stage_2"
254
+ };
255
+ if (englishCanonicalPresent && requiredKeysMissing.length === 0 && missingToolIds.length === 0 && missingModeOptionIds.length === 0) {
256
+ return unknown("The locale registry is loaded; localized runtime selector coverage is registry-only in Stage 2 and not fully proven.", undefined, details);
257
+ }
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");
259
+ }
260
+ async function reportsCheck(options) {
261
+ const destDir = options?.destDir ?? "reports/runs";
262
+ const includeContent = options?.includeContent === true;
263
+ const details = {
264
+ destDir,
265
+ includeContent,
266
+ redactionDefault: !includeContent,
267
+ maxPreviewChars: options?.maxPreviewChars ?? 240
268
+ };
269
+ try {
270
+ const current = await stat(destDir);
271
+ if (!current.isDirectory()) {
272
+ return unsupported("Report destination exists but is not a directory.", undefined, details);
273
+ }
274
+ await access(destDir, constants.W_OK);
275
+ return ok(includeContent
276
+ ? "Report destination is writable; raw content persistence is enabled by request."
277
+ : "Report destination is writable and redaction is enabled by default.", details);
278
+ }
279
+ catch (error) {
280
+ if (isNodeError(error) && error.code === "ENOENT") {
281
+ return unknown("Report destination does not exist yet; createReport will create it when a report is written.", undefined, {
282
+ ...details,
283
+ exists: false
284
+ }, "createReport");
285
+ }
286
+ if (isNodeError(error) && (error.code === "EACCES" || error.code === "EPERM")) {
287
+ return blocked("Report destination is not writable.", ["Choose a writable report destDir or update filesystem permissions."], details, "permission");
288
+ }
289
+ return unknown(`Report destination writability could not be proven: ${error instanceof Error ? error.message : String(error)}`, undefined, details);
290
+ }
291
+ }
105
292
  function bridgeRemediation(boot) {
106
293
  const remediation = boot.blocker?.remediation ?? BROWSER_BRIDGE_REMEDIATION;
107
294
  return remediation.map(step => `${step.label}: ${step.instruction}`);
108
295
  }
109
- function ok(message) {
110
- return { status: "ok", message };
296
+ function withBlockerDetails(check, result, command) {
297
+ if (result.blocker === undefined) {
298
+ return check;
299
+ }
300
+ const explanation = explainCommandBlocker(result, { command });
301
+ const details = {
302
+ severity: explanation.severity,
303
+ category: explanation.category,
304
+ userActionRequired: explanation.userActionRequired
305
+ };
306
+ if (explanation.diagnostics?.existingTab !== undefined) {
307
+ details.existingTab = explanation.diagnostics.existingTab;
308
+ }
309
+ if (explanation.candidates !== undefined) {
310
+ details.candidates = explanation.candidates;
311
+ }
312
+ const nextCommand = explanation.nextCommands[0];
313
+ const enriched = {
314
+ ...check,
315
+ blockerKind: explanation.kind,
316
+ details
317
+ };
318
+ if (result.blocker.code !== undefined)
319
+ enriched.code = result.blocker.code;
320
+ if (check.remediation === undefined && explanation.remediation.length > 0) {
321
+ enriched.remediation = explanation.remediation.map(step => `${step.label}: ${step.instruction}`);
322
+ }
323
+ if (nextCommand !== undefined)
324
+ enriched.nextCommand = nextCommand;
325
+ return enriched;
326
+ }
327
+ function ok(message, details) {
328
+ return details === undefined ? { status: "ok", message } : { status: "ok", message, details };
329
+ }
330
+ function blocked(message, remediation, details, blockerKind, code) {
331
+ return capability("blocked", message, remediation, details, undefined, blockerKind, code);
332
+ }
333
+ function unsupported(message, remediation, details, nextCommand, code) {
334
+ return capability("unsupported", message, remediation, details, nextCommand, undefined, code);
111
335
  }
112
- function blocked(message, remediation) {
113
- return remediation === undefined ? { status: "blocked", message } : { status: "blocked", message, remediation };
336
+ function unknown(message, remediation, details, nextCommand) {
337
+ return capability("unknown", message, remediation, details, nextCommand);
114
338
  }
115
- function unsupported(message, remediation) {
116
- return remediation === undefined ? { status: "unsupported", message } : { status: "unsupported", message, remediation };
339
+ function capability(status, message, remediation, details, nextCommand, blockerKind, code) {
340
+ const check = { status, message };
341
+ if (remediation !== undefined)
342
+ check.remediation = remediation;
343
+ if (details !== undefined)
344
+ check.details = details;
345
+ if (nextCommand !== undefined)
346
+ check.nextCommand = nextCommand;
347
+ if (blockerKind !== undefined)
348
+ check.blockerKind = blockerKind;
349
+ if (code !== undefined)
350
+ check.code = code;
351
+ return check;
117
352
  }
118
- function unknown(message, remediation) {
119
- return remediation === undefined ? { status: "unknown", message } : { status: "unknown", message, remediation };
353
+ function isNodeError(error) {
354
+ return error instanceof Error && "code" in error;
120
355
  }
@@ -1,4 +1,5 @@
1
- import type { AttachedFile, AttachFilesArgs, AttachFilesData, CommandResult, DownloadedFile, DownloadLatestArgs, RuntimeEnv } from "../types.js";
1
+ import type { AttachedFile, AttachFilesArgs, AttachFilesData, CommandResult, DownloadedFile, DownloadLatestArgs, FilePreflightArgs, FilePreflightData, RuntimeEnv } from "../types.js";
2
2
  export declare function validateAttachPaths(paths: string[]): Promise<AttachedFile[]>;
3
+ export declare function preflightFiles(env: RuntimeEnv, args: FilePreflightArgs): Promise<CommandResult<FilePreflightData>>;
3
4
  export declare function attachFiles(env: RuntimeEnv, args: AttachFilesArgs): Promise<CommandResult<AttachFilesData>>;
4
5
  export declare function downloadLatestFile(env: RuntimeEnv, args: DownloadLatestArgs): Promise<CommandResult<DownloadedFile>>;