@tiny-fish/cli 0.6.2-next.106 → 0.7.0

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/README.md CHANGED
@@ -12,10 +12,11 @@ Requires Node.js 24+.
12
12
 
13
13
  ### Connect Claude Code
14
14
 
15
- Add TinyFish, complete MCP OAuth, and start the interactive walkthrough with one command:
15
+ Add TinyFish MCP and the global `use-tinyfish` web skill, authenticate the CLI, and start the
16
+ interactive walkthrough with one command:
16
17
 
17
18
  ```bash
18
- npx -y @tiny-fish/cli@latest connect claude-code --launch
19
+ npx -y @tiny-fish/cli@latest connect claude-code --launch --api-key sk-tinyfish-...
19
20
  ```
20
21
 
21
22
  One-command OAuth requires a Claude Code release whose `claude mcp` command supports `login`.
@@ -28,10 +29,11 @@ configuration.
28
29
 
29
30
  ### Connect Codex
30
31
 
31
- Add TinyFish, complete MCP OAuth, and start the interactive walkthrough with one command:
32
+ Add TinyFish MCP and the global `use-tinyfish` web skill, authenticate the CLI, and start the
33
+ interactive walkthrough with one command:
32
34
 
33
35
  ```bash
34
- npx -y @tiny-fish/cli@latest connect codex --launch
36
+ npx -y @tiny-fish/cli@latest connect codex --launch --api-key sk-tinyfish-...
35
37
  ```
36
38
 
37
39
  One-command OAuth requires a Codex release whose `codex mcp add` command supports
@@ -40,10 +42,11 @@ One-command OAuth requires a Codex release whose `codex mcp add` command support
40
42
 
41
43
  ### Connect Hermes
42
44
 
43
- Add TinyFish, complete MCP OAuth, and start the interactive walkthrough with one command:
45
+ Add TinyFish MCP and the global `use-tinyfish` web skill, authenticate the CLI, and start the
46
+ interactive walkthrough with one command:
44
47
 
45
48
  ```bash
46
- npx -y @tiny-fish/cli@latest connect hermes --launch
49
+ npx -y @tiny-fish/cli@latest connect hermes --launch --api-key sk-tinyfish-...
47
50
  ```
48
51
 
49
52
  Hermes completes OAuth while adding the MCP server. After setup, the command starts the walkthrough
@@ -3,14 +3,17 @@ export declare const DEFAULT_ONBOARDING_PROMPT: string;
3
3
  export declare const OPENCLAW_ONBOARDING_PROMPT: string;
4
4
  export type AgentClient = "claude-code" | "codex" | "hermes" | "openclaw";
5
5
  export declare function connectClaudeCode(options: {
6
+ apiKey?: string;
6
7
  mcpUrl: string;
7
8
  launch: boolean;
8
9
  }): Promise<void>;
9
10
  export declare function connectCodex(options: {
11
+ apiKey?: string;
10
12
  mcpUrl: string;
11
13
  launch: boolean;
12
14
  }): Promise<void>;
13
15
  export declare function connectHermes(options: {
16
+ apiKey?: string;
14
17
  mcpUrl: string;
15
18
  launch: boolean;
16
19
  }): Promise<void>;
@@ -1,9 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import * as path from "node:path";
2
3
  import spawn from "cross-spawn";
4
+ import { persistApiKeyToEnvironment, saveConfig, validateKeyFormat } from "../lib/auth.js";
3
5
  import { CLI_VERSION } from "../lib/constants.js";
4
6
  import { errLine } from "../lib/output.js";
5
7
  const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
6
8
  const NON_INTERACTIVE_TIMEOUT_MS = 10_000;
9
+ const SKILL_INSTALL_TIMEOUT_MS = 120_000;
7
10
  const HERMES_SEED_TIMEOUT_MS = 120_000;
8
11
  const TELEMETRY_TIMEOUT_MS = 500;
9
12
  const MCP_LOGIN_UNAVAILABLE_MESSAGE = "This Claude Code installation does not expose `claude mcp login`, which is required for " +
@@ -16,6 +19,10 @@ const HERMES_OAUTH_UNAVAILABLE_MESSAGE = "This Hermes installation does not supp
16
19
  const OPENCLAW_SKILL_UNAVAILABLE_MESSAGE = "This OpenClaw installation does not support global skill installation. Update OpenClaw and " +
