@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.
@@ -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
- }): void;
20
+ installCli?: boolean;
21
+ }): Promise<void>;
22
+ export declare function launchAgent(client: AgentClient): void;
19
23
  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.";
@@ -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 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, {
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
- export function connectClaudeCode(options) {
196
- connectNativeMcpClient(CLAUDE_CODE, options);
197
- }
198
- export function connectCodex(options) {
199
- connectNativeMcpClient(CODEX, options);
200
- }
201
- export function connectHermes(options) {
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
- 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,
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
- 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 });
278
+ catch (error) {
279
+ await telemetry.track("failed", failureStage);
280
+ throw error;
228
281
  }
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 });
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 (loginResult.error || loginResult.status !== 0) {
236
- throw new Error("Could not authenticate TinyFish for OpenClaw", {
237
- cause: loginResult.error,
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
- if (!options.launch) {
242
- errLine("TinyFish is connected. Open OpenClaw to start using it.");
243
- return;
345
+ catch (error) {
346
+ await telemetry.track("failed", failureStage);
347
+ throw error;
244
348
  }
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 });
349
+ }
350
+ export function launchAgent(client) {
351
+ if (client === "claude-code") {
352
+ launchNativeMcpClient(CLAUDE_CODE);
249
353
  }
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"}`);
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({ launch: connectOptions.launch });
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,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerOnboard(program: Command): void;
@@ -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);
@@ -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 (must be https). */
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
- if (parsed.protocol !== "https:") {
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
  }
@@ -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
- process.stderr.write(JSON.stringify(payload) + "\n");
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.6.2-next.102",
3
+ "version": "0.6.2-next.105",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {