@tiny-fish/cli 0.29.1-next.237 → 0.29.1-next.240

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.
@@ -2,7 +2,7 @@ import { Command } from "commander";
2
2
  import { type TakeHoistedCliInstall } from "../lib/cli-install.js";
3
3
  import { type AgentClient, type ConnectAuthMode } from "../lib/connect-runtime.js";
4
4
  export declare const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
5
- interface NativeConnectOptions {
5
+ export interface NativeConnectOptions {
6
6
  apiKey?: string;
7
7
  mcpUrl: string;
8
8
  launch: boolean;
@@ -48,4 +48,3 @@ export declare function connectCursor(options: {
48
48
  }): Promise<void>;
49
49
  export declare function launchAgent(client: AgentClient): void;
50
50
  export declare function registerConnect(program: Command): void;
51
- export {};
@@ -446,34 +446,38 @@ function launchCursorDeeplink(mcpUrl) {
446
446
  const result = openExternalUrl(cursorInstallDeeplink(mcpUrl));
447
447
  return !result.error && result.status === 0;
448
448
  }
449
- export function launchAgent(client) {
450
- if (client === "claude-code") {
451
- launchNativeMcpClient(CLAUDE_CODE);
452
- }
453
- else if (client === "codex") {
454
- launchNativeMcpClient(CODEX);
455
- }
456
- else if (client === "hermes") {
457
- launchNativeMcpClient(HERMES);
458
- }
459
- else if (client === "opencode") {
460
- launchNativeMcpClient(OPENCODE);
461
- }
462
- else if (client === "grok") {
463
- launchNativeMcpClient(GROK);
464
- }
465
- else if (client === "openclaw") {
466
- launchOpenClawWalkthrough();
467
- }
468
- else if (client === "cursor") {
469
- // Cursor has no chat CLI to launch.
449
+ // A new client fails to compile until wired here.
450
+ const AGENT_LAUNCHERS = {
451
+ "claude-code": () => launchNativeMcpClient(CLAUDE_CODE),
452
+ codex: () => launchNativeMcpClient(CODEX),
453
+ grok: () => launchNativeMcpClient(GROK),
454
+ hermes: () => launchNativeMcpClient(HERMES),
455
+ opencode: () => launchNativeMcpClient(OPENCODE),
456
+ openclaw: launchOpenClawWalkthrough,
457
+ // Cursor has no chat CLI to launch.
458
+ cursor: () => {
470
459
  throw new Error("Cursor has no launchable walkthrough. Open Cursor and start chatting.");
471
- }
472
- else {
473
- const unhandled = client;
474
- throw new Error(`No launch support for client: ${String(unhandled)}`);
475
- }
460
+ },
461
+ };
462
+ export function launchAgent(client) {
463
+ AGENT_LAUNCHERS[client]();
476
464
  }
465
+ // A new client fails to compile until wired here.
466
+ const CLIENT_CONNECTORS = {
467
+ "claude-code": connectClaudeCode,
468
+ codex: connectCodex,
469
+ grok: connectGrok,
470
+ hermes: connectHermes,
471
+ opencode: connectOpencode,
472
+ openclaw: connectOpenClaw,
473
+ // `connect cursor --launch` still connects; it just cannot launch after.
474
+ cursor: async (options) => {
475
+ await connectCursor(options);
476
+ if (options.launch) {
477
+ errLine("Cursor has no launchable walkthrough. Open Cursor and start chatting.");
478
+ }
479
+ },
480
+ };
477
481
  export function registerConnect(program) {
478
482
  program
479
483
  .command("connect")
@@ -568,31 +572,9 @@ export function registerConnect(program) {
568
572
  keyAuthOnly: !detectHumanInitiated() && !!validatedApiKey(options.apiKey),
569
573
  verbose: options.verbose ?? false,
570
574
  };
571
- if (client === "claude-code") {
572
- await connectClaudeCode(connectOptions);
573
- }
574
- else if (client === "codex") {
575
- await connectCodex(connectOptions);
576
- }
577
- else if (client === "grok") {
578
- await connectGrok(connectOptions);
579
- }
580
- else if (client === "hermes") {
581
- await connectHermes(connectOptions);
582
- }
583
- else if (client === "opencode") {
584
- await connectOpencode(connectOptions);
585
- }
586
- else if (client === "openclaw") {
587
- await connectOpenClaw(connectOptions);
588
- }
589
- else if (client === "cursor") {
590
- // `connect <client> --launch` is the shape every doc uses; connect anyway, then say
591
- // why nothing launched rather than throwing away a working install.
592
- await connectCursor(connectOptions);
593
- if (connectOptions.launch) {
594
- errLine("Cursor has no launchable walkthrough. Open Cursor and start chatting.");
595
- }
575
+ const connector = CLIENT_CONNECTORS[client];
576
+ if (connector) {
577
+ await connector(connectOptions);
596
578
  }
597
579
  else {
598
580
  throw new Error("Unsupported client: " +
@@ -4,7 +4,7 @@ import { connectClaudeCode, connectCodex, connectCursor, connectGrok, connectHer
4
4
  import { loadConfig, validatedApiKey } from "./auth.js";
5
5
  import { TINYFISH_API_KEY_VAR } from "./constants.js";
6
6
  import { installTinyFishCli, } from "./cli-install.js";
7
- import { NATIVE_MCP_CLIENTS } from "./connect-clients.js";
7
+ import { HARNESS_CAPABILITIES, NATIVE_BY_HARNESS } from "./connect-clients.js";
8
8
  import { ensureCliAuthenticated, TINYFISH_CLI_NOT_FOUND_MESSAGE } from "./connect-auth.js";
9
9
  import { commandNotFound, ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, throwIfInterrupted, } from "./connect-runtime.js";
10
10
  import { pickHarnesses } from "./connect-picker.js";
@@ -15,9 +15,6 @@ import { removeCursorMcpServer, planCursorWrite } from "./cursor-config.js";
15
15
  import { errLine, outLine, setErrIndent } from "./output.js";
16
16
  import { sendSetupCompleted } from "./setup-telemetry.js";
17
17
  import { verifyMcpAuth, verifyMcpHealth } from "./verify.js";
18
- // `codex mcp add` succeeds unauthenticated; OAuth lands at first tool use.
19
- // TODO(PF-3580): connect now probes Codex sign-in, so this over-warns a signed-in install.
20
- const AUTH_DEFERRED_AT_INSTALL = new Set(["codex"]);
21
18
  // A probe that exits non-zero is an install so old the subcommand is missing entirely.
22
19
  const TOO_OLD_REASONS = new Set([
23
20
  "harness_too_old",
@@ -29,26 +26,16 @@ const NO_BROWSER_REASONS = new Set([
29
26
  "harness_not_installed",
30
27
  "command_not_found",
31
28
  ]);
32
- const AUTH_CHECK_CAPABLE = new Set(["cursor", "openclaw"]);
33
29
  const STEP_INDENT = " ";
34
30
  // Must undercut agent tool-call budgets (Claude Code Bash defaults to 120s).
35
31
  const OAUTH_SIGN_IN_TIMEOUT_MS = 90_000;
36
32
  // 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));
41
- // Descriptor-derived so a harness that cannot carry a key is skipped, not failed, when headless.
42
- // Cursor and OpenClaw hold the key themselves rather than handing it to a harness `mcp add`.
43
- const KEY_AUTH_CAPABLE = new Set([
44
- ...NATIVE_MCP_CLIENTS.filter((client) => !!client.keyAuth)
45
- .map((client) => client.connectClient)
46
- .filter((client) => ALL_HARNESSES.includes(client))
47
- // Second map only narrows connectClient to Harness; the filter above is the real gate.
48
- .map((client) => client),
49
- "cursor",
50
- "openclaw",
51
- ]);
33
+ const BROWSER_SIGN_IN = new Set(ALL_HARNESSES.filter((harness) => {
34
+ const native = NATIVE_BY_HARNESS.get(harness);
35
+ return !!native?.loginArgs || !!native?.oauthInAdd;
36
+ }));
37
+ // Derived: a native add carries the key, or the CLI holds it itself.
38
+ const KEY_AUTH_CAPABLE = new Set(ALL_HARNESSES.filter((harness) => !!NATIVE_BY_HARNESS.get(harness)?.keyAuth || HARNESS_CAPABILITIES[harness].keyHeldByCli));
52
39
  // Real removal commands — harness id is not the binary name.
53
40
  const UNINSTALL_POINTERS = {
54
41
  "claude-code": "claude mcp remove tinyfish --scope user",
@@ -59,8 +46,25 @@ const UNINSTALL_POINTERS = {
59
46
  // OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
60
47
  opencode: "remove the tinyfish entry from ~/.config/opencode/opencode.json",
61
48
  };
49
+ // A new harness fails to compile until wired here.
50
+ const HARNESS_CONNECTORS = {
51
+ "claude-code": connectClaudeCode,
52
+ codex: connectCodex,
53
+ grok: connectGrok,
54
+ hermes: connectHermes,
55
+ opencode: connectOpencode,
56
+ // Without the key the child shells an interactive `tinyfish auth login`.
57
+ openclaw: async (base) => {
58
+ await connectOpenClaw(base);
59
+ return undefined;
60
+ },
61
+ cursor: async (base) => {
62
+ await connectCursor({ apiKey: base.apiKey, mcpUrl: base.mcpUrl, verbose: base.verbose });
63
+ return undefined;
64
+ },
65
+ };
62
66
  async function runHarnessConnect(harness, opts, keyAuthOnly, authTimeoutMs, hoistedCliInstall, onPostInstallFailed, deferOutro) {
63
- const base = {
67
+ return HARNESS_CONNECTORS[harness]({
64
68
  apiKey: opts.apiKey,
65
69
  mcpUrl: opts.mcpUrl,
66
70
  launch: false,
@@ -70,35 +74,7 @@ async function runHarnessConnect(harness, opts, keyAuthOnly, authTimeoutMs, hois
70
74
  onPostInstallFailed,
71
75
  verbose: opts.verbose,
72
76
  deferOutro,
73
- };
74
- if (harness === "claude-code")
75
- return connectClaudeCode(base);
76
- if (harness === "codex")
77
- return connectCodex(base);
78
- if (harness === "hermes")
79
- return connectHermes(base);
80
- if (harness === "openclaw") {
81
- // Without the key the child shells an interactive `tinyfish auth login`.
82
- await connectOpenClaw({
83
- apiKey: opts.apiKey,
84
- mcpUrl: opts.mcpUrl,
85
- launch: false,
86
- hoistedCliInstall,
87
- verbose: opts.verbose,
88
- deferOutro,
89
- });
90
- return undefined;
91
- }
92
- if (harness === "opencode")
93
- return connectOpencode(base);
94
- if (harness === "grok")
95
- return connectGrok(base);
96
- if (harness === "cursor") {
97
- await connectCursor({ apiKey: opts.apiKey, mcpUrl: opts.mcpUrl, verbose: opts.verbose });
98
- return undefined;
99
- }
100
- // Explicit: a new harness reaching an untested branch must fail, not write Cursor's config.
101
- throw new Error(`No installer is wired for ${harness}`);
77
+ });
102
78
  }
103
79
  /** Undefined means Ctrl+C: the caller stops the whole run. */
104
80
  function runHoistedCliInstall(verbose) {
@@ -117,7 +93,7 @@ function runHoistedCliInstall(verbose) {
117
93
  async function verifyHarness(harness, mcpUrl, keyed, apiKey) {
118
94
  const health = await verifyMcpHealth(mcpUrl);
119
95
  // The own-OAuth harnesses are auth-checkable only in key mode (key-authed config).
120
- const capable = AUTH_CHECK_CAPABLE.has(harness) || keyed;
96
+ const capable = HARNESS_CAPABILITIES[harness].canVerifyAuth || keyed;
121
97
  if (!capable || !health.ok)
122
98
  return health;
123
99
  if (!apiKey)
@@ -186,9 +162,10 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
186
162
  let headlessAuth = false;
187
163
  let spentBrowserSlot = false;
188
164
  if (!isTTY && !keyed) {
189
- // OpenClaw signs in through the CLI's own interactive login — unboundable.
190
- if (harness === "openclaw")
165
+ // A CLI-login sign-in is interactive-only — unboundable headless.
166
+ if (HARNESS_CAPABILITIES[harness].signInViaCliLogin) {
191
167
  return { ...base, outcome: "auth_pending", fixCommand };
168
+ }
192
169
  if (BROWSER_SIGN_IN.has(harness)) {
193
170
  // One browser sign-in per run; the rest wait for a re-run.
194
171
  if (oauthBudget.spent)
@@ -233,6 +210,10 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
233
210
  if (spentBrowserSlot && e instanceof ConnectStepError && NO_BROWSER_REASONS.has(e.failureReason)) {
234
211
  oauthBudget.spent = false;
235
212
  }
213
+ // Detection saw a name on PATH; the probe proved nothing is behind it.
214
+ if (e instanceof ConnectStepError && e.failureReason === "harness_not_installed") {
215
+ return { ...base, detected: false, outcome: "not_detected" };
216
+ }
236
217
  // Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
237
218
  if (e instanceof ConnectStepError && TOO_OLD_REASONS.has(e.failureReason)) {
238
219
  return {
@@ -291,9 +272,6 @@ function summaryLine(result, uninstall) {
291
272
  ? `… ${name} — ${result.errorMessage}. Finish it in a terminal: ${result.fixCommand}`
292
273
  : `… ${name} — needs a browser sign-in. Run in a terminal: ${result.fixCommand}`;
293
274
  }
294
- if (result.outcome === "deselected") {
295
- return `… ${name} — skipped, not selected. Fix: ${result.fixCommand}`;
296
- }
297
275
  if (result.outcome === "uninstall_pointer") {
298
276
  return `… ${name} — not removed automatically (config is owned by ${name}). Run: ${result.fixCommand}`;
299
277
  }
@@ -320,7 +298,7 @@ function summaryLine(result, uninstall) {
320
298
  `Fix: ${finishSetupCommand(result.harness)}`);
321
299
  }
322
300
  // A keyed install has no sign-in left, so the deferred-auth warning would contradict it.
323
- const authDeferred = AUTH_DEFERRED_AT_INSTALL.has(result.harness) && !result.keyAuthed;
301
+ const authDeferred = HARNESS_CAPABILITIES[result.harness].authDeferredAtInstall && !result.keyAuthed;
324
302
  const verifyLabel = result.verifyDepth ? `verified: ${result.verifyDepth}` : "not verified";
325
303
  if (authDeferred || result.verifyOk === false) {
326
304
  const reason = authDeferred
@@ -330,10 +308,25 @@ function summaryLine(result, uninstall) {
330
308
  }
331
309
  return `✓ ${name} — installed, ${verifyLabel}`;
332
310
  }
333
- /** Cursor's connect writes its config file; every other harness spawns its binary. */
334
- // TODO(PF-3668): 4th cursor carve-out here; fold them into one harness capability table.
311
+ /** Absent and unpicked harnesses are noise once something real happened. */
312
+ function renderSummary(results, uninstall) {
313
+ if (!results.some(actionable))
314
+ return results.map((r) => summaryLine(r, uninstall));
315
+ // Ids, not display names: they double as the `connect` argument.
316
+ const skipped = results.filter((r) => r.outcome === "deselected").map((r) => r.harness);
317
+ const lines = results
318
+ .filter((r) => r.outcome !== "deselected" && r.outcome !== "not_detected")
319
+ .map((r) => summaryLine(r, uninstall));
320
+ if (skipped.length === 0)
321
+ return lines;
322
+ lines.push("", `Skipped (not selected): ${skipped.join(", ")}`);
323
+ return lines;
324
+ }
325
+ /** A config-writing connect needs no binary; every other harness spawns one. */
335
326
  function isStaleConfig(detection) {
336
- return detection.detected && !detection.binaryOnPath && detection.harness !== "cursor";
327
+ return (detection.detected &&
328
+ !detection.binaryOnPath &&
329
+ !HARNESS_CAPABILITIES[detection.harness].cliWritesConfig);
337
330
  }
338
331
  /** A stale config dir is a detection signal with nothing behind it. */
339
332
  function actionable(result) {
@@ -465,13 +458,18 @@ export async function runConnectAll(opts) {
465
458
  const chosen = new Set(pick.selection);
466
459
  deselected = new Set(offerable.filter((d) => !chosen.has(d.harness)).map((d) => d.harness));
467
460
  }
468
- // Mirrors processHarness's gate: headless, only keyless OpenClaw skips outright.
461
+ // Mirrors processHarness's gate: headless, only keyless CLI-login harnesses skip.
469
462
  const willAttempt = (harness) => !deselected.has(harness) &&
470
- (isTTY || harness !== "openclaw" || (!!gateKey && KEY_AUTH_CAPABLE.has(harness)));
471
- // Machine-global, so once per invocation; cursor's connect never installs it.
463
+ (isTTY ||
464
+ !HARNESS_CAPABILITIES[harness].signInViaCliLogin ||
465
+ (!!gateKey && KEY_AUTH_CAPABLE.has(harness)));
466
+ // Machine-global, so once per invocation; config-writing connects never install it.
472
467
  const cliNeeded = !opts.dryRun &&
473
468
  !opts.uninstall &&
474
- detections.some((d) => d.detected && d.binaryOnPath && d.harness !== "cursor" && willAttempt(d.harness));
469
+ detections.some((d) => d.detected &&
470
+ d.binaryOnPath &&
471
+ !HARNESS_CAPABILITIES[d.harness].cliWritesConfig &&
472
+ willAttempt(d.harness));
475
473
  let takeHoistedCli;
476
474
  if (cliNeeded) {
477
475
  errLine("First: the TinyFish CLI, shared by every harness.");
@@ -492,7 +490,9 @@ export async function runConnectAll(opts) {
492
490
  const outro = new Set();
493
491
  const results = [];
494
492
  const oauthBudget = { spent: false };
495
- const stepTotal = detections.length - deselected.size;
493
+ // Mirrors processHarness's own short-circuits: absent has no work, stale only when removing.
494
+ const stepped = (d) => !deselected.has(d.harness) && d.detected && (opts.uninstall || !isStaleConfig(d));
495
+ const stepTotal = detections.filter(stepped).length;
496
496
  let step = 0;
497
497
  for (const detection of detections) {
498
498
  const { harness } = detection;
@@ -505,6 +505,9 @@ export async function runConnectAll(opts) {
505
505
  });
506
506
  continue;
507
507
  }
508
+ // The pad loop below records these; announcing them is the noise this command had.
509
+ if (!stepped(detection))
510
+ continue;
508
511
  step += 1;
509
512
  errLine(`step ${step} of ${stepTotal} — ${DISPLAY_NAMES[harness]}`);
510
513
  let result;
@@ -550,8 +553,8 @@ export async function runConnectAll(opts) {
550
553
  }
551
554
  outLine("");
552
555
  outLine("TinyFish setup summary:");
553
- for (const result of results)
554
- outLine(summaryLine(result, opts.uninstall));
556
+ for (const line of renderSummary(results, opts.uninstall))
557
+ outLine(line);
555
558
  // Before the launch handover: anything printed after it is never seen.
556
559
  for (const line of outro)
557
560
  errLine(line);
@@ -1,4 +1,5 @@
1
1
  import spawn from "cross-spawn";
2
+ import type { Harness } from "./harness-detect.js";
2
3
  import { type AgentClient, type SupportedCommand } from "./connect-runtime.js";
3
4
  export declare const OPENCLAW_SKILL_INSTALL_ARGS: readonly string[];
4
5
  export declare const DEFAULT_ONBOARDING_PROMPT: string;
@@ -54,6 +55,23 @@ export declare const HERMES: NativeMcpClient;
54
55
  export declare const OPENCODE: NativeMcpClient;
55
56
  /** Every native MCP client. Derive from this, so a new harness cannot be missed. */
56
57
  export declare const NATIVE_MCP_CLIENTS: readonly NativeMcpClient[];
58
+ /** Native descriptor by harness id; Cursor and OpenClaw have none. */
59
+ export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code", NativeMcpClient>;
57
60
  export declare const OPENCLAW: SupportedCommand;
61
+ /** Traits with no native descriptor to live on (Cursor and OpenClaw included). */
62
+ export interface HarnessCapabilities {
63
+ /** Sign-in is the CLI's own interactive `tinyfish auth login`. */
64
+ signInViaCliLogin: boolean;
65
+ /** Stored-key auth verification works against this harness's config. */
66
+ canVerifyAuth: boolean;
67
+ /** The CLI holds or writes the key; no harness `mcp add` carries it. */
68
+ keyHeldByCli: boolean;
69
+ /** Connect writes the MCP config file; no harness binary is spawned. */
70
+ cliWritesConfig: boolean;
71
+ /** `mcp add` succeeds unauthenticated; OAuth lands at first tool use. */
72
+ authDeferredAtInstall: boolean;
73
+ }
74
+ /** Required booleans: a new harness or new flag fails to compile undecided. */
75
+ export declare const HARNESS_CAPABILITIES: Record<Harness, HarnessCapabilities>;
58
76
  export declare function launchNativeMcpClient(client: NativeMcpClient): void;
59
77
  export declare function launchOpenClawWalkthrough(): void;
@@ -267,6 +267,8 @@ export const NATIVE_MCP_CLIENTS = [
267
267
  HERMES,
268
268
  OPENCODE,
269
269
  ];
270
+ /** Native descriptor by harness id; Cursor and OpenClaw have none. */
271
+ export const NATIVE_BY_HARNESS = new Map(NATIVE_MCP_CLIENTS.map((client) => [client.connectClient, client]));
270
272
  export const OPENCLAW = {
271
273
  command: "openclaw",
272
274
  displayName: "OpenClaw",
@@ -276,6 +278,59 @@ export const OPENCLAW = {
276
278
  unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
277
279
  },
278
280
  };
281
+ /** Required booleans: a new harness or new flag fails to compile undecided. */
282
+ export const HARNESS_CAPABILITIES = {
283
+ "claude-code": {
284
+ signInViaCliLogin: false,
285
+ canVerifyAuth: false,
286
+ keyHeldByCli: false,
287
+ cliWritesConfig: false,
288
+ authDeferredAtInstall: false,
289
+ },
290
+ codex: {
291
+ signInViaCliLogin: false,
292
+ canVerifyAuth: false,
293
+ keyHeldByCli: false,
294
+ cliWritesConfig: false,
295
+ // TODO(PF-3580): connect now probes Codex sign-in, so this over-warns a signed-in install.
296
+ authDeferredAtInstall: true,
297
+ },
298
+ cursor: {
299
+ signInViaCliLogin: false,
300
+ canVerifyAuth: true,
301
+ keyHeldByCli: true,
302
+ cliWritesConfig: true,
303
+ authDeferredAtInstall: false,
304
+ },
305
+ grok: {
306
+ signInViaCliLogin: false,
307
+ canVerifyAuth: false,
308
+ keyHeldByCli: false,
309
+ cliWritesConfig: false,
310
+ authDeferredAtInstall: false,
311
+ },
312
+ hermes: {
313
+ signInViaCliLogin: false,
314
+ canVerifyAuth: false,
315
+ keyHeldByCli: false,
316
+ cliWritesConfig: false,
317
+ authDeferredAtInstall: false,
318
+ },
319
+ openclaw: {
320
+ signInViaCliLogin: true,
321
+ canVerifyAuth: true,
322
+ keyHeldByCli: true,
323
+ cliWritesConfig: false,
324
+ authDeferredAtInstall: false,
325
+ },
326
+ opencode: {
327
+ signInViaCliLogin: false,
328
+ canVerifyAuth: false,
329
+ keyHeldByCli: false,
330
+ cliWritesConfig: false,
331
+ authDeferredAtInstall: false,
332
+ },
333
+ };
279
334
  export function launchNativeMcpClient(client) {
280
335
  errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
281
336
  if (client.launchWalkthrough) {
@@ -137,7 +137,8 @@ export function requireCommandSupport(client) {
137
137
  env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
138
138
  timeout: NON_INTERACTIVE_TIMEOUT_MS,
139
139
  });
140
- if (commandNotFound(result.error)) {
140
+ // 127 is the shell's not-found code; a wrapper shim with no real binary behind it exits that.
141
+ if (commandNotFound(result.error) || result.status === 127) {
141
142
  throw new PrerequisiteError(`${client.displayName} is not installed or not available on PATH.`, "harness_not_installed", { cause: result.error });
142
143
  }
143
144
  if (result.error || result.status !== 0) {
@@ -3,7 +3,7 @@ import * as path from "path";
3
3
  import spawn from "cross-spawn";
4
4
  import { z } from "zod";
5
5
  import { loadConfig, matchesCliKey } from "./auth.js";
6
- import { NATIVE_MCP_CLIENTS } from "./connect-clients.js";
6
+ import { NATIVE_BY_HARNESS } from "./connect-clients.js";
7
7
  import { errLine } from "./output.js";
8
8
  import { readCursorTinyfishEntry } from "./cursor-config.js";
9
9
  import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMode, Registered, } from "./harness-detect.js";
@@ -227,7 +227,6 @@ export const SIGN_IN_PROBES = {
227
227
  };
228
228
  // This reaches the pasted report, so its shape is checked first.
229
229
  const ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
230
- const NATIVE_BY_HARNESS = new Map(NATIVE_MCP_CLIENTS.map((client) => [client.connectClient, client]));
231
230
  /** Undefined unless the descriptor routes this harness's key through a variable. */
232
231
  function declaredKeyEnvVar(harness) {
233
232
  return NATIVE_BY_HARNESS.get(harness)?.keyAuth?.envVar;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.29.1-next.237",
3
+ "version": "0.29.1-next.240",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {