@tiny-fish/cli 0.30.2-next.266 → 0.30.2-next.267
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.js +215 -177
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -148,6 +148,124 @@ async function runMissingHarnessFallback(displayName, connectClient, state, tele
|
|
|
148
148
|
errLine('The TinyFish CLI is installed and working.');
|
|
149
149
|
errLine('Using a different agent? Run: tinyfish connect --all');
|
|
150
150
|
}
|
|
151
|
+
/** Resolved before the removals, so a refusal strands no registration. */
|
|
152
|
+
async function prepareInstallAuth(client, options, keyAuthSupported) {
|
|
153
|
+
// Prod MCP accepts X-API-Key, so a stored key replaces the browser OAuth hop.
|
|
154
|
+
const storedKey = validatedApiKey(options.apiKey);
|
|
155
|
+
const keyedAdd = resolveKeyedAdd(client, storedKey, keyAuthSupported, options.deferOutro);
|
|
156
|
+
// Refuses here when key-required; others keep their OAuth argv.
|
|
157
|
+
const buildAddArgs = keyedAdd
|
|
158
|
+
? (mcpUrl) => keyedAdd.spec.addArgs(mcpUrl, keyedAdd.key)
|
|
159
|
+
: keylessAddArgs(client, keyAuthSupported, options.mcpUrl);
|
|
160
|
+
if (options.keyAuthOnly && !keyedAdd) {
|
|
161
|
+
// All native clients have keyAuth; missing flags mean an old install.
|
|
162
|
+
if (client.keyAuth) {
|
|
163
|
+
throw new ConnectStepError(`This ${client.displayName} install cannot take a TinyFish API key, and signing in ` +
|
|
164
|
+
`needs a terminal. Update it, then re-run: tinyfish connect ${client.connectClient}`, 'harness_too_old');
|
|
165
|
+
}
|
|
166
|
+
throw new ConnectStepError(`${client.displayName} cannot take a TinyFish API key, and signing in needs a ` +
|
|
167
|
+
`terminal. Run: tinyfish connect ${client.connectClient}`, 'key_auth_unsupported');
|
|
168
|
+
}
|
|
169
|
+
// Verify first: a revoked key in a foreign .env is unrecoverable.
|
|
170
|
+
if (keyedAdd?.spec.seedKey) {
|
|
171
|
+
await verifyKeyBeforeSeeding(keyedAdd.key, client.displayName, options.mcpUrl);
|
|
172
|
+
}
|
|
173
|
+
return { keyedAdd, buildAddArgs };
|
|
174
|
+
}
|
|
175
|
+
/** Returns the seeded-credential note when a key was written into a user-owned file. */
|
|
176
|
+
function performMcpAdd(client, options, plan, attemptId) {
|
|
177
|
+
const { keyedAdd, buildAddArgs } = plan;
|
|
178
|
+
const mcpUrl = new URL(options.mcpUrl);
|
|
179
|
+
mcpUrl.searchParams.set('source', CONNECT_SOURCE);
|
|
180
|
+
mcpUrl.searchParams.set('client', client.connectClient);
|
|
181
|
+
mcpUrl.searchParams.set('connect_attempt_id', attemptId);
|
|
182
|
+
errLine(`Adding TinyFish to ${client.displayName}...`);
|
|
183
|
+
// Header keys ride argv (/proc/<pid>/cmdline); these CLIs take headers no other way.
|
|
184
|
+
const addArgs = buildAddArgs(mcpUrl.toString());
|
|
185
|
+
// Capturing an inline-OAuth add hides the sign-in URL and hangs.
|
|
186
|
+
const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
|
|
187
|
+
// After the removals: an interrupt there leaves no key behind.
|
|
188
|
+
const seeded = keyedAdd?.spec.seedKey?.(keyedAdd.key);
|
|
189
|
+
const addOptions = interactiveAdd
|
|
190
|
+
? { stdio: 'inherit', timeout: options.authTimeoutMs }
|
|
191
|
+
: seeded
|
|
192
|
+
? {
|
|
193
|
+
encoding: 'utf8',
|
|
194
|
+
// Without it, the 1 MiB default overflows into a misread Ctrl+C.
|
|
195
|
+
maxBuffer: STEP_MAX_BUFFER,
|
|
196
|
+
// The unattended path had no bound at all before; captureStdio's is the floor.
|
|
197
|
+
timeout: options.authTimeoutMs ?? SKILL_INSTALL_TIMEOUT_MS,
|
|
198
|
+
input: seeded.stdinInput,
|
|
199
|
+
env: seeded.env,
|
|
200
|
+
}
|
|
201
|
+
: captureStdio(options.verbose ?? false);
|
|
202
|
+
const addResult = spawn.sync(client.command, addArgs, addOptions);
|
|
203
|
+
// Rolls back first: signInStepError throws on a Ctrl+C.
|
|
204
|
+
const failAdd = (build) => {
|
|
205
|
+
seeded?.rollback();
|
|
206
|
+
const error = build();
|
|
207
|
+
replay(capturedOutput(addResult));
|
|
208
|
+
throw error;
|
|
209
|
+
};
|
|
210
|
+
if (addResult.error || addResult.status !== 0) {
|
|
211
|
+
failAdd(() => signInStepError(`Could not add TinyFish to ${client.displayName}`, addResult, interactiveAdd && options.authTimeoutMs !== undefined));
|
|
212
|
+
}
|
|
213
|
+
const registration = seeded?.confirmRegistered();
|
|
214
|
+
if (registration && !registration.ok) {
|
|
215
|
+
failAdd(() => new ConnectStepError(`${client.displayName} reported success but TinyFish is not enabled there` +
|
|
216
|
+
`${registration.detail ? `: ${registration.detail}` : ''}.`, 'invalid_config'));
|
|
217
|
+
}
|
|
218
|
+
return seeded?.note;
|
|
219
|
+
}
|
|
220
|
+
function performLoginStep(client, pendingLogin, options) {
|
|
221
|
+
errLine('Signing in to TinyFish...');
|
|
222
|
+
const loginResult = spawn.sync(client.command, [...pendingLogin], {
|
|
223
|
+
stdio: 'inherit',
|
|
224
|
+
timeout: options.authTimeoutMs,
|
|
225
|
+
});
|
|
226
|
+
if (loginResult.error || loginResult.status !== 0) {
|
|
227
|
+
throw signInStepError(`Could not authenticate TinyFish in ${client.displayName}`, loginResult, options.authTimeoutMs !== undefined);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function announceAuthNotes(useKeyAuth, degradedNotes, seededNote) {
|
|
231
|
+
if (useKeyAuth) {
|
|
232
|
+
errLine(degradedNotes?.keyed ?? 'Using your stored TinyFish API key — no browser sign-in needed.');
|
|
233
|
+
// Every other credential the CLI writes into a user-owned file says so too.
|
|
234
|
+
if (seededNote)
|
|
235
|
+
errLine(seededNote);
|
|
236
|
+
}
|
|
237
|
+
else if (degradedNotes) {
|
|
238
|
+
// Printed here, not with the closing line, so a `--launch` run sees it before the handover.
|
|
239
|
+
errLine(degradedNotes.deferred);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function completeAuthFlow(client, options, state, telemetry, ctx) {
|
|
243
|
+
const { useKeyAuth, loginDegraded, loginArgs, probeAuthenticated } = ctx;
|
|
244
|
+
// Codex runs OAuth inside `mcp add` and exits 0 either way, so the recorded state decides
|
|
245
|
+
// whether the login step below still runs. Unknown keeps the pre-probe behaviour.
|
|
246
|
+
const addSignedIn = probeAuthenticated?.();
|
|
247
|
+
if (addSignedIn)
|
|
248
|
+
telemetry.track('checkpoint', { phase: 'oauth_done' });
|
|
249
|
+
announceAuthNotes(useKeyAuth, loginDegraded ? client.degradedAuthNotes : undefined, ctx.seededNote);
|
|
250
|
+
// A probed client signs in again only when its inline OAuth is known to have been abandoned.
|
|
251
|
+
const retryLogin = probeAuthenticated ? addSignedIn === false : true;
|
|
252
|
+
const pendingLogin = !useKeyAuth && retryLogin ? loginArgs : undefined;
|
|
253
|
+
let signInDeferred = !useKeyAuth && !addSignedIn && !pendingLogin;
|
|
254
|
+
if (pendingLogin) {
|
|
255
|
+
state.stage = 'client_oauth';
|
|
256
|
+
performLoginStep(client, pendingLogin, options);
|
|
257
|
+
if (probeAuthenticated?.() === false)
|
|
258
|
+
signInDeferred = true;
|
|
259
|
+
else
|
|
260
|
+
telemetry.track('checkpoint', { phase: 'oauth_done' });
|
|
261
|
+
}
|
|
262
|
+
const authMode = useKeyAuth
|
|
263
|
+
? AuthMode.ApiKey
|
|
264
|
+
: signInDeferred
|
|
265
|
+
? 'deferred'
|
|
266
|
+
: AuthMode.OAuth;
|
|
267
|
+
return { authMode, signInDeferred };
|
|
268
|
+
}
|
|
151
269
|
async function connectNativeMcpClient(client, options) {
|
|
152
270
|
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
|
|
153
271
|
const state = { stage: 'prerequisite_check', settled: false };
|
|
@@ -160,118 +278,27 @@ async function connectNativeMcpClient(client, options) {
|
|
|
160
278
|
const { optionalSupported, keyAuthSupported, harnessVersion } = support;
|
|
161
279
|
telemetry.setHarnessVersion(harnessVersion);
|
|
162
280
|
state.harnessDegraded = !!client.loginArgs && !optionalSupported;
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
const keyedAdd = resolveKeyedAdd(client, storedKey, keyAuthSupported, options.deferOutro);
|
|
166
|
-
const useKeyAuth = !!keyedAdd;
|
|
167
|
-
// Refuses here when key-required; others keep their OAuth argv.
|
|
168
|
-
const buildAddArgs = keyedAdd
|
|
169
|
-
? (mcpUrl) => keyedAdd.spec.addArgs(mcpUrl, keyedAdd.key)
|
|
170
|
-
: keylessAddArgs(client, keyAuthSupported, options.mcpUrl);
|
|
171
|
-
if (options.keyAuthOnly && !useKeyAuth) {
|
|
172
|
-
// All native clients have keyAuth; missing flags mean an old install.
|
|
173
|
-
if (client.keyAuth) {
|
|
174
|
-
throw new ConnectStepError(`This ${client.displayName} install cannot take a TinyFish API key, and signing in ` +
|
|
175
|
-
`needs a terminal. Update it, then re-run: tinyfish connect ${client.connectClient}`, 'harness_too_old');
|
|
176
|
-
}
|
|
177
|
-
throw new ConnectStepError(`${client.displayName} cannot take a TinyFish API key, and signing in needs a ` +
|
|
178
|
-
`terminal. Run: tinyfish connect ${client.connectClient}`, 'key_auth_unsupported');
|
|
179
|
-
}
|
|
180
|
-
// Verify first: a revoked key in a foreign .env is unrecoverable.
|
|
181
|
-
if (keyedAdd?.spec.seedKey) {
|
|
182
|
-
await verifyKeyBeforeSeeding(keyedAdd.key, client.displayName, options.mcpUrl);
|
|
183
|
-
}
|
|
281
|
+
const plan = await prepareInstallAuth(client, options, keyAuthSupported);
|
|
282
|
+
const useKeyAuth = !!plan.keyedAdd;
|
|
184
283
|
telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
|
|
185
284
|
state.stage = 'registration_cleanup';
|
|
186
285
|
for (const removal of client.removals)
|
|
187
286
|
removeExistingRegistration(client, removal, options.verbose ?? false);
|
|
188
|
-
const mcpUrl = new URL(options.mcpUrl);
|
|
189
|
-
mcpUrl.searchParams.set('source', CONNECT_SOURCE);
|
|
190
|
-
mcpUrl.searchParams.set('client', client.connectClient);
|
|
191
|
-
mcpUrl.searchParams.set('connect_attempt_id', telemetry.attemptId);
|
|
192
287
|
// Undefining `loginArgs` reuses the deferred-auth path codex/hermes already take.
|
|
193
288
|
const loginDegraded = state.harnessDegraded === true;
|
|
194
289
|
const loginArgs = loginDegraded ? undefined : client.loginArgs;
|
|
195
290
|
// A probed client authenticates inside `mcp add` too, so a failure there is either.
|
|
196
291
|
state.stage =
|
|
197
292
|
loginArgs && !probeAuthenticated ? 'registration' : 'registration_or_authentication';
|
|
198
|
-
|
|
199
|
-
// Header keys ride argv (/proc/<pid>/cmdline); these CLIs take headers no other way.
|
|
200
|
-
const addArgs = buildAddArgs(mcpUrl.toString());
|
|
201
|
-
// Capturing an inline-OAuth add hides the sign-in URL and hangs.
|
|
202
|
-
const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
|
|
203
|
-
// After the removals: an interrupt there leaves no key behind.
|
|
204
|
-
const seeded = keyedAdd?.spec.seedKey?.(keyedAdd.key);
|
|
205
|
-
const addOptions = interactiveAdd
|
|
206
|
-
? { stdio: 'inherit', timeout: options.authTimeoutMs }
|
|
207
|
-
: seeded
|
|
208
|
-
? {
|
|
209
|
-
encoding: 'utf8',
|
|
210
|
-
// Without it, the 1 MiB default overflows into a misread Ctrl+C.
|
|
211
|
-
maxBuffer: STEP_MAX_BUFFER,
|
|
212
|
-
// The unattended path had no bound at all before; captureStdio's is the floor.
|
|
213
|
-
timeout: options.authTimeoutMs ?? SKILL_INSTALL_TIMEOUT_MS,
|
|
214
|
-
input: seeded.stdinInput,
|
|
215
|
-
env: seeded.env,
|
|
216
|
-
}
|
|
217
|
-
: captureStdio(options.verbose ?? false);
|
|
218
|
-
const addResult = spawn.sync(client.command, addArgs, addOptions);
|
|
219
|
-
// Rolls back first: signInStepError throws on a Ctrl+C.
|
|
220
|
-
const failAdd = (build) => {
|
|
221
|
-
seeded?.rollback();
|
|
222
|
-
const error = build();
|
|
223
|
-
replay(capturedOutput(addResult));
|
|
224
|
-
throw error;
|
|
225
|
-
};
|
|
226
|
-
if (addResult.error || addResult.status !== 0) {
|
|
227
|
-
failAdd(() => signInStepError(`Could not add TinyFish to ${client.displayName}`, addResult, interactiveAdd && options.authTimeoutMs !== undefined));
|
|
228
|
-
}
|
|
229
|
-
const registration = seeded?.confirmRegistered();
|
|
230
|
-
if (registration && !registration.ok) {
|
|
231
|
-
failAdd(() => new ConnectStepError(`${client.displayName} reported success but TinyFish is not enabled there` +
|
|
232
|
-
`${registration.detail ? `: ${registration.detail}` : ''}.`, 'invalid_config'));
|
|
233
|
-
}
|
|
293
|
+
const seededNote = performMcpAdd(client, options, plan, telemetry.attemptId);
|
|
234
294
|
telemetry.track('checkpoint', { phase: 'registered' });
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
errLine(degradedNotes?.keyed ?? 'Using your stored TinyFish API key — no browser sign-in needed.');
|
|
243
|
-
// Every other credential the CLI writes into a user-owned file says so too.
|
|
244
|
-
if (seeded)
|
|
245
|
-
errLine(seeded.note);
|
|
246
|
-
}
|
|
247
|
-
else if (degradedNotes) {
|
|
248
|
-
// Printed here, not with the closing line, so a `--launch` run sees it before the handover.
|
|
249
|
-
errLine(degradedNotes.deferred);
|
|
250
|
-
}
|
|
251
|
-
// A probed client signs in again only when its inline OAuth is known to have been abandoned.
|
|
252
|
-
const retryLogin = probeAuthenticated ? addSignedIn === false : true;
|
|
253
|
-
const pendingLogin = !useKeyAuth && retryLogin ? loginArgs : undefined;
|
|
254
|
-
let signInDeferred = !useKeyAuth && !addSignedIn && !pendingLogin;
|
|
255
|
-
if (pendingLogin) {
|
|
256
|
-
state.stage = 'client_oauth';
|
|
257
|
-
errLine('Signing in to TinyFish...');
|
|
258
|
-
const loginResult = spawn.sync(client.command, pendingLogin, {
|
|
259
|
-
stdio: 'inherit',
|
|
260
|
-
timeout: options.authTimeoutMs,
|
|
261
|
-
});
|
|
262
|
-
if (loginResult.error || loginResult.status !== 0) {
|
|
263
|
-
throw signInStepError(`Could not authenticate TinyFish in ${client.displayName}`, loginResult, options.authTimeoutMs !== undefined);
|
|
264
|
-
}
|
|
265
|
-
if (probeAuthenticated?.() === false)
|
|
266
|
-
signInDeferred = true;
|
|
267
|
-
else
|
|
268
|
-
telemetry.track('checkpoint', { phase: 'oauth_done' });
|
|
269
|
-
}
|
|
270
|
-
const authMode = useKeyAuth
|
|
271
|
-
? AuthMode.ApiKey
|
|
272
|
-
: signInDeferred
|
|
273
|
-
? 'deferred'
|
|
274
|
-
: AuthMode.OAuth;
|
|
295
|
+
const { authMode, signInDeferred } = completeAuthFlow(client, options, state, telemetry, {
|
|
296
|
+
useKeyAuth,
|
|
297
|
+
loginDegraded,
|
|
298
|
+
loginArgs,
|
|
299
|
+
probeAuthenticated,
|
|
300
|
+
seededNote,
|
|
301
|
+
});
|
|
275
302
|
state.authMode = authMode;
|
|
276
303
|
// Registration/OAuth make MCP work; later steps are cosmetic and must not fail the attempt.
|
|
277
304
|
settle(state, telemetry, 'completed', { authMode });
|
|
@@ -611,6 +638,89 @@ const CLIENT_CONNECTORS = {
|
|
|
611
638
|
}
|
|
612
639
|
},
|
|
613
640
|
};
|
|
641
|
+
function parseMcpUrlOption(options) {
|
|
642
|
+
let mcpUrl = options.url ?? DEFAULT_MCP_URL;
|
|
643
|
+
if (mcpUrl.startsWith('-')) {
|
|
644
|
+
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
645
|
+
}
|
|
646
|
+
let parsedUrl;
|
|
647
|
+
try {
|
|
648
|
+
parsedUrl = new URL(mcpUrl);
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
652
|
+
}
|
|
653
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
654
|
+
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
655
|
+
}
|
|
656
|
+
// Page-minted attempt id rides in on --url. Strip before the URL becomes the MCP
|
|
657
|
+
// base — registration re-stamps it, and nothing else may carry it.
|
|
658
|
+
const pageAttemptId = parsedUrl.searchParams.get('connect_attempt_id');
|
|
659
|
+
let attemptId;
|
|
660
|
+
if (pageAttemptId !== null) {
|
|
661
|
+
parsedUrl.searchParams.delete('connect_attempt_id');
|
|
662
|
+
mcpUrl = parsedUrl.toString();
|
|
663
|
+
if (z.uuid().safeParse(pageAttemptId).success)
|
|
664
|
+
attemptId = pageAttemptId;
|
|
665
|
+
}
|
|
666
|
+
return { mcpUrl, attemptId };
|
|
667
|
+
}
|
|
668
|
+
async function connectAllPath(options, mcpUrl) {
|
|
669
|
+
if (options.launch) {
|
|
670
|
+
throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
|
|
671
|
+
}
|
|
672
|
+
const exitCode = await runConnectAll({
|
|
673
|
+
apiKey: options.apiKey,
|
|
674
|
+
mcpUrl,
|
|
675
|
+
dryRun: options.dryRun ?? false,
|
|
676
|
+
uninstall: options.uninstall ?? false,
|
|
677
|
+
noLaunch: options.skipLaunch === true,
|
|
678
|
+
verbose: options.verbose ?? false,
|
|
679
|
+
});
|
|
680
|
+
// Preserve the 130 runGuarded set on Ctrl+C.
|
|
681
|
+
if (!process.exitCode)
|
|
682
|
+
process.exitCode = exitCode;
|
|
683
|
+
}
|
|
684
|
+
async function connectPickPath(options, mcpUrl) {
|
|
685
|
+
if (options.launch) {
|
|
686
|
+
throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
|
|
687
|
+
}
|
|
688
|
+
const humanInitiated = detectHumanInitiated();
|
|
689
|
+
if (!humanInitiated) {
|
|
690
|
+
errLine('No terminal detected: connecting every detected harness (same as tinyfish connect --all).');
|
|
691
|
+
}
|
|
692
|
+
const exitCode = await runConnectAll({
|
|
693
|
+
apiKey: options.apiKey,
|
|
694
|
+
mcpUrl,
|
|
695
|
+
dryRun: false,
|
|
696
|
+
uninstall: false,
|
|
697
|
+
pick: humanInitiated,
|
|
698
|
+
verbose: options.verbose ?? false,
|
|
699
|
+
});
|
|
700
|
+
// Preserve the 130 runGuarded set on Ctrl+C.
|
|
701
|
+
if (!process.exitCode)
|
|
702
|
+
process.exitCode = exitCode;
|
|
703
|
+
}
|
|
704
|
+
async function connectSingleClient(client, options, mcpUrl, attemptId) {
|
|
705
|
+
const connectOptions = {
|
|
706
|
+
apiKey: options.apiKey,
|
|
707
|
+
mcpUrl,
|
|
708
|
+
launch: options.launch ?? false,
|
|
709
|
+
attemptId,
|
|
710
|
+
// A key that the install cannot use leaves only a browser hop no agent can finish.
|
|
711
|
+
keyAuthOnly: !detectHumanInitiated() && !!validatedApiKey(options.apiKey),
|
|
712
|
+
// Only here: a missing harness rescues via the CLI (PF-3707).
|
|
713
|
+
fallbackWhenMissing: true,
|
|
714
|
+
verbose: options.verbose ?? false,
|
|
715
|
+
};
|
|
716
|
+
const connector = CLIENT_CONNECTORS[client];
|
|
717
|
+
if (!connector) {
|
|
718
|
+
throw new Error('Unsupported client: ' +
|
|
719
|
+
client +
|
|
720
|
+
'. Supported clients: claude-code, codex, cursor, grok, hermes, openclaw, opencode');
|
|
721
|
+
}
|
|
722
|
+
await connector(connectOptions);
|
|
723
|
+
}
|
|
614
724
|
export function registerConnect(program) {
|
|
615
725
|
program
|
|
616
726
|
.command('connect')
|
|
@@ -628,94 +738,22 @@ export function registerConnect(program) {
|
|
|
628
738
|
if (options.apiKey && !validateKeyFormat(options.apiKey)) {
|
|
629
739
|
throw new Error('Invalid --api-key value');
|
|
630
740
|
}
|
|
631
|
-
|
|
632
|
-
if (mcpUrl.startsWith('-')) {
|
|
633
|
-
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
634
|
-
}
|
|
635
|
-
let parsedUrl;
|
|
636
|
-
try {
|
|
637
|
-
parsedUrl = new URL(mcpUrl);
|
|
638
|
-
}
|
|
639
|
-
catch {
|
|
640
|
-
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
641
|
-
}
|
|
642
|
-
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
643
|
-
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
644
|
-
}
|
|
645
|
-
// Page-minted attempt id rides in on --url. Strip before the URL becomes the MCP
|
|
646
|
-
// base — registration re-stamps it, and nothing else may carry it.
|
|
647
|
-
const pageAttemptId = parsedUrl.searchParams.get('connect_attempt_id');
|
|
648
|
-
let attemptId;
|
|
649
|
-
if (pageAttemptId !== null) {
|
|
650
|
-
parsedUrl.searchParams.delete('connect_attempt_id');
|
|
651
|
-
mcpUrl = parsedUrl.toString();
|
|
652
|
-
if (z.uuid().safeParse(pageAttemptId).success)
|
|
653
|
-
attemptId = pageAttemptId;
|
|
654
|
-
}
|
|
741
|
+
const { mcpUrl, attemptId } = parseMcpUrlOption(options);
|
|
655
742
|
if (options.all) {
|
|
656
743
|
if (client) {
|
|
657
744
|
throw new Error('Pass either a client or --all, not both.');
|
|
658
745
|
}
|
|
659
|
-
|
|
660
|
-
throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
|
|
661
|
-
}
|
|
662
|
-
const exitCode = await runConnectAll({
|
|
663
|
-
apiKey: options.apiKey,
|
|
664
|
-
mcpUrl,
|
|
665
|
-
dryRun: options.dryRun ?? false,
|
|
666
|
-
uninstall: options.uninstall ?? false,
|
|
667
|
-
noLaunch: options.skipLaunch === true,
|
|
668
|
-
verbose: options.verbose ?? false,
|
|
669
|
-
});
|
|
670
|
-
// Preserve the 130 runGuarded set on Ctrl+C.
|
|
671
|
-
if (!process.exitCode)
|
|
672
|
-
process.exitCode = exitCode;
|
|
746
|
+
await connectAllPath(options, mcpUrl);
|
|
673
747
|
return;
|
|
674
748
|
}
|
|
675
749
|
if (options.dryRun || options.uninstall || options.skipLaunch) {
|
|
676
750
|
throw new Error('--dry-run, --uninstall and --skip-launch are only supported with --all.');
|
|
677
751
|
}
|
|
678
752
|
if (!client) {
|
|
679
|
-
|
|
680
|
-
throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
|
|
681
|
-
}
|
|
682
|
-
const humanInitiated = detectHumanInitiated();
|
|
683
|
-
if (!humanInitiated) {
|
|
684
|
-
errLine('No terminal detected: connecting every detected harness (same as tinyfish connect --all).');
|
|
685
|
-
}
|
|
686
|
-
const exitCode = await runConnectAll({
|
|
687
|
-
apiKey: options.apiKey,
|
|
688
|
-
mcpUrl,
|
|
689
|
-
dryRun: false,
|
|
690
|
-
uninstall: false,
|
|
691
|
-
pick: humanInitiated,
|
|
692
|
-
verbose: options.verbose ?? false,
|
|
693
|
-
});
|
|
694
|
-
// Preserve the 130 runGuarded set on Ctrl+C.
|
|
695
|
-
if (!process.exitCode)
|
|
696
|
-
process.exitCode = exitCode;
|
|
753
|
+
await connectPickPath(options, mcpUrl);
|
|
697
754
|
return;
|
|
698
755
|
}
|
|
699
|
-
|
|
700
|
-
apiKey: options.apiKey,
|
|
701
|
-
mcpUrl,
|
|
702
|
-
launch: options.launch ?? false,
|
|
703
|
-
attemptId,
|
|
704
|
-
// A key that the install cannot use leaves only a browser hop no agent can finish.
|
|
705
|
-
keyAuthOnly: !detectHumanInitiated() && !!validatedApiKey(options.apiKey),
|
|
706
|
-
// Only here: a missing harness rescues via the CLI (PF-3707).
|
|
707
|
-
fallbackWhenMissing: true,
|
|
708
|
-
verbose: options.verbose ?? false,
|
|
709
|
-
};
|
|
710
|
-
const connector = CLIENT_CONNECTORS[client];
|
|
711
|
-
if (connector) {
|
|
712
|
-
await connector(connectOptions);
|
|
713
|
-
}
|
|
714
|
-
else {
|
|
715
|
-
throw new Error('Unsupported client: ' +
|
|
716
|
-
client +
|
|
717
|
-
'. Supported clients: claude-code, codex, cursor, grok, hermes, openclaw, opencode');
|
|
718
|
-
}
|
|
756
|
+
await connectSingleClient(client, options, mcpUrl, attemptId);
|
|
719
757
|
// Deferred to here so the notice lands after install output, not interleaved with it.
|
|
720
758
|
emitNotice();
|
|
721
759
|
});
|