@tiny-fish/cli 0.29.1-next.237 → 0.29.1-next.240
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 +1 -2
- package/dist/commands/connect.js +33 -51
- package/dist/lib/connect-all.js +70 -67
- package/dist/lib/connect-clients.d.ts +18 -0
- package/dist/lib/connect-clients.js +55 -0
- package/dist/lib/connect-runtime.js +2 -1
- package/dist/lib/registration-detect.js +1 -2
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { Command } from "commander";
|
|
|
2
2
|
import { type TakeHoistedCliInstall } from "../lib/cli-install.js";
|
|
3
3
|
import { type AgentClient, type ConnectAuthMode } from "../lib/connect-runtime.js";
|
|
4
4
|
export declare const DEFAULT_MCP_URL = "https://agent.tinyfish.ai/mcp";
|
|
5
|
-
interface NativeConnectOptions {
|
|
5
|
+
export interface NativeConnectOptions {
|
|
6
6
|
apiKey?: string;
|
|
7
7
|
mcpUrl: string;
|
|
8
8
|
launch: boolean;
|
|
@@ -48,4 +48,3 @@ export declare function connectCursor(options: {
|
|
|
48
48
|
}): Promise<void>;
|
|
49
49
|
export declare function launchAgent(client: AgentClient): void;
|
|
50
50
|
export declare function registerConnect(program: Command): void;
|
|
51
|
-
export {};
|
package/dist/commands/connect.js
CHANGED
|
@@ -446,34 +446,38 @@ function launchCursorDeeplink(mcpUrl) {
|
|
|
446
446
|
const result = openExternalUrl(cursorInstallDeeplink(mcpUrl));
|
|
447
447
|
return !result.error && result.status === 0;
|
|
448
448
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
else if (client === "opencode") {
|
|
460
|
-
launchNativeMcpClient(OPENCODE);
|
|
461
|
-
}
|
|
462
|
-
else if (client === "grok") {
|
|
463
|
-
launchNativeMcpClient(GROK);
|
|
464
|
-
}
|
|
465
|
-
else if (client === "openclaw") {
|
|
466
|
-
launchOpenClawWalkthrough();
|
|
467
|
-
}
|
|
468
|
-
else if (client === "cursor") {
|
|
469
|
-
// Cursor has no chat CLI to launch.
|
|
449
|
+
// A new client fails to compile until wired here.
|
|
450
|
+
const AGENT_LAUNCHERS = {
|
|
451
|
+
"claude-code": () => launchNativeMcpClient(CLAUDE_CODE),
|
|
452
|
+
codex: () => launchNativeMcpClient(CODEX),
|
|
453
|
+
grok: () => launchNativeMcpClient(GROK),
|
|
454
|
+
hermes: () => launchNativeMcpClient(HERMES),
|
|
455
|
+
opencode: () => launchNativeMcpClient(OPENCODE),
|
|
456
|
+
openclaw: launchOpenClawWalkthrough,
|
|
457
|
+
// Cursor has no chat CLI to launch.
|
|
458
|
+
cursor: () => {
|
|
470
459
|
throw new Error("Cursor has no launchable walkthrough. Open Cursor and start chatting.");
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
export function launchAgent(client) {
|
|
463
|
+
AGENT_LAUNCHERS[client]();
|
|
476
464
|
}
|
|
465
|
+
// A new client fails to compile until wired here.
|
|
466
|
+
const CLIENT_CONNECTORS = {
|
|
467
|
+
"claude-code": connectClaudeCode,
|
|
468
|
+
codex: connectCodex,
|
|
469
|
+
grok: connectGrok,
|
|
470
|
+
hermes: connectHermes,
|
|
471
|
+
opencode: connectOpencode,
|
|
472
|
+
openclaw: connectOpenClaw,
|
|
473
|
+
// `connect cursor --launch` still connects; it just cannot launch after.
|
|
474
|
+
cursor: async (options) => {
|
|
475
|
+
await connectCursor(options);
|
|
476
|
+
if (options.launch) {
|
|
477
|
+
errLine("Cursor has no launchable walkthrough. Open Cursor and start chatting.");
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
};
|
|
477
481
|
export function registerConnect(program) {
|
|
478
482
|
program
|
|
479
483
|
.command("connect")
|
|
@@ -568,31 +572,9 @@ export function registerConnect(program) {
|
|
|
568
572
|
keyAuthOnly: !detectHumanInitiated() && !!validatedApiKey(options.apiKey),
|
|
569
573
|
verbose: options.verbose ?? false,
|
|
570
574
|
};
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
else if (client === "codex") {
|
|
575
|
-
await connectCodex(connectOptions);
|
|
576
|
-
}
|
|
577
|
-
else if (client === "grok") {
|
|
578
|
-
await connectGrok(connectOptions);
|
|
579
|
-
}
|
|
580
|
-
else if (client === "hermes") {
|
|
581
|
-
await connectHermes(connectOptions);
|
|
582
|
-
}
|
|
583
|
-
else if (client === "opencode") {
|
|
584
|
-
await connectOpencode(connectOptions);
|
|
585
|
-
}
|
|
586
|
-
else if (client === "openclaw") {
|
|
587
|
-
await connectOpenClaw(connectOptions);
|
|
588
|
-
}
|
|
589
|
-
else if (client === "cursor") {
|
|
590
|
-
// `connect <client> --launch` is the shape every doc uses; connect anyway, then say
|
|
591
|
-
// why nothing launched rather than throwing away a working install.
|
|
592
|
-
await connectCursor(connectOptions);
|
|
593
|
-
if (connectOptions.launch) {
|
|
594
|
-
errLine("Cursor has no launchable walkthrough. Open Cursor and start chatting.");
|
|
595
|
-
}
|
|
575
|
+
const connector = CLIENT_CONNECTORS[client];
|
|
576
|
+
if (connector) {
|
|
577
|
+
await connector(connectOptions);
|
|
596
578
|
}
|
|
597
579
|
else {
|
|
598
580
|
throw new Error("Unsupported client: " +
|
package/dist/lib/connect-all.js
CHANGED
|
@@ -4,7 +4,7 @@ import { connectClaudeCode, connectCodex, connectCursor, connectGrok, connectHer
|
|
|
4
4
|
import { loadConfig, validatedApiKey } from "./auth.js";
|
|
5
5
|
import { TINYFISH_API_KEY_VAR } from "./constants.js";
|
|
6
6
|
import { installTinyFishCli, } from "./cli-install.js";
|
|
7
|
-
import {
|
|
7
|
+
import { HARNESS_CAPABILITIES, NATIVE_BY_HARNESS } from "./connect-clients.js";
|
|
8
8
|
import { ensureCliAuthenticated, TINYFISH_CLI_NOT_FOUND_MESSAGE } from "./connect-auth.js";
|
|
9
9
|
import { commandNotFound, ConnectInterruptedError, ConnectStepError, SignInTimeoutError, finishSetupCommand, throwIfInterrupted, } from "./connect-runtime.js";
|
|
10
10
|
import { pickHarnesses } from "./connect-picker.js";
|
|
@@ -15,9 +15,6 @@ import { removeCursorMcpServer, planCursorWrite } from "./cursor-config.js";
|
|
|
15
15
|
import { errLine, outLine, setErrIndent } from "./output.js";
|
|
16
16
|
import { sendSetupCompleted } from "./setup-telemetry.js";
|
|
17
17
|
import { verifyMcpAuth, verifyMcpHealth } from "./verify.js";
|
|
18
|
-
// `codex mcp add` succeeds unauthenticated; OAuth lands at first tool use.
|
|
19
|
-
// TODO(PF-3580): connect now probes Codex sign-in, so this over-warns a signed-in install.
|
|
20
|
-
const AUTH_DEFERRED_AT_INSTALL = new Set(["codex"]);
|
|
21
18
|
// A probe that exits non-zero is an install so old the subcommand is missing entirely.
|
|
22
19
|
const TOO_OLD_REASONS = new Set([
|
|
23
20
|
"harness_too_old",
|
|
@@ -29,26 +26,16 @@ const NO_BROWSER_REASONS = new Set([
|
|
|
29
26
|
"harness_not_installed",
|
|
30
27
|
"command_not_found",
|
|
31
28
|
]);
|
|
32
|
-
const AUTH_CHECK_CAPABLE = new Set(["cursor", "openclaw"]);
|
|
33
29
|
const STEP_INDENT = " ";
|
|
34
30
|
// Must undercut agent tool-call budgets (Claude Code Bash defaults to 120s).
|
|
35
31
|
const OAUTH_SIGN_IN_TIMEOUT_MS = 90_000;
|
|
36
32
|
// Derived: these block `connect` on a browser sign-in when keyless.
|
|
37
|
-
const BROWSER_SIGN_IN = new Set(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
const KEY_AUTH_CAPABLE = new Set([
|
|
44
|
-
...NATIVE_MCP_CLIENTS.filter((client) => !!client.keyAuth)
|
|
45
|
-
.map((client) => client.connectClient)
|
|
46
|
-
.filter((client) => ALL_HARNESSES.includes(client))
|
|
47
|
-
// Second map only narrows connectClient to Harness; the filter above is the real gate.
|
|
48
|
-
.map((client) => client),
|
|
49
|
-
"cursor",
|
|
50
|
-
"openclaw",
|
|
51
|
-
]);
|
|
33
|
+
const BROWSER_SIGN_IN = new Set(ALL_HARNESSES.filter((harness) => {
|
|
34
|
+
const native = NATIVE_BY_HARNESS.get(harness);
|
|
35
|
+
return !!native?.loginArgs || !!native?.oauthInAdd;
|
|
36
|
+
}));
|
|
37
|
+
// Derived: a native add carries the key, or the CLI holds it itself.
|
|
38
|
+
const KEY_AUTH_CAPABLE = new Set(ALL_HARNESSES.filter((harness) => !!NATIVE_BY_HARNESS.get(harness)?.keyAuth || HARNESS_CAPABILITIES[harness].keyHeldByCli));
|
|
52
39
|
// Real removal commands — harness id is not the binary name.
|
|
53
40
|
const UNINSTALL_POINTERS = {
|
|
54
41
|
"claude-code": "claude mcp remove tinyfish --scope user",
|
|
@@ -59,8 +46,25 @@ const UNINSTALL_POINTERS = {
|
|
|
59
46
|
// OpenCode ships no `mcp remove`, so the only removal is editing the config it wrote.
|
|
60
47
|
opencode: "remove the tinyfish entry from ~/.config/opencode/opencode.json",
|
|
61
48
|
};
|
|
49
|
+
// A new harness fails to compile until wired here.
|
|
50
|
+
const HARNESS_CONNECTORS = {
|
|
51
|
+
"claude-code": connectClaudeCode,
|
|
52
|
+
codex: connectCodex,
|
|
53
|
+
grok: connectGrok,
|
|
54
|
+
hermes: connectHermes,
|
|
55
|
+
opencode: connectOpencode,
|
|
56
|
+
// Without the key the child shells an interactive `tinyfish auth login`.
|
|
57
|
+
openclaw: async (base) => {
|
|
58
|
+
await connectOpenClaw(base);
|
|
59
|
+
return undefined;
|
|
60
|
+
},
|
|
61
|
+
cursor: async (base) => {
|
|
62
|
+
await connectCursor({ apiKey: base.apiKey, mcpUrl: base.mcpUrl, verbose: base.verbose });
|
|
63
|
+
return undefined;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
62
66
|
async function runHarnessConnect(harness, opts, keyAuthOnly, authTimeoutMs, hoistedCliInstall, onPostInstallFailed, deferOutro) {
|
|
63
|
-
|
|
67
|
+
return HARNESS_CONNECTORS[harness]({
|
|
64
68
|
apiKey: opts.apiKey,
|
|
65
69
|
mcpUrl: opts.mcpUrl,
|
|
66
70
|
launch: false,
|
|
@@ -70,35 +74,7 @@ async function runHarnessConnect(harness, opts, keyAuthOnly, authTimeoutMs, hois
|
|
|
70
74
|
onPostInstallFailed,
|
|
71
75
|
verbose: opts.verbose,
|
|
72
76
|
deferOutro,
|
|
73
|
-
};
|
|
74
|
-
if (harness === "claude-code")
|
|
75
|
-
return connectClaudeCode(base);
|
|
76
|
-
if (harness === "codex")
|
|
77
|
-
return connectCodex(base);
|
|
78
|
-
if (harness === "hermes")
|
|
79
|
-
return connectHermes(base);
|
|
80
|
-
if (harness === "openclaw") {
|
|
81
|
-
// Without the key the child shells an interactive `tinyfish auth login`.
|
|
82
|
-
await connectOpenClaw({
|
|
83
|
-
apiKey: opts.apiKey,
|
|
84
|
-
mcpUrl: opts.mcpUrl,
|
|
85
|
-
launch: false,
|
|
86
|
-
hoistedCliInstall,
|
|
87
|
-
verbose: opts.verbose,
|
|
88
|
-
deferOutro,
|
|
89
|
-
});
|
|
90
|
-
return undefined;
|
|
91
|
-
}
|
|
92
|
-
if (harness === "opencode")
|
|
93
|
-
return connectOpencode(base);
|
|
94
|
-
if (harness === "grok")
|
|
95
|
-
return connectGrok(base);
|
|
96
|
-
if (harness === "cursor") {
|
|
97
|
-
await connectCursor({ apiKey: opts.apiKey, mcpUrl: opts.mcpUrl, verbose: opts.verbose });
|
|
98
|
-
return undefined;
|
|
99
|
-
}
|
|
100
|
-
// Explicit: a new harness reaching an untested branch must fail, not write Cursor's config.
|
|
101
|
-
throw new Error(`No installer is wired for ${harness}`);
|
|
77
|
+
});
|
|
102
78
|
}
|
|
103
79
|
/** Undefined means Ctrl+C: the caller stops the whole run. */
|
|
104
80
|
function runHoistedCliInstall(verbose) {
|
|
@@ -117,7 +93,7 @@ function runHoistedCliInstall(verbose) {
|
|
|
117
93
|
async function verifyHarness(harness, mcpUrl, keyed, apiKey) {
|
|
118
94
|
const health = await verifyMcpHealth(mcpUrl);
|
|
119
95
|
// The own-OAuth harnesses are auth-checkable only in key mode (key-authed config).
|
|
120
|
-
const capable =
|
|
96
|
+
const capable = HARNESS_CAPABILITIES[harness].canVerifyAuth || keyed;
|
|
121
97
|
if (!capable || !health.ok)
|
|
122
98
|
return health;
|
|
123
99
|
if (!apiKey)
|
|
@@ -186,9 +162,10 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
|
|
|
186
162
|
let headlessAuth = false;
|
|
187
163
|
let spentBrowserSlot = false;
|
|
188
164
|
if (!isTTY && !keyed) {
|
|
189
|
-
//
|
|
190
|
-
if (harness
|
|
165
|
+
// A CLI-login sign-in is interactive-only — unboundable headless.
|
|
166
|
+
if (HARNESS_CAPABILITIES[harness].signInViaCliLogin) {
|
|
191
167
|
return { ...base, outcome: "auth_pending", fixCommand };
|
|
168
|
+
}
|
|
192
169
|
if (BROWSER_SIGN_IN.has(harness)) {
|
|
193
170
|
// One browser sign-in per run; the rest wait for a re-run.
|
|
194
171
|
if (oauthBudget.spent)
|
|
@@ -233,6 +210,10 @@ async function processHarness(detection, opts, prompt, oauthBudget, hoistedCliIn
|
|
|
233
210
|
if (spentBrowserSlot && e instanceof ConnectStepError && NO_BROWSER_REASONS.has(e.failureReason)) {
|
|
234
211
|
oauthBudget.spent = false;
|
|
235
212
|
}
|
|
213
|
+
// Detection saw a name on PATH; the probe proved nothing is behind it.
|
|
214
|
+
if (e instanceof ConnectStepError && e.failureReason === "harness_not_installed") {
|
|
215
|
+
return { ...base, detected: false, outcome: "not_detected" };
|
|
216
|
+
}
|
|
236
217
|
// Both reasons carry the same "flag not listed" message; the upgrade nudge is in it.
|
|
237
218
|
if (e instanceof ConnectStepError && TOO_OLD_REASONS.has(e.failureReason)) {
|
|
238
219
|
return {
|
|
@@ -291,9 +272,6 @@ function summaryLine(result, uninstall) {
|
|
|
291
272
|
? `… ${name} — ${result.errorMessage}. Finish it in a terminal: ${result.fixCommand}`
|
|
292
273
|
: `… ${name} — needs a browser sign-in. Run in a terminal: ${result.fixCommand}`;
|
|
293
274
|
}
|
|
294
|
-
if (result.outcome === "deselected") {
|
|
295
|
-
return `… ${name} — skipped, not selected. Fix: ${result.fixCommand}`;
|
|
296
|
-
}
|
|
297
275
|
if (result.outcome === "uninstall_pointer") {
|
|
298
276
|
return `… ${name} — not removed automatically (config is owned by ${name}). Run: ${result.fixCommand}`;
|
|
299
277
|
}
|
|
@@ -320,7 +298,7 @@ function summaryLine(result, uninstall) {
|
|
|
320
298
|
`Fix: ${finishSetupCommand(result.harness)}`);
|
|
321
299
|
}
|
|
322
300
|
// A keyed install has no sign-in left, so the deferred-auth warning would contradict it.
|
|
323
|
-
const authDeferred =
|
|
301
|
+
const authDeferred = HARNESS_CAPABILITIES[result.harness].authDeferredAtInstall && !result.keyAuthed;
|
|
324
302
|
const verifyLabel = result.verifyDepth ? `verified: ${result.verifyDepth}` : "not verified";
|
|
325
303
|
if (authDeferred || result.verifyOk === false) {
|
|
326
304
|
const reason = authDeferred
|
|
@@ -330,10 +308,25 @@ function summaryLine(result, uninstall) {
|
|
|
330
308
|
}
|
|
331
309
|
return `✓ ${name} — installed, ${verifyLabel}`;
|
|
332
310
|
}
|
|
333
|
-
/**
|
|
334
|
-
|
|
311
|
+
/** Absent and unpicked harnesses are noise once something real happened. */
|
|
312
|
+
function renderSummary(results, uninstall) {
|
|
313
|
+
if (!results.some(actionable))
|
|
314
|
+
return results.map((r) => summaryLine(r, uninstall));
|
|
315
|
+
// Ids, not display names: they double as the `connect` argument.
|
|
316
|
+
const skipped = results.filter((r) => r.outcome === "deselected").map((r) => r.harness);
|
|
317
|
+
const lines = results
|
|
318
|
+
.filter((r) => r.outcome !== "deselected" && r.outcome !== "not_detected")
|
|
319
|
+
.map((r) => summaryLine(r, uninstall));
|
|
320
|
+
if (skipped.length === 0)
|
|
321
|
+
return lines;
|
|
322
|
+
lines.push("", `Skipped (not selected): ${skipped.join(", ")}`);
|
|
323
|
+
return lines;
|
|
324
|
+
}
|
|
325
|
+
/** A config-writing connect needs no binary; every other harness spawns one. */
|
|
335
326
|
function isStaleConfig(detection) {
|
|
336
|
-
return detection.detected &&
|
|
327
|
+
return (detection.detected &&
|
|
328
|
+
!detection.binaryOnPath &&
|
|
329
|
+
!HARNESS_CAPABILITIES[detection.harness].cliWritesConfig);
|
|
337
330
|
}
|
|
338
331
|
/** A stale config dir is a detection signal with nothing behind it. */
|
|
339
332
|
function actionable(result) {
|
|
@@ -465,13 +458,18 @@ export async function runConnectAll(opts) {
|
|
|
465
458
|
const chosen = new Set(pick.selection);
|
|
466
459
|
deselected = new Set(offerable.filter((d) => !chosen.has(d.harness)).map((d) => d.harness));
|
|
467
460
|
}
|
|
468
|
-
// Mirrors processHarness's gate: headless, only keyless
|
|
461
|
+
// Mirrors processHarness's gate: headless, only keyless CLI-login harnesses skip.
|
|
469
462
|
const willAttempt = (harness) => !deselected.has(harness) &&
|
|
470
|
-
(isTTY ||
|
|
471
|
-
|
|
463
|
+
(isTTY ||
|
|
464
|
+
!HARNESS_CAPABILITIES[harness].signInViaCliLogin ||
|
|
465
|
+
(!!gateKey && KEY_AUTH_CAPABLE.has(harness)));
|
|
466
|
+
// Machine-global, so once per invocation; config-writing connects never install it.
|
|
472
467
|
const cliNeeded = !opts.dryRun &&
|
|
473
468
|
!opts.uninstall &&
|
|
474
|
-
detections.some((d) => d.detected &&
|
|
469
|
+
detections.some((d) => d.detected &&
|
|
470
|
+
d.binaryOnPath &&
|
|
471
|
+
!HARNESS_CAPABILITIES[d.harness].cliWritesConfig &&
|
|
472
|
+
willAttempt(d.harness));
|
|
475
473
|
let takeHoistedCli;
|
|
476
474
|
if (cliNeeded) {
|
|
477
475
|
errLine("First: the TinyFish CLI, shared by every harness.");
|
|
@@ -492,7 +490,9 @@ export async function runConnectAll(opts) {
|
|
|
492
490
|
const outro = new Set();
|
|
493
491
|
const results = [];
|
|
494
492
|
const oauthBudget = { spent: false };
|
|
495
|
-
|
|
493
|
+
// Mirrors processHarness's own short-circuits: absent has no work, stale only when removing.
|
|
494
|
+
const stepped = (d) => !deselected.has(d.harness) && d.detected && (opts.uninstall || !isStaleConfig(d));
|
|
495
|
+
const stepTotal = detections.filter(stepped).length;
|
|
496
496
|
let step = 0;
|
|
497
497
|
for (const detection of detections) {
|
|
498
498
|
const { harness } = detection;
|
|
@@ -505,6 +505,9 @@ export async function runConnectAll(opts) {
|
|
|
505
505
|
});
|
|
506
506
|
continue;
|
|
507
507
|
}
|
|
508
|
+
// The pad loop below records these; announcing them is the noise this command had.
|
|
509
|
+
if (!stepped(detection))
|
|
510
|
+
continue;
|
|
508
511
|
step += 1;
|
|
509
512
|
errLine(`step ${step} of ${stepTotal} — ${DISPLAY_NAMES[harness]}`);
|
|
510
513
|
let result;
|
|
@@ -550,8 +553,8 @@ export async function runConnectAll(opts) {
|
|
|
550
553
|
}
|
|
551
554
|
outLine("");
|
|
552
555
|
outLine("TinyFish setup summary:");
|
|
553
|
-
for (const
|
|
554
|
-
outLine(
|
|
556
|
+
for (const line of renderSummary(results, opts.uninstall))
|
|
557
|
+
outLine(line);
|
|
555
558
|
// Before the launch handover: anything printed after it is never seen.
|
|
556
559
|
for (const line of outro)
|
|
557
560
|
errLine(line);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import spawn from "cross-spawn";
|
|
2
|
+
import type { Harness } from "./harness-detect.js";
|
|
2
3
|
import { type AgentClient, type SupportedCommand } from "./connect-runtime.js";
|
|
3
4
|
export declare const OPENCLAW_SKILL_INSTALL_ARGS: readonly string[];
|
|
4
5
|
export declare const DEFAULT_ONBOARDING_PROMPT: string;
|
|
@@ -54,6 +55,23 @@ export declare const HERMES: NativeMcpClient;
|
|
|
54
55
|
export declare const OPENCODE: NativeMcpClient;
|
|
55
56
|
/** Every native MCP client. Derive from this, so a new harness cannot be missed. */
|
|
56
57
|
export declare const NATIVE_MCP_CLIENTS: readonly NativeMcpClient[];
|
|
58
|
+
/** Native descriptor by harness id; Cursor and OpenClaw have none. */
|
|
59
|
+
export declare const NATIVE_BY_HARNESS: Map<"openclaw" | "grok" | "cursor" | "codex" | "hermes" | "opencode" | "claude-code", NativeMcpClient>;
|
|
57
60
|
export declare const OPENCLAW: SupportedCommand;
|
|
61
|
+
/** Traits with no native descriptor to live on (Cursor and OpenClaw included). */
|
|
62
|
+
export interface HarnessCapabilities {
|
|
63
|
+
/** Sign-in is the CLI's own interactive `tinyfish auth login`. */
|
|
64
|
+
signInViaCliLogin: boolean;
|
|
65
|
+
/** Stored-key auth verification works against this harness's config. */
|
|
66
|
+
canVerifyAuth: boolean;
|
|
67
|
+
/** The CLI holds or writes the key; no harness `mcp add` carries it. */
|
|
68
|
+
keyHeldByCli: boolean;
|
|
69
|
+
/** Connect writes the MCP config file; no harness binary is spawned. */
|
|
70
|
+
cliWritesConfig: boolean;
|
|
71
|
+
/** `mcp add` succeeds unauthenticated; OAuth lands at first tool use. */
|
|
72
|
+
authDeferredAtInstall: boolean;
|
|
73
|
+
}
|
|
74
|
+
/** Required booleans: a new harness or new flag fails to compile undecided. */
|
|
75
|
+
export declare const HARNESS_CAPABILITIES: Record<Harness, HarnessCapabilities>;
|
|
58
76
|
export declare function launchNativeMcpClient(client: NativeMcpClient): void;
|
|
59
77
|
export declare function launchOpenClawWalkthrough(): void;
|
|
@@ -267,6 +267,8 @@ export const NATIVE_MCP_CLIENTS = [
|
|
|
267
267
|
HERMES,
|
|
268
268
|
OPENCODE,
|
|
269
269
|
];
|
|
270
|
+
/** Native descriptor by harness id; Cursor and OpenClaw have none. */
|
|
271
|
+
export const NATIVE_BY_HARNESS = new Map(NATIVE_MCP_CLIENTS.map((client) => [client.connectClient, client]));
|
|
270
272
|
export const OPENCLAW = {
|
|
271
273
|
command: "openclaw",
|
|
272
274
|
displayName: "OpenClaw",
|
|
@@ -276,6 +278,59 @@ export const OPENCLAW = {
|
|
|
276
278
|
unavailableMessage: OPENCLAW_SKILL_UNAVAILABLE_MESSAGE,
|
|
277
279
|
},
|
|
278
280
|
};
|
|
281
|
+
/** Required booleans: a new harness or new flag fails to compile undecided. */
|
|
282
|
+
export const HARNESS_CAPABILITIES = {
|
|
283
|
+
"claude-code": {
|
|
284
|
+
signInViaCliLogin: false,
|
|
285
|
+
canVerifyAuth: false,
|
|
286
|
+
keyHeldByCli: false,
|
|
287
|
+
cliWritesConfig: false,
|
|
288
|
+
authDeferredAtInstall: false,
|
|
289
|
+
},
|
|
290
|
+
codex: {
|
|
291
|
+
signInViaCliLogin: false,
|
|
292
|
+
canVerifyAuth: false,
|
|
293
|
+
keyHeldByCli: false,
|
|
294
|
+
cliWritesConfig: false,
|
|
295
|
+
// TODO(PF-3580): connect now probes Codex sign-in, so this over-warns a signed-in install.
|
|
296
|
+
authDeferredAtInstall: true,
|
|
297
|
+
},
|
|
298
|
+
cursor: {
|
|
299
|
+
signInViaCliLogin: false,
|
|
300
|
+
canVerifyAuth: true,
|
|
301
|
+
keyHeldByCli: true,
|
|
302
|
+
cliWritesConfig: true,
|
|
303
|
+
authDeferredAtInstall: false,
|
|
304
|
+
},
|
|
305
|
+
grok: {
|
|
306
|
+
signInViaCliLogin: false,
|
|
307
|
+
canVerifyAuth: false,
|
|
308
|
+
keyHeldByCli: false,
|
|
309
|
+
cliWritesConfig: false,
|
|
310
|
+
authDeferredAtInstall: false,
|
|
311
|
+
},
|
|
312
|
+
hermes: {
|
|
313
|
+
signInViaCliLogin: false,
|
|
314
|
+
canVerifyAuth: false,
|
|
315
|
+
keyHeldByCli: false,
|
|
316
|
+
cliWritesConfig: false,
|
|
317
|
+
authDeferredAtInstall: false,
|
|
318
|
+
},
|
|
319
|
+
openclaw: {
|
|
320
|
+
signInViaCliLogin: true,
|
|
321
|
+
canVerifyAuth: true,
|
|
322
|
+
keyHeldByCli: true,
|
|
323
|
+
cliWritesConfig: false,
|
|
324
|
+
authDeferredAtInstall: false,
|
|
325
|
+
},
|
|
326
|
+
opencode: {
|
|
327
|
+
signInViaCliLogin: false,
|
|
328
|
+
canVerifyAuth: false,
|
|
329
|
+
keyHeldByCli: false,
|
|
330
|
+
cliWritesConfig: false,
|
|
331
|
+
authDeferredAtInstall: false,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
279
334
|
export function launchNativeMcpClient(client) {
|
|
280
335
|
errLine(`Starting the TinyFish walkthrough in ${client.displayName}...`);
|
|
281
336
|
if (client.launchWalkthrough) {
|
|
@@ -137,7 +137,8 @@ export function requireCommandSupport(client) {
|
|
|
137
137
|
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
138
138
|
timeout: NON_INTERACTIVE_TIMEOUT_MS,
|
|
139
139
|
});
|
|
140
|
-
|
|
140
|
+
// 127 is the shell's not-found code; a wrapper shim with no real binary behind it exits that.
|
|
141
|
+
if (commandNotFound(result.error) || result.status === 127) {
|
|
141
142
|
throw new PrerequisiteError(`${client.displayName} is not installed or not available on PATH.`, "harness_not_installed", { cause: result.error });
|
|
142
143
|
}
|
|
143
144
|
if (result.error || result.status !== 0) {
|
|
@@ -3,7 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import spawn from "cross-spawn";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { loadConfig, matchesCliKey } from "./auth.js";
|
|
6
|
-
import {
|
|
6
|
+
import { NATIVE_BY_HARNESS } from "./connect-clients.js";
|
|
7
7
|
import { errLine } from "./output.js";
|
|
8
8
|
import { readCursorTinyfishEntry } from "./cursor-config.js";
|
|
9
9
|
import { detectInstalledHarnesses, harnessConfigPath, harnessDisplayPath, AuthMode, Registered, } from "./harness-detect.js";
|
|
@@ -227,7 +227,6 @@ export const SIGN_IN_PROBES = {
|
|
|
227
227
|
};
|
|
228
228
|
// This reaches the pasted report, so its shape is checked first.
|
|
229
229
|
const ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
230
|
-
const NATIVE_BY_HARNESS = new Map(NATIVE_MCP_CLIENTS.map((client) => [client.connectClient, client]));
|
|
231
230
|
/** Undefined unless the descriptor routes this harness's key through a variable. */
|
|
232
231
|
function declaredKeyEnvVar(harness) {
|
|
233
232
|
return NATIVE_BY_HARNESS.get(harness)?.keyAuth?.envVar;
|