@tiny-fish/cli 0.22.1-next.198 → 0.23.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 +18 -0
- package/dist/commands/connect.d.ts +1 -0
- package/dist/commands/connect.js +25 -8
- package/dist/lib/connect-all.js +6 -1
- package/dist/lib/connect-clients.d.ts +5 -1
- package/dist/lib/connect-clients.js +41 -0
- package/dist/lib/connect-install.js +14 -6
- package/dist/lib/connect-runtime.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,6 +44,24 @@ With `--api-key`, Codex reads the key from `TINYFISH_API_KEY` instead of signing
|
|
|
44
44
|
exports it to your shell profile, so restart Codex (or open a new terminal) before using it. Needs
|
|
45
45
|
`codex mcp add --bearer-token-env-var`; without that flag the command falls back to OAuth.
|
|
46
46
|
|
|
47
|
+
### Connect Grok
|
|
48
|
+
|
|
49
|
+
Add TinyFish MCP and start the interactive walkthrough with one command:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx -y @tiny-fish/cli@latest connect grok --launch --api-key sk-tinyfish-...
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With a key, the registration carries `Authorization: Bearer ${TINYFISH_API_KEY}` and no browser
|
|
56
|
+
sign-in is needed — Grok expands the variable at load, so the key itself never reaches argv or
|
|
57
|
+
`config.toml`. The command exports it to your shell profile, so restart Grok (or open a new
|
|
58
|
+
terminal) before using it. `X-API-Key` is not an option: Grok drops it whenever the server
|
|
59
|
+
advertises OAuth metadata. Without a key, Grok has no `mcp login`, so sign in from `/mcps` inside
|
|
60
|
+
Grok: select `tinyfish` and press `i`.
|
|
61
|
+
|
|
62
|
+
Grok's skills come from its own marketplace plugin (`grok plugin install tinyfish --trust`), not
|
|
63
|
+
the `use-tinyfish` skill this command installs for the other harnesses.
|
|
64
|
+
|
|
47
65
|
### Connect Hermes
|
|
48
66
|
|
|
49
67
|
Add TinyFish MCP and the global `use-tinyfish` web skill, authenticate the CLI, and start the
|
|
@@ -11,6 +11,7 @@ interface NativeConnectOptions {
|
|
|
11
11
|
}
|
|
12
12
|
export declare function connectClaudeCode(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
|
|
13
13
|
export declare function connectCodex(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
|
|
14
|
+
export declare function connectGrok(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
|
|
14
15
|
export declare function connectHermes(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
|
|
15
16
|
export declare function connectOpencode(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
|
|
16
17
|
export declare function connectOpenClaw(options: {
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from "../lib/auth.js";
|
|
3
|
-
import { CLAUDE_CODE, CODEX, HERMES, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, OPENCODE, launchNativeMcpClient, launchOpenClawWalkthrough, openExternalUrl, } from "../lib/connect-clients.js";
|
|
3
|
+
import { CLAUDE_CODE, CODEX, GROK, HERMES, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, OPENCODE, launchNativeMcpClient, launchOpenClawWalkthrough, openExternalUrl, } from "../lib/connect-clients.js";
|
|
4
4
|
import { ensureCliAuthenticated, installTinyFishCli, installWebSkill, SKILL_INSTALL_TIMEOUT_MS, UPGRADE_HINT, } from "../lib/connect-install.js";
|
|
5
5
|
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, requireCommandSupport, runGuarded, settle, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "../lib/connect-runtime.js";
|
|
6
6
|
import { cursorInstallDeeplink, cursorMcpPath, writeCursorMcpConfig, } from "../lib/cursor-config.js";
|
|
@@ -21,7 +21,8 @@ function removeExistingRegistration(client, removal) {
|
|
|
21
21
|
return;
|
|
22
22
|
throwIfInterrupted(result);
|
|
23
23
|
const details = result.stderr?.trim() || result.error?.message || "unknown error";
|
|
24
|
-
|
|
24
|
+
// Claude Code double-quotes the name, Grok single-quotes it.
|
|
25
|
+
if (/No MCP server named ['"]tinyfish['"]/i.test(details))
|
|
25
26
|
return;
|
|
26
27
|
throw spawnStepError(`Could not remove existing ${removal.label}: ${details}`, result);
|
|
27
28
|
}
|
|
@@ -135,6 +136,8 @@ async function connectNativeMcpClient(client, options) {
|
|
|
135
136
|
function connectedLine(client, signInDeferred) {
|
|
136
137
|
// The CLI never observes deferred auth, so don't claim "connected".
|
|
137
138
|
if (signInDeferred) {
|
|
139
|
+
if (client.signInHint)
|
|
140
|
+
return client.signInHint;
|
|
138
141
|
return (`TinyFish is configured in ${client.displayName}. ${client.displayName} will ask you ` +
|
|
139
142
|
"to sign in to TinyFish the first time you use it.");
|
|
140
143
|
}
|
|
@@ -147,9 +150,11 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
|
|
|
147
150
|
// Verbose on purpose: first-run setup must not go silent.
|
|
148
151
|
installTinyFishCli({ verbose: true });
|
|
149
152
|
telemetry.track("checkpoint", { phase: "cli_installed" });
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
if (client.skillAgent) {
|
|
154
|
+
state.stage = "skill_install";
|
|
155
|
+
installWebSkill(client);
|
|
156
|
+
telemetry.track("checkpoint", { phase: "skill_installed" });
|
|
157
|
+
}
|
|
153
158
|
state.stage = "authentication";
|
|
154
159
|
ensureCliAuthenticated(client.connectClient, options.apiKey);
|
|
155
160
|
telemetry.track("checkpoint", { phase: "authenticated" });
|
|
@@ -164,6 +169,9 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
|
|
|
164
169
|
return;
|
|
165
170
|
}
|
|
166
171
|
state.stage = "walkthrough_launch";
|
|
172
|
+
// Before the handover, or the only sign-in instructions scroll past under the agent.
|
|
173
|
+
if (signInDeferred && client.signInHint)
|
|
174
|
+
errLine(client.signInHint);
|
|
167
175
|
launchNativeMcpClient(client);
|
|
168
176
|
}
|
|
169
177
|
catch (error) {
|
|
@@ -188,6 +196,9 @@ export async function connectClaudeCode(options) {
|
|
|
188
196
|
export async function connectCodex(options) {
|
|
189
197
|
return connectNativeMcpClient(CODEX, options);
|
|
190
198
|
}
|
|
199
|
+
export async function connectGrok(options) {
|
|
200
|
+
return connectNativeMcpClient(GROK, options);
|
|
201
|
+
}
|
|
191
202
|
export async function connectHermes(options) {
|
|
192
203
|
return connectNativeMcpClient(HERMES, options);
|
|
193
204
|
}
|
|
@@ -321,6 +332,9 @@ export function launchAgent(client) {
|
|
|
321
332
|
else if (client === "opencode") {
|
|
322
333
|
launchNativeMcpClient(OPENCODE);
|
|
323
334
|
}
|
|
335
|
+
else if (client === "grok") {
|
|
336
|
+
launchNativeMcpClient(GROK);
|
|
337
|
+
}
|
|
324
338
|
else if (client === "openclaw") {
|
|
325
339
|
launchOpenClawWalkthrough();
|
|
326
340
|
}
|
|
@@ -337,7 +351,7 @@ export function registerConnect(program) {
|
|
|
337
351
|
program
|
|
338
352
|
.command("connect")
|
|
339
353
|
.description("Connect TinyFish to an AI agent")
|
|
340
|
-
.argument("[client]", "Agent client to connect (claude-code, codex, cursor, hermes, openclaw, or opencode)")
|
|
354
|
+
.argument("[client]", "Agent client to connect (claude-code, codex, cursor, grok, hermes, openclaw, or opencode)")
|
|
341
355
|
.option("--all", "Detect and connect every supported harness found on this machine")
|
|
342
356
|
.option("--dry-run", "Print planned writes per harness without touching anything (--all only)")
|
|
343
357
|
.option("--uninstall", "Remove TinyFish entries written by connect --all")
|
|
@@ -396,7 +410,7 @@ export function registerConnect(program) {
|
|
|
396
410
|
throw new Error("--dry-run, --uninstall and --skip-launch are only supported with --all.");
|
|
397
411
|
}
|
|
398
412
|
if (!client) {
|
|
399
|
-
throw new Error("Pass a client (claude-code, codex, cursor, hermes, openclaw) or --all.");
|
|
413
|
+
throw new Error("Pass a client (claude-code, codex, cursor, grok, hermes, openclaw, opencode) or --all.");
|
|
400
414
|
}
|
|
401
415
|
const connectOptions = {
|
|
402
416
|
apiKey: options.apiKey,
|
|
@@ -412,6 +426,9 @@ export function registerConnect(program) {
|
|
|
412
426
|
else if (client === "codex") {
|
|
413
427
|
await connectCodex(connectOptions);
|
|
414
428
|
}
|
|
429
|
+
else if (client === "grok") {
|
|
430
|
+
await connectGrok(connectOptions);
|
|
431
|
+
}
|
|
415
432
|
else if (client === "hermes") {
|
|
416
433
|
await connectHermes(connectOptions);
|
|
417
434
|
}
|
|
@@ -432,7 +449,7 @@ export function registerConnect(program) {
|
|
|
432
449
|
else {
|
|
433
450
|
throw new Error("Unsupported client: " +
|
|
434
451
|
client +
|
|
435
|
-
". Supported clients: claude-code, codex, cursor, hermes, openclaw, opencode");
|
|
452
|
+
". Supported clients: claude-code, codex, cursor, grok, hermes, openclaw, opencode");
|
|
436
453
|
}
|
|
437
454
|
// Deferred to here so the notice lands after install output, not interleaved with it.
|
|
438
455
|
emitNotice();
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -24,8 +24,13 @@ const AUTH_DEFERRED_AT_INSTALL = new Set(["codex"]);
|
|
|
24
24
|
const AUTH_CHECK_CAPABLE = new Set(["cursor", "openclaw"]);
|
|
25
25
|
// Descriptor-derived so a harness that cannot carry a key is skipped, not failed, when headless.
|
|
26
26
|
// Cursor and OpenClaw hold the key themselves rather than handing it to a harness `mcp add`.
|
|
27
|
+
// `--all` only reaches harnesses it can detect; Grok has a keyAuth but no detection yet.
|
|
27
28
|
const KEY_AUTH_CAPABLE = new Set([
|
|
28
|
-
...NATIVE_MCP_CLIENTS.filter((client) => !!client.keyAuth)
|
|
29
|
+
...NATIVE_MCP_CLIENTS.filter((client) => !!client.keyAuth)
|
|
30
|
+
.map((client) => client.connectClient)
|
|
31
|
+
.filter((client) => ALL_HARNESSES.includes(client))
|
|
32
|
+
// Second map only narrows connectClient to Harness; the filter above is the real gate.
|
|
33
|
+
.map((client) => client),
|
|
29
34
|
"cursor",
|
|
30
35
|
"openclaw",
|
|
31
36
|
]);
|
|
@@ -5,13 +5,16 @@ export declare const DEFAULT_ONBOARDING_PROMPT: string;
|
|
|
5
5
|
export declare const OPENCLAW_ONBOARDING_PROMPT: string;
|
|
6
6
|
export interface NativeMcpClient extends SupportedCommand {
|
|
7
7
|
connectClient: Exclude<AgentClient, "openclaw">;
|
|
8
|
-
|
|
8
|
+
/** Omitted where the `skills` CLI has no agent for this harness (Grok ships its own plugin). */
|
|
9
|
+
skillAgent?: "claude-code" | "codex" | "hermes-agent" | "opencode";
|
|
9
10
|
/** Replaces `addArgs` when a stored key is available: key auth removes the OAuth hop. */
|
|
10
11
|
keyAuth?: {
|
|
11
12
|
/** Key reaches the client through TINYFISH_API_KEY, not argv. */
|
|
12
13
|
viaEnv?: boolean;
|
|
13
14
|
addArgs: (mcpUrl: string, apiKey: string) => string[];
|
|
14
15
|
};
|
|
16
|
+
/** Shown when a keyless install leaves sign-in to the harness. */
|
|
17
|
+
signInHint?: string;
|
|
15
18
|
loginArgs?: string[];
|
|
16
19
|
/** Replaces the `loginArgs` step when `supportCheck.optionalPatterns` miss. */
|
|
17
20
|
degradedAuthNotes?: {
|
|
@@ -37,6 +40,7 @@ export declare const CLAUDE_CODE: NativeMcpClient;
|
|
|
37
40
|
*/
|
|
38
41
|
export declare function openExternalUrl(url: string): ReturnType<typeof spawn.sync>;
|
|
39
42
|
export declare const CODEX: NativeMcpClient;
|
|
43
|
+
export declare const GROK: NativeMcpClient;
|
|
40
44
|
export declare const HERMES: NativeMcpClient;
|
|
41
45
|
export declare const OPENCODE: NativeMcpClient;
|
|
42
46
|
/** Every native MCP client. Derive from this, so a new harness cannot be missed. */
|
|
@@ -23,6 +23,14 @@ const HERMES_OAUTH_UNAVAILABLE_MESSAGE = "Could not confirm this Hermes installa
|
|
|
23
23
|
"mcp add --help` did not list `--auth oauth`. Update Hermes and retry, or add TinyFish " +
|
|
24
24
|
"manually with `hermes mcp add`." +
|
|
25
25
|
SUPPORT_CHECK_DEBUG_HINT;
|
|
26
|
+
const GROK_HTTP_UNAVAILABLE_MESSAGE = "Could not confirm this Grok installation supports remote MCP servers: `grok mcp add --help` " +
|
|
27
|
+
"did not list `--transport`. Update Grok and retry, or add TinyFish manually with " +
|
|
28
|
+
"`grok mcp add`." +
|
|
29
|
+
SUPPORT_CHECK_DEBUG_HINT;
|
|
30
|
+
// Grok has no `mcp login`; sign-in only happens inside the TUI. No key nudge here: a revoked
|
|
31
|
+
// key leaves Grok with no OAuth fallback, and doctor cannot spot that yet (PF-3618).
|
|
32
|
+
const GROK_SIGN_IN_HINT = "TinyFish is registered in Grok. Open Grok, run `/mcps`, select tinyfish, and press `i` to " +
|
|
33
|
+
"sign in.";
|
|
26
34
|
const OPENCLAW_SKILL_UNAVAILABLE_MESSAGE = "Could not confirm this OpenClaw installation supports global skill installation: `openclaw " +
|
|
27
35
|
"skills install --help` did not list `--global` and `--acknowledge-clawhub-risk`. Update " +
|
|
28
36
|
"OpenClaw and retry, or install manually with `openclaw skills install @tinyfish/tinyfish " +
|
|
@@ -150,6 +158,38 @@ export const CODEX = {
|
|
|
150
158
|
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Codex" }],
|
|
151
159
|
addArgs: (mcpUrl, oauthResource) => [...urlAddArgs(mcpUrl), "--oauth-resource", oauthResource],
|
|
152
160
|
};
|
|
161
|
+
const grokAddArgs = (mcpUrl) => [
|
|
162
|
+
"mcp",
|
|
163
|
+
"add",
|
|
164
|
+
"--transport",
|
|
165
|
+
"http",
|
|
166
|
+
"tinyfish",
|
|
167
|
+
mcpUrl,
|
|
168
|
+
];
|
|
169
|
+
export const GROK = {
|
|
170
|
+
command: "grok",
|
|
171
|
+
connectClient: "grok",
|
|
172
|
+
displayName: "Grok",
|
|
173
|
+
supportCheck: {
|
|
174
|
+
args: ["mcp", "add", "--help"],
|
|
175
|
+
patterns: [/--transport(?:[\s<=]|$)/m],
|
|
176
|
+
keyAuthPattern: HEADER_FLAG,
|
|
177
|
+
unavailableMessage: GROK_HTTP_UNAVAILABLE_MESSAGE,
|
|
178
|
+
},
|
|
179
|
+
// X-API-Key is dropped whenever the server advertises OAuth metadata; Bearer is not.
|
|
180
|
+
// Grok expands `${VAR}` in headers at load, so the key stays out of argv and config.toml.
|
|
181
|
+
keyAuth: {
|
|
182
|
+
viaEnv: true,
|
|
183
|
+
addArgs: (mcpUrl) => [
|
|
184
|
+
...grokAddArgs(mcpUrl),
|
|
185
|
+
"--header",
|
|
186
|
+
"Authorization: Bearer ${TINYFISH_API_KEY}",
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
signInHint: GROK_SIGN_IN_HINT,
|
|
190
|
+
removals: [{ args: ["mcp", "remove", "tinyfish"], label: "TinyFish registration from Grok" }],
|
|
191
|
+
addArgs: grokAddArgs,
|
|
192
|
+
};
|
|
153
193
|
function launchHermesWalkthrough() {
|
|
154
194
|
const seedResult = spawn.sync("hermes", ["chat", "-Q", "-q", DEFAULT_ONBOARDING_PROMPT], {
|
|
155
195
|
encoding: "utf8",
|
|
@@ -229,6 +269,7 @@ export const OPENCODE = {
|
|
|
229
269
|
export const NATIVE_MCP_CLIENTS = [
|
|
230
270
|
CLAUDE_CODE,
|
|
231
271
|
CODEX,
|
|
272
|
+
GROK,
|
|
232
273
|
HERMES,
|
|
233
274
|
OPENCODE,
|
|
234
275
|
];
|
|
@@ -81,6 +81,8 @@ const SKILL_UNCHECKABLE_PATTERN = /cannot be checked automatically/;
|
|
|
81
81
|
// `skills add` exits non-zero on a fatal error, but per-skill install failures only print.
|
|
82
82
|
const SKILL_INSTALL_FAILURE_PATTERN = /Failed to install/;
|
|
83
83
|
export function installWebSkill(client) {
|
|
84
|
+
if (!client.skillAgent)
|
|
85
|
+
return;
|
|
84
86
|
errLine(`Installing the TinyFish web skill in ${client.displayName}...`);
|
|
85
87
|
// Piped rather than inherited so the per-skill failure line can be matched; echoed back
|
|
86
88
|
// so the user still sees what `skills` reported.
|
|
@@ -112,11 +114,15 @@ export function updateWebSkill({ verbose }) {
|
|
|
112
114
|
const connected = loadConfig().connect ?? {};
|
|
113
115
|
const skillAgents = NATIVE_MCP_CLIENTS
|
|
114
116
|
.filter((client) => connected[client.connectClient])
|
|
115
|
-
.map((client) => client.skillAgent)
|
|
117
|
+
.map((client) => client.skillAgent)
|
|
118
|
+
.filter((agent) => agent !== undefined);
|
|
116
119
|
const hasOpenClaw = Boolean(connected["openclaw"]);
|
|
117
|
-
// Installs predating PF-3169 recorded nothing; ask `skills` to refresh what it tracks.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
// Installs predating PF-3169 recorded nothing; ask `skills` to refresh what it tracks. A
|
|
121
|
+
// skill-less recorded install (Grok, Cursor) may still have one from before, so refresh anyway
|
|
122
|
+
// — but there having been none is then expected, not an upgrade failure.
|
|
123
|
+
if (skillAgents.length === 0 && !hasOpenClaw) {
|
|
124
|
+
return refreshWebSkillByHash(verbose, Object.keys(connected).length > 0);
|
|
125
|
+
}
|
|
120
126
|
let refreshed = false;
|
|
121
127
|
if (skillAgents.length > 0) {
|
|
122
128
|
reinstallWebSkill(skillAgents, verbose);
|
|
@@ -162,8 +168,8 @@ function reinstallWebSkill(skillAgents, verbose) {
|
|
|
162
168
|
if (failed)
|
|
163
169
|
throw new Error("Could not refresh the TinyFish web skill", { cause: result.error });
|
|
164
170
|
}
|
|
165
|
-
/** Fallback when no harness is recorded:
|
|
166
|
-
function refreshWebSkillByHash(verbose) {
|
|
171
|
+
/** Fallback when no skill-bearing harness is recorded: refresh whatever `skills` tracks. */
|
|
172
|
+
function refreshWebSkillByHash(verbose, untrackedIsExpected = false) {
|
|
167
173
|
if (verbose)
|
|
168
174
|
errLine("Refreshing the TinyFish web skill...");
|
|
169
175
|
const result = spawn.sync("npx", ["-y", SKILLS_CLI_PACKAGE, "update", TINYFISH_WEB_SKILL, "--global", "--yes"], {
|
|
@@ -184,6 +190,8 @@ function refreshWebSkillByHash(verbose) {
|
|
|
184
190
|
throw new Error("Could not refresh the TinyFish web skill", { cause: result.error });
|
|
185
191
|
if (unchecked) {
|
|
186
192
|
errLine("Run `tinyfish connect <client>` to reinstall the skill and restore update tracking.");
|
|
193
|
+
if (untrackedIsExpected)
|
|
194
|
+
return false;
|
|
187
195
|
throw new Error("The TinyFish web skill cannot be checked for updates");
|
|
188
196
|
}
|
|
189
197
|
if (SKILL_NOT_INSTALLED_PATTERN.test(output)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AuthMode } from "./harness-detect.js";
|
|
2
2
|
export declare const NON_INTERACTIVE_TIMEOUT_MS = 10000;
|
|
3
|
-
export type AgentClient = "claude-code" | "codex" | "cursor" | "hermes" | "openclaw" | "opencode";
|
|
3
|
+
export type AgentClient = "claude-code" | "codex" | "cursor" | "grok" | "hermes" | "openclaw" | "opencode";
|
|
4
4
|
type ConnectStage = "started" | "checkpoint" | "completed" | "failed" | "aborted" | "post_install_failed";
|
|
5
5
|
type ConnectFailureStage = "prerequisite_check" | "registration_cleanup" | "registration" | "registration_or_authentication" | "client_oauth" | "cli_install" | "skill_install" | "authentication" | "walkthrough_launch";
|
|
6
6
|
export declare const CONNECT_FAILURE_REASONS: readonly ["harness_not_installed", "harness_command_unsupported", "harness_too_old", "command_not_found", "timeout", "spawn_error", "nonzero_exit", "invalid_config"];
|