@tiny-fish/cli 0.38.1-next.307 → 0.39.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.
@@ -1,6 +1,6 @@
1
1
  import spawn from 'cross-spawn';
2
2
  import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from '../lib/auth.js';
3
- import { CURSOR_SKILL_TARGET, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
3
+ import { CURSOR_SKILL_TARGET, OPENCLAW, openclawSkillInstallArgs, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
4
4
  import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from '../lib/cli-install.js';
5
5
  import { ensureCliAuthenticated } from '../lib/connect-auth.js';
6
6
  import { createStdinPrompt, runCliFallback } from '../lib/connect-fallback.js';
@@ -474,7 +474,7 @@ export async function connectOpenClaw(options) {
474
474
  }
475
475
  state.stage = 'skill_install';
476
476
  errLine('Installing the TinyFish skill in OpenClaw...');
477
- const skillInstallResult = spawn.sync('openclaw', OPENCLAW_SKILL_INSTALL_ARGS, captureStdio(options.verbose ?? false));
477
+ const skillInstallResult = spawn.sync('openclaw', openclawSkillInstallArgs(support.matchedVariant), captureStdio(options.verbose ?? false));
478
478
  if (skillInstallResult.error || skillInstallResult.status !== 0) {
479
479
  // Built first: its interrupt check must run before any replay.
480
480
  const error = spawnStepError('Could not install the TinyFish skill in OpenClaw', skillInstallResult);
@@ -1,7 +1,7 @@
1
1
  import spawn from 'cross-spawn';
2
2
  import { type AgentClient, type SupportedCommand } from './connect-runtime.js';
3
3
  import { type HarnessSpec, type SkillAgent } from './harness-spec.js';
4
- export declare const OPENCLAW_SKILL_INSTALL_ARGS: readonly string[];
4
+ export declare function openclawSkillInstallArgs(ackFlag: string | undefined): string[];
5
5
  /** `openclaw skills` has no uninstall; ClawHub removes from the global dir. */
6
6
  export declare function openclawSkillUninstall(): {
7
7
  command: string;
@@ -12,14 +12,10 @@ import { HERMES_PLUGIN_VERSION, installHermesPlugin, setHermesWebBackends, } fro
12
12
  const HERMES_SEED_TIMEOUT_MS = 120_000;
13
13
  const OPENCLAW_SKILL = '@tinyfish/tinyfish';
14
14
  // --force makes this an unconditional overwrite, so the same call installs and refreshes.
15
- export const OPENCLAW_SKILL_INSTALL_ARGS = [
16
- 'skills',
17
- 'install',
18
- OPENCLAW_SKILL,
19
- '--global',
20
- '--force',
21
- '--acknowledge-clawhub-risk',
22
- ];
15
+ export function openclawSkillInstallArgs(ackFlag) {
16
+ const ack = ackFlag === undefined ? [] : [ackFlag];
17
+ return ['skills', 'install', OPENCLAW_SKILL, '--global', '--force', ...ack];
18
+ }
23
19
  /** `openclaw skills` has no uninstall; ClawHub removes from the global dir. */
24
20
  export function openclawSkillUninstall() {
25
21
  return {
@@ -92,9 +92,12 @@ export interface SupportedCommand {
92
92
  displayName: string;
93
93
  supportCheck: HarnessSupportCheck;
94
94
  }
95
+ /** Refresh paths need the advertised alternative without connect's version probe. */
96
+ export declare function probeSupportVariant(client: SupportedCommand): string | undefined;
95
97
  export declare function requireCommandSupport(client: SupportedCommand): {
96
98
  optionalSupported: boolean;
97
99
  keyAuthSupported: boolean;
100
+ matchedVariant?: string;
98
101
  harnessVersion?: string;
99
102
  };
100
103
  export declare function createConnectTelemetry(mcpUrl: string, client: AgentClient, opts?: {
@@ -178,23 +178,26 @@ export async function runGuarded(state, telemetry, body) {
178
178
  await telemetry.flush();
179
179
  }
180
180
  }
181
+ function probeTimeoutMs(client) {
182
+ return client.supportCheck.timeoutMs ?? NON_INTERACTIVE_TIMEOUT_MS;
183
+ }
181
184
  function throwProbeFailure(client, result) {
182
185
  throwIfInterrupted(result);
183
186
  // A hung probe is a timeout, not a missing capability.
184
187
  if (result.error?.code === 'ETIMEDOUT') {
185
- throw new ConnectStepError(client.supportCheck.unavailableMessage, 'timeout', {
186
- cause: result.error,
187
- });
188
+ const probe = `${client.command} ${client.supportCheck.args.join(' ')}`;
189
+ throw new ConnectStepError(`${client.displayName} did not respond to \`${probe}\` within ` +
190
+ `${probeTimeoutMs(client) / 1000}s. Retry.`, 'timeout', { cause: result.error });
188
191
  }
189
192
  // It ran and exited non-zero, so the version is still answerable.
190
- throw new PrerequisiteError(client.supportCheck.unavailableMessage, 'harness_command_unsupported', { cause: result.error, harnessVersion: probeHarnessVersion(client.command) });
193
+ throw new PrerequisiteError(client.supportCheck.unavailableMessage, 'harness_command_unsupported', { cause: result.error, harnessVersion: probeHarnessVersion(client) });
191
194
  }
192
- export function requireCommandSupport(client) {
195
+ function supportProbeOutput(client) {
193
196
  const result = spawn.sync(client.command, client.supportCheck.args, {
194
197
  encoding: 'utf8',
195
198
  // Agent harnesses export FORCE_COLOR, which makes clients colourise even a piped --help.
196
199
  env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
197
- timeout: NON_INTERACTIVE_TIMEOUT_MS,
200
+ timeout: probeTimeoutMs(client),
198
201
  });
199
202
  // 127 is the shell's not-found code; a wrapper shim with no real binary behind it exits that.
200
203
  if (commandNotFound(result.error) || result.status === 127) {
@@ -204,31 +207,63 @@ export function requireCommandSupport(client) {
204
207
  throwProbeFailure(client, result);
205
208
  }
206
209
  // Belt and braces with the colour env: a client that ignores NO_COLOR still has to match.
207
- const output = sanitizeLine(`${result.stdout ?? ''}\n${result.stderr ?? ''}`);
208
- const listed = (patterns) => patterns.every((pattern) => pattern.test(output));
209
- const essential = listed(client.supportCheck.patterns);
210
- const optionalSupported = listed(client.supportCheck.optionalPatterns ?? []);
210
+ return sanitizeLine(`${result.stdout ?? ''}\n${result.stderr ?? ''}`);
211
+ }
212
+ function debugProbeOutput(client, output) {
213
+ if (!process.env['TINYFISH_DEBUG'])
214
+ return;
215
+ errLine(`${client.command} ${client.supportCheck.args.join(' ')} printed:\n${output.trim()}`);
216
+ }
217
+ function requireEssential(client, output) {
218
+ if (client.supportCheck.patterns.every((pattern) => pattern.test(output)))
219
+ return;
220
+ debugProbeOutput(client, output);
221
+ throw new PrerequisiteError(client.supportCheck.unavailableMessage, 'harness_too_old', {
222
+ harnessVersion: probeHarnessVersion(client),
223
+ });
224
+ }
225
+ /** Absence is a fact; an unknown flag means an upstream rename. */
226
+ function resolveVariant(client, output) {
227
+ const check = client.supportCheck;
228
+ // Token equality, not a built regex: exact on both edges, and no dynamic RegExp.
229
+ const listed = new Set(output.split(/[\s,=]+/));
230
+ const matched = check.variants?.find((flag) => listed.has(flag));
231
+ if (matched)
232
+ return matched;
233
+ const unknown = check.unknownVariant?.exec(output)?.[0];
234
+ if (unknown === undefined)
235
+ return undefined;
236
+ // Unacknowledged policy warnings can block the install; fail before it does.
237
+ debugProbeOutput(client, output);
238
+ throw new PrerequisiteError(`${client.displayName} lists \`${unknown}\`, which this TinyFish CLI does not recognise. ` +
239
+ 'Upgrade the TinyFish CLI with `tinyfish upgrade` and retry.', 'harness_too_old', { harnessVersion: probeHarnessVersion(client) });
240
+ }
241
+ /** Refresh paths need the advertised alternative without connect's version probe. */
242
+ export function probeSupportVariant(client) {
243
+ const output = supportProbeOutput(client);
244
+ requireEssential(client, output);
245
+ return resolveVariant(client, output);
246
+ }
247
+ export function requireCommandSupport(client) {
248
+ const output = supportProbeOutput(client);
249
+ requireEssential(client, output);
250
+ const optionalSupported = (client.supportCheck.optionalPatterns ?? []).every((pattern) => pattern.test(output));
211
251
  const keyAuthPattern = client.supportCheck.keyAuthPattern;
212
252
  const keyAuthSupported = keyAuthPattern ? keyAuthPattern.test(output) : true;
213
- if ((!essential || !optionalSupported || !keyAuthSupported) && process.env['TINYFISH_DEBUG']) {
214
- errLine(`${client.command} ${client.supportCheck.args.join(' ')} printed:\n${output.trim()}`);
215
- }
216
- if (!essential) {
217
- throw new PrerequisiteError(client.supportCheck.unavailableMessage, 'harness_too_old', {
218
- harnessVersion: probeHarnessVersion(client.command),
219
- });
220
- }
253
+ if (!optionalSupported || !keyAuthSupported)
254
+ debugProbeOutput(client, output);
221
255
  return {
222
256
  optionalSupported,
223
257
  keyAuthSupported,
224
- harnessVersion: probeHarnessVersion(client.command),
258
+ matchedVariant: resolveVariant(client, output),
259
+ harnessVersion: probeHarnessVersion(client),
225
260
  };
226
261
  }
227
262
  /** Best-effort; the server rejects non-printable characters and >32 chars. */
228
- function probeHarnessVersion(command) {
229
- const result = spawn.sync(command, ['--version'], {
263
+ function probeHarnessVersion(client) {
264
+ const result = spawn.sync(client.command, ['--version'], {
230
265
  encoding: 'utf8',
231
- timeout: NON_INTERACTIVE_TIMEOUT_MS,
266
+ timeout: probeTimeoutMs(client),
232
267
  });
233
268
  if (result.error || result.status !== 0)
234
269
  return undefined;
@@ -8,6 +8,12 @@ export interface HarnessSupportCheck {
8
8
  optionalPatterns?: RegExp[];
9
9
  /** Missing → this install cannot take a stored key; fall back to OAuth. */
10
10
  keyAuthPattern?: RegExp;
11
+ /** Ordered alternative flags in one help output; first match wins. */
12
+ variants?: string[];
13
+ /** Matches an alternative we do not know; renames fail loudly. */
14
+ unknownVariant?: RegExp;
15
+ /** Defaults to NON_INTERACTIVE_TIMEOUT_MS; raise for a slow cold start. */
16
+ timeoutMs?: number;
11
17
  unavailableMessage: string;
12
18
  }
13
19
  /** One entry per harness: every per-harness fact, and no behaviour. */
@@ -221,6 +227,9 @@ export declare const HARNESS_SPECS: {
221
227
  supportCheck: {
222
228
  args: string[];
223
229
  patterns: RegExp[];
230
+ variants: string[];
231
+ unknownVariant: RegExp;
232
+ timeoutMs: number;
224
233
  unavailableMessage: string;
225
234
  };
226
235
  signInViaCliLogin: true;
@@ -33,9 +33,8 @@ const GROK_HTTP_UNAVAILABLE_MESSAGE = 'Could not confirm this Grok installation
33
33
  const GROK_SIGN_IN_HINT = 'TinyFish is registered in Grok. Open Grok, run `/mcps`, select tinyfish, and press `i` to ' +
34
34
  'sign in.';
35
35
  const OPENCLAW_SKILL_UNAVAILABLE_MESSAGE = 'Could not confirm this OpenClaw installation supports global skill installation: `openclaw ' +
36
- 'skills install --help` did not list `--global` and `--acknowledge-clawhub-risk`. Update ' +
37
- 'OpenClaw and retry, or install manually with `openclaw skills install @tinyfish/tinyfish ' +
38
- '--global --acknowledge-clawhub-risk`.' +
36
+ 'skills install --help` did not list `--global`. Update OpenClaw and retry, or install ' +
37
+ 'manually with `openclaw skills install @tinyfish/tinyfish --global`.' +
39
38
  SUPPORT_CHECK_DEBUG_HINT;
40
39
  const OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this OpenCode installation supports one-command MCP setup: `opencode mcp ' +
41
40
  'add --help` did not list `--url`. Update OpenCode and retry, or add TinyFish manually with ' +
@@ -202,7 +201,12 @@ export const HARNESS_SPECS = {
202
201
  reloadAction: 'restart it',
203
202
  supportCheck: {
204
203
  args: ['skills', 'install', '--help'],
205
- patterns: [/--global(?:\s|$)/, /--acknowledge-clawhub-risk(?:\s|$)/],
204
+ patterns: [/--global(?:\s|$)/],
205
+ // Renamed in 2026.8.1; absent before 2026.7, where no trust gate existed.
206
+ variants: ['--acknowledge-install-policy-warning', '--acknowledge-clawhub-risk'],
207
+ unknownVariant: /--acknowledge-[a-z-]+/,
208
+ // 212MB dist; win32 cold starts drove most of the 10s timeouts.
209
+ timeoutMs: 30_000,
206
210
  unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
207
211
  },
208
212
  signInViaCliLogin: true,
@@ -3,8 +3,8 @@ import spawn from 'cross-spawn';
3
3
  import { z } from 'zod';
4
4
  import { loadConfig } from './auth.js';
5
5
  import { captureStdio, capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
6
- import { CURSOR_SKILL_TARGET, NATIVE_MCP_CLIENTS, OPENCLAW_SKILL_INSTALL_ARGS, } from './connect-clients.js';
7
- import { commandNotFound, spawnFailureDetail, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
6
+ import { CURSOR_SKILL_TARGET, NATIVE_MCP_CLIENTS, OPENCLAW, openclawSkillInstallArgs, } from './connect-clients.js';
7
+ import { ConnectInterruptedError, ConnectStepError, probeSupportVariant, spawnFailureDetail, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
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.
@@ -128,16 +128,37 @@ export function updateWebSkill({ verbose }) {
128
128
  refreshed = reinstallOpenClawSkill(verbose) || refreshed;
129
129
  return refreshed;
130
130
  }
131
+ /** OpenClaw rejects an unknown ack flag, so never guess. */
132
+ function openclawAckFlag() {
133
+ try {
134
+ return { ok: true, flag: probeSupportVariant(OPENCLAW) };
135
+ }
136
+ catch (error) {
137
+ // Ctrl+C during the probe must still abort the upgrade, not skip a step.
138
+ if (error instanceof ConnectInterruptedError)
139
+ throw error;
140
+ // A stale context entry must not fail an otherwise successful upgrade.
141
+ if (error instanceof ConnectStepError) {
142
+ const detail = error.failureReason === 'harness_not_installed'
143
+ ? 'OpenClaw is not on PATH.'
144
+ : error.message;
145
+ return { ok: false, reason: `${detail} Its TinyFish skill was left unchanged.` };
146
+ }
147
+ return {
148
+ ok: false,
149
+ reason: "Could not read OpenClaw's skill installer options, so its TinyFish skill was left unchanged.",
150
+ };
151
+ }
152
+ }
131
153
  function reinstallOpenClawSkill(verbose) {
132
154
  if (verbose)
133
155
  errLine('Refreshing the TinyFish skill in OpenClaw...');
134
- const result = spawn.sync('openclaw', OPENCLAW_SKILL_INSTALL_ARGS, captureStdio(verbose));
135
- // OpenClaw may have been removed since connect; a stale context entry must not fail an
136
- // upgrade that otherwise succeeded.
137
- if (commandNotFound(result.error)) {
138
- errLine('OpenClaw is not on PATH, so its TinyFish skill was left unchanged.');
156
+ const ack = openclawAckFlag();
157
+ if (!ack.ok) {
158
+ errLine(ack.reason);
139
159
  return false;
140
160
  }
161
+ const result = spawn.sync('openclaw', openclawSkillInstallArgs(ack.flag), captureStdio(verbose));
141
162
  if (result.error || result.status !== 0) {
142
163
  throwIfInterrupted(result);
143
164
  replay(capturedOutput(result));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.38.1-next.307",
3
+ "version": "0.39.0",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {