@tiny-fish/cli 0.19.1-next.178 → 0.19.1-next.179

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,23 +1,12 @@
1
1
  import { Command } from "commander";
2
2
  import { type AgentClient } from "../lib/connect-runtime.js";
3
3
  export declare const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
4
- export declare const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
5
4
  interface NativeConnectOptions {
6
5
  apiKey?: string;
7
6
  mcpUrl: string;
8
7
  launch: boolean;
9
8
  attemptId?: string;
10
9
  }
11
- export declare function installTinyFishCli(): void;
12
- /**
13
- * Refresh the skill we installed, for the harnesses we installed it in.
14
- *
15
- * Reinstalling beats `skills update`: it reports failure through the exit code instead of
16
- * only in prose, it repairs a copy whose lock entry lost its hash, and it cannot spread the
17
- * skill to agents the user never connected (`update` re-adds with no --agent, which targets
18
- * every agent on the machine).
19
- */
20
- export declare function updateWebSkill(): void;
21
10
  export declare function connectClaudeCode(options: NativeConnectOptions): Promise<void>;
22
11
  export declare function connectCodex(options: NativeConnectOptions): Promise<void>;
23
12
  export declare function connectHermes(options: NativeConnectOptions): Promise<void>;
@@ -1,9 +1,8 @@
1
- import * as path from "node:path";
2
1
  import spawn from "cross-spawn";
3
- import { CONNECT_SOURCE, loadConfig, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, writeConfig, } from "../lib/auth.js";
4
- import { CLAUDE_CODE, CODEX, HERMES, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, OPENCODE, launchNativeMcpClient, launchOpenClawWalkthrough, } from "../lib/connect-clients.js";
2
+ import { CONNECT_SOURCE, saveConnectContext, validateKeyFormat, validatedApiKey, } from "../lib/auth.js";
3
+ import { CLAUDE_CODE, CODEX, HERMES, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, OPENCODE, launchNativeMcpClient, launchOpenClawWalkthrough, openExternalUrl, } from "../lib/connect-clients.js";
4
+ import { ensureCliAuthenticated, installTinyFishCli, installWebSkill, SKILL_INSTALL_TIMEOUT_MS, TINYFISH_CLI_NOT_FOUND_MESSAGE, UPGRADE_HINT, } from "../lib/connect-install.js";
5
5
  import { commandNotFound, ConnectInterruptedError, createConnectTelemetry, requireCommandSupport, runGuarded, settle, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "../lib/connect-runtime.js";
6
- import { TINYFISH_CLI_PACKAGE } from "../lib/constants.js";
7
6
  import { cursorInstallDeeplink, cursorMcpPath, writeCursorMcpConfig, } from "../lib/cursor-config.js";
8
7
  import { runConnectAll } from "../lib/connect-all.js";
9
8
  import { detectHumanInitiated } from "../lib/harness.js";
@@ -12,181 +11,6 @@ import { errLine } from "../lib/output.js";
12
11
  import { z } from "zod";
13
12
  import { verifyMcpAuth } from "../lib/verify.js";
14
13
  export const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
15
- const SKILL_INSTALL_TIMEOUT_MS = 120_000;
16
- const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
17
- const TINYFISH_CLI_INSTALL_SPEC = `${TINYFISH_CLI_PACKAGE}@latest`;
18
- export const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
19
- // Supports Hermes without node:util.styleText, so the installer still runs on Node 20.11.
20
- const SKILLS_CLI_PACKAGE = "skills@1.5.15";
21
- const TINYFISH_WEB_SKILL_SOURCE = "tinyfish-io/tinyfish-cookbook";
22
- const TINYFISH_WEB_SKILL = "use-tinyfish";
23
- export function installTinyFishCli() {
24
- errLine("Installing the TinyFish CLI...");
25
- const result = spawn.sync("npm", ["install", "--global", TINYFISH_CLI_INSTALL_SPEC], {
26
- stdio: "inherit",
27
- timeout: SKILL_INSTALL_TIMEOUT_MS,
28
- });
29
- if (result.error || result.status !== 0) {
30
- throwIfInterrupted(result);
31
- throw new Error("Could not install the TinyFish CLI", { cause: result.error });
32
- }
33
- }
34
- /** `skills add` is an unconditional overwrite, so it doubles as the refresh path. */
35
- function skillAddArgs(skillAgents) {
36
- return [
37
- "-y",
38
- SKILLS_CLI_PACKAGE,
39
- "add",
40
- TINYFISH_WEB_SKILL_SOURCE,
41
- "--skill",
42
- TINYFISH_WEB_SKILL,
43
- "--global",
44
- "--agent",
45
- ...skillAgents,
46
- "--yes",
47
- ];
48
- }
49
- // npx resolves from PATH, which a global npm install may not have exported yet.
50
- function skillSpawnEnv() {
51
- return {
52
- ...process.env,
53
- PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`,
54
- };
55
- }
56
- function installWebSkill(client) {
57
- errLine(`Installing the TinyFish web skill in ${client.displayName}...`);
58
- const result = spawn.sync("npx", skillAddArgs([client.skillAgent]), {
59
- env: skillSpawnEnv(),
60
- stdio: "inherit",
61
- timeout: SKILL_INSTALL_TIMEOUT_MS,
62
- });
63
- if (result.error || result.status !== 0) {
64
- throwIfInterrupted(result);
65
- throw new Error(`Could not install the TinyFish web skill in ${client.displayName}`, {
66
- cause: result.error,
67
- });
68
- }
69
- }
70
- // The `skills` update flow exits 0 even when it fails, so status alone proves nothing.
71
- // Safe to match on because SKILLS_CLI_PACKAGE is pinned.
72
- const SKILL_UPDATE_FAILURE_PATTERN = /Failed to (?:update|check|fetch)/;
73
- const SKILL_NOT_INSTALLED_PATTERN = /No installed skills found matching/;
74
- // A lock entry with no recorded hash is untrackable, so `skills` reports it as skipped rather
75
- // than failed. Left undetected that reads as "up to date" while nothing was refreshed.
76
- const SKILL_UNCHECKABLE_PATTERN = /cannot be checked automatically/;
77
- // `skills add` exits non-zero on a fatal error, but per-skill install failures only print.
78
- const SKILL_INSTALL_FAILURE_PATTERN = /Failed to install/;
79
- /**
80
- * Refresh the skill we installed, for the harnesses we installed it in.
81
- *
82
- * Reinstalling beats `skills update`: it reports failure through the exit code instead of
83
- * only in prose, it repairs a copy whose lock entry lost its hash, and it cannot spread the
84
- * skill to agents the user never connected (`update` re-adds with no --agent, which targets
85
- * every agent on the machine).
86
- */
87
- export function updateWebSkill() {
88
- // One entry per `tinyfish connect <client>`, persisted by PF-3169.
89
- const connected = loadConfig().connect ?? {};
90
- const skillAgents = [CLAUDE_CODE, CODEX, HERMES, OPENCODE]
91
- .filter((client) => connected[client.connectClient])
92
- .map((client) => client.skillAgent);
93
- const hasOpenClaw = Boolean(connected["openclaw"]);
94
- // Installs predating PF-3169 recorded nothing; ask `skills` to refresh what it tracks.
95
- if (skillAgents.length === 0 && !hasOpenClaw) {
96
- refreshWebSkillByHash();
97
- return;
98
- }
99
- if (skillAgents.length > 0)
100
- reinstallWebSkill(skillAgents);
101
- // OpenClaw keeps its skill under its own CLI, so it needs its own refresh.
102
- if (hasOpenClaw)
103
- reinstallOpenClawSkill();
104
- }
105
- function reinstallOpenClawSkill() {
106
- errLine("Refreshing the TinyFish skill in OpenClaw...");
107
- const result = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, { stdio: "inherit" });
108
- // OpenClaw may have been removed since connect; a stale context entry must not fail an
109
- // upgrade that otherwise succeeded.
110
- if (commandNotFound(result.error)) {
111
- errLine("OpenClaw is not on PATH, so its TinyFish skill was left unchanged.");
112
- return;
113
- }
114
- if (result.error || result.status !== 0) {
115
- throwIfInterrupted(result);
116
- throw new Error("Could not refresh the TinyFish skill in OpenClaw", { cause: result.error });
117
- }
118
- }
119
- function reinstallWebSkill(skillAgents) {
120
- errLine(`Refreshing the TinyFish web skill for ${skillAgents.join(", ")}...`);
121
- const result = spawn.sync("npx", skillAddArgs(skillAgents), {
122
- encoding: "utf8",
123
- env: skillSpawnEnv(),
124
- timeout: SKILL_INSTALL_TIMEOUT_MS,
125
- });
126
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
127
- if (output.trim())
128
- errLine(output.trimEnd());
129
- if (result.error || result.status !== 0 || SKILL_INSTALL_FAILURE_PATTERN.test(output)) {
130
- throwIfInterrupted(result);
131
- throw new Error("Could not refresh the TinyFish web skill", { cause: result.error });
132
- }
133
- }
134
- /** Fallback when no harness is recorded: ask `skills` to refresh whatever it tracks. */
135
- function refreshWebSkillByHash() {
136
- errLine("Refreshing the TinyFish web skill...");
137
- const result = spawn.sync("npx", ["-y", SKILLS_CLI_PACKAGE, "update", TINYFISH_WEB_SKILL, "--global", "--yes"], {
138
- encoding: "utf8",
139
- env: skillSpawnEnv(),
140
- timeout: SKILL_INSTALL_TIMEOUT_MS,
141
- });
142
- // Piped rather than inherited so the output can be inspected; echoed back for the user.
143
- const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
144
- if (output.trim())
145
- errLine(output.trimEnd());
146
- if (result.error || result.status !== 0 || SKILL_UPDATE_FAILURE_PATTERN.test(output)) {
147
- throwIfInterrupted(result);
148
- throw new Error("Could not refresh the TinyFish web skill", { cause: result.error });
149
- }
150
- if (SKILL_UNCHECKABLE_PATTERN.test(output)) {
151
- errLine("Run `tinyfish connect <client>` to reinstall the skill and restore update tracking.");
152
- throw new Error("The TinyFish web skill cannot be checked for updates");
153
- }
154
- if (SKILL_NOT_INSTALLED_PATTERN.test(output)) {
155
- errLine("No use-tinyfish skill installed. Run `tinyfish connect <client>` to add it.");
156
- }
157
- }
158
- function ensureCliAuthenticated(source, apiKey) {
159
- const envKey = apiKey ?? process.env["TINYFISH_API_KEY"];
160
- if (envKey) {
161
- if (!validateKeyFormat(envKey)) {
162
- throw new Error("TINYFISH_API_KEY has invalid format");
163
- }
164
- // Throwing variant: a write failure must reach connect's catch/flush, not exit(1) past it.
165
- writeConfig(envKey);
166
- persistApiKeyToEnvironment(envKey);
167
- return;
168
- }
169
- const authStatus = spawn.sync("tinyfish", ["auth", "status"], {
170
- stdio: "ignore",
171
- timeout: NON_INTERACTIVE_TIMEOUT_MS,
172
- });
173
- if (commandNotFound(authStatus.error)) {
174
- throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: authStatus.error });
175
- }
176
- if (authStatus.status === 0)
177
- return;
178
- errLine("Signing in to the TinyFish CLI...");
179
- const loginResult = spawn.sync("tinyfish", ["auth", "login", "--source", source], {
180
- stdio: "inherit",
181
- });
182
- if (commandNotFound(loginResult.error)) {
183
- throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: loginResult.error });
184
- }
185
- if (loginResult.error || loginResult.status !== 0) {
186
- throwIfInterrupted(loginResult);
187
- throw new Error("Could not authenticate the TinyFish CLI", { cause: loginResult.error });
188
- }
189
- }
190
14
  function removeExistingRegistration(client, removal) {
191
15
  const result = spawn.sync(client.command, removal.args, {
192
16
  encoding: "utf8",
@@ -353,6 +177,7 @@ export async function connectOpenClaw(options) {
353
177
  errLine("Installing the TinyFish skill in OpenClaw...");
354
178
  const skillInstallResult = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, {
355
179
  stdio: "inherit",
180
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
356
181
  });
357
182
  if (skillInstallResult.error || skillInstallResult.status !== 0) {
358
183
  throwIfInterrupted(skillInstallResult);
@@ -459,13 +284,7 @@ export async function connectCursor(options) {
459
284
  }
460
285
  /** Best-effort: false when no handler/open fails — caller falls back to reload copy. */
461
286
  function launchCursorDeeplink(mcpUrl) {
462
- const deepLink = cursorInstallDeeplink(mcpUrl);
463
- const [command, args] = process.platform === "darwin"
464
- ? ["open", [deepLink]]
465
- : process.platform === "win32"
466
- ? ["cmd", ["/c", "start", "", deepLink]]
467
- : ["xdg-open", [deepLink]];
468
- const result = spawn.sync(command, args, { stdio: "ignore" });
287
+ const result = openExternalUrl(cursorInstallDeeplink(mcpUrl));
469
288
  return !result.error && result.status === 0;
470
289
  }
471
290
  export function launchAgent(client) {
@@ -5,8 +5,8 @@ import { suppressNotice } from "../lib/notice.js";
5
5
  import { errLine } from "../lib/output.js";
6
6
  import { sendUpgradeCompleted, telemetryDisabled, } from "../lib/setup-telemetry.js";
7
7
  import { installSignalGuard, SIGNAL_EXIT_CODES } from "../lib/signals.js";
8
+ import { installTinyFishCli, updateWebSkill } from "../lib/connect-install.js";
8
9
  import { ConnectInterruptedError } from "../lib/connect-runtime.js";
9
- import { installTinyFishCli, updateWebSkill } from "./connect.js";
10
10
  const VERSION_READ_TIMEOUT_MS = 2_000;
11
11
  const INTERRUPTED_MESSAGE = "Upgrade interrupted — run `tinyfish upgrade` again to finish.";
12
12
  function attempt(label, run) {
@@ -1,3 +1,4 @@
1
+ import spawn from "cross-spawn";
1
2
  import { type AgentClient, type SupportedCommand } from "./connect-runtime.js";
2
3
  export declare const OPENCLAW_SKILL_INSTALL_ARGS: readonly string[];
3
4
  export declare const DEFAULT_ONBOARDING_PROMPT: string;
@@ -25,6 +26,12 @@ export interface NativeMcpClient extends SupportedCommand {
25
26
  postConnectNote?: string;
26
27
  }
27
28
  export declare const CLAUDE_CODE: NativeMcpClient;
29
+ /**
30
+ * `cmd` re-parses its command line after `/c`, so an unquoted `&` in the query string ends the
31
+ * command and truncates the URL. Quoting needs windowsVerbatimArguments, or Node escapes the
32
+ * quotes back out. Untested on Windows.
33
+ */
34
+ export declare function openExternalUrl(url: string): ReturnType<typeof spawn.sync>;
28
35
  export declare const CODEX: NativeMcpClient;
29
36
  export declare const HERMES: NativeMcpClient;
30
37
  export declare const OPENCODE: NativeMcpClient;
@@ -80,16 +80,27 @@ export const CLAUDE_CODE = {
80
80
  ],
81
81
  addArgs: (mcpUrl) => ["mcp", "add", "--scope", "user", "--transport", "http", "tinyfish", mcpUrl],
82
82
  };
83
+ /**
84
+ * `cmd` re-parses its command line after `/c`, so an unquoted `&` in the query string ends the
85
+ * command and truncates the URL. Quoting needs windowsVerbatimArguments, or Node escapes the
86
+ * quotes back out. Untested on Windows.
87
+ */
88
+ export function openExternalUrl(url) {
89
+ if (process.platform === "darwin")
90
+ return spawn.sync("open", [url], { stdio: "ignore" });
91
+ if (process.platform === "win32") {
92
+ return spawn.sync("cmd", ["/c", "start", '""', `"${url}"`], {
93
+ stdio: "ignore",
94
+ windowsVerbatimArguments: true,
95
+ });
96
+ }
97
+ return spawn.sync("xdg-open", [url], { stdio: "ignore" });
98
+ }
83
99
  function launchCodexWalkthrough() {
84
100
  const deepLink = new URL("codex://new");
85
101
  deepLink.searchParams.set("prompt", DEFAULT_ONBOARDING_PROMPT);
86
102
  deepLink.searchParams.set("path", process.cwd());
87
- const [command, args] = process.platform === "darwin"
88
- ? ["open", [deepLink.toString()]]
89
- : process.platform === "win32"
90
- ? ["cmd", ["/c", "start", "", deepLink.toString()]]
91
- : ["xdg-open", [deepLink.toString()]];
92
- const result = spawn.sync(command, args, { stdio: "ignore" });
103
+ const result = openExternalUrl(deepLink.toString());
93
104
  if (result.error || result.status !== 0) {
94
105
  throw new Error("Could not open Codex", { cause: result.error });
95
106
  }
@@ -0,0 +1,17 @@
1
+ import { type NativeMcpClient } from "./connect-clients.js";
2
+ import { type AgentClient } from "./connect-runtime.js";
3
+ export declare const SKILL_INSTALL_TIMEOUT_MS = 120000;
4
+ export declare const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
5
+ export declare const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
6
+ export declare function installTinyFishCli(): void;
7
+ export declare function installWebSkill(client: NativeMcpClient): void;
8
+ /**
9
+ * Refresh the skill we installed, for the harnesses we installed it in.
10
+ *
11
+ * Reinstalling beats `skills update`: it reports failure through the exit code instead of
12
+ * only in prose, it repairs a copy whose lock entry lost its hash, and it cannot spread the
13
+ * skill to agents the user never connected (`update` re-adds with no --agent, which targets
14
+ * every agent on the machine).
15
+ */
16
+ export declare function updateWebSkill(): void;
17
+ export declare function ensureCliAuthenticated(source: AgentClient, apiKey?: string): void;
@@ -0,0 +1,196 @@
1
+ import * as path from "node:path";
2
+ import spawn from "cross-spawn";
3
+ import { loadConfig, persistApiKeyToEnvironment, validateKeyFormat, writeConfig } from "./auth.js";
4
+ import { CLAUDE_CODE, CODEX, HERMES, OPENCLAW_SKILL_INSTALL_ARGS, OPENCODE, } from "./connect-clients.js";
5
+ import { commandNotFound, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "./connect-runtime.js";
6
+ import { TINYFISH_CLI_PACKAGE } from "./constants.js";
7
+ import { errLine } from "./output.js";
8
+ export const SKILL_INSTALL_TIMEOUT_MS = 120_000;
9
+ export const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
10
+ const TINYFISH_CLI_INSTALL_SPEC = `${TINYFISH_CLI_PACKAGE}@latest`;
11
+ export const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
12
+ // Supports Hermes without node:util.styleText, so the installer still runs on Node 20.11.
13
+ const SKILLS_CLI_PACKAGE = "skills@1.5.15";
14
+ const TINYFISH_WEB_SKILL_SOURCE = "tinyfish-io/tinyfish-cookbook";
15
+ const TINYFISH_WEB_SKILL = "use-tinyfish";
16
+ export function installTinyFishCli() {
17
+ errLine("Installing the TinyFish CLI...");
18
+ const result = spawn.sync("npm", ["install", "--global", TINYFISH_CLI_INSTALL_SPEC], {
19
+ stdio: "inherit",
20
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
21
+ });
22
+ if (result.error || result.status !== 0) {
23
+ throwIfInterrupted(result);
24
+ throw new Error("Could not install the TinyFish CLI", { cause: result.error });
25
+ }
26
+ }
27
+ /** `skills add` is an unconditional overwrite, so it doubles as the refresh path. */
28
+ function skillAddArgs(skillAgents) {
29
+ return [
30
+ "-y",
31
+ SKILLS_CLI_PACKAGE,
32
+ "add",
33
+ TINYFISH_WEB_SKILL_SOURCE,
34
+ "--skill",
35
+ TINYFISH_WEB_SKILL,
36
+ "--global",
37
+ "--agent",
38
+ ...skillAgents,
39
+ "--yes",
40
+ ];
41
+ }
42
+ // npx resolves from PATH, which a global npm install may not have exported yet.
43
+ function skillSpawnEnv() {
44
+ return {
45
+ ...process.env,
46
+ PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`,
47
+ };
48
+ }
49
+ // The `skills` update flow exits 0 even when it fails, so status alone proves nothing.
50
+ // Safe to match on because SKILLS_CLI_PACKAGE is pinned.
51
+ const SKILL_UPDATE_FAILURE_PATTERN = /Failed to (?:update|check|fetch)/;
52
+ const SKILL_NOT_INSTALLED_PATTERN = /No installed skills found matching/;
53
+ // A lock entry with no recorded hash is untrackable, so `skills` reports it as skipped rather
54
+ // than failed. Left undetected that reads as "up to date" while nothing was refreshed.
55
+ const SKILL_UNCHECKABLE_PATTERN = /cannot be checked automatically/;
56
+ // `skills add` exits non-zero on a fatal error, but per-skill install failures only print.
57
+ const SKILL_INSTALL_FAILURE_PATTERN = /Failed to install/;
58
+ export function installWebSkill(client) {
59
+ errLine(`Installing the TinyFish web skill in ${client.displayName}...`);
60
+ // Piped rather than inherited so the per-skill failure line can be matched; echoed back
61
+ // so the user still sees what `skills` reported.
62
+ const result = spawn.sync("npx", skillAddArgs([client.skillAgent]), {
63
+ encoding: "utf8",
64
+ env: skillSpawnEnv(),
65
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
66
+ });
67
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
68
+ if (output.trim())
69
+ errLine(output.trimEnd());
70
+ if (result.error || result.status !== 0 || SKILL_INSTALL_FAILURE_PATTERN.test(output)) {
71
+ throwIfInterrupted(result);
72
+ throw new Error(`Could not install the TinyFish web skill in ${client.displayName}`, {
73
+ cause: result.error,
74
+ });
75
+ }
76
+ }
77
+ /**
78
+ * Refresh the skill we installed, for the harnesses we installed it in.
79
+ *
80
+ * Reinstalling beats `skills update`: it reports failure through the exit code instead of
81
+ * only in prose, it repairs a copy whose lock entry lost its hash, and it cannot spread the
82
+ * skill to agents the user never connected (`update` re-adds with no --agent, which targets
83
+ * every agent on the machine).
84
+ */
85
+ export function updateWebSkill() {
86
+ // One entry per `tinyfish connect <client>`, persisted by PF-3169.
87
+ const connected = loadConfig().connect ?? {};
88
+ const skillAgents = [CLAUDE_CODE, CODEX, HERMES, OPENCODE]
89
+ .filter((client) => connected[client.connectClient])
90
+ .map((client) => client.skillAgent);
91
+ const hasOpenClaw = Boolean(connected["openclaw"]);
92
+ // Installs predating PF-3169 recorded nothing; ask `skills` to refresh what it tracks.
93
+ if (skillAgents.length === 0 && !hasOpenClaw) {
94
+ refreshWebSkillByHash();
95
+ return;
96
+ }
97
+ if (skillAgents.length > 0)
98
+ reinstallWebSkill(skillAgents);
99
+ // OpenClaw keeps its skill under its own CLI, so it needs its own refresh.
100
+ if (hasOpenClaw)
101
+ reinstallOpenClawSkill();
102
+ }
103
+ function reinstallOpenClawSkill() {
104
+ errLine("Refreshing the TinyFish skill in OpenClaw...");
105
+ const result = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, {
106
+ stdio: "inherit",
107
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
108
+ });
109
+ // OpenClaw may have been removed since connect; a stale context entry must not fail an
110
+ // upgrade that otherwise succeeded.
111
+ if (commandNotFound(result.error)) {
112
+ errLine("OpenClaw is not on PATH, so its TinyFish skill was left unchanged.");
113
+ return;
114
+ }
115
+ if (result.error || result.status !== 0) {
116
+ throwIfInterrupted(result);
117
+ throw new Error("Could not refresh the TinyFish skill in OpenClaw", { cause: result.error });
118
+ }
119
+ }
120
+ function reinstallWebSkill(skillAgents) {
121
+ errLine(`Refreshing the TinyFish web skill for ${skillAgents.join(", ")}...`);
122
+ const result = spawn.sync("npx", skillAddArgs(skillAgents), {
123
+ encoding: "utf8",
124
+ env: skillSpawnEnv(),
125
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
126
+ });
127
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
128
+ if (output.trim())
129
+ errLine(output.trimEnd());
130
+ if (result.error || result.status !== 0 || SKILL_INSTALL_FAILURE_PATTERN.test(output)) {
131
+ throwIfInterrupted(result);
132
+ throw new Error("Could not refresh the TinyFish web skill", { cause: result.error });
133
+ }
134
+ }
135
+ /** Fallback when no harness is recorded: ask `skills` to refresh whatever it tracks. */
136
+ function refreshWebSkillByHash() {
137
+ errLine("Refreshing the TinyFish web skill...");
138
+ const result = spawn.sync("npx", ["-y", SKILLS_CLI_PACKAGE, "update", TINYFISH_WEB_SKILL, "--global", "--yes"], {
139
+ encoding: "utf8",
140
+ env: skillSpawnEnv(),
141
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
142
+ });
143
+ // Piped rather than inherited so the output can be inspected; echoed back for the user.
144
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
145
+ if (output.trim())
146
+ errLine(output.trimEnd());
147
+ if (result.error || result.status !== 0 || SKILL_UPDATE_FAILURE_PATTERN.test(output)) {
148
+ throwIfInterrupted(result);
149
+ throw new Error("Could not refresh the TinyFish web skill", { cause: result.error });
150
+ }
151
+ if (SKILL_UNCHECKABLE_PATTERN.test(output)) {
152
+ errLine("Run `tinyfish connect <client>` to reinstall the skill and restore update tracking.");
153
+ throw new Error("The TinyFish web skill cannot be checked for updates");
154
+ }
155
+ if (SKILL_NOT_INSTALLED_PATTERN.test(output)) {
156
+ errLine("No use-tinyfish skill installed. Run `tinyfish connect <client>` to add it.");
157
+ }
158
+ }
159
+ export function ensureCliAuthenticated(source, apiKey) {
160
+ const envKey = apiKey ?? process.env["TINYFISH_API_KEY"];
161
+ if (envKey) {
162
+ if (!validateKeyFormat(envKey)) {
163
+ throw new Error("TINYFISH_API_KEY has invalid format");
164
+ }
165
+ // Throwing variant: a write failure must reach connect's catch/flush, not exit(1) past it.
166
+ writeConfig(envKey);
167
+ persistApiKeyToEnvironment(envKey);
168
+ return;
169
+ }
170
+ const authStatus = spawn.sync("tinyfish", ["auth", "status"], {
171
+ stdio: "ignore",
172
+ timeout: NON_INTERACTIVE_TIMEOUT_MS,
173
+ });
174
+ if (commandNotFound(authStatus.error)) {
175
+ throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: authStatus.error });
176
+ }
177
+ if (authStatus.status === 0)
178
+ return;
179
+ // A crashed or timed-out probe says nothing about the stored key, so it must not read as
180
+ // "not signed in" and drop the user into an interactive login they never asked for.
181
+ throwIfInterrupted(authStatus);
182
+ if (authStatus.error) {
183
+ throw new Error("Could not read TinyFish CLI auth status", { cause: authStatus.error });
184
+ }
185
+ errLine("Signing in to the TinyFish CLI...");
186
+ const loginResult = spawn.sync("tinyfish", ["auth", "login", "--source", source], {
187
+ stdio: "inherit",
188
+ });
189
+ if (commandNotFound(loginResult.error)) {
190
+ throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: loginResult.error });
191
+ }
192
+ if (loginResult.error || loginResult.status !== 0) {
193
+ throwIfInterrupted(loginResult);
194
+ throw new Error("Could not authenticate the TinyFish CLI", { cause: loginResult.error });
195
+ }
196
+ }
@@ -178,9 +178,10 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
178
178
  errLine(`connect telemetry failed: ${error instanceof Error ? error.message : String(error)}`);
179
179
  }));
180
180
  },
181
- // Awaited in finally so in-flight events land before exit.
181
+ // Awaited in finally so in-flight events land before exit. Drains, so the signal-guard
182
+ // flush and the finally flush do not re-await the same events.
182
183
  async flush() {
183
- await Promise.all(pending);
184
+ await Promise.all(pending.splice(0));
184
185
  },
185
186
  };
186
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.19.1-next.178",
3
+ "version": "0.19.1-next.179",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {