@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.
- package/dist/lib/connect-native.js +24 -3
- package/dist/lib/connect-runtime.d.ts +6 -1
- package/dist/lib/connect-runtime.js +19 -2
- package/dist/lib/setup-telemetry.d.ts +2 -0
- package/dist/lib/setup-telemetry.js +4 -2
- package/dist/lib/suggested-prompt.d.ts +12 -0
- package/dist/lib/suggested-prompt.js +17 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
409
|
-
|
|
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
|
+
}
|