@tiny-fish/cli 0.14.1-next.151 → 0.14.1-next.154
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/commands/connect.d.ts +13 -20
- package/dist/commands/connect.js +107 -35
- package/dist/lib/harness.js +2 -4
- package/package.json +1 -1
|
@@ -7,6 +7,12 @@ export type AgentClient = "claude-code" | "codex" | "cursor" | "hermes" | "openc
|
|
|
7
7
|
/** Ctrl+C/SIGTERM killed a setup child — abandonment, not error. */
|
|
8
8
|
export declare class ConnectInterruptedError extends Error {
|
|
9
9
|
}
|
|
10
|
+
interface NativeConnectOptions {
|
|
11
|
+
apiKey?: string;
|
|
12
|
+
mcpUrl: string;
|
|
13
|
+
launch: boolean;
|
|
14
|
+
attemptId?: string;
|
|
15
|
+
}
|
|
10
16
|
export declare function installTinyFishCli(): void;
|
|
11
17
|
/**
|
|
12
18
|
* Refresh the skill we installed, for the harnesses we installed it in.
|
|
@@ -17,35 +23,22 @@ export declare function installTinyFishCli(): void;
|
|
|
17
23
|
* every agent on the machine).
|
|
18
24
|
*/
|
|
19
25
|
export declare function updateWebSkill(): void;
|
|
20
|
-
export declare function connectClaudeCode(options:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}): Promise<void>;
|
|
25
|
-
export declare function connectCodex(options: {
|
|
26
|
-
apiKey?: string;
|
|
27
|
-
mcpUrl: string;
|
|
28
|
-
launch: boolean;
|
|
29
|
-
}): Promise<void>;
|
|
30
|
-
export declare function connectHermes(options: {
|
|
31
|
-
apiKey?: string;
|
|
32
|
-
mcpUrl: string;
|
|
33
|
-
launch: boolean;
|
|
34
|
-
}): Promise<void>;
|
|
35
|
-
export declare function connectOpencode(options: {
|
|
36
|
-
apiKey?: string;
|
|
37
|
-
mcpUrl: string;
|
|
38
|
-
launch: boolean;
|
|
39
|
-
}): Promise<void>;
|
|
26
|
+
export declare function connectClaudeCode(options: NativeConnectOptions): Promise<void>;
|
|
27
|
+
export declare function connectCodex(options: NativeConnectOptions): Promise<void>;
|
|
28
|
+
export declare function connectHermes(options: NativeConnectOptions): Promise<void>;
|
|
29
|
+
export declare function connectOpencode(options: NativeConnectOptions): Promise<void>;
|
|
40
30
|
export declare function connectOpenClaw(options: {
|
|
41
31
|
mcpUrl: string;
|
|
42
32
|
launch: boolean;
|
|
43
33
|
installCli?: boolean;
|
|
34
|
+
attemptId?: string;
|
|
44
35
|
}): Promise<void>;
|
|
45
36
|
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
46
37
|
export declare function connectCursor(options: {
|
|
47
38
|
apiKey?: string;
|
|
48
39
|
mcpUrl: string;
|
|
40
|
+
attemptId?: string;
|
|
49
41
|
}): Promise<void>;
|
|
50
42
|
export declare function launchAgent(client: AgentClient): void;
|
|
51
43
|
export declare function registerConnect(program: Command): void;
|
|
44
|
+
export {};
|
package/dist/commands/connect.js
CHANGED
|
@@ -7,7 +7,8 @@ import { cursorInstallDeeplink, cursorMcpPath, writeCursorMcpConfig, } from "../
|
|
|
7
7
|
import { runConnectAll } from "../lib/connect-all.js";
|
|
8
8
|
import { detectHumanInitiated } from "../lib/harness.js";
|
|
9
9
|
import { emitNotice } from "../lib/notice.js";
|
|
10
|
-
import { errLine } from "../lib/output.js";
|
|
10
|
+
import { errLine, sanitizeLine } from "../lib/output.js";
|
|
11
|
+
import { z } from "zod";
|
|
11
12
|
import { installSignalGuard } from "../lib/signals.js";
|
|
12
13
|
import { postConnectEvent, telemetryDisabled } from "../lib/setup-telemetry.js";
|
|
13
14
|
import { verifyMcpAuth } from "../lib/verify.js";
|
|
@@ -58,6 +59,15 @@ export const OPENCLAW_ONBOARDING_PROMPT = "I just installed the TinyFish skill.
|
|
|
58
59
|
/** Ctrl+C/SIGTERM killed a setup child — abandonment, not error. */
|
|
59
60
|
export class ConnectInterruptedError extends Error {
|
|
60
61
|
}
|
|
62
|
+
class PrerequisiteError extends Error {
|
|
63
|
+
failureReason;
|
|
64
|
+
harnessVersion;
|
|
65
|
+
constructor(message, failureReason, opts) {
|
|
66
|
+
super(message, { cause: opts?.cause });
|
|
67
|
+
this.failureReason = failureReason;
|
|
68
|
+
this.harnessVersion = opts?.harnessVersion;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
61
71
|
function throwIfInterrupted(result) {
|
|
62
72
|
// spawn.sync kills a timed-out child with SIGTERM, so signal alone would misread a slow
|
|
63
73
|
// network as the user walking away. A timeout is a failure and must report as one.
|
|
@@ -78,14 +88,15 @@ function settle(state, telemetry, stage, detail) {
|
|
|
78
88
|
// (npm/npx/OAuth waits) land here.
|
|
79
89
|
function installConnectSignalGuard(state, telemetry) {
|
|
80
90
|
return installSignalGuard(async () => {
|
|
81
|
-
if (state.settled)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
if (!state.settled) {
|
|
92
|
+
settle(state, telemetry, "aborted", { failedStage: state.stage });
|
|
93
|
+
errLine("Setup interrupted — run the command again to finish.");
|
|
94
|
+
}
|
|
95
|
+
// Settled spans the whole post-install phase; the `completed` POST may still be in flight.
|
|
85
96
|
await telemetry.flush();
|
|
86
97
|
});
|
|
87
98
|
}
|
|
88
|
-
/** Shared terminal handling: interrupted → aborted
|
|
99
|
+
/** Shared terminal handling: interrupted → aborted, else failed. */
|
|
89
100
|
async function runGuarded(state, telemetry, body) {
|
|
90
101
|
const uninstallSignalGuard = installConnectSignalGuard(state, telemetry);
|
|
91
102
|
try {
|
|
@@ -93,16 +104,17 @@ async function runGuarded(state, telemetry, body) {
|
|
|
93
104
|
}
|
|
94
105
|
catch (error) {
|
|
95
106
|
if (error instanceof ConnectInterruptedError) {
|
|
96
|
-
if (state.stage === "walkthrough_launch") {
|
|
97
|
-
settle(state, telemetry, "completed");
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
107
|
settle(state, telemetry, "aborted", { failedStage: state.stage });
|
|
101
108
|
errLine("Setup interrupted — run the command again to finish.");
|
|
102
109
|
process.exitCode = 130;
|
|
103
110
|
return;
|
|
104
111
|
}
|
|
105
|
-
settle(state, telemetry, "failed", {
|
|
112
|
+
settle(state, telemetry, "failed", {
|
|
113
|
+
failedStage: state.stage,
|
|
114
|
+
...(error instanceof PrerequisiteError
|
|
115
|
+
? { failureReason: error.failureReason, harnessVersion: error.harnessVersion }
|
|
116
|
+
: {}),
|
|
117
|
+
});
|
|
106
118
|
throw error;
|
|
107
119
|
}
|
|
108
120
|
finally {
|
|
@@ -252,8 +264,9 @@ const OPENCLAW = {
|
|
|
252
264
|
unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
|
|
253
265
|
},
|
|
254
266
|
};
|
|
255
|
-
function createConnectTelemetry(mcpUrl, client,
|
|
256
|
-
|
|
267
|
+
function createConnectTelemetry(mcpUrl, client, opts) {
|
|
268
|
+
// A setup-page id (via --url) joins "copied the command" to this install attempt.
|
|
269
|
+
const attemptId = opts?.attemptId ?? randomUUID();
|
|
257
270
|
const endpoint = new URL("/api/cli/connect-event", mcpUrl).toString();
|
|
258
271
|
const pending = [];
|
|
259
272
|
async function deliver(body) {
|
|
@@ -265,7 +278,7 @@ function createConnectTelemetry(mcpUrl, client, apiKey) {
|
|
|
265
278
|
endpoint,
|
|
266
279
|
body,
|
|
267
280
|
attempts: 2,
|
|
268
|
-
apiKey: resolvedApiKey(apiKey),
|
|
281
|
+
apiKey: resolvedApiKey(opts?.apiKey),
|
|
269
282
|
label: "connect",
|
|
270
283
|
});
|
|
271
284
|
}
|
|
@@ -279,6 +292,8 @@ function createConnectTelemetry(mcpUrl, client, apiKey) {
|
|
|
279
292
|
stage,
|
|
280
293
|
failed_stage: detail?.failedStage,
|
|
281
294
|
phase: detail?.phase,
|
|
295
|
+
failure_reason: detail?.failureReason,
|
|
296
|
+
harness_version: detail?.harnessVersion,
|
|
282
297
|
runtime_platform: process.platform,
|
|
283
298
|
node_version: process.version,
|
|
284
299
|
cli_version: CLI_VERSION,
|
|
@@ -296,19 +311,32 @@ function requireCommandSupport(client) {
|
|
|
296
311
|
timeout: NON_INTERACTIVE_TIMEOUT_MS,
|
|
297
312
|
});
|
|
298
313
|
if (commandNotFound(result.error)) {
|
|
299
|
-
throw new
|
|
300
|
-
cause: result.error,
|
|
301
|
-
});
|
|
314
|
+
throw new PrerequisiteError(`${client.displayName} is not installed or not available on PATH.`, "harness_not_installed", { cause: result.error });
|
|
302
315
|
}
|
|
303
316
|
if (result.error || result.status !== 0) {
|
|
304
317
|
throwIfInterrupted(result);
|
|
305
|
-
throw new
|
|
318
|
+
throw new PrerequisiteError(client.supportCheck.unavailableMessage, "harness_command_unsupported", { cause: result.error });
|
|
306
319
|
}
|
|
307
320
|
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
308
321
|
if (!client.supportCheck.patterns.every((pattern) => pattern.test(output))) {
|
|
309
|
-
throw new
|
|
322
|
+
throw new PrerequisiteError(client.supportCheck.unavailableMessage, "harness_too_old", {
|
|
323
|
+
harnessVersion: probeHarnessVersion(client.command),
|
|
324
|
+
});
|
|
310
325
|
}
|
|
311
326
|
}
|
|
327
|
+
/** Best-effort; the server rejects non-printable characters and >32 chars. */
|
|
328
|
+
function probeHarnessVersion(command) {
|
|
329
|
+
const result = spawn.sync(command, ["--version"], {
|
|
330
|
+
encoding: "utf8",
|
|
331
|
+
timeout: NON_INTERACTIVE_TIMEOUT_MS,
|
|
332
|
+
});
|
|
333
|
+
if (result.error || result.status !== 0)
|
|
334
|
+
return undefined;
|
|
335
|
+
const version = sanitizeLine(result.stdout ?? "")
|
|
336
|
+
.replace(/[^\x20-\x7E]/g, "")
|
|
337
|
+
.trim();
|
|
338
|
+
return version ? version.slice(0, 32) : undefined;
|
|
339
|
+
}
|
|
312
340
|
function commandNotFound(error) {
|
|
313
341
|
return error?.code === "ENOENT";
|
|
314
342
|
}
|
|
@@ -530,7 +558,7 @@ function launchOpenClawWalkthrough() {
|
|
|
530
558
|
return true;
|
|
531
559
|
}
|
|
532
560
|
async function connectNativeMcpClient(client, options) {
|
|
533
|
-
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options
|
|
561
|
+
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
|
|
534
562
|
const state = { stage: "prerequisite_check", settled: false };
|
|
535
563
|
await runGuarded(state, telemetry, () => {
|
|
536
564
|
telemetry.track("started");
|
|
@@ -580,6 +608,22 @@ async function connectNativeMcpClient(client, options) {
|
|
|
580
608
|
}
|
|
581
609
|
telemetry.track("checkpoint", { phase: "oauth_done" });
|
|
582
610
|
}
|
|
611
|
+
// Registration/OAuth make MCP work; later steps are cosmetic and must not fail the attempt.
|
|
612
|
+
settle(state, telemetry, "completed");
|
|
613
|
+
runPostInstallSteps(client, options, state, telemetry, useKeyAuth);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
function connectedLine(client, useKeyAuth) {
|
|
617
|
+
// Deferred OAuth (codex/hermes): the CLI never observes auth, so don't claim "connected".
|
|
618
|
+
if (!client.loginArgs && !useKeyAuth) {
|
|
619
|
+
return (`TinyFish is configured in ${client.displayName}. ${client.displayName} will ask you ` +
|
|
620
|
+
"to sign in to TinyFish the first time you use it.");
|
|
621
|
+
}
|
|
622
|
+
return `TinyFish is connected. Open ${client.displayName} to start using it.`;
|
|
623
|
+
}
|
|
624
|
+
// Runs after `completed` settled: failures warn and report post_install_failed, exit 0.
|
|
625
|
+
function runPostInstallSteps(client, options, state, telemetry, useKeyAuth) {
|
|
626
|
+
try {
|
|
583
627
|
state.stage = "cli_install";
|
|
584
628
|
installTinyFishCli();
|
|
585
629
|
telemetry.track("checkpoint", { phase: "cli_installed" });
|
|
@@ -596,15 +640,24 @@ async function connectNativeMcpClient(client, options) {
|
|
|
596
640
|
if (client.postConnectNote)
|
|
597
641
|
errLine(client.postConnectNote);
|
|
598
642
|
if (!options.launch) {
|
|
599
|
-
errLine(
|
|
600
|
-
settle(state, telemetry, "completed");
|
|
643
|
+
errLine(connectedLine(client, useKeyAuth));
|
|
601
644
|
return;
|
|
602
645
|
}
|
|
603
646
|
state.stage = "walkthrough_launch";
|
|
604
|
-
// Install already done; walkthrough Ctrl+C still counts completed.
|
|
605
647
|
launchNativeMcpClient(client);
|
|
606
|
-
|
|
607
|
-
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
reportPostInstallFailure(client.connectClient, client.displayName, state, telemetry, error);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/** Warn + report post_install_failed; interrupts are abandonment, not failure. */
|
|
654
|
+
function reportPostInstallFailure(connectClient, displayName, state, telemetry, error) {
|
|
655
|
+
if (!(error instanceof ConnectInterruptedError)) {
|
|
656
|
+
telemetry.track("post_install_failed", { failedStage: state.stage });
|
|
657
|
+
errLine(`The ${displayName} MCP connection succeeded, but a finishing step ` +
|
|
658
|
+
`(${state.stage}) failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
659
|
+
}
|
|
660
|
+
errLine(`Finish setup with: npx -y @tiny-fish/cli@latest connect ${connectClient}`);
|
|
608
661
|
}
|
|
609
662
|
export async function connectClaudeCode(options) {
|
|
610
663
|
await connectNativeMcpClient(CLAUDE_CODE, options);
|
|
@@ -619,7 +672,7 @@ export async function connectOpencode(options) {
|
|
|
619
672
|
await connectNativeMcpClient(OPENCODE, options);
|
|
620
673
|
}
|
|
621
674
|
export async function connectOpenClaw(options) {
|
|
622
|
-
const telemetry = createConnectTelemetry(options.mcpUrl, "openclaw");
|
|
675
|
+
const telemetry = createConnectTelemetry(options.mcpUrl, "openclaw", options);
|
|
623
676
|
const state = { stage: "prerequisite_check", settled: false };
|
|
624
677
|
await runGuarded(state, telemetry, () => {
|
|
625
678
|
telemetry.track("started");
|
|
@@ -668,20 +721,24 @@ export async function connectOpenClaw(options) {
|
|
|
668
721
|
telemetry.track("checkpoint", { phase: "authenticated" });
|
|
669
722
|
saveConnectContext("openclaw", telemetry.attemptId);
|
|
670
723
|
errLine(UPGRADE_HINT);
|
|
724
|
+
// For openclaw the skill install and auth ARE functional; only the walkthrough is cosmetic.
|
|
725
|
+
settle(state, telemetry, "completed");
|
|
671
726
|
if (!options.launch) {
|
|
672
727
|
errLine("TinyFish is connected. Open OpenClaw to start using it.");
|
|
673
|
-
settle(state, telemetry, "completed");
|
|
674
728
|
return;
|
|
675
729
|
}
|
|
676
730
|
state.stage = "walkthrough_launch";
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
731
|
+
try {
|
|
732
|
+
launchOpenClawWalkthrough();
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
reportPostInstallFailure("openclaw", "OpenClaw", state, telemetry, error);
|
|
736
|
+
}
|
|
680
737
|
});
|
|
681
738
|
}
|
|
682
739
|
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
683
740
|
export async function connectCursor(options) {
|
|
684
|
-
const telemetry = createConnectTelemetry(options.mcpUrl, "cursor", options
|
|
741
|
+
const telemetry = createConnectTelemetry(options.mcpUrl, "cursor", options);
|
|
685
742
|
const state = { stage: "prerequisite_check", settled: false };
|
|
686
743
|
await runGuarded(state, telemetry, async () => {
|
|
687
744
|
telemetry.track("started");
|
|
@@ -784,7 +841,7 @@ export function registerConnect(program) {
|
|
|
784
841
|
if (options.apiKey && !validateKeyFormat(options.apiKey)) {
|
|
785
842
|
throw new Error("Invalid --api-key value");
|
|
786
843
|
}
|
|
787
|
-
|
|
844
|
+
let mcpUrl = options.url ?? DEFAULT_MCP_URL;
|
|
788
845
|
if (mcpUrl.startsWith("-")) {
|
|
789
846
|
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
790
847
|
}
|
|
@@ -798,6 +855,16 @@ export function registerConnect(program) {
|
|
|
798
855
|
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
799
856
|
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
800
857
|
}
|
|
858
|
+
// Page-minted attempt id rides in on --url. Strip before the URL becomes the MCP
|
|
859
|
+
// base — registration re-stamps it, and nothing else may carry it.
|
|
860
|
+
const pageAttemptId = parsedUrl.searchParams.get("connect_attempt_id");
|
|
861
|
+
let attemptId;
|
|
862
|
+
if (pageAttemptId !== null) {
|
|
863
|
+
parsedUrl.searchParams.delete("connect_attempt_id");
|
|
864
|
+
mcpUrl = parsedUrl.toString();
|
|
865
|
+
if (z.uuid().safeParse(pageAttemptId).success)
|
|
866
|
+
attemptId = pageAttemptId;
|
|
867
|
+
}
|
|
801
868
|
if (options.all) {
|
|
802
869
|
if (client) {
|
|
803
870
|
throw new Error("Pass either a client or --all, not both.");
|
|
@@ -823,7 +890,12 @@ export function registerConnect(program) {
|
|
|
823
890
|
if (!client) {
|
|
824
891
|
throw new Error("Pass a client (claude-code, codex, cursor, hermes, openclaw) or --all.");
|
|
825
892
|
}
|
|
826
|
-
const connectOptions = {
|
|
893
|
+
const connectOptions = {
|
|
894
|
+
apiKey: options.apiKey,
|
|
895
|
+
mcpUrl,
|
|
896
|
+
launch: options.launch ?? false,
|
|
897
|
+
attemptId,
|
|
898
|
+
};
|
|
827
899
|
if (client === "claude-code") {
|
|
828
900
|
await connectClaudeCode(connectOptions);
|
|
829
901
|
}
|
|
@@ -837,12 +909,12 @@ export function registerConnect(program) {
|
|
|
837
909
|
await connectOpencode(connectOptions);
|
|
838
910
|
}
|
|
839
911
|
else if (client === "openclaw") {
|
|
840
|
-
await connectOpenClaw(
|
|
912
|
+
await connectOpenClaw(connectOptions);
|
|
841
913
|
}
|
|
842
914
|
else if (client === "cursor") {
|
|
843
915
|
// `connect <client> --launch` is the shape every doc uses; connect anyway, then say
|
|
844
916
|
// why nothing launched rather than throwing away a working install.
|
|
845
|
-
await connectCursor(
|
|
917
|
+
await connectCursor(connectOptions);
|
|
846
918
|
if (connectOptions.launch) {
|
|
847
919
|
errLine("Cursor has no launchable walkthrough. Open Cursor and start chatting.");
|
|
848
920
|
}
|
package/dist/lib/harness.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// value (unenumerated, lowercase-kebab, matching MCP client_name like
|
|
4
4
|
// "claude-code"). Explicit TF_CLIENT_NAME always wins upstream in client.ts.
|
|
5
5
|
//
|
|
6
|
-
// Fingerprints verified by running scripts/harness-probe
|
|
6
|
+
// Fingerprints verified by running scripts/harness-probe.mjs (repo root) in each harness.
|
|
7
7
|
// Order matters: wrappers that embed another agent must precede the embedded
|
|
8
8
|
// one. OpenClaw drives Claude Code via `sdk-cli`, so its env carries both
|
|
9
9
|
// CLAUDECODE=1 and OPENCLAW_MCP_*; it must be checked before claude-code.
|
|
@@ -21,9 +21,7 @@ const HARNESS_FINGERPRINTS = [
|
|
|
21
21
|
// prefix so we catch a Codex run regardless of which of its vars are set.
|
|
22
22
|
{ name: "codex", matches: (env) => hasVarWithPrefix(env, "CODEX_") },
|
|
23
23
|
{ name: "hermes", matches: (env) => hasVarWithPrefix(env, "HERMES_") },
|
|
24
|
-
//
|
|
25
|
-
// the commands it shells out. Exact key, not a prefix — `OPENCODE_*` are user config inputs.
|
|
26
|
-
// Sourced from OpenCode's docs, not yet confirmed against scripts/harness-probe.
|
|
24
|
+
// Exact key, not a prefix: opencode writes OPENCODE=1 at startup, reads every OPENCODE_* as config.
|
|
27
25
|
{ name: "opencode", matches: (env) => Boolean(env["OPENCODE"]) },
|
|
28
26
|
{
|
|
29
27
|
name: "claude-code",
|