@tiny-fish/cli 0.57.0 → 0.57.1-next.464

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.
@@ -17,6 +17,7 @@ import { dotenvMarkerHead, harnessDotenvPath, writeDotenvKey } from './dotenv-fi
17
17
  import { apiBaseFromMcpUrl, isDefaultEndpoint } from './mcp-endpoint.js';
18
18
  import { removeWebSkills } from './skill-vendor.js';
19
19
  import { finishSetupHint, requireKeylessMcp, requireSupportOrRescue, runCliInstallStep, trackPostInstallFailure, walkthroughPhase, } from './connect-steps.js';
20
+ import { SUGGESTED_PROMPTS } from './suggested-prompt.js';
20
21
  /** Several installs can disagree; every later spawn reuses this one. */
21
22
  function pinExecutable(client) {
22
23
  const resolved = resolveCommand(client.command);
@@ -446,6 +447,28 @@ export async function connectNativeMcpClient(unpinned, options) {
446
447
  });
447
448
  return state.authMode;
448
449
  }
450
+ /** `cli_connect_suggested_prompt` A/B; control records exposure without printing. */
451
+ async function printSuggestedPrompt(client, telemetry) {
452
+ if (client.connectClient !== 'claude-code')
453
+ return;
454
+ const variant = await telemetry.suggestedPromptVariant();
455
+ if (!variant)
456
+ return;
457
+ const line = SUGGESTED_PROMPTS[variant];
458
+ if (line)
459
+ errLine(line);
460
+ telemetry.track('checkpoint', {
461
+ phase: 'suggested_prompt_shown',
462
+ suggestedPromptVariant: variant,
463
+ });
464
+ }
465
+ async function printConnectedMessage(client, options, telemetry, signInDeferred, failed) {
466
+ // Under --all the summary replaces this; sign-in hints still print.
467
+ if (signInDeferred || !options.deferOutro)
468
+ errLine(connectedLine(client, signInDeferred));
469
+ if (!options.deferOutro && !failed)
470
+ await printSuggestedPrompt(client, telemetry);
471
+ }
449
472
  function connectedLine(client, signInDeferred) {
450
473
  // The CLI never observes deferred auth, so don't claim "connected".
451
474
  if (signInDeferred) {
@@ -528,9 +551,7 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
528
551
  if (client.postConnectNote)
529
552
  errLine(client.postConnectNote);
530
553
  if (!options.launch) {
531
- // Under --all the summary replaces this; sign-in hints still print.
532
- if (signInDeferred || !options.deferOutro)
533
- errLine(connectedLine(client, signInDeferred));
554
+ await printConnectedMessage(client, options, telemetry, signInDeferred, failed);
534
555
  }
535
556
  else {
536
557
  await attempt('walkthrough_launch', async () => {
@@ -1,6 +1,7 @@
1
1
  import { AuthMode } from './harness/harness-detect.js';
2
2
  import type { Harness, HarnessSupportCheck } from './harness/harness-spec.js';
3
3
  import type { PiAdapterState } from './config/pi-config.js';
4
+ import { type SuggestedPromptVariant } from './suggested-prompt.js';
4
5
  export declare const NON_INTERACTIVE_TIMEOUT_MS = 10000;
5
6
  export type AgentClient = Harness;
6
7
  type ConnectStage = 'started' | 'checkpoint' | 'completed' | 'failed' | 'aborted' | 'post_install_failed';
@@ -8,7 +9,7 @@ export type ConnectFailureStage = 'prerequisite_check' | 'registration_cleanup'
8
9
  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"];
9
10
  export type ConnectFailureReason = (typeof CONNECT_FAILURE_REASONS)[number];
10
11
  export type ConnectAuthMode = Exclude<AuthMode, AuthMode.Unknown> | 'deferred';
11
- export type ConnectCheckpoint = 'prerequisite_ok' | 'registered' | 'oauth_done' | 'cli_installed' | 'skill_installed' | 'plugin_installed' | 'authenticated' | 'cli_verified' | 'walkthrough_launched' | 'walkthrough_prompt_printed';
12
+ export type ConnectCheckpoint = 'prerequisite_ok' | 'registered' | 'oauth_done' | 'cli_installed' | 'skill_installed' | 'plugin_installed' | 'authenticated' | 'cli_verified' | 'walkthrough_launched' | 'walkthrough_prompt_printed' | 'suggested_prompt_shown';
12
13
  export type CliFallbackOutcome = 'cli_verified' | 'cli_install_failed' | 'no_credentials' | 'invalid_credentials' | 'authentication_failed' | 'verification_failed' | 'abandoned';
13
14
  /** Ctrl+C killed a setup child — abandonment, not error. */
14
15
  export declare class ConnectInterruptedError extends Error {
@@ -83,6 +84,8 @@ interface ConnectStageDetail {
83
84
  piAdapterState?: PiAdapterState;
84
85
  /** claude-code only: why connect skipped the Directory plugin. */
85
86
  pluginFallbackReason?: string;
87
+ /** Only with `suggested_prompt_shown` (route refine). */
88
+ suggestedPromptVariant?: SuggestedPromptVariant;
86
89
  }
87
90
  export interface ConnectTelemetry {
88
91
  attemptId: string;
@@ -90,6 +93,8 @@ export interface ConnectTelemetry {
90
93
  setHarnessVersion(version?: string): void;
91
94
  setFallbackOutcome(outcome: CliFallbackOutcome): void;
92
95
  flush(): Promise<void>;
96
+ /** Waits up to 2s for in-flight events; the variant rides `completed`. */
97
+ suggestedPromptVariant(): Promise<SuggestedPromptVariant | undefined>;
93
98
  }
94
99
  /** Terminal events exactly once, whichever of flow or signal handler wins. */
95
100
  export declare function settle(state: ConnectRunState, telemetry: ConnectTelemetry, stage: ConnectStage, detail?: ConnectStageDetail): void;
@@ -5,9 +5,12 @@ import { CLI_INVOCATION_ID, CLI_VERSION } from './constants.js';
5
5
  import { detectHarness, detectHumanInitiated, detectRuntimeEnv } from './harness.js';
6
6
  import { boundedVersion, errLine, noColourEnv, sanitizeLine } from './output.js';
7
7
  import { isOlder } from './cli-presence.js';
8
+ import { parseSuggestedPromptVariant } from './suggested-prompt.js';
8
9
  import { postConnectEvent, telemetryDisabled } from './setup-telemetry.js';
9
10
  import { installSignalGuard } from './signals.js';
10
11
  export const NON_INTERACTIVE_TIMEOUT_MS = 10_000;
12
+ // One POST attempt; a retried `completed` skips the prompt instead of stalling connect.
13
+ const SUGGESTED_PROMPT_WAIT_MS = 2_000;
11
14
  // Mirrors the route's Zod enum; tests both sides pin it.
12
15
  export const CONNECT_FAILURE_REASONS = [
13
16
  'harness_not_installed',
@@ -339,6 +342,10 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
339
342
  let lastTrackAt;
340
343
  let harnessVersion;
341
344
  let fallbackOutcome;
345
+ let suggestedPromptVariant;
346
+ async function flush() {
347
+ await Promise.all(pending.splice(0));
348
+ }
342
349
  async function deliver(body) {
343
350
  // Ahead of the key read; postConnectEvent's own opt-out check is too late to skip it.
344
351
  if (telemetryDisabled())
@@ -350,6 +357,9 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
350
357
  attempts: 2,
351
358
  apiKey: resolvedApiKey(opts?.apiKey),
352
359
  label: 'connect',
360
+ onResponse: (headers) => {
361
+ suggestedPromptVariant ??= parseSuggestedPromptVariant(headers);
362
+ },
353
363
  });
354
364
  }
355
365
  function eventBody(stage, detail, stageDurationMs) {
@@ -371,6 +381,7 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
371
381
  harness_degraded: d.harnessDegraded,
372
382
  pi_adapter_state: d.piAdapterState,
373
383
  plugin_fallback_reason: d.pluginFallbackReason,
384
+ suggested_prompt_variant: d.suggestedPromptVariant,
374
385
  stage_duration_ms: stageDurationMs,
375
386
  runtime_platform: process.platform,
376
387
  node_version: process.version,
@@ -405,8 +416,14 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
405
416
  },
406
417
  // Awaited in finally so in-flight events land before exit. Drains, so the signal-guard
407
418
  // flush and the finally flush do not re-await the same events.
408
- async flush() {
409
- await Promise.all(pending.splice(0));
419
+ flush,
420
+ async suggestedPromptVariant() {
421
+ // Snapshot, not drain: the final flush still awaits these.
422
+ await Promise.race([
423
+ Promise.all([...pending]),
424
+ new Promise((resolve) => setTimeout(resolve, SUGGESTED_PROMPT_WAIT_MS).unref()),
425
+ ]);
426
+ return suggestedPromptVariant;
410
427
  },
411
428
  };
412
429
  }
@@ -19,6 +19,8 @@ export declare function postConnectEvent(options: {
19
19
  attempts: number;
20
20
  label: string;
21
21
  apiKey?: string;
22
+ /** Trusted origins only, like the notice. */
23
+ onResponse?: (headers: Headers) => void;
22
24
  }): Promise<void>;
23
25
  declare const harnessResultSchema: z.ZodObject<{
24
26
  harness: z.ZodEnum<{
@@ -53,7 +53,7 @@ function telemetryHeaders(endpoint, apiKey) {
53
53
  * being installed against, upgrade has none and uses BASE_URL.
54
54
  */
55
55
  export async function postConnectEvent(options) {
56
- const { endpoint, body, attempts, label, apiKey } = options;
56
+ const { endpoint, body, attempts, label, apiKey, onResponse } = options;
57
57
  if (telemetryDisabled())
58
58
  return;
59
59
  // Swallows its own write failure so callers can treat this function as total.
@@ -80,8 +80,10 @@ export async function postConnectEvent(options) {
80
80
  });
81
81
  // Before the ok branch so a rejected event still yields a notice; gated on origin so a
82
82
  // `--url` host cannot print its own text as a TinyFish advisory.
83
- if (trusted)
83
+ if (trusted) {
84
84
  captureNotice(response.headers);
85
+ onResponse?.(response.headers);
86
+ }
85
87
  if (response.ok)
86
88
  return;
87
89
  // 4xx will reject the same body again (schema mismatch, e.g. new stages on an old server).
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ declare const suggestedPromptVariantSchema: z.ZodEnum<{
3
+ search: "search";
4
+ control: "control";
5
+ fetch: "fetch";
6
+ }>;
7
+ export type SuggestedPromptVariant = z.infer<typeof suggestedPromptVariantSchema>;
8
+ /** CLI-owned copy: the server sends only the key. Control prints nothing. */
9
+ export declare const SUGGESTED_PROMPTS: Record<SuggestedPromptVariant, string | undefined>;
10
+ /** Unknown or missing header = unenrolled. */
11
+ export declare function parseSuggestedPromptVariant(headers: Headers): SuggestedPromptVariant | undefined;
12
+ export {};
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ // Mirrors suggestedPromptVariantSchema in frontend/app/api/cli/connect-event/schemas.ts.
3
+ const suggestedPromptVariantSchema = z.enum(['control', 'search', 'fetch']);
4
+ const VARIANT_HEADER = 'x-tf-suggested-prompt-variant';
5
+ /** CLI-owned copy: the server sends only the key. Control prints nothing. */
6
+ export const SUGGESTED_PROMPTS = {
7
+ control: undefined,
8
+ search: 'Next: in Claude Code, ask "Use TinyFish to search the web for today\'s top AI news and summarize it."',
9
+ fetch: 'Next: in Claude Code, ask "Use TinyFish to fetch https://news.ycombinator.com and summarize the top 3 stories."',
10
+ };
11
+ /** Unknown or missing header = unenrolled. */
12
+ export function parseSuggestedPromptVariant(headers) {
13
+ // A throw here would read as a failed POST and retry it.
14
+ if (typeof headers?.get !== 'function')
15
+ return undefined;
16
+ return suggestedPromptVariantSchema.safeParse(headers.get(VARIANT_HEADER)?.trim()).data;
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiny-fish/cli",
3
- "version": "0.57.0",
3
+ "version": "0.57.1-next.464",
4
4
  "description": "TinyFish CLI — run web automations from your terminal",
5
5
  "type": "module",
6
6
  "bin": {