@tiny-fish/cli 0.23.1-next.201 → 0.23.1-next.204

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -142,7 +142,18 @@ tinyfish agent run "Check checkout" \
142
142
  --url https://example.com \
143
143
  --browser-profile stealth \
144
144
  --mode strict \
145
- --max-steps 75
145
+ --max-steps 75 \
146
+ --max-duration-seconds 300 \
147
+ --cursor-style standard
148
+
149
+ # Capture run artifacts, send a webhook, and use a saved profile
150
+ tinyfish agent run "Check checkout" \
151
+ --url https://example.com \
152
+ --capture-screenshots \
153
+ --capture-html \
154
+ --capture-recording \
155
+ --webhook-url https://example.com/tinyfish-webhook \
156
+ --use-profile
146
157
 
147
158
  # Wait for result without streaming
148
159
  tinyfish agent run "Find the pricing page" --url https://example.com --sync
@@ -163,6 +174,15 @@ tinyfish agent run "Extract the product price" \
163
174
  --output-schema-file ./schemas/product-price.json
164
175
  ```
165
176
 
177
+ ### Agent option flags
178
+
179
+ - `--cursor-style`, `--max-duration-seconds`, `--mode`, `--max-steps` — agent behavior, sent as `agent_config`
180
+ - `--capture-elements`, `--capture-snapshots`, `--capture-screenshots`, `--capture-recording`, `--capture-html` — artifacts to capture, sent as `capture_config`
181
+ - `--webhook-url` — HTTPS URL notified on run lifecycle events; validated locally
182
+ - `--use-profile`, `--profile-id` — start from a Browser Context Profile; `--profile-id` requires `--use-profile`
183
+
184
+ `--capture-elements` and `--capture-recording` are gated capabilities: an account without them enabled gets a 403 saying the capability is not enabled. The other capture flags are ungated.
185
+
166
186
  ### Structured output
167
187
 
168
188
  Use `--output-schema <json>` for short inline schemas and `--output-schema-file <path>` for schemas you want to reuse, review, or keep in source control.
@@ -7,6 +7,7 @@ import { err, errLine, handleApiError, out, outLine } from "../lib/output.js";
7
7
  import { BASE_URL } from "../lib/constants.js";
8
8
  const VALID_BROWSER_PROFILES = ["lite", "stealth"];
9
9
  const VALID_MODES = ["default", "strict"];
10
+ const VALID_CURSOR_STYLES = ["fish", "standard", "green-pointer"];
10
11
  const MAX_STEPS_LIMIT = 500;
11
12
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
13
  function parseSessionId(value) {
@@ -69,19 +70,36 @@ function parseChoice(value, valid, flag) {
69
70
  err({ error: `Invalid ${flag} "${value}". Must be one of: ${valid.join(", ")}` });
70
71
  process.exit(1);
71
72
  }
72
- function parseMaxSteps(value) {
73
+ function parsePositiveInteger(value, flag, max) {
73
74
  if (value === undefined)
74
75
  return undefined;
75
- if (!/^\d+$/.test(value)) {
76
- err({ error: `Invalid --max-steps "${value}". Must be a positive integer.` });
76
+ const parsed = Number(value);
77
+ if (!/^\d+$/.test(value) || parsed < 1 || (max !== undefined && parsed > max)) {
78
+ err({
79
+ error: max === undefined
80
+ ? `Invalid ${flag} "${value}". Must be a positive integer.`
81
+ : `${flag} must be between 1 and ${max}.`,
82
+ });
83
+ process.exit(1);
84
+ }
85
+ return parsed;
86
+ }
87
+ function parseHttpsUrl(value, flag) {
88
+ if (value === undefined)
89
+ return undefined;
90
+ let parsed;
91
+ try {
92
+ parsed = new URL(value);
93
+ }
94
+ catch {
95
+ err({ error: `Invalid ${flag} "${value}". Provide a valid URL.` });
77
96
  process.exit(1);
78
97
  }
79
- const maxSteps = Number(value);
80
- if (maxSteps < 1 || maxSteps > MAX_STEPS_LIMIT) {
81
- err({ error: `--max-steps must be between 1 and ${MAX_STEPS_LIMIT}.` });
98
+ if (parsed.protocol !== "https:") {
99
+ err({ error: `${flag} must use HTTPS.` });
82
100
  process.exit(1);
83
101
  }
84
- return maxSteps;
102
+ return value;
85
103
  }
86
104
  async function loadOutputSchema(opts) {
87
105
  if (opts.outputSchema !== undefined && opts.outputSchemaFile !== undefined) {
@@ -114,7 +132,18 @@ export function registerRun(agentCmd) {
114
132
  .option("--output-schema-file <path>", "Path to a JSON file containing the output schema")
115
133
  .option("--browser-profile <profile>", "Browser profile for execution (lite|stealth)")
116
134
  .option("--max-steps <n>", `Maximum tool-call steps before stopping (1-${MAX_STEPS_LIMIT})`)
135
+ .option("--max-duration-seconds <n>", "Maximum wall-clock seconds before stopping")
117
136
  .option("--mode <mode>", "Agent behavior mode (default|strict)")
137
+ .option("--cursor-style <style>", "Cursor style (fish|standard|green-pointer)")
138
+ .option("--capture-elements", "Capture page element metadata (gated capability; returns 403 if not enabled for your account)")
139
+ .option("--capture-snapshots", "Capture page snapshots")
140
+ .option("--capture-screenshots", "Capture screenshots")
141
+ .option("--no-capture-screenshots", "Disable screenshot capture")
142
+ .option("--capture-recording", "Capture a browser recording (gated capability; returns 403 if not enabled for your account)")
143
+ .option("--capture-html", "Capture HTML snapshots")
144
+ .option("--webhook-url <url>", "HTTPS webhook URL for run lifecycle events")
145
+ .option("--use-profile", "Use an attached browser profile")
146
+ .option("--profile-id <id>", "Browser Context Profile ID to start from (requires --use-profile)")
118
147
  .option("--session-id <uuid>", "Caller-provided UUID for idempotency / parallel-safe --sync")
119
148
  .option("--use-vault", "Consume vault credentials in this run")
120
149
  .option("--credential-item-id <id>", "Scope vault to a specific credential item ID (repeat for multiple; get IDs from `vault item list`; requires --use-vault)", (v, acc) => [...acc, v], [])
@@ -161,18 +190,58 @@ export function registerRun(agentCmd) {
161
190
  err({ error: "--credential-item-id requires --use-vault" });
162
191
  process.exit(1);
163
192
  }
193
+ // The server rejects profile_id without use_profile (400); fail fast here.
194
+ if (opts.profileId !== undefined && !opts.useProfile) {
195
+ err({ error: "--profile-id requires --use-profile" });
196
+ process.exit(1);
197
+ }
164
198
  const outputSchema = await loadOutputSchema(opts);
165
199
  const browserProfile = parseChoice(opts.browserProfile, VALID_BROWSER_PROFILES, "--browser-profile");
166
200
  const mode = parseChoice(opts.mode, VALID_MODES, "--mode");
167
- const maxSteps = parseMaxSteps(opts.maxSteps);
201
+ const cursorStyle = parseChoice(opts.cursorStyle, VALID_CURSOR_STYLES, "--cursor-style");
202
+ const maxSteps = parsePositiveInteger(opts.maxSteps, "--max-steps", MAX_STEPS_LIMIT);
203
+ const maxDurationSeconds = parsePositiveInteger(opts.maxDurationSeconds, "--max-duration-seconds");
168
204
  const sessionId = parseSessionId(opts.sessionId);
205
+ const webhookUrl = parseHttpsUrl(opts.webhookUrl, "--webhook-url");
206
+ const agentConfig = mode !== undefined ||
207
+ cursorStyle !== undefined ||
208
+ maxSteps !== undefined ||
209
+ maxDurationSeconds !== undefined
210
+ ? {
211
+ ...(mode !== undefined ? { mode } : {}),
212
+ ...(cursorStyle !== undefined ? { cursor_style: cursorStyle } : {}),
213
+ ...(maxSteps !== undefined ? { max_steps: maxSteps } : {}),
214
+ ...(maxDurationSeconds !== undefined
215
+ ? { max_duration_seconds: maxDurationSeconds }
216
+ : {}),
217
+ }
218
+ : undefined;
219
+ const captureConfig = opts.captureElements !== undefined ||
220
+ opts.captureSnapshots !== undefined ||
221
+ opts.captureScreenshots !== undefined ||
222
+ opts.captureRecording !== undefined ||
223
+ opts.captureHtml !== undefined
224
+ ? {
225
+ ...(opts.captureElements !== undefined ? { elements: opts.captureElements } : {}),
226
+ ...(opts.captureSnapshots !== undefined ? { snapshots: opts.captureSnapshots } : {}),
227
+ ...(opts.captureScreenshots !== undefined
228
+ ? { screenshots: opts.captureScreenshots }
229
+ : {}),
230
+ ...(opts.captureRecording !== undefined ? { recording: opts.captureRecording } : {}),
231
+ ...(opts.captureHtml !== undefined ? { html: opts.captureHtml } : {}),
232
+ }
233
+ : undefined;
169
234
  const apiKey = getApiKey();
170
235
  const req = {
171
236
  goal,
172
237
  url: normalizedUrl,
173
238
  ...(outputSchema !== undefined ? { output_schema: outputSchema } : {}),
174
239
  ...(browserProfile !== undefined ? { browser_profile: browserProfile } : {}),
175
- ...(mode !== undefined || maxSteps !== undefined ? { agent_config: { mode, max_steps: maxSteps } } : {}),
240
+ ...(agentConfig !== undefined ? { agent_config: agentConfig } : {}),
241
+ ...(captureConfig !== undefined ? { capture_config: captureConfig } : {}),
242
+ ...(webhookUrl !== undefined ? { webhook_url: webhookUrl } : {}),
243
+ ...(opts.useProfile !== undefined ? { use_profile: opts.useProfile } : {}),
244
+ ...(opts.profileId !== undefined ? { profile_id: opts.profileId } : {}),
176
245
  ...(sessionId !== undefined ? { session_id: sessionId } : {}),
177
246
  ...(opts.useVault ? { use_vault: true } : {}),
178
247
  ...(hasCredentialItemIds ? { credential_item_ids: opts.credentialItemId } : {}),
@@ -54,6 +54,9 @@ class TinyFishCliClient extends TinyFish {
54
54
  headers["X-TF-Client-Version"] = clientVersion;
55
55
  // Agent-vs-human signal, independent of harness detection or skill install.
56
56
  headers["X-TF-Human-Initiated"] = detectHumanInitiated() ? "true" : "false";
57
+ if (process.env["TF_SETUP_VERIFICATION"] === "1") {
58
+ headers["X-TF-Setup-Verification"] = "true";
59
+ }
57
60
  // Reuse the already-resolved client_name so connect_client can never diverge from it
58
61
  // (same sanitize + precedence, not a second raw read of TF_CLIENT_NAME).
59
62
  const connectCaller = clientName || null;
@@ -3,6 +3,8 @@ import spawn from "cross-spawn";
3
3
  import { connectClaudeCode, connectCodex, connectCursor, connectHermes, connectOpenClaw, connectOpencode, } from "../commands/connect.js";
4
4
  import { validatedApiKey } from "./auth.js";
5
5
  import { NATIVE_MCP_CLIENTS } from "./connect-clients.js";
6
+ import { ensureCliAuthenticated, installTinyFishCli, TINYFISH_CLI_NOT_FOUND_MESSAGE, } from "./connect-install.js";
7
+ import { commandNotFound, ConnectInterruptedError, ConnectStepError, throwIfInterrupted, } from "./connect-runtime.js";
6
8
  import { detectInstalledHarnesses, ALL_HARNESSES, } from "./harness-detect.js";
7
9
  import { detectHumanInitiated } from "./harness.js";
8
10
  import { emitNotice } from "./notice.js";
@@ -234,6 +236,72 @@ function computeExitCode(results) {
234
236
  return 0;
235
237
  return attempted.some((r) => r.outcome === "installed") ? 0 : 1;
236
238
  }
239
+ const SEARCH_RETRY = 'tinyfish search query --pretty -- "<your query>"';
240
+ function searchRetry(query) {
241
+ if (process.platform === "win32")
242
+ return SEARCH_RETRY;
243
+ return `tinyfish search query --pretty -- '${query.replaceAll("'", "'\\''")}'`;
244
+ }
245
+ async function runCliFallback(opts, prompt) {
246
+ try {
247
+ installTinyFishCli({ verbose: true });
248
+ }
249
+ catch (error) {
250
+ if (error instanceof ConnectInterruptedError)
251
+ throw error;
252
+ errLine(error instanceof Error ? error.message : String(error));
253
+ outLine("Retry: npx @tiny-fish/cli connect --all");
254
+ return 1;
255
+ }
256
+ const apiKey = opts.apiKey ?? process.env["TINYFISH_API_KEY"];
257
+ if (!prompt && !apiKey) {
258
+ outLine("Retry in a terminal: npx @tiny-fish/cli connect --all");
259
+ return 1;
260
+ }
261
+ try {
262
+ ensureCliAuthenticated("cli", apiKey);
263
+ }
264
+ catch (error) {
265
+ if (error instanceof ConnectInterruptedError)
266
+ throw error;
267
+ errLine(error instanceof Error ? error.message : String(error));
268
+ outLine(error instanceof ConnectStepError && error.failureReason === "command_not_found"
269
+ ? "Retry in a new terminal: tinyfish auth login"
270
+ : "Retry: tinyfish auth login");
271
+ return 1;
272
+ }
273
+ if (!prompt) {
274
+ outLine(`Retry in a terminal: ${SEARCH_RETRY}`);
275
+ return 1;
276
+ }
277
+ let query;
278
+ try {
279
+ query = (await prompt("What would you like to search? ")).trim();
280
+ }
281
+ catch {
282
+ outLine(`Retry: ${SEARCH_RETRY}`);
283
+ return 1;
284
+ }
285
+ if (!query) {
286
+ outLine(`Retry: ${SEARCH_RETRY}`);
287
+ return 1;
288
+ }
289
+ const search = spawn.sync("tinyfish", ["search", "query", "--pretty", "--", query], {
290
+ stdio: "inherit",
291
+ env: { ...process.env, TF_SETUP_VERIFICATION: "1" },
292
+ });
293
+ if (commandNotFound(search.error)) {
294
+ errLine(TINYFISH_CLI_NOT_FOUND_MESSAGE);
295
+ outLine(`Retry in a new terminal: ${searchRetry(query)}`);
296
+ return 1;
297
+ }
298
+ throwIfInterrupted(search);
299
+ if (search.error || search.status !== 0) {
300
+ outLine(`Retry: ${searchRetry(query)}`);
301
+ return 1;
302
+ }
303
+ return 0;
304
+ }
237
305
  export async function runConnectAll(opts) {
238
306
  const isTTY = detectHumanInitiated();
239
307
  let stdinClosed = false;
@@ -300,6 +368,18 @@ export async function runConnectAll(opts) {
300
368
  for (const result of results)
301
369
  outLine(summaryLine(result, opts.uninstall));
302
370
  if (results.every((r) => !r.detected)) {
371
+ if (!opts.dryRun && !opts.uninstall) {
372
+ try {
373
+ return await runCliFallback(opts, prompt);
374
+ }
375
+ catch (error) {
376
+ if (!(error instanceof ConnectInterruptedError))
377
+ throw error;
378
+ errLine("Setup interrupted — run the command again to finish.");
379
+ process.exitCode = 130;
380
+ return 130;
381
+ }
382
+ }
303
383
  outLine("");
304
384
  outLine("No supported harness found. Install one of: " + ALL_HARNESSES.join(", "));
305
385
  if (!opts.uninstall)
@@ -20,4 +20,4 @@ export declare function installWebSkill(client: NativeMcpClient): void;
20
20
  * Returns whether a skill was actually rewritten.
21
21
  */
22
22
  export declare function updateWebSkill({ verbose }: InstallOptions): boolean;
23
- export declare function ensureCliAuthenticated(source: AgentClient, apiKey?: string): void;
23
+ export declare function ensureCliAuthenticated(source: AgentClient | "cli", apiKey?: string): void;
@@ -4,8 +4,18 @@ export interface CliAgentRunParams extends AgentRunParams {
4
4
  output_schema?: OutputSchema;
5
5
  agent_config?: {
6
6
  mode?: "default" | "strict";
7
+ cursor_style?: "fish" | "standard" | "green-pointer";
7
8
  max_steps?: number;
9
+ max_duration_seconds?: number;
8
10
  };
11
+ capture_config?: {
12
+ elements?: boolean;
13
+ snapshots?: boolean;
14
+ screenshots?: boolean;
15
+ recording?: boolean;
16
+ html?: boolean;
17
+ };
18
+ webhook_url?: string;
9
19
  browser_profile?: BrowserProfile;
10
20
  session_id?: string;
11
21
  use_vault?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.23.1-next.201",
3
+ "version": "0.23.1-next.204",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,7 +32,7 @@
32
32
  "prepublishOnly": "npm run build"
33
33
  },
34
34
  "dependencies": {
35
- "@tiny-fish/sdk": "^0.4.0",
35
+ "@tiny-fish/sdk": "^0.5.0",
36
36
  "chrome-cookies-secure": "^3.0.2",
37
37
  "commander": "^12.0.0",
38
38
  "cross-spawn": "^7.0.6",