17
20
  "retry.";
18
21
  const TINYFISH_CLI_NOT_FOUND_MESSAGE = "TinyFish CLI installed but is not available on PATH. Open a new terminal and retry.";
22
+ const TINYFISH_CLI_PACKAGE = "@tiny-fish/cli@latest";
23
+ const SKILLS_CLI_PACKAGE = "skills@1.4.6";
24
+ const TINYFISH_WEB_SKILL_SOURCE = "tinyfish-io/tinyfish-cookbook";
25
+ const TINYFISH_WEB_SKILL = "use-tinyfish";
19
26
  const OPENCLAW_SKILL = "@tinyfish/tinyfish";
20
27
  export const DEFAULT_ONBOARDING_PROMPT = "I just connected TinyFish. Call the guide_next_step tool now to begin. Then follow its " +
21
28
  "instructions one step at a time. Ask for my input and wait for my reply before each subsequent " +
@@ -27,6 +34,7 @@ export const OPENCLAW_ONBOARDING_PROMPT = "I just installed the TinyFish skill.
27
34
  const CLAUDE_CODE = {
28
35
  command: "claude",
29
36
  connectClient: "claude-code",
37
+ skillAgent: "claude-code",
30
38
  displayName: "Claude Code",
31
39
  supportCheck: {
32
40
  args: ["mcp", "--help"],
@@ -62,6 +70,7 @@ function launchCodexWalkthrough() {
62
70
  const CODEX = {
63
71
  command: "codex",
64
72
  connectClient: "codex",
73
+ skillAgent: "codex",
65
74
  displayName: "Codex",
66
75
  supportCheck: {
67
76
  args: ["mcp", "add", "--help"],
@@ -107,6 +116,7 @@ function launchHermesWalkthrough() {
107
116
  const HERMES = {
108
117
  command: "hermes",
109
118
  connectClient: "hermes",
119
+ skillAgent: "hermes-agent",
110
120
  displayName: "Hermes",
111
121
  supportCheck: {
112
122
  args: ["mcp", "add", "--help"],
@@ -181,6 +191,73 @@ function requireCommandSupport(client) {
181
191
  function commandNotFound(error) {
182
192
  return error?.code === "ENOENT";
183
193
  }
194
+ function installTinyFishCli() {
195
+ errLine("Installing the TinyFish CLI...");
196
+ const result = spawn.sync("npm", ["install", "--global", TINYFISH_CLI_PACKAGE], {
197
+ stdio: "inherit",
198
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
199
+ });
200
+ if (result.error || result.status !== 0) {
201
+ throw new Error("Could not install the TinyFish CLI", { cause: result.error });
202
+ }
203
+ }
204
+ function installWebSkill(client) {
205
+ errLine(`Installing the TinyFish web skill in ${client.displayName}...`);
206
+ const result = spawn.sync("npx", [
207
+ "-y",
208
+ SKILLS_CLI_PACKAGE,
209
+ "add",
210
+ TINYFISH_WEB_SKILL_SOURCE,
211
+ "--skill",
212
+ TINYFISH_WEB_SKILL,
213
+ "--global",
214
+ "--agent",
215
+ client.skillAgent,
216
+ "--yes",
217
+ ], {
218
+ env: {
219
+ ...process.env,
220
+ PATH: `${path.dirname(process.execPath)}${path.delimiter}${process.env.PATH ?? ""}`,
221
+ },
222
+ stdio: "inherit",
223
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
224
+ });
225
+ if (result.error || result.status !== 0) {
226
+ throw new Error(`Could not install the TinyFish web skill in ${client.displayName}`, {
227
+ cause: result.error,
228
+ });
229
+ }
230
+ }
231
+ function ensureCliAuthenticated(source, apiKey) {
232
+ const envKey = apiKey ?? process.env["TINYFISH_API_KEY"];
233
+ if (envKey) {
234
+ if (!validateKeyFormat(envKey)) {
235
+ throw new Error("TINYFISH_API_KEY has invalid format");
236
+ }
237
+ saveConfig(envKey);
238
+ persistApiKeyToEnvironment(envKey);
239
+ return;
240
+ }
241
+ const authStatus = spawn.sync("tinyfish", ["auth", "status"], {
242
+ stdio: "ignore",
243
+ timeout: NON_INTERACTIVE_TIMEOUT_MS,
244
+ });
245
+ if (commandNotFound(authStatus.error)) {
246
+ throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: authStatus.error });
247
+ }
248
+ if (authStatus.status === 0)
249
+ return;
250
+ errLine("Signing in to the TinyFish CLI...");
251
+ const loginResult = spawn.sync("tinyfish", ["auth", "login", "--source", source], {
252
+ stdio: "inherit",
253
+ });
254
+ if (commandNotFound(loginResult.error)) {
255
+ throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: loginResult.error });
256
+ }
257
+ if (loginResult.error || loginResult.status !== 0) {
258
+ throw new Error("Could not authenticate the TinyFish CLI", { cause: loginResult.error });
259
+ }
260
+ }
184
261
  function removeExistingRegistration(client, removal) {
185
262
  const result = spawn.sync(client.command, removal.args, {
186
263
  encoding: "utf8",
@@ -265,6 +342,12 @@ async function connectNativeMcpClient(client, options) {
265
342
  });
266
343
  }
267
344
  }
345
+ failureStage = "cli_install";
346
+ installTinyFishCli();
347
+ failureStage = "skill_install";
348
+ installWebSkill(client);
349
+ failureStage = "authentication";
350
+ ensureCliAuthenticated(client.connectClient, options.apiKey);
268
351
  if (!options.launch) {
269
352
  errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
270
353
  await telemetry.track("completed");
@@ -300,6 +383,7 @@ export async function connectOpenClaw(options) {
300
383
  errLine("Installing the TinyFish CLI...");
301
384
  const cliInstallResult = spawn.sync("npm", ["install", "--global", "@tiny-fish/cli@latest"], {
302
385
  stdio: "inherit",
386
+ timeout: SKILL_INSTALL_TIMEOUT_MS,
303
387
  });
304
388
  if (cliInstallResult.error || cliInstallResult.status !== 0) {
305
389
  throw new Error("Could not install the TinyFish CLI", { cause: cliInstallResult.error });
@@ -314,7 +398,10 @@ export async function connectOpenClaw(options) {
314
398
  });
315
399
  }
316
400
  failureStage = "authentication";
317
- const authStatus = spawn.sync("tinyfish", ["auth", "status"], { stdio: "ignore" });
401
+ const authStatus = spawn.sync("tinyfish", ["auth", "status"], {
402
+ stdio: "ignore",
403
+ timeout: NON_INTERACTIVE_TIMEOUT_MS,
404
+ });
318
405
  if (commandNotFound(authStatus.error)) {
319
406
  throw new Error(TINYFISH_CLI_NOT_FOUND_MESSAGE, { cause: authStatus.error });
320
407
  }
@@ -366,9 +453,13 @@ export function registerConnect(program) {
366
453
  .command("connect")
367
454
  .description("Connect TinyFish to an AI agent")
368
455
  .argument("<client>", "Agent client to connect (claude-code, codex, hermes, or openclaw)")
456
+ .option("--api-key <apiKey>", "TinyFish API key to persist for the installed skill")
369
457
  .option("--launch", "Launch the agent and start the TinyFish walkthrough")
370
458
  .option("--url <mcpUrl>", "MCP endpoint override")
371
459
  .action(async (client, options) => {
460
+ if (options.apiKey && !validateKeyFormat(options.apiKey)) {
461
+ throw new Error("Invalid --api-key value");
462
+ }
372
463
  const mcpUrl = options.url ?? DEFAULT_MCP_URL;
373
464
  if (mcpUrl.startsWith("-")) {
374
465
  throw new Error(`Invalid --url value: ${mcpUrl}`);
@@ -383,7 +474,7 @@ export function registerConnect(program) {
383
474
  if (!["http:", "https:"].includes(parsedUrl.protocol)) {
384
475
  throw new Error(`Invalid --url value: ${mcpUrl}`);
385
476
  }
386
- const connectOptions = { mcpUrl, launch: options.launch ?? false };
477
+ const connectOptions = { apiKey: options.apiKey, mcpUrl, launch: options.launch ?? false };
387
478
  if (client === "claude-code") {
388
479
  await connectClaudeCode(connectOptions);
389
480
  }
@@ -394,7 +485,7 @@ export function registerConnect(program) {
394
485
  await connectHermes(connectOptions);
395
486
  }
396
487
  else if (client === "openclaw") {
397
- await connectOpenClaw(connectOptions);
488
+ await connectOpenClaw({ mcpUrl: connectOptions.mcpUrl, launch: connectOptions.launch });
398
489
  }
399
490
  else {
400
491
  throw new Error(`Unsupported client: ${client}. Supported clients: claude-code, codex, hermes, openclaw`);
@@ -5,6 +5,11 @@ interface TinyfishConfig {
5
5
  }
6
6
  export declare function loadConfig(): TinyfishConfig;
7
7
  export declare function saveConfig(apiKey: string): void;
8
+ interface EnvironmentOptions {
9
+ platform?: typeof process.platform;
10
+ shell?: string;
11
+ }
12
+ export declare function persistApiKeyToEnvironment(apiKey: string, options?: EnvironmentOptions): void;
8
13
  export declare function clearConfig(): boolean;
9
14
  export declare function validateKeyFormat(key: string): boolean;
10
15
  export declare function maskKey(key: string): string;
package/dist/lib/auth.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as fs from "fs";
2
2
  import * as os from "os";
3
3
  import * as path from "path";
4
+ import spawn from "cross-spawn";
4
5
  import { err } from "./output.js";
5
6
  const KEY_PREFIXES = ["sk-tinyfish-", "sk-mino-"];
6
7
  const MIN_KEY_LENGTH = 20;
@@ -41,6 +42,46 @@ export function saveConfig(apiKey) {
41
42
  process.exit(1);
42
43
  }
43
44
  }
45
+ export function persistApiKeyToEnvironment(apiKey, options = {}) {
46
+ const platform = options.platform ?? process.platform;
47
+ if (platform === "win32") {
48
+ const result = spawn.sync("setx", ["TINYFISH_API_KEY", apiKey], { stdio: "ignore" });
49
+ if (result.error || result.status !== 0) {
50
+ throw new Error("Could not set TINYFISH_API_KEY in the Windows user environment", {
51
+ cause: result.error,
52
+ });
53
+ }
54
+ return;
55
+ }
56
+ const home = os.homedir();
57
+ const shell = path.basename(options.shell ?? process.env["SHELL"] ?? "");
58
+ const fish = shell === "fish";
59
+ const macBashProfile = [".bash_profile", ".bash_login", ".profile"]
60
+ .map((name) => path.join(home, name)) // nosemgrep: path-join-resolve-traversal -- fixed filenames under os.homedir()
61
+ .find((candidate) => fs.existsSync(candidate)) ?? path.join(home, ".bash_profile");
62
+ const profile = shell === "zsh"
63
+ ? path.join(home, ".zshrc")
64
+ : shell === "bash"
65
+ ? platform === "darwin"
66
+ ? macBashProfile
67
+ : path.join(home, ".bashrc")
68
+ : fish
69
+ ? path.join(home, ".config", "fish", "config.fish")
70
+ : path.join(home, ".profile");
71
+ const quotedKey = `'${apiKey.replaceAll("'", "'\\''")}'`;
72
+ const assignment = fish
73
+ ? `set -gx TINYFISH_API_KEY ${quotedKey}`
74
+ : `export TINYFISH_API_KEY=${quotedKey}`;
75
+ const matcher = fish
76
+ ? /^set -gx TINYFISH_API_KEY .*(?:\r?\n|$)/gm
77
+ : /^export TINYFISH_API_KEY=.*(?:\r?\n|$)/gm;
78
+ const existing = fs.existsSync(profile) ? fs.readFileSync(profile, "utf8") : "";
79
+ const cleaned = existing.replace(matcher, "");
80
+ const updated = `${cleaned}${cleaned && !cleaned.endsWith("\n") ? "\n" : ""}${assignment}\n`;
81
+ fs.mkdirSync(path.dirname(profile), { recursive: true });
82
+ fs.writeFileSync(profile, updated, { mode: 0o600 });
83
+ fs.chmodSync(profile, 0o600);
84
+ }
44
85
  export function clearConfig() {
45
86
  const config = loadConfig();
46
87
  if (!config.api_key)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.6.2-next.106",
3
+ "version": "0.7.0",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {