codex-chatgpt-control 0.1.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 (179) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +54 -0
  4. package/contracts/v1/fixtures/backend-capabilities.json +54 -0
  5. package/contracts/v1/fixtures/backend-error-event-missing-stream-input.json +10 -0
  6. package/contracts/v1/fixtures/backend-error-missing-run-input.json +10 -0
  7. package/contracts/v1/fixtures/backend-runner-plan-request.json +12 -0
  8. package/contracts/v1/fixtures/backend-version.json +10 -0
  9. package/contracts/v1/fixtures/command-descriptors.json +803 -0
  10. package/contracts/v1/fixtures/describe-runner-run.json +31 -0
  11. package/contracts/v1/fixtures/doctor-bridge-upload.json +34 -0
  12. package/contracts/v1/fixtures/help-root.json +6 -0
  13. package/contracts/v1/fixtures/named-plan-two-turn.json +39 -0
  14. package/contracts/v1/fixtures/output-json-parse-success.json +115 -0
  15. package/contracts/v1/fixtures/primitive-bootstrap-blocker.json +43 -0
  16. package/contracts/v1/fixtures/report-redaction-default.json +16 -0
  17. package/contracts/v1/fixtures/reports-create-redacted.json +16 -0
  18. package/contracts/v1/fixtures/reports-summarize-redacted.json +26 -0
  19. package/contracts/v1/fixtures/responses-basic-success.json +27 -0
  20. package/contracts/v1/fixtures/responses-hidden-instructions-unsupported.json +22 -0
  21. package/contracts/v1/fixtures/responses-unknown-field-unsupported.json +22 -0
  22. package/contracts/v1/fixtures/responses-unsupported-previous-response-id.json +22 -0
  23. package/contracts/v1/fixtures/responses-unsupported-temperature.json +22 -0
  24. package/contracts/v1/fixtures/run-basic-success.json +36 -0
  25. package/contracts/v1/fixtures/run-browser-bridge-blocker.json +180 -0
  26. package/contracts/v1/fixtures/run-file-attach-success.json +63 -0
  27. package/contracts/v1/fixtures/run-report-redacted.json +52 -0
  28. package/contracts/v1/fixtures/run-selector-drift-blocker.json +60 -0
  29. package/contracts/v1/fixtures/run-timeout-partial.json +81 -0
  30. package/contracts/v1/fixtures/run-two-turn-success.json +83 -0
  31. package/contracts/v1/fixtures/run-upload-permission-blocker.json +56 -0
  32. package/contracts/v1/fixtures/runner-budget-blocker.json +106 -0
  33. package/contracts/v1/fixtures/runner-full-agent-config.json +50 -0
  34. package/contracts/v1/fixtures/runner-input-items-and-files-plan.json +53 -0
  35. package/contracts/v1/fixtures/runner-metadata-only-plan.json +31 -0
  36. package/contracts/v1/fixtures/runner-visible-prefix-plan.json +28 -0
  37. package/contracts/v1/fixtures/runner-visible-setup-plan.json +37 -0
  38. package/contracts/v1/fixtures/stream-basic.ndjson +3 -0
  39. package/contracts/v1/fixtures/stream-blocked.ndjson +2 -0
  40. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +3 -0
  41. package/contracts/v1/fixtures/workflow-ask-success.json +67 -0
  42. package/contracts/v1/manifest.json +210 -0
  43. package/contracts/v1/parity-suite.json +635 -0
  44. package/contracts/v1/schemas/agent.schema.json +52 -0
  45. package/contracts/v1/schemas/backend-event.schema.json +74 -0
  46. package/contracts/v1/schemas/backend-request.schema.json +59 -0
  47. package/contracts/v1/schemas/backend-response.schema.json +42 -0
  48. package/contracts/v1/schemas/capabilities.schema.json +33 -0
  49. package/contracts/v1/schemas/command-descriptor.schema.json +31 -0
  50. package/contracts/v1/schemas/command-result.schema.json +99 -0
  51. package/contracts/v1/schemas/manifest.schema.json +69 -0
  52. package/contracts/v1/schemas/response.schema.json +51 -0
  53. package/contracts/v1/schemas/run-result.schema.json +138 -0
  54. package/contracts/v1/schemas/sequence-plan.schema.json +44 -0
  55. package/contracts/v1/schemas/stream-event.schema.json +25 -0
  56. package/dist/codex-chatgpt-control-backend.mjs +5284 -0
  57. package/dist/codex-chatgpt-control.bundle.mjs +6013 -0
  58. package/dist/src/backend/client.d.ts +129 -0
  59. package/dist/src/backend/client.js +415 -0
  60. package/dist/src/backend/protocol.d.ts +74 -0
  61. package/dist/src/backend/protocol.js +128 -0
  62. package/dist/src/backend/session.d.ts +11 -0
  63. package/dist/src/backend/session.js +223 -0
  64. package/dist/src/backend/stdio-server.d.ts +9 -0
  65. package/dist/src/backend/stdio-server.js +88 -0
  66. package/dist/src/browser/attach.d.ts +8 -0
  67. package/dist/src/browser/attach.js +456 -0
  68. package/dist/src/browser/clipboard.d.ts +2 -0
  69. package/dist/src/browser/clipboard.js +26 -0
  70. package/dist/src/browser/downloads.d.ts +7 -0
  71. package/dist/src/browser/downloads.js +29 -0
  72. package/dist/src/browser/page-state.d.ts +17 -0
  73. package/dist/src/browser/page-state.js +66 -0
  74. package/dist/src/client.d.ts +167 -0
  75. package/dist/src/client.js +666 -0
  76. package/dist/src/commands/confirmations.d.ts +9 -0
  77. package/dist/src/commands/confirmations.js +33 -0
  78. package/dist/src/commands/context.d.ts +2 -0
  79. package/dist/src/commands/context.js +32 -0
  80. package/dist/src/commands/doctor.d.ts +16 -0
  81. package/dist/src/commands/doctor.js +120 -0
  82. package/dist/src/commands/files.d.ts +4 -0
  83. package/dist/src/commands/files.js +314 -0
  84. package/dist/src/commands/helpers.d.ts +16 -0
  85. package/dist/src/commands/helpers.js +128 -0
  86. package/dist/src/commands/messages.d.ts +16 -0
  87. package/dist/src/commands/messages.js +538 -0
  88. package/dist/src/commands/modes.d.ts +9 -0
  89. package/dist/src/commands/modes.js +323 -0
  90. package/dist/src/commands/output.d.ts +5 -0
  91. package/dist/src/commands/output.js +31 -0
  92. package/dist/src/commands/registry.d.ts +17 -0
  93. package/dist/src/commands/registry.js +275 -0
  94. package/dist/src/commands/reports.d.ts +14 -0
  95. package/dist/src/commands/reports.js +47 -0
  96. package/dist/src/commands/response-actions.d.ts +2 -0
  97. package/dist/src/commands/response-actions.js +141 -0
  98. package/dist/src/commands/sequence.d.ts +9 -0
  99. package/dist/src/commands/sequence.js +228 -0
  100. package/dist/src/commands/session.d.ts +2 -0
  101. package/dist/src/commands/session.js +25 -0
  102. package/dist/src/commands/threads.d.ts +6 -0
  103. package/dist/src/commands/threads.js +300 -0
  104. package/dist/src/dom/fixtures.d.ts +1 -0
  105. package/dist/src/dom/fixtures.js +8 -0
  106. package/dist/src/dom/menus.d.ts +8 -0
  107. package/dist/src/dom/menus.js +40 -0
  108. package/dist/src/dom/message-format.d.ts +30 -0
  109. package/dist/src/dom/message-format.js +593 -0
  110. package/dist/src/dom/messages.d.ts +40 -0
  111. package/dist/src/dom/messages.js +127 -0
  112. package/dist/src/dom/selectors.d.ts +20 -0
  113. package/dist/src/dom/selectors.js +72 -0
  114. package/dist/src/dom/visible-text.d.ts +5 -0
  115. package/dist/src/dom/visible-text.js +27 -0
  116. package/dist/src/errors.d.ts +32 -0
  117. package/dist/src/errors.js +127 -0
  118. package/dist/src/index.d.ts +34 -0
  119. package/dist/src/index.js +34 -0
  120. package/dist/src/logger.d.ts +15 -0
  121. package/dist/src/logger.js +25 -0
  122. package/dist/src/runner/agent.d.ts +2 -0
  123. package/dist/src/runner/agent.js +17 -0
  124. package/dist/src/runner/index.d.ts +7 -0
  125. package/dist/src/runner/index.js +7 -0
  126. package/dist/src/runner/interruptions.d.ts +3 -0
  127. package/dist/src/runner/interruptions.js +65 -0
  128. package/dist/src/runner/responses.d.ts +30 -0
  129. package/dist/src/runner/responses.js +171 -0
  130. package/dist/src/runner/result.d.ts +3 -0
  131. package/dist/src/runner/result.js +128 -0
  132. package/dist/src/runner/resume.d.ts +11 -0
  133. package/dist/src/runner/resume.js +26 -0
  134. package/dist/src/runner/stream.d.ts +13 -0
  135. package/dist/src/runner/stream.js +66 -0
  136. package/dist/src/runner/types.d.ts +264 -0
  137. package/dist/src/runner/types.js +1 -0
  138. package/dist/src/safety/blockers.d.ts +7 -0
  139. package/dist/src/safety/blockers.js +60 -0
  140. package/dist/src/safety/redaction.d.ts +2 -0
  141. package/dist/src/safety/redaction.js +18 -0
  142. package/dist/src/safety/report-redaction.d.ts +8 -0
  143. package/dist/src/safety/report-redaction.js +74 -0
  144. package/dist/src/safety/risk.d.ts +26 -0
  145. package/dist/src/safety/risk.js +28 -0
  146. package/dist/src/scripts/backend-server.d.ts +2 -0
  147. package/dist/src/scripts/backend-server.js +7 -0
  148. package/dist/src/scripts/check-parity-suite.d.ts +1 -0
  149. package/dist/src/scripts/check-parity-suite.js +10 -0
  150. package/dist/src/scripts/continue-thread.d.ts +27 -0
  151. package/dist/src/scripts/continue-thread.js +388 -0
  152. package/dist/src/scripts/live-smoke/harness.d.ts +11 -0
  153. package/dist/src/scripts/live-smoke/harness.js +151 -0
  154. package/dist/src/scripts/live-smoke/scenarios.d.ts +3 -0
  155. package/dist/src/scripts/live-smoke/scenarios.js +648 -0
  156. package/dist/src/scripts/live-smoke/types.d.ts +53 -0
  157. package/dist/src/scripts/live-smoke/types.js +1 -0
  158. package/dist/src/scripts/live-smoke-module.d.ts +3 -0
  159. package/dist/src/scripts/live-smoke-module.js +2 -0
  160. package/dist/src/scripts/live-smoke.d.ts +1 -0
  161. package/dist/src/scripts/live-smoke.js +54 -0
  162. package/dist/src/scripts/smoke-attach-file.d.ts +1 -0
  163. package/dist/src/scripts/smoke-attach-file.js +39 -0
  164. package/dist/src/scripts/smoke-hi.d.ts +1 -0
  165. package/dist/src/scripts/smoke-hi.js +25 -0
  166. package/dist/src/scripts/smoke-search-open-read.d.ts +1 -0
  167. package/dist/src/scripts/smoke-search-open-read.js +41 -0
  168. package/dist/src/testing/parity-suite.d.ts +12 -0
  169. package/dist/src/testing/parity-suite.js +213 -0
  170. package/dist/src/types.d.ts +495 -0
  171. package/dist/src/types.js +1 -0
  172. package/package.json +81 -0
  173. package/references/agents-runner.md +32 -0
  174. package/references/backend-protocol.md +238 -0
  175. package/references/python-parity.md +168 -0
  176. package/references/responses-adapter.md +28 -0
  177. package/references/safety.md +14 -0
  178. package/references/streaming.md +15 -0
  179. package/references/troubleshooting.md +91 -0
