@tiny-fish/cli 0.2.1 → 0.3.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
@@ -26,6 +26,29 @@ The command replaces existing user- and local-scope TinyFish registrations. It i
26
26
  leaves project-scoped `.mcp.json` entries unchanged because those are shared repository
27
27
  configuration.
28
28
 
29
+ ### Connect Codex
30
+
31
+ Add TinyFish, complete MCP OAuth, and start the interactive walkthrough with one command:
32
+
33
+ ```bash
34
+ npx -y @tiny-fish/cli@latest connect codex --launch
35
+ ```
36
+
37
+ One-command OAuth requires a Codex release whose `codex mcp add` command supports
38
+ `--oauth-resource`. Update Codex if support is unavailable, or add TinyFish manually with
39
+ `codex mcp add`.
40
+
41
+ ### Connect Hermes
42
+
43
+ Add TinyFish, complete MCP OAuth, and start the interactive walkthrough with one command:
44
+
45
+ ```bash
46
+ npx -y @tiny-fish/cli@latest connect hermes --launch
47
+ ```
48
+
49
+ Hermes completes OAuth while adding the MCP server. After setup, the command starts the walkthrough
50
+ in a Hermes session and leaves that session open for your replies.
51
+
29
52
  ## Authentication
30
53
 
31
54
  Get your API key from [agent.tinyfish.ai](https://agent.tinyfish.ai).
@@ -4,4 +4,12 @@ export declare function connectClaudeCode(options: {
4
4
  mcpUrl: string;
5
5
  launch: boolean;
6
6
  }): void;
7
+ export declare function connectCodex(options: {
8
+ mcpUrl: string;
9
+ launch: boolean;
10
+ }): void;
11
+ export declare function connectHermes(options: {
12
+ mcpUrl: string;
13
+ launch: boolean;
14
+ }): void;
7
15
  export declare function registerConnect(program: Command): void;
@@ -2,25 +2,104 @@ import spawn from "cross-spawn";
2
2
  import { errLine } from "../lib/output.js";
3
3
  const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
4
4
  const NON_INTERACTIVE_TIMEOUT_MS = 10_000;
5
+ const HERMES_SEED_TIMEOUT_MS = 120_000;
5
6
  const MCP_LOGIN_UNAVAILABLE_MESSAGE = "This Claude Code installation does not expose `claude mcp login`, which is required for " +
6
7
  "one-command MCP authentication. Update Claude Code and retry, or authenticate TinyFish " +
7
8
  "manually from /mcp.";
8
- export const DEFAULT_ONBOARDING_PROMPT = "I just connected TinyFish. Call the guide_next_step tool and walk me through it one step at a " +
9
- "time. Ask for my input and wait for my reply before every tool call.";
10
- function requireNativeMcpLogin() {
11
- const result = spawn.sync("claude", ["mcp", "--help"], {
9
+ const CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE = "This Codex installation does not support one-command MCP authentication. Update Codex and " +
10
+ "retry, or add TinyFish manually with `codex mcp add`.";
11
+ const HERMES_OAUTH_UNAVAILABLE_MESSAGE = "This Hermes installation does not support one-command MCP authentication. Update Hermes and " +
12
+ "retry, or add TinyFish manually with `hermes mcp add`.";
13
+ export const DEFAULT_ONBOARDING_PROMPT = "I just connected TinyFish. Call the guide_next_step tool now to begin. Then follow its " +
14
+ "instructions one step at a time. Ask for my input and wait for my reply before each subsequent " +
15
+ "TinyFish tool call.";
16
+ const CLAUDE_CODE = {
17
+ command: "claude",
18
+ displayName: "Claude Code",
19
+ supportCheck: {
20
+ args: ["mcp", "--help"],
21
+ pattern: /^\s*login(?:\s|\[)/m,
22
+ unavailableMessage: MCP_LOGIN_UNAVAILABLE_MESSAGE,
23
+ },
24
+ loginArgs: ["mcp", "login", "tinyfish"],
25
+ // Project scope is shared in .mcp.json; a user setup command must not rewrite it.
26
+ removals: [
27
+ { args: ["mcp", "remove", "tinyfish", "--scope", "user"], label: "user TinyFish registration" },
28
+ {
29
+ args: ["mcp", "remove", "tinyfish", "--scope", "local"],
30
+ label: "local TinyFish registration",
31
+ },
32
+ ],
33
+ addArgs: (mcpUrl) => ["mcp", "add", "--scope", "user", "--transport", "http", "tinyfish", mcpUrl],
34
+ };
35
+ const CODEX = {
36
+ command: "codex",
37
+ displayName: "Codex",
38
+ supportCheck: {
39
+ args: ["mcp", "add", "--help"],
40
+ pattern: /--oauth-resource(?:\s|<)/,
41
+ unavailableMessage: CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE,
42
+ },
43
+ removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Codex" }],
44
+ addArgs: (mcpUrl) => ["mcp", "add", "tinyfish", "--url", mcpUrl, "--oauth-resource", mcpUrl],
45
+ };
46
+ function launchHermesWalkthrough() {
47
+ const seedResult = spawn.sync("hermes", ["chat", "-Q", "-q", DEFAULT_ONBOARDING_PROMPT], {
48
+ encoding: "utf8",
49
+ timeout: HERMES_SEED_TIMEOUT_MS,
50
+ });
51
+ if (seedResult.error || seedResult.status !== 0) {
52
+ throw new Error("Could not start the TinyFish walkthrough in Hermes", {
53
+ cause: seedResult.error,
54
+ });
55
+ }
56
+ const sessionId = seedResult.stderr?.match(/session_id:\s*([^\s]+)/i)?.[1];
57
+ if (!sessionId) {
58
+ throw new Error("Hermes did not return a walkthrough session ID");
59
+ }
60
+ const result = spawn.sync("hermes", ["--resume", sessionId], { stdio: "inherit" });
61
+ if (result.error) {
62
+ throw new Error("Could not launch Hermes", { cause: result.error });
63
+ }
64
+ if (result.signal === "SIGINT" || result.signal === "SIGTERM")
65
+ return;
66
+ if (result.status !== 0) {
67
+ throw new Error(`Hermes walkthrough exited with status ${result.status ?? "unknown"}`);
68
+ }
69
+ }
70
+ const HERMES = {
71
+ command: "hermes",
72
+ displayName: "Hermes",
73
+ supportCheck: {
74
+ args: ["mcp", "add", "--help"],
75
+ pattern: /--auth\s+\{[^}]*oauth[^}]*\}/,
76
+ unavailableMessage: HERMES_OAUTH_UNAVAILABLE_MESSAGE,
77
+ },
78
+ removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Hermes" }],
79
+ // Hermes completes OAuth while adding the server; a separate `mcp login` would authenticate twice.
80
+ addArgs: (mcpUrl) => ["mcp", "add", "tinyfish", "--url", mcpUrl, "--auth", "oauth"],
81
+ launchWalkthrough: launchHermesWalkthrough,
82
+ };
83
+ function requireNativeMcpSupport(client) {
84
+ const result = spawn.sync(client.command, client.supportCheck.args, {
12
85
  encoding: "utf8",
13
86
  timeout: NON_INTERACTIVE_TIMEOUT_MS,
14
87
  });
88
+ if (result.error?.code === "ENOENT") {
89
+ throw new Error(`${client.displayName} is not installed or not available on PATH.`, {
90
+ cause: result.error,
91
+ });
92
+ }
15
93
  if (result.error || result.status !== 0) {
16
- throw new Error(MCP_LOGIN_UNAVAILABLE_MESSAGE, { cause: result.error });
94
+ throw new Error(client.supportCheck.unavailableMessage, { cause: result.error });
17
95
  }
18
- if (!/^\s*login(?:\s|\[)/m.test(result.stdout ?? "")) {
19
- throw new Error(MCP_LOGIN_UNAVAILABLE_MESSAGE);
96
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
97
+ if (!client.supportCheck.pattern.test(output)) {
98
+ throw new Error(client.supportCheck.unavailableMessage);
20
99
  }
21
100
  }
22
- function removeExistingRegistration(scope) {
23
- const result = spawn.sync("claude", ["mcp", "remove", "tinyfish", "--scope", scope], {
101
+ function removeExistingRegistration(client, removal) {
102
+ const result = spawn.sync(client.command, removal.args, {
24
103
  encoding: "utf8",
25
104
  timeout: NON_INTERACTIVE_TIMEOUT_MS,
26
105
  });
@@ -30,55 +109,72 @@ function removeExistingRegistration(scope) {
30
109
  if (/No MCP server named "tinyfish"/i.test(details))
31
110
  return;
32
111
  if (result.error) {
33
- throw new Error(`Could not remove existing ${scope} TinyFish registration: ${details}`, {
112
+ throw new Error(`Could not remove existing ${removal.label}: ${details}`, {
34
113
  cause: result.error,
35
114
  });
36
115
  }
37
- throw new Error(`Could not remove existing ${scope} TinyFish registration: ${details}`);
116
+ throw new Error(`Could not remove existing ${removal.label}: ${details}`);
38
117
  }
39
- export function connectClaudeCode(options) {
40
- requireNativeMcpLogin();
41
- // Project scope is shared in .mcp.json; a user setup command must not rewrite it.
42
- removeExistingRegistration("user");
43
- removeExistingRegistration("local");
44
- errLine("Adding TinyFish to Claude Code...");
45
- const addResult = spawn.sync("claude", ["mcp", "add", "--scope", "user", "--transport", "http", "tinyfish", options.mcpUrl], { stdio: "inherit" });
118
+ function connectNativeMcpClient(client, options) {
119
+ requireNativeMcpSupport(client);
120
+ for (const removal of client.removals)
121
+ removeExistingRegistration(client, removal);
122
+ errLine(`Adding TinyFish to ${client.displayName}...`);
123
+ const addResult = spawn.sync(client.command, client.addArgs(options.mcpUrl), {
124
+ stdio: "inherit",
125
+ });
46
126
  if (addResult.error || addResult.status !== 0) {
47
- throw new Error("Could not add TinyFish to Claude Code", { cause: addResult.error });
127
+ throw new Error(`Could not add TinyFish to ${client.displayName}`, {
128
+ cause: addResult.error,
129
+ });
48
130
  }
49
- errLine("Signing in to TinyFish...");
50
- const loginResult = spawn.sync("claude", ["mcp", "login", "tinyfish"], { stdio: "inherit" });
51
- if (loginResult.error || loginResult.status !== 0) {
52
- throw new Error("Could not authenticate TinyFish in Claude Code", {
53
- cause: loginResult.error,
131
+ if (client.loginArgs) {
132
+ errLine("Signing in to TinyFish...");
133
+ const loginResult = spawn.sync(client.command, client.loginArgs, {
134
+ stdio: "inherit",
54
135
  });
136
+ if (loginResult.error || loginResult.status !== 0) {
137
+ throw new Error(`Could not authenticate TinyFish in ${client.displayName}`, {
138
+ cause: loginResult.error,
139
+ });
140
+ }
55
141
  }
56
142
  if (!options.launch) {
57
- errLine("TinyFish is connected. Open Claude Code to start using it.");
143
+ errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
58
144
  return;
59
145
  }
60
- errLine("Starting the TinyFish walkthrough in Claude Code...");
61
- const result = spawn.sync("claude", [DEFAULT_ONBOARDING_PROMPT], { stdio: "inherit" });
146
+ errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
147
+ if (client.launchWalkthrough) {
148
+ client.launchWalkthrough();
149
+ return;
150
+ }
151
+ const result = spawn.sync(client.command, [DEFAULT_ONBOARDING_PROMPT], { stdio: "inherit" });
62
152
  if (result.error) {
63
- throw new Error("Could not launch Claude Code", { cause: result.error });
153
+ throw new Error(`Could not launch ${client.displayName}`, { cause: result.error });
64
154
  }
65
155
  if (result.signal === "SIGINT" || result.signal === "SIGTERM")
66
156
  return;
67
157
  if (result.status !== 0) {
68
- throw new Error(`Claude Code walkthrough exited with status ${result.status ?? "unknown"}`);
158
+ throw new Error(`${client.displayName} walkthrough exited with status ${result.status ?? "unknown"}`);
69
159
  }
70
160
  }
161
+ export function connectClaudeCode(options) {
162
+ connectNativeMcpClient(CLAUDE_CODE, options);
163
+ }
164
+ export function connectCodex(options) {
165
+ connectNativeMcpClient(CODEX, options);
166
+ }
167
+ export function connectHermes(options) {
168
+ connectNativeMcpClient(HERMES, options);
169
+ }
71
170
  export function registerConnect(program) {
72
171
  program
73
172
  .command("connect")
74
173
  .description("Connect TinyFish to an AI agent")
75
- .argument("<client>", "Agent client to connect (claude-code)")
174
+ .argument("<client>", "Agent client to connect (claude-code, codex, or hermes)")
76
175
  .option("--launch", "Launch the agent and start the TinyFish walkthrough")
77
176
  .option("--url <mcpUrl>", "MCP endpoint override")
78
177
  .action((client, options) => {
79
- if (client !== "claude-code") {
80
- throw new Error(`Unsupported client: ${client}. Supported client: claude-code`);
81
- }
82
178
  const mcpUrl = options.url ?? DEFAULT_MCP_URL;
83
179
  if (mcpUrl.startsWith("-")) {
84
180
  throw new Error(`Invalid --url value: ${mcpUrl}`);
@@ -93,9 +189,18 @@ export function registerConnect(program) {
93
189
  if (!["http:", "https:"].includes(parsedUrl.protocol)) {
94
190
  throw new Error(`Invalid --url value: ${mcpUrl}`);
95
191
  }
96
- connectClaudeCode({
97
- mcpUrl,
98
- launch: options.launch ?? false,
99
- });
192
+ const connectOptions = { mcpUrl, launch: options.launch ?? false };
193
+ if (client === "claude-code") {
194
+ connectClaudeCode(connectOptions);
195
+ }
196
+ else if (client === "codex") {
197
+ connectCodex(connectOptions);
198
+ }
199
+ else if (client === "hermes") {
200
+ connectHermes(connectOptions);
201
+ }
202
+ else {
203
+ throw new Error(`Unsupported client: ${client}. Supported clients: claude-code, codex, hermes`);
204
+ }
100
205
  });
101
206
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {