@tiny-fish/cli 0.1.1 → 0.1.2-next.16

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 ADDED
@@ -0,0 +1,87 @@
1
+ # TinyFish CLI
2
+
3
+ Run web automations from your terminal, shell scripts, and CI/CD pipelines.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @tiny-fish/cli
9
+ ```
10
+
11
+ Requires Node.js 24+.
12
+
13
+ ## Authentication
14
+
15
+ Get your API key from [agent.tinyfish.ai](https://agent.tinyfish.ai).
16
+
17
+ ```bash
18
+ # Interactive (opens browser, prompts for key)
19
+ tinyfish auth login
20
+
21
+ # CI/CD safe (pipe key via stdin)
22
+ echo $TINYFISH_API_KEY | tinyfish auth set
23
+
24
+ # Or set via environment variable
25
+ export TINYFISH_API_KEY=sk-tinyfish-...
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Run an automation
31
+
32
+ ```bash
33
+ # Stream steps as they happen (default)
34
+ tinyfish agent run "Find the pricing page" --url https://example.com
35
+
36
+ # Human-readable output
37
+ tinyfish agent run "Find the pricing page" --url https://example.com --pretty
38
+
39
+ # Wait for result without streaming
40
+ tinyfish agent run "Find the pricing page" --url https://example.com --sync
41
+
42
+ # Submit and return immediately (don't wait)
43
+ tinyfish agent run "Find the pricing page" --url https://example.com --async
44
+ ```
45
+
46
+ ### Manage runs
47
+
48
+ ```bash
49
+ # List recent runs
50
+ tinyfish agent run list --pretty
51
+
52
+ # Filter by status
53
+ tinyfish agent run list --status RUNNING --pretty
54
+
55
+ # Inspect a run
56
+ tinyfish agent run get <run_id> --pretty
57
+
58
+ # Cancel a run
59
+ tinyfish agent run cancel <run_id> --pretty
60
+ ```
61
+
62
+ ### Output format
63
+
64
+ By default all commands output newline-delimited JSON to stdout — pipe-friendly for agents and scripts. Add `--pretty` for human-readable output.
65
+
66
+ Errors are JSON to stderr with exit code 1. Ctrl+C during a streaming run cancels it automatically.
67
+
68
+ ### Debug
69
+
70
+ ```bash
71
+ TINYFISH_DEBUG=1 tinyfish agent run "..." --url https://example.com
72
+ # or
73
+ tinyfish --debug agent run "..." --url https://example.com
74
+ ```
75
+
76
+ ## CI/CD
77
+
78
+ ```yaml
79
+ - name: Run automation
80
+ env:
81
+ TINYFISH_API_KEY: ${{ secrets.TINYFISH_API_KEY }}
82
+ run: |
83
+ tinyfish agent run "Check that the login flow works" \
84
+ --url https://staging.example.com \
85
+ --sync \
86
+ --pretty
87
+ ```
@@ -1,7 +1,7 @@
1
1
  import { spawn } from "child_process";
2
2
  import * as readline from "readline";
3
3
  import { DASHBOARD_URL, clearConfig, loadConfig, maskKey, saveConfig, validateKeyFormat, } from "../lib/auth.js";
4
- import { err, out, outLine } from "../lib/output.js";
4
+ import { err, errLine, out, outLine } from "../lib/output.js";
5
5
  /**
6
6
  * Read a key from stdin without echoing it to the terminal.
7
7
  * Uses setRawMode on TTY so characters are never written to the screen.
@@ -71,7 +71,7 @@ export function registerAuth(program) {
71
71
  .command("login")
72
72
  .description("Open the API keys page and save your key interactively")
73
73
  .action(async () => {
74
- process.stderr.write(`Opening ${DASHBOARD_URL} in your browser...\n`);
74
+ errLine(`Opening ${DASHBOARD_URL} in your browser...`);
75
75
  try {
76
76
  // Use spawn with an explicit args array — no shell, no interpolation
77
77
  let cmd;
@@ -166,6 +166,7 @@ export function registerAuth(program) {
166
166
  else {
167
167
  out(result);
168
168
  }
169
+ process.exit(1);
169
170
  });
170
171
  auth
171
172
  .command("logout")
@@ -1,6 +1,6 @@
1
1
  import { getApiKey } from "../lib/auth.js";
2
- import { runAsync, runStream, runSync } from "../lib/client.js";
3
- import { err, handleApiError, out, outLine } from "../lib/output.js";
2
+ import { cancelRun, runAsync, runStream, runSync } from "../lib/client.js";
3
+ import { err, errLine, handleApiError, out, outLine } from "../lib/output.js";
4
4
  import { RunStatus, StreamEventType } from "../lib/types.js";
5
5
  import { BASE_URL } from "../lib/constants.js";
6
6
  const RUN_TIMEOUT_MS = 20 * 60 * 1000; // 20 minutes
@@ -53,6 +53,10 @@ export function registerRun(agentCmd) {
53
53
  process.exit(1);
54
54
  }
55
55
  }
56
+ if (!goal.trim()) {
57
+ err({ error: 'Goal cannot be empty' });
58
+ process.exit(1);
59
+ }
56
60
  const apiKey = getApiKey();
57
61
  const req = { goal, url: normalizedUrl };
58
62
  if (opts.async) {
@@ -67,7 +71,7 @@ export function registerRun(agentCmd) {
67
71
  const { run_id: _asyncRunId, ...asyncRest } = result; // eslint-disable-line @typescript-eslint/no-unused-vars
68
72
  const asyncResultWithUrl = { run_id: result.run_id, run_url: `${BASE_URL}/runs/${result.run_id}`, ...asyncRest };
69
73
  if (opts.pretty) {
70
- outLine(`Run submitted\nID: ${asyncResultWithUrl.run_id}\nStatus: ${asyncResultWithUrl.status}\nURL: ${asyncResultWithUrl.run_url}`);
74
+ outLine(`Run submitted\nID: ${asyncResultWithUrl.run_id}\nURL: ${asyncResultWithUrl.run_url}`);
71
75
  }
72
76
  else {
73
77
  out(asyncResultWithUrl);
@@ -75,7 +79,7 @@ export function registerRun(agentCmd) {
75
79
  return;
76
80
  }
77
81
  if (opts.sync) {
78
- process.stderr.write("Waiting for result...\n");
82
+ errLine("Waiting for result...");
79
83
  let result;
80
84
  try {
81
85
  result = await runSync(req, apiKey, AbortSignal.timeout(RUN_TIMEOUT_MS));
@@ -97,6 +101,7 @@ export function registerRun(agentCmd) {
97
101
  // Default: SSE stream — 20 min timeout + Ctrl+C cancellation
98
102
  const controller = new AbortController();
99
103
  let siginted = false;
104
+ let capturedRunId = null;
100
105
  const timeout = setTimeout(() => controller.abort(), RUN_TIMEOUT_MS);
101
106
  const onSigint = () => {
102
107
  siginted = true;
@@ -106,18 +111,26 @@ export function registerRun(agentCmd) {
106
111
  let streamFailed = false;
107
112
  try {
108
113
  for await (const event of runStream(req, apiKey, controller.signal)) {
109
- if (!handleStreamEvent(event, opts.pretty)) {
114
+ if (!handleStreamEvent(event, opts.pretty, (id) => { capturedRunId = id; })) {
110
115
  streamFailed = true;
111
116
  break;
112
117
  }
113
118
  }
114
119
  }
115
120
  catch (e) {
116
- if (controller.signal.aborted) {
117
- if (siginted) {
118
- process.stderr.write("\n");
119
- process.exit(130);
121
+ if (controller.signal.aborted && siginted) {
122
+ let cancelled = false;
123
+ if (capturedRunId) {
124
+ try {
125
+ await cancelRun(capturedRunId, apiKey);
126
+ cancelled = true;
127
+ }
128
+ catch { /* ignore */ }
120
129
  }
130
+ process.stderr.write(cancelled ? "\nRun cancelled.\n" : "\n");
131
+ process.exit(130);
132
+ }
133
+ if (controller.signal.aborted) {
121
134
  err({ error: "Run timed out after 20 minutes" });
122
135
  process.exit(1);
123
136
  }
@@ -136,7 +149,7 @@ export function registerRun(agentCmd) {
136
149
  * Handle one SSE event from the stream.
137
150
  * Returns false on terminal error (caller should exit 1); true otherwise.
138
151
  */
139
- function handleStreamEvent(event, pretty) {
152
+ function handleStreamEvent(event, pretty, onRunId) {
140
153
  if (event.type === StreamEventType.ERROR) {
141
154
  err({ error: event.error, run_id: event.run_id, status: StreamEventType.ERROR });
142
155
  return false;
@@ -148,6 +161,8 @@ function handleStreamEvent(event, pretty) {
148
161
  if (pretty) {
149
162
  switch (event.type) {
150
163
  case StreamEventType.STARTED:
164
+ if (onRunId && event.run_id)
165
+ onRunId(event.run_id);
151
166
  outLine(`▶ Run started`);
152
167
  break;
153
168
  case StreamEventType.STREAMING_URL:
@@ -1,5 +1,5 @@
1
1
  import { getApiKey } from "../lib/auth.js";
2
- import { getRun, listRuns } from "../lib/client.js";
2
+ import { cancelRun, getRun, listRuns } from "../lib/client.js";
3
3
  import { err, handleApiError, out, outLine } from "../lib/output.js";
4
4
  import { RunStatus } from "../lib/types.js";
5
5
  const VALID_STATUSES = Object.values(RunStatus);
@@ -95,4 +95,31 @@ export function registerRuns(runCmd) {
95
95
  handleApiError(e);
96
96
  }
97
97
  });
98
+ // ── run cancel ────────────────────────────────────────────────────────────
99
+ runCmd
100
+ .command("cancel <run_id>")
101
+ .description("Cancel a run by ID")
102
+ .option("--pretty", "Human-readable output")
103
+ .action(async (runId, opts) => {
104
+ const apiKey = getApiKey();
105
+ try {
106
+ const result = await cancelRun(runId, apiKey);
107
+ if (opts.pretty) {
108
+ if (result.status === RunStatus.CANCELLED) {
109
+ outLine(result.message
110
+ ? `Run ${result.run_id} cancelled (${result.message}).`
111
+ : `Run ${result.run_id} cancelled.`);
112
+ }
113
+ else {
114
+ outLine(`Run ${result.run_id} already finished (${result.status}).`);
115
+ }
116
+ }
117
+ else {
118
+ out(result);
119
+ }
120
+ }
121
+ catch (e) {
122
+ handleApiError(e);
123
+ }
124
+ });
98
125
  }
@@ -1,6 +1,7 @@
1
- import type { ListRunsOptions, ListRunsResponse, RunApiResponse, RunRequest, RunResult, StreamEvent } from "./types.js";
1
+ import type { CancelRunResponse, ListRunsOptions, ListRunsResponse, RunApiResponse, RunRequest, RunResult, StreamEvent } from "./types.js";
2
2
  export declare function runSync(req: RunRequest, apiKey: string, signal?: AbortSignal): Promise<RunResult>;
3
3
  export declare function runAsync(req: RunRequest, apiKey: string): Promise<RunResult>;
4
4
  export declare function runStream(req: RunRequest, apiKey: string, signal?: AbortSignal): AsyncGenerator<StreamEvent>;
5
5
  export declare function listRuns(opts: ListRunsOptions, apiKey: string): Promise<ListRunsResponse>;
6
6
  export declare function getRun(runId: string, apiKey: string): Promise<RunApiResponse>;
7
+ export declare function cancelRun(runId: string, apiKey: string): Promise<CancelRunResponse>;
@@ -1,5 +1,6 @@
1
1
  import { BASE_URL } from "./constants.js";
2
2
  import { ApiError } from "./output.js";
3
+ const API_TIMEOUT_MS = 30_000;
3
4
  // ── Debug logging ─────────────────────────────────────────────────────────────
4
5
  const DEBUG_ENABLED = /^(1|true)$/i.test(process.env["TINYFISH_DEBUG"] ?? "");
5
6
  function debugLog(msg) {
@@ -73,7 +74,7 @@ export async function runSync(req, apiKey, signal) {
73
74
  }
74
75
  export async function runAsync(req, apiKey) {
75
76
  // Async just submits the job — 30 s is plenty to get an ACK
76
- const res = await postJson("/v1/automation/run-async", req, apiKey, AbortSignal.timeout(30_000));
77
+ const res = await postJson("/v1/automation/run-async", req, apiKey, AbortSignal.timeout(API_TIMEOUT_MS));
77
78
  return res.json();
78
79
  }
79
80
  export async function* runStream(req, apiKey, signal) {
@@ -111,8 +112,12 @@ export async function listRuns(opts, apiKey) {
111
112
  if (opts.cursor)
112
113
  params.set("cursor", opts.cursor);
113
114
  const qs = params.size ? `?${params}` : "";
114
- return getJson(`/v1/runs${qs}`, apiKey, AbortSignal.timeout(30_000));
115
+ return getJson(`/v1/runs${qs}`, apiKey, AbortSignal.timeout(API_TIMEOUT_MS));
115
116
  }
116
117
  export async function getRun(runId, apiKey) {
117
- return getJson(`/v1/runs/${encodeURIComponent(runId)}`, apiKey, AbortSignal.timeout(30_000));
118
+ return getJson(`/v1/runs/${encodeURIComponent(runId)}`, apiKey, AbortSignal.timeout(API_TIMEOUT_MS));
119
+ }
120
+ export async function cancelRun(runId, apiKey) {
121
+ const res = await postJson(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {}, apiKey, AbortSignal.timeout(API_TIMEOUT_MS));
122
+ return res.json();
118
123
  }
@@ -5,5 +5,6 @@ export declare class ApiError extends Error {
5
5
  }
6
6
  export declare function out(data: unknown): void;
7
7
  export declare function outLine(line: string): void;
8
+ export declare function errLine(line: string): void;
8
9
  export declare function err(data: unknown): void;
9
10
  export declare function handleApiError(e: unknown): never;
@@ -14,6 +14,9 @@ export function out(data) {
14
14
  export function outLine(line) {
15
15
  process.stdout.write(line + "\n");
16
16
  }
17
+ export function errLine(line) {
18
+ process.stderr.write(line + "\n");
19
+ }
17
20
  export function err(data) {
18
21
  const payload = typeof data === "string" ? { error: data } : data;
19
22
  process.stderr.write(JSON.stringify(payload) + "\n");
@@ -94,3 +94,9 @@ export interface ListRunsOptions {
94
94
  limit?: number;
95
95
  cursor?: string;
96
96
  }
97
+ export interface CancelRunResponse {
98
+ run_id: string;
99
+ status: RunStatus | string;
100
+ cancelled_at: string;
101
+ message: string | null;
102
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.1.1",
4
- "description": "TinyFish CLI \u2014 run web automations from your terminal",
3
+ "version": "0.1.2-next.16",
4
+ "description": "TinyFish CLI run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tinyfish": "./dist/index.js"
@@ -39,7 +39,6 @@
39
39
  "node": ">=24.0.0"
40
40
  },
41
41
  "publishConfig": {
42
- "registry": "https://registry.npmjs.org/",
43
- "access": "restricted"
42
+ "registry": "https://registry.npmjs.org/"
44
43
  }
45
44
  }