@tiny-fish/cli 0.42.1 → 0.44.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.
@@ -41,6 +41,7 @@ export function errLine(line) {
41
41
  }
42
42
  const WARN_ON = '\x1b[33m';
43
43
  const WARN_OFF = '\x1b[39m';
44
+ const VAULT_RECONNECT_REQUIRED = 'VAULT_RECONNECT_REQUIRED';
44
45
  /** Advisory stderr line, yellow on a terminal and plain everywhere else so piped output stays clean. */
45
46
  export function warnLine(line) {
46
47
  const text = sanitizeLine(line);
@@ -57,9 +58,14 @@ export function handleApiError(e) {
57
58
  const payload = { error: e.message, status: e.status };
58
59
  if (e.code)
59
60
  payload.code = e.code;
60
- if (e.status === 401)
61
+ if (e.code === VAULT_RECONNECT_REQUIRED) {
62
+ payload.hint =
63
+ 'Reconnect the provider with `tinyfish vault connection add --provider 1password` or `tinyfish vault connection add --provider bitwarden --client-id <client-id>`. Vault secrets are read from TINYFISH_VAULT_TOKEN, TINYFISH_VAULT_CLIENT_SECRET, and TINYFISH_VAULT_MASTER_PASSWORD.';
64
+ }
65
+ else if (e.status === 401) {
61
66
  payload.hint =
62
67
  'Clear the TINYFISH_API_KEY environment variable, run `tinyfish auth login`, then try again.';
68
+ }
63
69
  err(payload);
64
70
  }
65
71
  else if (e instanceof Error) {
@@ -326,9 +326,14 @@ function probeOmp() {
326
326
  : entry.keyMatchesCliKey
327
327
  ? { keyMatchesCliKey: true }
328
328
  : {};
329
+ let authMode = AuthMode.Unknown;
330
+ if (entry.hasApiKeyHeader)
331
+ authMode = AuthMode.ApiKey;
332
+ else if (entry.keyless)
333
+ authMode = AuthMode.Keyless;
329
334
  return {
330
335
  registered: Registered.Yes,
331
- authMode: entry.hasApiKeyHeader ? AuthMode.ApiKey : AuthMode.Unknown,
336
+ authMode,
332
337
  ...(entry.url ? { registeredUrl: entry.url } : {}),
333
338
  ...keyVerdict,
334
339
  };
@@ -391,16 +396,19 @@ function probeHermes() {
391
396
  if (entry.state === 'absent')
392
397
  return NOT_REGISTERED;
393
398
  // `mcp add` saves a disabled entry on a failed connect, which connect refuses too.
394
- const enabled = entry.state === 'enabled' ? {} : { connected: false };
399
+ const details = entry.state === 'enabled' ? {} : { connected: false };
400
+ const registeredUrl = entry.url ? { registeredUrl: entry.url } : {};
395
401
  // The .env key is Hermes-wide; the header template ties it here.
396
402
  const storedKey = entry.usesKeyHeader ? readHermesKey(home) : undefined;
397
403
  if (!storedKey) {
398
- return { registered: Registered.Yes, authMode: AuthMode.OAuth, ...enabled };
404
+ const authMode = entry.keyless ? AuthMode.Keyless : AuthMode.OAuth;
405
+ return { registered: Registered.Yes, authMode, ...details, ...registeredUrl };
399
406
  }
400
407
  return {
401
408
  registered: Registered.Yes,
402
409
  authMode: AuthMode.ApiKey,
403
- ...enabled,
410
+ ...details,
411
+ ...registeredUrl,
404
412
  ...(matchesCliKey(storedKey) ? { keyMatchesCliKey: true } : {}),
405
413
  };
406
414
  }
@@ -163,6 +163,12 @@ declare const doctorCompletedPayloadSchema: z.ZodObject<{
163
163
  detected: z.ZodBoolean;
164
164
  registered: z.ZodEnum<typeof Registered>;
165
165
  auth_mode: z.ZodEnum<typeof AuthMode>;
166
+ recorded_auth_mode: z.ZodOptional<z.ZodEnum<{
167
+ keyless: "keyless";
168
+ "api-key": "api-key";
169
+ oauth: "oauth";
170
+ deferred: "deferred";
171
+ }>>;
166
172
  proves_harness_reach: z.ZodBoolean;
167
173
  }, z.core.$strip>>;
168
174
  duration_ms: z.ZodInt;
@@ -266,6 +272,12 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
266
272
  detected: z.ZodBoolean;
267
273
  registered: z.ZodEnum<typeof Registered>;
268
274
  auth_mode: z.ZodEnum<typeof AuthMode>;
275
+ recorded_auth_mode: z.ZodOptional<z.ZodEnum<{
276
+ keyless: "keyless";
277
+ "api-key": "api-key";
278
+ oauth: "oauth";
279
+ deferred: "deferred";
280
+ }>>;
269
281
  proves_harness_reach: z.ZodBoolean;
270
282
  }, z.core.$strip>>;
271
283
  duration_ms: z.ZodInt;
@@ -265,6 +265,7 @@ const doctorCompletedPayloadSchema = z.object({
265
265
  detected: z.boolean(),
266
266
  registered: z.enum(Registered),
267
267
  auth_mode: z.enum(AuthMode),
268
+ recorded_auth_mode: z.enum(['api-key', 'keyless', 'oauth', 'deferred']).optional(),
268
269
  proves_harness_reach: z.boolean(),
269
270
  }))
270
271
  .max(16),
@@ -9,6 +9,6 @@ export interface VerifyResult {
9
9
  status?: number;
10
10
  }
11
11
  /** Reachability check. Verify failure is a warning, never install failure. */
12
- export declare function verifyMcpHealth(mcpUrl: string): Promise<VerifyResult>;
12
+ export declare function verifyMcpHealth(mcpUrl: string, keyless?: boolean): Promise<VerifyResult>;
13
13
  /** Authenticated check — where the CLI holds the key (Cursor, OpenClaw, Hermes). */
14
14
  export declare function verifyMcpAuth(apiKey: string, apiBaseUrl?: string): Promise<VerifyResult>;
@@ -1,6 +1,16 @@
1
1
  import { listRuns } from './client.js';
2
+ import { TINYFISH_ACCESS_MODE_HEADER, TINYFISH_KEYLESS_ACCESS_MODE } from './constants.js';
2
3
  import { ApiError } from './output.js';
4
+ import { z } from 'zod';
3
5
  const VERIFY_TIMEOUT_MS = 5_000;
6
+ const keylessInitializeSchema = z.object({
7
+ jsonrpc: z.literal('2.0'),
8
+ id: z.literal(1),
9
+ result: z.object({
10
+ capabilities: z.object({}),
11
+ serverInfo: z.object({ name: z.literal('tinyfish') }),
12
+ }),
13
+ });
4
14
  // `fetch` collapses every network fault to "fetch failed" and hides the cause on `e.cause`.
5
15
  function networkCode(e) {
6
16
  const cause = e?.cause;
@@ -32,24 +42,34 @@ function authCode(e) {
32
42
  return 'rate limited';
33
43
  return e.status >= 500 ? 'TinyFish returned a server error' : `the API returned HTTP ${e.status}`;
34
44
  }
45
+ function failedHealth(reason) {
46
+ return { depth: 'health', ok: false, reason, code: reason };
47
+ }
35
48
  /** Reachability check. Verify failure is a warning, never install failure. */
36
- export async function verifyMcpHealth(mcpUrl) {
49
+ export async function verifyMcpHealth(mcpUrl, keyless = false) {
37
50
  try {
38
51
  const response = await fetch(mcpUrl, {
39
- method: 'GET',
52
+ method: keyless ? 'POST' : 'GET',
53
+ ...(keyless
54
+ ? {
55
+ headers: {
56
+ [TINYFISH_ACCESS_MODE_HEADER]: TINYFISH_KEYLESS_ACCESS_MODE,
57
+ 'Content-Type': 'application/json',
58
+ Accept: 'application/json, text/event-stream',
59
+ },
60
+ body: '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}',
61
+ }
62
+ : {}),
40
63
  // Redirect (captive portal, typo'd --url) is not healthy.
41
64
  redirect: 'error',
42
65
  signal: AbortSignal.timeout(VERIFY_TIMEOUT_MS),
43
66
  });
44
67
  // MCP rejects bare GET with 4xx — still proves endpoint routed.
45
- if (response.status >= 500) {
46
- return {
47
- depth: 'health',
48
- ok: false,
49
- reason: `endpoint returned HTTP ${response.status}`,
50
- code: `the endpoint returned HTTP ${response.status}`,
51
- };
68
+ if (response.status >= 500 || (keyless && response.status !== 200)) {
69
+ return failedHealth(`endpoint returned HTTP ${response.status}`);
52
70
  }
71
+ if (keyless && !keylessInitializeSchema.safeParse(await response.json()).success)
72
+ return failedHealth('endpoint returned an invalid MCP initialize response');
53
73
  return { depth: 'health', ok: true };
54
74
  }
55
75
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.42.1",
3
+ "version": "0.44.0",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "format:check": "oxfmt --check ."
36
36
  },
37
37
  "dependencies": {
38
- "@tiny-fish/sdk": "^0.6.0",
38
+ "@tiny-fish/sdk": "^0.7.0",
39
39
  "commander": "^12.0.0",
40
40
  "cross-spawn": "^7.0.6",
41
41
  "tldts": "^6.1.86",