@tiny-fish/cli 0.26.1-next.226 → 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 +39 -21
- 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/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,7 +1,7 @@
|
|
|
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";
|
|
@@ -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";
|
|
@@ -360,7 +374,7 @@ export async function connectCursor(options) {
|
|
|
360
374
|
telemetry.track("started");
|
|
361
375
|
telemetry.track("checkpoint", { phase: "prerequisite_ok" });
|
|
362
376
|
state.stage = "skill_install";
|
|
363
|
-
installWebSkill(CURSOR_SKILL_TARGET);
|
|
377
|
+
installWebSkill(CURSOR_SKILL_TARGET, { verbose: options.verbose ?? false });
|
|
364
378
|
telemetry.track("checkpoint", { phase: "skill_installed" });
|
|
365
379
|
// Auth first: a key lets the config carry an X-API-Key header, removing the sign-in hand-back.
|
|
366
380
|
state.stage = "authentication";
|
|
@@ -455,6 +469,7 @@ export function registerConnect(program) {
|
|
|
455
469
|
.option("--api-key <apiKey>", "TinyFish API key to persist for the installed skill")
|
|
456
470
|
.option("--launch", "Launch the agent and start the TinyFish walkthrough")
|
|
457
471
|
.option("--url <mcpUrl>", "MCP endpoint override")
|
|
472
|
+
.option("--verbose", "Print the full npm and skills output instead of only on failure")
|
|
458
473
|
.action(async (client, options) => {
|
|
459
474
|
if (options.apiKey && !validateKeyFormat(options.apiKey)) {
|
|
460
475
|
throw new Error("Invalid --api-key value");
|
|
@@ -496,6 +511,7 @@ export function registerConnect(program) {
|
|
|
496
511
|
dryRun: options.dryRun ?? false,
|
|
497
512
|
uninstall: options.uninstall ?? false,
|
|
498
513
|
noLaunch: options.skipLaunch === true,
|
|
514
|
+
verbose: options.verbose ?? false,
|
|
499
515
|
});
|
|
500
516
|
// Preserve the 130 runGuarded set on Ctrl+C.
|
|
501
517
|
if (!process.exitCode)
|
|
@@ -519,6 +535,7 @@ export function registerConnect(program) {
|
|
|
519
535
|
dryRun: false,
|
|
520
536
|
uninstall: false,
|
|
521
537
|
pick: humanInitiated,
|
|
538
|
+
verbose: options.verbose ?? false,
|
|
522
539
|
});
|
|
523
540
|
// Preserve the 130 runGuarded set on Ctrl+C.
|
|
524
541
|
if (!process.exitCode)
|
|
@@ -532,6 +549,7 @@ export function registerConnect(program) {
|
|
|
532
549
|
attemptId,
|
|
533
550
|
// A key that the install cannot use leaves only a browser hop no agent can finish.
|
|
534
551
|
keyAuthOnly: !detectHumanInitiated() && !!validatedApiKey(options.apiKey),
|
|
552
|
+
verbose: options.verbose ?? false,
|
|
535
553
|
};
|
|
536
554
|
if (client === "claude-code") {
|
|
537
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"],
|
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
|
/**
|