@tiny-fish/cli 0.6.1 → 0.6.2-next.104

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.
@@ -4,16 +4,17 @@ export declare const OPENCLAW_ONBOARDING_PROMPT: string;
4
4
  export declare function connectClaudeCode(options: {
5
5
  mcpUrl: string;
6
6
  launch: boolean;
7
- }): void;
7
+ }): Promise<void>;
8
8
  export declare function connectCodex(options: {
9
9
  mcpUrl: string;
10
10
  launch: boolean;
11
- }): void;
11
+ }): Promise<void>;
12
12
  export declare function connectHermes(options: {
13
13
  mcpUrl: string;
14
14
  launch: boolean;
15
- }): void;
15
+ }): Promise<void>;
16
16
  export declare function connectOpenClaw(options: {
17
+ mcpUrl: string;
17
18
  launch: boolean;
18
- }): void;
19
+ }): Promise<void>;
19
20
  export declare function registerConnect(program: Command): void;
@@ -1,8 +1,11 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import spawn from "cross-spawn";
3
+ import { CLI_VERSION } from "../lib/constants.js";
2
4
  import { errLine } from "../lib/output.js";
3
5
  const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
4
6
  const NON_INTERACTIVE_TIMEOUT_MS = 10_000;
5
7
  const HERMES_SEED_TIMEOUT_MS = 120_000;
8
+ const TELEMETRY_TIMEOUT_MS = 500;
6
9
  const MCP_LOGIN_UNAVAILABLE_MESSAGE = "This Claude Code installation does not expose `claude mcp login`, which is required for " +
7
10
  "one-command MCP authentication. Update Claude Code and retry, or authenticate TinyFish " +
8
11
  "manually from /mcp.";
@@ -108,6 +111,39 @@ const OPENCLAW = {
108
111
  unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
109
112
  },
110
113
  };
114
+ function createConnectTelemetry(mcpUrl, client) {
115
+ const attemptId = randomUUID();
116
+ const endpoint = new URL("/api/cli/connect-event", mcpUrl).toString();
117
+ let enabled = true;
118
+ return {
119
+ attemptId,
120
+ async track(stage, failedStage) {
121
+ if (!enabled)
122
+ return;
123
+ try {
124
+ const response = await fetch(endpoint, {
125
+ method: "POST",
126
+ headers: { "content-type": "application/json" },
127
+ body: JSON.stringify({
128
+ attempt_id: attemptId,
129
+ client,
130
+ stage,
131
+ failed_stage: failedStage,
132
+ runtime_platform: process.platform,
133
+ node_version: process.version,
134
+ cli_version: CLI_VERSION,
135
+ }),
136
+ signal: AbortSignal.timeout(TELEMETRY_TIMEOUT_MS),
137
+ });
138
+ if (!response.ok)
139
+ enabled = false;
140
+ }
141
+ catch {
142
+ enabled = false;
143
+ }
144
+ },
145
+ };
146
+ }
111
147
  function requireCommandSupport(client) {
112
148
  const result = spawn.sync(client.command, client.supportCheck.args, {
113
149
  encoding: "utf8",
@@ -146,111 +182,142 @@ function removeExistingRegistration(client, removal) {
146
182
  }
147
183
  throw new Error(`Could not remove existing ${removal.label}: ${details}`);
148
184
  }
149
- function connectNativeMcpClient(client, options) {
150
- requireCommandSupport(client);
151
- for (const removal of client.removals)
152
- removeExistingRegistration(client, removal);
153
- const mcpUrl = new URL(options.mcpUrl);
154
- mcpUrl.searchParams.set("source", "tinyfish_cli");
155
- mcpUrl.searchParams.set("client", client.connectClient);
156
- errLine(`Adding TinyFish to ${client.displayName}...`);
157
- const addResult = spawn.sync(client.command, client.addArgs(mcpUrl.toString(), options.mcpUrl), {
158
- stdio: "inherit",
159
- });
160
- if (addResult.error || addResult.status !== 0) {
161
- throw new Error(`Could not add TinyFish to ${client.displayName}`, {
162
- cause: addResult.error,
163
- });
164
- }
165
- if (client.loginArgs) {
166
- errLine("Signing in to TinyFish...");
167
- const loginResult = spawn.sync(client.command, client.loginArgs, {
185
+ async function connectNativeMcpClient(client, options) {
186
+ const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient);
187
+ let failureStage = "prerequisite_check";
188
+ try {
189
+ await telemetry.track("started");
190
+ requireCommandSupport(client);
191
+ failureStage = "registration_cleanup";
192
+ for (const removal of client.removals)
193
+ removeExistingRegistration(client, removal);
194
+ const mcpUrl = new URL(options.mcpUrl);
195
+ mcpUrl.searchParams.set("source", "tinyfish_cli");
196
+ mcpUrl.searchParams.set("client", client.connectClient);
197
+ mcpUrl.searchParams.set("connect_attempt_id", telemetry.attemptId);
198
+ failureStage = client.loginArgs ? "registration" : "registration_or_authentication";
199
+ errLine(`Adding TinyFish to ${client.displayName}...`);
200
+ const addResult = spawn.sync(client.command, client.addArgs(mcpUrl.toString(), options.mcpUrl), {
168
201
  stdio: "inherit",
169
202
  });
170
- if (loginResult.error || loginResult.status !== 0) {
171
- throw new Error(`Could not authenticate TinyFish in ${client.displayName}`, {
172
- cause: loginResult.error,
203
+ if (addResult.error || addResult.status !== 0) {
204
+ throw new Error(`Could not add TinyFish to ${client.displayName}`, {
205
+ cause: addResult.error,
173
206
  });
174
207
  }
208
+ if (client.loginArgs) {
209
+ failureStage = "authentication";
210
+ errLine("Signing in to TinyFish...");
211
+ const loginResult = spawn.sync(client.command, client.loginArgs, {
212
+ stdio: "inherit",
213
+ });
214
+ if (loginResult.error || loginResult.status !== 0) {
215
+ throw new Error(`Could not authenticate TinyFish in ${client.displayName}`, {
216
+ cause: loginResult.error,
217
+ });
218
+ }
219
+ }
220
+ if (!options.launch) {
221
+ errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
222
+ await telemetry.track("completed");
223
+ return;
224
+ }
225
+ failureStage = "walkthrough_launch";
226
+ errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
227
+ if (client.launchWalkthrough) {
228
+ client.launchWalkthrough();
229
+ await telemetry.track("completed");
230
+ return;
231
+ }
232
+ const result = spawn.sync(client.command, [DEFAULT_ONBOARDING_PROMPT], { stdio: "inherit" });
233
+ if (result.error) {
234
+ throw new Error(`Could not launch ${client.displayName}`, { cause: result.error });
235
+ }
236
+ if (result.signal === "SIGINT" || result.signal === "SIGTERM")
237
+ return;
238
+ if (result.status !== 0) {
239
+ throw new Error(`${client.displayName} walkthrough exited with status ${result.status ?? "unknown"}`);
240
+ }
241
+ await telemetry.track("completed");
175
242
  }
176
- if (!options.launch) {
177
- errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
178
- return;
179
- }
180
- errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
181
- if (client.launchWalkthrough) {
182
- client.launchWalkthrough();
183
- return;
184
- }
185
- const result = spawn.sync(client.command, [DEFAULT_ONBOARDING_PROMPT], { stdio: "inherit" });
186
- if (result.error) {
187
- throw new Error(`Could not launch ${client.displayName}`, { cause: result.error });
188
- }
189
- if (result.signal === "SIGINT" || result.signal === "SIGTERM")
190
- return;
191
- if (result.status !== 0) {
192
- throw new Error(`${client.displayName} walkthrough exited with status ${result.status ?? "unknown"}`);
243
+ catch (error) {
244
+ await telemetry.track("failed", failureStage);
245
+ throw error;
193
246
  }
194
247
  }
195
- export function connectClaudeCode(options) {
196
- connectNativeMcpClient(CLAUDE_CODE, options);
248
+ export async function connectClaudeCode(options) {
249
+ await connectNativeMcpClient(CLAUDE_CODE, options);
197
250
  }
198
- export function connectCodex(options) {
199
- connectNativeMcpClient(CODEX, options);
251
+ export async function connectCodex(options) {
252
+ await connectNativeMcpClient(CODEX, options);
200
253
  }
201
- export function connectHermes(options) {
202
- connectNativeMcpClient(HERMES, options);
254
+ export async function connectHermes(options) {
255
+ await connectNativeMcpClient(HERMES, options);
203
256
  }
204
- export function connectOpenClaw(options) {
205
- requireCommandSupport(OPENCLAW);
206
- errLine("Installing the TinyFish CLI...");
207
- const cliInstallResult = spawn.sync("npm", ["install", "--global", "@tiny-fish/cli@latest"], { stdio: "inherit" });
208
- if (cliInstallResult.error || cliInstallResult.status !== 0) {
209
- throw new Error("Could not install the TinyFish CLI", { cause: cliInstallResult.error });
210
- }
211
- errLine("Installing the TinyFish skill in OpenClaw...");
212
- const skillInstallResult = spawn.sync("openclaw", [
213
- "skills",
214
- "install",
215
- OPENCLAW_SKILL,
216
- "--global",
217
- "--force",
218
- "--acknowledge-clawhub-risk",
219
- ], { stdio: "inherit" });
220
- if (skillInstallResult.error || skillInstallResult.status !== 0) {
221
- throw new Error("Could not install the TinyFish skill in OpenClaw", {
222
- cause: skillInstallResult.error,
257
+ export async function connectOpenClaw(options) {
258
+ const telemetry = createConnectTelemetry(options.mcpUrl, "openclaw");
259
+ let failureStage = "prerequisite_check";
260
+ try {
261
+ await telemetry.track("started");
262
+ requireCommandSupport(OPENCLAW);
263
+ failureStage = "cli_install";
264
+ errLine("Installing the TinyFish CLI...");
265
+ const cliInstallResult = spawn.sync("npm", ["install", "--global", "@tiny-fish/cli@latest"], {
266
+ stdio: "inherit",
223
267
  });
224
- }
225
- const authStatus = spawn.sync("tinyfish", ["auth", "status"], { stdio: "ignore" });
226
- if (commandNotFound(authStatus.error)) {
227
- throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: authStatus.error });
228
- }
229
- if (authStatus.status !== 0) {
230
- errLine("Signing in to TinyFish...");
231
- const loginResult = spawn.sync("tinyfish", ["auth", "login", "--source", "openclaw"], { stdio: "inherit" });
232
- if (commandNotFound(loginResult.error)) {
233
- throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: loginResult.error });
268
+ if (cliInstallResult.error || cliInstallResult.status !== 0) {
269
+ throw new Error("Could not install the TinyFish CLI", { cause: cliInstallResult.error });
234
270
  }
235
- if (loginResult.error || loginResult.status !== 0) {
236
- throw new Error("Could not authenticate TinyFish for OpenClaw", {
237
- cause: loginResult.error,
271
+ failureStage = "skill_install";
272
+ errLine("Installing the TinyFish skill in OpenClaw...");
273
+ const skillInstallResult = spawn.sync("openclaw", ["skills", "install", OPENCLAW_SKILL, "--global", "--force", "--acknowledge-clawhub-risk"], { stdio: "inherit" });
274
+ if (skillInstallResult.error || skillInstallResult.status !== 0) {
275
+ throw new Error("Could not install the TinyFish skill in OpenClaw", {
276
+ cause: skillInstallResult.error,
238
277
  });
239
278
  }
279
+ failureStage = "authentication";
280
+ const authStatus = spawn.sync("tinyfish", ["auth", "status"], { stdio: "ignore" });
281
+ if (commandNotFound(authStatus.error)) {
282
+ throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: authStatus.error });
283
+ }
284
+ if (authStatus.status !== 0) {
285
+ errLine("Signing in to TinyFish...");
286
+ const loginResult = spawn.sync("tinyfish", ["auth", "login", "--source", "openclaw"], {
287
+ stdio: "inherit",
288
+ });
289
+ if (commandNotFound(loginResult.error)) {
290
+ throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: loginResult.error });
291
+ }
292
+ if (loginResult.error || loginResult.status !== 0) {
293
+ throw new Error("Could not authenticate TinyFish for OpenClaw", {
294
+ cause: loginResult.error,
295
+ });
296
+ }
297
+ }
298
+ if (!options.launch) {
299
+ errLine("TinyFish is connected. Open OpenClaw to start using it.");
300
+ await telemetry.track("completed");
301
+ return;
302
+ }
303
+ failureStage = "walkthrough_launch";
304
+ errLine("Starting the TinyFish walkthrough in OpenClaw...");
305
+ const launchResult = spawn.sync("openclaw", ["chat", "--message", OPENCLAW_ONBOARDING_PROMPT], {
306
+ stdio: "inherit",
307
+ });
308
+ if (launchResult.error) {
309
+ throw new Error("Could not launch OpenClaw", { cause: launchResult.error });
310
+ }
311
+ if (launchResult.signal === "SIGINT" || launchResult.signal === "SIGTERM")
312
+ return;
313
+ if (launchResult.status !== 0) {
314
+ throw new Error(`OpenClaw walkthrough exited with status ${launchResult.status ?? "unknown"}`);
315
+ }
316
+ await telemetry.track("completed");
240
317
  }
241
- if (!options.launch) {
242
- errLine("TinyFish is connected. Open OpenClaw to start using it.");
243
- return;
244
- }
245
- errLine("Starting the TinyFish walkthrough in OpenClaw...");
246
- const launchResult = spawn.sync("openclaw", ["chat", "--message", OPENCLAW_ONBOARDING_PROMPT], { stdio: "inherit" });
247
- if (launchResult.error) {
248
- throw new Error("Could not launch OpenClaw", { cause: launchResult.error });
249
- }
250
- if (launchResult.signal === "SIGINT" || launchResult.signal === "SIGTERM")
251
- return;
252
- if (launchResult.status !== 0) {
253
- throw new Error(`OpenClaw walkthrough exited with status ${launchResult.status ?? "unknown"}`);
318
+ catch (error) {
319
+ await telemetry.track("failed", failureStage);
320
+ throw error;
254
321
  }
255
322
  }
256
323
  export function registerConnect(program) {
@@ -260,7 +327,7 @@ export function registerConnect(program) {
260
327
  .argument("<client>", "Agent client to connect (claude-code, codex, hermes, or openclaw)")
261
328
  .option("--launch", "Launch the agent and start the TinyFish walkthrough")
262
329
  .option("--url <mcpUrl>", "MCP endpoint override")
263
- .action((client, options) => {
330
+ .action(async (client, options) => {
264
331
  const mcpUrl = options.url ?? DEFAULT_MCP_URL;
265
332
  if (mcpUrl.startsWith("-")) {
266
333
  throw new Error(`Invalid --url value: ${mcpUrl}`);
@@ -277,16 +344,16 @@ export function registerConnect(program) {
277
344
  }
278
345
  const connectOptions = { mcpUrl, launch: options.launch ?? false };
279
346
  if (client === "claude-code") {
280
- connectClaudeCode(connectOptions);
347
+ await connectClaudeCode(connectOptions);
281
348
  }
282
349
  else if (client === "codex") {
283
- connectCodex(connectOptions);
350
+ await connectCodex(connectOptions);
284
351
  }
285
352
  else if (client === "hermes") {
286
- connectHermes(connectOptions);
353
+ await connectHermes(connectOptions);
287
354
  }
288
355
  else if (client === "openclaw") {
289
- connectOpenClaw({ launch: connectOptions.launch });
356
+ await connectOpenClaw(connectOptions);
290
357
  }
291
358
  else {
292
359
  throw new Error(`Unsupported client: ${client}. Supported clients: claude-code, codex, hermes, openclaw`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.2-next.104",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {