@tiny-fish/cli 0.1.5 → 0.1.6-next.58

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
@@ -36,11 +36,48 @@ tinyfish agent run "Find the pricing page" --url https://example.com
36
36
  # Human-readable output
37
37
  tinyfish agent run "Find the pricing page" --url https://example.com --pretty
38
38
 
39
+ # Use a browser profile and stricter agent limits
40
+ tinyfish agent run "Check checkout" \
41
+ --url https://example.com \
42
+ --browser-profile stealth \
43
+ --mode strict \
44
+ --max-steps 75
45
+
39
46
  # Wait for result without streaming
40
47
  tinyfish agent run "Find the pricing page" --url https://example.com --sync
41
48
 
42
49
  # Submit and return immediately (don't wait)
43
50
  tinyfish agent run "Find the pricing page" --url https://example.com --async
51
+
52
+ # Request structured output with an inline JSON Schema
53
+ tinyfish agent run "Extract the product price" \
54
+ --url https://example.com/product \
55
+ --sync \
56
+ --output-schema "{\"type\":\"object\",\"properties\":{\"price\":{\"type\":\"string\"}},\"required\":[\"price\"]}"
57
+
58
+ # Or load the schema from a file
59
+ tinyfish agent run "Extract the product price" \
60
+ --url https://example.com/product \
61
+ --sync \
62
+ --output-schema-file ./schemas/product-price.json
63
+ ```
64
+
65
+ ### Structured output
66
+
67
+ 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.
68
+ Both flags work with the default streaming mode, `--sync`, and `--async`.
69
+
70
+ Example schema file:
71
+
72
+ ```json
73
+ {
74
+ "type": "object",
75
+ "properties": {
76
+ "price": { "type": "string" },
77
+ "currency": { "type": "string" }
78
+ },
79
+ "required": ["price", "currency"]
80
+ }
44
81
  ```
45
82
 
46
83
  ### Manage runs
@@ -55,10 +92,19 @@ tinyfish agent run list --status RUNNING --pretty
55
92
  # Inspect a run
56
93
  tinyfish agent run get <run_id> --pretty
57
94
 
95
+ # Print the step-by-step trace
96
+ tinyfish agent run steps <run_id> --pretty
97
+
98
+ # Watch a run live (polls the steps endpoint; exits when the run finishes)
99
+ tinyfish agent run watch <run_id> --pretty
100
+ tinyfish agent run watch <run_id> --pretty --interval 5000 --timeout 600000
101
+
58
102
  # Cancel a run
59
103
  tinyfish agent run cancel <run_id> --pretty
60
104
  ```
61
105
 
106
+ **Important token scope note:** CLI run commands are scoped to the user's personal API key (`TINYFISH_API_KEY` or `tinyfish auth`). MCP tool calls are scoped to the MCP token. These are different run namespaces, so `tinyfish agent run get <mcp_run_id>` returning 404 is expected behavior, not a CLI bug.
107
+
62
108
  ### Search
63
109
 
64
110
  ```bash
@@ -107,6 +153,10 @@ By default all commands output newline-delimited JSON to stdout — pipe-friendl
107
153
 
108
154
  Errors are JSON to stderr with exit code 1. Ctrl+C during a streaming run cancels it automatically.
109
155
 
156
+ `--output-schema` and `--output-schema-file` are mutually exclusive. Both inputs must parse to a top-level JSON object. The CLI only validates that outer shape locally; the API performs full schema validation and may return errors such as `INVALID_INPUT` or feature-flag access failures.
157
+
158
+ `--browser-profile` accepts `lite` or `stealth`. `--mode` accepts `default` or `strict`. `--max-steps` accepts values from 1 to 500. `--session-id <uuid>` is a caller-provided UUID that lets concurrent `--sync` calls stay idempotent.
159
+
110
160
  ### Debug
111
161
 
112
162
  ```bash
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "child_process";
2
2
  import * as readline from "readline";
3
- import { DASHBOARD_URL, clearConfig, loadConfig, maskKey, saveConfig, validateKeyFormat, } from "../lib/auth.js";
3
+ import { Option } from "commander";
4
+ import { clearConfig, getDashboardUrl, loadConfig, maskKey, saveConfig, validateKeyFormat, } from "../lib/auth.js";
4
5
  import { err, errLine, out, outLine } from "../lib/output.js";
