@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 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: {
@@ -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
- if (/No MCP server named "tinyfish"/i.test(details))
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
- state.stage = "skill_install";
151
- installWebSkill(client);
152
- telemetry.track("checkpoint", { phase: "skill_installed" });
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();
@@ -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).map((client) => client.connectClient),
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
- skillAgent: "claude-code" | "codex" | "hermes-agent" | "opencode";
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
- if (skillAgents.length === 0 && !hasOpenClaw)
119
- return refreshWebSkillByHash(verbose);
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: ask `skills` to refresh whatever it tracks. */
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"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.22.1-next.198",
3
+ "version": "0.23.0",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {