@tiny-fish/cli 0.6.2-next.102 → 0.6.2-next.105
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 +8 -4
- package/dist/commands/connect.js +195 -87
- package/dist/commands/onboard.d.ts +2 -0
- package/dist/commands/onboard.js +230 -0
- package/dist/index.js +2 -0
- package/dist/lib/constants.js +3 -2
- package/dist/lib/output.js +12 -3
- package/package.json +1 -1
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
export declare const DEFAULT_ONBOARDING_PROMPT: string;
|
|
3
3
|
export declare const OPENCLAW_ONBOARDING_PROMPT: string;
|
|
4
|
+
export type AgentClient = "claude-code" | "codex" | "hermes" | "openclaw";
|
|
4
5
|
export declare function connectClaudeCode(options: {
|
|
5
6
|
mcpUrl: string;
|
|
6
7
|
launch: boolean;
|
|
7
|
-
}): void
|
|
8
|
+
}): Promise<void>;
|
|
8
9
|
export declare function connectCodex(options: {
|
|
9
10
|
mcpUrl: string;
|
|
10
11
|
launch: boolean;
|
|
11
|
-
}): void
|
|
12
|
+
}): Promise<void>;
|
|
12
13
|
export declare function connectHermes(options: {
|
|
13
14
|
mcpUrl: string;
|
|
14
15
|
launch: boolean;
|
|
15
|
-
}): void
|
|
16
|
+
}): Promise<void>;
|
|
16
17
|
export declare function connectOpenClaw(options: {
|
|
18
|
+
mcpUrl: string;
|
|
17
19
|
launch: boolean;
|
|
18
|
-
|
|
20
|
+
installCli?: boolean;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
export declare function launchAgent(client: AgentClient): void;
|
|
19
23
|
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.";
|
|
@@ -41,6 +44,21 @@ const CLAUDE_CODE = {
|
|
|
41
44
|
],
|
|
42
45
|
addArgs: (mcpUrl) => ["mcp", "add", "--scope", "user", "--transport", "http", "tinyfish", mcpUrl],
|
|
43
46
|
};
|
|
47
|
+
function launchCodexWalkthrough() {
|
|
48
|
+
const deepLink = new URL("codex://new");
|
|
49
|
+
deepLink.searchParams.set("prompt", DEFAULT_ONBOARDING_PROMPT);
|
|
50
|
+
deepLink.searchParams.set("path", process.cwd());
|
|
51
|
+
const [command, args] = process.platform === "darwin"
|
|
52
|
+
? ["open", [deepLink.toString()]]
|
|
53
|
+
: process.platform === "win32"
|
|
54
|
+
? ["cmd", ["/c", "start", "", deepLink.toString()]]
|
|
55
|
+
: ["xdg-open", [deepLink.toString()]];
|
|
56
|
+
const result = spawn.sync(command, args, { stdio: "ignore" });
|
|
57
|
+
if (result.error || result.status !== 0) {
|
|
58
|
+
throw new Error("Could not open Codex", { cause: result.error });
|
|
59
|
+
}
|
|
60
|
+
errLine("Codex opened with the TinyFish walkthrough ready. Send the prompt to start.");
|
|
61
|
+
}
|
|
44
62
|
const CODEX = {
|
|
45
63
|
command: "codex",
|
|
46
64
|
connectClient: "codex",
|
|
@@ -50,6 +68,7 @@ const CODEX = {
|
|
|
50
68
|
patterns: [/--oauth-resource(?:\s|<)/],
|
|
51
69
|
unavailableMessage: CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE,
|
|
52
70
|
},
|
|
71
|
+
launchWalkthrough: launchCodexWalkthrough,
|
|
53
72
|
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Codex" }],
|
|
54
73
|
addArgs: (mcpUrl, oauthResource) => [
|
|
55
74
|
"mcp",
|
|
@@ -108,6 +127,39 @@ const OPENCLAW = {
|
|
|
108
127
|
unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
|
|
109
128
|
},
|
|
110
129
|
};
|
|
130
|
+
function createConnectTelemetry(mcpUrl, client) {
|
|
131
|
+
const attemptId = randomUUID();
|
|
132
|
+
const endpoint = new URL("/api/cli/connect-event", mcpUrl).toString();
|
|
133
|
+
let enabled = true;
|
|
134
|
+
return {
|
|
135
|
+
attemptId,
|
|
136
|
+
async track(stage, failedStage) {
|
|
137
|
+
if (!enabled)
|
|
138
|
+
return;
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(endpoint, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: { "content-type": "application/json" },
|
|
143
|
+
body: JSON.stringify({
|
|
144
|
+
attempt_id: attemptId,
|
|
145
|
+
client,
|
|
146
|
+
stage,
|
|
147
|
+
failed_stage: failedStage,
|
|
148
|
+
runtime_platform: process.platform,
|
|
149
|
+
node_version: process.version,
|
|
150
|
+
cli_version: CLI_VERSION,
|
|
151
|
+
}),
|
|
152
|
+
signal: AbortSignal.timeout(TELEMETRY_TIMEOUT_MS),
|
|
153
|
+
});
|
|
154
|
+
if (!response.ok)
|
|
155
|
+
enabled = false;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
enabled = false;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
111
163
|
function requireCommandSupport(client) {
|
|
112
164
|
const result = spawn.sync(client.command, client.supportCheck.args, {
|
|
113
165
|
encoding: "utf8",
|
|
@@ -146,111 +198,167 @@ function removeExistingRegistration(client, removal) {
|
|
|
146
198
|
}
|
|
147
199
|
throw new Error(`Could not remove existing ${removal.label}: ${details}`);
|
|
148
200
|
}
|
|
149
|
-
function
|
|
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, {
|
|
168
|
-
stdio: "inherit",
|
|
169
|
-
});
|
|
170
|
-
if (loginResult.error || loginResult.status !== 0) {
|
|
171
|
-
throw new Error(`Could not authenticate TinyFish in ${client.displayName}`, {
|
|
172
|
-
cause: loginResult.error,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (!options.launch) {
|
|
177
|
-
errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
201
|
+
function launchNativeMcpClient(client) {
|
|
180
202
|
errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
|
|
181
203
|
if (client.launchWalkthrough) {
|
|
182
204
|
client.launchWalkthrough();
|
|
183
|
-
return;
|
|
205
|
+
return true;
|
|
184
206
|
}
|
|
185
207
|
const result = spawn.sync(client.command, [DEFAULT_ONBOARDING_PROMPT], { stdio: "inherit" });
|
|
186
208
|
if (result.error) {
|
|
187
209
|
throw new Error(`Could not launch ${client.displayName}`, { cause: result.error });
|
|
188
210
|
}
|
|
189
211
|
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
190
|
-
return;
|
|
212
|
+
return false;
|
|
191
213
|
if (result.status !== 0) {
|
|
192
214
|
throw new Error(`${client.displayName} walkthrough exited with status ${result.status ?? "unknown"}`);
|
|
193
215
|
}
|
|
216
|
+
return true;
|
|
194
217
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
connectNativeMcpClient(HERMES, options);
|
|
203
|
-
}
|
|
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 });
|
|
218
|
+
function launchOpenClawWalkthrough() {
|
|
219
|
+
errLine("Starting the TinyFish walkthrough in OpenClaw...");
|
|
220
|
+
const result = spawn.sync("openclaw", ["chat", "--message", OPENCLAW_ONBOARDING_PROMPT], {
|
|
221
|
+
stdio: "inherit",
|
|
222
|
+
});
|
|
223
|
+
if (result.error) {
|
|
224
|
+
throw new Error("Could not launch OpenClaw", { cause: result.error });
|
|
210
225
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
"
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
226
|
+
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
227
|
+
return false;
|
|
228
|
+
if (result.status !== 0) {
|
|
229
|
+
throw new Error(`OpenClaw walkthrough exited with status ${result.status ?? "unknown"}`);
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
async function connectNativeMcpClient(client, options) {
|
|
234
|
+
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient);
|
|
235
|
+
let failureStage = "prerequisite_check";
|
|
236
|
+
try {
|
|
237
|
+
await telemetry.track("started");
|
|
238
|
+
requireCommandSupport(client);
|
|
239
|
+
failureStage = "registration_cleanup";
|
|
240
|
+
for (const removal of client.removals)
|
|
241
|
+
removeExistingRegistration(client, removal);
|
|
242
|
+
const mcpUrl = new URL(options.mcpUrl);
|
|
243
|
+
mcpUrl.searchParams.set("source", "tinyfish_cli");
|
|
244
|
+
mcpUrl.searchParams.set("client", client.connectClient);
|
|
245
|
+
mcpUrl.searchParams.set("connect_attempt_id", telemetry.attemptId);
|
|
246
|
+
failureStage = client.loginArgs ? "registration" : "registration_or_authentication";
|
|
247
|
+
errLine(`Adding TinyFish to ${client.displayName}...`);
|
|
248
|
+
const addResult = spawn.sync(client.command, client.addArgs(mcpUrl.toString(), options.mcpUrl), {
|
|
249
|
+
stdio: "inherit",
|
|
223
250
|
});
|
|
251
|
+
if (addResult.error || addResult.status !== 0) {
|
|
252
|
+
throw new Error(`Could not add TinyFish to ${client.displayName}`, {
|
|
253
|
+
cause: addResult.error,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (client.loginArgs) {
|
|
257
|
+
failureStage = "authentication";
|
|
258
|
+
errLine("Signing in to TinyFish...");
|
|
259
|
+
const loginResult = spawn.sync(client.command, client.loginArgs, {
|
|
260
|
+
stdio: "inherit",
|
|
261
|
+
});
|
|
262
|
+
if (loginResult.error || loginResult.status !== 0) {
|
|
263
|
+
throw new Error(`Could not authenticate TinyFish in ${client.displayName}`, {
|
|
264
|
+
cause: loginResult.error,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!options.launch) {
|
|
269
|
+
errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
|
|
270
|
+
await telemetry.track("completed");
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
failureStage = "walkthrough_launch";
|
|
274
|
+
if (!launchNativeMcpClient(client))
|
|
275
|
+
return;
|
|
276
|
+
await telemetry.track("completed");
|
|
224
277
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
throw
|
|
278
|
+
catch (error) {
|
|
279
|
+
await telemetry.track("failed", failureStage);
|
|
280
|
+
throw error;
|
|
228
281
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
282
|
+
}
|
|
283
|
+
export async function connectClaudeCode(options) {
|
|
284
|
+
await connectNativeMcpClient(CLAUDE_CODE, options);
|
|
285
|
+
}
|
|
286
|
+
export async function connectCodex(options) {
|
|
287
|
+
await connectNativeMcpClient(CODEX, options);
|
|
288
|
+
}
|
|
289
|
+
export async function connectHermes(options) {
|
|
290
|
+
await connectNativeMcpClient(HERMES, options);
|
|
291
|
+
}
|
|
292
|
+
export async function connectOpenClaw(options) {
|
|
293
|
+
const telemetry = createConnectTelemetry(options.mcpUrl, "openclaw");
|
|
294
|
+
let failureStage = "prerequisite_check";
|
|
295
|
+
try {
|
|
296
|
+
await telemetry.track("started");
|
|
297
|
+
requireCommandSupport(OPENCLAW);
|
|
298
|
+
if (options.installCli !== false) {
|
|
299
|
+
failureStage = "cli_install";
|
|
300
|
+
errLine("Installing the TinyFish CLI...");
|
|
301
|
+
const cliInstallResult = spawn.sync("npm", ["install", "--global", "@tiny-fish/cli@latest"], {
|
|
302
|
+
stdio: "inherit",
|
|
303
|
+
});
|
|
304
|
+
if (cliInstallResult.error || cliInstallResult.status !== 0) {
|
|
305
|
+
throw new Error("Could not install the TinyFish CLI", { cause: cliInstallResult.error });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
failureStage = "skill_install";
|
|
309
|
+
errLine("Installing the TinyFish skill in OpenClaw...");
|
|
310
|
+
const skillInstallResult = spawn.sync("openclaw", ["skills", "install", OPENCLAW_SKILL, "--global", "--force", "--acknowledge-clawhub-risk"], { stdio: "inherit" });
|
|
311
|
+
if (skillInstallResult.error || skillInstallResult.status !== 0) {
|
|
312
|
+
throw new Error("Could not install the TinyFish skill in OpenClaw", {
|
|
313
|
+
cause: skillInstallResult.error,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
failureStage = "authentication";
|
|
317
|
+
const authStatus = spawn.sync("tinyfish", ["auth", "status"], { stdio: "ignore" });
|
|
318
|
+
if (commandNotFound(authStatus.error)) {
|
|
319
|
+
throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: authStatus.error });
|
|
234
320
|
}
|
|
235
|
-
if (
|
|
236
|
-
|
|
237
|
-
|
|
321
|
+
if (authStatus.status !== 0) {
|
|
322
|
+
errLine("Signing in to TinyFish...");
|
|
323
|
+
const loginResult = spawn.sync("tinyfish", ["auth", "login", "--source", "openclaw"], {
|
|
324
|
+
stdio: "inherit",
|
|
238
325
|
});
|
|
326
|
+
if (commandNotFound(loginResult.error)) {
|
|
327
|
+
throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: loginResult.error });
|
|
328
|
+
}
|
|
329
|
+
if (loginResult.error || loginResult.status !== 0) {
|
|
330
|
+
throw new Error("Could not authenticate TinyFish for OpenClaw", {
|
|
331
|
+
cause: loginResult.error,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!options.launch) {
|
|
336
|
+
errLine("TinyFish is connected. Open OpenClaw to start using it.");
|
|
337
|
+
await telemetry.track("completed");
|
|
338
|
+
return;
|
|
239
339
|
}
|
|
340
|
+
failureStage = "walkthrough_launch";
|
|
341
|
+
if (!launchOpenClawWalkthrough())
|
|
342
|
+
return;
|
|
343
|
+
await telemetry.track("completed");
|
|
240
344
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
345
|
+
catch (error) {
|
|
346
|
+
await telemetry.track("failed", failureStage);
|
|
347
|
+
throw error;
|
|
244
348
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
349
|
+
}
|
|
350
|
+
export function launchAgent(client) {
|
|
351
|
+
if (client === "claude-code") {
|
|
352
|
+
launchNativeMcpClient(CLAUDE_CODE);
|
|
249
353
|
}
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
354
|
+
else if (client === "codex") {
|
|
355
|
+
launchNativeMcpClient(CODEX);
|
|
356
|
+
}
|
|
357
|
+
else if (client === "hermes") {
|
|
358
|
+
launchNativeMcpClient(HERMES);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
launchOpenClawWalkthrough();
|
|
254
362
|
}
|
|
255
363
|
}
|
|
256
364
|
export function registerConnect(program) {
|
|
@@ -260,7 +368,7 @@ export function registerConnect(program) {
|
|
|
260
368
|
.argument("<client>", "Agent client to connect (claude-code, codex, hermes, or openclaw)")
|
|
261
369
|
.option("--launch", "Launch the agent and start the TinyFish walkthrough")
|
|
262
370
|
.option("--url <mcpUrl>", "MCP endpoint override")
|
|
263
|
-
.action((client, options) => {
|
|
371
|
+
.action(async (client, options) => {
|
|
264
372
|
const mcpUrl = options.url ?? DEFAULT_MCP_URL;
|
|
265
373
|
if (mcpUrl.startsWith("-")) {
|
|
266
374
|
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
@@ -277,16 +385,16 @@ export function registerConnect(program) {
|
|
|
277
385
|
}
|
|
278
386
|
const connectOptions = { mcpUrl, launch: options.launch ?? false };
|
|
279
387
|
if (client === "claude-code") {
|
|
280
|
-
connectClaudeCode(connectOptions);
|
|
388
|
+
await connectClaudeCode(connectOptions);
|
|
281
389
|
}
|
|
282
390
|
else if (client === "codex") {
|
|
283
|
-
connectCodex(connectOptions);
|
|
391
|
+
await connectCodex(connectOptions);
|
|
284
392
|
}
|
|
285
393
|
else if (client === "hermes") {
|
|
286
|
-
connectHermes(connectOptions);
|
|
394
|
+
await connectHermes(connectOptions);
|
|
287
395
|
}
|
|
288
396
|
else if (client === "openclaw") {
|
|
289
|
-
connectOpenClaw(
|
|
397
|
+
await connectOpenClaw(connectOptions);
|
|
290
398
|
}
|
|
291
399
|
else {
|
|
292
400
|
throw new Error(`Unsupported client: ${client}. Supported clients: claude-code, codex, hermes, openclaw`);
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import * as readline from "readline/promises";
|
|
2
|
+
import { getApiKey, saveConfig } from "../lib/auth.js";
|
|
3
|
+
import { fetchContentGet, searchQuery } from "../lib/client.js";
|
|
4
|
+
import { BASE_URL } from "../lib/constants.js";
|
|
5
|
+
import { errLine, handleApiError, outLine } from "../lib/output.js";
|
|
6
|
+
import { connectClaudeCode, connectCodex, connectHermes, connectOpenClaw, launchAgent, } from "./connect.js";
|
|
7
|
+
const EXAMPLE_QUERIES = [
|
|
8
|
+
"What are recent changes in the AI dev stack? Give me the top 5 changes that matter, why they matter, and any action needed.",
|
|
9
|
+
"Find a new-grad software engineering role posted in the last 24 hours and fetch the job description.",
|
|
10
|
+
"Analyse today's market and recommend the best investments for $5,000 with expected returns and risks.",
|
|
11
|
+
];
|
|
12
|
+
const AGENTS = [
|
|
13
|
+
{ id: "claude-code", label: "Claude Code", connect: connectClaudeCode },
|
|
14
|
+
{ id: "codex", label: "Codex", connect: connectCodex },
|
|
15
|
+
{ id: "hermes", label: "Hermes", connect: connectHermes },
|
|
16
|
+
{ id: "openclaw", label: "OpenClaw", connect: connectOpenClaw },
|
|
17
|
+
];
|
|
18
|
+
function cleanFetchedContent(text) {
|
|
19
|
+
const cleaned = text
|
|
20
|
+
.replace(/!\[[^\]]*\]\([^)]+\)/g, "")
|
|
21
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
22
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
23
|
+
.replace(/[ \t]+/g, " ")
|
|
24
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
25
|
+
.trim();
|
|
26
|
+
if (cleaned.length <= 1_200)
|
|
27
|
+
return cleaned;
|
|
28
|
+
const shortened = cleaned.slice(0, 1_200);
|
|
29
|
+
return `${shortened.slice(0, shortened.lastIndexOf(" "))}…`;
|
|
30
|
+
}
|
|
31
|
+
function isUsefulFetchedContent(text) {
|
|
32
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
33
|
+
return (normalized.length >= 120 &&
|
|
34
|
+
normalized.split(" ").length >= 20 &&
|
|
35
|
+
!/about press copyright contact us creators|there was an error while loading/i.test(normalized));
|
|
36
|
+
}
|
|
37
|
+
function parseAgents(value) {
|
|
38
|
+
if (!value.trim())
|
|
39
|
+
return [];
|
|
40
|
+
const selections = value.split(",").map((item) => item.trim());
|
|
41
|
+
return selections.map((selection) => {
|
|
42
|
+
const agent = AGENTS[Number(selection) - 1];
|
|
43
|
+
if (!agent)
|
|
44
|
+
throw new Error(`Unknown agent: ${selection}`);
|
|
45
|
+
return agent.id;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function ask(question) {
|
|
49
|
+
const prompt = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
50
|
+
try {
|
|
51
|
+
return (await prompt.question(question)).trim();
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
prompt.close();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function sdkSnippet(language, query) {
|
|
58
|
+
if (language === "python") {
|
|
59
|
+
return `from tinyfish import TinyFish
|
|
60
|
+
|
|
61
|
+
client = TinyFish()
|
|
62
|
+
|
|
63
|
+
# TinyFish Search: discover the right live source
|
|
64
|
+
search = client.search.query(${JSON.stringify(query)})
|
|
65
|
+
top_url = search.results[0].url
|
|
66
|
+
|
|
67
|
+
# TinyFish Fetch: retrieve clean content from that source
|
|
68
|
+
page = client.fetch.get_contents([top_url], format="markdown")
|
|
69
|
+
print(page.results[0].text)`;
|
|
70
|
+
}
|
|
71
|
+
return `import { TinyFish } from "@tiny-fish/sdk";
|
|
72
|
+
|
|
73
|
+
const client = new TinyFish();
|
|
74
|
+
|
|
75
|
+
// TinyFish Search: discover the right live source
|
|
76
|
+
const search = await client.search.query({ query: ${JSON.stringify(query)} });
|
|
77
|
+
const topUrl = search.results[0].url;
|
|
78
|
+
|
|
79
|
+
// TinyFish Fetch: retrieve clean content from that source
|
|
80
|
+
const page = await client.fetch.getContents({ urls: [topUrl], format: "markdown" });
|
|
81
|
+
console.log(page.results[0]?.text);`;
|
|
82
|
+
}
|
|
83
|
+
async function connectAgent(id) {
|
|
84
|
+
const agent = AGENTS.find((candidate) => candidate.id === id);
|
|
85
|
+
if (!agent)
|
|
86
|
+
return;
|
|
87
|
+
const mcpUrl = new URL("/mcp", BASE_URL).toString();
|
|
88
|
+
if (id === "openclaw") {
|
|
89
|
+
await connectOpenClaw({ mcpUrl, launch: false, installCli: false });
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
await agent.connect({ mcpUrl, launch: false });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function parseSelectedAgent(value, selected) {
|
|
96
|
+
if (!value.trim())
|
|
97
|
+
return undefined;
|
|
98
|
+
const agent = selected[Number(value) - 1];
|
|
99
|
+
if (!agent)
|
|
100
|
+
throw new Error("Choose one of the agents you connected.");
|
|
101
|
+
return agent;
|
|
102
|
+
}
|
|
103
|
+
async function chooseQuery(query, interactive) {
|
|
104
|
+
if (query)
|
|
105
|
+
return query;
|
|
106
|
+
if (!interactive)
|
|
107
|
+
throw new Error("Pass a query, or run `tinyfish onboard` in a terminal.");
|
|
108
|
+
outLine("\nChoose a live-web job, or type your own:");
|
|
109
|
+
EXAMPLE_QUERIES.forEach((example, index) => outLine(` ${index + 1}. ${example}`));
|
|
110
|
+
const answer = await ask("Job [1]: ");
|
|
111
|
+
return EXAMPLE_QUERIES[Number(answer || "1") - 1] ?? answer;
|
|
112
|
+
}
|
|
113
|
+
async function runSearchStep(query, apiKey, persistEnvKey) {
|
|
114
|
+
outLine(`\n1. Searching the live web for: ${query}`);
|
|
115
|
+
const search = await searchQuery({ query }, apiKey);
|
|
116
|
+
if (persistEnvKey) {
|
|
117
|
+
saveConfig(apiKey);
|
|
118
|
+
outLine("✓ API key saved to ~/.tinyfish/config.json");
|
|
119
|
+
}
|
|
120
|
+
if (!search.results.length) {
|
|
121
|
+
throw new Error("Search returned no results. Try a more specific query.");
|
|
122
|
+
}
|
|
123
|
+
outLine("\nSearch results");
|
|
124
|
+
for (const result of search.results.slice(0, 3)) {
|
|
125
|
+
outLine(` ${result.position}. ${result.title}\n` +
|
|
126
|
+
` ${result.site_name}${result.date ? ` · ${result.date}` : ""}\n` +
|
|
127
|
+
` ${result.snippet}\n` +
|
|
128
|
+
` ${result.url}`);
|
|
129
|
+
}
|
|
130
|
+
return search.results;
|
|
131
|
+
}
|
|
132
|
+
async function runFetchStep(results, apiKey) {
|
|
133
|
+
const candidates = results.slice(0, 5);
|
|
134
|
+
let failedFetches = 0;
|
|
135
|
+
let lastFetchError;
|
|
136
|
+
for (const candidate of candidates) {
|
|
137
|
+
try {
|
|
138
|
+
const fetched = await fetchContentGet({ urls: [candidate.url], format: "markdown" }, apiKey);
|
|
139
|
+
const page = fetched.results[0];
|
|
140
|
+
if (page && typeof page.text === "string" && isUsefulFetchedContent(page.text)) {
|
|
141
|
+
return { source: candidate, text: cleanFetchedContent(page.text) };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
failedFetches += 1;
|
|
146
|
+
lastFetchError = error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (failedFetches === candidates.length)
|
|
150
|
+
throw lastFetchError;
|
|
151
|
+
throw new Error("Search found sources, but Fetch did not return useful page content.");
|
|
152
|
+
}
|
|
153
|
+
function renderFetchedResult({ source, text }) {
|
|
154
|
+
outLine(`\n2. Fetched source\n ${source.title}\n ${source.site_name} · ${source.url}`);
|
|
155
|
+
outLine(`\nWhy it matched\n${source.snippet}`);
|
|
156
|
+
outLine(`\nFetched content\n${text}`);
|
|
157
|
+
outLine("\n✓ Search found relevant sources; Fetch returned clean, usable content.");
|
|
158
|
+
}
|
|
159
|
+
function agentLabel(id) {
|
|
160
|
+
return AGENTS.find((agent) => agent.id === id)?.label ?? id;
|
|
161
|
+
}
|
|
162
|
+
function errorMessage(error) {
|
|
163
|
+
return error instanceof Error ? error.message : String(error);
|
|
164
|
+
}
|
|
165
|
+
async function runAgentSteps(interactive) {
|
|
166
|
+
if (!interactive)
|
|
167
|
+
return;
|
|
168
|
+
outLine("\n3. Add TinyFish to your agents (choose any, or press Enter to skip):");
|
|
169
|
+
AGENTS.forEach((agent, index) => outLine(` ${index + 1}. ${agent.label}`));
|
|
170
|
+
const selected = parseAgents(await ask("Agents [e.g. 1,2]: "));
|
|
171
|
+
const connected = [];
|
|
172
|
+
for (const id of selected) {
|
|
173
|
+
try {
|
|
174
|
+
await connectAgent(id);
|
|
175
|
+
connected.push(id);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
errLine(` Could not connect ${agentLabel(id)}: ${errorMessage(error)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!connected.length)
|
|
182
|
+
return;
|
|
183
|
+
outLine("\n4. Open an agent now:");
|
|
184
|
+
connected.forEach((id, index) => outLine(` ${index + 1}. ${agentLabel(id)}`));
|
|
185
|
+
const launchId = parseSelectedAgent(await ask("Open [number or Enter to skip]: "), connected);
|
|
186
|
+
if (!launchId)
|
|
187
|
+
return;
|
|
188
|
+
try {
|
|
189
|
+
launchAgent(launchId);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
errLine(` Could not open ${agentLabel(launchId)}: ${errorMessage(error)}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function runSdkStep(interactive, query) {
|
|
196
|
+
if (!interactive)
|
|
197
|
+
return;
|
|
198
|
+
const choice = await ask("\nShow this as SDK code? [t] TypeScript, [p] Python, [Enter] skip: ");
|
|
199
|
+
if (!choice)
|
|
200
|
+
return;
|
|
201
|
+
const language = choice === "p" || choice === "python" ? "python" : "typescript";
|
|
202
|
+
outLine(`\n${sdkSnippet(language, query)}\n`);
|
|
203
|
+
}
|
|
204
|
+
export function registerOnboard(program) {
|
|
205
|
+
program
|
|
206
|
+
.command("onboard")
|
|
207
|
+
.description("Run a guided Search → Fetch demo, then add TinyFish to your agents or SDK")
|
|
208
|
+
.argument("[query]", "A real topic to research")
|
|
209
|
+
.addHelpText("after", "\nExamples:\n" +
|
|
210
|
+
' tinyfish onboard "What changed in the AI dev stack this week?"\n' +
|
|
211
|
+
"\nAgents: claude-code, codex, hermes, openclaw.\n" +
|
|
212
|
+
"Run `tinyfish connect <client> --launch` later to add one directly.")
|
|
213
|
+
.action(async (query) => {
|
|
214
|
+
const apiKey = getApiKey();
|
|
215
|
+
const persistEnvKey = Boolean(process.env["TINYFISH_API_KEY"]);
|
|
216
|
+
delete process.env["TINYFISH_API_KEY"];
|
|
217
|
+
try {
|
|
218
|
+
const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
219
|
+
const selectedQuery = await chooseQuery(query, interactive);
|
|
220
|
+
const searchResults = await runSearchStep(selectedQuery, apiKey, persistEnvKey);
|
|
221
|
+
renderFetchedResult(await runFetchStep(searchResults, apiKey));
|
|
222
|
+
await runAgentSteps(interactive);
|
|
223
|
+
await runSdkStep(interactive, selectedQuery);
|
|
224
|
+
outLine('Try another target with `tinyfish onboard "<query>"`, or run `tinyfish onboard --help`.');
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
handleApiError(error);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { registerRuns } from "./commands/runs.js";
|
|
|
12
12
|
import { registerConfigureClaude } from "./commands/config-claude.js";
|
|
13
13
|
import { registerSearch } from "./commands/search.js";
|
|
14
14
|
import { registerConnect } from "./commands/connect.js";
|
|
15
|
+
import { registerOnboard } from "./commands/onboard.js";
|
|
15
16
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
16
17
|
const program = new Command();
|
|
17
18
|
program
|
|
@@ -28,6 +29,7 @@ program
|
|
|
28
29
|
}
|
|
29
30
|
});
|
|
30
31
|
registerAuth(program);
|
|
32
|
+
registerOnboard(program);
|
|
31
33
|
registerConnect(program);
|
|
32
34
|
registerConfigureClaude(program);
|
|
33
35
|
registerBrowser(program);
|
package/dist/lib/constants.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from "module";
|
|
|
2
2
|
const _require = createRequire(import.meta.url);
|
|
3
3
|
const _pkg = _require("../../package.json");
|
|
4
4
|
export const CLI_VERSION = _pkg.version;
|
|
5
|
-
/** Base URL for the TinyFish API. Override with TINYFISH_API_URL for staging/self-hosted
|
|
5
|
+
/** Base URL for the TinyFish API. Override with TINYFISH_API_URL for staging/self-hosted. */
|
|
6
6
|
const apiUrlOverride = process.env["TINYFISH_API_URL"]?.trim();
|
|
7
7
|
if (apiUrlOverride) {
|
|
8
8
|
let parsed;
|
|
@@ -13,7 +13,8 @@ if (apiUrlOverride) {
|
|
|
13
13
|
process.stderr.write("Error: TINYFISH_API_URL must be a valid https URL\n");
|
|
14
14
|
process.exit(1);
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
const loopbackHttp = parsed.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
|
|
17
|
+
if (parsed.protocol !== "https:" && !loopbackHttp) {
|
|
17
18
|
process.stderr.write(`Error: TINYFISH_API_URL must use https:// (got protocol "${parsed.protocol || "unknown"}")\n`);
|
|
18
19
|
process.exit(1);
|
|
19
20
|
}
|
package/dist/lib/output.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
1
2
|
export class ApiError extends Error {
|
|
2
3
|
status;
|
|
3
4
|
code;
|
|
@@ -11,21 +12,29 @@ export class ApiError extends Error {
|
|
|
11
12
|
export function out(data) {
|
|
12
13
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
13
14
|
}
|
|
15
|
+
function safe(text) {
|
|
16
|
+
// eslint-disable-next-line no-control-regex
|
|
17
|
+
return stripVTControlCharacters(text).replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g, "");
|
|
18
|
+
}
|
|
14
19
|
export function outLine(line) {
|
|
15
|
-
process.stdout.write(line + "\n");
|
|
20
|
+
process.stdout.write(safe(line) + "\n");
|
|
16
21
|
}
|
|
17
22
|
export function errLine(line) {
|
|
18
|
-
process.stderr.write(line + "\n");
|
|
23
|
+
process.stderr.write(safe(line) + "\n");
|
|
19
24
|
}
|
|
20
25
|
export function err(data) {
|
|
21
26
|
const payload = typeof data === "string" ? { error: data } : data;
|
|
22
|
-
|
|
27
|
+
const serialized = JSON.stringify(payload) ?? "undefined";
|
|
28
|
+
process.stderr.write(safe(serialized) + "\n");
|
|
23
29
|
}
|
|
24
30
|
export function handleApiError(e) {
|
|
25
31
|
if (e instanceof ApiError) {
|
|
26
32
|
const payload = { error: e.message, status: e.status };
|
|
27
33
|
if (e.code)
|
|
28
34
|
payload.code = e.code;
|
|
35
|
+
if (e.status === 401)
|
|
36
|
+
payload.hint =
|
|
37
|
+
"Clear the TINYFISH_API_KEY environment variable, run `tinyfish auth login`, then try again.";
|
|
29
38
|
err(payload);
|
|
30
39
|
}
|
|
31
40
|
else if (e instanceof Error) {
|