@tiny-fish/cli 0.51.2-next.415 → 0.51.2-next.417

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.
@@ -86,7 +86,7 @@ export function mapConnectError(base, e, oauthBudget, spentBrowserSlot, fixComma
86
86
  if (e instanceof ConnectStepError && e.failureReason === 'harness_not_installed') {
87
87
  return { ...base, detected: false, outcome: 'not_installed' };
88
88
  }
89
- // Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
89
+ // Every too-old message already carries its upgrade nudge.
90
90
  if (e instanceof ConnectStepError && TOO_OLD_REASONS.has(e.failureReason)) {
91
91
  return { ...base, outcome: 'harness_too_old', errorMessage: e.message, fixCommand };
92
92
  }
@@ -4,6 +4,7 @@ import { CONNECT_ATTEMPT_ENV, isAttemptId, resolvedApiKey, takePendingConnectAtt
4
4
  import { CLI_INVOCATION_ID, CLI_VERSION } from './constants.js';
5
5
  import { detectHarness, detectHumanInitiated, detectRuntimeEnv } from './harness.js';
6
6
  import { boundedVersion, errLine, sanitizeLine } from './output.js';
7
+ import { isOlder } from './cli-presence.js';
7
8
  import { postConnectEvent, telemetryDisabled } from './setup-telemetry.js';
8
9
  import { installSignalGuard } from './signals.js';
9
10
  export const NON_INTERACTIVE_TIMEOUT_MS = 10_000;
@@ -179,13 +180,19 @@ export async function runGuarded(state, telemetry, body) {
179
180
  function probeTimeoutMs(client) {
180
181
  return client.supportCheck.timeoutMs ?? NON_INTERACTIVE_TIMEOUT_MS;
181
182
  }
183
+ /** Undefined when either side is unknown; callers pick the safe default. */
184
+ function isBelowMin(version, minVersion) {
185
+ // Hermes prints `Hermes Agent v0.22.4+33.g3fa571a (2026.9.24)`; compare the number only.
186
+ const number = version && /\d+(?:\.\d+)+(?:-[0-9A-Za-z.]+)?/.exec(version)?.[0];
187
+ if (!number || !minVersion)
188
+ return undefined;
189
+ return isOlder(number, minVersion);
190
+ }
182
191
  // Flags go unseen for reasons besides age; state facts, not causes.
183
192
  function unavailableMessage(client, version) {
184
193
  const { minVersion } = client.supportCheck;
185
194
  const found = version ? ` (found ${version})` : '';
186
- // Numeric collation orders dotted versions: 7.7.3 sorts after 7.4.13.
187
- const belowMin = minVersion && (!version || version.localeCompare(minVersion, 'en', { numeric: true }) < 0);
188
- const required = belowMin
195
+ const required = minVersion && (isBelowMin(version, minVersion) ?? version === undefined)
189
196
  ? ` ${client.displayName} ${minVersion} or newer is required${found}.`
190
197
  : '';
191
198
  return (`${client.supportCheck.unavailableMessage}${required}` +
@@ -202,6 +209,7 @@ function throwProbeFailure(client, result) {
202
209
  // It ran and exited non-zero, so the version is still answerable.
203
210
  debugProbeOutput(client, sanitizeLine(`${result.stdout ?? ''}\n${result.stderr ?? ''}`));
204
211
  const harnessVersion = probeHarnessVersion(client);
212
+ requireMinVersion(client, harnessVersion);
205
213
  throw new ConnectStepError(unavailableMessage(client, harnessVersion), 'harness_command_unsupported', {
206
214
  cause: result.error,
207
215
  harnessVersion,
@@ -236,6 +244,7 @@ function requireEssential(client, output) {
236
244
  return;
237
245
  debugProbeOutput(client, output);
238
246
  const harnessVersion = probeHarnessVersion(client);
247
+ requireMinVersion(client, harnessVersion);
239
248
  throw new ConnectStepError(unavailableMessage(client, harnessVersion), 'harness_too_old', {
240
249
  harnessVersion,
241
250
  });
@@ -270,12 +279,19 @@ export function requireCommandSupport(client) {
270
279
  const keyAuthSupported = keyAuthPattern ? keyAuthPattern.test(output) : true;
271
280
  if (!optionalSupported || !keyAuthSupported)
272
281
  debugProbeOutput(client, output);
273
- return {
274
- optionalSupported,
275
- keyAuthSupported,
276
- matchedVariant: resolveVariant(client, output),
277
- harnessVersion: probeHarnessVersion(client),
278
- };
282
+ const matchedVariant = resolveVariant(client, output);
283
+ const harnessVersion = probeHarnessVersion(client);
284
+ requireMinVersion(client, harnessVersion);
285
+ return { optionalSupported, keyAuthSupported, matchedVariant, harnessVersion };
286
+ }
287
+ // Old releases can list the flags yet mishandle them; unknown passes.
288
+ function requireMinVersion(client, harnessVersion) {
289
+ const { minVersion, updateCommand, enforceMinVersion } = client.supportCheck;
290
+ if (!enforceMinVersion || !isBelowMin(harnessVersion, minVersion))
291
+ return;
292
+ const fix = updateCommand ? `Run \`${updateCommand}\`` : `Update ${client.displayName}`;
293
+ throw new ConnectStepError(`${client.displayName} ${minVersion} or newer is required (found ${harnessVersion}). ` +
294
+ `${fix} and retry.`, 'harness_too_old', { harnessVersion });
279
295
  }
280
296
  /** Best-effort; the server rejects non-printable characters and >32 chars. */
281
297
  function probeHarnessVersion(client) {
@@ -18,5 +18,6 @@ export declare const API_URL_OVERRIDE: string | undefined;
18
18
  export declare function getDashboardUrl(source?: string): string;
19
19
  export declare const DASHBOARD_URL: string;
20
20
  export declare const HARNESS_PROBE_TIMEOUT_MS = 15000;
21
+ export declare const HERMES_COLD_START_TIMEOUT_MS = 60000;
21
22
  /** Hermes' own id for the TinyFish web plugin. */
22
23
  export declare const HERMES_PLUGIN_NAME = "tinyfish";
@@ -62,5 +62,7 @@ export function getDashboardUrl(source = DEFAULT_DASHBOARD_SOURCE) {
62
62
  }
63
63
  export const DASHBOARD_URL = getDashboardUrl();
64
64
  export const HARNESS_PROBE_TIMEOUT_MS = 15_000;
65
+ // A cold Hermes installs its Python venv first, measured 8.8-11s locally.
66
+ export const HERMES_COLD_START_TIMEOUT_MS = 60_000;
65
67
  /** Hermes' own id for the TinyFish web plugin. */
66
68
  export const HERMES_PLUGIN_NAME = 'tinyfish';
@@ -16,6 +16,10 @@ export interface HarnessSupportCheck {
16
16
  timeoutMs?: number;
17
17
  /** First release with the probed flags; bisected upstream, not derivable. */
18
18
  minVersion?: string;
19
+ /** Refuse below minVersion even when the probed flags are listed. */
20
+ enforceMinVersion?: true;
21
+ /** Shown in the below-minVersion error as the one-step fix. */
22
+ updateCommand?: string;
19
23
  unavailableMessage: string;
20
24
  }
21
25
  /** One entry per harness: every per-harness fact, and no behaviour. */
@@ -214,6 +218,10 @@ export declare const HARNESS_SPECS: {
214
218
  args: string[];
215
219
  patterns: RegExp[];
216
220
  keyAuthPattern: RegExp;
221
+ timeoutMs: number;
222
+ minVersion: string;
223
+ enforceMinVersion: true;
224
+ updateCommand: string;
217
225
  unavailableMessage: string;
218
226
  };
219
227
  urlStyle: "flag";
@@ -1,4 +1,4 @@
1
- import { HERMES_PLUGIN_NAME } from '../constants.js';
1
+ import { HERMES_COLD_START_TIMEOUT_MS, HERMES_PLUGIN_NAME } from '../constants.js';
2
2
  import { CLAUDE_CODE_DEFERRED_AUTH_NOTE, CLAUDE_CODE_KEYED_FALLBACK_NOTE, CLINE_MCP_ADD_UNAVAILABLE_MESSAGE, CODEX_DEFERRED_AUTH_NOTE, CODEX_KEYED_FALLBACK_NOTE, CODEX_MCP_URL_UNAVAILABLE_MESSAGE, GROK_HTTP_UNAVAILABLE_MESSAGE, GROK_SIGN_IN_HINT, HERMES_OAUTH_UNAVAILABLE_MESSAGE, HERMES_RESTART_NOTE, KILO_MCP_ADD_UNAVAILABLE_MESSAGE, MCP_ADD_UNAVAILABLE_MESSAGE, OPENCLAW_SKILL_UNAVAILABLE_MESSAGE, OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE, OPENCODE_MODEL_NOTE, } from './harness-messages.js';
3
3
  const HEADER_FLAG = /(?:^|\s)--header(?:[\s<=]|$)/m;
4
4
  /** One entry per harness; every per-harness list derives from it. */
@@ -140,6 +140,11 @@ export const HARNESS_SPECS = {
140
140
  args: ['mcp', 'add', '--help'],
141
141
  patterns: [/--auth\s+\{[^}]*oauth[^}]*\}/],
142
142
  keyAuthPattern: /--auth\s+\{[^}]*header[^}]*\}/,
143
+ timeoutMs: HERMES_COLD_START_TIMEOUT_MS,
144
+ // Older `config set` saves our JSON entry as an unusable string.
145
+ minVersion: '0.21.2',
146
+ enforceMinVersion: true,
147
+ updateCommand: 'hermes update',
143
148
  unavailableMessage: HERMES_OAUTH_UNAVAILABLE_MESSAGE,
144
149
  },
145
150
  urlStyle: 'flag',
@@ -3,7 +3,8 @@ import path from 'node:path';
3
3
  import fs from 'node:fs';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import spawn from 'cross-spawn';
6
- import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
6
+ import { HERMES_COLD_START_TIMEOUT_MS } from './constants.js';
7
+ import { throwIfInterrupted } from './connect-runtime.js';
7
8
  import { sanitizeLine } from './output.js';
8
9
  // display_hermes_home() abbreviates under $HOME, so the value is not always absolute.
9
10
  const HERMES_HOME_LINE = /^\s*hermes_home:\s*(\S.*?)\s*$/m;
@@ -29,8 +30,10 @@ function probeHermesHome() {
29
30
  const result = spawn.sync('hermes', ['dump'], {
30
31
  encoding: 'utf8',
31
32
  env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
32
- timeout: HARNESS_PROBE_TIMEOUT_MS,
33
+ timeout: HERMES_COLD_START_TIMEOUT_MS,
33
34
  });
35
+ // Ctrl+C would otherwise read as `exit` and be cached.
36
+ throwIfInterrupted(result);
34
37
  if (result.error?.code === 'ETIMEDOUT') {
35
38
  return { reason: 'timeout' };
36
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.51.2-next.415",
3
+ "version": "0.51.2-next.417",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {