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

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)
@@ -320,7 +297,7 @@ function summaryLine(result, uninstall) {
320
297
  `Fix: ${finishSetupCommand(result.harness)}`);
321
298
  }
322
299
  // 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;
300
+ const authDeferred = HARNESS_CAPABILITIES[result.harness].authDeferredAtInstall && !result.keyAuthed;
324
301
  const verifyLabel = result.verifyDepth ? `verified: ${result.verifyDepth}` : "not verified";
325
302
  if (authDeferred || result.verifyOk === false) {
326
303
  const reason = authDeferred
@@ -330,10 +307,11 @@ function summaryLine(result, uninstall) {
330
307
  }
331
308
  return `✓ ${name} — installed, ${verifyLabel}`;
332
309
  }
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.
310
+ /** A config-writing connect needs no binary; every other harness spawns one. */
335
311
  function isStaleConfig(detection) {
336
- return detection.detected && !detection.binaryOnPath && detection.harness !== "cursor";
312
+ return (detection.detected &&
313
+ !detection.binaryOnPath &&
314
+ !HARNESS_CAPABILITIES[detection.harness].cliWritesConfig);
337
315
  }
338
316
  /** A stale config dir is a detection signal with nothing behind it. */
339
317
  function actionable(result) {
@@ -465,13 +443,18 @@ export async function runConnectAll(opts) {
465
443
  const chosen = new Set(pick.selection);
466
444
  deselected = new Set(offerable.filter((d) => !chosen.has(d.harness)).map((d) => d.harness));
467
445
  }
468
- // Mirrors processHarness's gate: headless, only keyless OpenClaw skips outright.
446
+ // Mirrors processHarness's gate: headless, only keyless CLI-login harnesses skip.
469
447
  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.
448
+ (isTTY ||
449
+ !HARNESS_CAPABILITIES[harness].signInViaCliLogin ||
450
+ (!!gateKey && KEY_AUTH_CAPABLE.has(harness)));
451
+ // Machine-global, so once per invocation; config-writing connects never install it.
472
452
  const cliNeeded = !opts.dryRun &&
473
453
  !opts.uninstall &&
474
- detections.some((d) => d.detected && d.binaryOnPath && d.harness !== "cursor" && willAttempt(d.harness));
454
+ detections.some((d) => d.detected &&
455
+ d.binaryOnPath &&
456
+ !HARNESS_CAPABILITIES[d.harness].cliWritesConfig &&
457
+ willAttempt(d.harness));
475
458
  let takeHoistedCli;
476
459
  if (cliNeeded) {
477
460
  errLine("First: the TinyFish CLI, shared by every harness.");
@@ -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) {
@@ -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.239",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {