@tiny-fish/cli 0.54.0 → 0.56.1-next.456

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,9 +1,12 @@
1
1
  import { spawn } from 'child_process';
2
2
  import * as readline from 'readline';
3
3
  import { Option } from 'commander';
4
- import { CONNECT_ATTEMPT_ENV, clearConfig, getDashboardUrl, loadConfig, maskKey, savePendingConnectAttempt, saveConfig, validateKeyFormat, } from '../lib/auth.js';
4
+ import { CONNECT_ATTEMPT_ENV, clearConfig, getDashboardUrl, loadConfig, maskKey, persistApiKeyToEnvironment, savePendingConnectAttempt, saveConfig, validateKeyFormat, } from '../lib/auth.js';
5
+ import { createStdinPrompt } from '../lib/connect-fallback.js';
6
+ import { ConnectInterruptedError } from '../lib/connect-runtime.js';
5
7
  import { TINYFISH_API_KEY_VAR } from '../lib/constants.js';
6
8
  import { err, errLine, out, outLine, warnLine } from '../lib/output.js';
9
+ import { verifyMcpAuth } from '../lib/verify.js';
7
10
  /**
8
11
  * Read a key from stdin without echoing it to the terminal.
9
12
  * Uses setRawMode on TTY so characters are never written to the screen.
@@ -82,6 +85,56 @@ function warnIfEnvShadows(savedKey) {
82
85
  warnLine(`Warning: ${TINYFISH_API_KEY_VAR} is set in your environment and overrides the key just saved. ` +
83
86
  `Unset it or update your shell profile, then check with 'tinyfish auth status'.`);
84
87
  }
88
+ async function confirmReplaceEnvKey() {
89
+ try {
90
+ const answer = await createStdinPrompt()(`${TINYFISH_API_KEY_VAR} in your environment is invalid or expired. Replace it with the new key? [Y/n] `);
91
+ return !/^n/i.test(answer.trim());
92
+ }
93
+ catch (error) {
94
+ if (error instanceof ConnectInterruptedError)
95
+ process.exit(130);
96
+ return false;
97
+ }
98
+ }
99
+ function replaceEnvKey(key, envKey) {
100
+ let persisted;
101
+ try {
102
+ persisted = persistApiKeyToEnvironment(key, { previousKey: envKey });
103
+ }
104
+ catch (error) {
105
+ errLine(`Could not update your shell profile (${error instanceof Error ? error.message : String(error)}).`);
106
+ warnIfEnvShadows(key);
107
+ return;
108
+ }
109
+ if (!persisted.replacedPrevious) {
110
+ warnLine(`Could not find where ${TINYFISH_API_KEY_VAR} is set; unset it manually.`);
111
+ return;
112
+ }
113
+ errLine(process.platform === 'win32'
114
+ ? `Updated ${persisted.location}. Open a new terminal to use the new key.`
115
+ : `Updated ${persisted.location}. Run 'source ${persisted.location}' or open a new terminal.`);
116
+ }
117
+ // Only a 401 blames a key; piped input never verifies.
118
+ async function saveKey(key, dashboardUrl) {
119
+ const envKey = process.env[TINYFISH_API_KEY_VAR];
120
+ const shadowed = process.stdin.isTTY && envKey && envKey !== key ? envKey : undefined;
121
+ const [pasted, env] = shadowed
122
+ ? await Promise.all([verifyMcpAuth(key), verifyMcpAuth(shadowed)])
123
+ : [];
124
+ if (pasted?.status === 401) {
125
+ err(`That key was rejected by TinyFish. Copy a fresh one from ${dashboardUrl}.`);
126
+ process.exit(1);
127
+ }
128
+ saveConfig(key);
129
+ parkSeedAttempt();
130
+ out({ status: 'ok', message: 'API key saved', key_preview: maskKey(key) });
131
+ if (shadowed && env?.status === 401 && (await confirmReplaceEnvKey())) {
132
+ replaceEnvKey(key, shadowed);
133
+ }
134
+ else {
135
+ warnIfEnvShadows(key);
136
+ }
137
+ }
85
138
  export function registerAuth(program) {
86
139
  const auth = program.command('auth').description('Manage your TinyFish API key');
87
140
  auth
@@ -125,10 +178,7 @@ export function registerAuth(program) {
125
178
  err('Invalid key format. Expected sk-tinyfish-... or sk-mino-...');
126
179
  process.exit(1);
127
180
  }
128
- saveConfig(key);
129
- parkSeedAttempt();
130
- out({ status: 'ok', message: 'API key saved', key_preview: maskKey(key) });
131
- warnIfEnvShadows(key);
181
+ await saveKey(key, dashboardUrl);
132
182
  });
133
183
  auth
134
184
  .command('set')
@@ -147,10 +197,7 @@ export function registerAuth(program) {
147
197
  err('Invalid key format. Expected sk-tinyfish-... or sk-mino-...');
148
198
  process.exit(1);
149
199
  }
150
- saveConfig(key);
151
- parkSeedAttempt();
152
- out({ status: 'ok', message: 'API key saved', key_preview: maskKey(key) });
153
- warnIfEnvShadows(key);
200
+ await saveKey(key, getDashboardUrl());
154
201
  });
155
202
  auth
156
203
  .command('status')
@@ -47,8 +47,13 @@ export declare function clearConnectContext(client: string): void;
47
47
  interface EnvironmentOptions {
48
48
  platform?: typeof process.platform;
49
49
  shell?: string;
50
+ previousKey?: string;
50
51
  }
51
- export declare function persistApiKeyToEnvironment(apiKey: string, options?: EnvironmentOptions): void;
52
+ export interface PersistedEnvironment {
53
+ location: string;
54
+ replacedPrevious: boolean;
55
+ }
56
+ export declare function persistApiKeyToEnvironment(apiKey: string, options?: EnvironmentOptions): PersistedEnvironment;
52
57
  export declare function clearConfig(): boolean;
53
58
  export declare function validateKeyFormat(key: string): boolean;
54
59
  export declare function maskKey(key: string): string;
package/dist/lib/auth.js CHANGED
@@ -229,7 +229,7 @@ export function persistApiKeyToEnvironment(apiKey, options = {}) {
229
229
  cause: result.error,
230
230
  });
231
231
  }
232
- return;
232
+ return { location: 'your Windows user environment', replacedPrevious: true };
233
233
  }
234
234
  const shell = path.basename(options.shell ?? process.env['SHELL'] ?? '');
235
235
  const fish = shell === 'fish';
@@ -238,15 +238,17 @@ export function persistApiKeyToEnvironment(apiKey, options = {}) {
238
238
  const prefix = fish ? `set -gx ${TINYFISH_API_KEY_VAR} ` : `export ${TINYFISH_API_KEY_VAR}=`;
239
239
  const assignment = `${prefix}${quotedKey}`;
240
240
  const existing = fs.existsSync(profile) ? fs.readFileSync(profile, 'utf8') : '';
241
+ const lines = existing.split('\n');
241
242
  // Drops prior assignments, so repeated logins do not stack stale keys.
242
- const cleaned = existing
243
- .split('\n')
244
- .filter((line) => !line.startsWith(prefix))
245
- .join('\n');
243
+ const cleaned = lines.filter((line) => !line.startsWith(prefix)).join('\n');
244
+ const { previousKey } = options;
245
+ const replacedPrevious = previousKey !== undefined &&
246
+ lines.some((line) => line.startsWith(prefix) && line.includes(previousKey));
246
247
  const updated = `${cleaned}${cleaned && !cleaned.endsWith('\n') ? '\n' : ''}${assignment}\n`;
247
248
  fs.mkdirSync(path.dirname(profile), { recursive: true });
248
249
  fs.writeFileSync(profile, updated, { mode: 0o600 });
249
250
  fs.chmodSync(profile, 0o600);
251
+ return { location: profile, replacedPrevious };
250
252
  }
251
253
  export function clearConfig() {
252
254
  if (!loadConfig().api_key)
@@ -1,9 +1,9 @@
1
1
  import spawn from 'cross-spawn';
2
2
  import { z } from 'zod';
3
- import { CONNECT_SOURCE, loadConfig, matchesCliKey } from './auth.js';
3
+ import { loadConfig, matchesCliKey } from './auth.js';
4
4
  import { captureStdio, capturedOutput, replay } from './cli-install.js';
5
5
  import { throwIfInterrupted } from './connect-runtime.js';
6
- import { CLAUDE_PLUGIN_ID, HARNESS_PROBE_TIMEOUT_MS, TINYFISH_API_KEY_VAR } from './constants.js';
6
+ import { CLAUDE_PLUGIN_ID, HARNESS_PROBE_TIMEOUT_MS, PLUGIN_CONNECT_ATTEMPT_VAR, TINYFISH_API_KEY_VAR, } from './constants.js';
7
7
  import { claudeSettingsPath, readSettingsEnv, writeSettingsEnv } from './config/claude-config.js';
8
8
  import { noColourEnv, parseJson } from './output.js';
9
9
  export const CLAUDE_PLUGIN_SERVER = 'plugin:tinyfish:tinyfish';
@@ -27,21 +27,8 @@ function pluginEnabled(command) {
27
27
  const parsed = pluginListSchema.safeParse(parseJson(String(result.stdout ?? '')));
28
28
  return parsed.success && parsed.data.some((p) => p.id === CLAUDE_PLUGIN_ID && p.enabled);
29
29
  }
30
- function runInstall(command, attemptId, verbose) {
31
- const config = {
32
- connect_source: CONNECT_SOURCE,
33
- connect_client: 'claude-code',
34
- connect_attempt_id: attemptId,
35
- };
36
- const configArgs = Object.entries(config).flatMap(([k, v]) => ['--config', `${k}=${v}`]);
37
- const result = claudeJson(command, [
38
- 'plugin',
39
- 'install',
40
- CLAUDE_PLUGIN_ID,
41
- '--json',
42
- '-y',
43
- ...configArgs,
44
- ]);
30
+ function runInstall(command, verbose) {
31
+ const result = claudeJson(command, ['plugin', 'install', CLAUDE_PLUGIN_ID, '--json', '-y']);
45
32
  if (verbose)
46
33
  replay(capturedOutput(result));
47
34
  const parsed = installResultSchema.safeParse(parseJson(String(result.stdout ?? '').trim()));
@@ -55,6 +42,17 @@ function runInstall(command, attemptId, verbose) {
55
42
  // The connect-event route folds codes it cannot hold to `other`.
56
43
  return { installed: false, fallbackReason: parsed.data.failureCode ?? 'unknown' };
57
44
  }
45
+ // The plugin templates this into its X-TF-Connect-Attempt-Id header.
46
+ function setPluginAttemptId(attemptId) {
47
+ try {
48
+ if (attemptId !== undefined || readSettingsEnv(PLUGIN_CONNECT_ATTEMPT_VAR) !== undefined) {
49
+ writeSettingsEnv(PLUGIN_CONNECT_ATTEMPT_VAR, attemptId);
50
+ }
51
+ }
52
+ catch {
53
+ // Malformed settings.json only loses attribution, never the install.
54
+ }
55
+ }
58
56
  /** Seeds the key first; a fallback restores the prior value. */
59
57
  export function installClaudeDirectoryPlugin(options) {
60
58
  const { command, attemptId, apiKey, verbose } = options;
@@ -72,12 +70,13 @@ export function installClaudeDirectoryPlugin(options) {
72
70
  }
73
71
  let outcome;
74
72
  try {
75
- outcome = runInstall(command, attemptId, verbose);
73
+ outcome = runInstall(command, verbose);
76
74
  }
77
75
  catch (error) {
78
76
  restore?.();
79
77
  throw error;
80
78
  }
79
+ setPluginAttemptId(outcome.installed ? attemptId : undefined);
81
80
  if (!outcome.installed) {
82
81
  restore?.();
83
82
  return outcome;
@@ -91,6 +90,7 @@ export function installClaudeDirectoryPlugin(options) {
91
90
  }
92
91
  /** Compare-only: a key the user set themselves stays. */
93
92
  export function removeClaudeSettingsKey() {
93
+ setPluginAttemptId(undefined);
94
94
  if (matchesCliKey(readSettingsEnv(TINYFISH_API_KEY_VAR))) {
95
95
  writeSettingsEnv(TINYFISH_API_KEY_VAR, undefined);
96
96
  }
@@ -238,7 +238,7 @@ export declare function createDataset(req: datasets.CreateRequest, apiKey: strin
238
238
  column: string;
239
239
  } | undefined;
240
240
  };
241
- status: "ready" | "empty" | "populating" | "failed" | "cancelled";
241
+ status: "failed" | "empty" | "populating" | "ready" | "cancelled";
242
242
  target_rows: number;
243
243
  row_count: number;
244
244
  run: {
@@ -300,7 +300,7 @@ export declare function listDatasets(opts: datasets.ListQuery, apiKey: string):
300
300
  column: string;
301
301
  } | undefined;
302
302
  };
303
- status: "ready" | "empty" | "populating" | "failed" | "cancelled";
303
+ status: "failed" | "empty" | "populating" | "ready" | "cancelled";
304
304
  target_rows: number;
305
305
  row_count: number;
306
306
  run: {
@@ -343,7 +343,7 @@ export declare function getDataset(id: string, apiKey: string): Promise<{
343
343
  column: string;
344
344
  } | undefined;
345
345
  };
346
- status: "ready" | "empty" | "populating" | "failed" | "cancelled";
346
+ status: "failed" | "empty" | "populating" | "ready" | "cancelled";
347
347
  target_rows: number;
348
348
  row_count: number;
349
349
  run: {
@@ -398,7 +398,7 @@ export declare function populateDataset(id: string, req: datasets.PopulateReques
398
398
  column: string;
399
399
  } | undefined;
400
400
  };
401
- status: "ready" | "empty" | "populating" | "failed" | "cancelled";
401
+ status: "failed" | "empty" | "populating" | "ready" | "cancelled";
402
402
  target_rows: number;
403
403
  row_count: number;
404
404
  run: {
@@ -317,14 +317,16 @@ function performLoginStep(client, pendingLogin, options) {
317
317
  }
318
318
  // Upper bound only: the URL normally prints within two seconds.
319
319
  const SIGN_IN_URL_WAIT_MS = 15_000;
320
- // 2.x's service lags `mcp add` by under a second (measured on 2.1.4).
320
+ // 2.x's service lags `mcp add` by under a second (measured on 2.1.4 and 2.2.4).
321
321
  const STALE_REGISTRATION_RETRIES = 3;
322
322
  const STALE_REGISTRATION_DELAY_MS = 1_000;
323
+ // 2.1.x wording, then 2.2.x.
324
+ const STALE_REGISTRATION = /Integration not found|MCP server not found/;
323
325
  /** Relays the sign-in link; the harness service finishes it after we exit. */
324
326
  async function printDetachedLoginUrl(client, pendingLogin) {
325
327
  errLine('Starting TinyFish sign-in...');
326
328
  let run = await waitForSignInUrl(client.command, pendingLogin, SIGN_IN_URL_WAIT_MS);
327
- for (let retry = 0; retry < STALE_REGISTRATION_RETRIES && run.output.includes('Integration not found'); retry++) {
329
+ for (let retry = 0; retry < STALE_REGISTRATION_RETRIES && STALE_REGISTRATION.test(run.output); retry++) {
328
330
  await new Promise((resolve) => setTimeout(resolve, STALE_REGISTRATION_DELAY_MS));
329
331
  run = await waitForSignInUrl(client.command, pendingLogin, SIGN_IN_URL_WAIT_MS);
330
332
  }
@@ -10,6 +10,7 @@ export declare const WEB_SKILL_NAMES: readonly ["tinyfish-authenticated", "tinyf
10
10
  export declare const LEGACY_SKILL_NAMES: readonly ["use-tinyfish"];
11
11
  /** The descriptor hands this name to harnesses; the probes resolve it. */
12
12
  export declare const TINYFISH_API_KEY_VAR = "TINYFISH_API_KEY";
13
+ export declare const PLUGIN_CONNECT_ATTEMPT_VAR = "TINYFISH_PLUGIN_CONNECT_ATTEMPT_ID";
13
14
  export declare const TINYFISH_ACCESS_MODE_HEADER = "X-TinyFish-Access-Mode";
14
15
  export declare const TINYFISH_KEYLESS_ACCESS_MODE = "keyless";
15
16
  export declare const BASE_URL: string;
@@ -22,6 +22,8 @@ export const WEB_SKILL_NAMES = [
22
22
  export const LEGACY_SKILL_NAMES = ['use-tinyfish'];
23
23
  /** The descriptor hands this name to harnesses; the probes resolve it. */
24
24
  export const TINYFISH_API_KEY_VAR = 'TINYFISH_API_KEY';
25
+ // Not TINYFISH_CONNECT_ATTEMPT_ID: the CLI reads that as an install seed.
26
+ export const PLUGIN_CONNECT_ATTEMPT_VAR = 'TINYFISH_PLUGIN_CONNECT_ATTEMPT_ID';
25
27
  export const TINYFISH_ACCESS_MODE_HEADER = 'X-TinyFish-Access-Mode';
26
28
  export const TINYFISH_KEYLESS_ACCESS_MODE = 'keyless';
27
29
  /** Base URL for the TinyFish API. Override with TINYFISH_API_URL for staging/self-hosted. */
@@ -11,10 +11,10 @@ export declare const reattachDelayMs: (attempt: number) => number;
11
11
  export declare function pause(ms: number, signal: AbortSignal): Promise<void>;
12
12
  export declare const idSchema: z.ZodUUID;
13
13
  export declare const statusSchema: z.ZodEnum<{
14
- ready: "ready";
14
+ failed: "failed";
15
15
  empty: "empty";
16
16
  populating: "populating";
17
- failed: "failed";
17
+ ready: "ready";
18
18
  cancelled: "cancelled";
19
19
  }>;
20
20
  export declare const runStatusSchema: z.ZodEnum<{
@@ -77,10 +77,10 @@ export declare const datasetSchema: z.ZodObject<{
77
77
  }, z.core.$loose>>;
78
78
  }, z.core.$loose>;
79
79
  status: z.ZodEnum<{
80
- ready: "ready";
80
+ failed: "failed";
81
81
  empty: "empty";
82
82
  populating: "populating";
83
- failed: "failed";
83
+ ready: "ready";
84
84
  cancelled: "cancelled";
85
85
  }>;
86
86
  target_rows: z.ZodNumber;
@@ -172,10 +172,10 @@ export declare const listSchema: z.ZodObject<{
172
172
  }, z.core.$loose>>;
173
173
  }, z.core.$loose>;
174
174
  status: z.ZodEnum<{
175
- ready: "ready";
175
+ failed: "failed";
176
176
  empty: "empty";
177
177
  populating: "populating";
178
- failed: "failed";
178
+ ready: "ready";
179
179
  cancelled: "cancelled";
180
180
  }>;
181
181
  target_rows: z.ZodNumber;
@@ -3,6 +3,7 @@ export declare const RESEARCH_TIMEOUT_MS = 2700000;
3
3
  export declare const START_TIMEOUT_MS = 30000;
4
4
  export declare const READ_TIMEOUT_MS = 15000;
5
5
  export declare const CANCEL_TIMEOUT_MS = 5000;
6
+ export declare const RESEARCH_QUERY_MAX_LENGTH = 10000;
6
7
  export declare const requestSchema: z.ZodObject<{
7
8
  query: z.ZodString;
8
9
  mode: z.ZodOptional<z.ZodEnum<{
@@ -4,8 +4,9 @@ export const RESEARCH_TIMEOUT_MS = 2_700_000;
4
4
  export const START_TIMEOUT_MS = 30_000;
5
5
  export const READ_TIMEOUT_MS = 15_000;
6
6
  export const CANCEL_TIMEOUT_MS = 5_000;
7
+ export const RESEARCH_QUERY_MAX_LENGTH = 10_000;
7
8
  export const requestSchema = z.object({
8
- query: z.string().trim().min(1).max(2000),
9
+ query: z.string().trim().min(1).max(RESEARCH_QUERY_MAX_LENGTH),
9
10
  mode: z.enum(['deep', 'max']).optional(),
10
11
  output_language: z.string().optional(),
11
12
  });
@@ -218,8 +218,8 @@ declare const doctorCompletedPayloadSchema: z.ZodObject<{
218
218
  declare const doctorCouldNotRunPayloadSchema: z.ZodObject<{
219
219
  outcome: z.ZodLiteral<"could_not_run">;
220
220
  error_class: z.ZodEnum<{
221
- timeout: "timeout";
222
221
  command_not_found: "command_not_found";
222
+ timeout: "timeout";
223
223
  spawn_error: "spawn_error";
224
224
  nonzero_exit: "nonzero_exit";
225
225
  unexpected_error: "unexpected_error";
@@ -364,8 +364,8 @@ declare const doctorPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
364
364
  }, z.core.$strip>, z.ZodObject<{
365
365
  outcome: z.ZodLiteral<"could_not_run">;
366
366
  error_class: z.ZodEnum<{
367
- timeout: "timeout";
368
367
  command_not_found: "command_not_found";
368
+ timeout: "timeout";
369
369
  spawn_error: "spawn_error";
370
370
  nonzero_exit: "nonzero_exit";
371
371
  unexpected_error: "unexpected_error";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.54.0",
3
+ "version": "0.56.1-next.456",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- f8fd846c92a8a68cb98a5b51bc6e2fb420295dcd
1
+ d5d481e9ac88fc30b00e065444cc9574a3cb9cee
@@ -22,7 +22,7 @@ signal. You always know your own task, so there is never a reason to omit it.
22
22
  }
23
23
  ```
24
24
 
25
- Max 2000 characters. Describe the goal, not the query.
25
+ Max 10,000 characters. Describe the goal, not the query.
26
26
 
27
27
  ## Writing the query
28
28
 
@@ -22,7 +22,7 @@ signal. You always know your own task, so there is never a reason to omit it.
22
22
  }
23
23
  ```
24
24
 
25
- Max 2000 characters. Describe the goal, not the query.
25
+ Max 10,000 characters. Describe the goal, not the query.
26
26
 
27
27
  ## Writing the query
28
28