@tiny-fish/cli 0.35.0 → 0.35.1-next.290

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.
@@ -1,6 +1,7 @@
1
1
  import spawn from 'cross-spawn';
2
2
  import { type Harness } from './harness-detect.js';
3
3
  import { type AgentClient, type SupportedCommand } from './connect-runtime.js';
4
+ import { type HarnessSpec, type SkillAgent } from './harness-spec.js';
4
5
  export declare const OPENCLAW_SKILL_INSTALL_ARGS: readonly string[];
5
6
  /** `openclaw skills` has no uninstall; ClawHub removes from the global dir. */
6
7
  export declare function openclawSkillUninstall(): {
@@ -11,12 +12,11 @@ export declare function openclawSkillUninstall(): {
11
12
  };
12
13
  export declare const DEFAULT_ONBOARDING_PROMPT: string;
13
14
  export declare const OPENCLAW_ONBOARDING_PROMPT: string;
14
- /** The `skills` CLI's own agent names — not ours, and not every harness has one. */
15
- export type SkillAgent = 'claude-code' | 'codex' | 'cursor' | 'hermes-agent' | 'opencode';
15
+ export type { SkillAgent };
16
16
  /** Cursor writes its own MCP config, so it has no descriptor to carry this. */
17
17
  export declare const CURSOR_SKILL_TARGET: {
18
18
  readonly skillAgent: "cursor";
19
- readonly displayName: "Cursor";
19
+ readonly displayName: string;
20
20
  };
21
21
  /** The harness reads the key from its own store. */
22
22
  export interface SeededInstall {
@@ -35,12 +35,8 @@ export interface SeededInstall {
35
35
  tag?: string;
36
36
  };
37
37
  }
38
- interface BaseMcpClient extends SupportedCommand {
38
+ interface BaseMcpClient extends SupportedCommand, Pick<HarnessSpec, 'skillAgent' | 'nonInteractiveAdd' | 'signInHint' | 'degradedAuthNotes' | 'postConnectNote'> {
39
39
  connectClient: Exclude<AgentClient, 'openclaw'>;
40
- /** Omitted where the `skills` CLI has no agent for this harness (Grok ships its own plugin). */
41
- skillAgent?: SkillAgent;
42
- /** Capturable: this `mcp add` never runs OAuth inline; unset stays inherited. */
43
- nonInteractiveAdd?: true;
44
40
  /** Replaces `addArgs` when a stored key is available: key auth removes the OAuth hop. */
45
41
  keyAuth?: {
46
42
  /** Which variable the client reads the key from. */
@@ -49,16 +45,9 @@ interface BaseMcpClient extends SupportedCommand {
49
45
  /** Writes into the harness's store, so connect verifies the key first. */
50
46
  seedKey?: (apiKey: string) => SeededInstall;
51
47
  };
52
- /** Shown when a keyless install leaves sign-in to the harness. */
53
- signInHint?: string;
54
48
  loginArgs?: string[];
55
49
  /** Keyless `mcp add` itself blocks on the browser OAuth. */
56
50
  oauthInAdd?: boolean;
57
- /** Replaces the `loginArgs` step when `supportCheck.optionalPatterns` miss. */
58
- degradedAuthNotes?: {
59
- keyed: string;
60
- deferred: string;
61
- };
62
51
  removals: {
63
52
  args: string[];
64
53
  label: string;
@@ -67,8 +56,6 @@ interface BaseMcpClient extends SupportedCommand {
67
56
  launchWalkthrough?: () => void;
68
57
  /** Own window, never inherits stdio; reports its own outcome. */
69
58
  detachedLaunch?: () => WalkthroughOutcome;
70
- /** Printed after a successful connect, on both the launch and non-launch paths. */
71
- postConnectNote?: string;
72
59
  /** Post-install like the skill step: failures warn, never fail the attempt. */
73
60
  extraPostInstall?: (options: {
74
61
  apiKey?: string;
@@ -95,42 +82,29 @@ export interface KeyRequiredMcpClient extends BaseMcpClient {
95
82
  loginArgs?: never;
96
83
  }
97
84
  export type NativeMcpClient = OauthCapableMcpClient | KeyRequiredMcpClient;
98
- export declare const CLAUDE_CODE: NativeMcpClient;
99
85
  /**
100
86
  * `cmd` re-parses its command line after `/c`, so an unquoted `&` in the query string ends the
101
87
  * command and truncates the URL. Quoting needs windowsVerbatimArguments, or Node escapes the
102
88
  * quotes back out. Untested on Windows.
103
89
  */
104
90
  export declare function openExternalUrl(url: string): ReturnType<typeof spawn.sync>;
105
- export declare const CODEX: NativeMcpClient;
106
- export declare const GROK: NativeMcpClient;
107
91
  /** `mcp add` exits 0 after saving nothing, or a disabled entry. */
108
92
  export declare function hermesRegistrationEnabled(home: string): {
109
93
  ok: boolean;
110
94
  detail?: string;
111
95
  tag?: string;
112
96
  };
113
- export declare const HERMES: KeyRequiredMcpClient;
114
- export declare const OPENCODE: NativeMcpClient;
115
- /** Every native MCP client. Derive from this, so a new harness cannot be missed. */
116
97
  export declare const NATIVE_MCP_CLIENTS: readonly NativeMcpClient[];
117
98
  /** Native descriptor by harness id; Cursor and OpenClaw have none. */
118
99
  export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code", NativeMcpClient>;
100
+ export declare const CLAUDE_CODE: NativeMcpClient;
101
+ export declare const CODEX: NativeMcpClient;
102
+ export declare const GROK: NativeMcpClient;
103
+ export declare const HERMES: NativeMcpClient;
104
+ export declare const OPENCODE: NativeMcpClient;
119
105
  export declare const OPENCLAW: SupportedCommand;
120
106
  /** Traits with no native descriptor to live on (Cursor and OpenClaw included). */
121
- export interface HarnessCapabilities {
122
- /** Sign-in is the CLI's own interactive `tinyfish auth login`. */
123
- signInViaCliLogin: boolean;
124
- /** Stored-key auth verification works against this harness's config. */
125
- canVerifyAuth: boolean;
126
- /** The CLI holds or writes the key; no harness `mcp add` carries it. */
127
- keyHeldByCli: boolean;
128
- /** Connect writes the MCP config file; no harness binary is spawned. */
129
- cliWritesConfig: boolean;
130
- /** `mcp add` succeeds unauthenticated; OAuth lands at first tool use. */
131
- authDeferredAtInstall: boolean;
132
- }
133
- /** Required booleans: a new harness or new flag fails to compile undecided. */
107
+ export type HarnessCapabilities = Pick<HarnessSpec, 'signInViaCliLogin' | 'canVerifyAuth' | 'keyHeldByCli' | 'cliWritesConfig' | 'authDeferredAtInstall'>;
134
108
  export declare const HARNESS_CAPABILITIES: Record<Harness, HarnessCapabilities>;
135
109
  /** "printed" = handed to the user to paste; the walkthrough was never started for them. */
136
110
  export type WalkthroughOutcome = 'launched' | 'printed';
@@ -138,4 +112,3 @@ export type WalkthroughOutcome = 'launched' | 'printed';
138
112
  type OnWalkthroughDecided = (outcome: WalkthroughOutcome) => void | Promise<void>;
139
113
  export declare function launchNativeMcpClient(client: NativeMcpClient, onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
140
114
  export declare function launchOpenClawWalkthrough(onDecided?: OnWalkthroughDecided): Promise<WalkthroughOutcome>;
141
- export {};
@@ -5,57 +5,11 @@ import { harnessConfigPath } from './harness-detect.js';
5
5
  import { errLine, sanitizeLine, warnLine } from './output.js';
6
6
  import { ConnectStepError, spawnStepError, } from './connect-runtime.js';
7
7
  import { TINYFISH_API_KEY_VAR } from './constants.js';
8
+ import { ALL_HARNESSES, HARNESS_SPECS, NATIVE_HARNESSES, harnessSpec, } from './harness-spec.js';
8
9
  import { HERMES_KEY_VAR, captureHermesKeyRestore, hermesEnvPath, resolveHermesHome, writeHermesKey, } from './hermes-env.js';
9
10
  import { hermesConfigPath, readHermesEntry } from './hermes-config.js';
10
- import { HERMES_PLUGIN_NAME, installHermesPlugin, setHermesWebBackends } from './hermes-plugin.js';
11
+ import { installHermesPlugin, setHermesWebBackends } from './hermes-plugin.js';
11
12
  const HERMES_SEED_TIMEOUT_MS = 120_000;
12
- // An old install is only one reason a flag can go unseen, so no message asserts the cause.
13
- const SUPPORT_CHECK_DEBUG_HINT = ' Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.';
14
- const MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this Claude Code installation exposes `claude mcp add`: `claude mcp ' +
15
- '--help` did not list it. Update Claude Code with `claude update` and retry.' +
16
- SUPPORT_CHECK_DEBUG_HINT;
17
- // `claude mcp login` only exists upstream from Claude Code 2.1.186; older installs still register.
18
- const CLAUDE_CODE_KEYED_FALLBACK_NOTE = 'Using your stored TinyFish API key: this Claude Code predates one-command sign-in ' +
19
- '(`claude mcp login`), so no browser sign-in is needed.';
20
- const CLAUDE_CODE_DEFERRED_AUTH_NOTE = 'TinyFish is registered in Claude Code but not signed in: this Claude Code predates ' +
21
- 'one-command sign-in (`claude mcp login`). Open Claude Code and run `/mcp` to sign in to ' +
22
- 'TinyFish. `claude update` gets you one-command sign-in. Already have a TinyFish API key? ' +
23
- 'Run `tinyfish connect claude-code --api-key <key>` instead.';
24
- const CODEX_MCP_URL_UNAVAILABLE_MESSAGE = 'Could not confirm this Codex installation supports remote MCP servers: `codex mcp add ' +
25
- '--help` did not list `--url`. Update Codex and retry, or add TinyFish manually with ' +
26
- '`codex mcp add`.' +
27
- SUPPORT_CHECK_DEBUG_HINT;
28
- const CODEX_KEYED_FALLBACK_NOTE = 'Using your stored TinyFish API key: this Codex cannot report whether an OAuth sign-in ' +
29
- 'finished, so no browser sign-in is attempted.';
30
- const CODEX_DEFERRED_AUTH_NOTE = 'TinyFish is registered in Codex but not signed in: this Codex predates reliable sign-in ' +
31
- 'reporting. Run `codex mcp login tinyfish` to sign in, or update Codex with ' +
32
- '`npm install -g @openai/codex@latest` for one-command sign-in.';
33
- const HERMES_OAUTH_UNAVAILABLE_MESSAGE = 'Could not confirm this Hermes installation supports one-command MCP authentication: `hermes ' +
34
- 'mcp add --help` did not list `--auth oauth`. Update Hermes and retry, or add TinyFish ' +
35
- 'manually with `hermes mcp add`.' +
36
- SUPPORT_CHECK_DEBUG_HINT;
37
- const GROK_HTTP_UNAVAILABLE_MESSAGE = 'Could not confirm this Grok installation supports remote MCP servers: `grok mcp add --help` ' +
38
- 'did not list `--transport`. Update Grok and retry, or add TinyFish manually with ' +
39
- '`grok mcp add`.' +
40
- SUPPORT_CHECK_DEBUG_HINT;
41
- // Grok has no `mcp login`; sign-in only happens inside the TUI. No key nudge here: a revoked
42
- // key leaves Grok with no OAuth fallback, and doctor cannot spot that yet (PF-3618).
43
- const GROK_SIGN_IN_HINT = 'TinyFish is registered in Grok. Open Grok, run `/mcps`, select tinyfish, and press `i` to ' +
44
- 'sign in.';
45
- const OPENCLAW_SKILL_UNAVAILABLE_MESSAGE = 'Could not confirm this OpenClaw installation supports global skill installation: `openclaw ' +
46
- 'skills install --help` did not list `--global` and `--acknowledge-clawhub-risk`. Update ' +
47
- 'OpenClaw and retry, or install manually with `openclaw skills install @tinyfish/tinyfish ' +
48
- '--global --acknowledge-clawhub-risk`.' +
49
- SUPPORT_CHECK_DEBUG_HINT;
50
- const OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this OpenCode installation supports one-command MCP setup: `opencode mcp ' +
51
- 'add --help` did not list `--url`. Update OpenCode and retry, or add TinyFish manually with ' +
52
- '`opencode mcp add`.' +
53
- SUPPORT_CHECK_DEBUG_HINT;
54
- // OpenCode is model-agnostic (unlike Claude Code / Codex), so a fresh install can default to a
55
- // model with no tool support — TinyFish runs entirely on tool calls, so it would fail there.
56
- const OPENCODE_MODEL_NOTE = 'Note: TinyFish runs on tool calls, so OpenCode needs a model that supports tool use. Image-only ' +
57
- 'models (e.g. Nano Banana Pro) will show "No endpoints found that support tool use" — switch ' +
58
- "OpenCode's model if the walkthrough can't start.";
59
13
  const OPENCLAW_SKILL = '@tinyfish/tinyfish';
60
14
  // --force makes this an unconditional overwrite, so the same call installs and refreshes.
61
15
  export const OPENCLAW_SKILL_INSTALL_ARGS = [
@@ -95,53 +49,9 @@ export const OPENCLAW_ONBOARDING_PROMPT = 'I just installed the TinyFish skill.
95
49
  'which result I want to read, and wait before using TinyFish Fetch. Then ask what browser task ' +
96
50
  'I want to complete and wait before using TinyFish Agent. Never skip ahead or choose for me.';
97
51
  /** Cursor writes its own MCP config, so it has no descriptor to carry this. */
98
- export const CURSOR_SKILL_TARGET = { skillAgent: 'cursor', displayName: 'Cursor' };
99
- const HEADER_FLAG = /(?:^|\s)--header(?:[\s<=]|$)/m;
100
- const claudeAddArgs = (mcpUrl) => [
101
- 'mcp',
102
- 'add',
103
- '--scope',
104
- 'user',
105
- '--transport',
106
- 'http',
107
- 'tinyfish',
108
- mcpUrl,
109
- ];
110
- const urlAddArgs = (mcpUrl) => ['mcp', 'add', 'tinyfish', '--url', mcpUrl];
111
- // OpenCode takes `--header NAME=VALUE`, not Claude Code's `NAME: VALUE`.
112
- const headerKeyAuth = {
113
- addArgs: (mcpUrl, apiKey) => [...urlAddArgs(mcpUrl), '--header', `X-API-Key=${apiKey}`],
114
- };
115
- export const CLAUDE_CODE = {
116
- command: 'claude',
117
- connectClient: 'claude-code',
118
- skillAgent: 'claude-code',
119
- nonInteractiveAdd: true,
120
- displayName: 'Claude Code',
121
- supportCheck: {
122
- args: ['mcp', '--help'],
123
- patterns: [/^\s*add(?:\s|\[)/m],
124
- optionalPatterns: [/^\s*login(?:\s|\[)/m],
125
- keyAuthPattern: HEADER_FLAG,
126
- unavailableMessage: MCP_ADD_UNAVAILABLE_MESSAGE,
127
- },
128
- keyAuth: {
129
- addArgs: (mcpUrl, apiKey) => [...claudeAddArgs(mcpUrl), '--header', `X-API-Key: ${apiKey}`],
130
- },
131
- loginArgs: ['mcp', 'login', 'tinyfish'],
132
- degradedAuthNotes: {
133
- keyed: CLAUDE_CODE_KEYED_FALLBACK_NOTE,
134
- deferred: CLAUDE_CODE_DEFERRED_AUTH_NOTE,
135
- },
136
- // Project scope is shared in .mcp.json; a user setup command must not rewrite it.
137
- removals: [
138
- { args: ['mcp', 'remove', 'tinyfish', '--scope', 'user'], label: 'user TinyFish registration' },
139
- {
140
- args: ['mcp', 'remove', 'tinyfish', '--scope', 'local'],
141
- label: 'local TinyFish registration',
142
- },
143
- ],
144
- addArgs: (mcpUrl) => claudeAddArgs(mcpUrl),
52
+ export const CURSOR_SKILL_TARGET = {
53
+ skillAgent: HARNESS_SPECS.cursor.skillAgent,
54
+ displayName: HARNESS_SPECS.cursor.displayName,
145
55
  };
146
56
  /**
147
57
  * `cmd` re-parses its command line after `/c`, so an unquoted `&` in the query string ends the
@@ -172,69 +82,6 @@ function launchCodexWalkthrough() {
172
82
  errLine('Codex opened with the TinyFish walkthrough ready. Send the prompt to start.');
173
83
  return 'launched';
174
84
  }
175
- export const CODEX = {
176
- command: 'codex',
177
- connectClient: 'codex',
178
- skillAgent: 'codex',
179
- displayName: 'Codex',
180
- supportCheck: {
181
- args: ['mcp', 'add', '--help'],
182
- patterns: [/--url(?:[\s<=]|$)/m],
183
- // Codex before ~0.142 reports `o_auth` for a server it never signed in to, so the
184
- // sign-in probe lies; `--oauth-resource` is the only marker separating the two.
185
- optionalPatterns: [/--oauth-resource(?:[\s<=]|$)/m],
186
- keyAuthPattern: /(?:^|\s)--bearer-token-env-var(?:[\s<=]|$)/m,
187
- unavailableMessage: CODEX_MCP_URL_UNAVAILABLE_MESSAGE,
188
- },
189
- degradedAuthNotes: {
190
- keyed: CODEX_KEYED_FALLBACK_NOTE,
191
- deferred: CODEX_DEFERRED_AUTH_NOTE,
192
- },
193
- // Codex has no `--header`; the MCP server takes a key as `Bearer sk-…`.
194
- keyAuth: {
195
- envVar: TINYFISH_API_KEY_VAR,
196
- addArgs: (mcpUrl) => [...urlAddArgs(mcpUrl), '--bearer-token-env-var', TINYFISH_API_KEY_VAR],
197
- },
198
- detachedLaunch: launchCodexWalkthrough,
199
- loginArgs: ['mcp', 'login', 'tinyfish'],
200
- removals: [{ args: ['mcp', 'remove', 'tinyfish'], label: 'TinyFish registration from Codex' }],
201
- // `--oauth-resource` duplicates the resource Codex discovers; Clerk rejects both.
202
- // `mcp add` still runs OAuth inline, so `loginArgs` stays a retry.
203
- addArgs: urlAddArgs,
204
- };
205
- const grokAddArgs = (mcpUrl) => [
206
- 'mcp',
207
- 'add',
208
- '--transport',
209
- 'http',
210
- 'tinyfish',
211
- mcpUrl,
212
- ];
213
- export const GROK = {
214
- command: 'grok',
215
- connectClient: 'grok',
216
- nonInteractiveAdd: true,
217
- displayName: 'Grok',
218
- supportCheck: {
219
- args: ['mcp', 'add', '--help'],
220
- patterns: [/--transport(?:[\s<=]|$)/m],
221
- keyAuthPattern: HEADER_FLAG,
222
- unavailableMessage: GROK_HTTP_UNAVAILABLE_MESSAGE,
223
- },
224
- // X-API-Key is dropped whenever the server advertises OAuth metadata; Bearer is not.
225
- // Grok expands `${VAR}` in headers at load, so the key stays out of argv and config.toml.
226
- keyAuth: {
227
- envVar: TINYFISH_API_KEY_VAR,
228
- addArgs: (mcpUrl) => [
229
- ...grokAddArgs(mcpUrl),
230
- '--header',
231
- `Authorization: Bearer \${${TINYFISH_API_KEY_VAR}}`,
232
- ],
233
- },
234
- signInHint: GROK_SIGN_IN_HINT,
235
- removals: [{ args: ['mcp', 'remove', 'tinyfish'], label: 'TinyFish registration from Grok' }],
236
- addArgs: grokAddArgs,
237
- };
238
85
  // Handover: spawn errors are ours; the exit is the user's.
239
86
  function handOverTerminal(command, args, displayName) {
240
87
  const result = spawn.sync(command, args, { stdio: 'inherit' });
@@ -313,132 +160,129 @@ function installHermesWebPlugin({ apiKey, verbose }) {
313
160
  installHermesPlugin(home, apiKey, { verbose });
314
161
  setHermesWebBackends(home);
315
162
  }
316
- export const HERMES = {
317
- command: 'hermes',
318
- connectClient: 'hermes',
319
- skillAgent: 'hermes-agent',
320
- displayName: 'Hermes',
321
- keyRequired: true,
322
- supportCheck: {
323
- args: ['mcp', 'add', '--help'],
324
- patterns: [/--auth\s+\{[^}]*oauth[^}]*\}/],
325
- keyAuthPattern: /--auth\s+\{[^}]*header[^}]*\}/,
326
- unavailableMessage: HERMES_OAUTH_UNAVAILABLE_MESSAGE,
327
- },
328
- // No envVar: key goes into Hermes' .env, not the shell.
329
- keyAuth: {
330
- addArgs: (mcpUrl) => [...urlAddArgs(mcpUrl), '--auth', 'header'],
331
- seedKey: seedHermesKey,
332
- },
333
- removals: [{ args: ['mcp', 'remove', 'tinyfish'], label: 'TinyFish registration from Hermes' }],
334
- extraPostInstall: installHermesWebPlugin,
335
- uninstallExtra: {
336
- args: ['plugins', 'uninstall', HERMES_PLUGIN_NAME],
337
- label: 'TinyFish web plugin from Hermes',
338
- },
339
- launchWalkthrough: launchHermesWalkthrough,
340
- postConnectNote: 'Restart your Hermes session to pick up TinyFish — Hermes discovers MCP servers at startup.',
341
- };
342
163
  // OpenCode's TUI takes a positional as a project directory, so a bare `opencode "<prompt>"`
343
164
  // would be read as a folder. The `--prompt` flag seeds the interactive TUI with a first message
344
165
  // (opencode.ai/docs/cli), so the onboarding guide fires just like the other agents.
345
166
  function launchOpencode() {
346
167
  handOverTerminal('opencode', ['--prompt', DEFAULT_ONBOARDING_PROMPT], 'OpenCode');
347
168
  }
348
- export const OPENCODE = {
349
- command: 'opencode',
350
- connectClient: 'opencode',
351
- skillAgent: 'opencode',
352
- nonInteractiveAdd: true,
353
- displayName: 'OpenCode',
354
- supportCheck: {
355
- args: ['mcp', 'add', '--help'],
356
- patterns: [/--url(?:\s|$)/m],
357
- keyAuthPattern: HEADER_FLAG,
358
- unavailableMessage: OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE,
169
+ /** "positional": flags before `tinyfish <url>`. "flag": flags after `--url`. */
170
+ function specAddArgs(spec, mcpUrl) {
171
+ const flags = spec.addFlags ?? [];
172
+ return spec.urlStyle === 'positional'
173
+ ? ['mcp', 'add', ...flags, 'tinyfish', mcpUrl]
174
+ : ['mcp', 'add', 'tinyfish', '--url', mcpUrl, ...flags];
175
+ }
176
+ function specKeyAuth(spec) {
177
+ const { header, keyEnvFlag, keyAddFlags } = spec;
178
+ if (keyEnvFlag) {
179
+ return {
180
+ envVar: TINYFISH_API_KEY_VAR,
181
+ addArgs: (mcpUrl) => [...specAddArgs(spec, mcpUrl), keyEnvFlag, TINYFISH_API_KEY_VAR],
182
+ };
183
+ }
184
+ if (keyAddFlags) {
185
+ return { addArgs: (mcpUrl) => [...specAddArgs(spec, mcpUrl), ...keyAddFlags] };
186
+ }
187
+ if (!header)
188
+ return undefined;
189
+ const value = (apiKey) => `${header.name}${header.sep}${header.valuePrefix ?? ''}${apiKey}`;
190
+ if (header.viaEnv) {
191
+ return {
192
+ envVar: TINYFISH_API_KEY_VAR,
193
+ addArgs: (mcpUrl) => [
194
+ ...specAddArgs(spec, mcpUrl),
195
+ '--header',
196
+ value(`\${${TINYFISH_API_KEY_VAR}}`),
197
+ ],
198
+ };
199
+ }
200
+ return {
201
+ addArgs: (mcpUrl, apiKey) => [...specAddArgs(spec, mcpUrl), '--header', value(apiKey)],
202
+ };
203
+ }
204
+ /** Descriptor behaviour the spec cannot hold; every other field is spec data. */
205
+ const HARNESS_OVERRIDES = {
206
+ codex: { detachedLaunch: launchCodexWalkthrough },
207
+ hermes: {
208
+ launchWalkthrough: launchHermesWalkthrough,
209
+ extraPostInstall: installHermesWebPlugin,
210
+ seedKey: seedHermesKey,
359
211
  },
360
- keyAuth: headerKeyAuth,
361
- // `opencode mcp add` upserts by name and there is no `opencode mcp remove`, so no cleanup step.
362
- removals: [],
363
- // OpenCode writes ~/.config/opencode/opencode.json itself; `mcp auth` is the separate OAuth step.
364
- addArgs: (mcpUrl) => urlAddArgs(mcpUrl),
365
- loginArgs: ['mcp', 'auth', 'tinyfish'],
366
- launchWalkthrough: launchOpencode,
367
- postConnectNote: OPENCODE_MODEL_NOTE,
212
+ opencode: { launchWalkthrough: launchOpencode },
368
213
  };
369
- /** Every native MCP client. Derive from this, so a new harness cannot be missed. */
370
- export const NATIVE_MCP_CLIENTS = [
371
- CLAUDE_CODE,
372
- CODEX,
373
- GROK,
374
- HERMES,
375
- OPENCODE,
376
- ];
214
+ function toNativeClient(harness) {
215
+ const spec = harnessSpec(harness);
216
+ if (!spec.supportCheck)
217
+ throw new Error(`${harness} has no supportCheck to probe`);
218
+ const overrides = HARNESS_OVERRIDES[harness] ?? {};
219
+ const keyed = specKeyAuth(spec);
220
+ const keyAuth = keyed && { ...keyed, seedKey: overrides.seedKey };
221
+ const base = {
222
+ command: spec.command,
223
+ connectClient: harness,
224
+ displayName: spec.displayName,
225
+ supportCheck: spec.supportCheck,
226
+ skillAgent: spec.skillAgent,
227
+ nonInteractiveAdd: spec.nonInteractiveAdd,
228
+ keyAuth,
229
+ signInHint: spec.signInHint,
230
+ degradedAuthNotes: spec.degradedAuthNotes,
231
+ removals: spec.removals ?? [
232
+ {
233
+ args: ['mcp', 'remove', 'tinyfish'],
234
+ label: `TinyFish registration from ${spec.displayName}`,
235
+ },
236
+ ],
237
+ launchWalkthrough: overrides.launchWalkthrough,
238
+ detachedLaunch: overrides.detachedLaunch,
239
+ postConnectNote: spec.postConnectNote,
240
+ extraPostInstall: overrides.extraPostInstall,
241
+ uninstallExtra: spec.uninstallExtra,
242
+ };
243
+ if (spec.keyRequired) {
244
+ if (!keyAuth)
245
+ throw new Error(`${harness} takes only a key but has no keyed add`);
246
+ return { ...base, keyRequired: true, keyAuth };
247
+ }
248
+ return {
249
+ ...base,
250
+ loginArgs: spec.loginArgs,
251
+ addArgs: (mcpUrl) => specAddArgs(spec, mcpUrl),
252
+ };
253
+ }
254
+ export const NATIVE_MCP_CLIENTS = NATIVE_HARNESSES.map(toNativeClient);
377
255
  /** Native descriptor by harness id; Cursor and OpenClaw have none. */
378
256
  export const NATIVE_BY_HARNESS = new Map(NATIVE_MCP_CLIENTS.map((client) => [client.connectClient, client]));
257
+ function nativeClient(harness) {
258
+ const client = NATIVE_BY_HARNESS.get(harness);
259
+ if (!client)
260
+ throw new Error(`${harness} is not a native MCP client`);
261
+ return client;
262
+ }
263
+ export const CLAUDE_CODE = nativeClient('claude-code');
264
+ export const CODEX = nativeClient('codex');
265
+ export const GROK = nativeClient('grok');
266
+ export const HERMES = nativeClient('hermes');
267
+ export const OPENCODE = nativeClient('opencode');
379
268
  export const OPENCLAW = {
380
- command: 'openclaw',
381
- displayName: 'OpenClaw',
382
- supportCheck: {
383
- args: ['skills', 'install', '--help'],
384
- patterns: [/--global(?:\s|$)/, /--acknowledge-clawhub-risk(?:\s|$)/],
385
- unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
386
- },
387
- };
388
- /** Required booleans: a new harness or new flag fails to compile undecided. */
389
- export const HARNESS_CAPABILITIES = {
390
- 'claude-code': {
391
- signInViaCliLogin: false,
392
- canVerifyAuth: false,
393
- keyHeldByCli: false,
394
- cliWritesConfig: false,
395
- authDeferredAtInstall: false,
396
- },
397
- codex: {
398
- signInViaCliLogin: false,
399
- canVerifyAuth: false,
400
- keyHeldByCli: false,
401
- cliWritesConfig: false,
402
- // TODO(PF-3580): connect now probes Codex sign-in, so this over-warns a signed-in install.
403
- authDeferredAtInstall: true,
404
- },
405
- cursor: {
406
- signInViaCliLogin: false,
407
- canVerifyAuth: true,
408
- keyHeldByCli: true,
409
- cliWritesConfig: true,
410
- authDeferredAtInstall: false,
411
- },
412
- grok: {
413
- signInViaCliLogin: false,
414
- canVerifyAuth: false,
415
- keyHeldByCli: false,
416
- cliWritesConfig: false,
417
- authDeferredAtInstall: false,
418
- },
419
- hermes: {
420
- signInViaCliLogin: false,
421
- // Stays false: keyed already covers auth; true breaks keyless installs.
422
- canVerifyAuth: false,
423
- keyHeldByCli: true,
424
- cliWritesConfig: false,
425
- authDeferredAtInstall: false,
426
- },
427
- openclaw: {
428
- signInViaCliLogin: true,
429
- canVerifyAuth: true,
430
- keyHeldByCli: true,
431
- cliWritesConfig: false,
432
- authDeferredAtInstall: false,
433
- },
434
- opencode: {
435
- signInViaCliLogin: false,
436
- canVerifyAuth: false,
437
- keyHeldByCli: false,
438
- cliWritesConfig: false,
439
- authDeferredAtInstall: false,
440
- },
269
+ command: HARNESS_SPECS.openclaw.command,
270
+ displayName: HARNESS_SPECS.openclaw.displayName,
271
+ supportCheck: HARNESS_SPECS.openclaw.supportCheck,
441
272
  };
273
+ export const HARNESS_CAPABILITIES = Object.fromEntries(ALL_HARNESSES.map((harness) => {
274
+ const spec = harnessSpec(harness);
275
+ return [
276
+ harness,
277
+ {
278
+ signInViaCliLogin: spec.signInViaCliLogin,
279
+ canVerifyAuth: spec.canVerifyAuth,
280
+ keyHeldByCli: spec.keyHeldByCli,
281
+ cliWritesConfig: spec.cliWritesConfig,
282
+ authDeferredAtInstall: spec.authDeferredAtInstall,
283
+ },
284
+ ];
285
+ }));
442
286
  function printPromptForHandoff(displayName, command, prompt, reason) {
443
287
  // "set up", not "connected": sign-in can still be deferred here.
444
288
  errLine(`TinyFish is set up in ${displayName}.`);
@@ -1,6 +1,7 @@
1
1
  import { AuthMode } from './harness-detect.js';
2
+ import type { Harness, HarnessSupportCheck } from './harness-spec.js';
2
3
  export declare const NON_INTERACTIVE_TIMEOUT_MS = 10000;
3
- export type AgentClient = 'claude-code' | 'codex' | 'cursor' | 'grok' | 'hermes' | 'openclaw' | 'opencode';
4
+ export type AgentClient = Harness;
4
5
  type ConnectStage = 'started' | 'checkpoint' | 'completed' | 'failed' | 'aborted' | 'post_install_failed';
5
6
  export type ConnectFailureStage = 'prerequisite_check' | 'registration_cleanup' | 'registration' | 'registration_or_authentication' | 'client_oauth' | 'cli_install' | 'skill_install' | 'plugin_install' | 'authentication' | 'walkthrough_launch' | 'cli_verification';
6
7
  export declare const CONNECT_FAILURE_REASONS: readonly ["harness_not_installed", "harness_command_unsupported", "harness_too_old", "key_auth_unsupported", "command_not_found", "timeout", "spawn_error", "nonzero_exit", "invalid_config", "user_abandoned", "process_terminated", "unexpected_error"];
@@ -87,16 +88,7 @@ export declare function runGuarded(state: ConnectRunState, telemetry: ConnectTel
87
88
  export interface SupportedCommand {
88
89
  command: string;
89
90
  displayName: string;
90
- supportCheck: {
91
- args: string[];
92
- /** Missing → nothing can work; setup fails. */
93
- patterns: RegExp[];
94
- /** Missing → degrade to key or deferred auth, never refuse. */
95
- optionalPatterns?: RegExp[];
96
- /** Missing → this install cannot take a stored key; fall back to OAuth. */
97
- keyAuthPattern?: RegExp;
98
- unavailableMessage: string;
99
- };
91
+ supportCheck: HarnessSupportCheck;
100
92
  }
101
93
  export declare function requireCommandSupport(client: SupportedCommand): {
102
94
  optionalSupported: boolean;
@@ -12,3 +12,5 @@ export declare const API_URL_OVERRIDE: string | undefined;
12
12
  export declare function getDashboardUrl(source?: string): string;
13
13
  export declare const DASHBOARD_URL: string;
14
14
  export declare const HARNESS_PROBE_TIMEOUT_MS = 15000;
15
+ /** Hermes' own id for the TinyFish web plugin. */
16
+ export declare const HERMES_PLUGIN_NAME = "tinyfish";
@@ -49,3 +49,5 @@ export function getDashboardUrl(source = DEFAULT_DASHBOARD_SOURCE) {
49
49
  }
50
50
  export const DASHBOARD_URL = getDashboardUrl();
51
51
  export const HARNESS_PROBE_TIMEOUT_MS = 15_000;
52
+ /** Hermes' own id for the TinyFish web plugin. */
53
+ export const HERMES_PLUGIN_NAME = 'tinyfish';
@@ -1,7 +1,6 @@
1
- export declare const ALL_HARNESSES: readonly ["claude-code", "codex", "cursor", "grok", "hermes", "openclaw", "opencode"];
2
- export type Harness = (typeof ALL_HARNESSES)[number];
1
+ import { ALL_HARNESSES, type Harness } from './harness-spec.js';
2
+ export { ALL_HARNESSES, type Harness };
3
3
  export declare const HARNESS_DISPLAY_NAMES: Record<Harness, string>;
4
- /** MCP config is read at startup, so a running harness needs this to see TinyFish. */
5
4
  export declare const RELOAD_ACTION: Record<Harness, string>;
6
5
  export declare enum Registered {
7
6
  Yes = "yes",
@@ -16,7 +15,7 @@ export declare enum AuthMode {
16
15
  export declare const HARNESS_COMMANDS: Record<Harness, string>;
17
16
  export declare function commandOnPath(command: string): boolean;
18
17
  export declare function harnessConfigPath(harness: Harness): string;
19
- /** For reason strings that name a location; keeps them in sync with the table above. */
18
+ /** For reason strings that name a location; keeps them in sync with the spec. */
20
19
  export declare function harnessDisplayPath(harness: Harness): string;
21
20
  export interface HarnessDetection {
22
21
  harness: Harness;
@@ -2,35 +2,10 @@ import * as fs from 'fs';
2
2
  import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import which from 'which';
5
- export const ALL_HARNESSES = [
6
- 'claude-code',
7
- 'codex',
8
- 'cursor',
9
- 'grok',
10
- 'hermes',
11
- 'openclaw',
12
- 'opencode',
13
- ];
14
- export const HARNESS_DISPLAY_NAMES = {
15
- 'claude-code': 'Claude Code',
16
- codex: 'Codex',
17
- cursor: 'Cursor',
18
- grok: 'Grok',
19
- hermes: 'Hermes',
20
- openclaw: 'OpenClaw',
21
- opencode: 'OpenCode',
22
- };
23
- /** MCP config is read at startup, so a running harness needs this to see TinyFish. */
24
- export const RELOAD_ACTION = {
25
- 'claude-code': 'restart it',
26
- codex: 'restart it',
27
- // docs.x.ai/build/features/mcp-servers: `r` in /mcps rereads the config.
28
- grok: 'run /mcps and press r',
29
- hermes: 'restart it',
30
- openclaw: 'restart it',
31
- opencode: 'restart it',
32
- cursor: 'reload the window',
33
- };
5
+ import { ALL_HARNESSES, harnessSpec } from './harness-spec.js';
6
+ export { ALL_HARNESSES };
7
+ export const HARNESS_DISPLAY_NAMES = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).displayName]));
8
+ export const RELOAD_ACTION = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).reloadAction]));
34
9
  // Values are the wire contract; the report schema derives from these.
35
10
  export var Registered;
36
11
  (function (Registered) {
@@ -44,26 +19,9 @@ export var AuthMode;
44
19
  AuthMode["OAuth"] = "oauth";
45
20
  AuthMode["Unknown"] = "unknown";
46
21
  })(AuthMode || (AuthMode = {}));
47
- const CONFIG_DIRS = {
48
- 'claude-code': '.claude',
49
- codex: '.codex',
50
- cursor: '.cursor',
51
- grok: '.grok',
52
- hermes: '.hermes',
53
- openclaw: '.openclaw',
54
- opencode: '.config/opencode',
55
- };
56
22
  // Harness id is not the binary name (cursor-agent).
57
23
  // TODO(PF-3668): a same-named non-harness binary still reads as installed.
58
- export const HARNESS_COMMANDS = {
59
- 'claude-code': 'claude',
60
- codex: 'codex',
61
- cursor: 'cursor-agent',
62
- grok: 'grok',
63
- hermes: 'hermes',
64
- openclaw: 'openclaw',
65
- opencode: 'opencode',
66
- };
24
+ export const HARNESS_COMMANDS = Object.fromEntries(ALL_HARNESSES.map((harness) => [harness, harnessSpec(harness).command]));
67
25
  // cross-spawn resolves the same way, so detection cannot disagree with the spawn.
68
26
  export function commandOnPath(command) {
69
27
  try {
@@ -74,11 +32,11 @@ export function commandOnPath(command) {
74
32
  }
75
33
  }
76
34
  export function harnessConfigPath(harness) {
77
- return path.join(os.homedir(), CONFIG_DIRS[harness]); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
35
+ return path.join(os.homedir(), harnessSpec(harness).configDir); // nosemgrep: path-join-resolve-traversal -- fixed dir names under os.homedir()
78
36
  }
79
- /** For reason strings that name a location; keeps them in sync with the table above. */
37
+ /** For reason strings that name a location; keeps them in sync with the spec. */
80
38
  export function harnessDisplayPath(harness) {
81
- return `~/${CONFIG_DIRS[harness]}`;
39
+ return `~/${harnessSpec(harness).configDir}`;
82
40
  }
83
41
  export function detectInstalledHarnesses() {
84
42
  return ALL_HARNESSES.map((harness) => {
@@ -0,0 +1,251 @@
1
+ /** The `skills` CLI's own agent names, not ours. */
2
+ export type SkillAgent = 'claude-code' | 'codex' | 'cursor' | 'hermes-agent' | 'opencode';
3
+ export interface HarnessSupportCheck {
4
+ args: string[];
5
+ /** Missing → nothing can work; setup fails. */
6
+ patterns: RegExp[];
7
+ /** Missing → degrade to key or deferred auth, never refuse. */
8
+ optionalPatterns?: RegExp[];
9
+ /** Missing → this install cannot take a stored key; fall back to OAuth. */
10
+ keyAuthPattern?: RegExp;
11
+ unavailableMessage: string;
12
+ }
13
+ /** One entry per harness: every per-harness fact, and no behaviour. */
14
+ export interface HarnessSpec {
15
+ command: string;
16
+ displayName: string;
17
+ /** Home-relative; existing dir means the harness ran here once. */
18
+ configDir: string;
19
+ /** MCP config is read at startup; a running harness needs this to see TinyFish. */
20
+ reloadAction: string;
21
+ skillAgent?: SkillAgent;
22
+ /** Absent for Cursor: its connect writes mcp.json, no binary probe. */
23
+ supportCheck?: HarnessSupportCheck;
24
+ /** Capturable: this `mcp add` never runs OAuth inline; unset stays inherited. */
25
+ nonInteractiveAdd?: true;
26
+ /** Present only on native MCP harnesses; absent means connect is an override. */
27
+ urlStyle?: 'flag' | 'positional';
28
+ /** "positional": before `tinyfish <url>`. "flag": after `--url <url>`. */
29
+ addFlags?: string[];
30
+ /** Keyed adds append `--header` built from these parts. */
31
+ header?: {
32
+ name: string;
33
+ sep: ': ' | '=';
34
+ valuePrefix?: string;
35
+ viaEnv?: boolean;
36
+ };
37
+ /** Keyed adds append this flag plus the env var name (Codex). */
38
+ keyEnvFlag?: string;
39
+ /** Keyed adds append these flags; the key itself is seeded elsewhere (Hermes). */
40
+ keyAddFlags?: string[];
41
+ /** No sign-in exists here, so connect refuses a keyless install (Hermes). */
42
+ keyRequired?: true;
43
+ loginArgs?: string[];
44
+ /** Default: one `mcp remove tinyfish` labeled from displayName. */
45
+ removals?: {
46
+ args: string[];
47
+ label: string;
48
+ }[];
49
+ /** Extra --uninstall row beyond `removals`; never runs pre-add. */
50
+ uninstallExtra?: {
51
+ args: string[];
52
+ label: string;
53
+ };
54
+ /** Shown when a keyless install leaves sign-in to the harness. */
55
+ signInHint?: string;
56
+ /** Replaces the `loginArgs` step when `supportCheck.optionalPatterns` miss. */
57
+ degradedAuthNotes?: {
58
+ keyed: string;
59
+ deferred: string;
60
+ };
61
+ /** Printed after a successful connect, on both the launch and non-launch paths. */
62
+ postConnectNote?: string;
63
+ /** Sign-in is the CLI's own interactive `tinyfish auth login`. */
64
+ signInViaCliLogin: boolean;
65
+ /** Stored-key auth verification works against this harness's config. */
66
+ canVerifyAuth: boolean;
67
+ /** The CLI holds or writes the key; no harness `mcp add` carries it. */
68
+ keyHeldByCli: boolean;
69
+ /** Connect writes the MCP config file; no harness binary is spawned. */
70
+ cliWritesConfig: boolean;
71
+ /** `mcp add` succeeds unauthenticated; OAuth lands at first tool use. */
72
+ authDeferredAtInstall: boolean;
73
+ }
74
+ /** One entry per harness; every per-harness list derives from it. */
75
+ export declare const HARNESS_SPECS: {
76
+ 'claude-code': {
77
+ command: string;
78
+ displayName: string;
79
+ configDir: string;
80
+ reloadAction: string;
81
+ skillAgent: "claude-code";
82
+ nonInteractiveAdd: true;
83
+ supportCheck: {
84
+ args: string[];
85
+ patterns: RegExp[];
86
+ optionalPatterns: RegExp[];
87
+ keyAuthPattern: RegExp;
88
+ unavailableMessage: string;
89
+ };
90
+ urlStyle: "positional";
91
+ addFlags: string[];
92
+ header: {
93
+ name: string;
94
+ sep: ": ";
95
+ };
96
+ loginArgs: string[];
97
+ degradedAuthNotes: {
98
+ keyed: string;
99
+ deferred: string;
100
+ };
101
+ removals: {
102
+ args: string[];
103
+ label: string;
104
+ }[];
105
+ signInViaCliLogin: false;
106
+ canVerifyAuth: false;
107
+ keyHeldByCli: false;
108
+ cliWritesConfig: false;
109
+ authDeferredAtInstall: false;
110
+ };
111
+ codex: {
112
+ command: string;
113
+ displayName: string;
114
+ configDir: string;
115
+ reloadAction: string;
116
+ skillAgent: "codex";
117
+ supportCheck: {
118
+ args: string[];
119
+ patterns: RegExp[];
120
+ optionalPatterns: RegExp[];
121
+ keyAuthPattern: RegExp;
122
+ unavailableMessage: string;
123
+ };
124
+ urlStyle: "flag";
125
+ keyEnvFlag: string;
126
+ degradedAuthNotes: {
127
+ keyed: string;
128
+ deferred: string;
129
+ };
130
+ loginArgs: string[];
131
+ signInViaCliLogin: false;
132
+ canVerifyAuth: false;
133
+ keyHeldByCli: false;
134
+ cliWritesConfig: false;
135
+ authDeferredAtInstall: true;
136
+ };
137
+ cursor: {
138
+ command: string;
139
+ displayName: string;
140
+ configDir: string;
141
+ reloadAction: string;
142
+ skillAgent: "cursor";
143
+ signInViaCliLogin: false;
144
+ canVerifyAuth: true;
145
+ keyHeldByCli: true;
146
+ cliWritesConfig: true;
147
+ authDeferredAtInstall: false;
148
+ };
149
+ grok: {
150
+ command: string;
151
+ displayName: string;
152
+ configDir: string;
153
+ reloadAction: string;
154
+ nonInteractiveAdd: true;
155
+ supportCheck: {
156
+ args: string[];
157
+ patterns: RegExp[];
158
+ keyAuthPattern: RegExp;
159
+ unavailableMessage: string;
160
+ };
161
+ urlStyle: "positional";
162
+ addFlags: string[];
163
+ header: {
164
+ name: string;
165
+ sep: ": ";
166
+ valuePrefix: string;
167
+ viaEnv: true;
168
+ };
169
+ signInHint: string;
170
+ signInViaCliLogin: false;
171
+ canVerifyAuth: false;
172
+ keyHeldByCli: false;
173
+ cliWritesConfig: false;
174
+ authDeferredAtInstall: false;
175
+ };
176
+ hermes: {
177
+ command: string;
178
+ displayName: string;
179
+ configDir: string;
180
+ reloadAction: string;
181
+ skillAgent: "hermes-agent";
182
+ keyRequired: true;
183
+ supportCheck: {
184
+ args: string[];
185
+ patterns: RegExp[];
186
+ keyAuthPattern: RegExp;
187
+ unavailableMessage: string;
188
+ };
189
+ urlStyle: "flag";
190
+ keyAddFlags: string[];
191
+ uninstallExtra: {
192
+ args: string[];
193
+ label: string;
194
+ };
195
+ postConnectNote: string;
196
+ signInViaCliLogin: false;
197
+ canVerifyAuth: false;
198
+ keyHeldByCli: true;
199
+ cliWritesConfig: false;
200
+ authDeferredAtInstall: false;
201
+ };
202
+ openclaw: {
203
+ command: string;
204
+ displayName: string;
205
+ configDir: string;
206
+ reloadAction: string;
207
+ supportCheck: {
208
+ args: string[];
209
+ patterns: RegExp[];
210
+ unavailableMessage: string;
211
+ };
212
+ signInViaCliLogin: true;
213
+ canVerifyAuth: true;
214
+ keyHeldByCli: true;
215
+ cliWritesConfig: false;
216
+ authDeferredAtInstall: false;
217
+ };
218
+ opencode: {
219
+ command: string;
220
+ displayName: string;
221
+ configDir: string;
222
+ reloadAction: string;
223
+ skillAgent: "opencode";
224
+ nonInteractiveAdd: true;
225
+ supportCheck: {
226
+ args: string[];
227
+ patterns: RegExp[];
228
+ keyAuthPattern: RegExp;
229
+ unavailableMessage: string;
230
+ };
231
+ urlStyle: "flag";
232
+ header: {
233
+ name: string;
234
+ sep: "=";
235
+ };
236
+ loginArgs: string[];
237
+ removals: never[];
238
+ postConnectNote: string;
239
+ signInViaCliLogin: false;
240
+ canVerifyAuth: false;
241
+ keyHeldByCli: false;
242
+ cliWritesConfig: false;
243
+ authDeferredAtInstall: false;
244
+ };
245
+ };
246
+ /** Derived from spec keys; one entry extends every union. */
247
+ export type Harness = keyof typeof HARNESS_SPECS;
248
+ export declare const ALL_HARNESSES: readonly Harness[];
249
+ export declare function harnessSpec(harness: Harness): HarnessSpec;
250
+ /** Native MCP harnesses carry add-generation fields; the rest override connect. */
251
+ export declare const NATIVE_HARNESSES: ("openclaw" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code")[];
@@ -0,0 +1,235 @@
1
+ import { HERMES_PLUGIN_NAME } from './constants.js';
2
+ // An old install is only one reason a flag can go unseen, so no message asserts the cause.
3
+ const SUPPORT_CHECK_DEBUG_HINT = ' Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.';
4
+ const MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this Claude Code installation exposes `claude mcp add`: `claude mcp ' +
5
+ '--help` did not list it. Update Claude Code with `claude update` and retry.' +
6
+ SUPPORT_CHECK_DEBUG_HINT;
7
+ // `claude mcp login` only exists upstream from Claude Code 2.1.186; older installs still register.
8
+ const CLAUDE_CODE_KEYED_FALLBACK_NOTE = 'Using your stored TinyFish API key: this Claude Code predates one-command sign-in ' +
9
+ '(`claude mcp login`), so no browser sign-in is needed.';
10
+ const CLAUDE_CODE_DEFERRED_AUTH_NOTE = 'TinyFish is registered in Claude Code but not signed in: this Claude Code predates ' +
11
+ 'one-command sign-in (`claude mcp login`). Open Claude Code and run `/mcp` to sign in to ' +
12
+ 'TinyFish. `claude update` gets you one-command sign-in. Already have a TinyFish API key? ' +
13
+ 'Run `tinyfish connect claude-code --api-key <key>` instead.';
14
+ const CODEX_MCP_URL_UNAVAILABLE_MESSAGE = 'Could not confirm this Codex installation supports remote MCP servers: `codex mcp add ' +
15
+ '--help` did not list `--url`. Update Codex and retry, or add TinyFish manually with ' +
16
+ '`codex mcp add`.' +
17
+ SUPPORT_CHECK_DEBUG_HINT;
18
+ const CODEX_KEYED_FALLBACK_NOTE = 'Using your stored TinyFish API key: this Codex cannot report whether an OAuth sign-in ' +
19
+ 'finished, so no browser sign-in is attempted.';
20
+ const CODEX_DEFERRED_AUTH_NOTE = 'TinyFish is registered in Codex but not signed in: this Codex predates reliable sign-in ' +
21
+ 'reporting. Run `codex mcp login tinyfish` to sign in, or update Codex with ' +
22
+ '`npm install -g @openai/codex@latest` for one-command sign-in.';
23
+ const HERMES_OAUTH_UNAVAILABLE_MESSAGE = 'Could not confirm this Hermes installation supports one-command MCP authentication: `hermes ' +
24
+ 'mcp add --help` did not list `--auth oauth`. Update Hermes and retry, or add TinyFish ' +
25
+ 'manually with `hermes mcp add`.' +
26
+ SUPPORT_CHECK_DEBUG_HINT;
27
+ const GROK_HTTP_UNAVAILABLE_MESSAGE = 'Could not confirm this Grok installation supports remote MCP servers: `grok mcp add --help` ' +
28
+ 'did not list `--transport`. Update Grok and retry, or add TinyFish manually with ' +
29
+ '`grok mcp add`.' +
30
+ SUPPORT_CHECK_DEBUG_HINT;
31
+ // Grok has no `mcp login`; sign-in only happens inside the TUI. No key nudge here: a revoked
32
+ // key leaves Grok with no OAuth fallback, and doctor cannot spot that yet (PF-3618).
33
+ const GROK_SIGN_IN_HINT = 'TinyFish is registered in Grok. Open Grok, run `/mcps`, select tinyfish, and press `i` to ' +
34
+ 'sign in.';
35
+ const OPENCLAW_SKILL_UNAVAILABLE_MESSAGE = 'Could not confirm this OpenClaw installation supports global skill installation: `openclaw ' +
36
+ 'skills install --help` did not list `--global` and `--acknowledge-clawhub-risk`. Update ' +
37
+ 'OpenClaw and retry, or install manually with `openclaw skills install @tinyfish/tinyfish ' +
38
+ '--global --acknowledge-clawhub-risk`.' +
39
+ SUPPORT_CHECK_DEBUG_HINT;
40
+ const OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE = 'Could not confirm this OpenCode installation supports one-command MCP setup: `opencode mcp ' +
41
+ 'add --help` did not list `--url`. Update OpenCode and retry, or add TinyFish manually with ' +
42
+ '`opencode mcp add`.' +
43
+ SUPPORT_CHECK_DEBUG_HINT;
44
+ // OpenCode is model-agnostic (unlike Claude Code / Codex), so a fresh install can default to a
45
+ // model with no tool support — TinyFish runs entirely on tool calls, so it would fail there.
46
+ const OPENCODE_MODEL_NOTE = 'Note: TinyFish runs on tool calls, so OpenCode needs a model that supports tool use. Image-only ' +
47
+ 'models (e.g. Nano Banana Pro) will show "No endpoints found that support tool use" — switch ' +
48
+ "OpenCode's model if the walkthrough can't start.";
49
+ const HERMES_RESTART_NOTE = 'Restart your Hermes session to pick up TinyFish — Hermes discovers MCP servers at startup.';
50
+ const HEADER_FLAG = /(?:^|\s)--header(?:[\s<=]|$)/m;
51
+ /** One entry per harness; every per-harness list derives from it. */
52
+ export const HARNESS_SPECS = {
53
+ 'claude-code': {
54
+ command: 'claude',
55
+ displayName: 'Claude Code',
56
+ configDir: '.claude',
57
+ reloadAction: 'restart it',
58
+ skillAgent: 'claude-code',
59
+ nonInteractiveAdd: true,
60
+ supportCheck: {
61
+ args: ['mcp', '--help'],
62
+ patterns: [/^\s*add(?:\s|\[)/m],
63
+ optionalPatterns: [/^\s*login(?:\s|\[)/m],
64
+ keyAuthPattern: HEADER_FLAG,
65
+ unavailableMessage: MCP_ADD_UNAVAILABLE_MESSAGE,
66
+ },
67
+ urlStyle: 'positional',
68
+ addFlags: ['--scope', 'user', '--transport', 'http'],
69
+ header: { name: 'X-API-Key', sep: ': ' },
70
+ loginArgs: ['mcp', 'login', 'tinyfish'],
71
+ degradedAuthNotes: {
72
+ keyed: CLAUDE_CODE_KEYED_FALLBACK_NOTE,
73
+ deferred: CLAUDE_CODE_DEFERRED_AUTH_NOTE,
74
+ },
75
+ // Project scope is shared in .mcp.json; a user setup command must not rewrite it.
76
+ removals: [
77
+ {
78
+ args: ['mcp', 'remove', 'tinyfish', '--scope', 'user'],
79
+ label: 'user TinyFish registration',
80
+ },
81
+ {
82
+ args: ['mcp', 'remove', 'tinyfish', '--scope', 'local'],
83
+ label: 'local TinyFish registration',
84
+ },
85
+ ],
86
+ signInViaCliLogin: false,
87
+ canVerifyAuth: false,
88
+ keyHeldByCli: false,
89
+ cliWritesConfig: false,
90
+ authDeferredAtInstall: false,
91
+ },
92
+ codex: {
93
+ command: 'codex',
94
+ displayName: 'Codex',
95
+ configDir: '.codex',
96
+ reloadAction: 'restart it',
97
+ skillAgent: 'codex',
98
+ supportCheck: {
99
+ args: ['mcp', 'add', '--help'],
100
+ patterns: [/--url(?:[\s<=]|$)/m],
101
+ // Codex before ~0.142 reports `o_auth` for a server it never signed in to, so the
102
+ // sign-in probe lies; `--oauth-resource` is the only marker separating the two.
103
+ optionalPatterns: [/--oauth-resource(?:[\s<=]|$)/m],
104
+ keyAuthPattern: /(?:^|\s)--bearer-token-env-var(?:[\s<=]|$)/m,
105
+ unavailableMessage: CODEX_MCP_URL_UNAVAILABLE_MESSAGE,
106
+ },
107
+ urlStyle: 'flag',
108
+ // Codex has no `--header`; the MCP server takes a key as `Bearer sk-…`.
109
+ keyEnvFlag: '--bearer-token-env-var',
110
+ degradedAuthNotes: { keyed: CODEX_KEYED_FALLBACK_NOTE, deferred: CODEX_DEFERRED_AUTH_NOTE },
111
+ // `--oauth-resource` duplicates the resource Codex discovers; Clerk rejects both.
112
+ // `mcp add` still runs OAuth inline, so `loginArgs` stays a retry.
113
+ loginArgs: ['mcp', 'login', 'tinyfish'],
114
+ signInViaCliLogin: false,
115
+ canVerifyAuth: false,
116
+ keyHeldByCli: false,
117
+ cliWritesConfig: false,
118
+ // TODO(PF-3580): connect now probes Codex sign-in, so this over-warns a signed-in install.
119
+ authDeferredAtInstall: true,
120
+ },
121
+ cursor: {
122
+ command: 'cursor-agent',
123
+ displayName: 'Cursor',
124
+ configDir: '.cursor',
125
+ reloadAction: 'reload the window',
126
+ skillAgent: 'cursor',
127
+ signInViaCliLogin: false,
128
+ canVerifyAuth: true,
129
+ keyHeldByCli: true,
130
+ cliWritesConfig: true,
131
+ authDeferredAtInstall: false,
132
+ },
133
+ grok: {
134
+ command: 'grok',
135
+ displayName: 'Grok',
136
+ configDir: '.grok',
137
+ // docs.x.ai/build/features/mcp-servers: `r` in /mcps rereads the config.
138
+ reloadAction: 'run /mcps and press r',
139
+ nonInteractiveAdd: true,
140
+ supportCheck: {
141
+ args: ['mcp', 'add', '--help'],
142
+ patterns: [/--transport(?:[\s<=]|$)/m],
143
+ keyAuthPattern: HEADER_FLAG,
144
+ unavailableMessage: GROK_HTTP_UNAVAILABLE_MESSAGE,
145
+ },
146
+ urlStyle: 'positional',
147
+ addFlags: ['--transport', 'http'],
148
+ // X-API-Key is dropped whenever the server advertises OAuth metadata; Bearer is not.
149
+ // Grok expands `${VAR}` in headers at load, so the key stays out of argv and config.toml.
150
+ header: { name: 'Authorization', sep: ': ', valuePrefix: 'Bearer ', viaEnv: true },
151
+ signInHint: GROK_SIGN_IN_HINT,
152
+ signInViaCliLogin: false,
153
+ canVerifyAuth: false,
154
+ keyHeldByCli: false,
155
+ cliWritesConfig: false,
156
+ authDeferredAtInstall: false,
157
+ },
158
+ hermes: {
159
+ command: 'hermes',
160
+ displayName: 'Hermes',
161
+ configDir: '.hermes',
162
+ reloadAction: 'restart it',
163
+ skillAgent: 'hermes-agent',
164
+ keyRequired: true,
165
+ supportCheck: {
166
+ args: ['mcp', 'add', '--help'],
167
+ patterns: [/--auth\s+\{[^}]*oauth[^}]*\}/],
168
+ keyAuthPattern: /--auth\s+\{[^}]*header[^}]*\}/,
169
+ unavailableMessage: HERMES_OAUTH_UNAVAILABLE_MESSAGE,
170
+ },
171
+ urlStyle: 'flag',
172
+ // No `header`: the key goes into Hermes' .env, and `--auth header` reads it from there.
173
+ keyAddFlags: ['--auth', 'header'],
174
+ uninstallExtra: {
175
+ args: ['plugins', 'uninstall', HERMES_PLUGIN_NAME],
176
+ label: 'TinyFish web plugin from Hermes',
177
+ },
178
+ postConnectNote: HERMES_RESTART_NOTE,
179
+ signInViaCliLogin: false,
180
+ // Stays false: keyed already covers auth; true breaks keyless installs.
181
+ canVerifyAuth: false,
182
+ keyHeldByCli: true,
183
+ cliWritesConfig: false,
184
+ authDeferredAtInstall: false,
185
+ },
186
+ openclaw: {
187
+ command: 'openclaw',
188
+ displayName: 'OpenClaw',
189
+ configDir: '.openclaw',
190
+ reloadAction: 'restart it',
191
+ supportCheck: {
192
+ args: ['skills', 'install', '--help'],
193
+ patterns: [/--global(?:\s|$)/, /--acknowledge-clawhub-risk(?:\s|$)/],
194
+ unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
195
+ },
196
+ signInViaCliLogin: true,
197
+ canVerifyAuth: true,
198
+ keyHeldByCli: true,
199
+ cliWritesConfig: false,
200
+ authDeferredAtInstall: false,
201
+ },
202
+ opencode: {
203
+ command: 'opencode',
204
+ displayName: 'OpenCode',
205
+ configDir: '.config/opencode',
206
+ reloadAction: 'restart it',
207
+ skillAgent: 'opencode',
208
+ nonInteractiveAdd: true,
209
+ supportCheck: {
210
+ args: ['mcp', 'add', '--help'],
211
+ patterns: [/--url(?:\s|$)/m],
212
+ keyAuthPattern: HEADER_FLAG,
213
+ unavailableMessage: OPENCODE_MCP_ADD_UNAVAILABLE_MESSAGE,
214
+ },
215
+ urlStyle: 'flag',
216
+ // OpenCode takes `--header NAME=VALUE`, not Claude Code's `NAME: VALUE`.
217
+ header: { name: 'X-API-Key', sep: '=' },
218
+ // OpenCode writes its config itself; `mcp auth` is the separate OAuth step.
219
+ loginArgs: ['mcp', 'auth', 'tinyfish'],
220
+ // `opencode mcp add` upserts by name and ships no `mcp remove`.
221
+ removals: [],
222
+ postConnectNote: OPENCODE_MODEL_NOTE,
223
+ signInViaCliLogin: false,
224
+ canVerifyAuth: false,
225
+ keyHeldByCli: false,
226
+ cliWritesConfig: false,
227
+ authDeferredAtInstall: false,
228
+ },
229
+ };
230
+ export const ALL_HARNESSES = Object.keys(HARNESS_SPECS);
231
+ export function harnessSpec(harness) {
232
+ return HARNESS_SPECS[harness];
233
+ }
234
+ /** Native MCP harnesses carry add-generation fields; the rest override connect. */
235
+ export const NATIVE_HARNESSES = ALL_HARNESSES.filter((harness) => harnessSpec(harness).urlStyle !== undefined);
@@ -1,7 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { type InstallOptions } from './cli-install.js';
3
3
  export declare const HERMES_PLUGIN_SHA = "496cd63fefd982bbaa8a85ce78ef2270d700984f";
4
- export declare const HERMES_PLUGIN_NAME = "tinyfish";
5
4
  export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish-io/tinyfish-web-agent-integrations/hermes --ref 496cd63fefd982bbaa8a85ce78ef2270d700984f --enable";
6
5
  export declare function installHermesPlugin(home: string, apiKey: string, { verbose }: Pick<InstallOptions, 'verbose'>): void;
7
6
  /** After install only: backends must never name an absent provider. */
@@ -11,7 +11,6 @@ import { errLine } from './output.js';
11
11
  // Bump per CLI release.
12
12
  export const HERMES_PLUGIN_SHA = '496cd63fefd982bbaa8a85ce78ef2270d700984f';
13
13
  // Manifest `name:`, not the repo subdir; `plugins uninstall` keys on it.
14
- export const HERMES_PLUGIN_NAME = 'tinyfish';
15
14
  const PLUGIN_SOURCE = 'tinyfish-io/tinyfish-web-agent-integrations/hermes';
16
15
  const PLUGIN_REPO_URL = 'https://github.com/tinyfish-io/tinyfish-web-agent-integrations.git';
17
16
  export const HERMES_PLUGIN_MANUAL_INSTALL = `hermes plugins install ${PLUGIN_SOURCE} --ref ${HERMES_PLUGIN_SHA} --enable`;
@@ -79,7 +79,7 @@ export type UpgradeScope = 'both' | 'cli' | 'skill';
79
79
  /** `toVersion` is null when the run was interrupted or the installed version could not be read. */
80
80
  export declare function sendUpgradeCompleted(fromVersion: string, toVersion: string | null, outcome: UpgradeOutcome, scope: UpgradeScope, apiKey?: string): Promise<void>;
81
81
  export declare function sendSetupCompleted(mcpUrl: string, harnesses: HarnessResult[], apiKey?: string, mode?: SetupMode, attemptId?: string): Promise<void>;
82
- export declare const DOCTOR_HARNESS_SCOPES: readonly ["all", "claude-code", "codex", "cursor", "grok", "hermes", "openclaw", "opencode"];
82
+ export declare const DOCTOR_HARNESS_SCOPES: readonly ["all", ...("openclaw" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code")[]];
83
83
  export declare const doctorCompletedPayloadSchema: z.ZodObject<{
84
84
  schema_version: z.ZodInt;
85
85
  cli_version: z.ZodString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.35.0",
3
+ "version": "0.35.1-next.290",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {