@tiny-fish/cli 0.1.0 → 0.1.1

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.
@@ -1,2 +1,2 @@
1
1
  import { Command } from "commander";
2
- export declare function registerRun(program: Command): void;
2
+ export declare function registerRun(agentCmd: Command): Command;
@@ -2,45 +2,59 @@ import { getApiKey } from "../lib/auth.js";
2
2
  import { runAsync, runStream, runSync } from "../lib/client.js";
3
3
  import { err, handleApiError, out, outLine } from "../lib/output.js";
4
4
  import { RunStatus, StreamEventType } from "../lib/types.js";
5
+ import { BASE_URL } from "../lib/constants.js";
5
6
  const RUN_TIMEOUT_MS = 20 * 60 * 1000; // 20 minutes
6
7
  function checkRunResult(result, expectComplete = true) {
7
8
  if (result.error) {
8
- err({ error: result.error, runId: result.runId, status: result.status });
9
+ err({ error: result.error, run_id: result.run_id, status: result.status });
9
10
  process.exit(1);
10
11
  }
11
12
  if (expectComplete && result.status && result.status !== RunStatus.COMPLETED) {
12
- err({ error: `Run ${result.status}`, runId: result.runId, status: result.status });
13
+ err({ error: `Run ${result.status}`, run_id: result.run_id, status: result.status });
13
14
  process.exit(1);
14
15
  }
15
16
  }
16
- export function registerRun(program) {
17
- program
18
- .command("run <goal>")
19
- .description("Run a browser automation")
20
- .requiredOption("--url <url>", "Target URL for the automation")
17
+ export function registerRun(agentCmd) {
18
+ const runCmd = agentCmd
19
+ .command("run")
20
+ .description("Run a browser automation or manage runs")
21
+ .option("--url <url>", "Target URL for the automation")
21
22
  .option("--sync", "Wait for result without streaming steps")
22
23
  .option("--async", "Submit without waiting for result")
23
24
  .option("--pretty", "Human-readable output")
25
+ .argument("[goal]", "Goal to automate")
24
26
  .action(async (goal, opts) => {
27
+ // If no goal is provided and no subcommand matched, show help
28
+ if (!goal) {
29
+ runCmd.help();
30
+ return;
31
+ }
32
+ if (!opts.url) {
33
+ err({ error: "required option '--url <url>' not specified" });
34
+ process.exit(1);
35
+ }
25
36
  if (opts.sync && opts.async) {
26
37
  err({ error: "--sync and --async are mutually exclusive" });
27
38
  process.exit(1);
28
39
  }
29
- // Reject obviously invalid input — accepts bare hostnames like "google.com"
40
+ // Normalise URL: accept bare hostnames by prepending https://.
41
+ // The server requires a valid URL — it does not auto-prefix.
42
+ let normalizedUrl = opts.url;
30
43
  try {
31
44
  new URL(opts.url);
32
45
  }
33
46
  catch {
34
47
  try {
35
48
  new URL(`https://${opts.url}`);
49
+ normalizedUrl = `https://${opts.url}`;
36
50
  }
37
51
  catch {
38
- err({ error: "--url does not look like a valid URL" });
52
+ err({ error: `Invalid --url "${opts.url}". Provide a valid URL, e.g. https://example.com or example.com` });
39
53
  process.exit(1);
40
54
  }
41
55
  }
42
56
  const apiKey = getApiKey();
43
- const req = { goal, url: opts.url };
57
+ const req = { goal, url: normalizedUrl };
44
58
  if (opts.async) {
45
59
  let result;
46
60
  try {
@@ -50,11 +64,13 @@ export function registerRun(program) {
50
64
  handleApiError(e);
51
65
  }
52
66
  checkRunResult(result, false); // async = PENDING is expected
67
+ const { run_id: _asyncRunId, ...asyncRest } = result; // eslint-disable-line @typescript-eslint/no-unused-vars
68
+ const asyncResultWithUrl = { run_id: result.run_id, run_url: `${BASE_URL}/runs/${result.run_id}`, ...asyncRest };
53
69
  if (opts.pretty) {
54
- outLine(`Run submitted\nID: ${result.runId}\nStatus: ${result.status}`);
70
+ outLine(`Run submitted\nID: ${asyncResultWithUrl.run_id}\nStatus: ${asyncResultWithUrl.status}\nURL: ${asyncResultWithUrl.run_url}`);
55
71
  }
56
72
  else {
57
- out(result);
73
+ out(asyncResultWithUrl);
58
74
  }
59
75
  return;
60
76
  }
@@ -68,11 +84,13 @@ export function registerRun(program) {
68
84
  handleApiError(e);
69
85
  }
70
86
  checkRunResult(result);
87
+ const { run_id: _syncRunId, ...syncRest } = result; // eslint-disable-line @typescript-eslint/no-unused-vars
88
+ const syncResultWithUrl = { run_id: result.run_id, run_url: `${BASE_URL}/runs/${result.run_id}`, ...syncRest };
71
89
  if (opts.pretty) {
72
- outLine(`Status: ${result.status}\n\n${JSON.stringify(result.resultJson ?? {}, null, 2)}`);
90
+ outLine(`Status: ${syncResultWithUrl.status}\n\n${JSON.stringify(syncResultWithUrl.result ?? {}, null, 2)}`);
73
91
  }
74
92
  else {
75
- out(result);
93
+ out(syncResultWithUrl);
76
94
  }
77
95
  return;
78
96
  }
@@ -112,6 +130,7 @@ export function registerRun(program) {
112
130
  if (streamFailed)
113
131
  process.exit(1);
114
132
  });
133
+ return runCmd;
115
134
  }
116
135
  /**
117
136
  * Handle one SSE event from the stream.
@@ -119,11 +138,11 @@ export function registerRun(program) {
119
138
  */
120
139
  function handleStreamEvent(event, pretty) {
121
140
  if (event.type === StreamEventType.ERROR) {
122
- err({ error: event.error, runId: event.runId, status: StreamEventType.ERROR });
141
+ err({ error: event.error, run_id: event.run_id, status: StreamEventType.ERROR });
123
142
  return false;
124
143
  }
125
144
  if (event.type === StreamEventType.COMPLETE && event.status !== RunStatus.COMPLETED) {
126
- err({ error: event.error ?? `Run ${event.status}`, runId: event.runId, status: event.status });
145
+ err({ error: event.error ?? `Run ${event.status}`, run_id: event.run_id, status: event.status });
127
146
  return false;
128
147
  }
129
148
  if (pretty) {
@@ -132,14 +151,16 @@ function handleStreamEvent(event, pretty) {
132
151
  outLine(`▶ Run started`);
133
152
  break;
134
153
  case StreamEventType.STREAMING_URL:
135
- outLine(`🔗 Live view: ${event.streamingUrl}`);
154
+ outLine(`🔗 Live view: ${event.streaming_url}`);
136
155
  break;
137
156
  case StreamEventType.PROGRESS:
138
157
  outLine(`• ${event.purpose}`);
139
158
  break;
140
- case StreamEventType.COMPLETE:
141
- outLine(`✓ Completed\n\n${JSON.stringify(event.resultJson ?? {}, null, 2)}`);
159
+ case StreamEventType.COMPLETE: {
160
+ const runUrl = `${BASE_URL}/runs/${event.run_id}`;
161
+ outLine(`✓ Completed\n\n${JSON.stringify(event.result ?? {}, null, 2)}\n\nView run: ${runUrl}`);
142
162
  break;
163
+ }
143
164
  case StreamEventType.HEARTBEAT:
144
165
  // silently skip — keep-alive noise
145
166
  break;
@@ -147,7 +168,13 @@ function handleStreamEvent(event, pretty) {
147
168
  }
148
169
  else {
149
170
  // Raw JSON: emit all events (STREAMING_URL and HEARTBEAT are useful for agents)
150
- out(event);
171
+ if (event.type === StreamEventType.COMPLETE) {
172
+ const { run_id: _eventRunId, ...eventRest } = event; // eslint-disable-line @typescript-eslint/no-unused-vars
173
+ out({ run_id: event.run_id, run_url: `${BASE_URL}/runs/${event.run_id}`, ...eventRest });
174
+ }
175
+ else {
176
+ out(event);
177
+ }
151
178
  }
152
179
  return true;
153
180
  }
@@ -1,2 +1,2 @@
1
1
  import { Command } from "commander";
2
- export declare function registerRuns(program: Command): void;
2
+ export declare function registerRuns(runCmd: Command): void;
@@ -3,12 +3,9 @@ import { 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);
6
- export function registerRuns(program) {
7
- const runs = program
8
- .command("runs")
9
- .description("Manage past automation runs");
10
- // ── runs list ──────────────────────────────────────────────────────────────
11
- runs
6
+ export function registerRuns(runCmd) {
7
+ // ── run list ───────────────────────────────────────────────────────────────
8
+ runCmd
12
9
  .command("list")
13
10
  .description("List your automation runs")
14
11
  .option("--status <status>", "Filter by status (PENDING|RUNNING|COMPLETED|FAILED|CANCELLED)")
@@ -65,8 +62,8 @@ export function registerRuns(program) {
65
62
  handleApiError(e);
66
63
  }
67
64
  });
68
- // ── runs get ───────────────────────────────────────────────────────────────
69
- runs
65
+ // ── run get ────────────────────────────────────────────────────────────────
66
+ runCmd
70
67
  .command("get <run_id>")
71
68
  .description("Get a run by ID")
72
69
  .option("--pretty", "Human-readable output")
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ program
12
12
  .version(version, "-V, --version", "Show version")
13
13
  .helpOption("-h, --help", "Show help")
14
14
  .addHelpCommand(false)
15
+ .enablePositionalOptions()
15
16
  .option("--debug", "Print HTTP requests and responses to stderr (or set TINYFISH_DEBUG=1)")
16
17
  .hook("preAction", (cmd) => {
17
18
  if (cmd.opts().debug) {
@@ -19,8 +20,12 @@ program
19
20
  }
20
21
  });
21
22
  registerAuth(program);
22
- registerRun(program);
23
- registerRuns(program);
23
+ const agentCmd = program
24
+ .command("agent")
25
+ .description("Agent automation commands")
26
+ .enablePositionalOptions();
27
+ const runCmd = registerRun(agentCmd);
28
+ registerRuns(runCmd);
24
29
  // Await parseAsync so async command handlers complete before the process exits
25
30
  program.parseAsync(process.argv).catch((e) => {
26
31
  process.stderr.write(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) + "\n");
@@ -18,7 +18,9 @@ function headers(apiKey) {
18
18
  async function throwIfError(res) {
19
19
  if (!res.ok) {
20
20
  const json = await res.json().catch(() => ({}));
21
- const message = json.detail ?? `HTTP ${res.status}`;
21
+ const message = json.error?.message ??
22
+ json.detail ??
23
+ `HTTP ${res.status}`;
22
24
  throw new ApiError(res.status, message);
23
25
  }
24
26
  }
@@ -20,41 +20,41 @@ export declare const StreamEventType: {
20
20
  };
21
21
  export type StreamEventType = (typeof StreamEventType)[keyof typeof StreamEventType];
22
22
  export interface RunResult {
23
- runId: string;
23
+ run_id: string;
24
24
  status: RunStatus;
25
- resultJson?: unknown;
25
+ result?: unknown;
26
26
  error?: string;
27
27
  }
28
28
  /** Discriminated union of all real SSE event shapes from the API */
29
29
  export type StreamEvent = {
30
30
  type: typeof StreamEventType.STARTED;
31
- runId: string;
31
+ run_id: string;
32
32
  timestamp: string;
33
33
  } | {
34
34
  type: typeof StreamEventType.STREAMING_URL;
35
- runId: string;
36
- streamingUrl: string;
35
+ run_id: string;
36
+ streaming_url: string;
37
37
  timestamp: string;
38
38
  } | {
39
39
  type: typeof StreamEventType.HEARTBEAT;
40
40
  timestamp: string;
41
41
  } | {
42
42
  type: typeof StreamEventType.PROGRESS;
43
- runId: string;
43
+ run_id: string;
44
44
  purpose: string;
45
45
  timestamp: string;
46
46
  } | {
47
47
  type: typeof StreamEventType.COMPLETE;
48
- runId: string;
48
+ run_id: string;
49
49
  status: RunStatus;
50
50
  timestamp: string;
51
- resultJson?: unknown;
51
+ result?: unknown;
52
52
  error?: string;
53
53
  help_url?: string;
54
54
  help_message?: string;
55
55
  } | {
56
56
  type: typeof StreamEventType.ERROR;
57
- runId?: string;
57
+ run_id?: string;
58
58
  error: string;
59
59
  timestamp: string;
60
60
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.1.0",
4
- "description": "TinyFish CLI run web automations from your terminal",
3
+ "version": "0.1.1",
4
+ "description": "TinyFish CLI \u2014 run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "tinyfish": "./dist/index.js"
@@ -13,14 +13,14 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "build": "tsc",
16
- "prepack": "npm run build",
17
16
  "test": "vitest --run",
18
17
  "test:file": "vitest --run",
19
18
  "test:watch": "vitest",
20
19
  "test:integration": "vitest --run --config vitest.integration.config.ts",
21
20
  "lint": "eslint src tests",
22
21
  "format": "prettier --write src tests",
23
- "type-check": "tsc --noEmit --project tsconfig.all.json"
22
+ "type-check": "tsc --noEmit --project tsconfig.all.json",
23
+ "prepublishOnly": "npm run build"
24
24
  },
25
25
  "dependencies": {
26
26
  "commander": "^12.0.0",
@@ -37,5 +37,9 @@
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=24.0.0"
40
+ },
41
+ "publishConfig": {
42
+ "registry": "https://registry.npmjs.org/",
43
+ "access": "restricted"
40
44
  }
41
45
  }