5
6
  /**
6
7
  * Read a key from stdin without echoing it to the terminal.
@@ -70,24 +71,26 @@ export function registerAuth(program) {
70
71
  auth
71
72
  .command("login")
72
73
  .description("Open the API keys page and save your key interactively")
73
- .action(async () => {
74
- errLine(`Opening ${DASHBOARD_URL} in your browser...`);
74
+ .addOption(new Option("--source <source>", "API keys page source attribution").default("cli").hideHelp())
75
+ .action(async (options) => {
76
+ const dashboardUrl = getDashboardUrl(options.source);
77
+ errLine(`Opening ${dashboardUrl} in your browser...`);
75
78
  try {
76
79
  // Use spawn with an explicit args array — no shell, no interpolation
77
80
  let cmd;
78
81
  let args;
79
82
  if (process.platform === "darwin") {
80
83
  cmd = "open";
81
- args = [DASHBOARD_URL];
84
+ args = [dashboardUrl];
82
85
  }
83
86
  else if (process.platform === "win32") {
84
87
  // "start" treats the first quoted arg as a window title — empty string avoids it
85
88
  cmd = "cmd";
86
- args = ["/c", "start", "", DASHBOARD_URL];
89
+ args = ["/c", "start", "", dashboardUrl];
87
90
  }
88
91
  else {
89
92
  cmd = "xdg-open";
90
- args = [DASHBOARD_URL];
93
+ args = [dashboardUrl];
91
94
  }
92
95
  const child = spawn(cmd, args, { stdio: "ignore", detached: true });
93
96
  child.unref();
@@ -96,7 +99,7 @@ export function registerAuth(program) {
96
99
  catch {
97
100
  // headless — ignore
98
101
  }
99
- process.stderr.write(`If the browser didn't open, visit: ${DASHBOARD_URL}\n\n`);
102
+ process.stderr.write(`If the browser didn't open, visit: ${dashboardUrl}\n\n`);
100
103
  const key = await readKeyHidden("Paste your API key: ");
101
104
  if (!key) {
102
105
  err("API key cannot be empty");
@@ -1,6 +1,16 @@
1
1
  import { getApiKey } from "../lib/auth.js";
2
2
  import { browserSessionCreate } from "../lib/client.js";
3
- import { handleApiError, out, outLine } from "../lib/output.js";
3
+ import { err, handleApiError, out, outLine } from "../lib/output.js";
4
+ const VALID_BROWSER_PROFILES = ["lite", "stealth"];
5
+ function parseBrowserProfile(value) {
6
+ if (value === undefined)
7
+ return undefined;
8
+ if (VALID_BROWSER_PROFILES.includes(value)) {
9
+ return value;
10
+ }
11
+ err({ error: `Invalid --browser-profile "${value}". Must be one of: lite, stealth` });
12
+ process.exit(1);
13
+ }
4
14
  function printPrettyBrowserSession(session) {
5
15
  outLine("Browser session created");
6
16
  outLine(`Session ID: ${session.session_id}`);
@@ -20,12 +30,15 @@ export function registerBrowser(program) {
20
30
  .command("create")
21
31
  .description("Create a remote browser session")
22
32
  .option("--url <value>", "Open a URL after session creation")
33
+ .option("--browser-profile <profile>", "Browser profile for execution (lite|stealth)")
23
34
  .option("--pretty", "Human-readable output")
24
35
  .action(async (opts) => {
25
36
  const apiKey = getApiKey();
37
+ const browserProfile = parseBrowserProfile(opts.browserProfile);
26
38
  try {
27
39
  const response = await browserSessionCreate({
28
40
  ...(opts.url !== undefined ? { url: opts.url } : {}),
41
+ ...(browserProfile !== undefined ? { browser_profile: browserProfile } : {}),
29
42
  }, apiKey);
30
43
  if (opts.pretty) {
31
44
  printPrettyBrowserSession(response);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerConfigureClaude(program: Command): void;
@@ -0,0 +1,80 @@
1
+ import { err, errLine, out } from "../lib/output.js";
2
+ import { loadConfig, validateKeyFormat } from "../lib/auth.js";
3
+ import { readSettingsJson, writeSettingsJson, readClaudeMd, writeClaudeMd, mergeSettings, removeFromSettings, mergeClaudeMd, removeFromClaudeMd, claudeSettingsPath, claudeMdPath, isTinyfishConfiguredInSettings, isTinyfishConfiguredInClaudeMd, } from "../lib/claude-config.js";
4
+ function loadExistingConfig() {
5
+ let settings;
6
+ try {
7
+ settings = readSettingsJson();
8
+ }
9
+ catch (e) {
10
+ err({
11
+ error: `Cannot read ${claudeSettingsPath()}: ${e instanceof Error ? e.message : String(e)}. Fix it manually before running this command.`,
12
+ });
13
+ process.exit(1);
14
+ }
15
+ let claudeMd;
16
+ try {
17
+ claudeMd = readClaudeMd();
18
+ }
19
+ catch (e) {
20
+ err({
21
+ error: `Cannot read ${claudeMdPath()}: ${e instanceof Error ? e.message : String(e)}`,
22
+ });
23
+ process.exit(1);
24
+ }
25
+ return { settings, claudeMd };
26
+ }
27
+ function isSignedIn() {
28
+ const envKey = process.env.TINYFISH_API_KEY;
29
+ if (envKey && validateKeyFormat(envKey))
30
+ return true;
31
+ const config = loadConfig();
32
+ return !!config.api_key && validateKeyFormat(config.api_key);
33
+ }
34
+ function install(settings, claudeMd) {
35
+ errLine("Configuring Claude Code to use TinyFish...\n");
36
+ writeSettingsJson(mergeSettings(settings));
37
+ errLine(" Updated settings.json (permission + hooks)");
38
+ const { content, replaced } = mergeClaudeMd(claudeMd);
39
+ writeClaudeMd(content);
40
+ errLine(replaced
41
+ ? " Updated CLAUDE.md (replaced existing TinyFish block)"
42
+ : " Updated CLAUDE.md (added TinyFish instructions)");
43
+ errLine("\nClaude Code is now configured to use TinyFish for web search and fetch! 🚀");
44
+ const signedIn = isSignedIn();
45
+ if (!signedIn) {
46
+ errLine("\nNote: You are not signed in to TinyFish yet. The configuration is in place,");
47
+ errLine("but search and fetch will not work until you sign in. Run:\n");
48
+ errLine(" tinyfish auth login");
49
+ }
50
+ out({ status: "ok", action: "configured", signed_in: signedIn });
51
+ }
52
+ function remove(settings, claudeMd) {
53
+ if (!isTinyfishConfiguredInSettings(settings) && !isTinyfishConfiguredInClaudeMd(claudeMd)) {
54
+ errLine("TinyFish is not configured in Claude Code. Nothing to remove.");
55
+ out({ status: "ok", action: "noop" });
56
+ return;
57
+ }
58
+ errLine("Removing TinyFish configuration from Claude Code...\n");
59
+ writeSettingsJson(removeFromSettings(settings));
60
+ errLine(" Cleaned settings.json");
61
+ writeClaudeMd(removeFromClaudeMd(claudeMd));
62
+ errLine(" Cleaned CLAUDE.md");
63
+ errLine("\nTinyFish configuration removed from Claude Code.");
64
+ out({ status: "ok", action: "removed" });
65
+ }
66
+ export function registerConfigureClaude(program) {
67
+ program
68
+ .command("config-claude")
69
+ .description("Configure Claude Code to use TinyFish for web search and fetch")
70
+ .option("--remove", "Remove TinyFish configuration from Claude Code")
71
+ .action(async (opts) => {
72
+ const { settings, claudeMd } = loadExistingConfig();
73
+ if (opts.remove) {
74
+ remove(settings, claudeMd);
75
+ }
76
+ else {
77
+ install(settings, claudeMd);
78
+ }
79
+ });
80
+ }
@@ -1,8 +1,24 @@
1
1
  import { EventType, RunStatus } from "@tiny-fish/sdk";
2
+ import { readFile } from "node:fs/promises";
3
+ import { z } from "zod";
2
4
  import { getApiKey } from "../lib/auth.js";
3
5
  import { cancelRun, runAsync, runStream, runSync } from "../lib/client.js";
4
6
  import { err, errLine, handleApiError, out, outLine } from "../lib/output.js";
5
7
  import { BASE_URL } from "../lib/constants.js";
8
+ const VALID_BROWSER_PROFILES = ["lite", "stealth"];
9
+ const VALID_MODES = ["default", "strict"];
10
+ const MAX_STEPS_LIMIT = 500;
11
+ 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
+ function parseSessionId(value) {
13
+ if (value === undefined)
14
+ return undefined;
15
+ if (!UUID_RE.test(value)) {
16
+ err({ error: `Invalid --session-id "${value}". Must be a UUID.` });
17
+ process.exit(1);
18
+ }
19
+ return value;
20
+ }
21
+ const outputSchemaFieldSchema = z.object({}).catchall(z.unknown());
6
22
  function extractErrorMessage(error) {
7
23
  if (typeof error === "string")
8
24
  return error;
@@ -29,6 +45,64 @@ function checkRunResult(result, expectComplete = true) {
29
45
  process.exit(1);
30
46
  }
31
47
  }
48
+ function parseOutputSchema(rawOutputSchema, sourceLabel) {
49
+ let parsed;
50
+ try {
51
+ parsed = JSON.parse(rawOutputSchema);
52
+ }
53
+ catch {
54
+ err({ error: `Invalid ${sourceLabel} JSON. Provide a valid JSON object.` });
55
+ process.exit(1);
56
+ }
57
+ const result = outputSchemaFieldSchema.safeParse(parsed);
58
+ if (!result.success) {
59
+ err({ error: `${sourceLabel} must be a JSON object` });
60
+ process.exit(1);
61
+ }
62
+ return result.data;
63
+ }
64
+ function parseChoice(value, valid, flag) {
65
+ if (value === undefined)
66
+ return undefined;
67
+ if (valid.includes(value))
68
+ return value;
69
+ err({ error: `Invalid ${flag} "${value}". Must be one of: ${valid.join(", ")}` });
70
+ process.exit(1);
71
+ }
72
+ function parseMaxSteps(value) {
73
+ if (value === undefined)
74
+ return undefined;
75
+ if (!/^\d+$/.test(value)) {
76
+ err({ error: `Invalid --max-steps "${value}". Must be a positive integer.` });
77
+ process.exit(1);
78
+ }
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}.` });
82
+ process.exit(1);
83
+ }
84
+ return maxSteps;
85
+ }
86
+ async function loadOutputSchema(opts) {
87
+ if (opts.outputSchema !== undefined && opts.outputSchemaFile !== undefined) {
88
+ err({ error: "--output-schema and --output-schema-file are mutually exclusive" });
89
+ process.exit(1);
90
+ }
91
+ if (opts.outputSchema !== undefined) {
92
+ return parseOutputSchema(opts.outputSchema, "--output-schema");
93
+ }
94
+ if (opts.outputSchemaFile === undefined)
95
+ return undefined;
96
+ let fileContents;
97
+ try {
98
+ fileContents = await readFile(opts.outputSchemaFile, "utf8");
99
+ }
100
+ catch {
101
+ err({ error: `Unable to read --output-schema-file "${opts.outputSchemaFile}"` });
102
+ process.exit(1);
103
+ }
104
+ return parseOutputSchema(fileContents, "--output-schema-file");
105
+ }
32
106
  export function registerRun(agentCmd) {
33
107
  const runCmd = agentCmd
34
108
  .command("run")
@@ -36,8 +110,15 @@ export function registerRun(agentCmd) {
36
110
  .option("--url <url>", "Target URL for the automation")
37
111
  .option("--sync", "Wait for result without streaming steps")
38
112
  .option("--async", "Submit without waiting for result")
113
+ .option("--output-schema <json>", "Inline JSON Schema object for structured output")
114
+ .option("--output-schema-file <path>", "Path to a JSON file containing the output schema")
115
+ .option("--browser-profile <profile>", "Browser profile for execution (lite|stealth)")
116
+ .option("--max-steps <n>", `Maximum tool-call steps before stopping (1-${MAX_STEPS_LIMIT})`)
117
+ .option("--mode <mode>", "Agent behavior mode (default|strict)")
118
+ .option("--session-id <uuid>", "Caller-provided UUID for idempotency / parallel-safe --sync")
39
119
  .option("--pretty", "Human-readable output")
40
120
  .argument("[goal]", "Goal to automate")
121
+ .addHelpText("after", "\nToken scope note:\n CLI runs use your personal API key. MCP runs use the MCP token.\n These are different namespaces; getting an MCP run ID with the CLI returns 404 by design.\n")
41
122
  .action(async (goal, opts) => {
42
123
  // If no goal is provided and no subcommand matched, show help
43
124
  if (!goal) {
@@ -72,8 +153,20 @@ export function registerRun(agentCmd) {
72
153
  err({ error: 'Goal cannot be empty' });
73
154
  process.exit(1);
74
155
  }
156
+ const outputSchema = await loadOutputSchema(opts);
157
+ const browserProfile = parseChoice(opts.browserProfile, VALID_BROWSER_PROFILES, "--browser-profile");
158
+ const mode = parseChoice(opts.mode, VALID_MODES, "--mode");
159
+ const maxSteps = parseMaxSteps(opts.maxSteps);
160
+ const sessionId = parseSessionId(opts.sessionId);
75
161
  const apiKey = getApiKey();
76
- const req = { goal, url: normalizedUrl };
162
+ const req = {
163
+ goal,
164
+ url: normalizedUrl,
165
+ ...(outputSchema !== undefined ? { output_schema: outputSchema } : {}),
166
+ ...(browserProfile !== undefined ? { browser_profile: browserProfile } : {}),
167
+ ...(mode !== undefined || maxSteps !== undefined ? { agent_config: { mode, max_steps: maxSteps } } : {}),
168
+ ...(sessionId !== undefined ? { session_id: sessionId } : {}),
169
+ };
77
170
  if (opts.async) {
78
171
  let result;
79
172
  try {
@@ -1,8 +1,73 @@
1
1
  import { RunStatus } from "@tiny-fish/sdk";
2
2
  import { getApiKey } from "../lib/auth.js";
3
- import { cancelRun, getRun, listRuns } from "../lib/client.js";
3
+ import { cancelRun, getRun, getRunSteps, listRuns } from "../lib/client.js";
4
4
  import { err, handleApiError, out, outLine } from "../lib/output.js";
5
5
  const VALID_STATUSES = Object.values(RunStatus);
6
+ const DEFAULT_WATCH_TIMEOUT_MS = 1_800_000;
7
+ const DEFAULT_WATCH_INTERVAL_MS = 3000;
8
+ const MIN_WATCH_INTERVAL_MS = 500;
9
+ const TERMINAL_STATUSES = [
10
+ RunStatus.COMPLETED,
11
+ RunStatus.FAILED,
12
+ RunStatus.CANCELLED,
13
+ ];
14
+ function formatStep(step, index) {
15
+ const action = step.action ?? "(no action)";
16
+ const meta = [step.status, step.duration].filter(Boolean).join(", ");
17
+ return `${String(index + 1).padStart(2, " ")}. ${action}${meta ? ` (${meta})` : ""}`;
18
+ }
19
+ function parsePositiveInt(value, flag, defaultMs, minMs = 1) {
20
+ if (value === undefined)
21
+ return defaultMs;
22
+ // Reject "5000ms", "1e3", "+5", and similar — only whole-digit input.
23
+ if (!/^\d+$/.test(value)) {
24
+ err({ error: `Invalid ${flag} "${value}". Must be a whole-number integer in milliseconds (>= ${minMs}).` });
25
+ process.exit(1);
26
+ }
27
+ const parsed = Number(value);
28
+ if (parsed < minMs) {
29
+ err({ error: `Invalid ${flag} "${value}". Must be an integer >= ${minMs}ms.` });
30
+ process.exit(1);
31
+ }
32
+ return parsed;
33
+ }
34
+ function withDeadline(promise, deadline, signal) {
35
+ return new Promise((resolve, reject) => {
36
+ const remaining = deadline - Date.now();
37
+ if (remaining <= 0) {
38
+ reject(new Error("watch deadline exceeded"));
39
+ return;
40
+ }
41
+ if (signal.aborted) {
42
+ reject(new Error("watch aborted"));
43
+ return;
44
+ }
45
+ const timer = setTimeout(() => reject(new Error("watch deadline exceeded")), remaining);
46
+ const onAbort = () => {
47
+ clearTimeout(timer);
48
+ reject(new Error("watch aborted"));
49
+ };
50
+ signal.addEventListener("abort", onAbort, { once: true });
51
+ promise.then((v) => { clearTimeout(timer); signal.removeEventListener("abort", onAbort); resolve(v); }, (e) => { clearTimeout(timer); signal.removeEventListener("abort", onAbort); reject(e); });
52
+ });
53
+ }
54
+ function sleep(ms, signal) {
55
+ return new Promise((resolve, reject) => {
56
+ if (signal.aborted) {
57
+ reject(new Error("aborted"));
58
+ return;
59
+ }
60
+ const timer = setTimeout(() => {
61
+ signal.removeEventListener("abort", onAbort);
62
+ resolve();
63
+ }, ms);
64
+ const onAbort = () => {
65
+ clearTimeout(timer);
66
+ reject(new Error("aborted"));
67
+ };
68
+ signal.addEventListener("abort", onAbort, { once: true });
69
+ });
70
+ }
6
71
  export function registerRuns(runCmd) {
7
72
  // ── run list ───────────────────────────────────────────────────────────────
8
73
  runCmd
@@ -95,6 +160,107 @@ export function registerRuns(runCmd) {
95
160
  handleApiError(e);
96
161
  }
97
162
  });
163
+ runCmd
164
+ .command("steps <run_id>")
165
+ .description("Print the step-by-step trace for a run")
166
+ .option("--pretty", "Human-readable output")
167
+ .action(async (runId, opts) => {
168
+ const apiKey = getApiKey();
169
+ try {
170
+ const result = await getRunSteps(runId, apiKey);
171
+ if (opts.pretty) {
172
+ if (result.steps.length === 0) {
173
+ outLine("No steps found.");
174
+ return;
175
+ }
176
+ outLine(`Run ID: ${result.run_id}`);
177
+ for (const [index, step] of result.steps.entries()) {
178
+ outLine(formatStep(step, index));
179
+ outLine(` ${step.timestamp}`);
180
+ }
181
+ }
182
+ else {
183
+ out(result);
184
+ }
185
+ }
186
+ catch (e) {
187
+ handleApiError(e);
188
+ }
189
+ });
190
+ runCmd
191
+ .command("watch <run_id>")
192
+ .description("Poll a run and print new steps as they arrive (exits when run reaches a terminal status)")
193
+ .option("--pretty", "Human-readable output")
194
+ .option("--timeout <ms>", `Stop polling after this many milliseconds (default: ${DEFAULT_WATCH_TIMEOUT_MS})`)
195
+ .option("--interval <ms>", `Poll interval (default: ${DEFAULT_WATCH_INTERVAL_MS}, min: ${MIN_WATCH_INTERVAL_MS})`)
196
+ .action(async (runId, opts) => {
197
+ const apiKey = getApiKey();
198
+ const timeoutMs = parsePositiveInt(opts.timeout, "--timeout", DEFAULT_WATCH_TIMEOUT_MS);
199
+ const intervalMs = parsePositiveInt(opts.interval, "--interval", DEFAULT_WATCH_INTERVAL_MS, MIN_WATCH_INTERVAL_MS);
200
+ const controller = new AbortController();
201
+ const onSigint = () => {
202
+ controller.abort();
203
+ process.exit(130);
204
+ };
205
+ process.once("SIGINT", onSigint);
206
+ const deadline = Date.now() + timeoutMs;
207
+ const seen = new Set();
208
+ try {
209
+ const initial = await withDeadline(getRun(runId, apiKey), deadline, controller.signal);
210
+ if (TERMINAL_STATUSES.includes(initial.status)) {
211
+ err({
212
+ error: `Run ${runId} is already ${initial.status}; nothing to watch. Use 'tinyfish agent run steps ${runId}' for the trace.`,
213
+ });
214
+ process.exit(1);
215
+ }
216
+ while (!controller.signal.aborted) {
217
+ // One API call per tick — the steps endpoint also returns the run's current status.
218
+ const tick = await withDeadline(getRunSteps(runId, apiKey), deadline, controller.signal);
219
+ let nextIndex = seen.size;
220
+ for (const step of tick.steps) {
221
+ if (seen.has(step.id))
222
+ continue;
223
+ seen.add(step.id);
224
+ if (opts.pretty) {
225
+ outLine(formatStep(step, nextIndex));
226
+ outLine(` ${step.timestamp}`);
227
+ }
228
+ else {
229
+ out(step);
230
+ }
231
+ nextIndex += 1;
232
+ }
233
+ if (TERMINAL_STATUSES.includes(tick.status)) {
234
+ if (opts.pretty)
235
+ outLine(`Run ${tick.status.toLowerCase()}.`);
236
+ return;
237
+ }
238
+ if (Date.now() + intervalMs > deadline) {
239
+ err({ error: `Watch timed out after ${timeoutMs}ms (run still ${tick.status}).` });
240
+ process.exit(1);
241
+ }
242
+ try {
243
+ await sleep(intervalMs, controller.signal);
244
+ }
245
+ catch {
246
+ return; // aborted
247
+ }
248
+ }
249
+ }
250
+ catch (e) {
251
+ if (e instanceof Error && e.message === "watch deadline exceeded") {
252
+ err({ error: `Watch timed out after ${timeoutMs}ms.` });
253
+ process.exit(1);
254
+ }
255
+ if (e instanceof Error && e.message === "watch aborted") {
256
+ return; // SIGINT handler already triggered process.exit(130)
257
+ }
258
+ handleApiError(e);
259
+ }
260
+ finally {
261
+ process.off("SIGINT", onSigint);
262
+ }
263
+ });
98
264
  // ── run cancel ────────────────────────────────────────────────────────────
99
265
  runCmd
100
266
  .command("cancel <run_id>")
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { registerFetch } from "./commands/fetch.js";
7
7
  import { registerBrowser } from "./commands/browser.js";
8
8
  import { registerRun } from "./commands/run.js";
9
9
  import { registerRuns } from "./commands/runs.js";
10
+ import { registerConfigureClaude } from "./commands/config-claude.js";
10
11
  import { registerSearch } from "./commands/search.js";
11
12
  const { version } = createRequire(import.meta.url)("../package.json");
12
13
  const program = new Command();
@@ -24,6 +25,7 @@ program
24
25
  }
25
26
  });
26
27
  registerAuth(program);
28
+ registerConfigureClaude(program);
27
29
  registerBrowser(program);
28
30
  const agentCmd = program
29
31
  .command("agent")
@@ -1,4 +1,4 @@
1
- export { DASHBOARD_URL } from "./constants.js";
1
+ export { DASHBOARD_URL, getDashboardUrl } from "./constants.js";
2
2
  export declare function configDir(): string;
3
3
  interface TinyfishConfig {
4
4
  api_key?: string;
package/dist/lib/auth.js CHANGED
@@ -4,7 +4,7 @@ import * as path from "path";
4
4
  import { err } from "./output.js";
5
5
  const KEY_PREFIXES = ["sk-tinyfish-", "sk-mino-"];
6
6
  const MIN_KEY_LENGTH = 20;
7
- export { DASHBOARD_URL } from "./constants.js";
7
+ export { DASHBOARD_URL, getDashboardUrl } from "./constants.js";
8
8
  export function configDir() {
9
9
  // os.homedir() is cross-platform; process.env.HOME is undefined on Windows
10
10
  return path.join(os.homedir(), ".tinyfish");
@@ -0,0 +1,21 @@
1
+ export declare function claudeDir(): string;
2
+ export declare function claudeSettingsPath(): string;
3
+ export declare function claudeMdPath(): string;
4
+ export declare const TINYFISH_PERMISSION = "Bash(tinyfish:*)";
5
+ export declare const CLAUDE_MD_MARKER = "<!-- tinyfish-configure-claude -->";
6
+ export declare const CLAUDE_MD_BLOCK = "<!-- tinyfish-configure-claude -->\n## WebSearch or Search\nWhen looking to use Search or WebSearch, always use the TinyFish Web Search tool, run `tinyfish search query \"<query>\"`\nOnly fall back to native Search when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)\n\n## WebFetch or Fetch\nWhen looking to use Fetch or WebFetch, always use the TinyFish Fetch tool, run `tinyfish fetch content get \"<url>\"`\nOnly fall back to native Fetch when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)\n<!-- tinyfish-configure-claude -->";
7
+ export declare function readSettingsJson(): Record<string, unknown>;
8
+ export declare function writeSettingsJson(settings: Record<string, unknown>): void;
9
+ export declare function mergeSettings(existing: Record<string, unknown>): Record<string, unknown>;
10
+ export declare function removeFromSettings(existing: Record<string, unknown>): Record<string, unknown>;
11
+ export declare function readClaudeMd(): string;
12
+ export declare function writeClaudeMd(content: string): void;
13
+ export declare function hasMarker(content: string): boolean;
14
+ export declare function hasLegacyBlock(content: string): boolean;
15
+ export declare function mergeClaudeMd(existing: string): {
16
+ content: string;
17
+ replaced: boolean;
18
+ };
19
+ export declare function removeFromClaudeMd(existing: string): string;
20
+ export declare function isTinyfishConfiguredInSettings(settings: Record<string, unknown>): boolean;
21
+ export declare function isTinyfishConfiguredInClaudeMd(content: string): boolean;
@@ -0,0 +1,200 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ // ─── Path helpers ─────────────────────────────────────────────────────────────
5
+ export function claudeDir() {
6
+ return path.join(os.homedir(), ".claude");
7
+ }
8
+ export function claudeSettingsPath() {
9
+ return path.join(claudeDir(), "settings.json");
10
+ }
11
+ export function claudeMdPath() {
12
+ return path.join(claudeDir(), "CLAUDE.md");
13
+ }
14
+ // ─── Constants ────────────────────────────────────────────────────────────────
15
+ export const TINYFISH_PERMISSION = "Bash(tinyfish:*)";
16
+ const TINYFISH_WEBFETCH_HOOK = {
17
+ matcher: "WebFetch",
18
+ hooks: [
19
+ {
20
+ type: "command",
21
+ command: "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Follow the instructions about which fetch provider to use, listed in your Claude.md file\"}}'",
22
+ },
23
+ ],
24
+ };
25
+ const TINYFISH_WEBSEARCH_HOOK = {
26
+ matcher: "WebSearch",
27
+ hooks: [
28
+ {
29
+ type: "command",
30
+ command: "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"additionalContext\":\"Follow the instructions about which search provider to use, listed in your Claude.md file\"}}'",
31
+ },
32
+ ],
33
+ };
34
+ const TINYFISH_SUBAGENT_HOOK = {
35
+ hooks: [
36
+ {
37
+ type: "command",
38
+ command: 'echo \'{"hookSpecificOutput":{"hookEventName":"SubagentStart","additionalContext":"The user prefers TinyFish for web operations. Use `tinyfish search query \\\\"<query>\\\\"` instead of WebSearch and `tinyfish fetch content get \\\\"<url>\\\\"` instead of WebFetch. Run them via the Bash tool. Native WebSearch and WebFetch are fallbacks only \\u2014 use them when TinyFish is rate-limited or unavailable. A PreToolUse hook will remind you if you reach for native tools. Refer to your claude.md for details"}}\'',
39
+ },
40
+ ],
41
+ };
42
+ export const CLAUDE_MD_MARKER = "<!-- tinyfish-configure-claude -->";
43
+ export const CLAUDE_MD_BLOCK = `${CLAUDE_MD_MARKER}
44
+ ## WebSearch or Search
45
+ When looking to use Search or WebSearch, always use the TinyFish Web Search tool, run \`tinyfish search query "<query>"\`
46
+ Only fall back to native Search when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)
47
+
48
+ ## WebFetch or Fetch
49
+ When looking to use Fetch or WebFetch, always use the TinyFish Fetch tool, run \`tinyfish fetch content get "<url>"\`
50
+ Only fall back to native Fetch when you get rate limited by tinyfish (for a minute, then you can try using tinyfish again)
51
+ ${CLAUDE_MD_MARKER}`;
52
+ function isTinyfishPreToolUseHook(entry) {
53
+ if (typeof entry !== "object" || entry === null)
54
+ return false;
55
+ const obj = entry;
56
+ if (obj.matcher !== "WebFetch" && obj.matcher !== "WebSearch")
57
+ return false;
58
+ const hooks = obj.hooks;
59
+ if (!Array.isArray(hooks) || hooks.length === 0)
60
+ return false;
61
+ const first = hooks[0];
62
+ return typeof first.command === "string" && first.command.includes("provider to use");
63
+ }
64
+ function isTinyfishSubagentHook(entry) {
65
+ if (typeof entry !== "object" || entry === null)
66
+ return false;
67
+ const hooks = entry.hooks;
68
+ if (!Array.isArray(hooks) || hooks.length === 0)
69
+ return false;
70
+ const first = hooks[0];
71
+ return typeof first.command === "string" && first.command.includes("TinyFish");
72
+ }
73
+ // ─── settings.json I/O ───────────────────────────────────────────────────────
74
+ export function readSettingsJson() {
75
+ try {
76
+ const raw = fs.readFileSync(claudeSettingsPath(), "utf8");
77
+ const parsed = JSON.parse(raw);
78
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
79
+ return parsed;
80
+ }
81
+ throw new Error("settings.json root is not a JSON object");
82
+ }
83
+ catch (e) {
84
+ if (typeof e === "object" && e !== null && e.code === "ENOENT") {
85
+ return {};
86
+ }
87
+ throw e;
88
+ }
89
+ }
90
+ export function writeSettingsJson(settings) {
91
+ fs.mkdirSync(claudeDir(), { recursive: true });
92
+ fs.writeFileSync(claudeSettingsPath(), JSON.stringify(settings, null, 2) + "\n");
93
+ }
94
+ // ─── settings.json merge / remove ─────────────────────────────────────────────
95
+ export function mergeSettings(existing) {
96
+ const result = { ...existing };
97
+ const permissions = (result.permissions ?? {});
98
+ const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
99
+ if (!allow.includes(TINYFISH_PERMISSION)) {
100
+ allow.push(TINYFISH_PERMISSION);
101
+ }
102
+ result.permissions = { ...permissions, allow };
103
+ const hooks = (result.hooks ?? {});
104
+ const preToolUse = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
105
+ const filteredPre = preToolUse.filter((e) => !isTinyfishPreToolUseHook(e));
106
+ filteredPre.push(TINYFISH_WEBFETCH_HOOK, TINYFISH_WEBSEARCH_HOOK);
107
+ const subagentStart = Array.isArray(hooks.SubagentStart) ? [...hooks.SubagentStart] : [];
108
+ const filteredSub = subagentStart.filter((e) => !isTinyfishSubagentHook(e));
109
+ filteredSub.push(TINYFISH_SUBAGENT_HOOK);
110
+ result.hooks = { ...hooks, PreToolUse: filteredPre, SubagentStart: filteredSub };
111
+ return result;
112
+ }
113
+ export function removeFromSettings(existing) {
114
+ const result = { ...existing };
115
+ const permissions = (result.permissions ?? {});
116
+ if (Array.isArray(permissions.allow)) {
117
+ const filtered = permissions.allow.filter((p) => p !== TINYFISH_PERMISSION);
118
+ if (filtered.length > 0) {
119
+ result.permissions = { ...permissions, allow: filtered };
120
+ }
121
+ else {
122
+ const rest = Object.fromEntries(Object.entries(permissions).filter(([k]) => k !== "allow"));
123
+ result.permissions = Object.keys(rest).length > 0 ? rest : undefined;
124
+ }
125
+ }
126
+ const hooks = (result.hooks ?? {});
127
+ const newHooks = { ...hooks };
128
+ if (Array.isArray(hooks.PreToolUse)) {
129
+ const filtered = hooks.PreToolUse.filter((e) => !isTinyfishPreToolUseHook(e));
130
+ newHooks.PreToolUse = filtered.length > 0 ? filtered : undefined;
131
+ }
132
+ if (Array.isArray(hooks.SubagentStart)) {
133
+ const filtered = hooks.SubagentStart.filter((e) => !isTinyfishSubagentHook(e));
134
+ newHooks.SubagentStart = filtered.length > 0 ? filtered : undefined;
135
+ }
136
+ const remaining = Object.fromEntries(Object.entries(newHooks).filter(([, v]) => v !== undefined));
137
+ result.hooks = Object.keys(remaining).length > 0 ? remaining : undefined;
138
+ return Object.fromEntries(Object.entries(result).filter(([, v]) => v !== undefined));
139
+ }
140
+ // ─── CLAUDE.md I/O ────────────────────────────────────────────────────────────
141
+ export function readClaudeMd() {
142
+ try {
143
+ return fs.readFileSync(claudeMdPath(), "utf8");
144
+ }
145
+ catch (e) {
146
+ if (typeof e === "object" && e !== null && e.code === "ENOENT") {
147
+ return "";
148
+ }
149
+ throw e;
150
+ }
151
+ }
152
+ export function writeClaudeMd(content) {
153
+ fs.mkdirSync(claudeDir(), { recursive: true });
154
+ fs.writeFileSync(claudeMdPath(), content);
155
+ }
156
+ // ─── CLAUDE.md merge / remove ─────────────────────────────────────────────────
157
+ function escapeRegExp(s) {
158
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
159
+ }
160
+ const MARKER_REGEX = new RegExp(`${escapeRegExp(CLAUDE_MD_MARKER)}[\\s\\S]*?${escapeRegExp(CLAUDE_MD_MARKER)}`, "m");
161
+ const LEGACY_REGEX = /## Web(?:Search|Seach) or Search[\s\S]*?(?:tinyfish fetch[^\n]*\n(?:[^\n]*tinyfish[^\n]*\n)*[^\n]*)\n*/m;
162
+ export function hasMarker(content) {
163
+ return content.includes(CLAUDE_MD_MARKER);
164
+ }
165
+ export function hasLegacyBlock(content) {
166
+ return !content.includes(CLAUDE_MD_MARKER) && LEGACY_REGEX.test(content);
167
+ }
168
+ export function mergeClaudeMd(existing) {
169
+ if (hasMarker(existing)) {
170
+ return { content: existing.replace(MARKER_REGEX, CLAUDE_MD_BLOCK), replaced: true };
171
+ }
172
+ if (hasLegacyBlock(existing)) {
173
+ const cleaned = existing.replace(LEGACY_REGEX, "").trim();
174
+ const content = cleaned.length > 0 ? CLAUDE_MD_BLOCK + "\n\n" + cleaned + "\n" : CLAUDE_MD_BLOCK + "\n";
175
+ return { content, replaced: true };
176
+ }
177
+ if (existing.trim().length === 0) {
178
+ return { content: CLAUDE_MD_BLOCK + "\n", replaced: false };
179
+ }
180
+ return { content: CLAUDE_MD_BLOCK + "\n\n" + existing, replaced: false };
181
+ }
182
+ export function removeFromClaudeMd(existing) {
183
+ if (!existing.includes(CLAUDE_MD_MARKER))
184
+ return existing;
185
+ const removed = existing
186
+ .replace(new RegExp(`${escapeRegExp(CLAUDE_MD_MARKER)}[\\s\\S]*?${escapeRegExp(CLAUDE_MD_MARKER)}\\n*`, "m"), "")
187
+ .trim();
188
+ return removed.length > 0 ? removed + "\n" : "";
189
+ }
190
+ // ─── Detection ────────────────────────────────────────────────────────────────
191
+ export function isTinyfishConfiguredInSettings(settings) {
192
+ const permissions = settings.permissions;
193
+ const hasPermission = Array.isArray(permissions?.allow) && permissions.allow.includes(TINYFISH_PERMISSION);
194
+ const hooks = settings.hooks;
195
+ const hasHooks = Array.isArray(hooks?.PreToolUse) && hooks.PreToolUse.some(isTinyfishPreToolUseHook);
196
+ return hasPermission || hasHooks;
197
+ }
198
+ export function isTinyfishConfiguredInClaudeMd(content) {
199
+ return hasMarker(content) || hasLegacyBlock(content);
200
+ }
@@ -1,13 +1,14 @@
1
- import { type AgentRunAsyncResponse, type AgentRunParams, type AgentRunResponse, type AgentRunWithStreamingResponse, type FetchGetContentsParams, type FetchResponse, type BrowserSession, type BrowserSessionCreateParams, type Run, type RunListParams, type RunListResponse, type SearchQueryParams, type SearchQueryResponse } from "@tiny-fish/sdk";
2
- import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse } from "./types.js";
3
- export declare function runSync(req: AgentRunParams, apiKey: string): Promise<AgentRunResponse>;
4
- export declare function runAsync(req: AgentRunParams, apiKey: string): Promise<AgentRunAsyncResponse>;
5
- export declare function runStream(req: AgentRunParams, apiKey: string, signal?: AbortSignal): AsyncGenerator<AgentRunWithStreamingResponse>;
1
+ import { type AgentRunAsyncResponse, type AgentRunResponse, type AgentRunWithStreamingResponse, type FetchGetContentsParams, type FetchResponse, type BrowserSession, type Run, type RunListParams, type RunListResponse, type SearchQueryParams, type SearchQueryResponse } from "@tiny-fish/sdk";
2
+ import type { BatchCancelResponse, BatchGetResponse, BatchRunRequest, BatchRunResponse, CancelRunResponse, CliAgentRunParams, CliBrowserSessionCreateParams, RunStepsResponse } from "./types.js";
3
+ export declare function runSync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunResponse>;
4
+ export declare function runAsync(req: CliAgentRunParams, apiKey: string): Promise<AgentRunAsyncResponse>;
5
+ export declare function runStream(req: CliAgentRunParams, apiKey: string, signal?: AbortSignal): AsyncGenerator<AgentRunWithStreamingResponse>;
6
6
  export declare function listRuns(opts: RunListParams, apiKey: string): Promise<RunListResponse>;
7
7
  export declare function getRun(runId: string, apiKey: string): Promise<Run>;
8
+ export declare function getRunSteps(runId: string, apiKey: string): Promise<RunStepsResponse>;
8
9
  export declare function searchQuery(params: SearchQueryParams, apiKey: string): Promise<SearchQueryResponse>;
9
10
  export declare function fetchContentGet(params: FetchGetContentsParams, apiKey: string): Promise<FetchResponse>;
10
- export declare function browserSessionCreate(params: BrowserSessionCreateParams, apiKey: string): Promise<BrowserSession>;
11
+ export declare function browserSessionCreate(params: CliBrowserSessionCreateParams, apiKey: string): Promise<BrowserSession>;
11
12
  export declare function cancelRun(runId: string, apiKey: string): Promise<CancelRunResponse>;
12
13
  export declare function submitBatch(req: BatchRunRequest, apiKey: string): Promise<BatchRunResponse>;
13
14
  export declare function getBatchRuns(runIds: string[], apiKey: string): Promise<BatchGetResponse>;
@@ -1,4 +1,4 @@
1
- import { APIStatusError, fetchResponseSchema, browserSessionSchema, searchQueryResponseSchema, TinyFish, runSchema, } from "@tiny-fish/sdk";
1
+ import { APIStatusError, agentRunAsyncResponseSchema, agentRunResponseSchema, agentRunWithStreamingResponseSchema, fetchResponseSchema, browserSessionSchema, searchQueryResponseSchema, TinyFish, runSchema, runStatusSchema, } from "@tiny-fish/sdk";
2
2
  import { BASE_URL } from "./constants.js";
