@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.
- package/dist/commands/connect.d.ts +5 -4
- package/dist/commands/connect.js +162 -95
- package/package.json +1 -1
|
@@ -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;
|
package/dist/commands/connect.js
CHANGED
|
@@ -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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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 (
|
|
171
|
-
throw new Error(`Could not
|
|
172
|
-
cause:
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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(
|
|
356
|
+
await connectOpenClaw(connectOptions);
|
|
290
357
|
}
|
|
291
358
|
else {
|
|
292
359
|
throw new Error(`Unsupported client: ${client}. Supported clients: claude-code, codex, hermes, openclaw`);
|