@tiny-fish/cli 0.26.1-next.225 → 0.26.1-next.227
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 +8 -0
- package/dist/commands/connect.js +71 -24
- package/dist/lib/cli-install.d.ts +3 -2
- package/dist/lib/cli-install.js +2 -3
- package/dist/lib/connect-all.d.ts +2 -0
- package/dist/lib/connect-all.js +25 -10
- package/dist/lib/connect-clients.d.ts +2 -0
- package/dist/lib/connect-clients.js +3 -0
- package/dist/lib/connect-runtime.d.ts +1 -0
- package/dist/lib/connect-runtime.js +1 -1
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +8 -1
- package/dist/lib/skill-install.d.ts +1 -1
- package/dist/lib/skill-install.js +7 -5
- package/package.json +1 -1
|
@@ -15,6 +15,10 @@ interface NativeConnectOptions {
|
|
|
15
15
|
hoistedCliInstall?: TakeHoistedCliInstall;
|
|
16
16
|
/** Lets `--all` warn in its summary instead of all-green. */
|
|
17
17
|
onPostInstallFailed?: () => void;
|
|
18
|
+
/** Inherit subprocess stdio instead of replaying it only on failure. */
|
|
19
|
+
verbose?: boolean;
|
|
20
|
+
/** Set by `--all`: repeated outro lines print once, after the summary. */
|
|
21
|
+
deferOutro?: (line: string) => void;
|
|
18
22
|
}
|
|
19
23
|
export declare function connectClaudeCode(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
|
|
20
24
|
export declare function connectCodex(options: NativeConnectOptions): Promise<ConnectAuthMode | undefined>;
|
|
@@ -29,12 +33,16 @@ export declare function connectOpenClaw(options: {
|
|
|
29
33
|
attemptId?: string;
|
|
30
34
|
/** Set by `--all`: the CLI install already ran once, upfront. */
|
|
31
35
|
hoistedCliInstall?: TakeHoistedCliInstall;
|
|
36
|
+
verbose?: boolean;
|
|
37
|
+
/** Set by `--all`: repeated outro lines print once, after the summary. */
|
|
38
|
+
deferOutro?: (line: string) => void;
|
|
32
39
|
}): Promise<void>;
|
|
33
40
|
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
34
41
|
export declare function connectCursor(options: {
|
|
35
42
|
apiKey?: string;
|
|
36
43
|
mcpUrl: string;
|
|
37
44
|
attemptId?: string;
|
|
45
|
+
verbose?: boolean;
|
|
38
46
|
}): Promise<void>;
|
|
39
47
|
export declare function launchAgent(client: AgentClient): void;
|
|
40
48
|
export declare function registerConnect(program: Command): void;
|
package/dist/commands/connect.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
2
|
import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from "../lib/auth.js";
|
|
3
3
|
import { CLAUDE_CODE, CODEX, CURSOR_SKILL_TARGET, GROK, HERMES, OPENCLAW, OPENCLAW_SKILL_INSTALL_ARGS, OPENCODE, launchNativeMcpClient, launchOpenClawWalkthrough, openExternalUrl, } from "../lib/connect-clients.js";
|
|
4
|
-
import { HoistedFailureReportedError, installTinyFishCli,
|
|
4
|
+
import { captureStdio, capturedOutput, HoistedFailureReportedError, installTinyFishCli, replay, } from "../lib/cli-install.js";
|
|
5
5
|
import { ensureCliAuthenticated } from "../lib/connect-auth.js";
|
|
6
6
|
import { installWebSkill } from "../lib/skill-install.js";
|
|
7
7
|
import { ConnectInterruptedError, ConnectStepError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, spawnStepError, throwIfInterrupted, NON_INTERACTIVE_TIMEOUT_MS, } from "../lib/connect-runtime.js";
|
|
8
|
-
import { cursorInstallDeeplink, cursorMcpPath, writeCursorMcpConfig, } from "../lib/cursor-config.js";
|
|
8
|
+
import { cursorInstallDeeplink, cursorMcpPath, readCursorTinyfishEntry, writeCursorMcpConfig, } from "../lib/cursor-config.js";
|
|
9
9
|
import { runConnectAll } from "../lib/connect-all.js";
|
|
10
10
|
import { AuthMode } from "../lib/harness-detect.js";
|
|
11
11
|
import { detectHumanInitiated } from "../lib/harness.js";
|
|
@@ -29,7 +29,7 @@ function removeExistingRegistration(client, removal) {
|
|
|
29
29
|
throw spawnStepError(`Could not remove existing ${removal.label}: ${details}`, result);
|
|
30
30
|
}
|
|
31
31
|
/** false: fall back to OAuth — a config naming an unset variable authenticates nothing. */
|
|
32
|
-
function exportApiKey(apiKey, displayName) {
|
|
32
|
+
function exportApiKey(apiKey, displayName, deferOutro) {
|
|
33
33
|
try {
|
|
34
34
|
persistApiKeyToEnvironment(apiKey);
|
|
35
35
|
}
|
|
@@ -39,8 +39,14 @@ function exportApiKey(apiKey, displayName) {
|
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
process.env["TINYFISH_API_KEY"] = apiKey;
|
|
42
|
-
|
|
43
|
-
"
|
|
42
|
+
if (deferOutro) {
|
|
43
|
+
deferOutro("Exported TINYFISH_API_KEY to your shell profile — restart your agents (or open a new " +
|
|
44
|
+
"terminal) before use.");
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
errLine(`Exported TINYFISH_API_KEY to your shell profile — ${displayName} reads the key from there, ` +
|
|
48
|
+
"so restart it (or open a new terminal) before use.");
|
|
49
|
+
}
|
|
44
50
|
return true;
|
|
45
51
|
}
|
|
46
52
|
async function connectNativeMcpClient(client, options) {
|
|
@@ -57,7 +63,8 @@ async function connectNativeMcpClient(client, options) {
|
|
|
57
63
|
? { key: storedKey, spec: client.keyAuth }
|
|
58
64
|
: undefined;
|
|
59
65
|
// Before the removals: a failed export must not strand the old registration.
|
|
60
|
-
if (keyedAdd?.spec.viaEnv &&
|
|
66
|
+
if (keyedAdd?.spec.viaEnv &&
|
|
67
|
+
!exportApiKey(keyedAdd.key, client.displayName, options.deferOutro)) {
|
|
61
68
|
keyedAdd = undefined;
|
|
62
69
|
}
|
|
63
70
|
const useKeyAuth = !!keyedAdd;
|
|
@@ -89,9 +96,15 @@ async function connectNativeMcpClient(client, options) {
|
|
|
89
96
|
const addArgs = keyedAdd
|
|
90
97
|
? keyedAdd.spec.addArgs(mcpUrl.toString(), keyedAdd.key)
|
|
91
98
|
: client.addArgs(mcpUrl.toString(), options.mcpUrl);
|
|
92
|
-
|
|
99
|
+
// Capturing an inline-OAuth add hides the sign-in URL and hangs.
|
|
100
|
+
const addResult = spawn.sync(client.command, addArgs, client.nonInteractiveAdd || keyedAdd
|
|
101
|
+
? captureStdio(options.verbose ?? false)
|
|
102
|
+
: { stdio: "inherit" });
|
|
93
103
|
if (addResult.error || addResult.status !== 0) {
|
|
94
|
-
|
|
104
|
+
// Built first: its interrupt check must run before any replay.
|
|
105
|
+
const error = spawnStepError(`Could not add TinyFish to ${client.displayName}`, addResult);
|
|
106
|
+
replay(capturedOutput(addResult));
|
|
107
|
+
throw error;
|
|
95
108
|
}
|
|
96
109
|
telemetry.track("checkpoint", { phase: "registered" });
|
|
97
110
|
// Codex runs OAuth inside `mcp add` and exits 0 either way, so the recorded state decides
|
|
@@ -167,7 +180,7 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
|
|
|
167
180
|
}
|
|
168
181
|
if (client.skillAgent) {
|
|
169
182
|
attempt("skill_install", () => {
|
|
170
|
-
installWebSkill(client);
|
|
183
|
+
installWebSkill(client, { verbose: options.verbose ?? false });
|
|
171
184
|
telemetry.track("checkpoint", { phase: "skill_installed" });
|
|
172
185
|
});
|
|
173
186
|
}
|
|
@@ -177,11 +190,12 @@ function runPostInstallSteps(client, options, state, telemetry, signInDeferred)
|
|
|
177
190
|
ensureCliAuthenticated(client.connectClient, options.apiKey);
|
|
178
191
|
telemetry.track("checkpoint", { phase: "authenticated" });
|
|
179
192
|
});
|
|
180
|
-
errLine(UPGRADE_HINT);
|
|
181
193
|
if (client.postConnectNote)
|
|
182
194
|
errLine(client.postConnectNote);
|
|
183
195
|
if (!options.launch) {
|
|
184
|
-
|
|
196
|
+
// Under --all the summary replaces this; sign-in hints still print.
|
|
197
|
+
if (signInDeferred || !options.deferOutro)
|
|
198
|
+
errLine(connectedLine(client, signInDeferred));
|
|
185
199
|
}
|
|
186
200
|
else {
|
|
187
201
|
attempt("walkthrough_launch", () => {
|
|
@@ -219,8 +233,7 @@ function runCliInstallStep(options, telemetry) {
|
|
|
219
233
|
}
|
|
220
234
|
return;
|
|
221
235
|
}
|
|
222
|
-
|
|
223
|
-
installTinyFishCli({ verbose: true });
|
|
236
|
+
installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
|
|
224
237
|
telemetry.track("checkpoint", { phase: "cli_installed" });
|
|
225
238
|
}
|
|
226
239
|
function finishSetupHint(connectClient) {
|
|
@@ -282,17 +295,17 @@ export async function connectOpenClaw(options) {
|
|
|
282
295
|
}
|
|
283
296
|
else if (options.cliInstall !== false) {
|
|
284
297
|
state.stage = "cli_install";
|
|
285
|
-
installTinyFishCli({ verbose: true });
|
|
298
|
+
installTinyFishCli({ verbose: options.verbose ?? false, announce: true });
|
|
286
299
|
telemetry.track("checkpoint", { phase: "cli_installed" });
|
|
287
300
|
}
|
|
288
301
|
state.stage = "skill_install";
|
|
289
302
|
errLine("Installing the TinyFish skill in OpenClaw...");
|
|
290
|
-
const skillInstallResult = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS,
|
|
291
|
-
stdio: "inherit",
|
|
292
|
-
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
293
|
-
});
|
|
303
|
+
const skillInstallResult = spawn.sync("openclaw", OPENCLAW_SKILL_INSTALL_ARGS, captureStdio(options.verbose ?? false));
|
|
294
304
|
if (skillInstallResult.error || skillInstallResult.status !== 0) {
|
|
295
|
-
|
|
305
|
+
// Built first: its interrupt check must run before any replay.
|
|
306
|
+
const error = spawnStepError("Could not install the TinyFish skill in OpenClaw", skillInstallResult);
|
|
307
|
+
replay(capturedOutput(skillInstallResult));
|
|
308
|
+
throw error;
|
|
296
309
|
}
|
|
297
310
|
telemetry.track("checkpoint", { phase: "skill_installed" });
|
|
298
311
|
// Stores the key before probing, so a passed --api-key never reaches an interactive login.
|
|
@@ -300,11 +313,12 @@ export async function connectOpenClaw(options) {
|
|
|
300
313
|
ensureCliAuthenticated("openclaw", options.apiKey);
|
|
301
314
|
telemetry.track("checkpoint", { phase: "authenticated" });
|
|
302
315
|
saveConnectContext("openclaw", telemetry.attemptId);
|
|
303
|
-
errLine(UPGRADE_HINT);
|
|
304
316
|
// For openclaw the skill install and auth ARE functional; only the walkthrough is cosmetic.
|
|
305
317
|
settle(state, telemetry, "completed");
|
|
306
318
|
if (!options.launch) {
|
|
307
|
-
|
|
319
|
+
// Under --all the summary already lists OpenClaw.
|
|
320
|
+
if (!options.deferOutro)
|
|
321
|
+
errLine("TinyFish is connected. Open OpenClaw to start using it.");
|
|
308
322
|
return;
|
|
309
323
|
}
|
|
310
324
|
state.stage = "walkthrough_launch";
|
|
@@ -319,7 +333,36 @@ export async function connectOpenClaw(options) {
|
|
|
319
333
|
}
|
|
320
334
|
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
321
335
|
export async function connectCursor(options) {
|
|
322
|
-
|
|
336
|
+
let fallbackAttemptId;
|
|
337
|
+
const registeredUrl = readCursorTinyfishEntry().url;
|
|
338
|
+
if (registeredUrl) {
|
|
339
|
+
try {
|
|
340
|
+
const registered = new URL(registeredUrl);
|
|
341
|
+
const requested = new URL(options.mcpUrl);
|
|
342
|
+
const registeredId = registered.searchParams.get("connect_attempt_id");
|
|
343
|
+
const registeredSource = registered.searchParams.get("source");
|
|
344
|
+
const registeredClient = registered.searchParams.get("client");
|
|
345
|
+
for (const key of ["source", "client", "connect_attempt_id"]) {
|
|
346
|
+
registered.searchParams.delete(key);
|
|
347
|
+
requested.searchParams.delete(key);
|
|
348
|
+
}
|
|
349
|
+
registered.searchParams.sort();
|
|
350
|
+
requested.searchParams.sort();
|
|
351
|
+
if (registered.toString() === requested.toString() &&
|
|
352
|
+
registeredSource === CONNECT_SOURCE &&
|
|
353
|
+
registeredClient === "cursor" &&
|
|
354
|
+
z.uuid().safeParse(registeredId).success) {
|
|
355
|
+
fallbackAttemptId = registeredId ?? undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
// A hand-edited invalid URL is repaired below with a fresh attempt id.
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const telemetry = createConnectTelemetry(options.mcpUrl, "cursor", {
|
|
363
|
+
...options,
|
|
364
|
+
fallbackAttemptId,
|
|
365
|
+
});
|
|
323
366
|
// Never degraded: no sign-in command to be missing. False, not absent, so a `= false`
|
|
324
367
|
// filter still catches Cursor.
|
|
325
368
|
const state = {
|
|
@@ -331,7 +374,7 @@ export async function connectCursor(options) {
|
|
|
331
374
|
telemetry.track("started");
|
|
332
375
|
telemetry.track("checkpoint", { phase: "prerequisite_ok" });
|
|
333
376
|
state.stage = "skill_install";
|
|
334
|
-
installWebSkill(CURSOR_SKILL_TARGET);
|
|
377
|
+
installWebSkill(CURSOR_SKILL_TARGET, { verbose: options.verbose ?? false });
|
|
335
378
|
telemetry.track("checkpoint", { phase: "skill_installed" });
|
|
336
379
|
// Auth first: a key lets the config carry an X-API-Key header, removing the sign-in hand-back.
|
|
337
380
|
state.stage = "authentication";
|
|
@@ -347,7 +390,7 @@ export async function connectCursor(options) {
|
|
|
347
390
|
const mcpUrl = new URL(options.mcpUrl);
|
|
348
391
|
mcpUrl.searchParams.set("source", CONNECT_SOURCE);
|
|
349
392
|
mcpUrl.searchParams.set("client", "cursor");
|
|
350
|
-
|
|
393
|
+
mcpUrl.searchParams.set("connect_attempt_id", telemetry.attemptId);
|
|
351
394
|
const result = writeCursorMcpConfig(mcpUrl.toString(), resolvedKey);
|
|
352
395
|
if (result.status === "corrupt_skip") {
|
|
353
396
|
throw new ConnectStepError(`Could not update ${cursorMcpPath()}: existing file could not be read or is not valid JSON (${result.error}). ` +
|
|
@@ -426,6 +469,7 @@ export function registerConnect(program) {
|
|
|
426
469
|
.option("--api-key <apiKey>", "TinyFish API key to persist for the installed skill")
|
|
427
470
|
.option("--launch", "Launch the agent and start the TinyFish walkthrough")
|
|
428
471
|
.option("--url <mcpUrl>", "MCP endpoint override")
|
|
472
|
+
.option("--verbose", "Print the full npm and skills output instead of only on failure")
|
|
429
473
|
.action(async (client, options) => {
|
|
430
474
|
if (options.apiKey && !validateKeyFormat(options.apiKey)) {
|
|
431
475
|
throw new Error("Invalid --api-key value");
|
|
@@ -467,6 +511,7 @@ export function registerConnect(program) {
|
|
|
467
511
|
dryRun: options.dryRun ?? false,
|
|
468
512
|
uninstall: options.uninstall ?? false,
|
|
469
513
|
noLaunch: options.skipLaunch === true,
|
|
514
|
+
verbose: options.verbose ?? false,
|
|
470
515
|
});
|
|
471
516
|
// Preserve the 130 runGuarded set on Ctrl+C.
|
|
472
517
|
if (!process.exitCode)
|
|
@@ -490,6 +535,7 @@ export function registerConnect(program) {
|
|
|
490
535
|
dryRun: false,
|
|
491
536
|
uninstall: false,
|
|
492
537
|
pick: humanInitiated,
|
|
538
|
+
verbose: options.verbose ?? false,
|
|
493
539
|
});
|
|
494
540
|
// Preserve the 130 runGuarded set on Ctrl+C.
|
|
495
541
|
if (!process.exitCode)
|
|
@@ -503,6 +549,7 @@ export function registerConnect(program) {
|
|
|
503
549
|
attemptId,
|
|
504
550
|
// A key that the install cannot use leaves only a browser hop no agent can finish.
|
|
505
551
|
keyAuthOnly: !detectHumanInitiated() && !!validatedApiKey(options.apiKey),
|
|
552
|
+
verbose: options.verbose ?? false,
|
|
506
553
|
};
|
|
507
554
|
if (client === "claude-code") {
|
|
508
555
|
await connectClaudeCode(connectOptions);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export declare const SKILL_INSTALL_TIMEOUT_MS = 120000;
|
|
2
|
-
export declare const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
|
|
3
2
|
export declare const STEP_MAX_BUFFER: number;
|
|
4
3
|
/** Quiet captures subprocess output and replays it only on failure. */
|
|
5
4
|
export type InstallOptions = {
|
|
6
5
|
verbose: boolean;
|
|
6
|
+
/** Progress line even when quiet; defaults to `verbose`. */
|
|
7
|
+
announce?: boolean;
|
|
7
8
|
};
|
|
8
9
|
/** Outcome of `--all`'s one npm install; error keeps ConnectStepError.failureReason. */
|
|
9
10
|
export type HoistedCliInstall = {
|
|
@@ -39,4 +40,4 @@ export declare function capturedOutput(result: {
|
|
|
39
40
|
}): string;
|
|
40
41
|
/** A verbose run captured nothing, so this is a no-op. */
|
|
41
42
|
export declare function replay(output: string): void;
|
|
42
|
-
export declare function installTinyFishCli({ verbose }: InstallOptions): void;
|
|
43
|
+
export declare function installTinyFishCli({ verbose, announce }: InstallOptions): void;
|
package/dist/lib/cli-install.js
CHANGED
|
@@ -5,7 +5,6 @@ import { installRoot } from "./install-root.js";
|
|
|
5
5
|
import { errLine } from "./output.js";
|
|
6
6
|
export const SKILL_INSTALL_TIMEOUT_MS = 120_000;
|
|
7
7
|
const TINYFISH_CLI_INSTALL_SPEC = `${TINYFISH_CLI_PACKAGE}@latest`;
|
|
8
|
-
export const UPGRADE_HINT = "Run `tinyfish upgrade` any time to update the CLI and skill.";
|
|
9
8
|
// spawnSync caps piped output at 1 MiB by default.
|
|
10
9
|
export const STEP_MAX_BUFFER = 10 * 1024 * 1024;
|
|
11
10
|
/** Carried failure an earlier harness already reported. */
|
|
@@ -28,8 +27,8 @@ export function replay(output) {
|
|
|
28
27
|
if (output.trim())
|
|
29
28
|
errLine(output.trimEnd());
|
|
30
29
|
}
|
|
31
|
-
export function installTinyFishCli({ verbose }) {
|
|
32
|
-
if (verbose)
|
|
30
|
+
export function installTinyFishCli({ verbose, announce }) {
|
|
31
|
+
if (announce ?? verbose)
|
|
33
32
|
errLine("Installing the TinyFish CLI...");
|
|
34
33
|
// Without the prefix, a curl install upgrades npm's global tree and never the PATH binary.
|
|
35
34
|
const root = installRoot();
|
|
@@ -7,5 +7,7 @@ export interface ConnectAllOptions {
|
|
|
7
7
|
noLaunch?: boolean;
|
|
8
8
|
/** Bare connect: offer the checkbox picker over the detected harnesses. */
|
|
9
9
|
pick?: boolean;
|
|
10
|
+
/** Inherit subprocess stdio instead of replaying it only on failure. */
|
|
11
|
+
verbose?: boolean;
|
|
10
12
|
}
|
|
11
13
|
export declare function runConnectAll(opts: ConnectAllOptions): Promise<number>;
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -11,7 +11,7 @@ import { commandOnPath, detectInstalledHarnesses, HARNESS_COMMANDS, HARNESS_DISP
|
|
|
11
11
|
import { detectHumanInitiated } from "./harness.js";
|
|
12
12
|
import { emitNotice } from "./notice.js";
|
|
13
13
|
import { removeCursorMcpServer, planCursorWrite } from "./cursor-config.js";
|
|
14
|
-
import { errLine, outLine } from "./output.js";
|
|
14
|
+
import { errLine, outLine, setErrIndent } from "./output.js";
|
|
15
15
|
import { sendSetupCompleted } from "./setup-telemetry.js";
|
|
16
16
|
import { verifyMcpAuth, verifyMcpHealth } from "./verify.js";
|
|
17
17
|
// `codex mcp add` succeeds unauthenticated; OAuth lands at first tool use.
|
|
@@ -23,6 +23,7 @@ const TOO_OLD_REASONS = new Set([
|
|
|
23
23
|
"harness_command_unsupported",
|
|
24
24
|
]);
|
|
25
25
|
const AUTH_CHECK_CAPABLE = new Set(["cursor", "openclaw"]);
|
|
26
|
+
const STEP_INDENT = " ";
|
|
26
27
|
// Descriptor-derived so a harness that cannot carry a key is skipped, not failed, when headless.
|
|
27
28
|
// Cursor and OpenClaw hold the key themselves rather than handing it to a harness `mcp add`.
|
|
28
29
|
const KEY_AUTH_CAPABLE = new Set([
|
|
@@ -44,7 +45,7 @@ const UNINSTALL_POINTERS = {
|
|
|
44
45
|
// OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
|
|
45
46
|
opencode: "remove the tinyfish entry from ~/.config/opencode/opencode.json",
|
|
46
47
|
};
|
|
47
|
-
async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall, onPostInstallFailed) {
|
|
48
|
+
async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall, onPostInstallFailed, deferOutro) {
|
|
48
49
|
const base = {
|
|
49
50
|
apiKey: opts.apiKey,
|
|
50
51
|
mcpUrl: opts.mcpUrl,
|
|
@@ -52,6 +53,8 @@ async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall,
|
|
|
52
53
|
keyAuthOnly,
|
|
53
54
|
hoistedCliInstall,
|
|
54
55
|
onPostInstallFailed,
|
|
56
|
+
verbose: opts.verbose,
|
|
57
|
+
deferOutro,
|
|
55
58
|
};
|
|
56
59
|
if (harness === "claude-code")
|
|
57
60
|
return connectClaudeCode(base);
|
|
@@ -66,6 +69,8 @@ async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall,
|
|
|
66
69
|
mcpUrl: opts.mcpUrl,
|
|
67
70
|
launch: false,
|
|
68
71
|
hoistedCliInstall,
|
|
72
|
+
verbose: opts.verbose,
|
|
73
|
+
deferOutro,
|
|
69
74
|
});
|
|
70
75
|
return undefined;
|
|
71
76
|
}
|
|
@@ -74,17 +79,17 @@ async function runHarnessConnect(harness, opts, keyAuthOnly, hoistedCliInstall,
|
|
|
74
79
|
if (harness === "grok")
|
|
75
80
|
return connectGrok(base);
|
|
76
81
|
if (harness === "cursor") {
|
|
77
|
-
await connectCursor({ apiKey: opts.apiKey, mcpUrl: opts.mcpUrl });
|
|
82
|
+
await connectCursor({ apiKey: opts.apiKey, mcpUrl: opts.mcpUrl, verbose: opts.verbose });
|
|
78
83
|
return undefined;
|
|
79
84
|
}
|
|
80
85
|
// Explicit: a new harness reaching an untested branch must fail, not write Cursor's config.
|
|
81
86
|
throw new Error(`No installer is wired for ${harness}`);
|
|
82
87
|
}
|
|
83
88
|
/** Undefined means Ctrl+C: the caller stops the whole run. */
|
|
84
|
-
function runHoistedCliInstall() {
|
|
89
|
+
function runHoistedCliInstall(verbose) {
|
|
85
90
|
const startedAt = Date.now();
|
|
86
91
|
try {
|
|
87
|
-
installTinyFishCli({ verbose: true });
|
|
92
|
+
installTinyFishCli({ verbose, announce: true });
|
|
88
93
|
// Clamped: a backward clock step would send a route-rejected negative.
|
|
89
94
|
return { ok: true, durationMs: Math.max(0, Date.now() - startedAt) };
|
|
90
95
|
}
|
|
@@ -141,7 +146,7 @@ function uninstallHarness(harness, configPath) {
|
|
|
141
146
|
}
|
|
142
147
|
return { ...detection, outcome: "uninstalled" };
|
|
143
148
|
}
|
|
144
|
-
async function processHarness(detection, opts, prompt, hoistedCliInstall) {
|
|
149
|
+
async function processHarness(detection, opts, prompt, hoistedCliInstall, deferOutro) {
|
|
145
150
|
const { harness, configPath, detected } = detection;
|
|
146
151
|
const base = { harness, detected, configPath, installed: false };
|
|
147
152
|
if (!detected)
|
|
@@ -188,7 +193,7 @@ async function processHarness(detection, opts, prompt, hoistedCliInstall) {
|
|
|
188
193
|
// Headless: an install too old for key auth must fail, not open a browser.
|
|
189
194
|
authMode = await runHarnessConnect(harness, opts, !isTTY, hoistedCliInstall, () => {
|
|
190
195
|
postInstallFailed = true;
|
|
191
|
-
});
|
|
196
|
+
}, deferOutro);
|
|
192
197
|
}
|
|
193
198
|
catch (e) {
|
|
194
199
|
// Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
|
|
@@ -316,7 +321,7 @@ function searchRetry(query) {
|
|
|
316
321
|
}
|
|
317
322
|
async function runCliFallback(opts, prompt) {
|
|
318
323
|
try {
|
|
319
|
-
installTinyFishCli({ verbose: true });
|
|
324
|
+
installTinyFishCli({ verbose: opts.verbose ?? false, announce: true });
|
|
320
325
|
}
|
|
321
326
|
catch (error) {
|
|
322
327
|
if (error instanceof ConnectInterruptedError)
|
|
@@ -426,7 +431,7 @@ export async function runConnectAll(opts) {
|
|
|
426
431
|
let takeHoistedCli;
|
|
427
432
|
if (cliNeeded) {
|
|
428
433
|
errLine("First: the TinyFish CLI, shared by every harness.");
|
|
429
|
-
const outcome = runHoistedCliInstall();
|
|
434
|
+
const outcome = runHoistedCliInstall(opts.verbose ?? false);
|
|
430
435
|
if (!outcome) {
|
|
431
436
|
errLine("Setup interrupted — run the command again to finish.");
|
|
432
437
|
return 130;
|
|
@@ -439,6 +444,8 @@ export async function runConnectAll(opts) {
|
|
|
439
444
|
return { outcome, firstTake };
|
|
440
445
|
};
|
|
441
446
|
}
|
|
447
|
+
// A Set dedupes: Codex and Grok defer the same export line.
|
|
448
|
+
const outro = new Set();
|
|
442
449
|
const results = [];
|
|
443
450
|
const stepTotal = detections.length - deselected.size;
|
|
444
451
|
let step = 0;
|
|
@@ -457,7 +464,8 @@ export async function runConnectAll(opts) {
|
|
|
457
464
|
errLine(`step ${step} of ${stepTotal} — ${DISPLAY_NAMES[harness]}`);
|
|
458
465
|
let result;
|
|
459
466
|
try {
|
|
460
|
-
|
|
467
|
+
setErrIndent(STEP_INDENT);
|
|
468
|
+
result = await processHarness(detection, opts, prompt, takeHoistedCli, (line) => outro.add(line));
|
|
461
469
|
}
|
|
462
470
|
catch (e) {
|
|
463
471
|
// Isolation: one harness throwing never stops the others.
|
|
@@ -471,6 +479,10 @@ export async function runConnectAll(opts) {
|
|
|
471
479
|
fixCommand: `tinyfish connect ${harness}`,
|
|
472
480
|
};
|
|
473
481
|
}
|
|
482
|
+
finally {
|
|
483
|
+
// The summary and the outro sit outside any step.
|
|
484
|
+
setErrIndent("");
|
|
485
|
+
}
|
|
474
486
|
results.push(result);
|
|
475
487
|
// Ctrl+C means stop, not continue to the next harness.
|
|
476
488
|
if (result.outcome === "interrupted")
|
|
@@ -495,6 +507,9 @@ export async function runConnectAll(opts) {
|
|
|
495
507
|
outLine("TinyFish setup summary:");
|
|
496
508
|
for (const result of results)
|
|
497
509
|
outLine(summaryLine(result, opts.uninstall));
|
|
510
|
+
// Before the launch handover: anything printed after it is never seen.
|
|
511
|
+
for (const line of outro)
|
|
512
|
+
errLine(line);
|
|
498
513
|
if (!results.some(actionable)) {
|
|
499
514
|
if (!opts.dryRun && !opts.uninstall) {
|
|
500
515
|
try {
|
|
@@ -14,6 +14,8 @@ export interface NativeMcpClient extends SupportedCommand {
|
|
|
14
14
|
connectClient: Exclude<AgentClient, "openclaw">;
|
|
15
15
|
/** Omitted where the `skills` CLI has no agent for this harness (Grok ships its own plugin). */
|
|
16
16
|
skillAgent?: SkillAgent;
|
|
17
|
+
/** Capturable: this `mcp add` never runs OAuth inline; unset stays inherited. */
|
|
18
|
+
nonInteractiveAdd?: true;
|
|
17
19
|
/** Replaces `addArgs` when a stored key is available: key auth removes the OAuth hop. */
|
|
18
20
|
keyAuth?: {
|
|
19
21
|
/** Key reaches the client through TINYFISH_API_KEY, not argv. */
|
|
@@ -84,6 +84,7 @@ export const CLAUDE_CODE = {
|
|
|
84
84
|
command: "claude",
|
|
85
85
|
connectClient: "claude-code",
|
|
86
86
|
skillAgent: "claude-code",
|
|
87
|
+
nonInteractiveAdd: true,
|
|
87
88
|
displayName: "Claude Code",
|
|
88
89
|
supportCheck: {
|
|
89
90
|
args: ["mcp", "--help"],
|
|
@@ -171,6 +172,7 @@ const grokAddArgs = (mcpUrl) => [
|
|
|
171
172
|
export const GROK = {
|
|
172
173
|
command: "grok",
|
|
173
174
|
connectClient: "grok",
|
|
175
|
+
nonInteractiveAdd: true,
|
|
174
176
|
displayName: "Grok",
|
|
175
177
|
supportCheck: {
|
|
176
178
|
args: ["mcp", "add", "--help"],
|
|
@@ -240,6 +242,7 @@ export const OPENCODE = {
|
|
|
240
242
|
command: "opencode",
|
|
241
243
|
connectClient: "opencode",
|
|
242
244
|
skillAgent: "opencode",
|
|
245
|
+
nonInteractiveAdd: true,
|
|
243
246
|
displayName: "OpenCode",
|
|
244
247
|
supportCheck: {
|
|
245
248
|
args: ["mcp", "add", "--help"],
|
|
@@ -84,5 +84,6 @@ export declare function requireCommandSupport(client: SupportedCommand): {
|
|
|
84
84
|
export declare function createConnectTelemetry(mcpUrl: string, client: AgentClient, opts?: {
|
|
85
85
|
apiKey?: string;
|
|
86
86
|
attemptId?: string;
|
|
87
|
+
fallbackAttemptId?: string;
|
|
87
88
|
}): ConnectTelemetry;
|
|
88
89
|
export {};
|
|
@@ -193,7 +193,7 @@ export function createConnectTelemetry(mcpUrl, client, opts) {
|
|
|
193
193
|
const seeded = takeSeedAttemptId();
|
|
194
194
|
// A setup-page id (via --url, the env var, or one parked by login) joins "copied the
|
|
195
195
|
// command" to this install attempt.
|
|
196
|
-
const attemptId = opts?.attemptId ?? seeded ?? randomUUID();
|
|
196
|
+
const attemptId = opts?.attemptId ?? seeded ?? opts?.fallbackAttemptId ?? randomUUID();
|
|
197
197
|
const endpoint = new URL("/api/cli/connect-event", mcpUrl).toString();
|
|
198
198
|
const pending = [];
|
|
199
199
|
let lastTrackAt;
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export declare function out(data: unknown): void;
|
|
|
7
7
|
/** The only writer for untrusted text: strips ANSI and C0/C1 controls. */
|
|
8
8
|
export declare function sanitizeLine(text: string): string;
|
|
9
9
|
export declare function outLine(line: string): void;
|
|
10
|
+
/** `connect --all` nests a harness's own lines under its step header. */
|
|
11
|
+
export declare function setErrIndent(prefix: string): void;
|
|
10
12
|
export declare function errLine(line: string): void;
|
|
11
13
|
/** Advisory stderr line, yellow on a terminal and plain everywhere else so piped output stays clean. */
|
|
12
14
|
export declare function warnLine(line: string): void;
|
package/dist/lib/output.js
CHANGED
|
@@ -20,8 +20,15 @@ export function sanitizeLine(text) {
|
|
|
20
20
|
export function outLine(line) {
|
|
21
21
|
process.stdout.write(sanitizeLine(line) + "\n");
|
|
22
22
|
}
|
|
23
|
+
let errIndent = "";
|
|
24
|
+
/** `connect --all` nests a harness's own lines under its step header. */
|
|
25
|
+
export function setErrIndent(prefix) {
|
|
26
|
+
errIndent = prefix;
|
|
27
|
+
}
|
|
23
28
|
export function errLine(line) {
|
|
24
|
-
|
|
29
|
+
const text = sanitizeLine(line);
|
|
30
|
+
// Per line, not once: a replayed capture carries newlines.
|
|
31
|
+
process.stderr.write((errIndent ? text.replace(/^(?=.)/gm, errIndent) : text) + "\n");
|
|
25
32
|
}
|
|
26
33
|
const WARN_ON = "\x1b[33m";
|
|
27
34
|
const WARN_OFF = "\x1b[39m";
|
|
@@ -3,7 +3,7 @@ import { type SkillAgent } from "./connect-clients.js";
|
|
|
3
3
|
export declare function installWebSkill(client: {
|
|
4
4
|
skillAgent?: SkillAgent;
|
|
5
5
|
displayName: string;
|
|
6
|
-
}): void;
|
|
6
|
+
}, { verbose }: Pick<InstallOptions, "verbose">): void;
|
|
7
7
|
/**
|
|
8
8
|
* Refresh the skill we installed, for the harnesses we installed it in.
|
|
9
9
|
*
|
|
@@ -70,19 +70,21 @@ function parseJson(text) {
|
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
export function installWebSkill(client) {
|
|
73
|
+
export function installWebSkill(client, { verbose }) {
|
|
74
74
|
if (!client.skillAgent)
|
|
75
75
|
return;
|
|
76
76
|
errLine(`Installing the TinyFish web skill in ${client.displayName}...`);
|
|
77
|
-
// Inherited stdio renders the spinner live as one overwriting line.
|
|
78
77
|
const add = spawn.sync("npx", skillAddArgs([client.skillAgent]), {
|
|
79
|
-
|
|
78
|
+
...captureStdio(verbose),
|
|
80
79
|
env: skillSpawnEnv(),
|
|
81
|
-
timeout: SKILL_INSTALL_TIMEOUT_MS,
|
|
82
80
|
});
|
|
83
81
|
const failed = add.error || add.status !== 0 ? add : failedListVerdict();
|
|
84
82
|
if (failed) {
|
|
85
|
-
|
|
83
|
+
// Built first: its interrupt check must run before any replay.
|
|
84
|
+
const error = spawnStepError(`Could not install the TinyFish web skill in ${client.displayName}`, failed);
|
|
85
|
+
// The add carries the failure prose; the list is JSON only.
|
|
86
|
+
replay(capturedOutput(add));
|
|
87
|
+
throw error;
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
/**
|