@tiny-fish/cli 0.30.2-next.264 → 0.30.2-next.265

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 (2) hide show
  1. package/dist/commands/run.js +193 -174
  2. package/package.json +1 -1
@@ -124,6 +124,194 @@ async function loadOutputSchema(opts) {
124
124
  }
125
125
  return parseOutputSchema(fileContents, '--output-schema-file');
126
126
  }
127
+ function validateRunInvocation(goal, opts) {
128
+ if (!opts.url) {
129
+ err({ error: "required option '--url <url>' not specified" });
130
+ process.exit(1);
131
+ }
132
+ if (opts.sync && opts.async) {
133
+ err({ error: '--sync and --async are mutually exclusive' });
134
+ process.exit(1);
135
+ }
136
+ // Normalise URL: accept bare hostnames by prepending https://.
137
+ // The server requires a valid URL — it does not auto-prefix.
138
+ let normalizedUrl = opts.url;
139
+ try {
140
+ new URL(opts.url);
141
+ }
142
+ catch {
143
+ try {
144
+ new URL(`https://${opts.url}`);
145
+ normalizedUrl = `https://${opts.url}`;
146
+ }
147
+ catch {
148
+ err({
149
+ error: `Invalid --url "${opts.url}". Provide a valid URL, e.g. https://example.com or example.com`,
150
+ });
151
+ process.exit(1);
152
+ }
153
+ }
154
+ if (!goal.trim()) {
155
+ err({ error: 'Goal cannot be empty' });
156
+ process.exit(1);
157
+ }
158
+ // The server rejects credential_item_ids without use_vault (400); fail fast here.
159
+ if (opts.credentialItemId.length > 0 && !opts.useVault) {
160
+ err({ error: '--credential-item-id requires --use-vault' });
161
+ process.exit(1);
162
+ }
163
+ // The server rejects profile_id without use_profile (400); fail fast here.
164
+ if (opts.profileId !== undefined && !opts.useProfile) {
165
+ err({ error: '--profile-id requires --use-profile' });
166
+ process.exit(1);
167
+ }
168
+ return normalizedUrl;
169
+ }
170
+ function buildAgentConfig(opts) {
171
+ const mode = parseChoice(opts.mode, VALID_MODES, '--mode');
172
+ const cursorStyle = parseChoice(opts.cursorStyle, VALID_CURSOR_STYLES, '--cursor-style');
173
+ const maxSteps = parsePositiveInteger(opts.maxSteps, '--max-steps', MAX_STEPS_LIMIT);
174
+ const maxDurationSeconds = parsePositiveInteger(opts.maxDurationSeconds, '--max-duration-seconds');
175
+ if (mode === undefined &&
176
+ cursorStyle === undefined &&
177
+ maxSteps === undefined &&
178
+ maxDurationSeconds === undefined) {
179
+ return undefined;
180
+ }
181
+ return {
182
+ ...(mode !== undefined ? { mode } : {}),
183
+ ...(cursorStyle !== undefined ? { cursor_style: cursorStyle } : {}),
184
+ ...(maxSteps !== undefined ? { max_steps: maxSteps } : {}),
185
+ ...(maxDurationSeconds !== undefined ? { max_duration_seconds: maxDurationSeconds } : {}),
186
+ };
187
+ }
188
+ function buildCaptureConfig(opts) {
189
+ if (opts.captureElements === undefined &&
190
+ opts.captureSnapshots === undefined &&
191
+ opts.captureScreenshots === undefined &&
192
+ opts.captureRecording === undefined &&
193
+ opts.captureHtml === undefined) {
194
+ return undefined;
195
+ }
196
+ return {
197
+ ...(opts.captureElements !== undefined ? { elements: opts.captureElements } : {}),
198
+ ...(opts.captureSnapshots !== undefined ? { snapshots: opts.captureSnapshots } : {}),
199
+ ...(opts.captureScreenshots !== undefined ? { screenshots: opts.captureScreenshots } : {}),
200
+ ...(opts.captureRecording !== undefined ? { recording: opts.captureRecording } : {}),
201
+ ...(opts.captureHtml !== undefined ? { html: opts.captureHtml } : {}),
202
+ };
203
+ }
204
+ async function buildRunRequest(goal, normalizedUrl, opts) {
205
+ const outputSchema = await loadOutputSchema(opts);
206
+ const browserProfile = parseChoice(opts.browserProfile, VALID_BROWSER_PROFILES, '--browser-profile');
207
+ const agentConfig = buildAgentConfig(opts);
208
+ const captureConfig = buildCaptureConfig(opts);
209
+ const sessionId = parseSessionId(opts.sessionId);
210
+ const webhookUrl = parseHttpsUrl(opts.webhookUrl, '--webhook-url');
211
+ return {
212
+ goal,
213
+ url: normalizedUrl,
214
+ ...(outputSchema !== undefined ? { output_schema: outputSchema } : {}),
215
+ ...(browserProfile !== undefined ? { browser_profile: browserProfile } : {}),
216
+ ...(agentConfig !== undefined ? { agent_config: agentConfig } : {}),
217
+ ...(captureConfig !== undefined ? { capture_config: captureConfig } : {}),
218
+ ...(webhookUrl !== undefined ? { webhook_url: webhookUrl } : {}),
219
+ ...(opts.useProfile !== undefined ? { use_profile: opts.useProfile } : {}),
220
+ ...(opts.profileId !== undefined ? { profile_id: opts.profileId } : {}),
221
+ ...(sessionId !== undefined ? { session_id: sessionId } : {}),
222
+ ...(opts.useVault ? { use_vault: true } : {}),
223
+ ...(opts.credentialItemId.length > 0 ? { credential_item_ids: opts.credentialItemId } : {}),
224
+ };
225
+ }
226
+ async function runAsyncPath(req, apiKey, pretty) {
227
+ let result;
228
+ try {
229
+ result = await runAsync(req, apiKey);
230
+ }
231
+ catch (e) {
232
+ handleApiError(e);
233
+ }
234
+ checkRunResult(result, false); // async = PENDING is expected
235
+ const asyncResultWithUrl = {
236
+ ...result,
237
+ status: RunStatus.PENDING,
238
+ run_url: `${BASE_URL}/runs/${result.run_id}`,
239
+ };
240
+ if (pretty) {
241
+ outLine(`Run submitted\nID: ${asyncResultWithUrl.run_id}\nURL: ${asyncResultWithUrl.run_url}`);
242
+ }
243
+ else {
244
+ out(asyncResultWithUrl);
245
+ }
246
+ }
247
+ async function runSyncPath(req, apiKey, pretty) {
248
+ errLine('Waiting for result...');
249
+ let result;
250
+ try {
251
+ result = await runSync(req, apiKey);
252
+ }
253
+ catch (e) {
254
+ handleApiError(e);
255
+ }
256
+ checkRunResult(result);
257
+ const syncResultWithUrl = {
258
+ ...result,
259
+ run_url: `${BASE_URL}/runs/${result.run_id}`,
260
+ };
261
+ if (pretty) {
262
+ outLine(`Status: ${syncResultWithUrl.status}\n\n${JSON.stringify(syncResultWithUrl.result ?? {}, null, 2)}`);
263
+ }
264
+ else {
265
+ out(syncResultWithUrl);
266
+ }
267
+ }
268
+ // Default: SSE stream — Ctrl+C cancels the active run.
269
+ async function runStreamPath(req, apiKey, pretty) {
270
+ const controller = new AbortController();
271
+ let siginted = false;
272
+ let capturedRunId = null;
273
+ const onSigint = () => {
274
+ siginted = true;
275
+ controller.abort();
276
+ };
277
+ process.once('SIGINT', onSigint);
278
+ let streamFailed = false;
279
+ try {
280
+ for await (const event of runStream(req, apiKey, controller.signal)) {
281
+ if (!handleStreamEvent(event, pretty, (id) => {
282
+ capturedRunId = id;
283
+ })) {
284
+ streamFailed = true;
285
+ break;
286
+ }
287
+ if (event.type === EventType.COMPLETE) {
288
+ break;
289
+ }
290
+ }
291
+ }
292
+ catch (e) {
293
+ if (controller.signal.aborted && siginted) {
294
+ let cancelled = false;
295
+ if (capturedRunId) {
296
+ try {
297
+ await cancelRun(capturedRunId, apiKey);
298
+ cancelled = true;
299
+ }
300
+ catch {
301
+ /* ignore */
302
+ }
303
+ }
304
+ process.stderr.write(cancelled ? '\nRun cancelled.\n' : '\n');
305
+ process.exit(130);
306
+ }
307
+ handleApiError(e);
308
+ }
309
+ finally {
310
+ process.removeListener('SIGINT', onSigint);
311
+ }
312
+ if (streamFailed)
313
+ process.exit(1);
314
+ }
127
315
  export function registerRun(agentCmd) {
128
316
  const runCmd = agentCmd
129
317
  .command('run')
@@ -159,187 +347,18 @@ export function registerRun(agentCmd) {
159
347
  runCmd.help();
160
348
  return;
161
349
  }
162
- if (!opts.url) {
163
- err({ error: "required option '--url <url>' not specified" });
164
- process.exit(1);
165
- }
166
- if (opts.sync && opts.async) {
167
- err({ error: '--sync and --async are mutually exclusive' });
168
- process.exit(1);
169
- }
170
- // Normalise URL: accept bare hostnames by prepending https://.
171
- // The server requires a valid URL — it does not auto-prefix.
172
- let normalizedUrl = opts.url;
173
- try {
174
- new URL(opts.url);
175
- }
176
- catch {
177
- try {
178
- new URL(`https://${opts.url}`);
179
- normalizedUrl = `https://${opts.url}`;
180
- }
181
- catch {
182
- err({
183
- error: `Invalid --url "${opts.url}". Provide a valid URL, e.g. https://example.com or example.com`,
184
- });
185
- process.exit(1);
186
- }
187
- }
188
- if (!goal.trim()) {
189
- err({ error: 'Goal cannot be empty' });
190
- process.exit(1);
191
- }
192
- // The server rejects credential_item_ids without use_vault (400); fail fast here.
193
- const hasCredentialItemIds = opts.credentialItemId.length > 0;
194
- if (hasCredentialItemIds && !opts.useVault) {
195
- err({ error: '--credential-item-id requires --use-vault' });
196
- process.exit(1);
197
- }
198
- // The server rejects profile_id without use_profile (400); fail fast here.
199
- if (opts.profileId !== undefined && !opts.useProfile) {
200
- err({ error: '--profile-id requires --use-profile' });
201
- process.exit(1);
202
- }
203
- const outputSchema = await loadOutputSchema(opts);
204
- const browserProfile = parseChoice(opts.browserProfile, VALID_BROWSER_PROFILES, '--browser-profile');
205
- const mode = parseChoice(opts.mode, VALID_MODES, '--mode');
206
- const cursorStyle = parseChoice(opts.cursorStyle, VALID_CURSOR_STYLES, '--cursor-style');
207
- const maxSteps = parsePositiveInteger(opts.maxSteps, '--max-steps', MAX_STEPS_LIMIT);
208
- const maxDurationSeconds = parsePositiveInteger(opts.maxDurationSeconds, '--max-duration-seconds');
209
- const sessionId = parseSessionId(opts.sessionId);
210
- const webhookUrl = parseHttpsUrl(opts.webhookUrl, '--webhook-url');
211
- const agentConfig = mode !== undefined ||
212
- cursorStyle !== undefined ||
213
- maxSteps !== undefined ||
214
- maxDurationSeconds !== undefined
215
- ? {
216
- ...(mode !== undefined ? { mode } : {}),
217
- ...(cursorStyle !== undefined ? { cursor_style: cursorStyle } : {}),
218
- ...(maxSteps !== undefined ? { max_steps: maxSteps } : {}),
219
- ...(maxDurationSeconds !== undefined
220
- ? { max_duration_seconds: maxDurationSeconds }
221
- : {}),
222
- }
223
- : undefined;
224
- const captureConfig = opts.captureElements !== undefined ||
225
- opts.captureSnapshots !== undefined ||
226
- opts.captureScreenshots !== undefined ||
227
- opts.captureRecording !== undefined ||
228
- opts.captureHtml !== undefined
229
- ? {
230
- ...(opts.captureElements !== undefined ? { elements: opts.captureElements } : {}),
231
- ...(opts.captureSnapshots !== undefined ? { snapshots: opts.captureSnapshots } : {}),
232
- ...(opts.captureScreenshots !== undefined
233
- ? { screenshots: opts.captureScreenshots }
234
- : {}),
235
- ...(opts.captureRecording !== undefined ? { recording: opts.captureRecording } : {}),
236
- ...(opts.captureHtml !== undefined ? { html: opts.captureHtml } : {}),
237
- }
238
- : undefined;
350
+ const normalizedUrl = validateRunInvocation(goal, opts);
351
+ const req = await buildRunRequest(goal, normalizedUrl, opts);
239
352
  const apiKey = getApiKey();
240
- const req = {
241
- goal,
242
- url: normalizedUrl,
243
- ...(outputSchema !== undefined ? { output_schema: outputSchema } : {}),
244
- ...(browserProfile !== undefined ? { browser_profile: browserProfile } : {}),
245
- ...(agentConfig !== undefined ? { agent_config: agentConfig } : {}),
246
- ...(captureConfig !== undefined ? { capture_config: captureConfig } : {}),
247
- ...(webhookUrl !== undefined ? { webhook_url: webhookUrl } : {}),
248
- ...(opts.useProfile !== undefined ? { use_profile: opts.useProfile } : {}),
249
- ...(opts.profileId !== undefined ? { profile_id: opts.profileId } : {}),
250
- ...(sessionId !== undefined ? { session_id: sessionId } : {}),
251
- ...(opts.useVault ? { use_vault: true } : {}),
252
- ...(hasCredentialItemIds ? { credential_item_ids: opts.credentialItemId } : {}),
253
- };
254
353
  if (opts.async) {
255
- let result;
256
- try {
257
- result = await runAsync(req, apiKey);
258
- }
259
- catch (e) {
260
- handleApiError(e);
261
- }
262
- checkRunResult(result, false); // async = PENDING is expected
263
- const asyncResultWithUrl = {
264
- ...result,
265
- status: RunStatus.PENDING,
266
- run_url: `${BASE_URL}/runs/${result.run_id}`,
267
- };
268
- if (opts.pretty) {
269
- outLine(`Run submitted\nID: ${asyncResultWithUrl.run_id}\nURL: ${asyncResultWithUrl.run_url}`);
270
- }
271
- else {
272
- out(asyncResultWithUrl);
273
- }
354
+ await runAsyncPath(req, apiKey, opts.pretty);
274
355
  return;
275
356
  }
276
357
  if (opts.sync) {
277
- errLine('Waiting for result...');
278
- let result;
279
- try {
280
- result = await runSync(req, apiKey);
281
- }
282
- catch (e) {
283
- handleApiError(e);
284
- }
285
- checkRunResult(result);
286
- const syncResultWithUrl = {
287
- ...result,
288
- run_url: `${BASE_URL}/runs/${result.run_id}`,
289
- };
290
- if (opts.pretty) {
291
- outLine(`Status: ${syncResultWithUrl.status}\n\n${JSON.stringify(syncResultWithUrl.result ?? {}, null, 2)}`);
292
- }
293
- else {
294
- out(syncResultWithUrl);
295
- }
358
+ await runSyncPath(req, apiKey, opts.pretty);
296
359
  return;
297
360
  }
298
- // Default: SSE stream — Ctrl+C cancels the active run.
299
- const controller = new AbortController();
300
- let siginted = false;
301
- let capturedRunId = null;
302
- const onSigint = () => {
303
- siginted = true;
304
- controller.abort();
305
- };
306
- process.once('SIGINT', onSigint);
307
- let streamFailed = false;
308
- try {
309
- for await (const event of runStream(req, apiKey, controller.signal)) {
310
- if (!handleStreamEvent(event, opts.pretty, (id) => {
311
- capturedRunId = id;
312
- })) {
313
- streamFailed = true;
314
- break;
315
- }
316
- if (event.type === EventType.COMPLETE) {
317
- break;
318
- }
319
- }
320
- }
321
- catch (e) {
322
- if (controller.signal.aborted && siginted) {
323
- let cancelled = false;
324
- if (capturedRunId) {
325
- try {
326
- await cancelRun(capturedRunId, apiKey);
327
- cancelled = true;
328
- }
329
- catch {
330
- /* ignore */
331
- }
332
- }
333
- process.stderr.write(cancelled ? '\nRun cancelled.\n' : '\n');
334
- process.exit(130);
335
- }
336
- handleApiError(e);
337
- }
338
- finally {
339
- process.removeListener('SIGINT', onSigint);
340
- }
341
- if (streamFailed)
342
- process.exit(1);
361
+ await runStreamPath(req, apiKey, opts.pretty);
343
362
  });
344
363
  return runCmd;
345
364
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.30.2-next.264",
3
+ "version": "0.30.2-next.265",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {