@tiny-fish/cli 0.6.2-next.106 → 0.6.2-next.107
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 +9 -6
- package/dist/commands/connect.d.ts +3 -0
- package/dist/commands/connect.js +94 -3
- package/dist/lib/auth.d.ts +5 -0
- package/dist/lib/auth.js +41 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,10 +12,11 @@ Requires Node.js 24+.
|
|
|
12
12
|
|
|
13
13
|
### Connect Claude Code
|
|
14
14
|
|
|
15
|
-
Add TinyFish,
|
|
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,
|
|
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,
|
|
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>;
|
package/dist/commands/connect.js
CHANGED
|
@@ -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"], {
|
|
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`);
|
package/dist/lib/auth.d.ts
CHANGED
|
@@ -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)
|