@tiny-fish/cli 0.6.2-next.104 → 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,6 +1,7 @@
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;
@@ -16,5 +17,7 @@ export declare function connectHermes(options: {
16
17
  export declare function connectOpenClaw(options: {
17
18
  mcpUrl: string;
18
19
  launch: boolean;
20
+ installCli?: boolean;
19
21
  }): Promise<void>;
22
+ export declare function launchAgent(client: AgentClient): void;
20
23
  export declare function registerConnect(program: Command): void;
@@ -44,6 +44,21 @@ const CLAUDE_CODE = {
44
44
  ],
45
45
  addArgs: (mcpUrl) => ["mcp", "add", "--scope", "user", "--transport", "http", "tinyfish", mcpUrl],
46
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
+ }
47
62
  const CODEX = {
48
63
  command: "codex",
49
64
  connectClient: "codex",
@@ -53,6 +68,7 @@ const CODEX = {
53
68
  patterns: [/--oauth-resource(?:\s|<)/],
54
69
  unavailableMessage: CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE,
55
70
  },
71
+ launchWalkthrough: launchCodexWalkthrough,
56
72
  removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Codex" }],
57
73
  addArgs: (mcpUrl, oauthResource) => [
58
74
  "mcp",
@@ -182,6 +198,38 @@ function removeExistingRegistration(client, removal) {
182
198
  }
183
199
  throw new Error(`Could not remove existing ${removal.label}: ${details}`);
184
200
  }
201
+ function launchNativeMcpClient(client) {
202
+ errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
203
+ if (client.launchWalkthrough) {
204
+ client.launchWalkthrough();
205
+ return true;
206
+ }
207
+ const result = spawn.sync(client.command, [DEFAULT_ONBOARDING_PROMPT], { stdio: "inherit" });
208
+ if (result.error) {
209
+ throw new Error(`Could not launch ${client.displayName}`, { cause: result.error });
210
+ }
211
+ if (result.signal === "SIGINT" || result.signal === "SIGTERM")
212
+ return false;
213
+ if (result.status !== 0) {
214
+ throw new Error(`${client.displayName} walkthrough exited with status ${result.status ?? "unknown"}`);
215
+ }
216
+ return true;
217
+ }
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 });
225
+ }
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
+ }
185
233
  async function connectNativeMcpClient(client, options) {
186
234
  const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient);
187
235
  let failureStage = "prerequisite_check";
@@ -223,21 +271,8 @@ async function connectNativeMcpClient(client, options) {
223
271
  return;
224
272
  }
225
273
  failureStage = "walkthrough_launch";
226
- errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
227
- if (client.launchWalkthrough) {
228
- client.launchWalkthrough();
229
- await telemetry.track("completed");
274
+ if (!launchNativeMcpClient(client))
230
275
  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
276
  await telemetry.track("completed");
242
277
  }
243
278
  catch (error) {
@@ -260,13 +295,15 @@ export async function connectOpenClaw(options) {
260
295
  try {
261
296
  await telemetry.track("started");
262
297
  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",
267
- });
268
- if (cliInstallResult.error || cliInstallResult.status !== 0) {
269
- throw new Error("Could not install the TinyFish CLI", { cause: cliInstallResult.error });
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
+ }
270
307
  }
271
308
  failureStage = "skill_install";
272
309
  errLine("Installing the TinyFish skill in OpenClaw...");
@@ -301,18 +338,8 @@ export async function connectOpenClaw(options) {
301
338
  return;
302
339
  }
303
340
  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")
341
+ if (!launchOpenClawWalkthrough())
312
342
  return;
313
- if (launchResult.status !== 0) {
314
- throw new Error(`OpenClaw walkthrough exited with status ${launchResult.status ?? "unknown"}`);
315
- }
316
343
  await telemetry.track("completed");
317
344
  }
318
345
  catch (error) {
@@ -320,6 +347,20 @@ export async function connectOpenClaw(options) {
320
347
  throw error;
321
348
  }
322
349
  }
350
+ export function launchAgent(client) {
351
+ if (client === "claude-code") {
352
+ launchNativeMcpClient(CLAUDE_CODE);
353
+ }
354
+ else if (client === "codex") {
355
+ launchNativeMcpClient(CODEX);
356
+ }
357
+ else if (client === "hermes") {
358
+ launchNativeMcpClient(HERMES);
359
+ }
360
+ else {
361
+ launchOpenClawWalkthrough();
362
+ }
363
+ }
323
364
  export function registerConnect(program) {
324
365
  program
325
366
  .command("connect")
@@ -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.104",
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": {