@tiny-fish/cli 0.34.1-next.284 → 0.34.1-next.286

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
@@ -57,8 +57,8 @@ interactive walkthrough with one command:
57
57
  npx -y @tiny-fish/cli@latest connect codex --launch --api-key sk-tinyfish-...
58
58
  ```
59
59
 
60
- One-command OAuth requires a Codex release whose `codex mcp add` command supports
61
- `--oauth-resource`. Update Codex if support is unavailable, or add TinyFish manually with
60
+ One-command OAuth requires a Codex release whose `codex mcp add` command supports `--url`, plus
61
+ `codex mcp login`. Update Codex if support is unavailable, or add TinyFish manually with
62
62
  `codex mcp add`.
63
63
 
64
64
  With `--api-key`, Codex reads the key from `TINYFISH_API_KEY` instead of signing in — the command
@@ -75,11 +75,10 @@ function refuseKeylessInstall(client, keyAuthSupported) {
75
75
  `Run: tinyfish auth login, then ${rerun}`, 'invalid_config', { failureDetail: 'keyless_install_refused' });
76
76
  }
77
77
  /** Resolved before the removals, so a refusal strands no registration. */
78
- function keylessAddArgs(client, keyAuthSupported, oauthResource) {
78
+ function keylessAddArgs(client, keyAuthSupported) {
79
79
  if (!client.addArgs)
80
80
  refuseKeylessInstall(client, keyAuthSupported);
81
- const addArgs = client.addArgs;
82
- return (mcpUrl) => addArgs(mcpUrl, oauthResource);
81
+ return client.addArgs;
83
82
  }
84
83
  /** Runs before any write: a refused key strands nothing. */
85
84
  async function verifyKeyBeforeSeeding(apiKey, displayName, mcpUrl) {
@@ -157,7 +156,7 @@ async function prepareInstallAuth(client, options, keyAuthSupported) {
157
156
  // Refuses here when key-required; others keep their OAuth argv.
158
157
  const buildAddArgs = keyedAdd
159
158
  ? (mcpUrl) => keyedAdd.spec.addArgs(mcpUrl, keyedAdd.key)
160
- : keylessAddArgs(client, keyAuthSupported, options.mcpUrl);
159
+ : keylessAddArgs(client, keyAuthSupported);
161
160
  if (options.keyAuthOnly && !keyedAdd) {
162
161
  // All native clients have keyAuth; missing flags mean an old install.
163
162
  if (client.keyAuth) {
@@ -217,7 +217,9 @@ function repairsFor(checks, statuses) {
217
217
  for (const status of statuses) {
218
218
  // `unknown` earns nothing: connect would contradict the check's detail.
219
219
  // A registered entry that still fails earns one too.
220
- const brokenRegistration = checks.some((c) => c.harness === status.harness && c.status === 'fail');
220
+ // `connected: false` is the harness's own no; it only warns, so it earns a repair here.
221
+ const brokenRegistration = status.connected === false ||
222
+ checks.some((c) => c.harness === status.harness && c.status === 'fail');
221
223
  if (!status.detected || (status.registered !== Registered.No && !brokenRegistration))
222
224
  continue;
223
225
  repairs.push({
@@ -3,6 +3,7 @@ import { NATIVE_BY_HARNESS, openclawSkillUninstall } from './connect-clients.js'
3
3
  import { ConnectInterruptedError, spawnRemoval } from './connect-runtime.js';
4
4
  import { removeCursorMcpServer, planCursorWrite } from './cursor-config.js';
5
5
  import { HERMES_KEY_VAR, hermesEnvPath, removeHermesKey, resolveHermesHome } from './hermes-env.js';
6
+ import { clearHermesWebBackends, HERMES_PLUGIN_SHA } from './hermes-plugin.js';
6
7
  import { HARNESS_DISPLAY_NAMES as DISPLAY_NAMES } from './harness-detect.js';
7
8
  import { errLine } from './output.js';
8
9
  // OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
@@ -12,7 +13,9 @@ export function planText(harness, mcpUrl, apiKey) {
12
13
  return planCursorWrite(mcpUrl, apiKey);
13
14
  // Only the keyed path seeds the .env; a keyless Hermes install writes nothing.
14
15
  if (harness === 'hermes' && apiKey) {
15
- return `would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ${HERMES_KEY_VAR} to Hermes' .env)`;
16
+ return (`would run \`tinyfish connect hermes\` (harness-owned MCP write; the CLI writes ` +
17
+ `${HERMES_KEY_VAR} to Hermes' .env, installs the tinyfish plugin at ` +
18
+ `${HERMES_PLUGIN_SHA.slice(0, 12)}, and points its web backends at tinyfish)`);
16
19
  }
17
20
  return `would run \`tinyfish connect ${harness}\` (harness-owned MCP write; no local file touched by the CLI)`;
18
21
  }
@@ -24,7 +27,9 @@ export function uninstallPlanText(harness) {
24
27
  return `would print \`${uninstallPointer(harness)}\` (no removal command exists)`;
25
28
  }
26
29
  // Hermes alone leaves a seeded key behind; `mcp remove` never clears it.
27
- const envNote = harness === 'hermes' ? ` and clear ${HERMES_KEY_VAR} from Hermes' .env` : '';
30
+ const envNote = harness === 'hermes'
31
+ ? ` and clear ${HERMES_KEY_VAR} from Hermes' .env plus its tinyfish web backends`
32
+ : '';
28
33
  return `would run \`${uninstallPointer(harness)}\`${envNote}`;
29
34
  }
30
35
  /** Undefined: no harness-CLI removal exists (opencode). */
@@ -36,7 +41,8 @@ function uninstallRuns(harness) {
36
41
  const native = NATIVE_BY_HARNESS.get(harness);
37
42
  if (!native || native.removals.length === 0)
38
43
  return undefined;
39
- return { command: native.command, runs: native.removals.map((r) => r.args) };
44
+ const extra = native.uninstallExtra ? [native.uninstallExtra.args] : [];
45
+ return { command: native.command, runs: [...native.removals.map((r) => r.args), ...extra] };
40
46
  }
41
47
  /** Derived from the same args the spawn runs, so the advice cannot drift. */
42
48
  function uninstallPointer(harness) {
@@ -48,9 +54,29 @@ function uninstallPointer(harness) {
48
54
  const joiner = process.platform === 'win32' ? ' && ' : ' ; ';
49
55
  return removal.runs.map((args) => `${removal.command} ${args.join(' ')}`).join(joiner);
50
56
  }
51
- /** `mcp remove` leaves this behind; a stale value gets silently reused. */
52
- function clearHermesEnvKey() {
57
+ /** `mcp remove` clears neither the key nor the backends we wrote. */
58
+ function clearHermesLocalState() {
53
59
  const home = resolveHermesHome();
60
+ const advisories = [clearHermesEnvKey(home), clearHermesBackends(home)].filter((advisory) => advisory !== undefined);
61
+ return advisories.length > 0 ? advisories.join(' ') : undefined;
62
+ }
63
+ /** Backends left naming an absent provider would break Hermes' web tools. */
64
+ function clearHermesBackends(home) {
65
+ // The key clearer already reported the unresolved home.
66
+ if (!home)
67
+ return undefined;
68
+ try {
69
+ clearHermesWebBackends(home);
70
+ return undefined;
71
+ }
72
+ catch (e) {
73
+ const detail = e instanceof Error ? e.message : String(e);
74
+ return (`could not unset Hermes' tinyfish web backends (${detail}). Run \`hermes config unset ` +
75
+ 'web.search_backend`, `web.extract_backend` and `web.backend` by hand.');
76
+ }
77
+ }
78
+ /** `mcp remove` leaves this behind; a stale value gets silently reused. */
79
+ function clearHermesEnvKey(home) {
54
80
  const retry = 're-run `tinyfish connect --all --uninstall`';
55
81
  if (!home) {
56
82
  return (`could not locate Hermes' home, so ${HERMES_KEY_VAR} may still be set in its .env. ` +
@@ -177,7 +203,7 @@ export function uninstallHarness(harness, configPath, verbose, binaryOnPath) {
177
203
  // Clearing a key the entry still references would break a working install.
178
204
  const removed = !result.removalFailed &&
179
205
  (result.outcome === 'uninstalled' || result.outcome === 'uninstall_noop');
180
- const advisory = removed ? clearHermesEnvKey() : hermesKeyKeptAdvisory();
206
+ const advisory = removed ? clearHermesLocalState() : hermesKeyKeptAdvisory();
181
207
  if (!advisory)
182
208
  return result;
183
209
  // Appended, never replacing: an advisory must not hide the removal's message.
@@ -74,11 +74,16 @@ interface BaseMcpClient extends SupportedCommand {
74
74
  apiKey?: string;
75
75
  verbose: boolean;
76
76
  }) => void;
77
+ /** Extra --uninstall row beyond `removals`; never runs pre-add. */
78
+ uninstallExtra?: {
79
+ args: string[];
80
+ label: string;
81
+ };
77
82
  }
78
83
  /** Keyless installs use this argv; the harness signs itself in. */
79
84
  export interface OauthCapableMcpClient extends BaseMcpClient {
80
85
  keyRequired?: false;
81
- addArgs: (mcpUrl: string, oauthResource: string) => string[];
86
+ addArgs: (mcpUrl: string) => string[];
82
87
  }
83
88
  /** No sign-in exists here, so connect refuses a keyless install (Hermes). */
84
89
  export interface KeyRequiredMcpClient extends BaseMcpClient {
@@ -7,7 +7,7 @@ import { ConnectStepError, spawnStepError, } from './connect-runtime.js';
7
7
  import { TINYFISH_API_KEY_VAR } from './constants.js';
8
8
  import { HERMES_KEY_VAR, captureHermesKeyRestore, hermesEnvPath, resolveHermesHome, writeHermesKey, } from './hermes-env.js';
9
9
  import { hermesConfigPath, readHermesEntry } from './hermes-config.js';
10
- import { installHermesPlugin, setHermesWebBackends } from './hermes-plugin.js';
10
+ import { HERMES_PLUGIN_NAME, installHermesPlugin, setHermesWebBackends } from './hermes-plugin.js';
11
11
  const HERMES_SEED_TIMEOUT_MS = 120_000;
12
12
  // An old install is only one reason a flag can go unseen, so no message asserts the cause.
13
13
  const SUPPORT_CHECK_DEBUG_HINT = ' Set TINYFISH_DEBUG=1 and retry to print the help output TinyFish read.';
@@ -21,10 +21,15 @@ const CLAUDE_CODE_DEFERRED_AUTH_NOTE = 'TinyFish is registered in Claude Code bu
21
21
  'one-command sign-in (`claude mcp login`). Open Claude Code and run `/mcp` to sign in to ' +
22
22
  'TinyFish. `claude update` gets you one-command sign-in. Already have a TinyFish API key? ' +
23
23
  'Run `tinyfish connect claude-code --api-key <key>` instead.';
24
- const CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE = 'Could not confirm this Codex installation supports one-command MCP authentication: `codex ' +
25
- 'mcp add --help` did not list `--oauth-resource`. Update Codex and retry, or add TinyFish ' +
26
- 'manually with `codex mcp add`.' +
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
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.';
28
33
  const HERMES_OAUTH_UNAVAILABLE_MESSAGE = 'Could not confirm this Hermes installation supports one-command MCP authentication: `hermes ' +
29
34
  'mcp add --help` did not list `--auth oauth`. Update Hermes and retry, or add TinyFish ' +
30
35
  'manually with `hermes mcp add`.' +
@@ -174,21 +179,28 @@ export const CODEX = {
174
179
  displayName: 'Codex',
175
180
  supportCheck: {
176
181
  args: ['mcp', 'add', '--help'],
177
- patterns: [/--oauth-resource(?:\s|<)/],
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],
178
186
  keyAuthPattern: /(?:^|\s)--bearer-token-env-var(?:[\s<=]|$)/m,
179
- unavailableMessage: CODEX_OAUTH_RESOURCE_UNAVAILABLE_MESSAGE,
187
+ unavailableMessage: CODEX_MCP_URL_UNAVAILABLE_MESSAGE,
188
+ },
189
+ degradedAuthNotes: {
190
+ keyed: CODEX_KEYED_FALLBACK_NOTE,
191
+ deferred: CODEX_DEFERRED_AUTH_NOTE,
180
192
  },
181
193
  // Codex has no `--header`; the MCP server takes a key as `Bearer sk-…`.
182
- // Omitting `--oauth-resource` is what keeps `mcp add` from running OAuth.
183
194
  keyAuth: {
184
195
  envVar: TINYFISH_API_KEY_VAR,
185
196
  addArgs: (mcpUrl) => [...urlAddArgs(mcpUrl), '--bearer-token-env-var', TINYFISH_API_KEY_VAR],
186
197
  },
187
198
  detachedLaunch: launchCodexWalkthrough,
188
- // Retry only: running it after a finished inline OAuth is a second browser hop.
189
199
  loginArgs: ['mcp', 'login', 'tinyfish'],
190
200
  removals: [{ args: ['mcp', 'remove', 'tinyfish'], label: 'TinyFish registration from Codex' }],
191
- addArgs: (mcpUrl, oauthResource) => [...urlAddArgs(mcpUrl), '--oauth-resource', oauthResource],
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,
192
204
  };
193
205
  const grokAddArgs = (mcpUrl) => [
194
206
  'mcp',
@@ -320,6 +332,10 @@ export const HERMES = {
320
332
  },
321
333
  removals: [{ args: ['mcp', 'remove', 'tinyfish'], label: 'TinyFish registration from Hermes' }],
322
334
  extraPostInstall: installHermesWebPlugin,
335
+ uninstallExtra: {
336
+ args: ['plugins', 'uninstall', HERMES_PLUGIN_NAME],
337
+ label: 'TinyFish web plugin from Hermes',
338
+ },
323
339
  launchWalkthrough: launchHermesWalkthrough,
324
340
  postConnectNote: 'Restart your Hermes session to pick up TinyFish — Hermes discovers MCP servers at startup.',
325
341
  };
@@ -1,6 +1,9 @@
1
1
  import { type InstallOptions } from './cli-install.js';
2
2
  export declare const HERMES_PLUGIN_SHA = "496cd63fefd982bbaa8a85ce78ef2270d700984f";
3
+ export declare const HERMES_PLUGIN_NAME = "tinyfish";
3
4
  export declare const HERMES_PLUGIN_MANUAL_INSTALL = "hermes plugins install tinyfish-io/tinyfish-web-agent-integrations/hermes --ref 496cd63fefd982bbaa8a85ce78ef2270d700984f --enable";
4
5
  export declare function installHermesPlugin(home: string, apiKey: string, { verbose }: Pick<InstallOptions, 'verbose'>): void;
5
6
  /** After install only: backends must never name an absent provider. */
6
7
  export declare function setHermesWebBackends(home: string): void;
8
+ /** Ours-only unsets: a user-chosen backend value survives the uninstall. */
9
+ export declare function clearHermesWebBackends(home: string): void;
@@ -4,11 +4,13 @@ import * as path from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import spawn from 'cross-spawn';
6
6
  import { capturedOutput, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from './cli-install.js';
7
- import { commandNotFound, ConnectStepError, spawnStepError } from './connect-runtime.js';
7
+ import { commandNotFound, ConnectStepError, spawnStepError, throwIfInterrupted, } from './connect-runtime.js';
8
8
  import { HARNESS_PROBE_TIMEOUT_MS } from './constants.js';
9
9
  import { errLine } from './output.js';
10
10
  // Bump per CLI release.
11
11
  export const HERMES_PLUGIN_SHA = '496cd63fefd982bbaa8a85ce78ef2270d700984f';
12
+ // Manifest `name:`, not the repo subdir; `plugins uninstall` keys on it.
13
+ export const HERMES_PLUGIN_NAME = 'tinyfish';
12
14
  const PLUGIN_SOURCE = 'tinyfish-io/tinyfish-web-agent-integrations/hermes';
13
15
  const PLUGIN_REPO_URL = 'https://github.com/tinyfish-io/tinyfish-web-agent-integrations.git';
14
16
  export const HERMES_PLUGIN_MANUAL_INSTALL = `hermes plugins install ${PLUGIN_SOURCE} --ref ${HERMES_PLUGIN_SHA} --enable`;
@@ -92,8 +94,26 @@ function configWrite(home, args) {
92
94
  throw error;
93
95
  }
94
96
  const HERMES_BACKEND_KEYS = ['web.search_backend', 'web.extract_backend'];
97
+ // Dispatch falls back to web.backend, and the plugin's own setup can set it.
98
+ const HERMES_OWNED_BACKEND_KEYS = [...HERMES_BACKEND_KEYS, 'web.backend'];
95
99
  /** After install only: backends must never name an absent provider. */
96
100
  export function setHermesWebBackends(home) {
97
101
  for (const key of HERMES_BACKEND_KEYS)
98
102
  configWrite(home, ['set', key, 'tinyfish']);
99
103
  }
104
+ /** Unset prints empty and exits 0, so a failed read must not read as unset. */
105
+ function configGet(home, key) {
106
+ const result = hermesConfig(['get', key], home);
107
+ throwIfInterrupted(result);
108
+ if (result.error || result.status !== 0) {
109
+ throw spawnStepError(`Could not read \`hermes config get ${key}\``, result);
110
+ }
111
+ return String(result.stdout ?? '').trim() || undefined;
112
+ }
113
+ /** Ours-only unsets: a user-chosen backend value survives the uninstall. */
114
+ export function clearHermesWebBackends(home) {
115
+ for (const key of HERMES_OWNED_BACKEND_KEYS) {
116
+ if (configGet(home, key) === 'tinyfish')
117
+ configWrite(home, ['unset', key]);
118
+ }
119
+ }
@@ -13,6 +13,12 @@ import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMo
13
13
  // A list, verified against codex 0.146. A name-keyed map was accepted here too, but every
14
14
  // field is optional, so that arm parsed *any* object-of-objects: a wrapper like
15
15
  // `{"servers": {...}}` decoded as one entry named `servers` and reported TinyFish absent.
16
+ // Codex's wire values, verified against 0.146; anything else stays unknown.
17
+ var CodexAuthStatus;
18
+ (function (CodexAuthStatus) {
19
+ CodexAuthStatus["OAuth"] = "o_auth";
20
+ CodexAuthStatus["NotLoggedIn"] = "not_logged_in";
21
+ })(CodexAuthStatus || (CodexAuthStatus = {}));
16
22
  const codexEntrySchema = z.looseObject({
17
23
  name: z.string().optional(),
18
24
  auth_status: z.string().optional(),
@@ -216,9 +222,9 @@ export function codexOauthCompleted() {
216
222
  if ('reason' in read)
217
223
  return undefined;
218
224
  const status = read.entry?.auth_status;
219
- if (status === 'o_auth')
225
+ if (status === CodexAuthStatus.OAuth)
220
226
  return true;
221
- if (status === 'not_logged_in')
227
+ if (status === CodexAuthStatus.NotLoggedIn)
222
228
  return false;
223
229
  return undefined;
224
230
  }
@@ -255,10 +261,16 @@ function probeCodex() {
255
261
  const codexUrl = entry.transport?.url;
256
262
  // Read from this entry: codex emits `bearer_token_env_var` on every HTTP server, so testing
257
263
  // the whole payload reports api-key for TinyFish because some other server carries a key.
258
- const envVar = entry.auth_status === 'o_auth' ? undefined : entry.transport?.bearer_token_env_var;
264
+ const envVar = entry.auth_status === CodexAuthStatus.OAuth ? undefined : entry.transport?.bearer_token_env_var;
259
265
  return {
260
266
  registered: Registered.Yes,
261
- authMode: entry.auth_status === 'o_auth' ? AuthMode.OAuth : envVar ? AuthMode.ApiKey : AuthMode.Unknown,
267
+ authMode: entry.auth_status === CodexAuthStatus.OAuth
268
+ ? AuthMode.OAuth
269
+ : envVar
270
+ ? AuthMode.ApiKey
271
+ : AuthMode.Unknown,
272
+ // Codex's own verdict: a stale `oauth_resource` strands installs here.
273
+ ...(entry.auth_status === CodexAuthStatus.NotLoggedIn && !envVar ? { connected: false } : {}),
262
274
  ...(codexUrl ? { registeredUrl: codexUrl } : {}),
263
275
  ...(envVar ? envKeyVerdict(envVar) : {}),
264
276
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.34.1-next.284",
3
+ "version": "0.34.1-next.286",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {