@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 +87 -0
- package/dist/commands/auth.js +3 -2
- package/dist/commands/run.js +25 -10
- package/dist/commands/runs.js +28 -1
- package/dist/lib/client.d.ts +2 -1
- package/dist/lib/client.js +8 -3
- package/dist/lib/output.d.ts +1 -0
- package/dist/lib/output.js +3 -0
- package/dist/lib/types.d.ts +6 -0
- package/package.json +3 -4
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
|
+
```
|
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
-
|
|
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")
|
package/dist/commands/run.js
CHANGED
|
@@ -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}\
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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:
|
package/dist/commands/runs.js
CHANGED
|
@@ -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
|
}
|
package/dist/lib/client.d.ts
CHANGED
|
@@ -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>;
|
package/dist/lib/client.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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
|
}
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/output.js
CHANGED
|
@@ -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");
|
package/dist/lib/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiny-fish/cli",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "TinyFish CLI
|
|
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
|
}
|