3
3
  import { ApiError } from "./output.js";
4
4
  import { z } from "zod";
@@ -54,6 +54,23 @@ const batchCancelResponseSchema = z.object({
54
54
  results: z.array(batchCancelResultSchema),
55
55
  not_found: z.array(z.string()).nullable(),
56
56
  });
57
+ const runStepSchema = z.object({
58
+ id: z.string(),
59
+ timestamp: z.string(),
60
+ status: z.string(),
61
+ action: z.string().nullable(),
62
+ screenshot: z.string().nullable(),
63
+ duration: z.string().nullable(),
64
+ target_elements: z.array(z.unknown()).optional(),
65
+ snapshot_url: z.string().optional(),
66
+ });
67
+ const runStepsResponseSchema = z.object({
68
+ run_id: z.string(),
69
+ // Same payload as `GET /v1/runs/:id` — `?screenshots=none` just suppresses base64 image fields.
70
+ // We surface `status` so callers (e.g. `agent run watch` polling) can avoid a redundant get-run.
71
+ status: runStatusSchema,
72
+ steps: z.array(runStepSchema),
73
+ });
57
74
  function parseWithSchema(schema, value, message) {
58
75
  const result = schema.safeParse(value);
59
76
  if (!result.success) {
@@ -61,9 +78,59 @@ function parseWithSchema(schema, value, message) {
61
78
  }
62
79
  return result.data;
63
80
  }
81
+ function normalizeStreamEvent(event) {
82
+ if (typeof event !== "object" || event === null || event.type !== "COMPLETE") {
83
+ return event;
84
+ }
85
+ const data = event;
86
+ return {
87
+ ...data,
88
+ result: typeof data["result"] === "object" && data["result"] !== null ? data["result"] : null,
89
+ error: typeof data["error"] === "object" && data["error"] !== null ? data["error"] : null,
90
+ };
91
+ }
92
+ async function* parseSseStream(stream) {
93
+ const reader = stream.getReader();
94
+ let buffer = "";
95
+ const parseLine = (rawLine) => {
96
+ const line = rawLine.trim();
97
+ if (!line || line.startsWith(":") || !line.startsWith("data:"))
98
+ return undefined;
99
+ try {
100
+ return JSON.parse(line.slice("data:".length).trim());
101
+ }
102
+ catch {
103
+ return undefined;
104
+ }
105
+ };
106
+ try {
107
+ while (true) {
108
+ const { done, value } = await reader.read();
109
+ if (done)
110
+ break;
111
+ buffer += value;
112
+ const lines = buffer.split("\n");
113
+ buffer = lines.pop() ?? "";
114
+ for (const rawLine of lines) {
115
+ const event = parseLine(rawLine);
116
+ if (event !== undefined)
117
+ yield event;
118
+ }
119
+ }
120
+ const finalEvent = parseLine(buffer);
121
+ if (finalEvent !== undefined)
122
+ yield finalEvent;
123
+ }
124
+ finally {
125
+ reader.releaseLock();
126
+ }
127
+ }
64
128
  export async function runSync(req, apiKey) {
65
129
  try {
66
- return await createSdkClient(apiKey).agent.run(req);
130
+ const response = await createSdkClient(apiKey).post("/v1/automation/run", {
131
+ json: req,
132
+ });
133
+ return parseWithSchema(agentRunResponseSchema, response, "Invalid agent run response");
67
134
  }
68
135
  catch (error) {
69
136
  rethrowSdkError(error);
@@ -71,7 +138,10 @@ export async function runSync(req, apiKey) {
71
138
  }
72
139
  export async function runAsync(req, apiKey) {
73
140
  try {
74
- return await createSdkClient(apiKey).agent.queue(req);
141
+ const response = await createSdkClient(apiKey).post("/v1/automation/run-async", {
142
+ json: req,
143
+ });
144
+ return parseWithSchema(agentRunAsyncResponseSchema, response, "Invalid async run response");
75
145
  }
76
146
  catch (error) {
77
147
  rethrowSdkError(error);
@@ -80,9 +150,14 @@ export async function runAsync(req, apiKey) {
80
150
  export async function* runStream(req, apiKey, signal) {
81
151
  let stream = null;
82
152
  try {
83
- stream = await createSdkClient(apiKey).agent.stream(req, { signal });
84
- for await (const event of stream) {
85
- yield event;
153
+ stream = await createSdkClient(apiKey).postStream("/v1/automation/run-sse", {
154
+ json: req,
155
+ signal,
156
+ });
157
+ for await (const event of parseSseStream(stream)) {
158
+ const parsed = agentRunWithStreamingResponseSchema.safeParse(normalizeStreamEvent(event));
159
+ if (parsed.success)
160
+ yield parsed.data;
86
161
  }
87
162
  }
88
163
  catch (error) {
@@ -91,7 +166,7 @@ export async function* runStream(req, apiKey, signal) {
91
166
  finally {
92
167
  if (stream) {
93
168
  try {
94
- await stream.close();
169
+ await stream.cancel();
95
170
  }
96
171
  catch {
97
172
  // Ignore close errors — the original stream result/error already decided control flow.
@@ -115,6 +190,17 @@ export async function getRun(runId, apiKey) {
115
190
  rethrowSdkError(error);
116
191
  }
117
192
  }
193
+ export async function getRunSteps(runId, apiKey) {
194
+ try {
195
+ // The public GET /v1/runs/:id route returns the base run payload plus `steps`;
196
+ // the installed SDK runSchema only models the base run, so steps need this local schema.
197
+ const response = await createSdkClient(apiKey).get(`/v1/runs/${encodeURIComponent(runId)}`, { params: { screenshots: "none" } });
198
+ return parseWithSchema(runStepsResponseSchema, response, "Invalid run steps response");
199
+ }
200
+ catch (error) {
201
+ rethrowSdkError(error);
202
+ }
203
+ }
118
204
  export async function searchQuery(params, apiKey) {
119
205
  try {
120
206
  const response = await createSdkClient(apiKey).search.query(params);
@@ -135,7 +221,7 @@ export async function fetchContentGet(params, apiKey) {
135
221
  }
136
222
  export async function browserSessionCreate(params, apiKey) {
137
223
  try {
138
- const response = await createSdkClient(apiKey).browser.sessions.create(params);
224
+ const response = await createSdkClient(apiKey).post("/v1/browser", { json: params });
139
225
  return browserSessionSchema.parse(response);
140
226
  }
141
227
  catch (error) {
@@ -1,3 +1,4 @@
1
1
  export declare const BASE_URL: string;
2
2
  /** URL where users can create and manage API keys */
3
+ export declare function getDashboardUrl(source?: string): string;
3
4
  export declare const DASHBOARD_URL: string;
@@ -15,5 +15,19 @@ if (apiUrlOverride) {
15
15
  }
16
16
  }
17
17
  export const BASE_URL = apiUrlOverride || "https://agent.tinyfish.ai";
18
+ const DEFAULT_DASHBOARD_SOURCE = "cli";
19
+ const DASHBOARD_SOURCE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
20
+ function normalizeDashboardSource(source) {
21
+ const normalized = source?.trim().toLowerCase();
22
+ if (!normalized || !DASHBOARD_SOURCE_PATTERN.test(normalized)) {
23
+ return DEFAULT_DASHBOARD_SOURCE;
24
+ }
25
+ return normalized;
26
+ }
18
27
  /** URL where users can create and manage API keys */
19
- export const DASHBOARD_URL = `${BASE_URL}/api-keys`;
28
+ export function getDashboardUrl(source = DEFAULT_DASHBOARD_SOURCE) {
29
+ const url = new URL(`${BASE_URL}/api-keys`);
30
+ url.searchParams.set("source", normalizeDashboardSource(source));
31
+ return url.toString();
32
+ }
33
+ export const DASHBOARD_URL = getDashboardUrl();
@@ -1,4 +1,33 @@
1
- import type { Run, RunStatus } from "@tiny-fish/sdk";
1
+ import type { AgentRunParams, BrowserProfile, Run, RunStatus } from "@tiny-fish/sdk";
2
+ export type OutputSchema = Record<string, unknown>;
3
+ export interface CliAgentRunParams extends AgentRunParams {
4
+ output_schema?: OutputSchema;
5
+ agent_config?: {
6
+ mode?: "default" | "strict";
7
+ max_steps?: number;
8
+ };
9
+ browser_profile?: BrowserProfile;
10
+ session_id?: string;
11
+ }
12
+ export interface RunStep {
13
+ id: string;
14
+ timestamp: string;
15
+ status: string;
16
+ action: string | null;
17
+ screenshot: string | null;
18
+ duration: string | null;
19
+ target_elements?: unknown[];
20
+ snapshot_url?: string;
21
+ }
22
+ export interface RunStepsResponse {
23
+ run_id: string;
24
+ status: RunStatus;
25
+ steps: RunStep[];
26
+ }
27
+ export interface CliBrowserSessionCreateParams {
28
+ url?: string;
29
+ browser_profile?: "lite" | "stealth";
30
+ }
2
31
  export interface CancelRunResponse {
3
32
  run_id: string;
4
33
  status: RunStatus | string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.6-next.58",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,11 +45,12 @@
45
45
  "eslint": "^10.1.0",
46
46
  "prettier": "^3.0.0",
47
47
  "typescript": "^5.0.0",
48
- "vitest": "^3.0.0"
48
+ "vitest": "^4.1.0"
49
49
  },
50
50
  "overrides": {
51
51
  "vite": "7.3.2",
52
- "brace-expansion": "5.0.5"
52
+ "brace-expansion": "^5.0.6",
53
+ "postcss": "8.5.10"
53
54
  },
54
55
  "engines": {
55
56
  "node": ">=24.0.0"