@tiny-fish/cli 0.28.1 → 0.29.0

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
@@ -24,9 +24,11 @@ them all, or use the arrow keys and space to narrow the set first; `q` cancels w
24
24
  anything. Terminals without raw-key support get a numbered prompt instead.
25
25
 
26
26
  Without a terminal (agents, CI), bare `connect` behaves exactly like `tinyfish connect --all`
27
- and says so on its first line: every detected harness that can authenticate headlessly (via
28
- `TINYFISH_API_KEY` or `--api-key`) is connected, the rest are skipped with a fix command.
29
- `tinyfish connect --all` itself is unchanged and skips the picker; `--dry-run`, `--uninstall`
27
+ and says so on its first line: harnesses that can take your API key (via `TINYFISH_API_KEY` or
28
+ `--api-key`) connect first, then the first harness that needs a browser sign-in opens your
29
+ browser and waits up to 90 seconds for you to finish it. Any further sign-ins are reported as
30
+ waiting on you, with the single-harness command that finishes each one.
31
+ `tinyfish connect --all` itself skips the picker; `--dry-run`, `--uninstall`
30
32
  and `--skip-launch` still require it.
31
33
 
32
34
  ### Connect Claude Code
@@ -9,6 +9,8 @@ interface NativeConnectOptions {
9
9
  attemptId?: string;
10
10
  /** Headless: refuse rather than fall back to an unfinishable browser sign-in. */
11
11
  keyAuthOnly?: boolean;
12
+ /** Headless --all: bounds sign-in spawns and bars interactive CLI login. */
13
+ authTimeoutMs?: number;
12
14
  /** Internal only: false leaves the global npm tree untouched. */
13
15
  cliInstall?: boolean;
14
16
  /** Set by `--all`: the CLI install already ran once, upfront. */
@@ -4,7 +4,7 @@ import { CLAUDE_CODE, CODEX, CURSOR_SKILL_TARGET, GROK, HERMES, OPENCLAW, OPENCL
4
4
  import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, } from "../lib/cli-install.js";
5
5
  import { ensureCliAuthenticated } from "../lib/connect-auth.js";
6
6
  import { installWebSkill } from "../lib/skill-install.js";
7
- import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "../lib/connect-runtime.js";
7
+ import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "../lib/connect-runtime.js";
8
8
  import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from "../lib/cursor-config.js";
9
9
  import { runConnectAll } from "../lib/connect-all.js";
10
10
  import { AuthMode } from "../lib/harness-detect.js";
@@ -12,11 +12,11 @@ import { detectHumanInitiated } from "../lib/harness.js";
12
12
  import { TINYFISH_API_KEY_VAR } from "../lib/constants.js";
13
13
  import { emitNotice } from "../lib/notice.js";
14
14
  import { SIGN_IN_PROBES } from "../lib/registration-detect.js";
15
- import { errLine } from "../lib/output.js";
15
+ import { errLine, warnLine } from "../lib/output.js";
16
16
  import { z } from "zod";
17
17
  import { verifyMcpAuth } from "../lib/verify.js";
18
18
  export const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
19
- function removeExistingRegistration(client, removal) {
19
+ function removeExistingRegistration(client, removal, verbose) {
20
20
  const result = spawn.sync(client.command, removal.args, {
21
21
  encoding: "utf8",
22
22
  timeout: NON_INTERACTIVE_TIMEOUT_MS,
@@ -24,11 +24,11 @@ function removeExistingRegistration(client, removal) {
24
24
  if (result.status === 0)
25
25
  return;
26
26
  throwIfInterrupted(result);
27
- const details = result.stderr?.trim() || result.error?.message || "unknown error";
28
- // Claude Code double-quotes the name, Grok single-quotes it.
29
- if (/No MCP server named ['"]tinyfish['"]/i.test(details))
27
+ // Best-effort: a stale entry makes `mcp add` error or upsert, with a truer message.
28
+ if (!verbose)
30
29
  return;
31
- throw spawnStepError(`Could not remove existing ${removal.label}: ${details}`, result);
30
+ const details = result.stderr?.trim() || result.error?.message || "unknown error";
31
+ warnLine(`Could not remove existing ${removal.label}: ${details}`);
32
32
  }
33
33
  /** false: fall back to OAuth — a config naming an unset variable authenticates nothing. */
34
34
  function exportApiKey(apiKey, displayName, deferOutro) {
@@ -51,6 +51,13 @@ function exportApiKey(apiKey, displayName, deferOutro) {
51
51
  }
52
52
  return true;
53
53
  }
54
+ /** A bounded spawn's timeout means the sign-in never finished. */
55
+ function signInStepError(message, result, bounded) {
56
+ const error = spawnStepError(message, result);
57
+ return bounded && error.failureReason === "timeout"
58
+ ? new SignInTimeoutError(error.message, error.cause)
59
+ : error;
60
+ }
54
61
  async function connectNativeMcpClient(client, options) {
55
62
  const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
56
63
  const state = { stage: "prerequisite_check", settled: false };
@@ -83,7 +90,7 @@ async function connectNativeMcpClient(client, options) {
83
90
  telemetry.track("checkpoint", { phase: "prerequisite_ok" });
84
91
  state.stage = "registration_cleanup";
85
92
  for (const removal of client.removals)
86
- removeExistingRegistration(client, removal);
93
+ removeExistingRegistration(client, removal, options.verbose ?? false);
87
94
  const mcpUrl = new URL(options.mcpUrl);
88
95
  mcpUrl.searchParams.set("source", CONNECT_SOURCE);
89
96
  mcpUrl.searchParams.set("client", client.connectClient);
@@ -100,12 +107,13 @@ async function connectNativeMcpClient(client, options) {
100
107
  ? keyedAdd.spec.addArgs(mcpUrl.toString(), keyedAdd.key)
101
108
  : client.addArgs(mcpUrl.toString(), options.mcpUrl);
102
109
  // Capturing an inline-OAuth add hides the sign-in URL and hangs.
103
- const addResult = spawn.sync(client.command, addArgs, client.nonInteractiveAdd || keyedAdd
104
- ? captureStdio(options.verbose ?? false)
105
- : { stdio: "inherit" });
110
+ const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
111
+ const addResult = spawn.sync(client.command, addArgs, interactiveAdd
112
+ ? { stdio: "inherit", timeout: options.authTimeoutMs }
113
+ : captureStdio(options.verbose ?? false));
106
114
  if (addResult.error || addResult.status !== 0) {
107
115
  // Built first: its interrupt check must run before any replay.
108
- const error = spawnStepError(`Could not add TinyFish to ${client.displayName}`, addResult);
116
+ const error = signInStepError(`Could not add TinyFish to ${client.displayName}`, addResult, interactiveAdd && options.authTimeoutMs !== undefined);
109
117
  replay(capturedOutput(addResult));
110
118
  throw error;
111
119
  }
@@ -130,9 +138,12 @@ async function connectNativeMcpClient(client, options) {
130
138
  if (pendingLogin) {
131
139
  state.stage = "client_oauth";
132
140
  errLine("Signing in to TinyFish...");
133
- const loginResult = spawn.sync(client.command, pendingLogin, { stdio: "inherit" });
141
+ const loginResult = spawn.sync(client.command, pendingLogin, {
142
+ stdio: "inherit",
143
+ timeout: options.authTimeoutMs,
144
+ });
134
145
  if (loginResult.error || loginResult.status !== 0) {
135
- throw spawnStepError(`Could not authenticate TinyFish in ${client.displayName}`, loginResult);
146
+ throw signInStepError(`Could not authenticate TinyFish in ${client.displayName}`, loginResult, options.authTimeoutMs !== undefined);
136
147
  }
137
148
  if (probeAuthenticated?.() === false)
138
149
  signInDeferred = true;
@@ -190,7 +201,10 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
190
201
  // Before auth: a failed sign-in must not lose the harness record.
191
202
  saveConnectContext(client.connectClient, telemetry.attemptId);
192
203
  attempt("authentication", () => {
193
- ensureCliAuthenticated(client.connectClient, options.apiKey);
204
+ // A set timeout means headless; never shell an interactive login.
205
+ ensureCliAuthenticated(client.connectClient, options.apiKey, {
206
+ interactiveLogin: options.authTimeoutMs === undefined,
207
+ });
194
208
  telemetry.track("checkpoint", { phase: "authenticated" });
195
209
  });
196
210
  if (client.postConnectNote)
@@ -6,7 +6,7 @@ import { TINYFISH_API_KEY_VAR } from "./constants.js";
6
6
  import { installTinyFishCli, } from "./cli-install.js";
7
7
  import { NATIVE_MCP_CLIENTS } from "./connect-clients.js";
8
8
  import { ensureCliAuthenticated, TINYFISH_CLI_NOT_FOUND_MESSAGE } from "./connect-auth.js";
9
- import { commandNotFound, ConnectInterruptedError, ConnectStepError, finishSetupCommand, throwIfInterrupted, } from "./connect-runtime.js";
9
+ import { commandNotFound, ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, throwIfInterrupted, } from "./connect-runtime.js";
10
10
  import { pickHarnesses } from "./connect-picker.js";
11
11
  import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISPLAY_NAMES as DISPLAY_NAMES, ALL_HARNESSES, } from "./harness-detect.js";
12
12
  import { detectHumanInitiated } from "./harness.js";
@@ -23,8 +23,21 @@ const TOO_OLD_REASONS = new Set([
23
23
  "harness_too_old",
24
24
  "harness_command_unsupported",
25
25
  ]);
26
+ // These failures land before any browser could have opened.
27
+ const NO_BROWSER_REASONS = new Set([
28
+ ...TOO_OLD_REASONS,
29
+ "harness_not_installed",
30
+ "command_not_found",
31
+ ]);
26
32
  const AUTH_CHECK_CAPABLE = new Set(["cursor", "openclaw"]);
27
33
  const STEP_INDENT = " ";
34
+ // Must undercut agent tool-call budgets (Claude Code Bash defaults to 120s).
35
+ const OAUTH_SIGN_IN_TIMEOUT_MS = 90_000;
36
+ // Derived: these block `connect` on a browser sign-in when keyless.
37
+ const BROWSER_SIGN_IN = new Set(NATIVE_MCP_CLIENTS.filter((client) => client.loginArgs || client.oauthInAdd)
38
+ .map((client) => client.connectClient)
39
+ .filter((client) => ALL_HARNESSES.includes(client))
40
+ .map((client) => client));
28
41
  // Descriptor-derived so a harness that cannot carry a key is skipped, not failed, when headless.
29
42
  // Cursor and OpenClaw hold the key themselves rather than handing it to a harness `mcp add`.
30
43
  const KEY_AUTH_CAPABLE = new Set([
@@ -46,12 +59,13 @@ const UNINSTALL_POINTERS = {
46
59
  // OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
47
60
  opencode: "remove the tinyfish entry from ~/.config/opencode/opencode.json",
48
61
  };
49
- async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall, onPostInstallFailed, deferOutro) {
62
+ async function runHarnessConnect(harness, opts, keyAuthOnly, authTimeoutMs, hoistedCliInstall, onPostInstallFailed, deferOutro) {
50
63
  const base = {
51
64
  apiKey: opts.apiKey,
52
65
  mcpUrl: opts.mcpUrl,
53
66
  launch: false,
54
67
  keyAuthOnly,
68
+ authTimeoutMs,
55
69
  hoistedCliInstall,
56
70
  onPostInstallFailed,
57
71
  verbose: opts.verbose,
@@ -147,7 +161,7 @@ function uninstallHarness(harness, configPath) {
147
161
  }
148
162
  return { ...detection, outcome: "uninstalled" };
149
163
  }
150
- async function processHarness(detection, opts, prompt, hoistedCliInstall, deferOutro) {
164
+ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliInstall, deferOutro) {
151
165
  const { harness, configPath, detected } = detection;
152
166
  const base = { harness, detected, configPath, installed: false };
153
167
  if (!detected)
@@ -168,13 +182,23 @@ async function processHarness(detection, opts, prompt, hoistedCliInstall, deferO
168
182
  const apiKey = validatedApiKey(opts.apiKey);
169
183
  // A key only leaves the own-OAuth set where the harness can actually carry one.
170
184
  const keyed = !!apiKey && KEY_AUTH_CAPABLE.has(harness);
185
+ const fixCommand = `tinyfish connect ${harness}`;
186
+ let headlessAuth = false;
187
+ let spentBrowserSlot = false;
171
188
  if (!isTTY && !keyed) {
172
- return {
173
- ...base,
174
- outcome: "no_tty_auth_skip",
175
- keyUnusable: !!apiKey,
176
- fixCommand: `tinyfish connect ${harness}`,
177
- };
189
+ // OpenClaw signs in through the CLI's own interactive login — unboundable.
190
+ if (harness === "openclaw")
191
+ return { ...base, outcome: "auth_pending", fixCommand };
192
+ if (BROWSER_SIGN_IN.has(harness)) {
193
+ // One browser sign-in per run; the rest wait for a re-run.
194
+ if (oauthBudget.spent)
195
+ return { ...base, outcome: "auth_pending", fixCommand };
196
+ oauthBudget.spent = true;
197
+ spentBrowserSlot = true;
198
+ errLine(`${DISPLAY_NAMES[harness]} needs a browser sign-in — finish it when your browser ` +
199
+ `opens; waiting up to ${OAUTH_SIGN_IN_TIMEOUT_MS / 1000} seconds.`);
200
+ }
201
+ headlessAuth = true;
178
202
  }
179
203
  if (isTTY && !keyed && prompt) {
180
204
  errLine(`step: waiting for browser sign-in to TinyFish for ${DISPLAY_NAMES[harness]}`);
@@ -191,12 +215,24 @@ async function processHarness(detection, opts, prompt, hoistedCliInstall, deferO
191
215
  let authMode;
192
216
  let postInstallFailed = false;
193
217
  try {
194
- // Headless: an install too old for key auth must fail, not open a browser.
195
- authMode = await runHarnessConnect(harness, opts, !isTTY, hoistedCliInstall, () => {
218
+ // A keyed install too old for key auth fails, no browser.
219
+ authMode = await runHarnessConnect(harness, opts, keyed && !isTTY, headlessAuth ? OAUTH_SIGN_IN_TIMEOUT_MS : undefined, hoistedCliInstall, () => {
196
220
  postInstallFailed = true;
197
221
  }, deferOutro);
198
222
  }
199
223
  catch (e) {
224
+ if (e instanceof SignInTimeoutError) {
225
+ return {
226
+ ...base,
227
+ outcome: "auth_pending",
228
+ errorMessage: "sign-in did not finish in time",
229
+ fixCommand,
230
+ };
231
+ }
232
+ // A pre-browser failure must not burn the run's only browser slot.
233
+ if (spentBrowserSlot && e instanceof ConnectStepError && NO_BROWSER_REASONS.has(e.failureReason)) {
234
+ oauthBudget.spent = false;
235
+ }
200
236
  // Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
201
237
  if (e instanceof ConnectStepError && TOO_OLD_REASONS.has(e.failureReason)) {
202
238
  return {
@@ -248,10 +284,12 @@ function summaryLine(result, uninstall) {
248
284
  return `… ${name} — ${result.fixCommand}`;
249
285
  }
250
286
  if (result.outcome === "no_tty_auth_skip") {
251
- const why = result.keyUnusable
252
- ? `${name} cannot take an API key and there is no terminal to sign in`
253
- : "no stored credentials and no terminal to sign in";
254
- return `… ${name} — skipped, ${why}. Fix: ${result.fixCommand}`;
287
+ return `… ${name} — skipped, no stored credentials and no terminal to sign in. Fix: ${result.fixCommand}`;
288
+ }
289
+ if (result.outcome === "auth_pending") {
290
+ return result.errorMessage
291
+ ? `… ${name} — ${result.errorMessage}. Finish it in a terminal: ${result.fixCommand}`
292
+ : `… ${name} — needs a browser sign-in. Run in a terminal: ${result.fixCommand}`;
255
293
  }
256
294
  if (result.outcome === "deselected") {
257
295
  return `… ${name} — skipped, not selected. Fix: ${result.fixCommand}`;
@@ -405,6 +443,11 @@ export async function runConnectAll(opts) {
405
443
  }
406
444
  : undefined;
407
445
  const detections = detectInstalledHarnesses();
446
+ const gateKey = validatedApiKey(opts.apiKey);
447
+ // Keyed installs first: working tools land before sign-ins can block.
448
+ if (gateKey) {
449
+ detections.sort((a, b) => Number(KEY_AUTH_CAPABLE.has(b.harness)) - Number(KEY_AUTH_CAPABLE.has(a.harness)));
450
+ }
408
451
  let deselected = new Set();
409
452
  let mode = "all";
410
453
  // Before willAttempt/cliNeeded: a cursor-only pick must not hoist the CLI.
@@ -422,9 +465,9 @@ export async function runConnectAll(opts) {
422
465
  const chosen = new Set(pick.selection);
423
466
  deselected = new Set(offerable.filter((d) => !chosen.has(d.harness)).map((d) => d.harness));
424
467
  }
425
- const gateKey = validatedApiKey(opts.apiKey);
426
- // Mirrors processHarness's skip gate: a skipped harness installs nothing today.
427
- const willAttempt = (harness) => !deselected.has(harness) && (isTTY || (!!gateKey && KEY_AUTH_CAPABLE.has(harness)));
468
+ // Mirrors processHarness's gate: headless, only keyless OpenClaw skips outright.
469
+ const willAttempt = (harness) => !deselected.has(harness) &&
470
+ (isTTY || harness !== "openclaw" || (!!gateKey && KEY_AUTH_CAPABLE.has(harness)));
428
471
  // Machine-global, so once per invocation; cursor's connect never installs it.
429
472
  const cliNeeded = !opts.dryRun &&
430
473
  !opts.uninstall &&
@@ -448,6 +491,7 @@ export async function runConnectAll(opts) {
448
491
  // A Set dedupes: Codex and Grok defer the same export line.
449
492
  const outro = new Set();
450
493
  const results = [];
494
+ const oauthBudget = { spent: false };
451
495
  const stepTotal = detections.length - deselected.size;
452
496
  let step = 0;
453
497
  for (const detection of detections) {
@@ -466,7 +510,7 @@ export async function runConnectAll(opts) {
466
510
  let result;
467
511
  try {
468
512
  setErrIndent(STEP_INDENT);
469
- result = await processHarness(detection, opts, prompt, takeHoistedCli, (line) => outro.add(line));
513
+ result = await processHarness(detection, opts, prompt, oauthBudget, takeHoistedCli, (line) => outro.add(line));
470
514
  }
471
515
  catch (e) {
472
516
  // Isolation: one harness throwing never stops the others.
@@ -1,3 +1,5 @@
1
1
  import { type AgentClient } from "./connect-runtime.js";
2
2
  export declare const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
3
- export declare function ensureCliAuthenticated(source: AgentClient | "cli", apiKey?: string): void;
3
+ export declare function ensureCliAuthenticated(source: AgentClient | "cli", apiKey?: string, opts?: {
4
+ interactiveLogin?: boolean;
5
+ }): void;
@@ -4,7 +4,7 @@ import { persistApiKeyToEnvironment, validateKeyFormat, writeConfig } from "./au
4
4
  import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "./connect-runtime.js";
5
5
  import { errLine } from "./output.js";
6
6
  export const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
7
- export function ensureCliAuthenticated(source, apiKey) {
7
+ export function ensureCliAuthenticated(source, apiKey, opts) {
8
8
  const envKey = apiKey ?? process.env[TINYFISH_API_KEY_VAR];
9
9
  if (envKey) {
10
10
  if (!validateKeyFormat(envKey)) {
@@ -30,6 +30,9 @@ export function ensureCliAuthenticated(source, apiKey) {
30
30
  if (authStatus.error) {
31
31
  throw spawnStepError("Could not read TinyFish CLI auth status", authStatus);
32
32
  }
33
+ if (opts?.interactiveLogin === false) {
34
+ throw new Error("The TinyFish CLI is not signed in. Run: tinyfish auth login");
35
+ }
33
36
  errLine("Signing in to the TinyFish CLI...");
34
37
  const loginResult = spawn.sync("tinyfish", ["auth", "login", "--source", source], {
35
38
  stdio: "inherit",
@@ -25,6 +25,8 @@ export interface NativeMcpClient extends SupportedCommand {
25
25
  /** Shown when a keyless install leaves sign-in to the harness. */
26
26
  signInHint?: string;
27
27
  loginArgs?: string[];
28
+ /** Keyless `mcp add` itself blocks on the browser OAuth. */
29
+ oauthInAdd?: boolean;
28
30
  /** Replaces the `loginArgs` step when `supportCheck.optionalPatterns` miss. */
29
31
  degradedAuthNotes?: {
30
32
  keyed: string;
@@ -228,6 +228,7 @@ export const HERMES = {
228
228
  // No `keyAuth`: `hermes mcp add` has no --header, and `--auth header` prompts interactively.
229
229
  removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Hermes" }],
230
230
  // Hermes completes OAuth while adding the server; a separate `mcp login` would authenticate twice.
231
+ oauthInAdd: true,
231
232
  addArgs: (mcpUrl) => [...urlAddArgs(mcpUrl), "--auth", "oauth"],
232
233
  launchWalkthrough: launchHermesWalkthrough,
233
234
  };
@@ -19,11 +19,15 @@ export declare class ConnectStepError extends Error {
19
19
  cause?: unknown;
20
20
  });
21
21
  }
22
+ /** A bounded sign-in wait elapsed — unfinished, not broken. */
23
+ export declare class SignInTimeoutError extends ConnectStepError {
24
+ constructor(message: string, cause?: unknown);
25
+ }
22
26
  export declare function throwIfInterrupted(result: {
23
27
  signal?: string | null;
24
28
  error?: Error;
25
29
  }): void;
26
- interface SpawnStepResult {
30
+ export interface SpawnStepResult {
27
31
  status?: number | null;
28
32
  signal?: string | null;
29
33
  error?: Error;
@@ -37,6 +37,12 @@ export class ConnectStepError extends Error {
37
37
  }
38
38
  class PrerequisiteError extends ConnectStepError {
39
39
  }
40
+ /** A bounded sign-in wait elapsed — unfinished, not broken. */
41
+ export class SignInTimeoutError extends ConnectStepError {
42
+ constructor(message, cause) {
43
+ super(message, "timeout", { cause });
44
+ }
45
+ }
40
46
  // A maxBuffer overflow is also killed with SIGTERM, and is a failure too.
41
47
  const NOT_INTERRUPTIONS = new Set(["ETIMEDOUT", "ENOBUFS"]);
42
48
  export function throwIfInterrupted(result) {
@@ -107,6 +113,11 @@ export async function runGuarded(state, telemetry, body) {
107
113
  process.exitCode = 130;
108
114
  return;
109
115
  }
116
+ // An elapsed sign-in wait is abandonment, not breakage.
117
+ if (error instanceof SignInTimeoutError) {
118
+ settle(state, telemetry, "aborted", { failedStage: state.stage, failureReason: "timeout" });
119
+ throw error;
120
+ }
110
121
  settle(state, telemetry, "failed", {
111
122
  failedStage: state.stage,
112
123
  failureReason: error instanceof ConnectStepError ? error.failureReason : "unexpected_error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {