@tiny-fish/cli 0.2.0 → 0.2.1-next.83
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 +28 -0
- package/dist/commands/connect.d.ts +11 -0
- package/dist/commands/connect.js +164 -0
- package/dist/commands/fetch.js +24 -1
- package/dist/index.js +2 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -10,6 +10,34 @@ npm install -g @tiny-fish/cli
|
|
|
10
10
|
|
|
11
11
|
Requires Node.js 24+.
|
|
12
12
|
|
|
13
|
+
### Connect Claude Code
|
|
14
|
+
|
|
15
|
+
Add TinyFish, complete MCP OAuth, and start the interactive walkthrough with one command:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx -y @tiny-fish/cli@latest connect claude-code --launch
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
One-command OAuth requires a Claude Code release whose `claude mcp` command supports `login`.
|
|
22
|
+
Update Claude Code if the command reports that support is unavailable; older versions require
|
|
23
|
+
manual authentication from `/mcp`.
|
|
24
|
+
|
|
25
|
+
The command replaces existing user- and local-scope TinyFish registrations. It intentionally
|
|
26
|
+
leaves project-scoped `.mcp.json` entries unchanged because those are shared repository
|
|
27
|
+
configuration.
|
|
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
|
+
|
|
13
41
|
## Authentication
|
|
14
42
|
|
|
15
43
|
Get your API key from [agent.tinyfish.ai](https://agent.tinyfish.ai).
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
export declare const DEFAULT_ONBOARDING_PROMPT: string;
|
|
3
|
+
export declare function connectClaudeCode(options: {
|
|
4
|
+
mcpUrl: string;
|
|
5
|
+
launch: boolean;
|
|
6
|
+
}): void;
|
|
7
|
+
export declare function connectCodex(options: {
|
|
8
|
+
mcpUrl: string;
|
|
9
|
+
launch: boolean;
|
|
10
|
+
}): void;
|
|
11
|
+
export declare function registerConnect(program: Command): void;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import spawn from "cross-spawn";
|
|
2
|
+
import { errLine } from "../lib/output.js";
|
|
3
|
+
const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
|
|
4
|
+
const NON_INTERACTIVE_TIMEOUT_MS = 10_000;
|
|
5
|
+
const MCP_LOGIN_UNAVAILABLE_MESSAGE = "This Claude Code installation does not expose `claude mcp login`, which is required for " +
|
|
6
|
+
"one-command MCP authentication. Update Claude Code and retry, or authenticate TinyFish " +
|
|
7
|
+
"manually from /mcp.";
|
|
8
|
+
const CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE = "This Codex installation does not support one-command MCP authentication. Update Codex and " +
|
|
9
|
+
"retry, or add TinyFish manually with `codex mcp add`.";
|
|
10
|
+
export const DEFAULT_ONBOARDING_PROMPT = "I just connected TinyFish. Call the guide_next_step tool now to begin. Then follow its " +
|
|
11
|
+
"instructions one step at a time. Ask for my input and wait for my reply before each subsequent " +
|
|
12
|
+
"TinyFish tool call.";
|
|
13
|
+
const CLAUDE_CODE = {
|
|
14
|
+
command: "claude",
|
|
15
|
+
displayName: "Claude Code",
|
|
16
|
+
supportCheck: {
|
|
17
|
+
args: ["mcp", "--help"],
|
|
18
|
+
pattern: /^\s*login(?:\s|\[)/m,
|
|
19
|
+
unavailableMessage: MCP_LOGIN_UNAVAILABLE_MESSAGE,
|
|
20
|
+
},
|
|
21
|
+
loginArgs: ["mcp", "login", "tinyfish"],
|
|
22
|
+
// Project scope is shared in .mcp.json; a user setup command must not rewrite it.
|
|
23
|
+
removals: [
|
|
24
|
+
{ args: ["mcp", "remove", "tinyfish", "--scope", "user"], label: "user TinyFish registration" },
|
|
25
|
+
{
|
|
26
|
+
args: ["mcp", "remove", "tinyfish", "--scope", "local"],
|
|
27
|
+
label: "local TinyFish registration",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
addArgs: (mcpUrl) => ["mcp", "add", "--scope", "user", "--transport", "http", "tinyfish", mcpUrl],
|
|
31
|
+
};
|
|
32
|
+
const CODEX = {
|
|
33
|
+
command: "codex",
|
|
34
|
+
displayName: "Codex",
|
|
35
|
+
supportCheck: {
|
|
36
|
+
args: ["mcp", "add", "--help"],
|
|
37
|
+
pattern: /--oauth-resource(?:\s|<)/,
|
|
38
|
+
unavailableMessage: CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE,
|
|
39
|
+
},
|
|
40
|
+
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Codex" }],
|
|
41
|
+
addArgs: (mcpUrl) => [
|
|
42
|
+
"mcp",
|
|
43
|
+
"add",
|
|
44
|
+
"tinyfish",
|
|
45
|
+
"--url",
|
|
46
|
+
mcpUrl,
|
|
47
|
+
"--oauth-resource",
|
|
48
|
+
mcpUrl,
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
function requireNativeMcpSupport(client) {
|
|
52
|
+
const result = spawn.sync(client.command, client.supportCheck.args, {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
timeout: NON_INTERACTIVE_TIMEOUT_MS,
|
|
55
|
+
});
|
|
56
|
+
if (result.error?.code === "ENOENT") {
|
|
57
|
+
throw new Error(`${client.displayName} is not installed or not available on PATH.`, {
|
|
58
|
+
cause: result.error,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (result.error || result.status !== 0) {
|
|
62
|
+
throw new Error(client.supportCheck.unavailableMessage, { cause: result.error });
|
|
63
|
+
}
|
|
64
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
65
|
+
if (!client.supportCheck.pattern.test(output)) {
|
|
66
|
+
throw new Error(client.supportCheck.unavailableMessage);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function removeExistingRegistration(client, removal) {
|
|
70
|
+
const result = spawn.sync(client.command, removal.args, {
|
|
71
|
+
encoding: "utf8",
|
|
72
|
+
timeout: NON_INTERACTIVE_TIMEOUT_MS,
|
|
73
|
+
});
|
|
74
|
+
if (result.status === 0)
|
|
75
|
+
return;
|
|
76
|
+
const details = result.stderr?.trim() || result.error?.message || "unknown error";
|
|
77
|
+
if (/No MCP server named "tinyfish"/i.test(details))
|
|
78
|
+
return;
|
|
79
|
+
if (result.error) {
|
|
80
|
+
throw new Error(`Could not remove existing ${removal.label}: ${details}`, {
|
|
81
|
+
cause: result.error,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Could not remove existing ${removal.label}: ${details}`);
|
|
85
|
+
}
|
|
86
|
+
function connectNativeMcpClient(client, options) {
|
|
87
|
+
requireNativeMcpSupport(client);
|
|
88
|
+
for (const removal of client.removals)
|
|
89
|
+
removeExistingRegistration(client, removal);
|
|
90
|
+
errLine(`Adding TinyFish to ${client.displayName}...`);
|
|
91
|
+
const addResult = spawn.sync(client.command, client.addArgs(options.mcpUrl), {
|
|
92
|
+
stdio: "inherit",
|
|
93
|
+
});
|
|
94
|
+
if (addResult.error || addResult.status !== 0) {
|
|
95
|
+
throw new Error(`Could not add TinyFish to ${client.displayName}`, {
|
|
96
|
+
cause: addResult.error,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (client.loginArgs) {
|
|
100
|
+
errLine("Signing in to TinyFish...");
|
|
101
|
+
const loginResult = spawn.sync(client.command, client.loginArgs, {
|
|
102
|
+
stdio: "inherit",
|
|
103
|
+
});
|
|
104
|
+
if (loginResult.error || loginResult.status !== 0) {
|
|
105
|
+
throw new Error(`Could not authenticate TinyFish in ${client.displayName}`, {
|
|
106
|
+
cause: loginResult.error,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!options.launch) {
|
|
111
|
+
errLine(`TinyFish is connected. Open ${client.displayName} to start using it.`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
|
|
115
|
+
const result = spawn.sync(client.command, [DEFAULT_ONBOARDING_PROMPT], { stdio: "inherit" });
|
|
116
|
+
if (result.error) {
|
|
117
|
+
throw new Error(`Could not launch ${client.displayName}`, { cause: result.error });
|
|
118
|
+
}
|
|
119
|
+
if (result.signal === "SIGINT" || result.signal === "SIGTERM")
|
|
120
|
+
return;
|
|
121
|
+
if (result.status !== 0) {
|
|
122
|
+
throw new Error(`${client.displayName} walkthrough exited with status ${result.status ?? "unknown"}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function connectClaudeCode(options) {
|
|
126
|
+
connectNativeMcpClient(CLAUDE_CODE, options);
|
|
127
|
+
}
|
|
128
|
+
export function connectCodex(options) {
|
|
129
|
+
connectNativeMcpClient(CODEX, options);
|
|
130
|
+
}
|
|
131
|
+
export function registerConnect(program) {
|
|
132
|
+
program
|
|
133
|
+
.command("connect")
|
|
134
|
+
.description("Connect TinyFish to an AI agent")
|
|
135
|
+
.argument("<client>", "Agent client to connect (claude-code or codex)")
|
|
136
|
+
.option("--launch", "Launch the agent and start the TinyFish walkthrough")
|
|
137
|
+
.option("--url <mcpUrl>", "MCP endpoint override")
|
|
138
|
+
.action((client, options) => {
|
|
139
|
+
const mcpUrl = options.url ?? DEFAULT_MCP_URL;
|
|
140
|
+
if (mcpUrl.startsWith("-")) {
|
|
141
|
+
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
142
|
+
}
|
|
143
|
+
let parsedUrl;
|
|
144
|
+
try {
|
|
145
|
+
parsedUrl = new URL(mcpUrl);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
149
|
+
}
|
|
150
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
151
|
+
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
152
|
+
}
|
|
153
|
+
const connectOptions = { mcpUrl, launch: options.launch ?? false };
|
|
154
|
+
if (client === "claude-code") {
|
|
155
|
+
connectClaudeCode(connectOptions);
|
|
156
|
+
}
|
|
157
|
+
else if (client === "codex") {
|
|
158
|
+
connectCodex(connectOptions);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
throw new Error(`Unsupported client: ${client}. Supported clients: claude-code, codex`);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
package/dist/commands/fetch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { InvalidArgumentError } from "commander";
|
|
2
2
|
import { getApiKey } from "../lib/auth.js";
|
|
3
3
|
import { fetchContentGet } from "../lib/client.js";
|
|
4
|
-
import { handleApiError, out, outLine } from "../lib/output.js";
|
|
4
|
+
import { err, handleApiError, out, outLine } from "../lib/output.js";
|
|
5
5
|
const MIN_PER_URL_TIMEOUT_MS = 1;
|
|
6
6
|
const MAX_PER_URL_TIMEOUT_MS = 110000;
|
|
7
7
|
function parsePerUrlTimeoutMs(value) {
|
|
@@ -26,6 +26,12 @@ function printPrettyFetch(response) {
|
|
|
26
26
|
outLine(` Title: ${result.title}`);
|
|
27
27
|
if (result.final_url && result.final_url !== result.url)
|
|
28
28
|
outLine(` Final URL: ${result.final_url}`);
|
|
29
|
+
if (result.not_modified)
|
|
30
|
+
outLine(` Not Modified: true`);
|
|
31
|
+
if (result.etag)
|
|
32
|
+
outLine(` ETag: ${result.etag}`);
|
|
33
|
+
if (result.last_modified)
|
|
34
|
+
outLine(` Last-Modified: ${result.last_modified}`);
|
|
29
35
|
outLine("");
|
|
30
36
|
}
|
|
31
37
|
}
|
|
@@ -55,8 +61,18 @@ export function registerFetch(program) {
|
|
|
55
61
|
.option("--links", "Include extracted links")
|
|
56
62
|
.option("--image-links", "Include extracted image links")
|
|
57
63
|
.option("--per-url-timeout-ms <milliseconds>", "Per-URL timeout budget in milliseconds", parsePerUrlTimeoutMs)
|
|
64
|
+
.option("--if-none-match <etag>", "ETag validator for a conditional GET (single URL only)")
|
|
65
|
+
.option("--if-modified-since <http-date>", "Last-Modified validator for a conditional GET (single URL only)")
|
|
66
|
+
.option("--include-etag-and-last-modified", "Include etag / last_modified validators on each result")
|
|
58
67
|
.option("--pretty", "Human-readable output")
|
|
59
68
|
.action(async (urls, opts) => {
|
|
69
|
+
if ((opts.ifNoneMatch !== undefined || opts.ifModifiedSince !== undefined) &&
|
|
70
|
+
urls.length > 1) {
|
|
71
|
+
err({
|
|
72
|
+
error: "--if-none-match and --if-modified-since can only be used with a single URL, not a batch",
|
|
73
|
+
});
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
60
76
|
const apiKey = getApiKey();
|
|
61
77
|
try {
|
|
62
78
|
const response = await fetchContentGet({
|
|
@@ -67,6 +83,13 @@ export function registerFetch(program) {
|
|
|
67
83
|
...(opts.perUrlTimeoutMs !== undefined
|
|
68
84
|
? { per_url_timeout_ms: opts.perUrlTimeoutMs }
|
|
69
85
|
: {}),
|
|
86
|
+
...(opts.ifNoneMatch !== undefined ? { if_none_match: opts.ifNoneMatch } : {}),
|
|
87
|
+
...(opts.ifModifiedSince !== undefined
|
|
88
|
+
? { if_modified_since: opts.ifModifiedSince }
|
|
89
|
+
: {}),
|
|
90
|
+
...(opts.includeEtagAndLastModified !== undefined
|
|
91
|
+
? { include_etag_and_last_modified: opts.includeEtagAndLastModified }
|
|
92
|
+
: {}),
|
|
70
93
|
}, apiKey);
|
|
71
94
|
if (opts.pretty) {
|
|
72
95
|
printPrettyFetch(response);
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { registerRun } from "./commands/run.js";
|
|
|
11
11
|
import { registerRuns } from "./commands/runs.js";
|
|
12
12
|
import { registerConfigureClaude } from "./commands/config-claude.js";
|
|
13
13
|
import { registerSearch } from "./commands/search.js";
|
|
14
|
+
import { registerConnect } from "./commands/connect.js";
|
|
14
15
|
const { version } = createRequire(import.meta.url)("../package.json");
|
|
15
16
|
const program = new Command();
|
|
16
17
|
program
|
|
@@ -27,6 +28,7 @@ program
|
|
|
27
28
|
}
|
|
28
29
|
});
|
|
29
30
|
registerAuth(program);
|
|
31
|
+
registerConnect(program);
|
|
30
32
|
registerConfigureClaude(program);
|
|
31
33
|
registerBrowser(program);
|
|
32
34
|
registerProfile(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiny-fish/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-next.83",
|
|
4
4
|
"description": "TinyFish CLI — run web automations from your terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,14 +32,16 @@
|
|
|
32
32
|
"prepublishOnly": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@tiny-fish/sdk": "^0.0.
|
|
35
|
+
"@tiny-fish/sdk": "^0.0.11",
|
|
36
36
|
"chrome-cookies-secure": "^3.0.2",
|
|
37
37
|
"commander": "^12.0.0",
|
|
38
|
+
"cross-spawn": "^7.0.6",
|
|
38
39
|
"globals": "^17.4.0",
|
|
39
40
|
"zod": "^4.3.6"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@eslint/js": "^10.0.1",
|
|
44
|
+
"@types/cross-spawn": "^6.0.6",
|
|
43
45
|
"@types/node": "^20.0.0",
|
|
44
46
|
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
45
47
|
"@typescript-eslint/parser": "^8.57.2",
|
|
@@ -51,9 +53,9 @@
|
|
|
51
53
|
"overrides": {
|
|
52
54
|
"esbuild": "0.28.1",
|
|
53
55
|
"vite": "7.3.5",
|
|
54
|
-
"brace-expansion": "^5.0.
|
|
56
|
+
"brace-expansion": "^5.0.7",
|
|
55
57
|
"postcss": "8.5.10",
|
|
56
|
-
"tar": "^7"
|
|
58
|
+
"tar": "^7.5.20"
|
|
57
59
|
},
|
|
58
60
|
"engines": {
|
|
59
61
|
"node": ">=24.0.0"
|