@tiny-fish/cli 0.11.1-next.132 → 0.12.1-next.135

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.
package/README.md CHANGED
@@ -64,6 +64,26 @@ npx -y @tiny-fish/cli@latest connect openclaw --launch
64
64
  OpenClaw must already be installed. The command installs the TinyFish skill for all local OpenClaw
65
65
  agents and guides you through Search, Fetch, and Agent one step at a time.
66
66
 
67
+ ## Upgrading
68
+
69
+ ```bash
70
+ tinyfish upgrade
71
+ ```
72
+
73
+ Brings both halves of an install current: the global `@tiny-fish/cli` package and the global
74
+ `use-tinyfish` skill. Use this rather than remembering `npm i -g @tiny-fish/cli@latest` and
75
+ `skills update -g` separately.
76
+
77
+ The skill step refreshes only the TinyFish skill, and only for the agents you ran
78
+ `tinyfish connect` against. Other global skills, and agents you never connected, are left alone.
79
+ The refresh overwrites your copy of the skill, including any local edits to it. Restart your
80
+ agent afterward to pick it up.
81
+
82
+ If you never ran `tinyfish connect` on this machine, there is no record of which agent to
83
+ refresh, so the command falls back to updating a tracked `use-tinyfish` skill if one exists. When
84
+ it reports that the skill is missing or cannot be checked, run `tinyfish connect <client>` to
85
+ reinstall it.
86
+
67
87
  ## Authentication
68
88
 
69
89
  Get your API key from [agent.tinyfish.ai](https://agent.tinyfish.ai).
@@ -1,7 +1,21 @@
1
1
  import { Command } from "commander";
2
+ export declare const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
2
3
  export declare const DEFAULT_ONBOARDING_PROMPT: string;
3
4
  export declare const OPENCLAW_ONBOARDING_PROMPT: string;
4
5
  export type AgentClient = "claude-code" | "codex" | "hermes" | "openclaw";
6
+ /** Ctrl+C/SIGTERM killed a setup child — abandonment, not error. */
7
+ export declare class ConnectInterruptedError extends Error {
8
+ }
9
+ export declare function installTinyFishCli(): void;
10
+ /**
11
+ * Refresh the skill we installed, for the harnesses we installed it in.
12
+ *
13
+ * Reinstalling beats `skills update`: it reports failure through the exit code instead of
14
+ * only in prose, it repairs a copy whose lock entry lost its hash, and it cannot spread the
15
+ * skill to agents the user never connected (`update` re-adds with no --agent, which targets
16
+ * every agent on the machine).
17
+ */
18
+ export declare function updateWebSkill(): void;
5
19
  export declare function connectClaudeCode(options: {
6
20
  apiKey?: string;
7
21
  mcpUrl: string;
@@ -20,11 +20,21 @@ const OPENCLAW_SKILL_UNAVAILABLE_MESSAGE = "This OpenClaw installation does not
20
20
  "retry.";
21
21
  const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
22
22
  const TINYFISH_CLI_PACKAGE = "@tiny-fish/cli@latest";
23
+ export const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
23
24
  // Supports Hermes without node:util.styleText, so the installer still runs on Node 20.11.
24
25
  const SKILLS_CLI_PACKAGE = "skills@1.5.15";
25
26
  const TINYFISH_WEB_SKILL_SOURCE = "tinyfish-io/tinyfish-cookbook";
26
27
  const TINYFISH_WEB_SKILL = "use-tinyfish";
27
28
  const OPENCLAW_SKILL = "@tinyfish/tinyfish";
29
+ // --force makes this an unconditional overwrite, so the same call installs and refreshes.
30
+ const OPENCLAW_SKILL_INSTALL_ARGS = [
31
+ "skills",
32
+ "install",
33
+ OPENCLAW_SKILL,
34
+ "--global",
35
+ "--force",
36
+ "--acknowledge-clawhub-risk",
37
+ ];
28
38
  export const DEFAULT_ONBOARDING_PROMPT = "I just connected TinyFish. Call the guide_next_step tool now to begin. Then follow its " +
29
39
  "instructions one step at a time. Ask for my input and wait for my reply before each subsequent " +
30
40
  "TinyFish tool call.";
@@ -33,9 +43,13 @@ export const OPENCLAW_ONBOARDING_PROMPT = "I just installed the TinyFish skill.
33
43
  "which result I want to read, and wait before using TinyFish Fetch. Then ask what browser task " +
34
44
  "I want to complete and wait before using TinyFish Agent. Never skip ahead or choose for me.";
35
45
  /** Ctrl+C/SIGTERM killed a setup child — abandonment, not error. */
36
- class ConnectInterruptedError extends Error {
46
+ export class ConnectInterruptedError extends Error {
37
47
  }
38
48
  function throwIfInterrupted(result) {
49
+ // spawn.sync kills a timed-out child with SIGTERM, so signal alone would misread a slow
50
+ // network as the user walking away. A timeout is a failure and must report as one.
51
+ if (result.error?.code === "ETIMEDOUT")
52
+ return;
39
53
  if (result.signal === "SIGINT" || result.signal === "SIGTERM") {
40
54
  throw new ConnectInterruptedError("Setup interrupted");
41
55
  }
@@ -314,7 +328,7 @@ function requireCommandSupport(client) {
314
328
  function commandNotFound(error) {
315
329
  return error?.code === "ENOENT";
316
330
  }
317
- function installTinyFishCli() {
331
+ export function installTinyFishCli() {
318
332
  errLine("Installing the TinyFish CLI...");
319
333
  const result = spawn.sync("npm", ["install", "--global", TINYFISH_CLI_PACKAGE], {
320
334
  stdio: "inherit",
@@ -325,9 +339,9 @@ function installTinyFishCli() {
325
339
  throw new Error("Could not install the TinyFish CLI", { cause: result.error });
326
340
  }
327
341
  }
328
- function installWebSkill(client) {
329
- errLine(`Installing the TinyFish web skill in ${client.displayName}...`);
330
- const result = spawn.sync("npx", [
342
+ /** `skills add` is an unconditional overwrite, so it doubles as the refresh path. */
343
+ function skillAddArgs(skillAgents) {
344
+ return [
331
345
  "-y",
332
346
  SKILLS_CLI_PACKAGE,
333
347
  "add",
@@ -336,13 +350,21 @@ function installWebSkill(client) {
336
350
  TINYFISH_WEB_SKILL,
337
351
  "--global",
338
352
  "--agent",
339
- client.skillAgent,
353
+ ...skillAgents,
340
354
  "--yes",
341
- ], {
342
- env: {
343
- ...process.env,
344
- PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`,
345
- },
355
+ ];
356
+ }
357
+ // npx resolves from PATH, which a global npm install may not have exported yet.
358
+ function skillSpawnEnv() {
359
+ return {
360
+ ...process.env,
361
+ PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`,
362
+ };
363
+ }
364
+ function installWebSkill(client) {
365
+ errLine(`Installing the TinyFish web skill in ${client.displayName}...`);
366
+ const result = spawn.sync("npx", skillAddArgs([client.skillAgent]), {
367
+ env: skillSpawnEnv(),
346
368
  stdio: "inherit",
347
369
  timeout: SKILL_INSTALL_TIMEOUT_MS,
348
370
  });
@@ -353,6 +375,94 @@ function installWebSkill(client) {
353
375
  });
354
376
  }
355
377
  }
378
+ // The `skills` update flow exits 0 even when it fails, so status alone proves nothing.
379
+ // Safe to match on because SKILLS_CLI_PACKAGE is pinned.
380
+ const SKILL_UPDATE_FAILURE_PATTERN = /Failed to (?:update|check|fetch)/;
381
+ const SKILL_NOT_INSTALLED_PATTERN = /No installed skills found matching/;
382
+ // A lock entry with no recorded hash is untrackable, so `skills` reports it as skipped rather
383
+ // than failed. Left undetected that reads as "up to date" while nothing was refreshed.
384
+ const SKILL_UNCHECKABLE_PATTERN = /cannot be checked automatically/;
385
+ // `skills add` exits non-zero on a fatal error, but per-skill install failures only print.
386
+ const SKILL_INSTALL_FAILURE_PATTERN = /Failed to install/;
387
+ /**
388
+ * Refresh the skill we installed, for the harnesses we installed it in.
389
+ *
390
+ * Reinstalling beats `skills update`: it reports failure through the exit code instead of
391
+ * only in prose, it repairs a copy whose lock entry lost its hash, and it cannot spread the
392
+ * skill to agents the user never connected (`update` re-adds with no --agent, which targets
393
+ * every agent on the machine).
394
+ */
395
+ export function updateWebSkill() {
396
+ // One entry per `tinyfish connect <client>`, persisted by PF-3169.
397
+ const connected = loadConfig().connect ?? {};
398
+ const skillAgents = [CLAUDE_CODE, CODEX, HERMES]
399
+ .filter((client) => connected[client.connectClient])
400
+ .map((client) => client.skillAgent);
401
+ const hasOpenClaw = Boolean(connected["openclaw"]);
402
+ // Installs predating PF-3169 recorded nothing; ask `skills` to refresh what it tracks.
403
+ if (skillAgents.length === 0 && !hasOpenClaw) {
404
+ refreshWebSkillByHash();
405
+ return;
406
+ }
407
+ if (skillAgents.length > 0)
408
+ reinstallWebSkill(skillAgents);
409
+ // OpenClaw keeps its skill under its own CLI, so it needs its own refresh.
410
+ if (hasOpenClaw)
411
+ reinstallOpenClawSkill();
412
+ }
413
+ function reinstallOpenClawSkill() {
414
+ errLine("Refreshing the TinyFish skill in OpenClaw...");
415
+ const result = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, { stdio: "inherit" });
416
+ // OpenClaw may have been removed since connect; a stale context entry must not fail an
417
+ // upgrade that otherwise succeeded.
418
+ if (commandNotFound(result.error)) {
419
+ errLine("OpenClaw is not on PATH, so its TinyFish skill was left unchanged.");
420
+ return;
421
+ }
422
+ if (result.error || result.status !== 0) {
423
+ throwIfInterrupted(result);
424
+ throw new Error("Could not refresh the TinyFish skill in OpenClaw", { cause: result.error });
425
+ }
426
+ }
427
+ function reinstallWebSkill(skillAgents) {
428
+ errLine(`Refreshing the TinyFish web skill for ${skillAgents.join(", ")}...`);
429
+ const result = spawn.sync("npx", skillAddArgs(skillAgents), {
430
+ encoding: "utf8",
431
+ env: skillSpawnEnv(),
432
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
433
+ });
434
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
435
+ if (output.trim())
436
+ errLine(output.trimEnd());
437
+ if (result.error || result.status !== 0 || SKILL_INSTALL_FAILURE_PATTERN.test(output)) {
438
+ throwIfInterrupted(result);
439
+ throw new Error("Could not refresh the TinyFish web skill", { cause: result.error });
440
+ }
441
+ }
442
+ /** Fallback when no harness is recorded: ask `skills` to refresh whatever it tracks. */
443
+ function refreshWebSkillByHash() {
444
+ errLine("Refreshing the TinyFish web skill...");
445
+ const result = spawn.sync("npx", ["-y", SKILLS_CLI_PACKAGE, "update", TINYFISH_WEB_SKILL, "--global", "--yes"], {
446
+ encoding: "utf8",
447
+ env: skillSpawnEnv(),
448
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
449
+ });
450
+ // Piped rather than inherited so the output can be inspected; echoed back for the user.
451
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
452
+ if (output.trim())
453
+ errLine(output.trimEnd());
454
+ if (result.error || result.status !== 0 || SKILL_UPDATE_FAILURE_PATTERN.test(output)) {
455
+ throwIfInterrupted(result);
456
+ throw new Error("Could not refresh the TinyFish web skill", { cause: result.error });
457
+ }
458
+ if (SKILL_UNCHECKABLE_PATTERN.test(output)) {
459
+ errLine("Run `tinyfish connect <client>` to reinstall the skill and restore update tracking.");
460
+ throw new Error("The TinyFish web skill cannot be checked for updates");
461
+ }
462
+ if (SKILL_NOT_INSTALLED_PATTERN.test(output)) {
463
+ errLine("No use-tinyfish skill installed. Run `tinyfish connect <client>` to add it.");
464
+ }
465
+ }
356
466
  function ensureCliAuthenticated(source, apiKey) {
357
467
  const envKey = apiKey ?? process.env["TINYFISH_API_KEY"];
358
468
  if (envKey) {
@@ -487,6 +597,7 @@ async function connectNativeMcpClient(client, options) {
487
597
  // Persist once here (skill installed + authenticated) so both success paths, launch
488
598
  // or not, record the install; buildConnectHeaders reads it on subsequent requests.
489
599
  saveConnectContext(client.connectClient, telemetry.attemptId);
600
+ errLine(UPGRADE_HINT);
490
601
  if (!options.launch) {
491
602
  errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
492
603
  settle(state, telemetry, "completed");
@@ -529,7 +640,9 @@ export async function connectOpenClaw(options) {
529
640
  }
530
641
  state.stage = "skill_install";
531
642
  errLine("Installing the TinyFish skill in OpenClaw...");
532
- const skillInstallResult = spawn.sync("openclaw", ["skills", "install", OPENCLAW_SKILL, "--global", "--force", "--acknowledge-clawhub-risk"], { stdio: "inherit" });
643
+ const skillInstallResult = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, {
644
+ stdio: "inherit",
645
+ });
533
646
  if (skillInstallResult.error || skillInstallResult.status !== 0) {
534
647
  throwIfInterrupted(skillInstallResult);
535
648
  throw new Error("Could not install the TinyFish skill in OpenClaw", {
@@ -562,6 +675,7 @@ export async function connectOpenClaw(options) {
562
675
  }
563
676
  telemetry.track("checkpoint", { phase: "authenticated" });
564
677
  saveConnectContext("openclaw", telemetry.attemptId);
678
+ errLine(UPGRADE_HINT);
565
679
  if (!options.launch) {
566
680
  errLine("TinyFish is connected. Open OpenClaw to start using it.");
567
681
  settle(state, telemetry, "completed");
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ export declare function runUpgrade(): void;
3
+ export declare function registerUpgrade(program: Command): void;
@@ -0,0 +1,46 @@
1
+ import { errLine } from "../lib/output.js";
2
+ import { ConnectInterruptedError, installTinyFishCli, updateWebSkill } from "./connect.js";
3
+ const INTERRUPTED_EXIT_CODE = 130;
4
+ function attempt(label, run) {
5
+ try {
6
+ run();
7
+ errLine(`✓ ${label}: done`);
8
+ return undefined;
9
+ }
10
+ catch (error) {
11
+ // Ctrl+C is abandonment, not failure — neither step should be reported as broken.
12
+ if (error instanceof ConnectInterruptedError)
13
+ throw error;
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ errLine(`✗ ${label}: ${message}`);
16
+ return message;
17
+ }
18
+ }
19
+ export function runUpgrade() {
20
+ errLine("Upgrading TinyFish...");
21
+ try {
22
+ // Both steps always run: neither artifact should stay stale because the other failed.
23
+ const failures = [
24
+ attempt("update the TinyFish CLI", installTinyFishCli),
25
+ attempt("refresh the TinyFish web skill", updateWebSkill),
26
+ ].filter((failure) => failure !== undefined);
27
+ if (failures.length > 0)
28
+ throw new Error(`tinyfish upgrade failed: ${failures.join("; ")}`);
29
+ }
30
+ catch (error) {
31
+ if (!(error instanceof ConnectInterruptedError))
32
+ throw error;
33
+ errLine("Upgrade interrupted — run `tinyfish upgrade` again to finish.");
34
+ process.exitCode = INTERRUPTED_EXIT_CODE;
35
+ return;
36
+ }
37
+ errLine("TinyFish is up to date. Restart your agent to pick up the refreshed skill.");
38
+ }
39
+ export function registerUpgrade(program) {
40
+ program
41
+ .command("upgrade")
42
+ .description("Update the TinyFish CLI and the use-tinyfish skill to the latest versions")
43
+ .action(() => {
44
+ runUpgrade();
45
+ });
46
+ }
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { registerRuns } from "./commands/runs.js";
12
12
  import { registerConfigureClaude } from "./commands/config-claude.js";
13
13
  import { registerSearch } from "./commands/search.js";
14
14
  import { registerConnect } from "./commands/connect.js";
15
+ import { registerUpgrade } from "./commands/upgrade.js";
15
16
  import { registerOnboard } from "./commands/onboard.js";
16
17
  const { version } = createRequire(import.meta.url)("../package.json");
17
18
  const program = new Command();
@@ -31,6 +32,7 @@ program
31
32
  registerAuth(program);
32
33
  registerOnboard(program);
33
34
  registerConnect(program);
35
+ registerUpgrade(program);
34
36
  registerConfigureClaude(program);
35
37
  registerBrowser(program);
36
38
  registerProfile(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.11.1-next.132",
3
+ "version": "0.12.1-next.135",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {