@tiny-fish/cli 0.25.2-next.221 → 0.26.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.
@@ -63,11 +63,12 @@ async function connectNativeMcpClient(client, options) {
63
63
  const useKeyAuth = !!keyedAdd;
64
64
  if (options.keyAuthOnly && !useKeyAuth) {
65
65
  // No keyAuth at all is structural (Hermes); a missing flag is just an old install.
66
- throw new Error(client.keyAuth
67
- ? `This ${client.displayName} install cannot take a TinyFish API key, and signing in ` +
68
- `needs a terminal. Update it, then re-run: tinyfish connect ${client.connectClient}`
69
- : `${client.displayName} cannot take a TinyFish API key, and signing in needs a ` +
70
- `terminal. Run: tinyfish connect ${client.connectClient}`);
66
+ if (client.keyAuth) {
67
+ throw new ConnectStepError(`This ${client.displayName} install cannot take a TinyFish API key, and signing in ` +
68
+ `needs a terminal. Update it, then re-run: tinyfish connect ${client.connectClient}`, "harness_too_old");
69
+ }
70
+ throw new Error(`${client.displayName} cannot take a TinyFish API key, and signing in needs a ` +
71
+ `terminal. Run: tinyfish connect ${client.connectClient}`);
71
72
  }
72
73
  telemetry.track("checkpoint", { phase: "prerequisite_ok" });
73
74
  state.stage = "registration_cleanup";
@@ -17,6 +17,11 @@ import { verifyMcpAuth, verifyMcpHealth } from "./verify.js";
17
17
  // `codex mcp add` succeeds unauthenticated; OAuth lands at first tool use.
18
18
  // TODO(PF-3580): connect now probes Codex sign-in, so this over-warns a signed-in install.
19
19
  const AUTH_DEFERRED_AT_INSTALL = new Set(["codex"]);
20
+ // A probe that exits non-zero is an install so old the subcommand is missing entirely.
21
+ const TOO_OLD_REASONS = new Set([
22
+ "harness_too_old",
23
+ "harness_command_unsupported",
24
+ ]);
20
25
  const AUTH_CHECK_CAPABLE = new Set(["cursor", "openclaw"]);
21
26
  // Descriptor-derived so a harness that cannot carry a key is skipped, not failed, when headless.
22
27
  // Cursor and OpenClaw hold the key themselves rather than handing it to a harness `mcp add`.
@@ -186,6 +191,15 @@ async function processHarness(detection, opts, prompt, hoistedCliInstall) {
186
191
  });
187
192
  }
188
193
  catch (e) {
194
+ // Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
195
+ if (e instanceof ConnectStepError && TOO_OLD_REASONS.has(e.failureReason)) {
196
+ return {
197
+ ...base,
198
+ outcome: "harness_too_old",
199
+ errorMessage: e.message,
200
+ fixCommand: `tinyfish connect ${harness}`,
201
+ };
202
+ }
189
203
  return {
190
204
  ...base,
191
205
  outcome: "failed",
@@ -250,6 +264,9 @@ function summaryLine(result, uninstall) {
250
264
  if (result.outcome === "interrupted") {
251
265
  return `✗ ${name} — interrupted before finishing. Fix: ${result.fixCommand}`;
252
266
  }
267
+ if (result.outcome === "harness_too_old") {
268
+ return `… ${name} — needs a newer ${name}: ${result.errorMessage} Fix, once upgraded: ${result.fixCommand}`;
269
+ }
253
270
  if (result.outcome === "failed") {
254
271
  return `✗ ${name} — install failed: ${result.errorMessage}. Fix: ${result.fixCommand}`;
255
272
  }
@@ -283,7 +300,10 @@ function computeExitCode(results) {
283
300
  if (detected.length === 0)
284
301
  return 1;
285
302
  // Only real install attempts count toward the success/failure denominator.
286
- const attempted = detected.filter((r) => r.outcome === "installed" || r.outcome === "failed" || r.outcome === "interrupted");
303
+ const attempted = detected.filter((r) => r.outcome === "installed" ||
304
+ r.outcome === "failed" ||
305
+ r.outcome === "harness_too_old" ||
306
+ r.outcome === "interrupted");
287
307
  if (attempted.length === 0)
288
308
  return 0;
289
309
  return attempted.some((r) => r.outcome === "installed") ? 0 : 1;
@@ -4,7 +4,7 @@ export type AgentClient = "claude-code" | "codex" | "cursor" | "grok" | "hermes"
4
4
  type ConnectStage = "started" | "checkpoint" | "completed" | "failed" | "aborted" | "post_install_failed";
5
5
  type ConnectFailureStage = "prerequisite_check" | "registration_cleanup" | "registration" | "registration_or_authentication" | "client_oauth" | "cli_install" | "skill_install" | "authentication" | "walkthrough_launch";
6
6
  export declare const CONNECT_FAILURE_REASONS: readonly ["harness_not_installed", "harness_command_unsupported", "harness_too_old", "command_not_found", "timeout", "spawn_error", "nonzero_exit", "invalid_config", "user_abandoned", "process_terminated", "unexpected_error"];
7
- type ConnectFailureReason = (typeof CONNECT_FAILURE_REASONS)[number];
7
+ export type ConnectFailureReason = (typeof CONNECT_FAILURE_REASONS)[number];
8
8
  export type ConnectAuthMode = Exclude<AuthMode, AuthMode.Unknown> | "deferred";
9
9
  type ConnectCheckpoint = "prerequisite_ok" | "registered" | "oauth_done" | "cli_installed" | "skill_installed" | "authenticated";
10
10
  /** Ctrl+C killed a setup child — abandonment, not error. */
@@ -8,7 +8,6 @@ import { commandNotFound, spawnStepError, throwIfInterrupted } from "./connect-r
8
8
  import { CLI_AGENT_IDENTITY } from "./constants.js";
9
9
  import { errLine } from "./output.js";
10
10
  // Supports Hermes without node:util.styleText, so the installer still runs on Node 20.11.
11
- // A bump must re-verify SKILL_AGENT_DISPLAY_NAMES against `skills list --json`.
12
11
  const SKILLS_CLI_PACKAGE = "skills@1.5.15";
13
12
  const TINYFISH_WEB_SKILL_SOURCE = "tinyfish-io/tinyfish-cookbook";
14
13
  const TINYFISH_WEB_SKILL = "use-tinyfish";
@@ -45,35 +44,23 @@ const SKILL_ALREADY_CURRENT_PATTERN = /All global skills are up to date/;
45
44
  // A lock entry with no recorded hash is untrackable, so `skills` reports it as skipped rather
46
45
  // than failed. Left undetected that reads as "up to date" while nothing was refreshed.
47
46
  const SKILL_UNCHECKABLE_PATTERN = /cannot be checked automatically/;
48
- // `skills list --json` reports display names, never slugs (verified on 1.5.15).
49
- const SKILL_AGENT_DISPLAY_NAMES = {
50
- "claude-code": "Claude Code",
51
- codex: "Codex",
52
- cursor: "Cursor",
53
- "hermes-agent": "Hermes Agent",
54
- opencode: "OpenCode",
55
- };
56
- const skillListSchema = z.array(z.object({ name: z.string(), agents: z.array(z.string()) }));
47
+ const skillListSchema = z.array(z.object({ name: z.string() }));
57
48
  // `add` exits 0 on per-skill failure; the piped list is the verdict.
58
- function failedListVerdict(skillAgents) {
49
+ function failedListVerdict() {
59
50
  const list = spawn.sync("npx", ["-y", SKILLS_CLI_PACKAGE, "list", "--global", "--json"], {
60
51
  encoding: "utf8",
61
52
  env: skillSpawnEnv(),
62
53
  maxBuffer: STEP_MAX_BUFFER,
63
54
  timeout: SKILL_INSTALL_TIMEOUT_MS,
64
55
  });
65
- const verified = !list.error && list.status === 0 && verifiedForAgents(list.stdout ?? "", skillAgents);
56
+ const verified = !list.error && list.status === 0 && skillListed(list.stdout ?? "");
66
57
  return verified ? null : list;
67
58
  }
68
- // `list` folds in every agent's dir, so name-only matches stale copies.
69
- function verifiedForAgents(stdout, skillAgents) {
59
+ // Entry presence is the whole verdict: `agents` names the harnesses `skills` detects,
60
+ // so an install for a harness with no config dir yet lists none (PF-3680).
61
+ function skillListed(stdout) {
70
62
  const parsed = skillListSchema.safeParse(parseJson(stdout));
71
- if (!parsed.success)
72
- return false;
73
- const agents = parsed.data
74
- .filter((skill) => skill.name === TINYFISH_WEB_SKILL)
75
- .flatMap((skill) => skill.agents);
76
- return skillAgents.every((agent) => agents.includes(SKILL_AGENT_DISPLAY_NAMES[agent]));
63
+ return parsed.success && parsed.data.some((skill) => skill.name === TINYFISH_WEB_SKILL);
77
64
  }
78
65
  function parseJson(text) {
79
66
  try {
@@ -93,7 +80,7 @@ export function installWebSkill(client) {
93
80
  env: skillSpawnEnv(),
94
81
  timeout: SKILL_INSTALL_TIMEOUT_MS,
95
82
  });
96
- const failed = add.error || add.status !== 0 ? add : failedListVerdict([client.skillAgent]);
83
+ const failed = add.error || add.status !== 0 ? add : failedListVerdict();
97
84
  if (failed) {
98
85
  throw spawnStepError(`Could not install the TinyFish web skill in ${client.displayName}`, failed);
99
86
  }
@@ -162,7 +149,7 @@ function reinstallWebSkill(skillAgents, verbose) {
162
149
  const output = capturedOutput(result);
163
150
  // Quiet capture stays (#4434); the list verdict replaced prose matching.
164
151
  const addFailed = Boolean(result.error) || result.status !== 0;
165
- const failedList = addFailed ? null : failedListVerdict(skillAgents);
152
+ const failedList = addFailed ? null : failedListVerdict();
166
153
  const failed = addFailed || failedList !== null;
167
154
  if (failed)
168
155
  throwIfInterrupted(failedList ?? result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.25.2-next.221",
3
+ "version": "0.26.0",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {