@tiny-fish/cli 0.19.1-next.177 → 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.
- package/dist/commands/connect.d.ts +0 -13
- package/dist/commands/connect.js +6 -421
- package/dist/commands/upgrade.js +1 -1
- package/dist/lib/connect-clients.d.ts +40 -0
- package/dist/lib/connect-clients.js +249 -0
- package/dist/lib/connect-install.d.ts +17 -0
- package/dist/lib/connect-install.js +196 -0
- package/dist/lib/connect-runtime.js +3 -2
- package/package.json +1 -1
|
@@ -1,25 +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
|
-
export declare const DEFAULT_ONBOARDING_PROMPT: string;
|
|
6
|
-
export declare const OPENCLAW_ONBOARDING_PROMPT: string;
|
|
7
4
|
interface NativeConnectOptions {
|
|
8
5
|
apiKey?: string;
|
|
9
6
|
mcpUrl: string;
|
|
10
7
|
launch: boolean;
|
|
11
8
|
attemptId?: string;
|
|
12
9
|
}
|
|
13
|
-
export declare function installTinyFishCli(): void;
|
|
14
|
-
/**
|
|
15
|
-
* Refresh the skill we installed, for the harnesses we installed it in.
|
|
16
|
-
*
|
|
17
|
-
* Reinstalling beats `skills update`: it reports failure through the exit code instead of
|
|
18
|
-
* only in prose, it repairs a copy whose lock entry lost its hash, and it cannot spread the
|
|
19
|
-
* skill to agents the user never connected (`update` re-adds with no --agent, which targets
|
|
20
|
-
* every agent on the machine).
|
|
21
|
-
*/
|
|
22
|
-
export declare function updateWebSkill(): void;
|
|
23
10
|
export declare function connectClaudeCode(options: NativeConnectOptions): Promise<void>;
|
|
24
11
|
export declare function connectCodex(options: NativeConnectOptions): Promise<void>;
|
|
25
12
|
export declare function connectHermes(options: NativeConnectOptions): Promise<void>;
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,394 +1,16 @@
|
|
|
1
|
-
import * as path from "node:path";
|
|
2
1
|
import spawn from "cross-spawn";
|
|
3
|
-
import { CONNECT_SOURCE,
|
|
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";
|
|
4
5
|
import { commandNotFound, ConnectInterruptedError, createConnectTelemetry, requireCommandSupport, runGuarded, settle, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "../lib/connect-runtime.js";
|
|
5
|
-
import { TINYFISH_CLI_PACKAGE } from "../lib/constants.js";
|
|
6
6
|
import { cursorInstallDeeplink, cursorMcpPath, writeCursorMcpConfig, } from "../lib/cursor-config.js";
|
|
7
7
|
import { runConnectAll } from "../lib/connect-all.js";
|
|
8
8
|
import { detectHumanInitiated } from "../lib/harness.js";
|
|
9
9
|
import { emitNotice } from "../lib/notice.js";
|
|
10
|
-
import { errLine
|
|
11
|
-
import { codexOauthCompleted } from "../lib/registration-detect.js";
|
|
10
|
+
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 HERMES_SEED_TIMEOUT_MS = 120_000;
|
|
17
|
-
// An old install is only one reason a flag can go unseen, so no message asserts the cause.
|
|
18
|
-
const SUPPORT_CHECK_DEBUG_HINT = " Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.";
|
|
19
|
-
const MCP_ADD_UNAVAILABLE_MESSAGE = "Could not confirm this Claude Code installation exposes `claude mcp add`: `claude mcp " +
|
|
20
|
-
"--help` did not list it. Update Claude Code with `claude update` and retry." +
|
|
21
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
22
|
-
// `claude mcp login` only exists upstream from Claude Code 2.1.186; older installs still register.
|
|
23
|
-
const CLAUDE_CODE_KEYED_FALLBACK_NOTE = "Using your stored TinyFish API key: this Claude Code predates one-command sign-in " +
|
|
24
|
-
"(`claude mcp login`), so no browser sign-in is needed.";
|
|
25
|
-
const CLAUDE_CODE_DEFERRED_AUTH_NOTE = "TinyFish is registered in Claude Code but not signed in: this Claude Code predates " +
|
|
26
|
-
"one-command sign-in (`claude mcp login`). Open Claude Code and run `/mcp` to sign in to " +
|
|
27
|
-
"TinyFish. `claude update` gets you one-command sign-in.";
|
|
28
|
-
const CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE = "Could not confirm this Codex installation supports one-command MCP authentication: `codex " +
|
|
29
|
-
"mcp add --help` did not list `--oauth-resource`. Update Codex and retry, or add TinyFish " +
|
|
30
|
-
"manually with `codex mcp add`." +
|
|
31
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
32
|
-
const HERMES_OAUTH_UNAVAILABLE_MESSAGE = "Could not confirm this Hermes installation supports one-command MCP authentication: `hermes " +
|
|
33
|
-
"mcp add --help` did not list `--auth oauth`. Update Hermes and retry, or add TinyFish " +
|
|
34
|
-
"manually with `hermes mcp add`." +
|
|
35
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
36
|
-
const OPENCLAW_SKILL_UNAVAILABLE_MESSAGE = "Could not confirm this OpenClaw installation supports global skill installation: `openclaw " +
|
|
37
|
-
"skills install --help` did not list `--global` and `--acknowledge-clawhub-risk`. Update " +
|
|
38
|
-
"OpenClaw and retry, or install manually with `openclaw skills install @tinyfish/tinyfish " +
|
|
39
|
-
"--global --acknowledge-clawhub-risk`." +
|
|
40
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
41
|
-
const OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE = "Could not confirm this OpenCode installation supports one-command MCP setup: `opencode mcp " +
|
|
42
|
-
"add --help` did not list `--url`. Update OpenCode and retry, or add TinyFish manually with " +
|
|
43
|
-
"`opencode mcp add`." +
|
|
44
|
-
SUPPORT_CHECK_DEBUG_HINT;
|
|
45
|
-
// OpenCode is model-agnostic (unlike Claude Code / Codex), so a fresh install can default to a
|
|
46
|
-
// model with no tool support — TinyFish runs entirely on tool calls, so it would fail there.
|
|
47
|
-
const OPENCODE_MODEL_NOTE = "Note: TinyFish runs on tool calls, so OpenCode needs a model that supports tool use. Image-only " +
|
|
48
|
-
'models (e.g. Nano Banana Pro) will show "No endpoints found that support tool use" — switch ' +
|
|
49
|
-
"OpenCode's model if the walkthrough can't start.";
|
|
50
|
-
const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
|
|
51
|
-
const TINYFISH_CLI_INSTALL_SPEC = `${TINYFISH_CLI_PACKAGE}@latest`;
|
|
52
|
-
export const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
|
|
53
|
-
// Supports Hermes without node:util.styleText, so the installer still runs on Node 20.11.
|
|
54
|
-
const SKILLS_CLI_PACKAGE = "skills@1.5.15";
|
|
55
|
-
const TINYFISH_WEB_SKILL_SOURCE = "tinyfish-io/tinyfish-cookbook";
|
|
56
|
-
const TINYFISH_WEB_SKILL = "use-tinyfish";
|
|
57
|
-
const OPENCLAW_SKILL = "@tinyfish/tinyfish";
|
|
58
|
-
// --force makes this an unconditional overwrite, so the same call installs and refreshes.
|
|
59
|
-
const OPENCLAW_SKILL_INSTALL_ARGS = [
|
|
60
|
-
"skills",
|
|
61
|
-
"install",
|
|
62
|
-
OPENCLAW_SKILL,
|
|
63
|
-
"--global",
|
|
64
|
-
"--force",
|
|
65
|
-
"--acknowledge-clawhub-risk",
|
|
66
|
-
];
|
|
67
|
-
export const DEFAULT_ONBOARDING_PROMPT = "I just connected TinyFish. Call the guide_next_step tool now to begin. Then follow its " +
|
|
68
|
-
"instructions one step at a time. Ask for my input and wait for my reply before each subsequent " +
|
|
69
|
-
"TinyFish tool call.";
|
|
70
|
-
export const OPENCLAW_ONBOARDING_PROMPT = "I just installed the TinyFish skill. Guide me through it interactively: first ask what topic I " +
|
|
71
|
-
"want to search and wait for my reply before using TinyFish Search. Show me the results, ask " +
|
|
72
|
-
"which result I want to read, and wait before using TinyFish Fetch. Then ask what browser task " +
|
|
73
|
-
"I want to complete and wait before using TinyFish Agent. Never skip ahead or choose for me.";
|
|
74
|
-
const CLAUDE_CODE = {
|
|
75
|
-
command: "claude",
|
|
76
|
-
connectClient: "claude-code",
|
|
77
|
-
skillAgent: "claude-code",
|
|
78
|
-
displayName: "Claude Code",
|
|
79
|
-
supportCheck: {
|
|
80
|
-
args: ["mcp", "--help"],
|
|
81
|
-
patterns: [/^\s*add(?:\s|\[)/m],
|
|
82
|
-
optionalPatterns: [/^\s*login(?:\s|\[)/m],
|
|
83
|
-
unavailableMessage: MCP_ADD_UNAVAILABLE_MESSAGE,
|
|
84
|
-
},
|
|
85
|
-
headerAuthSupported: true,
|
|
86
|
-
loginArgs: ["mcp", "login", "tinyfish"],
|
|
87
|
-
degradedAuthNotes: {
|
|
88
|
-
keyed: CLAUDE_CODE_KEYED_FALLBACK_NOTE,
|
|
89
|
-
deferred: CLAUDE_CODE_DEFERRED_AUTH_NOTE,
|
|
90
|
-
},
|
|
91
|
-
// Project scope is shared in .mcp.json; a user setup command must not rewrite it.
|
|
92
|
-
removals: [
|
|
93
|
-
{ args: ["mcp", "remove", "tinyfish", "--scope", "user"], label: "user TinyFish registration" },
|
|
94
|
-
{
|
|
95
|
-
args: ["mcp", "remove", "tinyfish", "--scope", "local"],
|
|
96
|
-
label: "local TinyFish registration",
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
addArgs: (mcpUrl) => ["mcp", "add", "--scope", "user", "--transport", "http", "tinyfish", mcpUrl],
|
|
100
|
-
};
|
|
101
|
-
function launchCodexWalkthrough() {
|
|
102
|
-
const deepLink = new URL("codex://new");
|
|
103
|
-
deepLink.searchParams.set("prompt", DEFAULT_ONBOARDING_PROMPT);
|
|
104
|
-
deepLink.searchParams.set("path", process.cwd());
|
|
105
|
-
const [command, args] = process.platform === "darwin"
|
|
106
|
-
? ["open", [deepLink.toString()]]
|
|
107
|
-
: process.platform === "win32"
|
|
108
|
-
? ["cmd", ["/c", "start", "", deepLink.toString()]]
|
|
109
|
-
: ["xdg-open", [deepLink.toString()]];
|
|
110
|
-
const result = spawn.sync(command, args, { stdio: "ignore" });
|
|
111
|
-
if (result.error || result.status !== 0) {
|
|
112
|
-
throw new Error("Could not open Codex", { cause: result.error });
|
|
113
|
-
}
|
|
114
|
-
errLine("Codex opened with the TinyFish walkthrough ready. Send the prompt to start.");
|
|
115
|
-
}
|
|
116
|
-
const CODEX = {
|
|
117
|
-
command: "codex",
|
|
118
|
-
connectClient: "codex",
|
|
119
|
-
skillAgent: "codex",
|
|
120
|
-
displayName: "Codex",
|
|
121
|
-
supportCheck: {
|
|
122
|
-
args: ["mcp", "add", "--help"],
|
|
123
|
-
patterns: [/--oauth-resource(?:\s|<)/],
|
|
124
|
-
unavailableMessage: CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE,
|
|
125
|
-
},
|
|
126
|
-
launchWalkthrough: launchCodexWalkthrough,
|
|
127
|
-
probeAuthenticated: codexOauthCompleted,
|
|
128
|
-
// Retry only: running it after a finished inline OAuth is a second browser hop.
|
|
129
|
-
loginArgs: ["mcp", "login", "tinyfish"],
|
|
130
|
-
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Codex" }],
|
|
131
|
-
addArgs: (mcpUrl, oauthResource) => [
|
|
132
|
-
"mcp",
|
|
133
|
-
"add",
|
|
134
|
-
"tinyfish",
|
|
135
|
-
"--url",
|
|
136
|
-
mcpUrl,
|
|
137
|
-
"--oauth-resource",
|
|
138
|
-
oauthResource,
|
|
139
|
-
],
|
|
140
|
-
};
|
|
141
|
-
function launchHermesWalkthrough() {
|
|
142
|
-
const seedResult = spawn.sync("hermes", ["chat", "-Q", "-q", DEFAULT_ONBOARDING_PROMPT], {
|
|
143
|
-
encoding: "utf8",
|
|
144
|
-
timeout: HERMES_SEED_TIMEOUT_MS,
|
|
145
|
-
});
|
|
146
|
-
if (seedResult.error || seedResult.status !== 0) {
|
|
147
|
-
throwIfInterrupted(seedResult);
|
|
148
|
-
throw new Error("Could not start the TinyFish walkthrough in Hermes", {
|
|
149
|
-
cause: seedResult.error,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
// A colourised id would carry its trailing escape sequence into `--resume` (PF-3452).
|
|
153
|
-
const sessionId = sanitizeLine(seedResult.stderr ?? "").match(/session_id:\s*([^\s]+)/i)?.[1];
|
|
154
|
-
if (!sessionId) {
|
|
155
|
-
throw new Error("Hermes did not return a walkthrough session ID");
|
|
156
|
-
}
|
|
157
|
-
const result = spawn.sync("hermes", ["--resume", sessionId], { stdio: "inherit" });
|
|
158
|
-
if (result.error) {
|
|
159
|
-
throw new Error("Could not launch Hermes", { cause: result.error });
|
|
160
|
-
}
|
|
161
|
-
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
162
|
-
return;
|
|
163
|
-
if (result.status !== 0) {
|
|
164
|
-
throw new Error(`Hermes walkthrough exited with status ${result.status ?? "unknown"}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const HERMES = {
|
|
168
|
-
command: "hermes",
|
|
169
|
-
connectClient: "hermes",
|
|
170
|
-
skillAgent: "hermes-agent",
|
|
171
|
-
displayName: "Hermes",
|
|
172
|
-
supportCheck: {
|
|
173
|
-
args: ["mcp", "add", "--help"],
|
|
174
|
-
patterns: [/--auth\s+\{[^}]*oauth[^}]*\}/],
|
|
175
|
-
unavailableMessage: HERMES_OAUTH_UNAVAILABLE_MESSAGE,
|
|
176
|
-
},
|
|
177
|
-
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Hermes" }],
|
|
178
|
-
// Hermes completes OAuth while adding the server; a separate `mcp login` would authenticate twice.
|
|
179
|
-
addArgs: (mcpUrl) => ["mcp", "add", "tinyfish", "--url", mcpUrl, "--auth", "oauth"],
|
|
180
|
-
launchWalkthrough: launchHermesWalkthrough,
|
|
181
|
-
};
|
|
182
|
-
// OpenCode's TUI takes a positional as a project directory, so a bare `opencode "<prompt>"`
|
|
183
|
-
// would be read as a folder. The `--prompt` flag seeds the interactive TUI with a first message
|
|
184
|
-
// (opencode.ai/docs/cli), so the onboarding guide fires just like the other agents.
|
|
185
|
-
function launchOpencode() {
|
|
186
|
-
const result = spawn.sync("opencode", ["--prompt", DEFAULT_ONBOARDING_PROMPT], {
|
|
187
|
-
stdio: "inherit",
|
|
188
|
-
});
|
|
189
|
-
if (result.error) {
|
|
190
|
-
throw new Error("Could not launch OpenCode", { cause: result.error });
|
|
191
|
-
}
|
|
192
|
-
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
193
|
-
return;
|
|
194
|
-
if (result.status !== 0) {
|
|
195
|
-
throw new Error(`OpenCode exited with status ${result.status ?? "unknown"}`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const OPENCODE = {
|
|
199
|
-
command: "opencode",
|
|
200
|
-
connectClient: "opencode",
|
|
201
|
-
skillAgent: "opencode",
|
|
202
|
-
displayName: "OpenCode",
|
|
203
|
-
supportCheck: {
|
|
204
|
-
args: ["mcp", "add", "--help"],
|
|
205
|
-
patterns: [/--url(?:\s|$)/m],
|
|
206
|
-
unavailableMessage: OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE,
|
|
207
|
-
},
|
|
208
|
-
// `opencode mcp add` upserts by name and there is no `opencode mcp remove`, so no cleanup step.
|
|
209
|
-
removals: [],
|
|
210
|
-
// OpenCode writes ~/.config/opencode/opencode.jsonc itself; `mcp auth` is the separate OAuth step.
|
|
211
|
-
addArgs: (mcpUrl) => ["mcp", "add", "tinyfish", "--url", mcpUrl],
|
|
212
|
-
loginArgs: ["mcp", "auth", "tinyfish"],
|
|
213
|
-
launchWalkthrough: launchOpencode,
|
|
214
|
-
postConnectNote: OPENCODE_MODEL_NOTE,
|
|
215
|
-
};
|
|
216
|
-
const OPENCLAW = {
|
|
217
|
-
command: "openclaw",
|
|
218
|
-
displayName: "OpenClaw",
|
|
219
|
-
supportCheck: {
|
|
220
|
-
args: ["skills", "install", "--help"],
|
|
221
|
-
patterns: [/--global(?:\s|$)/, /--acknowledge-clawhub-risk(?:\s|$)/],
|
|
222
|
-
unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
export function installTinyFishCli() {
|
|
226
|
-
errLine("Installing the TinyFish CLI...");
|
|
227
|
-
const result = spawn.sync("npm", ["install", "--global", TINYFISH_CLI_INSTALL_SPEC], {
|
|
228
|
-
stdio: "inherit",
|
|
229
|
-
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
230
|
-
});
|
|
231
|
-
if (result.error || result.status !== 0) {
|
|
232
|
-
throwIfInterrupted(result);
|
|
233
|
-
throw new Error("Could not install the TinyFish CLI", { cause: result.error });
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
/** `skills add` is an unconditional overwrite, so it doubles as the refresh path. */
|
|
237
|
-
function skillAddArgs(skillAgents) {
|
|
238
|
-
return [
|
|
239
|
-
"-y",
|
|
240
|
-
SKILLS_CLI_PACKAGE,
|
|
241
|
-
"add",
|
|
242
|
-
TINYFISH_WEB_SKILL_SOURCE,
|
|
243
|
-
"--skill",
|
|
244
|
-
TINYFISH_WEB_SKILL,
|
|
245
|
-
"--global",
|
|
246
|
-
"--agent",
|
|
247
|
-
...skillAgents,
|
|
248
|
-
"--yes",
|
|
249
|
-
];
|
|
250
|
-
}
|
|
251
|
-
// npx resolves from PATH, which a global npm install may not have exported yet.
|
|
252
|
-
function skillSpawnEnv() {
|
|
253
|
-
return {
|
|
254
|
-
...process.env,
|
|
255
|
-
PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`,
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
function installWebSkill(client) {
|
|
259
|
-
errLine(`Installing the TinyFish web skill in ${client.displayName}...`);
|
|
260
|
-
const result = spawn.sync("npx", skillAddArgs([client.skillAgent]), {
|
|
261
|
-
env: skillSpawnEnv(),
|
|
262
|
-
stdio: "inherit",
|
|
263
|
-
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
264
|
-
});
|
|
265
|
-
if (result.error || result.status !== 0) {
|
|
266
|
-
throwIfInterrupted(result);
|
|
267
|
-
throw new Error(`Could not install the TinyFish web skill in ${client.displayName}`, {
|
|
268
|
-
cause: result.error,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// The `skills` update flow exits 0 even when it fails, so status alone proves nothing.
|
|
273
|
-
// Safe to match on because SKILLS_CLI_PACKAGE is pinned.
|
|
274
|
-
const SKILL_UPDATE_FAILURE_PATTERN = /Failed to (?:update|check|fetch)/;
|
|
275
|
-
const SKILL_NOT_INSTALLED_PATTERN = /No installed skills found matching/;
|
|
276
|
-
// A lock entry with no recorded hash is untrackable, so `skills` reports it as skipped rather
|
|
277
|
-
// than failed. Left undetected that reads as "up to date" while nothing was refreshed.
|
|
278
|
-
const SKILL_UNCHECKABLE_PATTERN = /cannot be checked automatically/;
|
|
279
|
-
// `skills add` exits non-zero on a fatal error, but per-skill install failures only print.
|
|
280
|
-
const SKILL_INSTALL_FAILURE_PATTERN = /Failed to install/;
|
|
281
|
-
/**
|
|
282
|
-
* Refresh the skill we installed, for the harnesses we installed it in.
|
|
283
|
-
*
|
|
284
|
-
* Reinstalling beats `skills update`: it reports failure through the exit code instead of
|
|
285
|
-
* only in prose, it repairs a copy whose lock entry lost its hash, and it cannot spread the
|
|
286
|
-
* skill to agents the user never connected (`update` re-adds with no --agent, which targets
|
|
287
|
-
* every agent on the machine).
|
|
288
|
-
*/
|
|
289
|
-
export function updateWebSkill() {
|
|
290
|
-
// One entry per `tinyfish connect <client>`, persisted by PF-3169.
|
|
291
|
-
const connected = loadConfig().connect ?? {};
|
|
292
|
-
const skillAgents = [CLAUDE_CODE, CODEX, HERMES, OPENCODE]
|
|
293
|
-
.filter((client) => connected[client.connectClient])
|
|
294
|
-
.map((client) => client.skillAgent);
|
|
295
|
-
const hasOpenClaw = Boolean(connected["openclaw"]);
|
|
296
|
-
// Installs predating PF-3169 recorded nothing; ask `skills` to refresh what it tracks.
|
|
297
|
-
if (skillAgents.length === 0 && !hasOpenClaw) {
|
|
298
|
-
refreshWebSkillByHash();
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (skillAgents.length > 0)
|
|
302
|
-
reinstallWebSkill(skillAgents);
|
|
303
|
-
// OpenClaw keeps its skill under its own CLI, so it needs its own refresh.
|
|
304
|
-
if (hasOpenClaw)
|
|
305
|
-
reinstallOpenClawSkill();
|
|
306
|
-
}
|
|
307
|
-
function reinstallOpenClawSkill() {
|
|
308
|
-
errLine("Refreshing the TinyFish skill in OpenClaw...");
|
|
309
|
-
const result = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, { stdio: "inherit" });
|
|
310
|
-
// OpenClaw may have been removed since connect; a stale context entry must not fail an
|
|
311
|
-
// upgrade that otherwise succeeded.
|
|
312
|
-
if (commandNotFound(result.error)) {
|
|
313
|
-
errLine("OpenClaw is not on PATH, so its TinyFish skill was left unchanged.");
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
if (result.error || result.status !== 0) {
|
|
317
|
-
throwIfInterrupted(result);
|
|
318
|
-
throw new Error("Could not refresh the TinyFish skill in OpenClaw", { cause: result.error });
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
function reinstallWebSkill(skillAgents) {
|
|
322
|
-
errLine(`Refreshing the TinyFish web skill for ${skillAgents.join(", ")}...`);
|
|
323
|
-
const result = spawn.sync("npx", skillAddArgs(skillAgents), {
|
|
324
|
-
encoding: "utf8",
|
|
325
|
-
env: skillSpawnEnv(),
|
|
326
|
-
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
327
|
-
});
|
|
328
|
-
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
329
|
-
if (output.trim())
|
|
330
|
-
errLine(output.trimEnd());
|
|
331
|
-
if (result.error || result.status !== 0 || SKILL_INSTALL_FAILURE_PATTERN.test(output)) {
|
|
332
|
-
throwIfInterrupted(result);
|
|
333
|
-
throw new Error("Could not refresh the TinyFish web skill", { cause: result.error });
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
/** Fallback when no harness is recorded: ask `skills` to refresh whatever it tracks. */
|
|
337
|
-
function refreshWebSkillByHash() {
|
|
338
|
-
errLine("Refreshing the TinyFish web skill...");
|
|
339
|
-
const result = spawn.sync("npx", ["-y", SKILLS_CLI_PACKAGE, "update", TINYFISH_WEB_SKILL, "--global", "--yes"], {
|
|
340
|
-
encoding: "utf8",
|
|
341
|
-
env: skillSpawnEnv(),
|
|
342
|
-
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
343
|
-
});
|
|
344
|
-
// Piped rather than inherited so the output can be inspected; echoed back for the user.
|
|
345
|
-
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
346
|
-
if (output.trim())
|
|
347
|
-
errLine(output.trimEnd());
|
|
348
|
-
if (result.error || result.status !== 0 || SKILL_UPDATE_FAILURE_PATTERN.test(output)) {
|
|
349
|
-
throwIfInterrupted(result);
|
|
350
|
-
throw new Error("Could not refresh the TinyFish web skill", { cause: result.error });
|
|
351
|
-
}
|
|
352
|
-
if (SKILL_UNCHECKABLE_PATTERN.test(output)) {
|
|
353
|
-
errLine("Run `tinyfish connect <client>` to reinstall the skill and restore update tracking.");
|
|
354
|
-
throw new Error("The TinyFish web skill cannot be checked for updates");
|
|
355
|
-
}
|
|
356
|
-
if (SKILL_NOT_INSTALLED_PATTERN.test(output)) {
|
|
357
|
-
errLine("No use-tinyfish skill installed. Run `tinyfish connect <client>` to add it.");
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
function ensureCliAuthenticated(source, apiKey) {
|
|
361
|
-
const envKey = apiKey ?? process.env["TINYFISH_API_KEY"];
|
|
362
|
-
if (envKey) {
|
|
363
|
-
if (!validateKeyFormat(envKey)) {
|
|
364
|
-
throw new Error("TINYFISH_API_KEY has invalid format");
|
|
365
|
-
}
|
|
366
|
-
// Throwing variant: a write failure must reach connect's catch/flush, not exit(1) past it.
|
|
367
|
-
writeConfig(envKey);
|
|
368
|
-
persistApiKeyToEnvironment(envKey);
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
const authStatus = spawn.sync("tinyfish", ["auth", "status"], {
|
|
372
|
-
stdio: "ignore",
|
|
373
|
-
timeout: NON_INTERACTIVE_TIMEOUT_MS,
|
|
374
|
-
});
|
|
375
|
-
if (commandNotFound(authStatus.error)) {
|
|
376
|
-
throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: authStatus.error });
|
|
377
|
-
}
|
|
378
|
-
if (authStatus.status === 0)
|
|
379
|
-
return;
|
|
380
|
-
errLine("Signing in to the TinyFish CLI...");
|
|
381
|
-
const loginResult = spawn.sync("tinyfish", ["auth", "login", "--source", source], {
|
|
382
|
-
stdio: "inherit",
|
|
383
|
-
});
|
|
384
|
-
if (commandNotFound(loginResult.error)) {
|
|
385
|
-
throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: loginResult.error });
|
|
386
|
-
}
|
|
387
|
-
if (loginResult.error || loginResult.status !== 0) {
|
|
388
|
-
throwIfInterrupted(loginResult);
|
|
389
|
-
throw new Error("Could not authenticate the TinyFish CLI", { cause: loginResult.error });
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
14
|
function removeExistingRegistration(client, removal) {
|
|
393
15
|
const result = spawn.sync(client.command, removal.args, {
|
|
394
16
|
encoding: "utf8",
|
|
@@ -407,38 +29,6 @@ function removeExistingRegistration(client, removal) {
|
|
|
407
29
|
}
|
|
408
30
|
throw new Error(`Could not remove existing ${removal.label}: ${details}`);
|
|
409
31
|
}
|
|
410
|
-
function launchNativeMcpClient(client) {
|
|
411
|
-
errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
|
|
412
|
-
if (client.launchWalkthrough) {
|
|
413
|
-
client.launchWalkthrough();
|
|
414
|
-
return true;
|
|
415
|
-
}
|
|
416
|
-
const result = spawn.sync(client.command, [DEFAULT_ONBOARDING_PROMPT], { stdio: "inherit" });
|
|
417
|
-
if (result.error) {
|
|
418
|
-
throw new Error(`Could not launch ${client.displayName}`, { cause: result.error });
|
|
419
|
-
}
|
|
420
|
-
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
421
|
-
return false;
|
|
422
|
-
if (result.status !== 0) {
|
|
423
|
-
throw new Error(`${client.displayName} walkthrough exited with status ${result.status ?? "unknown"}`);
|
|
424
|
-
}
|
|
425
|
-
return true;
|
|
426
|
-
}
|
|
427
|
-
function launchOpenClawWalkthrough() {
|
|
428
|
-
errLine("Starting the TinyFish walkthrough in OpenClaw...");
|
|
429
|
-
const result = spawn.sync("openclaw", ["chat", "--message", OPENCLAW_ONBOARDING_PROMPT], {
|
|
430
|
-
stdio: "inherit",
|
|
431
|
-
});
|
|
432
|
-
if (result.error) {
|
|
433
|
-
throw new Error("Could not launch OpenClaw", { cause: result.error });
|
|
434
|
-
}
|
|
435
|
-
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
436
|
-
return false;
|
|
437
|
-
if (result.status !== 0) {
|
|
438
|
-
throw new Error(`OpenClaw walkthrough exited with status ${result.status ?? "unknown"}`);
|
|
439
|
-
}
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
32
|
async function connectNativeMcpClient(client, options) {
|
|
443
33
|
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
|
|
444
34
|
const state = { stage: "prerequisite_check", settled: false };
|
|
@@ -587,6 +177,7 @@ export async function connectOpenClaw(options) {
|
|
|
587
177
|
errLine("Installing the TinyFish skill in OpenClaw...");
|
|
588
178
|
const skillInstallResult = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, {
|
|
589
179
|
stdio: "inherit",
|
|
180
|
+
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
590
181
|
});
|
|
591
182
|
if (skillInstallResult.error || skillInstallResult.status !== 0) {
|
|
592
183
|
throwIfInterrupted(skillInstallResult);
|
|
@@ -693,13 +284,7 @@ export async function connectCursor(options) {
|
|
|
693
284
|
}
|
|
694
285
|
/** Best-effort: false when no handler/open fails — caller falls back to reload copy. */
|
|
695
286
|
function launchCursorDeeplink(mcpUrl) {
|
|
696
|
-
const
|
|
697
|
-
const [command, args] = process.platform === "darwin"
|
|
698
|
-
? ["open", [deepLink]]
|
|
699
|
-
: process.platform === "win32"
|
|
700
|
-
? ["cmd", ["/c", "start", "", deepLink]]
|
|
701
|
-
: ["xdg-open", [deepLink]];
|
|
702
|
-
const result = spawn.sync(command, args, { stdio: "ignore" });
|
|
287
|
+
const result = openExternalUrl(cursorInstallDeeplink(mcpUrl));
|
|
703
288
|
return !result.error && result.status === 0;
|
|
704
289
|
}
|
|
705
290
|
export function launchAgent(client) {
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -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) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import spawn from "cross-spawn";
|
|
2
|
+
import { type AgentClient, type SupportedCommand } from "./connect-runtime.js";
|
|
3
|
+
export declare const OPENCLAW_SKILL_INSTALL_ARGS: readonly string[];
|
|
4
|
+
export declare const DEFAULT_ONBOARDING_PROMPT: string;
|
|
5
|
+
export declare const OPENCLAW_ONBOARDING_PROMPT: string;
|
|
6
|
+
export interface NativeMcpClient extends SupportedCommand {
|
|
7
|
+
connectClient: Exclude<AgentClient, "openclaw">;
|
|
8
|
+
skillAgent: "claude-code" | "codex" | "hermes-agent" | "opencode";
|
|
9
|
+
/** `mcp add --header` support: a stored TinyFish key replaces the OAuth login. */
|
|
10
|
+
headerAuthSupported?: boolean;
|
|
11
|
+
loginArgs?: string[];
|
|
12
|
+
/** Replaces the `loginArgs` step when `supportCheck.optionalPatterns` miss. */
|
|
13
|
+
degradedAuthNotes?: {
|
|
14
|
+
keyed: string;
|
|
15
|
+
deferred: string;
|
|
16
|
+
};
|
|
17
|
+
/** Did the OAuth inside `mcp add` finish? undefined when the client cannot say. */
|
|
18
|
+
probeAuthenticated?: () => boolean | undefined;
|
|
19
|
+
removals: {
|
|
20
|
+
args: string[];
|
|
21
|
+
label: string;
|
|
22
|
+
}[];
|
|
23
|
+
addArgs: (mcpUrl: string, oauthResource: string) => string[];
|
|
24
|
+
launchWalkthrough?: () => void;
|
|
25
|
+
/** Printed after a successful connect, on both the launch and non-launch paths. */
|
|
26
|
+
postConnectNote?: string;
|
|
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>;
|
|
35
|
+
export declare const CODEX: NativeMcpClient;
|
|
36
|
+
export declare const HERMES: NativeMcpClient;
|
|
37
|
+
export declare const OPENCODE: NativeMcpClient;
|
|
38
|
+
export declare const OPENCLAW: SupportedCommand;
|
|
39
|
+
export declare function launchNativeMcpClient(client: NativeMcpClient): boolean;
|
|
40
|
+
export declare function launchOpenClawWalkthrough(): boolean;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import spawn from "cross-spawn";
|
|
2
|
+
import { errLine, sanitizeLine } from "./output.js";
|
|
3
|
+
import { throwIfInterrupted } from "./connect-runtime.js";
|
|
4
|
+
import { codexOauthCompleted } from "./registration-detect.js";
|
|
5
|
+
const HERMES_SEED_TIMEOUT_MS = 120_000;
|
|
6
|
+
// An old install is only one reason a flag can go unseen, so no message asserts the cause.
|
|
7
|
+
const SUPPORT_CHECK_DEBUG_HINT = " Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.";
|
|
8
|
+
const MCP_ADD_UNAVAILABLE_MESSAGE = "Could not confirm this Claude Code installation exposes `claude mcp add`: `claude mcp " +
|
|
9
|
+
"--help` did not list it. Update Claude Code with `claude update` and retry." +
|
|
10
|
+
SUPPORT_CHECK_DEBUG_HINT;
|
|
11
|
+
// `claude mcp login` only exists upstream from Claude Code 2.1.186; older installs still register.
|
|
12
|
+
const CLAUDE_CODE_KEYED_FALLBACK_NOTE = "Using your stored TinyFish API key: this Claude Code predates one-command sign-in " +
|
|
13
|
+
"(`claude mcp login`), so no browser sign-in is needed.";
|
|
14
|
+
const CLAUDE_CODE_DEFERRED_AUTH_NOTE = "TinyFish is registered in Claude Code but not signed in: this Claude Code predates " +
|
|
15
|
+
"one-command sign-in (`claude mcp login`). Open Claude Code and run `/mcp` to sign in to " +
|
|
16
|
+
"TinyFish. `claude update` gets you one-command sign-in.";
|
|
17
|
+
const CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE = "Could not confirm this Codex installation supports one-command MCP authentication: `codex " +
|
|
18
|
+
"mcp add --help` did not list `--oauth-resource`. Update Codex and retry, or add TinyFish " +
|
|
19
|
+
"manually with `codex mcp add`." +
|
|
20
|
+
SUPPORT_CHECK_DEBUG_HINT;
|
|
21
|
+
const HERMES_OAUTH_UNAVAILABLE_MESSAGE = "Could not confirm this Hermes installation supports one-command MCP authentication: `hermes " +
|
|
22
|
+
"mcp add --help` did not list `--auth oauth`. Update Hermes and retry, or add TinyFish " +
|
|
23
|
+
"manually with `hermes mcp add`." +
|
|
24
|
+
SUPPORT_CHECK_DEBUG_HINT;
|
|
25
|
+
const OPENCLAW_SKILL_UNAVAILABLE_MESSAGE = "Could not confirm this OpenClaw installation supports global skill installation: `openclaw " +
|
|
26
|
+
"skills install --help` did not list `--global` and `--acknowledge-clawhub-risk`. Update " +
|
|
27
|
+
"OpenClaw and retry, or install manually with `openclaw skills install @tinyfish/tinyfish " +
|
|
28
|
+
"--global --acknowledge-clawhub-risk`." +
|
|
29
|
+
SUPPORT_CHECK_DEBUG_HINT;
|
|
30
|
+
const OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE = "Could not confirm this OpenCode installation supports one-command MCP setup: `opencode mcp " +
|
|
31
|
+
"add --help` did not list `--url`. Update OpenCode and retry, or add TinyFish manually with " +
|
|
32
|
+
"`opencode mcp add`." +
|
|
33
|
+
SUPPORT_CHECK_DEBUG_HINT;
|
|
34
|
+
// OpenCode is model-agnostic (unlike Claude Code / Codex), so a fresh install can default to a
|
|
35
|
+
// model with no tool support — TinyFish runs entirely on tool calls, so it would fail there.
|
|
36
|
+
const OPENCODE_MODEL_NOTE = "Note: TinyFish runs on tool calls, so OpenCode needs a model that supports tool use. Image-only " +
|
|
37
|
+
'models (e.g. Nano Banana Pro) will show "No endpoints found that support tool use" — switch ' +
|
|
38
|
+
"OpenCode's model if the walkthrough can't start.";
|
|
39
|
+
const OPENCLAW_SKILL = "@tinyfish/tinyfish";
|
|
40
|
+
// --force makes this an unconditional overwrite, so the same call installs and refreshes.
|
|
41
|
+
export const OPENCLAW_SKILL_INSTALL_ARGS = [
|
|
42
|
+
"skills",
|
|
43
|
+
"install",
|
|
44
|
+
OPENCLAW_SKILL,
|
|
45
|
+
"--global",
|
|
46
|
+
"--force",
|
|
47
|
+
"--acknowledge-clawhub-risk",
|
|
48
|
+
];
|
|
49
|
+
export const DEFAULT_ONBOARDING_PROMPT = "I just connected TinyFish. Call the guide_next_step tool now to begin. Then follow its " +
|
|
50
|
+
"instructions one step at a time. Ask for my input and wait for my reply before each subsequent " +
|
|
51
|
+
"TinyFish tool call.";
|
|
52
|
+
export const OPENCLAW_ONBOARDING_PROMPT = "I just installed the TinyFish skill. Guide me through it interactively: first ask what topic I " +
|
|
53
|
+
"want to search and wait for my reply before using TinyFish Search. Show me the results, ask " +
|
|
54
|
+
"which result I want to read, and wait before using TinyFish Fetch. Then ask what browser task " +
|
|
55
|
+
"I want to complete and wait before using TinyFish Agent. Never skip ahead or choose for me.";
|
|
56
|
+
export const CLAUDE_CODE = {
|
|
57
|
+
command: "claude",
|
|
58
|
+
connectClient: "claude-code",
|
|
59
|
+
skillAgent: "claude-code",
|
|
60
|
+
displayName: "Claude Code",
|
|
61
|
+
supportCheck: {
|
|
62
|
+
args: ["mcp", "--help"],
|
|
63
|
+
patterns: [/^\s*add(?:\s|\[)/m],
|
|
64
|
+
optionalPatterns: [/^\s*login(?:\s|\[)/m],
|
|
65
|
+
unavailableMessage: MCP_ADD_UNAVAILABLE_MESSAGE,
|
|
66
|
+
},
|
|
67
|
+
headerAuthSupported: true,
|
|
68
|
+
loginArgs: ["mcp", "login", "tinyfish"],
|
|
69
|
+
degradedAuthNotes: {
|
|
70
|
+
keyed: CLAUDE_CODE_KEYED_FALLBACK_NOTE,
|
|
71
|
+
deferred: CLAUDE_CODE_DEFERRED_AUTH_NOTE,
|
|
72
|
+
},
|
|
73
|
+
// Project scope is shared in .mcp.json; a user setup command must not rewrite it.
|
|
74
|
+
removals: [
|
|
75
|
+
{ args: ["mcp", "remove", "tinyfish", "--scope", "user"], label: "user TinyFish registration" },
|
|
76
|
+
{
|
|
77
|
+
args: ["mcp", "remove", "tinyfish", "--scope", "local"],
|
|
78
|
+
label: "local TinyFish registration",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
addArgs: (mcpUrl) => ["mcp", "add", "--scope", "user", "--transport", "http", "tinyfish", mcpUrl],
|
|
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
|
+
}
|
|
99
|
+
function launchCodexWalkthrough() {
|
|
100
|
+
const deepLink = new URL("codex://new");
|
|
101
|
+
deepLink.searchParams.set("prompt", DEFAULT_ONBOARDING_PROMPT);
|
|
102
|
+
deepLink.searchParams.set("path", process.cwd());
|
|
103
|
+
const result = openExternalUrl(deepLink.toString());
|
|
104
|
+
if (result.error || result.status !== 0) {
|
|
105
|
+
throw new Error("Could not open Codex", { cause: result.error });
|
|
106
|
+
}
|
|
107
|
+
errLine("Codex opened with the TinyFish walkthrough ready. Send the prompt to start.");
|
|
108
|
+
}
|
|
109
|
+
export const CODEX = {
|
|
110
|
+
command: "codex",
|
|
111
|
+
connectClient: "codex",
|
|
112
|
+
skillAgent: "codex",
|
|
113
|
+
displayName: "Codex",
|
|
114
|
+
supportCheck: {
|
|
115
|
+
args: ["mcp", "add", "--help"],
|
|
116
|
+
patterns: [/--oauth-resource(?:\s|<)/],
|
|
117
|
+
unavailableMessage: CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE,
|
|
118
|
+
},
|
|
119
|
+
launchWalkthrough: launchCodexWalkthrough,
|
|
120
|
+
probeAuthenticated: codexOauthCompleted,
|
|
121
|
+
// Retry only: running it after a finished inline OAuth is a second browser hop.
|
|
122
|
+
loginArgs: ["mcp", "login", "tinyfish"],
|
|
123
|
+
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Codex" }],
|
|
124
|
+
addArgs: (mcpUrl, oauthResource) => [
|
|
125
|
+
"mcp",
|
|
126
|
+
"add",
|
|
127
|
+
"tinyfish",
|
|
128
|
+
"--url",
|
|
129
|
+
mcpUrl,
|
|
130
|
+
"--oauth-resource",
|
|
131
|
+
oauthResource,
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
function launchHermesWalkthrough() {
|
|
135
|
+
const seedResult = spawn.sync("hermes", ["chat", "-Q", "-q", DEFAULT_ONBOARDING_PROMPT], {
|
|
136
|
+
encoding: "utf8",
|
|
137
|
+
timeout: HERMES_SEED_TIMEOUT_MS,
|
|
138
|
+
});
|
|
139
|
+
if (seedResult.error || seedResult.status !== 0) {
|
|
140
|
+
throwIfInterrupted(seedResult);
|
|
141
|
+
throw new Error("Could not start the TinyFish walkthrough in Hermes", {
|
|
142
|
+
cause: seedResult.error,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
// A colourised id would carry its trailing escape sequence into `--resume` (PF-3452).
|
|
146
|
+
const sessionId = sanitizeLine(seedResult.stderr ?? "").match(/session_id:\s*([^\s]+)/i)?.[1];
|
|
147
|
+
if (!sessionId) {
|
|
148
|
+
throw new Error("Hermes did not return a walkthrough session ID");
|
|
149
|
+
}
|
|
150
|
+
const result = spawn.sync("hermes", ["--resume", sessionId], { stdio: "inherit" });
|
|
151
|
+
if (result.error) {
|
|
152
|
+
throw new Error("Could not launch Hermes", { cause: result.error });
|
|
153
|
+
}
|
|
154
|
+
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
155
|
+
return;
|
|
156
|
+
if (result.status !== 0) {
|
|
157
|
+
throw new Error(`Hermes walkthrough exited with status ${result.status ?? "unknown"}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export const HERMES = {
|
|
161
|
+
command: "hermes",
|
|
162
|
+
connectClient: "hermes",
|
|
163
|
+
skillAgent: "hermes-agent",
|
|
164
|
+
displayName: "Hermes",
|
|
165
|
+
supportCheck: {
|
|
166
|
+
args: ["mcp", "add", "--help"],
|
|
167
|
+
patterns: [/--auth\s+\{[^}]*oauth[^}]*\}/],
|
|
168
|
+
unavailableMessage: HERMES_OAUTH_UNAVAILABLE_MESSAGE,
|
|
169
|
+
},
|
|
170
|
+
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Hermes" }],
|
|
171
|
+
// Hermes completes OAuth while adding the server; a separate `mcp login` would authenticate twice.
|
|
172
|
+
addArgs: (mcpUrl) => ["mcp", "add", "tinyfish", "--url", mcpUrl, "--auth", "oauth"],
|
|
173
|
+
launchWalkthrough: launchHermesWalkthrough,
|
|
174
|
+
};
|
|
175
|
+
// OpenCode's TUI takes a positional as a project directory, so a bare `opencode "<prompt>"`
|
|
176
|
+
// would be read as a folder. The `--prompt` flag seeds the interactive TUI with a first message
|
|
177
|
+
// (opencode.ai/docs/cli), so the onboarding guide fires just like the other agents.
|
|
178
|
+
function launchOpencode() {
|
|
179
|
+
const result = spawn.sync("opencode", ["--prompt", DEFAULT_ONBOARDING_PROMPT], {
|
|
180
|
+
stdio: "inherit",
|
|
181
|
+
});
|
|
182
|
+
if (result.error) {
|
|
183
|
+
throw new Error("Could not launch OpenCode", { cause: result.error });
|
|
184
|
+
}
|
|
185
|
+
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
186
|
+
return;
|
|
187
|
+
if (result.status !== 0) {
|
|
188
|
+
throw new Error(`OpenCode exited with status ${result.status ?? "unknown"}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export const OPENCODE = {
|
|
192
|
+
command: "opencode",
|
|
193
|
+
connectClient: "opencode",
|
|
194
|
+
skillAgent: "opencode",
|
|
195
|
+
displayName: "OpenCode",
|
|
196
|
+
supportCheck: {
|
|
197
|
+
args: ["mcp", "add", "--help"],
|
|
198
|
+
patterns: [/--url(?:\s|$)/m],
|
|
199
|
+
unavailableMessage: OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE,
|
|
200
|
+
},
|
|
201
|
+
// `opencode mcp add` upserts by name and there is no `opencode mcp remove`, so no cleanup step.
|
|
202
|
+
removals: [],
|
|
203
|
+
// OpenCode writes ~/.config/opencode/opencode.jsonc itself; `mcp auth` is the separate OAuth step.
|
|
204
|
+
addArgs: (mcpUrl) => ["mcp", "add", "tinyfish", "--url", mcpUrl],
|
|
205
|
+
loginArgs: ["mcp", "auth", "tinyfish"],
|
|
206
|
+
launchWalkthrough: launchOpencode,
|
|
207
|
+
postConnectNote: OPENCODE_MODEL_NOTE,
|
|
208
|
+
};
|
|
209
|
+
export const OPENCLAW = {
|
|
210
|
+
command: "openclaw",
|
|
211
|
+
displayName: "OpenClaw",
|
|
212
|
+
supportCheck: {
|
|
213
|
+
args: ["skills", "install", "--help"],
|
|
214
|
+
patterns: [/--global(?:\s|$)/, /--acknowledge-clawhub-risk(?:\s|$)/],
|
|
215
|
+
unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
export function launchNativeMcpClient(client) {
|
|
219
|
+
errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
|
|
220
|
+
if (client.launchWalkthrough) {
|
|
221
|
+
client.launchWalkthrough();
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
const result = spawn.sync(client.command, [DEFAULT_ONBOARDING_PROMPT], { stdio: "inherit" });
|
|
225
|
+
if (result.error) {
|
|
226
|
+
throw new Error(`Could not launch ${client.displayName}`, { cause: result.error });
|
|
227
|
+
}
|
|
228
|
+
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
229
|
+
return false;
|
|
230
|
+
if (result.status !== 0) {
|
|
231
|
+
throw new Error(`${client.displayName} walkthrough exited with status ${result.status ?? "unknown"}`);
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
export function launchOpenClawWalkthrough() {
|
|
236
|
+
errLine("Starting the TinyFish walkthrough in OpenClaw...");
|
|
237
|
+
const result = spawn.sync("openclaw", ["chat", "--message", OPENCLAW_ONBOARDING_PROMPT], {
|
|
238
|
+
stdio: "inherit",
|
|
239
|
+
});
|
|
240
|
+
if (result.error) {
|
|
241
|
+
throw new Error("Could not launch OpenClaw", { cause: result.error });
|
|
242
|
+
}
|
|
243
|
+
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
244
|
+
return false;
|
|
245
|
+
if (result.status !== 0) {
|
|
246
|
+
throw new Error(`OpenClaw walkthrough exited with status ${result.status ?? "unknown"}`);
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
@@ -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
|
}
|