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.
Files changed (242) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +55 -0
  3. package/contracts/v1/fixtures/backend-capabilities.json +7 -0
  4. package/contracts/v1/fixtures/blocker-explanation-profiles.json +114 -0
  5. package/contracts/v1/fixtures/command-descriptors.json +192 -15
  6. package/contracts/v1/fixtures/doctor-bridge-upload.json +8 -0
  7. package/contracts/v1/fixtures/doctor-scenario-preflight.json +108 -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 +72 -6
  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 +5137 -488
  30. package/dist/codex-chatgpt-control.bundle.mjs +5001 -286
  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 +7 -0
  35. package/dist/src/backend/session.js +14 -0
  36. package/dist/src/browser/attach.js +128 -20
  37. package/dist/src/browser/downloads.js +2 -1
  38. package/dist/src/browser/page-state.js +6 -3
  39. package/dist/src/client.d.ts +16 -1
  40. package/dist/src/client.js +86 -7
  41. package/dist/src/commands/artifacts.d.ts +5 -0
  42. package/dist/src/commands/artifacts.js +432 -0
  43. package/dist/src/commands/context.js +5 -2
  44. package/dist/src/commands/doctor.d.ts +10 -2
  45. package/dist/src/commands/doctor.js +242 -14
  46. package/dist/src/commands/files.d.ts +2 -1
  47. package/dist/src/commands/files.js +352 -18
  48. package/dist/src/commands/messages.d.ts +2 -1
  49. package/dist/src/commands/messages.js +232 -40
  50. package/dist/src/commands/modes.js +202 -20
  51. package/dist/src/commands/project-sources.d.ts +10 -0
  52. package/dist/src/commands/project-sources.js +643 -0
  53. package/dist/src/commands/registry.js +76 -14
  54. package/dist/src/commands/reports.d.ts +9 -0
  55. package/dist/src/commands/reports.js +83 -6
  56. package/dist/src/commands/response-actions.js +33 -0
  57. package/dist/src/commands/sequence.js +14 -0
  58. package/dist/src/commands/threads.js +2 -1
  59. package/dist/src/commands/timeouts.d.ts +2 -0
  60. package/dist/src/commands/timeouts.js +18 -0
  61. package/dist/src/diagnostics/blockers.d.ts +54 -0
  62. package/dist/src/diagnostics/blockers.js +299 -0
  63. package/dist/src/dom/artifacts.d.ts +7 -0
  64. package/dist/src/dom/artifacts.js +198 -0
  65. package/dist/src/dom/generation-state.d.ts +8 -0
  66. package/dist/src/dom/generation-state.js +103 -0
  67. package/dist/src/dom/locale/am.d.ts +28 -0
  68. package/dist/src/dom/locale/am.js +28 -0
  69. package/dist/src/dom/locale/ar.d.ts +28 -0
  70. package/dist/src/dom/locale/ar.js +28 -0
  71. package/dist/src/dom/locale/bg.d.ts +28 -0
  72. package/dist/src/dom/locale/bg.js +28 -0
  73. package/dist/src/dom/locale/bn.d.ts +28 -0
  74. package/dist/src/dom/locale/bn.js +28 -0
  75. package/dist/src/dom/locale/bs.d.ts +28 -0
  76. package/dist/src/dom/locale/bs.js +28 -0
  77. package/dist/src/dom/locale/ca.d.ts +28 -0
  78. package/dist/src/dom/locale/ca.js +28 -0
  79. package/dist/src/dom/locale/cs.d.ts +28 -0
  80. package/dist/src/dom/locale/cs.js +28 -0
  81. package/dist/src/dom/locale/da.d.ts +28 -0
  82. package/dist/src/dom/locale/da.js +28 -0
  83. package/dist/src/dom/locale/de.d.ts +30 -0
  84. package/dist/src/dom/locale/de.js +30 -0
  85. package/dist/src/dom/locale/el.d.ts +28 -0
  86. package/dist/src/dom/locale/el.js +28 -0
  87. package/dist/src/dom/locale/en.d.ts +50 -0
  88. package/dist/src/dom/locale/en.js +56 -0
  89. package/dist/src/dom/locale/es-419.d.ts +28 -0
  90. package/dist/src/dom/locale/es-419.js +28 -0
  91. package/dist/src/dom/locale/es-ES.d.ts +28 -0
  92. package/dist/src/dom/locale/es-ES.js +28 -0
  93. package/dist/src/dom/locale/et.d.ts +28 -0
  94. package/dist/src/dom/locale/et.js +28 -0
  95. package/dist/src/dom/locale/fa.d.ts +28 -0
  96. package/dist/src/dom/locale/fa.js +28 -0
  97. package/dist/src/dom/locale/fi.d.ts +28 -0
  98. package/dist/src/dom/locale/fi.js +28 -0
  99. package/dist/src/dom/locale/fr-CA.d.ts +29 -0
  100. package/dist/src/dom/locale/fr-CA.js +29 -0
  101. package/dist/src/dom/locale/fr-FR.d.ts +30 -0
  102. package/dist/src/dom/locale/fr-FR.js +30 -0
  103. package/dist/src/dom/locale/gu.d.ts +28 -0
  104. package/dist/src/dom/locale/gu.js +28 -0
  105. package/dist/src/dom/locale/hi.d.ts +28 -0
  106. package/dist/src/dom/locale/hi.js +28 -0
  107. package/dist/src/dom/locale/hr.d.ts +28 -0
  108. package/dist/src/dom/locale/hr.js +28 -0
  109. package/dist/src/dom/locale/hu.d.ts +28 -0
  110. package/dist/src/dom/locale/hu.js +28 -0
  111. package/dist/src/dom/locale/hy.d.ts +28 -0
  112. package/dist/src/dom/locale/hy.js +28 -0
  113. package/dist/src/dom/locale/id.d.ts +28 -0
  114. package/dist/src/dom/locale/id.js +28 -0
  115. package/dist/src/dom/locale/index.d.ts +79 -0
  116. package/dist/src/dom/locale/index.js +202 -0
  117. package/dist/src/dom/locale/is.d.ts +28 -0
  118. package/dist/src/dom/locale/is.js +28 -0
  119. package/dist/src/dom/locale/it.d.ts +27 -0
  120. package/dist/src/dom/locale/it.js +27 -0
  121. package/dist/src/dom/locale/ja.d.ts +27 -0
  122. package/dist/src/dom/locale/ja.js +27 -0
  123. package/dist/src/dom/locale/ka.d.ts +28 -0
  124. package/dist/src/dom/locale/ka.js +28 -0
  125. package/dist/src/dom/locale/kk.d.ts +28 -0
  126. package/dist/src/dom/locale/kk.js +28 -0
  127. package/dist/src/dom/locale/kn.d.ts +28 -0
  128. package/dist/src/dom/locale/kn.js +28 -0
  129. package/dist/src/dom/locale/ko.d.ts +28 -0
  130. package/dist/src/dom/locale/ko.js +28 -0
  131. package/dist/src/dom/locale/lt.d.ts +28 -0
  132. package/dist/src/dom/locale/lt.js +28 -0
  133. package/dist/src/dom/locale/lv.d.ts +28 -0
  134. package/dist/src/dom/locale/lv.js +28 -0
  135. package/dist/src/dom/locale/mk.d.ts +28 -0
  136. package/dist/src/dom/locale/mk.js +28 -0
  137. package/dist/src/dom/locale/ml.d.ts +28 -0
  138. package/dist/src/dom/locale/ml.js +28 -0
  139. package/dist/src/dom/locale/mn.d.ts +28 -0
  140. package/dist/src/dom/locale/mn.js +28 -0
  141. package/dist/src/dom/locale/mr.d.ts +28 -0
  142. package/dist/src/dom/locale/mr.js +28 -0
  143. package/dist/src/dom/locale/ms.d.ts +28 -0
  144. package/dist/src/dom/locale/ms.js +28 -0
  145. package/dist/src/dom/locale/my.d.ts +28 -0
  146. package/dist/src/dom/locale/my.js +28 -0
  147. package/dist/src/dom/locale/nb.d.ts +28 -0
  148. package/dist/src/dom/locale/nb.js +28 -0
  149. package/dist/src/dom/locale/nl.d.ts +28 -0
  150. package/dist/src/dom/locale/nl.js +28 -0
  151. package/dist/src/dom/locale/pa.d.ts +28 -0
  152. package/dist/src/dom/locale/pa.js +28 -0
  153. package/dist/src/dom/locale/pl.d.ts +28 -0
  154. package/dist/src/dom/locale/pl.js +28 -0
  155. package/dist/src/dom/locale/pt-BR.d.ts +28 -0
  156. package/dist/src/dom/locale/pt-BR.js +28 -0
  157. package/dist/src/dom/locale/pt-PT.d.ts +28 -0
  158. package/dist/src/dom/locale/pt-PT.js +28 -0
  159. package/dist/src/dom/locale/ro.d.ts +28 -0
  160. package/dist/src/dom/locale/ro.js +28 -0
  161. package/dist/src/dom/locale/ru.d.ts +28 -0
  162. package/dist/src/dom/locale/ru.js +28 -0
  163. package/dist/src/dom/locale/sk.d.ts +28 -0
  164. package/dist/src/dom/locale/sk.js +28 -0
  165. package/dist/src/dom/locale/sl.d.ts +28 -0
  166. package/dist/src/dom/locale/sl.js +28 -0
  167. package/dist/src/dom/locale/so.d.ts +28 -0
  168. package/dist/src/dom/locale/so.js +28 -0
  169. package/dist/src/dom/locale/sq.d.ts +28 -0
  170. package/dist/src/dom/locale/sq.js +28 -0
  171. package/dist/src/dom/locale/sr.d.ts +28 -0
  172. package/dist/src/dom/locale/sr.js +28 -0
  173. package/dist/src/dom/locale/sv.d.ts +28 -0
  174. package/dist/src/dom/locale/sv.js +28 -0
  175. package/dist/src/dom/locale/sw.d.ts +28 -0
  176. package/dist/src/dom/locale/sw.js +28 -0
  177. package/dist/src/dom/locale/ta.d.ts +28 -0
  178. package/dist/src/dom/locale/ta.js +28 -0
  179. package/dist/src/dom/locale/te.d.ts +28 -0
  180. package/dist/src/dom/locale/te.js +28 -0
  181. package/dist/src/dom/locale/th.d.ts +28 -0
  182. package/dist/src/dom/locale/th.js +28 -0
  183. package/dist/src/dom/locale/tl.d.ts +28 -0
  184. package/dist/src/dom/locale/tl.js +28 -0
  185. package/dist/src/dom/locale/tr.d.ts +28 -0
  186. package/dist/src/dom/locale/tr.js +28 -0
  187. package/dist/src/dom/locale/types.d.ts +45 -0
  188. package/dist/src/dom/locale/types.js +1 -0
  189. package/dist/src/dom/locale/uk.d.ts +28 -0
  190. package/dist/src/dom/locale/uk.js +28 -0
  191. package/dist/src/dom/locale/ur.d.ts +28 -0
  192. package/dist/src/dom/locale/ur.js +28 -0
  193. package/dist/src/dom/locale/vi.d.ts +28 -0
  194. package/dist/src/dom/locale/vi.js +28 -0
  195. package/dist/src/dom/locale/zh-HK.d.ts +28 -0
  196. package/dist/src/dom/locale/zh-HK.js +28 -0
  197. package/dist/src/dom/locale/zh-Hans.d.ts +28 -0
  198. package/dist/src/dom/locale/zh-Hans.js +28 -0
  199. package/dist/src/dom/locale/zh-TW.d.ts +29 -0
  200. package/dist/src/dom/locale/zh-TW.js +29 -0
  201. package/dist/src/dom/locale-labels.d.ts +26 -0
  202. package/dist/src/dom/locale-labels.js +26 -0
  203. package/dist/src/dom/menus.d.ts +6 -0
  204. package/dist/src/dom/menus.js +34 -8
  205. package/dist/src/dom/message-format.d.ts +2 -1
  206. package/dist/src/dom/message-format.js +67 -10
  207. package/dist/src/dom/messages.d.ts +2 -1
  208. package/dist/src/dom/messages.js +2 -4
  209. package/dist/src/dom/selectors.d.ts +1 -0
  210. package/dist/src/dom/selectors.js +42 -15
  211. package/dist/src/index.d.ts +5 -0
  212. package/dist/src/index.js +5 -0
  213. package/dist/src/platform/local-paths.d.ts +4 -0
  214. package/dist/src/platform/local-paths.js +20 -0
  215. package/dist/src/runner/interruptions.js +15 -4
  216. package/dist/src/runner/responses.js +18 -1
  217. package/dist/src/runner/result.js +15 -0
  218. package/dist/src/runner/resume.js +1 -0
  219. package/dist/src/runner/types.d.ts +3 -0
  220. package/dist/src/safety/risk.d.ts +7 -0
  221. package/dist/src/safety/risk.js +7 -0
  222. package/dist/src/safety/untrusted-output.d.ts +73 -0
  223. package/dist/src/safety/untrusted-output.js +199 -0
  224. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +1 -0
  225. package/dist/src/scripts/apply-intelligence-locale-captures.js +184 -0
  226. package/dist/src/scripts/capture-intelligence-locales.d.ts +25 -0
  227. package/dist/src/scripts/capture-intelligence-locales.js +669 -0
  228. package/dist/src/scripts/check-surface-drift.d.ts +1 -0
  229. package/dist/src/scripts/check-surface-drift.js +11 -0
  230. package/dist/src/scripts/live-smoke/scenarios.js +84 -0
  231. package/dist/src/scripts/locale-capture/language-coverage.d.ts +10 -0
  232. package/dist/src/scripts/locale-capture/language-coverage.js +39 -0
  233. package/dist/src/testing/surface-drift.d.ts +66 -0
  234. package/dist/src/testing/surface-drift.js +363 -0
  235. package/dist/src/types.d.ts +201 -2
  236. package/package.json +2 -1
  237. package/references/backend-protocol.md +93 -1
  238. package/references/language-coverage.md +142 -0
  239. package/references/localization.md +247 -0
  240. package/references/python-parity.md +63 -1
  241. package/references/safety.md +2 -0
  242. package/references/troubleshooting.md +58 -0
@@ -6,7 +6,7 @@ import type { ChatGPTResponsesCreateArgs } from "../runner/responses.js";
6
6
  import type { ChatGPTRunStream } from "../runner/stream.js";
7
7
  import type { ChatGPTAgent, ChatGPTAgentConfig, ChatGPTResponse, ChatGPTRunInput, ChatGPTRunResult } from "../runner/types.js";
8
8
  import type { ReportRedactionOptions } from "../safety/report-redaction.js";
9
- import type { BootstrapArgs, CommandResult, CopyResponseArgs, DownloadLatestArgs, NewThreadArgs, OpenThreadArgs, ReadLatestArgs, SearchThreadsArgs, SelectToolArgs, SequencePlan, SetModeArgs, WaitArgs } from "../types.js";
9
+ import type { ArtifactDownloadArgs, ArtifactWaitArgs, BootstrapArgs, CommandResult, CopyResponseArgs, DownloadLatestArgs, ListArtifactsArgs, NewThreadArgs, OpenThreadArgs, ReadLatestArgs, SearchThreadsArgs, SelectToolArgs, SequencePlan, SetModeArgs, WaitArgs } from "../types.js";
10
10
  import { type BackendEvent, type BackendRequest, type BackendResponse } from "./protocol.js";
11
11
  export type BackendTransport = {
12
12
  request(request: BackendRequest): Promise<BackendResponse>;
@@ -44,6 +44,11 @@ export type ChatGPTBackendClient = {
44
44
  readLatest(args?: ReadLatestArgs): Promise<CommandResult<unknown>>;
45
45
  copyLatest(args?: CopyResponseArgs): Promise<CommandResult<unknown>>;
46
46
  downloadLatest(args: DownloadLatestArgs): Promise<CommandResult<unknown>>;
47
+ artifacts: {
48
+ listLatest(args?: ListArtifactsArgs): Promise<CommandResult<unknown>>;
49
+ wait(args?: ArtifactWaitArgs): Promise<CommandResult<unknown>>;
50
+ downloadLatest(args: ArtifactDownloadArgs): Promise<CommandResult<unknown>>;
51
+ };
47
52
  runPlan(plan: SequencePlan | NamedWorkflowInvocation): Promise<CommandResult<unknown>>;
48
53
  doctor(args?: DoctorArgs): Promise<CommandResult<DoctorReport>>;
49
54
  createReport(result: CommandResult<unknown>, args?: RunReportOptions): Promise<CommandResult<RunReportData>>;
@@ -76,6 +76,11 @@ export function createChatGPTBackendClient(transport) {
76
76
  readLatest: args => request("messages.readLatest", args ?? {}),
77
77
  waitAndRead: args => request("messages.waitAndRead", args)
78
78
  },
79
+ artifacts: {
80
+ listLatest: args => request("artifacts.listLatest", args ?? {}),
81
+ wait: args => request("artifacts.wait", args ?? {}),
82
+ downloadLatest: args => request("artifacts.downloadLatest", args)
83
+ },
79
84
  files: {
80
85
  attach: args => request("files.attach", args),
81
86
  downloadLatest: args => request("files.downloadLatest", args)
@@ -1,7 +1,7 @@
1
1
  export declare const BACKEND_REQUEST_SCHEMA_VERSION: "chatgpt.browser_control.backend_request.v1";
2
2
  export declare const BACKEND_RESPONSE_SCHEMA_VERSION: "chatgpt.browser_control.backend_response.v1";
3
3
  export declare const BACKEND_EVENT_SCHEMA_VERSION: "chatgpt.browser_control.backend_event.v1";
4
- export declare const backendCommands: readonly ["backend.version", "backend.health", "backend.capabilities", "runner.run", "runner.plan", "runner.stream", "responses.create", "ask", "askInThread", "askWithFiles", "askAndDownload", "runMessages", "openThread", "readLatest", "copyLatest", "downloadLatest", "runPlan", "doctor", "createReport", "reports.create", "reports.redact", "reports.summarize", "commands", "describe", "help", "session.bootstrap", "threads.new", "threads.search", "threads.open", "messages.compose", "messages.submit", "messages.ask", "messages.wait", "messages.readLatest", "messages.waitAndRead", "files.attach", "files.downloadLatest", "modes.set", "tools.select", "response.copy"];
4
+ export declare const backendCommands: readonly ["backend.version", "backend.health", "backend.capabilities", "runner.run", "runner.plan", "runner.stream", "responses.create", "ask", "askInThread", "askWithFiles", "askAndDownload", "runMessages", "openThread", "readLatest", "copyLatest", "downloadLatest", "runPlan", "doctor", "createReport", "reports.create", "reports.redact", "reports.summarize", "commands", "describe", "help", "session.bootstrap", "threads.new", "threads.search", "threads.open", "messages.compose", "messages.submit", "messages.ask", "messages.wait", "messages.readLatest", "messages.waitAndRead", "artifacts.listLatest", "artifacts.wait", "artifacts.downloadLatest", "files.preflight", "files.attach", "files.downloadLatest", "projects.sources.list", "projects.sources.planAdd", "projects.sources.add", "modes.set", "tools.select", "response.copy"];
5
5
  export type BackendCommand = typeof backendCommands[number];
6
6
  export type BackendRequest = {
7
7
  schemaVersion: typeof BACKEND_REQUEST_SCHEMA_VERSION;
@@ -37,8 +37,15 @@ export const backendCommands = [
37
37
  "messages.wait",
38
38
  "messages.readLatest",
39
39
  "messages.waitAndRead",
40
+ "artifacts.listLatest",
41
+ "artifacts.wait",
42
+ "artifacts.downloadLatest",
43
+ "files.preflight",
40
44
  "files.attach",
41
45
  "files.downloadLatest",
46
+ "projects.sources.list",
47
+ "projects.sources.planAdd",
48
+ "projects.sources.add",
42
49
  "modes.set",
43
50
  "tools.select",
44
51
  "response.copy"
@@ -141,10 +141,24 @@ async function dispatchBackendCommand(client, request) {
141
141
  return client.messages.readLatest(emptyToUndefined(payload));
142
142
  case "messages.waitAndRead":
143
143
  return client.messages.waitAndRead(payload);
144
+ case "artifacts.listLatest":
145
+ return client.artifacts.listLatest(emptyToUndefined(payload));
146
+ case "artifacts.wait":
147
+ return client.artifacts.wait(emptyToUndefined(payload));
148
+ case "artifacts.downloadLatest":
149
+ return client.artifacts.downloadLatest(payload);
150
+ case "files.preflight":
151
+ return client.files.preflight(payload);
144
152
  case "files.attach":
145
153
  return client.files.attach(payload);
146
154
  case "files.downloadLatest":
147
155
  return client.files.downloadLatest(payload);
156
+ case "projects.sources.list":
157
+ return client.projects.sources.list(payload);
158
+ case "projects.sources.planAdd":
159
+ return client.projects.sources.planAdd(payload);
160
+ case "projects.sources.add":
161
+ return client.projects.sources.add(payload);
148
162
  case "modes.set":
149
163
  return client.modes.set(payload);
150
164
  case "tools.select":
@@ -2,6 +2,8 @@ import { BrowserBridgeUnavailableError, ChatGPTControlError, LoginRequiredError
2
2
  import { parseConversationId, readPageState } from "./page-state.js";
3
3
  const CHATGPT_HOME = "https://chatgpt.com/";
4
4
  const CHATGPT_HOSTS = new Set(["chatgpt.com", "www.chatgpt.com", "chat.openai.com"]);
5
+ const MAX_EXISTING_TAB_DIAGNOSTIC_CANDIDATES = 10;
6
+ const MAX_EXISTING_TAB_DIAGNOSTIC_FIELD_LENGTH = 240;
5
7
  export async function attachChatGPTBrowser(env, args = {}) {
6
8
  const browser = await getBrowser(env);
7
9
  const page = await getOrCreateChatGPTPage(browser, env, args);
@@ -102,12 +104,12 @@ async function getOrCreateChatGPTPage(browser, env, args) {
102
104
  }
103
105
  if (explicitExistingPolicy !== undefined) {
104
106
  const existing = await selectExistingTab(browser, explicitExistingPolicy);
105
- if (existing !== undefined) {
106
- return existing;
107
+ if (existing.page !== undefined) {
108
+ return existing.page;
107
109
  }
108
110
  const ifMissing = explicitExistingPolicy.ifMissing ?? "block";
109
111
  if (ifMissing === "block") {
110
- throw new ExistingTabSelectionError("No already-open ChatGPT tab matched the requested existing-tab target.", "existing_tab_not_found");
112
+ throw new ExistingTabSelectionError("No already-open ChatGPT tab matched the requested existing-tab target.", "existing_tab_not_found", existing.diagnostics?.candidateTabs, existing.diagnostics);
111
113
  }
112
114
  const missingUrl = ifMissing === "open"
113
115
  ? urlFromExistingTarget(explicitExistingPolicy.target) ?? targetUrl
@@ -163,8 +165,8 @@ function normalizeExplicitExistingTabPolicy(args) {
163
165
  };
164
166
  }
165
167
  async function selectExistingTab(browser, policy) {
166
- const userMatch = await selectExistingUserTab(browser, policy);
167
- if (userMatch !== undefined) {
168
+ const userMatch = await selectExistingUserTab(browser, policy, shouldCollectExistingTabDiagnostics(policy));
169
+ if (userMatch.page !== undefined) {
168
170
  return userMatch;
169
171
  }
170
172
  if (policy.target?.type === "selected" && typeof browser.tabs?.selected === "function") {
@@ -172,7 +174,7 @@ async function selectExistingTab(browser, policy) {
172
174
  if (selected !== undefined) {
173
175
  const normalized = normalizePage(selected);
174
176
  if (await pageMatchesExistingTarget(normalized, policy)) {
175
- return normalized;
177
+ return { page: normalized };
176
178
  }
177
179
  }
178
180
  }
@@ -181,28 +183,37 @@ async function selectExistingTab(browser, policy) {
181
183
  if (tab !== undefined) {
182
184
  const normalized = normalizePage(tab);
183
185
  if (await pageMatchesExistingTarget(normalized, policy)) {
184
- return normalized;
186
+ return { page: normalized };
185
187
  }
186
188
  }
187
189
  }
188
- return undefined;
190
+ return userMatch.diagnostics === undefined
191
+ ? { diagnostics: diagnosticsForUnavailableUserTabs(policy) }
192
+ : userMatch;
189
193
  }
190
- async function selectExistingUserTab(browser, policy) {
194
+ async function selectExistingUserTab(browser, policy, collectDiagnostics) {
191
195
  const openTabs = browser.user?.openTabs;
192
196
  const claimTab = browser.user?.claimTab;
193
197
  if (typeof openTabs !== "function" || typeof claimTab !== "function") {
194
- return undefined;
198
+ return {};
199
+ }
200
+ const tabs = await Promise.resolve(openTabs.call(browser.user)).catch(() => undefined);
201
+ if (tabs === undefined) {
202
+ return collectDiagnostics
203
+ ? { diagnostics: diagnosticsForUnavailableUserTabs(policy, "user_open_tabs_unavailable") }
204
+ : {};
195
205
  }
196
- const tabs = await Promise.resolve(openTabs.call(browser.user)).catch(() => []);
197
206
  const matches = tabs.filter(tab => userTabMatchesTarget(tab, policy));
207
+ const diagnostics = collectDiagnostics ? diagnosticsForUserTabs(policy, tabs, matches) : undefined;
198
208
  if (matches.length === 0) {
199
- return undefined;
209
+ return diagnostics === undefined ? {} : { diagnostics };
200
210
  }
201
211
  if (matches.length > 1 && (policy.ifMultiple ?? "block") !== "first") {
202
- throw new ExistingTabSelectionError("Multiple already-open ChatGPT tabs matched the requested existing-tab target.", "existing_tab_ambiguous", matches);
212
+ throw new ExistingTabSelectionError("Multiple already-open ChatGPT tabs matched the requested existing-tab target.", "existing_tab_ambiguous", matches, diagnostics);
203
213
  }
204
214
  const selected = matches[0];
205
- return normalizePage(await claimTab.call(browser.user, selected));
215
+ const page = normalizePage(await claimTab.call(browser.user, selected));
216
+ return diagnostics === undefined ? { page } : { page, diagnostics };
206
217
  }
207
218
  function userTabMatchesTarget(tab, policy) {
208
219
  const target = policy.target ?? { type: "selected", host: "chatgpt" };
@@ -224,6 +235,100 @@ function userTabMatchesTarget(tab, policy) {
224
235
  return titleMatches(tab.title, target.title, target.exact ?? true);
225
236
  }
226
237
  }
238
+ function diagnosticsForUserTabs(policy, tabs, matches) {
239
+ const chatgptTabs = tabs.filter(tab => isChatGPTUrl(tab.url));
240
+ const candidateTabs = matches.length > 1 ? matches : chatgptTabs;
241
+ const cappedTabs = candidateTabs.slice(0, MAX_EXISTING_TAB_DIAGNOSTIC_CANDIDATES);
242
+ const diagnostics = {
243
+ requestedTarget: diagnosticTarget(policy.target ?? { type: "selected", host: "chatgpt" }),
244
+ userOpenTabsAvailable: true,
245
+ chatgptTabCount: chatgptTabs.length,
246
+ mismatchReason: matches.length > 1 ? "multiple_candidates" : mismatchReasonForNoMatches(policy, tabs, chatgptTabs),
247
+ candidateTabs: cappedTabs.map(diagnosticCandidate)
248
+ };
249
+ const omittedCandidateCount = candidateTabs.length - cappedTabs.length;
250
+ if (omittedCandidateCount > 0)
251
+ diagnostics.omittedCandidateCount = omittedCandidateCount;
252
+ return diagnostics;
253
+ }
254
+ function shouldCollectExistingTabDiagnostics(policy) {
255
+ return (policy.ifMissing ?? "block") === "block" || (policy.ifMultiple ?? "block") !== "first";
256
+ }
257
+ function diagnosticsForUnavailableUserTabs(policy, mismatchReason = undefined) {
258
+ const target = policy.target ?? { type: "selected", host: "chatgpt" };
259
+ return {
260
+ requestedTarget: diagnosticTarget(target),
261
+ userOpenTabsAvailable: false,
262
+ chatgptTabCount: 0,
263
+ mismatchReason: mismatchReason ?? (target.type === "tabId" ? "explicit_tab_id_not_open" : "selected_tab_unavailable"),
264
+ candidateTabs: []
265
+ };
266
+ }
267
+ function diagnosticTarget(target) {
268
+ switch (target.type) {
269
+ case "selected": {
270
+ const value = { type: target.type };
271
+ if (target.host !== undefined)
272
+ value.host = target.host;
273
+ return value;
274
+ }
275
+ case "tabId":
276
+ return { type: target.type, tabId: target.tabId };
277
+ case "conversationId":
278
+ case "conversation_id":
279
+ return { type: target.type, conversationId: target.conversationId };
280
+ case "url":
281
+ return { type: target.type, url: target.url };
282
+ case "title": {
283
+ const value = { type: target.type, title: target.title };
284
+ if (target.exact !== undefined)
285
+ value.exact = target.exact;
286
+ return value;
287
+ }
288
+ }
289
+ }
290
+ function diagnosticCandidate(tab) {
291
+ const candidate = { id: tab.id };
292
+ if (tab.url !== undefined) {
293
+ candidate.url = truncateDiagnosticField(tab.url);
294
+ const conversationId = parseConversationId(tab.url);
295
+ if (conversationId !== undefined)
296
+ candidate.conversationId = conversationId;
297
+ }
298
+ if (tab.title !== undefined)
299
+ candidate.title = truncateDiagnosticField(tab.title);
300
+ if (tab.lastOpened !== undefined)
301
+ candidate.lastOpened = truncateDiagnosticField(tab.lastOpened);
302
+ if (tab.tabGroup !== undefined)
303
+ candidate.tabGroup = truncateDiagnosticField(tab.tabGroup);
304
+ return candidate;
305
+ }
306
+ function truncateDiagnosticField(value) {
307
+ return value.length <= MAX_EXISTING_TAB_DIAGNOSTIC_FIELD_LENGTH
308
+ ? value
309
+ : `${value.slice(0, MAX_EXISTING_TAB_DIAGNOSTIC_FIELD_LENGTH - 1)}…`;
310
+ }
311
+ function mismatchReasonForNoMatches(policy, tabs, chatgptTabs) {
312
+ const target = policy.target ?? { type: "selected", host: "chatgpt" };
313
+ if (tabs.length === 0)
314
+ return "no_candidate";
315
+ if (chatgptTabs.length === 0 && (policy.requireChatGPT ?? targetRequiresChatGPT(target))) {
316
+ return "non_chatgpt_tab";
317
+ }
318
+ switch (target.type) {
319
+ case "tabId":
320
+ return tabs.some(tab => tab.id === target.tabId) ? "non_chatgpt_tab" : "explicit_tab_id_not_open";
321
+ case "conversationId":
322
+ case "conversation_id":
323
+ return "conversation_id_mismatch";
324
+ case "url":
325
+ return "url_mismatch";
326
+ case "title":
327
+ return "title_mismatch";
328
+ case "selected":
329
+ return "selected_tab_unavailable";
330
+ }
331
+ }
227
332
  async function pageMatchesExistingTarget(page, policy) {
228
333
  const url = await Promise.resolve(page.url?.()).catch(() => undefined);
229
334
  const title = await Promise.resolve(page.title?.()).catch(() => undefined);
@@ -239,9 +344,9 @@ async function findExistingChatGPTTab(browser) {
239
344
  target: { type: "selected", host: "chatgpt" },
240
345
  ifMultiple: "first",
241
346
  requireChatGPT: true
242
- });
243
- if (userTab !== undefined) {
244
- return userTab;
347
+ }, false).catch(() => ({ page: undefined }));
348
+ if (userTab.page !== undefined) {
349
+ return userTab.page;
245
350
  }
246
351
  const selected = browser.tabs?.selected;
247
352
  if (typeof selected === "function") {
@@ -282,8 +387,8 @@ async function findExistingChatGPTTab(browser) {
282
387
  return undefined;
283
388
  }
284
389
  class ExistingTabSelectionError extends ChatGPTControlError {
285
- constructor(message, code, candidates = []) {
286
- super(message, "not_found", true, undefined, {
390
+ constructor(message, code, candidates = [], diagnostics) {
391
+ const details = {
287
392
  code,
288
393
  candidates: candidates.map(tab => ({ label: userTabCandidateLabel(tab) })),
289
394
  remediation: [
@@ -298,7 +403,10 @@ class ExistingTabSelectionError extends ChatGPTControlError {
298
403
  userActionRequired: false
299
404
  }
300
405
  ]
301
- });
406
+ };
407
+ if (diagnostics !== undefined)
408
+ details.diagnostics = { existingTab: diagnostics };
409
+ super(message, "not_found", true, undefined, details);
302
410
  }
303
411
  }
304
412
  function targetRequiresChatGPT(target) {
@@ -1,5 +1,6 @@
1
1
  import { mkdir, stat } from "node:fs/promises";
2
2
  import { basename, join, resolve } from "node:path";
3
+ import { localGuardTimeout, withTimeout } from "../commands/timeouts.js";
3
4
  export async function waitForDownloadFromClick(page, click, destDir, timeoutMs) {
4
5
  const absoluteDest = resolve(destDir);
5
6
  await mkdir(absoluteDest, { recursive: true });
@@ -7,7 +8,7 @@ export async function waitForDownloadFromClick(page, click, destDir, timeoutMs)
7
8
  if (downloadPromise === undefined) {
8
9
  throw new Error("The active browser page does not expose download events.");
9
10
  }
10
- await click();
11
+ await withTimeout(click(), localGuardTimeout(timeoutMs, 10000), "Download control click did not complete before the local guard timeout.");
11
12
  const download = await downloadPromise;
12
13
  const suggestedFilename = download.suggestedFilename?.() ?? `chatgpt-download-${Date.now()}`;
13
14
  const targetPath = join(absoluteDest, basename(suggestedFilename));
@@ -1,5 +1,7 @@
1
1
  import { classifyVisibleText } from "../safety/blockers.js";
2
2
  import { compactVisibleText } from "../safety/redaction.js";
3
+ import { escapeRegExp, localeLabels } from "../dom/locale-labels.js";
4
+ import { withTimeout } from "../commands/timeouts.js";
3
5
  export function parseConversationId(url) {
4
6
  const match = /\/c\/([A-Za-z0-9-]+)/.exec(url);
5
7
  return match?.[1];
@@ -32,7 +34,7 @@ export async function readPageState(page) {
32
34
  export async function readVisibleText(page) {
33
35
  if (typeof page.evaluate === "function") {
34
36
  try {
35
- return await page.evaluate(() => document.body?.innerText ?? "");
37
+ return await withTimeout(page.evaluate(() => document.body?.innerText ?? ""), 1000, "Timed out while reading visible page text.");
36
38
  }
37
39
  catch {
38
40
  // Fall back to content parsing below.
@@ -40,7 +42,7 @@ export async function readVisibleText(page) {
40
42
  }
41
43
  if (typeof page.content === "function") {
42
44
  try {
43
- const html = await page.content();
45
+ const html = await withTimeout(page.content(), 1000, "Timed out while reading page content.");
44
46
  return htmlToText(html);
45
47
  }
46
48
  catch {
@@ -62,5 +64,6 @@ export function htmlToText(html) {
62
64
  .trim();
63
65
  }
64
66
  function isLikelySignedIn(visibleText) {
65
- return /\b(New chat|Search chats|Chat with ChatGPT|Recents|Projects)\b/i.test(visibleText);
67
+ const markers = localeLabels.signedInMarkers.map(escapeRegExp).join("|");
68
+ return new RegExp(`\\b(${markers})\\b`, "i").test(visibleText);
66
69
  }
@@ -1,10 +1,11 @@
1
- import type { AskArgs, AttachFilesArgs, BootstrapArgs, CommandResult, CopyResponseArgs, DownloadLatestArgs, NewThreadArgs, OpenThreadArgs, ReadLatestArgs, RuntimeEnv, SearchThreadsArgs, SelectToolArgs, SequencePlan, SetModeArgs, ThreadTarget, WaitArgs } from "./types.js";
1
+ import type { AskArgs, ArtifactDownloadArgs, ArtifactWaitArgs, AttachFilesArgs, BootstrapArgs, CommandResult, CopyResponseArgs, DownloadLatestArgs, FilePreflightArgs, FilePreflightData, ListArtifactsArgs, NewThreadArgs, OpenThreadArgs, ProjectSourcesAddArgs, ProjectSourcesAddData, ProjectSourcesAddPlanData, ProjectSourcesListArgs, ProjectSourcesListData, ProjectSourcesPlanAddArgs, ReadLatestArgs, RuntimeEnv, SearchThreadsArgs, SelectToolArgs, SequencePlan, SetModeArgs, ThreadTarget, WaitArgs } from "./types.js";
2
2
  import { type DoctorArgs, type DoctorReport } from "./commands/doctor.js";
3
3
  import { type RunReportData, type RunReportOptions } from "./commands/reports.js";
4
4
  import { type CommandDescriptor } from "./commands/registry.js";
5
5
  import type { ChatGPTAgent, ChatGPTAgentConfig, ChatGPTResponse, ChatGPTRunner, ChatGPTRunInput, ChatGPTRunResult } from "./runner/types.js";
6
6
  import { type ChatGPTResponsesCreateArgs } from "./runner/responses.js";
7
7
  import { type ReportRedactionOptions } from "./safety/report-redaction.js";
8
+ import { type BlockerExplanation, type ExplainBlockerOptions } from "./diagnostics/blockers.js";
8
9
  export type ChatGPTClientOptions = RuntimeEnv & {
9
10
  defaults?: {
10
11
  mode?: SetModeArgs;
@@ -112,9 +113,15 @@ export type ChatGPTClient = {
112
113
  readLatest(args?: ReadLatestArgs): Promise<CommandResult<unknown>>;
113
114
  copyLatest(args?: CopyResponseArgs): Promise<CommandResult<unknown>>;
114
115
  downloadLatest(args: DownloadLatestArgs): Promise<CommandResult<unknown>>;
116
+ artifacts: {
117
+ listLatest(args?: ListArtifactsArgs): Promise<CommandResult<unknown>>;
118
+ wait(args?: ArtifactWaitArgs): Promise<CommandResult<unknown>>;
119
+ downloadLatest(args: ArtifactDownloadArgs): Promise<CommandResult<unknown>>;
120
+ };
115
121
  runPlan(plan: SequencePlan | NamedWorkflowInvocation): Promise<CommandResult<unknown>>;
116
122
  doctor(args?: DoctorArgs): Promise<CommandResult<DoctorReport>>;
117
123
  createReport(result: CommandResult<unknown>, args?: RunReportOptions): Promise<CommandResult<RunReportData>>;
124
+ explainBlocker(resultOrBlocker: CommandResult<unknown> | NonNullable<CommandResult["blocker"]> | undefined, options?: ExplainBlockerOptions): BlockerExplanation;
118
125
  reports: {
119
126
  create(result: CommandResult<unknown>, args?: RunReportOptions): Promise<CommandResult<RunReportData>>;
120
127
  redact(value: unknown, args?: ReportRedactionOptions): Promise<CommandResult<unknown>>;
@@ -151,9 +158,17 @@ export type ChatGPTClient = {
151
158
  waitAndRead(args?: WaitArgs & ReadLatestArgs): Promise<CommandResult<unknown>>;
152
159
  };
153
160
  files: {
161
+ preflight(args: FilePreflightArgs): Promise<CommandResult<FilePreflightData>>;
154
162
  attach(args: AttachFilesArgs): Promise<CommandResult<unknown>>;
155
163
  downloadLatest(args: DownloadLatestArgs): Promise<CommandResult<unknown>>;
156
164
  };
165
+ projects: {
166
+ sources: {
167
+ list(args: ProjectSourcesListArgs): Promise<CommandResult<ProjectSourcesListData>>;
168
+ planAdd(args: ProjectSourcesPlanAddArgs): Promise<CommandResult<ProjectSourcesAddPlanData>>;
169
+ add(args: ProjectSourcesAddArgs): Promise<CommandResult<ProjectSourcesAddData | ProjectSourcesAddPlanData>>;
170
+ };
171
+ };
157
172
  modes: {
158
173
  set(args: SetModeArgs): Promise<CommandResult<unknown>>;
159
174
  };
@@ -1,4 +1,6 @@
1
- import { attachFiles, downloadLatestFile } from "./commands/files.js";
1
+ import { downloadLatestArtifact, listLatestArtifacts, waitForArtifact } from "./commands/artifacts.js";
2
+ import { attachFiles, downloadLatestFile, preflightFiles } from "./commands/files.js";
3
+ import { addProjectSources, buildProjectSourceAddPlan, listProjectSources } from "./commands/project-sources.js";
2
4
  import { doctor } from "./commands/doctor.js";
3
5
  import { askMessage, composeMessage, readLatest, submitMessage, waitAndRead, waitForMessage } from "./commands/messages.js";
4
6
  import { selectTool, setMode } from "./commands/modes.js";
@@ -14,6 +16,7 @@ import { toRunResult } from "./runner/result.js";
14
16
  import { responseFromRunResult, responsesCreateArgsToRunInput, unsupportedResponse, validateResponsesCreateArgs } from "./runner/responses.js";
15
17
  import { streamFromRunResult } from "./runner/stream.js";
16
18
  import { redactReportValue } from "./safety/report-redaction.js";
19
+ import { explainCommandBlocker } from "./diagnostics/blockers.js";
17
20
  export function createChatGPT(options = {}) {
18
21
  const env = runtimeEnv(options);
19
22
  const limits = normalizeLimits(options.limits);
@@ -44,6 +47,7 @@ export function createChatGPT(options = {}) {
44
47
  runPlan: plan => runPlanInvocation(plan, env, limits, options.defaults, options.reporting),
45
48
  doctor: args => doctor(env, args),
46
49
  createReport: (result, args) => createRunReport(env, result, args ?? options.reporting ?? {}),
50
+ explainBlocker: (resultOrBlocker, args) => explainCommandBlocker(resultOrBlocker, args),
47
51
  reports: {
48
52
  create: (result, args) => createRunReport(env, result, args ?? options.reporting ?? {}),
49
53
  redact: async (value, args) => resultOk(redactReportValue(value, args), {}),
@@ -70,9 +74,22 @@ export function createChatGPT(options = {}) {
70
74
  waitAndRead: args => waitAndRead(env, args)
71
75
  },
72
76
  files: {
77
+ preflight: args => preflightFiles(env, args),
73
78
  attach: args => attachFiles(env, args),
74
79
  downloadLatest: args => downloadLatestFile(env, args)
75
80
  },
81
+ projects: {
82
+ sources: {
83
+ list: args => listProjectSources(env, args),
84
+ planAdd: args => buildProjectSourceAddPlan(env, args),
85
+ add: args => addProjectSources(env, args)
86
+ }
87
+ },
88
+ artifacts: {
89
+ listLatest: args => listLatestArtifacts(env, args),
90
+ wait: args => waitForArtifact(env, args),
91
+ downloadLatest: args => downloadLatestArtifact(env, args)
92
+ },
76
93
  modes: {
77
94
  set: args => setMode(env, args)
78
95
  },
@@ -88,6 +105,9 @@ async function runGuarded(plan, env, limits, report) {
88
105
  const budget = checkRunBudget(plan, limits);
89
106
  if (budget !== undefined)
90
107
  return budget;
108
+ const filePreflight = await preflightPlanFiles(plan, env);
109
+ if (filePreflight !== undefined)
110
+ return filePreflight;
91
111
  const result = await runSequence(plan, env);
92
112
  if (report === undefined || report.enabled === false)
93
113
  return result;
@@ -133,6 +153,18 @@ async function runGuarded(plan, env, limits, report) {
133
153
  ]
134
154
  };
135
155
  }
156
+ async function preflightPlanFiles(plan, env) {
157
+ const paths = plan.steps
158
+ .flatMap(step => step.command === "files.attach" ? pathsFromAttachStep(step) : []);
159
+ if (paths.length === 0)
160
+ return undefined;
161
+ const result = await preflightFiles(env, { paths });
162
+ return result.ok ? undefined : result;
163
+ }
164
+ function pathsFromAttachStep(step) {
165
+ const paths = step.args.paths;
166
+ return paths.every(item => typeof item === "string") ? paths : [];
167
+ }
136
168
  function normalizeLimits(limits) {
137
169
  return {
138
170
  maxPromptsPerRun: limits?.maxPromptsPerRun ?? 5,
@@ -232,6 +264,7 @@ function planAgentWorkflowFromNormalized(agent, input, defaults = {}) {
232
264
  const wait = input.wait ?? agent.defaults.wait ?? defaults.wait ?? true;
233
265
  const read = input.read ?? agent.defaults.read ?? defaults.read ?? { format: "markdown" };
234
266
  const thread = input.thread ?? agent.defaults.thread ?? { type: "new" };
267
+ const artifactDownload = input.download !== undefined && input.download !== false && usesCreateImageTool(input.tools);
235
268
  const steps = [
236
269
  bootstrapStepForWorkflow(thread, input.existingTab ?? agent.defaults.existingTab ?? defaults.existingTab, input.preferExistingTab ?? agent.defaults.preferExistingTab ?? defaults.preferExistingTab),
237
270
  ...threadSteps(thread)
@@ -246,6 +279,9 @@ function planAgentWorkflowFromNormalized(agent, input, defaults = {}) {
246
279
  if (input.files.length > 0) {
247
280
  steps.push({ id: "attach", command: "files.attach", args: { paths: input.files } });
248
281
  }
282
+ if (artifactDownload) {
283
+ steps.push({ id: "artifactBaseline", command: "artifacts.listLatest", args: { kind: "image" } });
284
+ }
249
285
  if (agent.instructionsMode === "visible_setup_message" && hasInstructions(agent)) {
250
286
  steps.push({
251
287
  id: "agent_setup",
@@ -262,15 +298,22 @@ function planAgentWorkflowFromNormalized(agent, input, defaults = {}) {
262
298
  command: "messages.ask",
263
299
  args: {
264
300
  text: renderRunnerPrompt(agent, input.prompt),
265
- wait,
266
- read
301
+ wait: artifactDownload ? false : wait,
302
+ read: artifactDownload ? false : read
267
303
  }
268
304
  });
305
+ if (artifactDownload) {
306
+ steps.push({
307
+ id: "artifact",
308
+ command: "artifacts.wait",
309
+ args: artifactWaitArgs(wait, input.download === false ? undefined : input.download)
310
+ });
311
+ }
269
312
  if (input.copy !== undefined && input.copy !== false) {
270
313
  steps.push({ id: "copy", command: "response.copy", args: input.copy });
271
314
  }
272
315
  if (input.download !== undefined && input.download !== false) {
273
- steps.push({ id: "download", command: "files.downloadLatest", args: input.download });
316
+ steps.push({ id: "download", command: artifactDownload ? "artifacts.downloadLatest" : "files.downloadLatest", args: input.download });
274
317
  }
275
318
  return {
276
319
  name: `agent-run:${agent.name}`,
@@ -425,17 +468,28 @@ function planAskWorkflow(args, defaults = {}) {
425
468
  if (files.length > 0) {
426
469
  steps.push({ id: "attach", command: "files.attach", args: { paths: files } });
427
470
  }
471
+ const artifactDownload = args.download !== undefined && usesCreateImageTool(args.tools ?? []);
472
+ if (artifactDownload) {
473
+ steps.push({ id: "artifactBaseline", command: "artifacts.listLatest", args: { kind: "image" } });
474
+ }
428
475
  steps.push({
429
476
  id: "ask",
430
477
  command: "messages.ask",
431
478
  args: {
432
479
  text: args.prompt,
433
- wait: args.wait ?? defaults.wait ?? true,
434
- read: args.read ?? defaults.read ?? { format: "markdown" }
480
+ wait: artifactDownload ? false : args.wait ?? defaults.wait ?? true,
481
+ read: artifactDownload ? false : args.read ?? defaults.read ?? { format: "markdown" }
435
482
  }
436
483
  });
437
484
  if (args.download !== undefined) {
438
- steps.push({ id: "download", command: "files.downloadLatest", args: args.download });
485
+ if (artifactDownload) {
486
+ steps.push({
487
+ id: "artifact",
488
+ command: "artifacts.wait",
489
+ args: artifactWaitArgs(args.wait ?? defaults.wait ?? true, args.download)
490
+ });
491
+ }
492
+ steps.push({ id: "download", command: artifactDownload ? "artifacts.downloadLatest" : "files.downloadLatest", args: args.download });
439
493
  }
440
494
  return {
441
495
  name: args.download === undefined ? "ask" : "ask-and-download",
@@ -443,6 +497,31 @@ function planAskWorkflow(args, defaults = {}) {
443
497
  steps
444
498
  };
445
499
  }
500
+ function usesCreateImageTool(tools) {
501
+ return tools.some(tool => normalizeToolName(tool.tool) === "create_image");
502
+ }
503
+ function normalizeToolName(tool) {
504
+ return tool.trim().toLowerCase().replace(/[\s-]+/g, "_");
505
+ }
506
+ function artifactWaitArgs(wait, download) {
507
+ const args = {
508
+ kind: "image",
509
+ afterArtifactCount: "${artifactBaseline.data.count}",
510
+ requireDownload: true
511
+ };
512
+ if (typeof wait === "object") {
513
+ if (wait.timeoutMs !== undefined)
514
+ args.timeoutMs = wait.timeoutMs;
515
+ if (wait.stableMs !== undefined)
516
+ args.stableMs = wait.stableMs;
517
+ if (wait.pollMs !== undefined)
518
+ args.pollMs = wait.pollMs;
519
+ }
520
+ if (args.timeoutMs === undefined && download?.timeoutMs !== undefined) {
521
+ args.timeoutMs = download.timeoutMs;
522
+ }
523
+ return args;
524
+ }
446
525
  function planRunMessages(args, defaults = {}) {
447
526
  const thread = args.thread ?? { type: "new" };
448
527
  const steps = [
@@ -0,0 +1,5 @@
1
+ import type { ArtifactDownloadArgs, ArtifactListData, ArtifactWaitArgs, ArtifactWaitData, CommandResult, DownloadedFile, ListArtifactsArgs, LocatorLike, RuntimeEnv } from "../types.js";
2
+ export declare function listLatestArtifacts(env: RuntimeEnv, args?: ListArtifactsArgs): Promise<CommandResult<ArtifactListData>>;
3
+ export declare function waitForArtifact(env: RuntimeEnv, args?: ArtifactWaitArgs): Promise<CommandResult<ArtifactWaitData>>;
4
+ export declare function downloadLatestArtifact(env: RuntimeEnv, args: ArtifactDownloadArgs): Promise<CommandResult<DownloadedFile>>;
5
+ export declare function locatorCountWithTimeout(locator: LocatorLike | undefined, timeoutMs: number, code: string): Promise<number>;