@springbrand/cli 0.1.4-alpha.0 → 0.1.5-alpha.1
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/README.md +13 -22
- package/dist/commands/connect.d.ts +4 -2
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +184 -64
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +24 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/lib/auth.d.ts +1 -1
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/auth.js +2 -2
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/command-code-config.d.ts +1 -1
- package/dist/lib/command-code-config.d.ts.map +1 -1
- package/dist/lib/command-code-config.js +3 -3
- package/dist/lib/command-code-config.js.map +1 -1
- package/dist/lib/connect-all-summary.d.ts.map +1 -1
- package/dist/lib/connect-all-summary.js +8 -2
- package/dist/lib/connect-all-summary.js.map +1 -1
- package/dist/lib/connect-all.d.ts +3 -1
- package/dist/lib/connect-all.d.ts.map +1 -1
- package/dist/lib/connect-all.js +14 -6
- package/dist/lib/connect-all.js.map +1 -1
- package/dist/lib/connect-auth.d.ts +4 -3
- package/dist/lib/connect-auth.d.ts.map +1 -1
- package/dist/lib/connect-auth.js +14 -48
- package/dist/lib/connect-auth.js.map +1 -1
- package/dist/lib/connect-preflight.d.ts +1 -0
- package/dist/lib/connect-preflight.d.ts.map +1 -1
- package/dist/lib/connect-preflight.js +12 -7
- package/dist/lib/connect-preflight.js.map +1 -1
- package/dist/lib/connect-result.d.ts +139 -0
- package/dist/lib/connect-result.d.ts.map +1 -0
- package/dist/lib/connect-result.js +151 -0
- package/dist/lib/connect-result.js.map +1 -0
- package/dist/lib/connect-runtime.d.ts +1 -1
- package/dist/lib/connect-runtime.d.ts.map +1 -1
- package/dist/lib/connect-runtime.js +4 -2
- package/dist/lib/connect-runtime.js.map +1 -1
- package/dist/lib/cursor-config.d.ts +1 -1
- package/dist/lib/cursor-config.d.ts.map +1 -1
- package/dist/lib/cursor-config.js +3 -3
- package/dist/lib/cursor-config.js.map +1 -1
- package/dist/lib/doctor-checks.d.ts.map +1 -1
- package/dist/lib/doctor-checks.js +2 -4
- package/dist/lib/doctor-checks.js.map +1 -1
- package/dist/lib/doctor-machine.d.ts.map +1 -1
- package/dist/lib/doctor-machine.js +10 -2
- package/dist/lib/doctor-machine.js.map +1 -1
- package/dist/lib/doctor-repairs.d.ts.map +1 -1
- package/dist/lib/doctor-repairs.js +7 -8
- package/dist/lib/doctor-repairs.js.map +1 -1
- package/dist/lib/doctor-report.d.ts +161 -5
- package/dist/lib/doctor-report.d.ts.map +1 -1
- package/dist/lib/doctor-report.js +4 -1
- package/dist/lib/doctor-report.js.map +1 -1
- package/dist/lib/harness-spec.d.ts +2 -0
- package/dist/lib/harness-spec.d.ts.map +1 -1
- package/dist/lib/harness-spec.js +23 -7
- package/dist/lib/harness-spec.js.map +1 -1
- package/dist/lib/omp-config.d.ts +1 -1
- package/dist/lib/omp-config.d.ts.map +1 -1
- package/dist/lib/omp-config.js +3 -3
- package/dist/lib/omp-config.js.map +1 -1
- package/dist/lib/pi-config.d.ts +1 -1
- package/dist/lib/pi-config.d.ts.map +1 -1
- package/dist/lib/pi-config.js +3 -3
- package/dist/lib/pi-config.js.map +1 -1
- package/dist/lib/registration-detect.d.ts +2 -2
- package/dist/lib/registration-detect.d.ts.map +1 -1
- package/dist/lib/registration-detect.js +60 -43
- package/dist/lib/registration-detect.js.map +1 -1
- package/dist/lib/workbuddy-config.d.ts +1 -1
- package/dist/lib/workbuddy-config.d.ts.map +1 -1
- package/dist/lib/workbuddy-config.js +3 -3
- package/dist/lib/workbuddy-config.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/connect.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** 复刻自 tiny-fish CLI 编译产物:connect.d.ts + connect.impl.js */
|
|
2
2
|
import spawn from 'cross-spawn';
|
|
3
|
-
import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat,
|
|
3
|
+
import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, } from '../lib/auth.js';
|
|
4
4
|
import { CURSOR_SKILL_TARGET, OPENCLAW, openclawSkillInstallArgs, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchPiWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
|
|
5
5
|
import { captureStdio, capturedOutput, installSpringBrandCli, replay, SKILL_INSTALL_TIMEOUT_MS, STEP_MAX_BUFFER, } from '../lib/cli-install.js';
|
|
6
|
-
import { ensureCliAuthenticated } from '../lib/connect-auth.js';
|
|
6
|
+
import { connectApiKey, ensureCliAuthenticated } from '../lib/connect-auth.js';
|
|
7
7
|
import { createStdinPrompt, runCliFallback } from '../lib/connect-fallback.js';
|
|
8
8
|
import { installWebSkill, removeWebSkill } from '../lib/skill-install.js';
|
|
9
9
|
import { ConnectInterruptedError, ConnectStepError, HoistedFailureReportedError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, stageDurationOf, withStageDuration, spawnRemoval, } from '../lib/connect-runtime.js';
|
|
@@ -16,12 +16,14 @@ import { ALL_HARNESSES, AuthMode, HARNESS_DISPLAY_NAMES, RELOAD_ACTION, } from '
|
|
|
16
16
|
import { detectHumanInitiated } from '../lib/harness.js';
|
|
17
17
|
import { SPRINGBRAND_API_KEY_VAR } from '../lib/constants.js';
|
|
18
18
|
import { emitNotice } from '../lib/notice.js';
|
|
19
|
-
import { detectLegacyPlugin, legacyPluginHint, SIGN_IN_PROBES } from '../lib/registration-detect.js';
|
|
20
|
-
import { errLine, warnLine } from '../lib/output.js';
|
|
19
|
+
import { detectLegacyPlugin, detectRegistrations, legacyPluginHint, SIGN_IN_PROBES } from '../lib/registration-detect.js';
|
|
20
|
+
import { errLine, out, warnLine } from '../lib/output.js';
|
|
21
21
|
import { z } from 'zod';
|
|
22
|
-
import { verifyMcpAuth } from '../lib/verify.js';
|
|
22
|
+
import { verifyMcpAuth, verifyMcpHealth } from '../lib/verify.js';
|
|
23
23
|
import { gateApiKey } from '../lib/connect-preflight.js';
|
|
24
|
-
import { apiBaseFromMcpUrl, DEFAULT_MCP_URL, mcpServerName } from '../lib/mcp-endpoint.js';
|
|
24
|
+
import { apiBaseFromMcpUrl, DEFAULT_MCP_URL, endpointOf, mcpServerName } from '../lib/mcp-endpoint.js';
|
|
25
|
+
import { harnessOauthAction } from '../lib/harness-spec.js';
|
|
26
|
+
import { buildConnectResult } from '../lib/connect-result.js';
|
|
25
27
|
function removeExistingRegistration(client, removal, verbose) {
|
|
26
28
|
const result = spawnRemoval(client.command, removal.args);
|
|
27
29
|
if (result.status === 0)
|
|
@@ -42,9 +44,7 @@ function exportApiKey(apiKey, displayName, deferOutro) {
|
|
|
42
44
|
persistApiKeyToEnvironment(apiKey);
|
|
43
45
|
}
|
|
44
46
|
catch (error) {
|
|
45
|
-
|
|
46
|
-
'signing in instead.');
|
|
47
|
-
return false;
|
|
47
|
+
throw new ConnectStepError('Could not persist the API-key environment variable. OAuth was not started.', 'invalid_config');
|
|
48
48
|
}
|
|
49
49
|
process.env[SPRINGBRAND_API_KEY_VAR] = apiKey;
|
|
50
50
|
if (deferOutro) {
|
|
@@ -165,7 +165,9 @@ async function runMissingHarnessFallback(displayName, connectClient, state, tele
|
|
|
165
165
|
/** Resolved before the removals, so a refusal strands no registration. */
|
|
166
166
|
async function prepareInstallAuth(client, options, keyAuthSupported) {
|
|
167
167
|
// `/mcp` accepts the key as `Authorization: Bearer`, so a stored key replaces the browser OAuth hop.
|
|
168
|
-
const storedKey =
|
|
168
|
+
const storedKey = connectApiKey(options.apiKey);
|
|
169
|
+
if (storedKey && (!keyAuthSupported || !client.keyAuth))
|
|
170
|
+
throw new ConnectStepError('This Host version cannot install API-key authentication. Update the Host and retry with the same key.', 'key_auth_unsupported');
|
|
169
171
|
const candidate = resolveKeyedAdd(client, storedKey, keyAuthSupported, options.deferOutro);
|
|
170
172
|
// Direct seeded clients need their own pre-write probe. Codex and other env-backed clients
|
|
171
173
|
// have already passed the shared connect preflight before this function is reached.
|
|
@@ -183,7 +185,7 @@ async function prepareInstallAuth(client, options, keyAuthSupported) {
|
|
|
183
185
|
/** Inherit for an inline-OAuth add; capture otherwise, piping prompts when seeded. */
|
|
184
186
|
function addSpawnOptions(options, interactiveAdd, seeded) {
|
|
185
187
|
if (interactiveAdd)
|
|
186
|
-
return { stdio: 'inherit', timeout: options.authTimeoutMs };
|
|
188
|
+
return { stdio: ['inherit', 2, 2], timeout: options.authTimeoutMs ?? 60_000 };
|
|
187
189
|
if (!seeded)
|
|
188
190
|
return captureStdio(options.verbose ?? false);
|
|
189
191
|
return {
|
|
@@ -207,7 +209,7 @@ function performMcpAdd(client, options, plan, attemptId) {
|
|
|
207
209
|
// Header keys ride argv (/proc/<pid>/cmdline); these CLIs take headers no other way.
|
|
208
210
|
const addArgs = buildAddArgs(mcpUrl.toString());
|
|
209
211
|
// Capturing an inline-OAuth add hides the sign-in URL and hangs.
|
|
210
|
-
const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
|
|
212
|
+
const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd || options.deferOAuth);
|
|
211
213
|
// After the removals: an interrupt there leaves no key behind.
|
|
212
214
|
const seeded = keyedAdd?.spec.seedKey?.(keyedAdd.key);
|
|
213
215
|
// Writing the entry ourselves skips the harness probe that saves it disabled.
|
|
@@ -237,8 +239,8 @@ function performMcpAdd(client, options, plan, attemptId) {
|
|
|
237
239
|
function performLoginStep(client, pendingLogin, options) {
|
|
238
240
|
errLine('Signing in to SpringBrand...');
|
|
239
241
|
const loginResult = spawn.sync(client.command, [...pendingLogin], {
|
|
240
|
-
stdio: 'inherit',
|
|
241
|
-
timeout: options.authTimeoutMs,
|
|
242
|
+
stdio: ['inherit', 2, 2],
|
|
243
|
+
timeout: options.authTimeoutMs ?? 60_000,
|
|
242
244
|
});
|
|
243
245
|
if (loginResult.error || loginResult.status !== 0) {
|
|
244
246
|
throw signInStepError(`Could not authenticate SpringBrand in ${client.displayName}`, loginResult, options.authTimeoutMs !== undefined);
|
|
@@ -256,17 +258,20 @@ function announceAuthNotes(useKeyAuth, degradedNotes, seededNote) {
|
|
|
256
258
|
errLine(degradedNotes.deferred);
|
|
257
259
|
}
|
|
258
260
|
}
|
|
261
|
+
function loginArgsFor(client, mcpUrl) {
|
|
262
|
+
return client.loginArgs?.map((arg) => arg === 'springbrand' ? mcpServerName(mcpUrl) : arg);
|
|
263
|
+
}
|
|
259
264
|
function completeAuthFlow(client, options, state, telemetry, ctx) {
|
|
260
265
|
const { useKeyAuth, loginDegraded, loginArgs, probeAuthenticated } = ctx;
|
|
261
266
|
// Codex runs OAuth inside `mcp add` and exits 0 either way, so the recorded state decides
|
|
262
267
|
// whether the login step below still runs. Unknown keeps the pre-probe behaviour.
|
|
263
|
-
const addSignedIn = probeAuthenticated?.();
|
|
268
|
+
const addSignedIn = useKeyAuth ? undefined : probeAuthenticated?.();
|
|
264
269
|
if (addSignedIn)
|
|
265
270
|
telemetry.track('checkpoint', { phase: 'oauth_done' });
|
|
266
271
|
announceAuthNotes(useKeyAuth, loginDegraded ? client.degradedAuthNotes : undefined, ctx.seededNote);
|
|
267
272
|
// A probed client signs in again only when its inline OAuth is known to have been abandoned.
|
|
268
|
-
const retryLogin =
|
|
269
|
-
const pendingLogin = !useKeyAuth && retryLogin ? loginArgs : undefined;
|
|
273
|
+
const retryLogin = addSignedIn !== true;
|
|
274
|
+
const pendingLogin = !useKeyAuth && !options.deferOAuth && retryLogin ? loginArgs : undefined;
|
|
270
275
|
let signInDeferred = !useKeyAuth && !addSignedIn && !pendingLogin;
|
|
271
276
|
if (pendingLogin) {
|
|
272
277
|
state.stage = 'client_oauth';
|
|
@@ -286,7 +291,8 @@ function completeAuthFlow(client, options, state, telemetry, ctx) {
|
|
|
286
291
|
async function connectNativeMcpClient(client, options) {
|
|
287
292
|
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
|
|
288
293
|
const state = { stage: 'prerequisite_check', settled: false };
|
|
289
|
-
const
|
|
294
|
+
const nativeProbe = SIGN_IN_PROBES[client.connectClient];
|
|
295
|
+
const probeAuthenticated = nativeProbe ? () => nativeProbe(options.mcpUrl) : undefined;
|
|
290
296
|
await runGuarded(state, telemetry, async () => {
|
|
291
297
|
telemetry.track('started');
|
|
292
298
|
const support = await requireSupportOrRescue(client, client.connectClient, state, telemetry, options);
|
|
@@ -318,7 +324,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
318
324
|
try {
|
|
319
325
|
client.pluginInstall({
|
|
320
326
|
mcpUrl: pluginUrl.toString(),
|
|
321
|
-
apiKey: useKeyAuth ?
|
|
327
|
+
apiKey: useKeyAuth ? connectApiKey(options.apiKey) : undefined,
|
|
322
328
|
});
|
|
323
329
|
}
|
|
324
330
|
catch (error) {
|
|
@@ -345,7 +351,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
345
351
|
useKeyAuth,
|
|
346
352
|
loginDegraded,
|
|
347
353
|
// The Plugin names its server after the endpoint's environment.
|
|
348
|
-
loginArgs: loginDegraded ? undefined :
|
|
354
|
+
loginArgs: loginDegraded ? undefined : loginArgsFor(client, options.mcpUrl),
|
|
349
355
|
probeAuthenticated,
|
|
350
356
|
seededNote: undefined,
|
|
351
357
|
});
|
|
@@ -360,7 +366,7 @@ async function connectNativeMcpClient(client, options) {
|
|
|
360
366
|
removeExistingRegistration(client, scopedRemoval(removal, options.mcpUrl), options.verbose ?? false);
|
|
361
367
|
// Undefining `loginArgs` reuses the deferred-auth path codex/hermes already take.
|
|
362
368
|
const loginDegraded = state.harnessDegraded === true;
|
|
363
|
-
const loginArgs = loginDegraded ? undefined : client.
|
|
369
|
+
const loginArgs = loginDegraded ? undefined : loginArgsFor(client, options.mcpUrl);
|
|
364
370
|
// A probed client authenticates inside `mcp add` too, so a failure there is either.
|
|
365
371
|
state.stage =
|
|
366
372
|
loginArgs && !probeAuthenticated ? 'registration' : 'registration_or_authentication';
|
|
@@ -380,15 +386,18 @@ async function connectNativeMcpClient(client, options) {
|
|
|
380
386
|
});
|
|
381
387
|
return state.authMode;
|
|
382
388
|
}
|
|
383
|
-
function connectedLine(client, signInDeferred) {
|
|
389
|
+
function connectedLine(client, signInDeferred, mcpUrl) {
|
|
384
390
|
// The CLI never observes deferred auth, so don't claim "connected".
|
|
385
391
|
if (signInDeferred) {
|
|
392
|
+
const action = harnessOauthAction(client.connectClient, mcpUrl);
|
|
393
|
+
if (action)
|
|
394
|
+
return `SpringBrand is configured in ${client.displayName}, but authentication is pending. ${action}`;
|
|
386
395
|
if (client.signInHint)
|
|
387
396
|
return client.signInHint;
|
|
388
397
|
return (`SpringBrand is configured in ${client.displayName}. ${client.displayName} will ask you ` +
|
|
389
398
|
'to sign in to SpringBrand the first time you use it.');
|
|
390
399
|
}
|
|
391
|
-
return (`SpringBrand is
|
|
400
|
+
return (`SpringBrand configuration is installed. Open ${client.displayName}; if it is already running, ` +
|
|
392
401
|
`${RELOAD_ACTION[client.connectClient]} to pick up the tools.`);
|
|
393
402
|
}
|
|
394
403
|
// Runs after `completed` settled: steps fail alone, the rest still run.
|
|
@@ -421,7 +430,7 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
421
430
|
if (extraPostInstall) {
|
|
422
431
|
await attempt('plugin_install', () => {
|
|
423
432
|
extraPostInstall({
|
|
424
|
-
apiKey:
|
|
433
|
+
apiKey: connectApiKey(options.apiKey),
|
|
425
434
|
verbose: options.verbose ?? false,
|
|
426
435
|
seededHome,
|
|
427
436
|
});
|
|
@@ -432,7 +441,7 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
432
441
|
});
|
|
433
442
|
}
|
|
434
443
|
// Before auth: a failed sign-in must not lose the harness record.
|
|
435
|
-
saveConnectContext(client.connectClient, telemetry.attemptId, state.authMode);
|
|
444
|
+
saveConnectContext(client.connectClient, telemetry.attemptId, state.authMode, options.mcpUrl);
|
|
436
445
|
if (cliAuthRequired) {
|
|
437
446
|
await attempt('authentication', () => {
|
|
438
447
|
// A set timeout means headless; never shell an interactive login.
|
|
@@ -448,13 +457,18 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
448
457
|
if (!options.launch) {
|
|
449
458
|
// Under --all the summary replaces this; sign-in hints still print.
|
|
450
459
|
if (signInDeferred || !options.deferOutro)
|
|
451
|
-
errLine(connectedLine(client, signInDeferred));
|
|
460
|
+
errLine(connectedLine(client, signInDeferred, options.mcpUrl));
|
|
452
461
|
}
|
|
453
462
|
else {
|
|
454
463
|
await attempt('walkthrough_launch', async () => {
|
|
455
464
|
// Before the handover, or the only sign-in instructions scroll past under the agent.
|
|
456
|
-
if (signInDeferred
|
|
457
|
-
|
|
465
|
+
if (signInDeferred) {
|
|
466
|
+
const action = harnessOauthAction(client.connectClient, options.mcpUrl);
|
|
467
|
+
if (action)
|
|
468
|
+
errLine(action);
|
|
469
|
+
else if (client.signInHint)
|
|
470
|
+
errLine(client.signInHint);
|
|
471
|
+
}
|
|
458
472
|
await launchNativeMcpClient(client, async (outcome) => {
|
|
459
473
|
telemetry.track('checkpoint', { phase: walkthroughPhase(outcome) });
|
|
460
474
|
// Flushed pre-block; the session-long handover would strand the delivery.
|
|
@@ -470,7 +484,7 @@ async function runPostInstallSteps(client, options, state, telemetry, signInDefe
|
|
|
470
484
|
failed = true;
|
|
471
485
|
}
|
|
472
486
|
if (failed) {
|
|
473
|
-
errLine(finishSetupHint(client.connectClient));
|
|
487
|
+
errLine(finishSetupHint(client.connectClient, options.mcpUrl));
|
|
474
488
|
options.onPostInstallFailed?.();
|
|
475
489
|
}
|
|
476
490
|
}
|
|
@@ -502,8 +516,8 @@ function runCliInstallStep(options, telemetry) {
|
|
|
502
516
|
function walkthroughPhase(outcome) {
|
|
503
517
|
return outcome === 'printed' ? 'walkthrough_prompt_printed' : 'walkthrough_launched';
|
|
504
518
|
}
|
|
505
|
-
function finishSetupHint(connectClient) {
|
|
506
|
-
return `Finish setup with: ${finishSetupCommand(connectClient)}`;
|
|
519
|
+
function finishSetupHint(connectClient, mcpUrl) {
|
|
520
|
+
return `Finish setup with: ${finishSetupCommand(connectClient, mcpUrl)}`;
|
|
507
521
|
}
|
|
508
522
|
/** Warn + report post_install_failed; interrupts are abandonment, not failure. */
|
|
509
523
|
function trackPostInstallFailure(displayName, state, telemetry, error) {
|
|
@@ -530,11 +544,15 @@ export async function connectOpenClaw(options) {
|
|
|
530
544
|
settled: false,
|
|
531
545
|
harnessDegraded: false,
|
|
532
546
|
};
|
|
547
|
+
let authMode;
|
|
533
548
|
await runGuarded(state, telemetry, async () => {
|
|
534
549
|
telemetry.track('started');
|
|
535
550
|
const support = await requireSupportOrRescue(OPENCLAW, 'openclaw', state, telemetry, options);
|
|
536
551
|
if (!support)
|
|
537
552
|
return;
|
|
553
|
+
if (!connectApiKey(options.apiKey)) {
|
|
554
|
+
throw new ConnectStepError('OpenClaw requires a SpringBrand API key; keyless OAuth is not supported by this Host.', 'key_auth_unsupported');
|
|
555
|
+
}
|
|
538
556
|
telemetry.setHarnessVersion(support.harnessVersion);
|
|
539
557
|
telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
|
|
540
558
|
if (options.cliInstall !== false) {
|
|
@@ -558,6 +576,7 @@ export async function connectOpenClaw(options) {
|
|
|
558
576
|
interactiveLogin: options.authTimeoutMs === undefined,
|
|
559
577
|
mcpUrl: options.mcpUrl,
|
|
560
578
|
});
|
|
579
|
+
authMode = AuthMode.ApiKey;
|
|
561
580
|
telemetry.track('checkpoint', { phase: 'authenticated' });
|
|
562
581
|
saveConnectContext('openclaw', telemetry.attemptId);
|
|
563
582
|
// For openclaw the skill install and auth ARE functional; only the walkthrough is cosmetic.
|
|
@@ -580,9 +599,10 @@ export async function connectOpenClaw(options) {
|
|
|
580
599
|
}
|
|
581
600
|
catch (error) {
|
|
582
601
|
trackPostInstallFailure('OpenClaw', state, telemetry, error);
|
|
583
|
-
errLine(finishSetupHint('openclaw'));
|
|
602
|
+
errLine(finishSetupHint('openclaw', options.mcpUrl));
|
|
584
603
|
}
|
|
585
604
|
});
|
|
605
|
+
return authMode;
|
|
586
606
|
}
|
|
587
607
|
const PI_RELOAD_COPY = 'SpringBrand is configured in pi. Restart pi, then run `/mcp-auth springbrand` there if it asks you ' +
|
|
588
608
|
'to sign in.';
|
|
@@ -620,11 +640,11 @@ const CONFIG_FILE_HARNESSES = {
|
|
|
620
640
|
configPath: cursorMcpPath,
|
|
621
641
|
tryLaunch: launchCursorDeeplink,
|
|
622
642
|
copy: {
|
|
623
|
-
verified: "
|
|
643
|
+
verified: "The API key was accepted; Host loading is still unverified. Reload the Cursor window (or restart cursor-agent) and it's live.",
|
|
624
644
|
authFailed: (reason) => `SpringBrand is configured, but the authenticated check failed (${reason}). ` +
|
|
625
645
|
'Fix: rotate/re-enter your key with `sp auth login`, then re-run: sp connect cursor',
|
|
626
646
|
launched: 'Cursor should pop a SpringBrand install confirmation — approve it, then sign in when prompted.',
|
|
627
|
-
reload: 'SpringBrand is
|
|
647
|
+
reload: 'SpringBrand is configured. Reload the Cursor window, then approve/sign in under Settings → MCP.',
|
|
628
648
|
},
|
|
629
649
|
},
|
|
630
650
|
omp: {
|
|
@@ -636,7 +656,7 @@ const CONFIG_FILE_HARNESSES = {
|
|
|
636
656
|
// No install deeplink exists; the keyless copy carries the sign-in.
|
|
637
657
|
tryLaunch: () => false,
|
|
638
658
|
copy: {
|
|
639
|
-
verified: "
|
|
659
|
+
verified: "The API key was accepted; Host loading is still unverified. Restart omp and it's live.",
|
|
640
660
|
authFailed: (reason) => `SpringBrand is configured, but the authenticated check failed (${reason}). ` +
|
|
641
661
|
'Fix: rotate/re-enter your key with `sp auth login`, then re-run: sp connect omp',
|
|
642
662
|
launched: OMP_KEYLESS_COPY,
|
|
@@ -652,7 +672,7 @@ const CONFIG_FILE_HARNESSES = {
|
|
|
652
672
|
tryLaunch: () => false,
|
|
653
673
|
postConnectNote: piPostConnectNotes,
|
|
654
674
|
copy: {
|
|
655
|
-
verified: "
|
|
675
|
+
verified: "The API key was accepted; Host loading is still unverified. Restart pi and it's live.",
|
|
656
676
|
authFailed: (reason) => `SpringBrand is configured, but the authenticated check failed (${reason}). ` +
|
|
657
677
|
'Fix: rotate/re-enter your key with `sp auth login`, then re-run: sp connect pi',
|
|
658
678
|
launched: PI_RELOAD_COPY,
|
|
@@ -666,7 +686,7 @@ const CONFIG_FILE_HARNESSES = {
|
|
|
666
686
|
configPath: workbuddyMcpPath,
|
|
667
687
|
tryLaunch: () => false,
|
|
668
688
|
copy: {
|
|
669
|
-
verified: "
|
|
689
|
+
verified: "The API key was accepted; Host loading is still unverified. Restart WorkBuddy and it's live.",
|
|
670
690
|
authFailed: (reason) => `SpringBrand is configured, but the authenticated check failed (${reason}). ` +
|
|
671
691
|
'Fix: rotate/re-enter your key with `sp auth login`, then re-run: sp connect workbuddy-ai',
|
|
672
692
|
launched: 'SpringBrand is configured. Open WorkBuddy to load the MCP server.',
|
|
@@ -690,10 +710,10 @@ function installSkillPostSettle(spec, displayName, state, telemetry, options) {
|
|
|
690
710
|
}
|
|
691
711
|
}
|
|
692
712
|
/** Prints last: the outro must own the final line. */
|
|
693
|
-
function reportPostInstallFailure(harness, failed, onPostInstallFailed) {
|
|
713
|
+
function reportPostInstallFailure(harness, failed, mcpUrl, onPostInstallFailed) {
|
|
694
714
|
if (!failed)
|
|
695
715
|
return;
|
|
696
|
-
errLine(finishSetupHint(harness));
|
|
716
|
+
errLine(finishSetupHint(harness, mcpUrl));
|
|
697
717
|
onPostInstallFailed?.();
|
|
698
718
|
}
|
|
699
719
|
/** Writes the harness's MCP config file itself; no `mcp add` exists. */
|
|
@@ -737,12 +757,19 @@ async function connectConfigFileHarness(harness, options) {
|
|
|
737
757
|
settled: false,
|
|
738
758
|
harnessDegraded: false,
|
|
739
759
|
};
|
|
760
|
+
let resolvedAuthMode;
|
|
740
761
|
await runGuarded(state, telemetry, async () => {
|
|
741
762
|
telemetry.track('started');
|
|
742
763
|
telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
|
|
743
764
|
// A key lets the config carry an Authorization header. With no key, leave the
|
|
744
765
|
// client on its own native MCP OAuth path; never open the CLI API-key page.
|
|
745
|
-
const
|
|
766
|
+
const rawKey = connectApiKey(options.apiKey);
|
|
767
|
+
if (rawKey && !validateKeyFormat(rawKey)) {
|
|
768
|
+
throw new ConnectStepError('The supplied SpringBrand API key is invalid; OAuth was not started.', 'invalid_config', {
|
|
769
|
+
failureDetail: 'key_rejected_before_oauth',
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const resolvedKey = connectApiKey(options.apiKey);
|
|
746
773
|
if (resolvedKey) {
|
|
747
774
|
state.stage = 'authentication';
|
|
748
775
|
ensureCliAuthenticated(harness, options.apiKey, {
|
|
@@ -751,10 +778,6 @@ async function connectConfigFileHarness(harness, options) {
|
|
|
751
778
|
});
|
|
752
779
|
telemetry.track('checkpoint', { phase: 'authenticated' });
|
|
753
780
|
}
|
|
754
|
-
// ensureCliAuthenticated leaves a key stored, and `auth status` exits 0 on a malformed one.
|
|
755
|
-
if (!resolvedKey) {
|
|
756
|
-
errLine('Ignoring the stored API key: invalid format. Run: sp auth login');
|
|
757
|
-
}
|
|
758
781
|
state.stage = 'registration';
|
|
759
782
|
errLine(`Adding SpringBrand to ${displayName}...`);
|
|
760
783
|
const mcpUrl = new URL(options.mcpUrl);
|
|
@@ -774,8 +797,9 @@ async function connectConfigFileHarness(harness, options) {
|
|
|
774
797
|
errLine('Repaired the existing SpringBrand entry (updated auth/config to current).');
|
|
775
798
|
}
|
|
776
799
|
telemetry.track('checkpoint', { phase: 'registered' });
|
|
777
|
-
saveConnectContext(harness, telemetry.attemptId);
|
|
778
|
-
|
|
800
|
+
saveConnectContext(harness, telemetry.attemptId, resolvedKey ? AuthMode.ApiKey : 'oauth', options.mcpUrl);
|
|
801
|
+
resolvedAuthMode = resolvedKey ? AuthMode.ApiKey : 'deferred';
|
|
802
|
+
settle(state, telemetry, 'completed', { authMode: resolvedAuthMode });
|
|
779
803
|
const postInstallFailed = installSkillPostSettle(spec, displayName, state, telemetry, options);
|
|
780
804
|
if (resolvedKey) {
|
|
781
805
|
// Deeplink would embed the key in a URL (process args, LaunchServices logs) — reload instead.
|
|
@@ -797,12 +821,13 @@ async function connectConfigFileHarness(harness, options) {
|
|
|
797
821
|
const note = spec.postConnectNote?.();
|
|
798
822
|
if (note)
|
|
799
823
|
errLine(note);
|
|
800
|
-
reportPostInstallFailure(harness, postInstallFailed, options.onPostInstallFailed);
|
|
824
|
+
reportPostInstallFailure(harness, postInstallFailed, options.mcpUrl, options.onPostInstallFailed);
|
|
801
825
|
});
|
|
826
|
+
return resolvedAuthMode;
|
|
802
827
|
}
|
|
803
828
|
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
804
829
|
export async function connectCursor(options) {
|
|
805
|
-
|
|
830
|
+
return connectConfigFileHarness('cursor', options);
|
|
806
831
|
}
|
|
807
832
|
/** Best-effort: false when no handler/open fails — caller falls back to reload copy. */
|
|
808
833
|
function launchCursorDeeplink(mcpUrl) {
|
|
@@ -831,19 +856,18 @@ export async function launchAgent(client) {
|
|
|
831
856
|
/** Total over the non-native harnesses; native harnesses use the descriptor engine. */
|
|
832
857
|
const CONNECTOR_OVERRIDES = {
|
|
833
858
|
openclaw: async (options) => {
|
|
834
|
-
|
|
835
|
-
return undefined;
|
|
859
|
+
return connectOpenClaw(options);
|
|
836
860
|
},
|
|
837
861
|
cursor: async (options) => {
|
|
838
|
-
await connectCursor(options);
|
|
862
|
+
const authMode = await connectCursor(options);
|
|
839
863
|
// `connect cursor --launch` still connects; it just cannot launch after.
|
|
840
864
|
if (options.launch) {
|
|
841
865
|
errLine('Cursor has no launchable walkthrough. Open Cursor and start chatting.');
|
|
842
866
|
}
|
|
843
|
-
return
|
|
867
|
+
return authMode;
|
|
844
868
|
},
|
|
845
869
|
omp: async (options) => {
|
|
846
|
-
await connectConfigFileHarness('omp', options);
|
|
870
|
+
const authMode = await connectConfigFileHarness('omp', options);
|
|
847
871
|
if (options.launch) {
|
|
848
872
|
try {
|
|
849
873
|
await launchOmpWalkthrough();
|
|
@@ -851,30 +875,30 @@ const CONNECTOR_OVERRIDES = {
|
|
|
851
875
|
catch (error) {
|
|
852
876
|
// Post-settle, like the native walkthrough step: never fail the connect.
|
|
853
877
|
errLine(error instanceof Error ? error.message : String(error));
|
|
854
|
-
errLine(finishSetupHint('omp'));
|
|
878
|
+
errLine(finishSetupHint('omp', options.mcpUrl));
|
|
855
879
|
}
|
|
856
880
|
}
|
|
857
|
-
return
|
|
881
|
+
return authMode;
|
|
858
882
|
},
|
|
859
883
|
pi: async (options) => {
|
|
860
|
-
await connectConfigFileHarness('pi', options);
|
|
884
|
+
const authMode = await connectConfigFileHarness('pi', options);
|
|
861
885
|
if (options.launch) {
|
|
862
886
|
try {
|
|
863
887
|
await launchPiWalkthrough();
|
|
864
888
|
}
|
|
865
889
|
catch (error) {
|
|
866
890
|
errLine(error instanceof Error ? error.message : String(error));
|
|
867
|
-
errLine(finishSetupHint('pi'));
|
|
891
|
+
errLine(finishSetupHint('pi', options.mcpUrl));
|
|
868
892
|
}
|
|
869
893
|
}
|
|
870
|
-
return
|
|
894
|
+
return authMode;
|
|
871
895
|
},
|
|
872
896
|
'workbuddy-ai': async (options) => {
|
|
873
|
-
await connectConfigFileHarness('workbuddy-ai', options);
|
|
897
|
+
const authMode = await connectConfigFileHarness('workbuddy-ai', options);
|
|
874
898
|
if (options.launch) {
|
|
875
899
|
errLine('WorkBuddy has no launchable CLI walkthrough. Open WorkBuddy and start chatting.');
|
|
876
900
|
}
|
|
877
|
-
return
|
|
901
|
+
return authMode;
|
|
878
902
|
},
|
|
879
903
|
};
|
|
880
904
|
function requireNative(harness) {
|
|
@@ -930,6 +954,62 @@ async function connectManyPath(options, mcpUrl, attemptId) {
|
|
|
930
954
|
if (!process.exitCode)
|
|
931
955
|
process.exitCode = exitCode;
|
|
932
956
|
}
|
|
957
|
+
function safeConnectError(error) {
|
|
958
|
+
if (error instanceof ConnectStepError) {
|
|
959
|
+
if (error.message === 'unsupported')
|
|
960
|
+
return { code: 'unsupported', message: 'This Host is not supported by the CLI.', phase: 'input' };
|
|
961
|
+
const phase = error.failureReason === 'key_auth_unsupported' || error.failureReason === 'invalid_config'
|
|
962
|
+
? 'authentication'
|
|
963
|
+
: error.failureReason === 'harness_command_unsupported' || error.failureReason === 'harness_too_old'
|
|
964
|
+
? 'registration'
|
|
965
|
+
: 'registration';
|
|
966
|
+
return {
|
|
967
|
+
code: error.failureReason,
|
|
968
|
+
message: error.failureReason === 'key_auth_unsupported'
|
|
969
|
+
? 'This Host does not support the requested authentication mode.'
|
|
970
|
+
: error.failureReason === 'invalid_config'
|
|
971
|
+
? 'The Host configuration could not be updated. Follow the CLI recovery message and retry.'
|
|
972
|
+
: 'The Host could not be configured. Follow the CLI recovery message and retry.',
|
|
973
|
+
phase,
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
return { code: 'unexpected_error', message: 'The Host installation did not complete. Retry the connect command.', phase: 'registration' };
|
|
977
|
+
}
|
|
978
|
+
async function singleConnectResult(client, mcpUrl, authMode, installed, apiKey, error) {
|
|
979
|
+
const health = await verifyMcpHealth(mcpUrl);
|
|
980
|
+
const key = connectApiKey(apiKey);
|
|
981
|
+
// A preflight failure must never send the rejected or cross-environment key a
|
|
982
|
+
// second time merely to build a machine result.
|
|
983
|
+
const auth = !error && authMode === 'api-key' && key && validateKeyFormat(key)
|
|
984
|
+
? await verifyMcpAuth(key, apiBaseFromMcpUrl(mcpUrl))
|
|
985
|
+
: undefined;
|
|
986
|
+
let registrationConnected;
|
|
987
|
+
let registrationMatches = false;
|
|
988
|
+
let keyMatchesRegistration = false;
|
|
989
|
+
try {
|
|
990
|
+
const status = ALL_HARNESSES.includes(client)
|
|
991
|
+
? detectRegistrations([client], mcpUrl)[0] : undefined;
|
|
992
|
+
registrationConnected = status?.connected;
|
|
993
|
+
registrationMatches = status?.registered === 'yes' && !!status.registeredUrl &&
|
|
994
|
+
endpointOf(status.registeredUrl) === endpointOf(mcpUrl);
|
|
995
|
+
keyMatchesRegistration = status?.keyMatchesCliKey === true;
|
|
996
|
+
}
|
|
997
|
+
catch {
|
|
998
|
+
registrationConnected = undefined;
|
|
999
|
+
}
|
|
1000
|
+
return buildConnectResult({
|
|
1001
|
+
host: client,
|
|
1002
|
+
mcpUrl,
|
|
1003
|
+
authMode,
|
|
1004
|
+
installed,
|
|
1005
|
+
health,
|
|
1006
|
+
auth,
|
|
1007
|
+
registrationConnected,
|
|
1008
|
+
registrationMatches,
|
|
1009
|
+
keyMatchesRegistration,
|
|
1010
|
+
...(error ? { error: safeConnectError(error) } : {}),
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
933
1013
|
async function connectSingleClient(client, options, mcpUrl, attemptId) {
|
|
934
1014
|
const connectOptions = {
|
|
935
1015
|
apiKey: options.apiKey,
|
|
@@ -937,12 +1017,21 @@ async function connectSingleClient(client, options, mcpUrl, attemptId) {
|
|
|
937
1017
|
launch: options.launch ?? false,
|
|
938
1018
|
attemptId,
|
|
939
1019
|
// A key that the install cannot use leaves only a browser hop no agent can finish.
|
|
940
|
-
keyAuthOnly: !detectHumanInitiated() && !!
|
|
941
|
-
|
|
942
|
-
|
|
1020
|
+
keyAuthOnly: !detectHumanInitiated() && !!connectApiKey(options.apiKey),
|
|
1021
|
+
authTimeoutMs: 60_000,
|
|
1022
|
+
// Machine mode is consumed by the landing agent: install the Host entry and
|
|
1023
|
+
// return its native OAuth action instead of trying to drive an interactive login.
|
|
1024
|
+
deferOAuth: options.json === true,
|
|
1025
|
+
// A selected Host failure never switches clients or auth modes.
|
|
1026
|
+
fallbackWhenMissing: false,
|
|
943
1027
|
verbose: options.verbose ?? false,
|
|
944
1028
|
};
|
|
945
1029
|
if (!ALL_HARNESSES.includes(client)) {
|
|
1030
|
+
if (options.json) {
|
|
1031
|
+
out(await singleConnectResult(String(client), mcpUrl, undefined, false, undefined, new ConnectStepError('unsupported', 'harness_command_unsupported')));
|
|
1032
|
+
process.exitCode = 1;
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
946
1035
|
throw new Error('Unsupported client: ' + client + '. Supported clients: ' + ALL_HARNESSES.join(', '));
|
|
947
1036
|
}
|
|
948
1037
|
const gate = await gateApiKey({
|
|
@@ -951,8 +1040,12 @@ async function connectSingleClient(client, options, mcpUrl, attemptId) {
|
|
|
951
1040
|
retryCommand: `sp connect ${client}`,
|
|
952
1041
|
mode: 'single',
|
|
953
1042
|
attemptId,
|
|
1043
|
+
strict: options.json === true,
|
|
954
1044
|
});
|
|
955
1045
|
if (gate === 'blocked') {
|
|
1046
|
+
if (options.json) {
|
|
1047
|
+
out(await singleConnectResult(client, mcpUrl, connectApiKey(options.apiKey) !== undefined ? AuthMode.ApiKey : undefined, false, options.apiKey, new ConnectStepError('invalid key', 'invalid_config')));
|
|
1048
|
+
}
|
|
956
1049
|
process.exitCode = 1;
|
|
957
1050
|
return;
|
|
958
1051
|
}
|
|
@@ -961,7 +1054,21 @@ async function connectSingleClient(client, options, mcpUrl, attemptId) {
|
|
|
961
1054
|
const hint = legacyPluginHint(client, legacy);
|
|
962
1055
|
if (hint)
|
|
963
1056
|
errLine(hint);
|
|
964
|
-
|
|
1057
|
+
let authMode = connectApiKey(options.apiKey) !== undefined ? AuthMode.ApiKey : AuthMode.OAuth;
|
|
1058
|
+
let error;
|
|
1059
|
+
try {
|
|
1060
|
+
authMode = await connectHarness(client, connectOptions) ?? authMode;
|
|
1061
|
+
}
|
|
1062
|
+
catch (caught) {
|
|
1063
|
+
error = caught;
|
|
1064
|
+
if (!options.json)
|
|
1065
|
+
throw caught;
|
|
1066
|
+
}
|
|
1067
|
+
const result = await singleConnectResult(client, mcpUrl, authMode, !error, options.apiKey, error);
|
|
1068
|
+
if (options.json)
|
|
1069
|
+
out(result);
|
|
1070
|
+
if (result.outcome !== 'verified' && process.exitCode !== 130)
|
|
1071
|
+
process.exitCode = 1;
|
|
965
1072
|
}
|
|
966
1073
|
export function registerConnect(program) {
|
|
967
1074
|
program
|
|
@@ -975,9 +1082,16 @@ export function registerConnect(program) {
|
|
|
975
1082
|
.option('--api-key <apiKey>', 'SpringBrand API key to persist for the installed skill')
|
|
976
1083
|
.option('--launch', 'Launch the agent and start the SpringBrand walkthrough')
|
|
977
1084
|
.option('--url <mcpUrl>', 'MCP endpoint override')
|
|
1085
|
+
.option('--json', 'Print one machine-readable bootstrap result')
|
|
978
1086
|
.option('--verbose', 'Print the full npm and skills output instead of only on failure')
|
|
979
1087
|
.action(async (client, options) => {
|
|
980
1088
|
if (options.apiKey && !validateKeyFormat(options.apiKey)) {
|
|
1089
|
+
if (options.json && client && ALL_HARNESSES.includes(client)) {
|
|
1090
|
+
const { mcpUrl } = parseMcpUrlOption(options);
|
|
1091
|
+
out(await singleConnectResult(client, mcpUrl, AuthMode.ApiKey, false, options.apiKey, new ConnectStepError('invalid key', 'invalid_config')));
|
|
1092
|
+
process.exitCode = 1;
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
981
1095
|
throw new Error('Invalid --api-key value');
|
|
982
1096
|
}
|
|
983
1097
|
const { mcpUrl, attemptId } = parseMcpUrlOption(options);
|
|
@@ -985,9 +1099,15 @@ export function registerConnect(program) {
|
|
|
985
1099
|
if (client) {
|
|
986
1100
|
throw new Error('Pass either a client or --all, not both.');
|
|
987
1101
|
}
|
|
1102
|
+
if (options.json) {
|
|
1103
|
+
throw new Error('--json requires a single client; --all has per-Host text output');
|
|
1104
|
+
}
|
|
988
1105
|
await connectManyPath(options, mcpUrl, attemptId);
|
|
989
1106
|
return;
|
|
990
1107
|
}
|
|
1108
|
+
if (options.json && !client) {
|
|
1109
|
+
throw new Error('--json requires a single client; pass a client name');
|
|
1110
|
+
}
|
|
991
1111
|
if (options.dryRun || options.uninstall || options.skipLaunch) {
|
|
992
1112
|
throw new Error('--dry-run, --uninstall and --skip-launch are only supported with --all.');
|
|
993
1113
|
}
|