@@ -0,0 +1,388 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { createChatGPT } from "../client.js";
3
+ const DEFAULT_SEARCH_LIMIT = 5;
4
+ const DEFAULT_FORMAT = "markdown";
5
+ const CHATGPT_HOSTS = new Set(["chatgpt.com", "www.chatgpt.com", "chat.openai.com"]);
6
+ const RESPONSE_FORMATS = new Set([
7
+ "markdown",
8
+ "text",
9
+ "normalized_text",
10
+ "visible_text",
11
+ "html",
12
+ "blocks",
13
+ "all"
14
+ ]);
15
+ export const CONTINUE_THREAD_USAGE = [
16
+ "Usage:",
17
+ " npm run thread -- \"<ChatGPT thread URL or history search query>\"",
18
+ " npm run thread -- \"<target>\" --prompt \"Continue from the latest answer.\"",
19
+ " npm run thread -- --existing selected",
20
+ " npm run thread -- --existing-conversation-id \"<conversation-id>\" --prompt \"Continue.\"",
21
+ " CHATGPT_THREAD_TARGET=\"<target>\" CHATGPT_THREAD_PROMPT=\"<prompt>\" npm run thread",
22
+ "",
23
+ "Options:",
24
+ " --target, -t ChatGPT /c/... URL or history search query.",
25
+ " --existing selected Claim the selected open ChatGPT tab instead of opening/searching.",
26
+ " --existing-url Claim an open tab by exact ChatGPT thread URL.",
27
+ " --existing-conversation-id Claim an open tab by ChatGPT conversation id.",
28
+ " --existing-tab-id Claim an open user tab by browser bridge tab id.",
29
+ " --open-if-missing Open a URL/conversation target if no matching open tab exists.",
30
+ " --prompt, -p Optional prompt to send after opening the thread. Omit to read only.",
31
+ " --format Response format for the read step. Default: markdown.",
32
+ " --max-chars Maximum response characters to return.",
33
+ " --timeout-ms Wait timeout for continue prompts.",
34
+ " --stable-ms Stable wait window for continue prompts."
35
+ ].join("\n");
36
+ export class ContinueThreadUsageError extends Error {
37
+ exitCode;
38
+ constructor(message, exitCode = 2) {
39
+ super(message);
40
+ this.exitCode = exitCode;
41
+ this.name = "ContinueThreadUsageError";
42
+ }
43
+ }
44
+ export function parseContinueThreadCliArgs(argv, env = process.env) {
45
+ let targetFlag;
46
+ let promptFlag;
47
+ let formatFlag;
48
+ let maxCharsFlag;
49
+ let timeoutMsFlag;
50
+ let stableMsFlag;
51
+ let existingFlag;
52
+ let existingUrlFlag;
53
+ let existingConversationIdFlag;
54
+ let existingTabIdFlag;
55
+ let openIfMissingFlag = false;
56
+ const positionals = [];
57
+ for (let index = 0; index < argv.length; index += 1) {
58
+ const arg = argv[index];
59
+ if (arg === undefined)
60
+ continue;
61
+ switch (arg) {
62
+ case "--help":
63
+ case "-h":
64
+ throw new ContinueThreadUsageError(CONTINUE_THREAD_USAGE, 0);
65
+ case "--target":
66
+ case "-t":
67
+ targetFlag = requiredValue(argv, ++index, arg);
68
+ break;
69
+ case "--prompt":
70
+ case "-p":
71
+ promptFlag = requiredValue(argv, ++index, arg);
72
+ break;
73
+ case "--format":
74
+ formatFlag = requiredValue(argv, ++index, arg);
75
+ break;
76
+ case "--max-chars":
77
+ maxCharsFlag = requiredValue(argv, ++index, arg);
78
+ break;
79
+ case "--timeout-ms":
80
+ timeoutMsFlag = requiredValue(argv, ++index, arg);
81
+ break;
82
+ case "--stable-ms":
83
+ stableMsFlag = requiredValue(argv, ++index, arg);
84
+ break;
85
+ case "--existing":
86
+ existingFlag = requiredValue(argv, ++index, arg);
87
+ break;
88
+ case "--existing-url":
89
+ existingUrlFlag = requiredValue(argv, ++index, arg);
90
+ break;
91
+ case "--existing-conversation-id":
92
+ existingConversationIdFlag = requiredValue(argv, ++index, arg);
93
+ break;
94
+ case "--existing-tab-id":
95
+ existingTabIdFlag = requiredValue(argv, ++index, arg);
96
+ break;
97
+ case "--open-if-missing":
98
+ openIfMissingFlag = true;
99
+ break;
100
+ default:
101
+ positionals.push(arg);
102
+ break;
103
+ }
104
+ }
105
+ const target = firstText(targetFlag, positionals.join(" "), env.CHATGPT_THREAD_TARGET);
106
+ const existing = parseExistingTabPolicy({
107
+ existing: firstText(existingFlag, env.CHATGPT_THREAD_EXISTING),
108
+ url: firstText(existingUrlFlag, env.CHATGPT_THREAD_EXISTING_URL),
109
+ conversationId: firstText(existingConversationIdFlag, env.CHATGPT_THREAD_EXISTING_CONVERSATION_ID),
110
+ tabId: firstText(existingTabIdFlag, env.CHATGPT_THREAD_EXISTING_TAB_ID),
111
+ openIfMissing: openIfMissingFlag || env.CHATGPT_THREAD_OPEN_IF_MISSING === "1" || env.CHATGPT_THREAD_OPEN_IF_MISSING === "true"
112
+ });
113
+ if (target === undefined && existing === undefined) {
114
+ throw new ContinueThreadUsageError(`Missing ChatGPT thread URL, search query, or existing-tab selector.\n\n${CONTINUE_THREAD_USAGE}`);
115
+ }
116
+ if (target !== undefined && existing !== undefined) {
117
+ throw new ContinueThreadUsageError(`Use either a target/search query or an existing-tab selector, not both.\n\n${CONTINUE_THREAD_USAGE}`);
118
+ }
119
+ const options = {
120
+ format: parseResponseFormat(firstText(formatFlag, env.CHATGPT_THREAD_FORMAT) ?? DEFAULT_FORMAT)
121
+ };
122
+ if (target !== undefined)
123
+ options.target = target;
124
+ if (existing !== undefined)
125
+ options.existing = existing;
126
+ const prompt = firstText(promptFlag, env.CHATGPT_THREAD_PROMPT);
127
+ if (prompt !== undefined)
128
+ options.prompt = prompt;
129
+ const maxChars = parsePositiveInteger(firstText(maxCharsFlag, env.CHATGPT_THREAD_MAX_CHARS), "--max-chars");
130
+ if (maxChars !== undefined)
131
+ options.maxChars = maxChars;
132
+ const timeoutMs = parsePositiveInteger(firstText(timeoutMsFlag, env.CHATGPT_THREAD_TIMEOUT_MS), "--timeout-ms");
133
+ if (timeoutMs !== undefined)
134
+ options.timeoutMs = timeoutMs;
135
+ const stableMs = parsePositiveInteger(firstText(stableMsFlag, env.CHATGPT_THREAD_STABLE_MS), "--stable-ms");
136
+ if (stableMs !== undefined)
137
+ options.stableMs = stableMs;
138
+ return options;
139
+ }
140
+ export function threadSelectorFromTarget(target, args = {}) {
141
+ const value = target.trim();
142
+ if (value.length === 0) {
143
+ throw new ContinueThreadUsageError("Thread target must not be empty.");
144
+ }
145
+ const url = chatgptUrlFromTarget(value);
146
+ if (url !== undefined) {
147
+ return { type: "url", url };
148
+ }
149
+ return {
150
+ type: "search",
151
+ query: value,
152
+ select: "first",
153
+ limit: args.limit ?? DEFAULT_SEARCH_LIMIT
154
+ };
155
+ }
156
+ export async function runContinueThread(client, options) {
157
+ const read = readArgs(options);
158
+ const prompt = options.prompt?.trim();
159
+ if (options.existing !== undefined) {
160
+ const bootstrapped = await client.session.bootstrap({ existingTab: options.existing });
161
+ if (!bootstrapped.ok) {
162
+ return bootstrapped;
163
+ }
164
+ if (prompt !== undefined && prompt.length > 0) {
165
+ const asked = await client.askInThread({
166
+ thread: { type: "current" },
167
+ prompt,
168
+ wait: waitArgs(options) ?? true,
169
+ read
170
+ });
171
+ return mergeOpenReadResult(bootstrapped, asked);
172
+ }
173
+ const latest = await client.readLatest(read);
174
+ return mergeOpenReadResult(bootstrapped, latest);
175
+ }
176
+ if (options.target === undefined) {
177
+ throw new ContinueThreadUsageError(`Missing ChatGPT thread URL, search query, or existing-tab selector.\n\n${CONTINUE_THREAD_USAGE}`);
178
+ }
179
+ const thread = threadSelectorFromTarget(options.target);
180
+ if (prompt !== undefined && prompt.length > 0) {
181
+ return client.askInThread({
182
+ thread,
183
+ prompt,
184
+ wait: waitArgs(options) ?? true,
185
+ read
186
+ });
187
+ }
188
+ const opened = await client.openThread(thread);
189
+ if (!opened.ok) {
190
+ return opened;
191
+ }
192
+ const latest = await client.readLatest(read);
193
+ return mergeOpenReadResult(opened, latest);
194
+ }
195
+ export function renderContinueThreadOutput(result) {
196
+ const output = {
197
+ ok: result.ok,
198
+ status: result.status,
199
+ context: result.context
200
+ };
201
+ const text = textFromData(result.data);
202
+ if (text !== undefined)
203
+ output.text = text;
204
+ if (text === undefined && result.data !== undefined)
205
+ output.data = result.data;
206
+ if (result.warnings.length > 0)
207
+ output.warnings = result.warnings;
208
+ if (result.blocker !== undefined)
209
+ output.blocker = result.blocker;
210
+ if (result.error !== undefined)
211
+ output.error = result.error;
212
+ if (result.reportPath !== undefined)
213
+ output.reportPath = result.reportPath;
214
+ return output;
215
+ }
216
+ export async function main(argv = process.argv.slice(2), env = process.env) {
217
+ try {
218
+ const options = parseContinueThreadCliArgs(argv, env);
219
+ const chatgpt = createChatGPT({ agent: globalThis.agent });
220
+ const result = await runContinueThread(chatgpt, options);
221
+ console.log(JSON.stringify(renderContinueThreadOutput(result), null, 2));
222
+ return result.ok ? 0 : result.blocker !== undefined ? 2 : 1;
223
+ }
224
+ catch (error) {
225
+ if (error instanceof ContinueThreadUsageError) {
226
+ console.error(error.message);
227
+ return error.exitCode;
228
+ }
229
+ console.error(error instanceof Error ? error.message : String(error));
230
+ return 1;
231
+ }
232
+ }
233
+ function requiredValue(argv, index, flag) {
234
+ const value = argv[index];
235
+ if (value === undefined || value.startsWith("--")) {
236
+ throw new ContinueThreadUsageError(`Missing value for ${flag}.\n\n${CONTINUE_THREAD_USAGE}`);
237
+ }
238
+ return value;
239
+ }
240
+ function firstText(...values) {
241
+ for (const value of values) {
242
+ const trimmed = value?.trim();
243
+ if (trimmed !== undefined && trimmed.length > 0) {
244
+ return trimmed;
245
+ }
246
+ }
247
+ return undefined;
248
+ }
249
+ function parseResponseFormat(value) {
250
+ if (RESPONSE_FORMATS.has(value)) {
251
+ return value;
252
+ }
253
+ throw new ContinueThreadUsageError(`Unsupported response format "${value}". Use one of: ${Array.from(RESPONSE_FORMATS).join(", ")}.`);
254
+ }
255
+ function parseExistingTabPolicy(args) {
256
+ const selectors = [
257
+ args.existing !== undefined ? "existing" : undefined,
258
+ args.url !== undefined ? "existing-url" : undefined,
259
+ args.conversationId !== undefined ? "existing-conversation-id" : undefined,
260
+ args.tabId !== undefined ? "existing-tab-id" : undefined
261
+ ].filter(Boolean);
262
+ if (selectors.length === 0) {
263
+ return undefined;
264
+ }
265
+ if (selectors.length > 1) {
266
+ throw new ContinueThreadUsageError(`Use only one existing-tab selector, not ${selectors.join(", ")}.\n\n${CONTINUE_THREAD_USAGE}`);
267
+ }
268
+ const ifMissing = args.openIfMissing === true ? "open" : "block";
269
+ if (args.existing !== undefined) {
270
+ const mode = args.existing.trim().toLowerCase();
271
+ if (mode !== "selected") {
272
+ throw new ContinueThreadUsageError(`Unsupported --existing value "${args.existing}". Use: selected.`);
273
+ }
274
+ return { target: { type: "selected", host: "chatgpt" }, ifMissing };
275
+ }
276
+ if (args.url !== undefined) {
277
+ const url = chatgptUrlFromTarget(args.url);
278
+ if (url === undefined) {
279
+ throw new ContinueThreadUsageError("--existing-url must be a ChatGPT thread URL.");
280
+ }
281
+ return { target: { type: "url", url }, ifMissing };
282
+ }
283
+ if (args.conversationId !== undefined) {
284
+ return { target: { type: "conversationId", conversationId: args.conversationId }, ifMissing };
285
+ }
286
+ if (args.tabId !== undefined) {
287
+ return { target: { type: "tabId", tabId: args.tabId }, ifMissing };
288
+ }
289
+ return undefined;
290
+ }
291
+ function parsePositiveInteger(value, label) {
292
+ if (value === undefined)
293
+ return undefined;
294
+ const parsed = Number(value);
295
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
296
+ throw new ContinueThreadUsageError(`${label} must be a positive integer.`);
297
+ }
298
+ return parsed;
299
+ }
300
+ function chatgptUrlFromTarget(target) {
301
+ const maybeUrl = target.startsWith("/c/") ? new URL(target, "https://chatgpt.com") : parseUrl(target);
302
+ if (maybeUrl === undefined) {
303
+ return undefined;
304
+ }
305
+ if (!CHATGPT_HOSTS.has(maybeUrl.hostname)) {
306
+ throw new ContinueThreadUsageError("Target URL must be a ChatGPT thread URL from chatgpt.com or chat.openai.com.");
307
+ }
308
+ return maybeUrl.toString();
309
+ }
310
+ function parseUrl(value) {
311
+ try {
312
+ const url = new URL(value);
313
+ return url.protocol === "http:" || url.protocol === "https:" ? url : undefined;
314
+ }
315
+ catch {
316
+ return undefined;
317
+ }
318
+ }
319
+ function readArgs(options) {
320
+ const args = {
321
+ role: "assistant",
322
+ format: options.format
323
+ };
324
+ if (options.maxChars !== undefined)
325
+ args.maxChars = options.maxChars;
326
+ return args;
327
+ }
328
+ function waitArgs(options) {
329
+ if (options.timeoutMs === undefined && options.stableMs === undefined) {
330
+ return undefined;
331
+ }
332
+ const args = {};
333
+ if (options.timeoutMs !== undefined)
334
+ args.timeoutMs = options.timeoutMs;
335
+ if (options.stableMs !== undefined)
336
+ args.stableMs = options.stableMs;
337
+ return args;
338
+ }
339
+ function mergeOpenReadResult(opened, latest) {
340
+ const merged = {
341
+ ...latest,
342
+ warnings: [...opened.warnings, ...latest.warnings],
343
+ context: mergeContext(opened.context, latest.context)
344
+ };
345
+ if (opened.steps !== undefined || latest.steps !== undefined) {
346
+ merged.steps = [...(opened.steps ?? []), ...(latest.steps ?? [])];
347
+ }
348
+ return merged;
349
+ }
350
+ function mergeContext(opened, latest) {
351
+ const context = { timestamp: latest.timestamp };
352
+ const url = latest.url ?? opened.url;
353
+ const conversationId = latest.conversationId ?? opened.conversationId;
354
+ const title = latest.title ?? opened.title;
355
+ const turnCount = latest.turnCount ?? opened.turnCount;
356
+ const assistantTurnCount = latest.assistantTurnCount ?? opened.assistantTurnCount;
357
+ const browserName = latest.browserName ?? opened.browserName;
358
+ const tabId = latest.tabId ?? opened.tabId;
359
+ if (url !== undefined)
360
+ context.url = url;
361
+ if (conversationId !== undefined)
362
+ context.conversationId = conversationId;
363
+ if (title !== undefined)
364
+ context.title = title;
365
+ if (turnCount !== undefined)
366
+ context.turnCount = turnCount;
367
+ if (assistantTurnCount !== undefined)
368
+ context.assistantTurnCount = assistantTurnCount;
369
+ if (browserName !== undefined)
370
+ context.browserName = browserName;
371
+ if (tabId !== undefined)
372
+ context.tabId = tabId;
373
+ return context;
374
+ }
375
+ function textFromData(data) {
376
+ if (data === null || typeof data !== "object") {
377
+ return undefined;
378
+ }
379
+ const record = data;
380
+ const text = record.text ?? record.responseText;
381
+ return typeof text === "string" ? text : undefined;
382
+ }
383
+ function isDirectRun() {
384
+ return process.argv[1] !== undefined && fileURLToPath(import.meta.url) === process.argv[1];
385
+ }
386
+ if (isDirectRun()) {
387
+ process.exitCode = await main();
388
+ }
@@ -0,0 +1,11 @@
1
+ import type { LiveSmokeContext, LiveSmokeRunResult, LiveSmokeScenario, LiveSmokeScenarioResult } from "./types.js";
2
+ export declare function envFlag(name: string): boolean;
3
+ export declare function envText(name: string): string | undefined;
4
+ export declare function contextEnvFlag(context: LiveSmokeContext, name: string): boolean;
5
+ export declare function contextEnvText(context: LiveSmokeContext, name: string): string | undefined;
6
+ export declare function runScenario(scenario: LiveSmokeScenario, context: LiveSmokeContext): Promise<LiveSmokeScenarioResult>;
7
+ export declare function runLiveSmoke(context: LiveSmokeContext, scenarios: LiveSmokeScenario[]): Promise<LiveSmokeRunResult>;
8
+ export declare function writeReport(reportDir: string, results: LiveSmokeScenarioResult[]): Promise<string>;
9
+ export declare function redactLiveSmokeResult(result: LiveSmokeScenarioResult): LiveSmokeScenarioResult;
10
+ export declare function requiredFailures(results: LiveSmokeScenarioResult[]): LiveSmokeScenarioResult[];
11
+ export declare function filterScenarios(scenarios: LiveSmokeScenario[], namesCsv: string | undefined): LiveSmokeScenario[];
@@ -0,0 +1,151 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { redactReportValue } from "../../safety/report-redaction.js";
4
+ const CLEANUP_TIMEOUT_MS = 10_000;
5
+ export function envFlag(name) {
6
+ const value = readEnv(name);
7
+ return value === "1" || value?.toLowerCase() === "true";
8
+ }
9
+ export function envText(name) {
10
+ const value = readEnv(name)?.trim();
11
+ return value && value.length > 0 ? value : undefined;
12
+ }
13
+ export function contextEnvFlag(context, name) {
14
+ const value = contextEnvText(context, name);
15
+ return value === "1" || value?.toLowerCase() === "true";
16
+ }
17
+ export function contextEnvText(context, name) {
18
+ const value = context.env?.[name]?.trim() ?? envText(name);
19
+ return value && value.length > 0 ? value : undefined;
20
+ }
21
+ function readEnv(name) {
22
+ return typeof process === "undefined" ? undefined : process.env[name];
23
+ }
24
+ export async function runScenario(scenario, context) {
25
+ const startedAt = new Date().toISOString();
26
+ const startedMs = Date.now();
27
+ let result;
28
+ if (!scenario.enabled(context)) {
29
+ result = {
30
+ name: scenario.name,
31
+ status: "skip",
32
+ required: scenario.required,
33
+ startedAt,
34
+ endedAt: new Date().toISOString(),
35
+ durationMs: Date.now() - startedMs,
36
+ details: { reason: "scenario disabled" }
37
+ };
38
+ }
39
+ else {
40
+ try {
41
+ result = await scenario.run(context);
42
+ }
43
+ catch (error) {
44
+ result = {
45
+ name: scenario.name,
46
+ status: "fail",
47
+ required: scenario.required,
48
+ startedAt,
49
+ endedAt: new Date().toISOString(),
50
+ durationMs: Date.now() - startedMs,
51
+ error: {
52
+ name: error instanceof Error ? error.name : "Error",
53
+ message: error instanceof Error ? error.message : String(error)
54
+ }
55
+ };
56
+ }
57
+ }
58
+ const cleanup = await finalizeBrowserTabs(context.browser);
59
+ return { ...result, cleanup };
60
+ }
61
+ export async function runLiveSmoke(context, scenarios) {
62
+ const results = [];
63
+ for (const scenario of scenarios) {
64
+ const result = await runScenario(scenario, context);
65
+ results.push(result);
66
+ console.log(JSON.stringify(redactLiveSmokeResult(result), null, 2));
67
+ }
68
+ const reportPath = await writeReport(context.reportDir, results);
69
+ const failures = requiredFailures(results);
70
+ console.log(JSON.stringify({ reportPath, requiredFailures: failures.map(failure => failure.name) }, null, 2));
71
+ return { reportPath, results, requiredFailures: failures };
72
+ }
73
+ export async function writeReport(reportDir, results) {
74
+ await mkdir(reportDir, { recursive: true });
75
+ const stamp = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-");
76
+ const path = join(reportDir, `${stamp}-live-smoke.json`);
77
+ const summary = {
78
+ total: results.length,
79
+ passed: results.filter(result => result.status === "pass").length,
80
+ failed: results.filter(result => result.status === "fail").length,
81
+ skipped: results.filter(result => result.status === "skip").length,
82
+ requiredFailures: requiredFailures(results).map(result => result.name)
83
+ };
84
+ await writeFile(path, `${JSON.stringify({ summary, results: results.map(redactLiveSmokeResult) }, null, 2)}\n`, "utf8");
85
+ return path;
86
+ }
87
+ export function redactLiveSmokeResult(result) {
88
+ const redacted = redactReportValue(result, { includeContent: false });
89
+ return {
90
+ ...redacted,
91
+ name: result.name,
92
+ status: result.status,
93
+ required: result.required,
94
+ startedAt: result.startedAt,
95
+ endedAt: result.endedAt,
96
+ durationMs: result.durationMs
97
+ };
98
+ }
99
+ export function requiredFailures(results) {
100
+ return results.filter(result => result.required && result.status !== "pass");
101
+ }
102
+ export function filterScenarios(scenarios, namesCsv) {
103
+ if (namesCsv === undefined || namesCsv.trim().length === 0) {
104
+ return scenarios;
105
+ }
106
+ const wanted = new Set(namesCsv.split(",")
107
+ .map(name => name.trim())
108
+ .filter(Boolean));
109
+ return scenarios.filter(scenario => wanted.has(scenario.name));
110
+ }
111
+ async function finalizeBrowserTabs(browser) {
112
+ const tabs = browser?.tabs;
113
+ const finalize = tabs?.finalize;
114
+ if (typeof finalize !== "function") {
115
+ return {
116
+ attempted: false,
117
+ ok: false,
118
+ reason: "browser.tabs.finalize unavailable"
119
+ };
120
+ }
121
+ try {
122
+ await withTimeout(finalize.call(tabs, { keep: [] }), CLEANUP_TIMEOUT_MS, `browser.tabs.finalize timed out after ${CLEANUP_TIMEOUT_MS}ms`);
123
+ return { attempted: true, ok: true };
124
+ }
125
+ catch (error) {
126
+ return {
127
+ attempted: true,
128
+ ok: false,
129
+ error: {
130
+ name: error instanceof Error ? error.name : "Error",
131
+ message: error instanceof Error ? error.message : String(error)
132
+ }
133
+ };
134
+ }
135
+ }
136
+ async function withTimeout(promise, timeoutMs, message) {
137
+ let timeout;
138
+ try {
139
+ return await Promise.race([
140
+ promise,
141
+ new Promise((_, reject) => {
142
+ timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
143
+ })
144
+ ]);
145
+ }
146
+ finally {
147
+ if (timeout !== undefined) {
148
+ clearTimeout(timeout);
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,3 @@
1
+ import type { LiveSmokeScenario } from "./types.js";
2
+ export declare const requiredScenarios: LiveSmokeScenario[];
3
+ export declare const optionalScenarios: LiveSmokeScenario[];