@springbrand/cli 0.1.0
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 +167 -0
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +241 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/check-version.d.ts +3 -0
- package/dist/commands/check-version.d.ts.map +1 -0
- package/dist/commands/check-version.js +112 -0
- package/dist/commands/check-version.js.map +1 -0
- package/dist/commands/config-claude.d.ts +4 -0
- package/dist/commands/config-claude.d.ts.map +1 -0
- package/dist/commands/config-claude.js +78 -0
- package/dist/commands/config-claude.js.map +1 -0
- package/dist/commands/connect.d.ts +41 -0
- package/dist/commands/connect.d.ts.map +1 -0
- package/dist/commands/connect.js +901 -0
- package/dist/commands/connect.js.map +1 -0
- package/dist/commands/doctor.d.ts +6 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +124 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/upgrade.d.ts +9 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +133 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/agent-detect.d.ts +33 -0
- package/dist/lib/agent-detect.d.ts.map +1 -0
- package/dist/lib/agent-detect.js +81 -0
- package/dist/lib/agent-detect.js.map +1 -0
- package/dist/lib/agent-launcher.d.ts +22 -0
- package/dist/lib/agent-launcher.d.ts.map +1 -0
- package/dist/lib/agent-launcher.js +67 -0
- package/dist/lib/agent-launcher.js.map +1 -0
- package/dist/lib/auth.d.ts +90 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +327 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/bundled-skill.d.ts +5 -0
- package/dist/lib/bundled-skill.d.ts.map +1 -0
- package/dist/lib/bundled-skill.js +13 -0
- package/dist/lib/bundled-skill.js.map +1 -0
- package/dist/lib/claude-config.d.ts +19 -0
- package/dist/lib/claude-config.d.ts.map +1 -0
- package/dist/lib/claude-config.js +202 -0
- package/dist/lib/claude-config.js.map +1 -0
- package/dist/lib/cli-install.d.ts +40 -0
- package/dist/lib/cli-install.d.ts.map +1 -0
- package/dist/lib/cli-install.js +42 -0
- package/dist/lib/cli-install.js.map +1 -0
- package/dist/lib/command-code-config.d.ts +5 -0
- package/dist/lib/command-code-config.d.ts.map +1 -0
- package/dist/lib/command-code-config.js +16 -0
- package/dist/lib/command-code-config.js.map +1 -0
- package/dist/lib/connect-all-auth.d.ts +21 -0
- package/dist/lib/connect-all-auth.d.ts.map +1 -0
- package/dist/lib/connect-all-auth.js +113 -0
- package/dist/lib/connect-all-auth.js.map +1 -0
- package/dist/lib/connect-all-summary.d.ts +10 -0
- package/dist/lib/connect-all-summary.d.ts.map +1 -0
- package/dist/lib/connect-all-summary.js +153 -0
- package/dist/lib/connect-all-summary.js.map +1 -0
- package/dist/lib/connect-all-uninstall.d.ts +7 -0
- package/dist/lib/connect-all-uninstall.d.ts.map +1 -0
- package/dist/lib/connect-all-uninstall.js +249 -0
- package/dist/lib/connect-all-uninstall.js.map +1 -0
- package/dist/lib/connect-all.d.ts +42 -0
- package/dist/lib/connect-all.d.ts.map +1 -0
- package/dist/lib/connect-all.js +438 -0
- package/dist/lib/connect-all.js.map +1 -0
- package/dist/lib/connect-auth.d.ts +7 -0
- package/dist/lib/connect-auth.d.ts.map +1 -0
- package/dist/lib/connect-auth.js +50 -0
- package/dist/lib/connect-auth.js.map +1 -0
- package/dist/lib/connect-clients.d.ts +108 -0
- package/dist/lib/connect-clients.d.ts.map +1 -0
- package/dist/lib/connect-clients.js +358 -0
- package/dist/lib/connect-clients.js.map +1 -0
- package/dist/lib/connect-fallback.d.ts +27 -0
- package/dist/lib/connect-fallback.d.ts.map +1 -0
- package/dist/lib/connect-fallback.js +166 -0
- package/dist/lib/connect-fallback.js.map +1 -0
- package/dist/lib/connect-picker.d.ts +19 -0
- package/dist/lib/connect-picker.d.ts.map +1 -0
- package/dist/lib/connect-picker.js +155 -0
- package/dist/lib/connect-picker.js.map +1 -0
- package/dist/lib/connect-preflight.d.ts +17 -0
- package/dist/lib/connect-preflight.d.ts.map +1 -0
- package/dist/lib/connect-preflight.js +81 -0
- package/dist/lib/connect-preflight.js.map +1 -0
- package/dist/lib/connect-runtime.d.ts +109 -0
- package/dist/lib/connect-runtime.d.ts.map +1 -0
- package/dist/lib/connect-runtime.js +360 -0
- package/dist/lib/connect-runtime.js.map +1 -0
- package/dist/lib/constants.d.ts +23 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +87 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/cursor-config.d.ts +16 -0
- package/dist/lib/cursor-config.d.ts.map +1 -0
- package/dist/lib/cursor-config.js +34 -0
- package/dist/lib/cursor-config.js.map +1 -0
- package/dist/lib/doctor-checks.d.ts +21 -0
- package/dist/lib/doctor-checks.d.ts.map +1 -0
- package/dist/lib/doctor-checks.js +312 -0
- package/dist/lib/doctor-checks.js.map +1 -0
- package/dist/lib/doctor-repairs.d.ts +11 -0
- package/dist/lib/doctor-repairs.d.ts.map +1 -0
- package/dist/lib/doctor-repairs.js +130 -0
- package/dist/lib/doctor-repairs.js.map +1 -0
- package/dist/lib/doctor-report.d.ts +195 -0
- package/dist/lib/doctor-report.d.ts.map +1 -0
- package/dist/lib/doctor-report.js +102 -0
- package/dist/lib/doctor-report.js.map +1 -0
- package/dist/lib/doctor-telemetry.d.ts +9 -0
- package/dist/lib/doctor-telemetry.d.ts.map +1 -0
- package/dist/lib/doctor-telemetry.js +63 -0
- package/dist/lib/doctor-telemetry.js.map +1 -0
- package/dist/lib/harness-detect.d.ts +33 -0
- package/dist/lib/harness-detect.d.ts.map +1 -0
- package/dist/lib/harness-detect.js +70 -0
- package/dist/lib/harness-detect.js.map +1 -0
- package/dist/lib/harness-spec.d.ts +285 -0
- package/dist/lib/harness-spec.d.ts.map +1 -0
- package/dist/lib/harness-spec.js +268 -0
- package/dist/lib/harness-spec.js.map +1 -0
- package/dist/lib/harness.d.ts +27 -0
- package/dist/lib/harness.d.ts.map +1 -0
- package/dist/lib/harness.js +125 -0
- package/dist/lib/harness.js.map +1 -0
- package/dist/lib/hermes-config.d.ts +22 -0
- package/dist/lib/hermes-config.d.ts.map +1 -0
- package/dist/lib/hermes-config.js +76 -0
- package/dist/lib/hermes-config.js.map +1 -0
- package/dist/lib/hermes-env.d.ts +19 -0
- package/dist/lib/hermes-env.d.ts.map +1 -0
- package/dist/lib/hermes-env.js +169 -0
- package/dist/lib/hermes-env.js.map +1 -0
- package/dist/lib/hermes-plugin.d.ts +40 -0
- package/dist/lib/hermes-plugin.d.ts.map +1 -0
- package/dist/lib/hermes-plugin.js +168 -0
- package/dist/lib/hermes-plugin.js.map +1 -0
- package/dist/lib/install-root.d.ts +18 -0
- package/dist/lib/install-root.d.ts.map +1 -0
- package/dist/lib/install-root.js +55 -0
- package/dist/lib/install-root.js.map +1 -0
- package/dist/lib/mcp-config.d.ts +36 -0
- package/dist/lib/mcp-config.d.ts.map +1 -0
- package/dist/lib/mcp-config.js +90 -0
- package/dist/lib/mcp-config.js.map +1 -0
- package/dist/lib/mcp-endpoint.d.ts +16 -0
- package/dist/lib/mcp-endpoint.d.ts.map +1 -0
- package/dist/lib/mcp-endpoint.js +41 -0
- package/dist/lib/mcp-endpoint.js.map +1 -0
- package/dist/lib/mcp-json-config.d.ts +43 -0
- package/dist/lib/mcp-json-config.d.ts.map +1 -0
- package/dist/lib/mcp-json-config.js +164 -0
- package/dist/lib/mcp-json-config.js.map +1 -0
- package/dist/lib/notice.d.ts +13 -0
- package/dist/lib/notice.d.ts.map +1 -0
- package/dist/lib/notice.js +73 -0
- package/dist/lib/notice.js.map +1 -0
- package/dist/lib/omp-config.d.ts +18 -0
- package/dist/lib/omp-config.d.ts.map +1 -0
- package/dist/lib/omp-config.js +73 -0
- package/dist/lib/omp-config.js.map +1 -0
- package/dist/lib/output.d.ts +22 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +92 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/pi-config.d.ts +27 -0
- package/dist/lib/pi-config.d.ts.map +1 -0
- package/dist/lib/pi-config.js +113 -0
- package/dist/lib/pi-config.js.map +1 -0
- package/dist/lib/registration-detect.d.ts +28 -0
- package/dist/lib/registration-detect.d.ts.map +1 -0
- package/dist/lib/registration-detect.js +563 -0
- package/dist/lib/registration-detect.js.map +1 -0
- package/dist/lib/setup-telemetry.d.ts +273 -0
- package/dist/lib/setup-telemetry.d.ts.map +1 -0
- package/dist/lib/setup-telemetry.js +309 -0
- package/dist/lib/setup-telemetry.js.map +1 -0
- package/dist/lib/signals.d.ts +14 -0
- package/dist/lib/signals.d.ts.map +1 -0
- package/dist/lib/signals.js +29 -0
- package/dist/lib/signals.js.map +1 -0
- package/dist/lib/skill-install.d.ts +10 -0
- package/dist/lib/skill-install.d.ts.map +1 -0
- package/dist/lib/skill-install.js +94 -0
- package/dist/lib/skill-install.js.map +1 -0
- package/dist/lib/skill-paths.d.ts +7 -0
- package/dist/lib/skill-paths.d.ts.map +1 -0
- package/dist/lib/skill-paths.js +37 -0
- package/dist/lib/skill-paths.js.map +1 -0
- package/dist/lib/skill-vendor.d.ts +20 -0
- package/dist/lib/skill-vendor.d.ts.map +1 -0
- package/dist/lib/skill-vendor.js +215 -0
- package/dist/lib/skill-vendor.js.map +1 -0
- package/dist/lib/skill.d.ts +14 -0
- package/dist/lib/skill.d.ts.map +1 -0
- package/dist/lib/skill.js +87 -0
- package/dist/lib/skill.js.map +1 -0
- package/dist/lib/verify.d.ts +35 -0
- package/dist/lib/verify.d.ts.map +1 -0
- package/dist/lib/verify.js +169 -0
- package/dist/lib/verify.js.map +1 -0
- package/dist/program.d.ts +8 -0
- package/dist/program.d.ts.map +1 -0
- package/dist/program.js +54 -0
- package/dist/program.js.map +1 -0
- package/package.json +55 -0
- package/skill/springbrand-action-api/SKILL.md +49 -0
- package/skill/springbrand-action-api/references/action-aliases.md +156 -0
- package/skill/springbrand-action-api/references/action-discovery.md +95 -0
- package/skill/springbrand-connector/SKILL.md +49 -0
- package/skill/springbrand-gtm/SKILL.md +34 -0
- package/skill/springbrand-platform/SKILL.md +55 -0
- package/skill/springbrand-platform/references/plugin-discovery.md +94 -0
- package/skill/use-springbrand/SKILL.md +70 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
/** 复刻自 tiny-fish CLI 编译产物:connect.d.ts + connect.impl.js */
|
|
2
|
+
import spawn from 'cross-spawn';
|
|
3
|
+
import { CONNECT_SOURCE, persistApiKeyToEnvironment, saveConnectContext, validateKeyFormat, validatedApiKey, } from '../lib/auth.js';
|
|
4
|
+
import { CURSOR_SKILL_TARGET, OPENCLAW, openclawSkillInstallArgs, NATIVE_BY_HARNESS, launchNativeMcpClient, launchOmpWalkthrough, launchPiWalkthrough, launchOpenClawWalkthrough, openExternalUrl, } from '../lib/connect-clients.js';
|
|
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';
|
|
7
|
+
import { createStdinPrompt, runCliFallback } from '../lib/connect-fallback.js';
|
|
8
|
+
import { installWebSkill } from '../lib/skill-install.js';
|
|
9
|
+
import { ConnectInterruptedError, ConnectStepError, HoistedFailureReportedError, createConnectTelemetry, finishSetupCommand, requireCommandSupport, runGuarded, settle, SignInTimeoutError, spawnStepError, stageDurationOf, withStageDuration, spawnRemoval, } from '../lib/connect-runtime.js';
|
|
10
|
+
import { cursorInstallDeeplink, cursorMcpPath, readCursorSpringbrandEntry, writeCursorMcpConfig, } from '../lib/cursor-config.js';
|
|
11
|
+
import { runConnectAll } from '../lib/connect-all.js';
|
|
12
|
+
import { ompMcpPath, readOmpSpringbrandEntry, writeOmpMcpConfig } from '../lib/omp-config.js';
|
|
13
|
+
import { PI_ADAPTER_INSTALL_COMMAND, piMcpAdapterState, piSkillDirMismatch, piMcpPath, readPiSpringbrandEntry, writePiMcpConfig, } from '../lib/pi-config.js';
|
|
14
|
+
import { ALL_HARNESSES, AuthMode, HARNESS_DISPLAY_NAMES, RELOAD_ACTION, } from '../lib/harness-detect.js';
|
|
15
|
+
import { detectHumanInitiated } from '../lib/harness.js';
|
|
16
|
+
import { SPRINGBRAND_API_KEY_VAR } from '../lib/constants.js';
|
|
17
|
+
import { emitNotice } from '../lib/notice.js';
|
|
18
|
+
import { SIGN_IN_PROBES } from '../lib/registration-detect.js';
|
|
19
|
+
import { errLine, warnLine } from '../lib/output.js';
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
import { verifyMcpAuth } from '../lib/verify.js';
|
|
22
|
+
import { gateApiKey } from '../lib/connect-preflight.js';
|
|
23
|
+
import { apiBaseFromMcpUrl, DEFAULT_MCP_URL } from '../lib/mcp-endpoint.js';
|
|
24
|
+
function removeExistingRegistration(client, removal, verbose) {
|
|
25
|
+
const result = spawnRemoval(client.command, removal.args);
|
|
26
|
+
if (result.status === 0)
|
|
27
|
+
return;
|
|
28
|
+
// Best-effort: a stale entry makes `mcp add` error or upsert, with a truer message.
|
|
29
|
+
if (!verbose)
|
|
30
|
+
return;
|
|
31
|
+
const details = result.stderr?.trim() || result.error?.message || 'unknown error';
|
|
32
|
+
warnLine(`Could not remove existing ${removal.label}: ${details}`);
|
|
33
|
+
}
|
|
34
|
+
/** false: fall back to OAuth — a config naming an unset variable authenticates nothing. */
|
|
35
|
+
function exportApiKey(apiKey, displayName, deferOutro) {
|
|
36
|
+
try {
|
|
37
|
+
persistApiKeyToEnvironment(apiKey);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
errLine(`Could not export SPRINGBRAND_API_KEY (${error instanceof Error ? error.message : String(error)}); ` +
|
|
41
|
+
'signing in instead.');
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
process.env[SPRINGBRAND_API_KEY_VAR] = apiKey;
|
|
45
|
+
if (deferOutro) {
|
|
46
|
+
deferOutro('Exported SPRINGBRAND_API_KEY to your shell profile — restart your agents (or open a new ' +
|
|
47
|
+
'terminal) before use.');
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
errLine(`Exported SPRINGBRAND_API_KEY to your shell profile — ${displayName} reads the key from there, ` +
|
|
51
|
+
'so restart it (or open a new terminal) before use.');
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/** Undefined leaves the client on its keyless path, if any. */
|
|
56
|
+
function resolveKeyedAdd(client, storedKey, keyAuthSupported, deferOutro) {
|
|
57
|
+
if (!storedKey || !keyAuthSupported || !client.keyAuth)
|
|
58
|
+
return undefined;
|
|
59
|
+
const keyed = { key: storedKey, spec: client.keyAuth };
|
|
60
|
+
// Before the removals: a failed export must not strand the old registration.
|
|
61
|
+
if (keyed.spec.envVar && !exportApiKey(keyed.key, client.displayName, deferOutro)) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return keyed;
|
|
65
|
+
}
|
|
66
|
+
/** No sign-in to degrade to, so the install stops. */
|
|
67
|
+
function refuseKeylessInstall(client, keyAuthSupported, headless = false) {
|
|
68
|
+
const noSignIn = headless
|
|
69
|
+
? 'signing in needs a terminal'
|
|
70
|
+
: `SpringBrand does not sign in to ${client.displayName} any other way`;
|
|
71
|
+
const rerun = `re-run: sp connect ${client.connectClient}`;
|
|
72
|
+
if (!client.keyAuth) {
|
|
73
|
+
throw new ConnectStepError(`${client.displayName} cannot take a SpringBrand API key, and ${noSignIn}. ` +
|
|
74
|
+
`Run: sp connect ${client.connectClient}`, 'key_auth_unsupported');
|
|
75
|
+
}
|
|
76
|
+
if (!keyAuthSupported || headless) {
|
|
77
|
+
throw new ConnectStepError(`This ${client.displayName} install cannot take a SpringBrand API key, and ${noSignIn}. ` +
|
|
78
|
+
`Update it, then ${rerun}`, 'harness_too_old');
|
|
79
|
+
}
|
|
80
|
+
throw new ConnectStepError(`Connecting ${client.displayName} needs a SpringBrand API key, and ${noSignIn}. ` +
|
|
81
|
+
`Run: sp auth login, then ${rerun}`, 'invalid_config', { failureDetail: 'keyless_install_refused' });
|
|
82
|
+
}
|
|
83
|
+
/** Resolved before the removals, so a refusal strands no registration. */
|
|
84
|
+
function keylessAddArgs(client, keyAuthSupported) {
|
|
85
|
+
if (!client.addArgs)
|
|
86
|
+
refuseKeylessInstall(client, keyAuthSupported);
|
|
87
|
+
return client.addArgs;
|
|
88
|
+
}
|
|
89
|
+
/** Runs before any write: a refused key strands nothing. */
|
|
90
|
+
async function verifyKeyBeforeSeeding(apiKey, displayName, mcpUrl) {
|
|
91
|
+
// One host serves `/mcp` and `/v1`, so `--url` decides where the key is checked.
|
|
92
|
+
const verified = await verifyMcpAuth(apiKey, apiBaseFromMcpUrl(mcpUrl));
|
|
93
|
+
if (verified.ok)
|
|
94
|
+
return;
|
|
95
|
+
// Anything but a refusal leaves the key usable, so it is reported as an outage, not a bad key.
|
|
96
|
+
if (verified.kind !== 'credential_rejected') {
|
|
97
|
+
throw new ConnectStepError(`Could not reach SpringBrand to check your API key (${verified.code ?? 'unknown'}), so ` +
|
|
98
|
+
`nothing was written to ${displayName}. Check your connection and retry.`, 'timeout');
|
|
99
|
+
}
|
|
100
|
+
// The tag carries the HTTP status; `verified.code` is authored prose.
|
|
101
|
+
throw new ConnectStepError(`Your SpringBrand API key was rejected (${verified.code ?? 'unknown'}), so it was not ` +
|
|
102
|
+
`written to ${displayName}. Run: sp auth login`, 'invalid_config', { failureDetail: `key_rejected status=${verified.status}` });
|
|
103
|
+
}
|
|
104
|
+
/** A bounded spawn's timeout means the sign-in never finished. */
|
|
105
|
+
function signInStepError(message, result, bounded) {
|
|
106
|
+
const error = spawnStepError(message, result);
|
|
107
|
+
return bounded && error.failureReason === 'timeout'
|
|
108
|
+
? new SignInTimeoutError(error.message, error.cause)
|
|
109
|
+
: error;
|
|
110
|
+
}
|
|
111
|
+
function isMissingHarness(error) {
|
|
112
|
+
return error instanceof ConnectStepError && error.failureReason === 'harness_not_installed';
|
|
113
|
+
}
|
|
114
|
+
/** Undefined means the rescue ran; the caller returns immediately. */
|
|
115
|
+
async function requireSupportOrRescue(client, connectClient, state, telemetry, options) {
|
|
116
|
+
try {
|
|
117
|
+
return requireCommandSupport(client);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (!options.fallbackWhenMissing || !isMissingHarness(error))
|
|
121
|
+
throw error;
|
|
122
|
+
await runMissingHarnessFallback(client.displayName, connectClient, state, telemetry, options);
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// The harness attempt stays failed; the rescue rides fallback_outcome (PF-3707).
|
|
127
|
+
async function runMissingHarnessFallback(displayName, connectClient, state, telemetry, options) {
|
|
128
|
+
// Stamped before any step: Ctrl+C settles before an outcome exists.
|
|
129
|
+
telemetry.setFallbackOutcome('abandoned');
|
|
130
|
+
errLine(`${displayName} was not found on PATH. Setting up the SpringBrand CLI instead.`);
|
|
131
|
+
const outcome = await runCliFallback({
|
|
132
|
+
apiKey: options.apiKey,
|
|
133
|
+
verbose: options.verbose ?? false,
|
|
134
|
+
prompt: detectHumanInitiated() ? createStdinPrompt() : undefined,
|
|
135
|
+
retryCommand: `npx @springbrand/cli connect ${connectClient}`,
|
|
136
|
+
hooks: {
|
|
137
|
+
// Terminal detail is explicit below; abort rows still name the step.
|
|
138
|
+
onStepStart: (stage) => {
|
|
139
|
+
state.stage = stage;
|
|
140
|
+
},
|
|
141
|
+
onStepDone: (phase) => telemetry.track('checkpoint', { phase }),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
telemetry.setFallbackOutcome(outcome);
|
|
145
|
+
// Never `completed`: dashboards read that as "the harness install worked".
|
|
146
|
+
settle(state, telemetry, 'failed', {
|
|
147
|
+
failedStage: 'prerequisite_check',
|
|
148
|
+
failureReason: 'harness_not_installed',
|
|
149
|
+
});
|
|
150
|
+
if (outcome !== 'cli_verified') {
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
errLine('The SpringBrand CLI is installed and working.');
|
|
155
|
+
errLine('Using a different agent? Run: sp connect --all');
|
|
156
|
+
}
|
|
157
|
+
/** Resolved before the removals, so a refusal strands no registration. */
|
|
158
|
+
async function prepareInstallAuth(client, options, keyAuthSupported) {
|
|
159
|
+
// `/mcp` accepts the key as `Authorization: Bearer`, so a stored key replaces the browser OAuth hop.
|
|
160
|
+
const storedKey = validatedApiKey(options.apiKey);
|
|
161
|
+
const keyedAdd = resolveKeyedAdd(client, storedKey, keyAuthSupported, options.deferOutro);
|
|
162
|
+
// Refuses here when key-required; others keep their OAuth argv.
|
|
163
|
+
const buildAddArgs = keyedAdd
|
|
164
|
+
? (mcpUrl) => keyedAdd.spec.addArgs(mcpUrl, keyedAdd.key)
|
|
165
|
+
: keylessAddArgs(client, keyAuthSupported);
|
|
166
|
+
if (options.keyAuthOnly && !keyedAdd)
|
|
167
|
+
refuseKeylessInstall(client, keyAuthSupported, true);
|
|
168
|
+
// Verify first: a revoked key in a foreign .env is unrecoverable.
|
|
169
|
+
if (keyedAdd?.spec.seedKey) {
|
|
170
|
+
await verifyKeyBeforeSeeding(keyedAdd.key, client.displayName, options.mcpUrl);
|
|
171
|
+
}
|
|
172
|
+
return { keyedAdd, buildAddArgs };
|
|
173
|
+
}
|
|
174
|
+
/** Inherit for an inline-OAuth add; capture otherwise, piping prompts when seeded. */
|
|
175
|
+
function addSpawnOptions(options, interactiveAdd, seeded) {
|
|
176
|
+
if (interactiveAdd)
|
|
177
|
+
return { stdio: 'inherit', timeout: options.authTimeoutMs };
|
|
178
|
+
if (!seeded)
|
|
179
|
+
return captureStdio(options.verbose ?? false);
|
|
180
|
+
return {
|
|
181
|
+
encoding: 'utf8',
|
|
182
|
+
// Without it, the 1 MiB default overflows into a misread Ctrl+C.
|
|
183
|
+
maxBuffer: STEP_MAX_BUFFER,
|
|
184
|
+
// The unattended path had no bound at all before; captureStdio's is the floor.
|
|
185
|
+
timeout: options.authTimeoutMs ?? SKILL_INSTALL_TIMEOUT_MS,
|
|
186
|
+
input: seeded.stdinInput,
|
|
187
|
+
env: seeded.env,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/** Returns the seeded-credential note when a key was written into a user-owned file. */
|
|
191
|
+
function performMcpAdd(client, options, plan, attemptId) {
|
|
192
|
+
const { keyedAdd, buildAddArgs } = plan;
|
|
193
|
+
const mcpUrl = new URL(options.mcpUrl);
|
|
194
|
+
mcpUrl.searchParams.set('source', CONNECT_SOURCE);
|
|
195
|
+
mcpUrl.searchParams.set('client', client.connectClient);
|
|
196
|
+
mcpUrl.searchParams.set('connect_attempt_id', attemptId);
|
|
197
|
+
errLine(`Adding SpringBrand to ${client.displayName}...`);
|
|
198
|
+
// Header keys ride argv (/proc/<pid>/cmdline); these CLIs take headers no other way.
|
|
199
|
+
const addArgs = buildAddArgs(mcpUrl.toString());
|
|
200
|
+
// Capturing an inline-OAuth add hides the sign-in URL and hangs.
|
|
201
|
+
const interactiveAdd = !(client.nonInteractiveAdd || keyedAdd);
|
|
202
|
+
// After the removals: an interrupt there leaves no key behind.
|
|
203
|
+
const seeded = keyedAdd?.spec.seedKey?.(keyedAdd.key);
|
|
204
|
+
// Writing the entry ourselves skips the harness probe that saves it disabled.
|
|
205
|
+
if (seeded?.register(mcpUrl.toString())) {
|
|
206
|
+
return { note: seeded.note, seededHome: seeded.home };
|
|
207
|
+
}
|
|
208
|
+
const addResult = spawn.sync(client.command, addArgs, addSpawnOptions(options, interactiveAdd, seeded));
|
|
209
|
+
// Rolls back first: signInStepError throws on a Ctrl+C.
|
|
210
|
+
const failAdd = (build) => {
|
|
211
|
+
seeded?.rollback();
|
|
212
|
+
const error = build();
|
|
213
|
+
replay(capturedOutput(addResult));
|
|
214
|
+
throw error;
|
|
215
|
+
};
|
|
216
|
+
if (addResult.error || addResult.status !== 0) {
|
|
217
|
+
failAdd(() => signInStepError(`Could not add SpringBrand to ${client.displayName}`, addResult, interactiveAdd && options.authTimeoutMs !== undefined));
|
|
218
|
+
}
|
|
219
|
+
const registration = seeded?.confirmRegistered();
|
|
220
|
+
if (registration && !registration.ok) {
|
|
221
|
+
failAdd(() => new ConnectStepError(`${client.displayName} reported success but SpringBrand is not enabled there` +
|
|
222
|
+
`${registration.detail ? `: ${registration.detail}` : ''}.`, 'invalid_config',
|
|
223
|
+
// The tag keeps the path-bearing prose out of telemetry.
|
|
224
|
+
{ failureDetail: registration.tag }));
|
|
225
|
+
}
|
|
226
|
+
return { note: seeded?.note, seededHome: seeded?.home };
|
|
227
|
+
}
|
|
228
|
+
function performLoginStep(client, pendingLogin, options) {
|
|
229
|
+
errLine('Signing in to SpringBrand...');
|
|
230
|
+
const loginResult = spawn.sync(client.command, [...pendingLogin], {
|
|
231
|
+
stdio: 'inherit',
|
|
232
|
+
timeout: options.authTimeoutMs,
|
|
233
|
+
});
|
|
234
|
+
if (loginResult.error || loginResult.status !== 0) {
|
|
235
|
+
throw signInStepError(`Could not authenticate SpringBrand in ${client.displayName}`, loginResult, options.authTimeoutMs !== undefined);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function announceAuthNotes(useKeyAuth, degradedNotes, seededNote) {
|
|
239
|
+
if (useKeyAuth) {
|
|
240
|
+
errLine(degradedNotes?.keyed ?? 'Using your stored SpringBrand API key — no browser sign-in needed.');
|
|
241
|
+
// Every other credential the CLI writes into a user-owned file says so too.
|
|
242
|
+
if (seededNote)
|
|
243
|
+
errLine(seededNote);
|
|
244
|
+
}
|
|
245
|
+
else if (degradedNotes) {
|
|
246
|
+
// Printed here, not with the closing line, so a `--launch` run sees it before the handover.
|
|
247
|
+
errLine(degradedNotes.deferred);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function completeAuthFlow(client, options, state, telemetry, ctx) {
|
|
251
|
+
const { useKeyAuth, loginDegraded, loginArgs, probeAuthenticated } = ctx;
|
|
252
|
+
// Codex runs OAuth inside `mcp add` and exits 0 either way, so the recorded state decides
|
|
253
|
+
// whether the login step below still runs. Unknown keeps the pre-probe behaviour.
|
|
254
|
+
const addSignedIn = probeAuthenticated?.();
|
|
255
|
+
if (addSignedIn)
|
|
256
|
+
telemetry.track('checkpoint', { phase: 'oauth_done' });
|
|
257
|
+
announceAuthNotes(useKeyAuth, loginDegraded ? client.degradedAuthNotes : undefined, ctx.seededNote);
|
|
258
|
+
// A probed client signs in again only when its inline OAuth is known to have been abandoned.
|
|
259
|
+
const retryLogin = probeAuthenticated ? addSignedIn === false : true;
|
|
260
|
+
const pendingLogin = !useKeyAuth && retryLogin ? loginArgs : undefined;
|
|
261
|
+
let signInDeferred = !useKeyAuth && !addSignedIn && !pendingLogin;
|
|
262
|
+
if (pendingLogin) {
|
|
263
|
+
state.stage = 'client_oauth';
|
|
264
|
+
performLoginStep(client, pendingLogin, options);
|
|
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;
|
|
275
|
+
return { authMode, signInDeferred };
|
|
276
|
+
}
|
|
277
|
+
async function connectNativeMcpClient(client, options) {
|
|
278
|
+
const telemetry = createConnectTelemetry(options.mcpUrl, client.connectClient, options);
|
|
279
|
+
const state = { stage: 'prerequisite_check', settled: false };
|
|
280
|
+
const probeAuthenticated = SIGN_IN_PROBES[client.connectClient];
|
|
281
|
+
await runGuarded(state, telemetry, async () => {
|
|
282
|
+
telemetry.track('started');
|
|
283
|
+
const support = await requireSupportOrRescue(client, client.connectClient, state, telemetry, options);
|
|
284
|
+
if (!support)
|
|
285
|
+
return;
|
|
286
|
+
const { optionalSupported, keyAuthSupported, harnessVersion } = support;
|
|
287
|
+
telemetry.setHarnessVersion(harnessVersion);
|
|
288
|
+
state.harnessDegraded = !!client.loginArgs && !optionalSupported;
|
|
289
|
+
const plan = await prepareInstallAuth(client, options, keyAuthSupported);
|
|
290
|
+
const useKeyAuth = !!plan.keyedAdd;
|
|
291
|
+
telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
|
|
292
|
+
state.stage = 'registration_cleanup';
|
|
293
|
+
for (const removal of client.removals)
|
|
294
|
+
removeExistingRegistration(client, removal, options.verbose ?? false);
|
|
295
|
+
// Undefining `loginArgs` reuses the deferred-auth path codex/hermes already take.
|
|
296
|
+
const loginDegraded = state.harnessDegraded === true;
|
|
297
|
+
const loginArgs = loginDegraded ? undefined : client.loginArgs;
|
|
298
|
+
// A probed client authenticates inside `mcp add` too, so a failure there is either.
|
|
299
|
+
state.stage =
|
|
300
|
+
loginArgs && !probeAuthenticated ? 'registration' : 'registration_or_authentication';
|
|
301
|
+
const { note: seededNote, seededHome } = performMcpAdd(client, options, plan, telemetry.attemptId);
|
|
302
|
+
telemetry.track('checkpoint', { phase: 'registered' });
|
|
303
|
+
const { authMode, signInDeferred } = completeAuthFlow(client, options, state, telemetry, {
|
|
304
|
+
useKeyAuth,
|
|
305
|
+
loginDegraded,
|
|
306
|
+
loginArgs,
|
|
307
|
+
probeAuthenticated,
|
|
308
|
+
seededNote,
|
|
309
|
+
});
|
|
310
|
+
state.authMode = authMode;
|
|
311
|
+
// Registration/OAuth make MCP work; later steps are cosmetic and must not fail the attempt.
|
|
312
|
+
settle(state, telemetry, 'completed', { authMode });
|
|
313
|
+
await runPostInstallSteps(client, options, state, telemetry, signInDeferred, seededHome);
|
|
314
|
+
});
|
|
315
|
+
return state.authMode;
|
|
316
|
+
}
|
|
317
|
+
function connectedLine(client, signInDeferred) {
|
|
318
|
+
// The CLI never observes deferred auth, so don't claim "connected".
|
|
319
|
+
if (signInDeferred) {
|
|
320
|
+
if (client.signInHint)
|
|
321
|
+
return client.signInHint;
|
|
322
|
+
return (`SpringBrand is configured in ${client.displayName}. ${client.displayName} will ask you ` +
|
|
323
|
+
'to sign in to SpringBrand the first time you use it.');
|
|
324
|
+
}
|
|
325
|
+
return (`SpringBrand is connected. Open ${client.displayName}; if it is already running, ` +
|
|
326
|
+
`${RELOAD_ACTION[client.connectClient]} to pick up the tools.`);
|
|
327
|
+
}
|
|
328
|
+
// Runs after `completed` settled: steps fail alone, the rest still run.
|
|
329
|
+
async function runPostInstallSteps(client, options, state, telemetry, signInDeferred, seededHome) {
|
|
330
|
+
let failed = false;
|
|
331
|
+
// Modelled on upgrade's attempt(); interrupts rethrow and abandon remaining steps.
|
|
332
|
+
const attempt = async (stage, run) => {
|
|
333
|
+
state.stage = stage;
|
|
334
|
+
try {
|
|
335
|
+
await run();
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
if (error instanceof ConnectInterruptedError)
|
|
339
|
+
throw error;
|
|
340
|
+
failed = true;
|
|
341
|
+
trackPostInstallFailure(client.displayName, state, telemetry, error);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
try {
|
|
345
|
+
if (options.cliInstall !== false) {
|
|
346
|
+
await attempt('cli_install', () => runCliInstallStep(options, telemetry));
|
|
347
|
+
}
|
|
348
|
+
if (client.skillAgent) {
|
|
349
|
+
await attempt('skill_install', () => {
|
|
350
|
+
installWebSkill(client, { verbose: options.verbose ?? false });
|
|
351
|
+
telemetry.track('checkpoint', { phase: 'skill_installed' });
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const extraPostInstall = client.extraPostInstall;
|
|
355
|
+
if (extraPostInstall) {
|
|
356
|
+
await attempt('plugin_install', () => {
|
|
357
|
+
extraPostInstall({
|
|
358
|
+
apiKey: validatedApiKey(options.apiKey),
|
|
359
|
+
verbose: options.verbose ?? false,
|
|
360
|
+
seededHome,
|
|
361
|
+
});
|
|
362
|
+
telemetry.track('checkpoint', {
|
|
363
|
+
phase: 'plugin_installed',
|
|
364
|
+
pluginVersion: client.pluginVersion,
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
// Before auth: a failed sign-in must not lose the harness record.
|
|
369
|
+
saveConnectContext(client.connectClient, telemetry.attemptId, state.authMode);
|
|
370
|
+
await attempt('authentication', () => {
|
|
371
|
+
// A set timeout means headless; never shell an interactive login.
|
|
372
|
+
ensureCliAuthenticated(client.connectClient, options.apiKey, {
|
|
373
|
+
interactiveLogin: options.authTimeoutMs === undefined,
|
|
374
|
+
});
|
|
375
|
+
telemetry.track('checkpoint', { phase: 'authenticated' });
|
|
376
|
+
});
|
|
377
|
+
if (client.postConnectNote)
|
|
378
|
+
errLine(client.postConnectNote);
|
|
379
|
+
if (!options.launch) {
|
|
380
|
+
// Under --all the summary replaces this; sign-in hints still print.
|
|
381
|
+
if (signInDeferred || !options.deferOutro)
|
|
382
|
+
errLine(connectedLine(client, signInDeferred));
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
await attempt('walkthrough_launch', async () => {
|
|
386
|
+
// Before the handover, or the only sign-in instructions scroll past under the agent.
|
|
387
|
+
if (signInDeferred && client.signInHint)
|
|
388
|
+
errLine(client.signInHint);
|
|
389
|
+
await launchNativeMcpClient(client, async (outcome) => {
|
|
390
|
+
telemetry.track('checkpoint', { phase: walkthroughPhase(outcome) });
|
|
391
|
+
// Flushed pre-block; the session-long handover would strand the delivery.
|
|
392
|
+
await telemetry.flush();
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
// Interrupts stop quietly; anything else escapes to runGuarded.
|
|
399
|
+
if (!(error instanceof ConnectInterruptedError))
|
|
400
|
+
throw error;
|
|
401
|
+
failed = true;
|
|
402
|
+
}
|
|
403
|
+
if (failed) {
|
|
404
|
+
errLine(finishSetupHint(client.connectClient));
|
|
405
|
+
options.onPostInstallFailed?.();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/** Fails on a carried failure; only the first take reports anything. */
|
|
409
|
+
function takeHoistedCliInstall(take, telemetry) {
|
|
410
|
+
const { outcome, firstTake } = take();
|
|
411
|
+
if (!outcome.ok) {
|
|
412
|
+
// Late takers fail too; only the first reports the install.
|
|
413
|
+
if (!firstTake)
|
|
414
|
+
throw new HoistedFailureReportedError(outcome.error.message);
|
|
415
|
+
throw withStageDuration(outcome.error, outcome.durationMs);
|
|
416
|
+
}
|
|
417
|
+
if (firstTake) {
|
|
418
|
+
telemetry.track('checkpoint', {
|
|
419
|
+
phase: 'cli_installed',
|
|
420
|
+
stageDurationMs: outcome.durationMs,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
/** Consumes the `--all` hoist when present; installs inline otherwise. */
|
|
425
|
+
function runCliInstallStep(options, telemetry) {
|
|
426
|
+
if (options.hoistedCliInstall) {
|
|
427
|
+
takeHoistedCliInstall(options.hoistedCliInstall, telemetry);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
installSpringBrandCli({ verbose: options.verbose ?? false, announce: true });
|
|
431
|
+
telemetry.track('checkpoint', { phase: 'cli_installed' });
|
|
432
|
+
}
|
|
433
|
+
function walkthroughPhase(outcome) {
|
|
434
|
+
return outcome === 'printed' ? 'walkthrough_prompt_printed' : 'walkthrough_launched';
|
|
435
|
+
}
|
|
436
|
+
function finishSetupHint(connectClient) {
|
|
437
|
+
return `Finish setup with: ${finishSetupCommand(connectClient)}`;
|
|
438
|
+
}
|
|
439
|
+
/** Warn + report post_install_failed; interrupts are abandonment, not failure. */
|
|
440
|
+
function trackPostInstallFailure(displayName, state, telemetry, error) {
|
|
441
|
+
if (error instanceof ConnectInterruptedError)
|
|
442
|
+
return;
|
|
443
|
+
// An earlier harness already owns the shared install's failure record.
|
|
444
|
+
if (!(error instanceof HoistedFailureReportedError)) {
|
|
445
|
+
telemetry.track('post_install_failed', {
|
|
446
|
+
failedStage: state.stage,
|
|
447
|
+
failureReason: error instanceof ConnectStepError ? error.failureReason : 'unexpected_error',
|
|
448
|
+
failureDetail: error instanceof ConnectStepError ? (error.failureDetail ?? error.message) : undefined,
|
|
449
|
+
stageDurationMs: stageDurationOf(error),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
errLine(`The ${displayName} MCP connection succeeded, but a finishing step ` +
|
|
453
|
+
`(${state.stage}) failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
454
|
+
}
|
|
455
|
+
/** Skill install only: no key handoff or post-install callback to honour. */
|
|
456
|
+
export async function connectOpenClaw(options) {
|
|
457
|
+
const telemetry = createConnectTelemetry(options.mcpUrl, 'openclaw', options);
|
|
458
|
+
// Skill install, no MCP server — never degraded, and false keeps it inside a `= false` filter.
|
|
459
|
+
const state = {
|
|
460
|
+
stage: 'prerequisite_check',
|
|
461
|
+
settled: false,
|
|
462
|
+
harnessDegraded: false,
|
|
463
|
+
};
|
|
464
|
+
await runGuarded(state, telemetry, async () => {
|
|
465
|
+
telemetry.track('started');
|
|
466
|
+
const support = await requireSupportOrRescue(OPENCLAW, 'openclaw', state, telemetry, options);
|
|
467
|
+
if (!support)
|
|
468
|
+
return;
|
|
469
|
+
telemetry.setHarnessVersion(support.harnessVersion);
|
|
470
|
+
telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
|
|
471
|
+
if (options.cliInstall !== false) {
|
|
472
|
+
state.stage = 'cli_install';
|
|
473
|
+
runCliInstallStep(options, telemetry);
|
|
474
|
+
}
|
|
475
|
+
state.stage = 'skill_install';
|
|
476
|
+
errLine('Installing the SpringBrand skill in OpenClaw...');
|
|
477
|
+
const skillInstallResult = spawn.sync('openclaw', openclawSkillInstallArgs(support.matchedVariant), captureStdio(options.verbose ?? false));
|
|
478
|
+
if (skillInstallResult.error || skillInstallResult.status !== 0) {
|
|
479
|
+
// Built first: its interrupt check must run before any replay.
|
|
480
|
+
const error = spawnStepError('Could not install the SpringBrand skill in OpenClaw', skillInstallResult);
|
|
481
|
+
replay(capturedOutput(skillInstallResult));
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
telemetry.track('checkpoint', { phase: 'skill_installed' });
|
|
485
|
+
// Stores the key before probing, so a passed --api-key never reaches an interactive login.
|
|
486
|
+
state.stage = 'authentication';
|
|
487
|
+
// A set timeout means headless; never shell an interactive login.
|
|
488
|
+
ensureCliAuthenticated('openclaw', options.apiKey, {
|
|
489
|
+
interactiveLogin: options.authTimeoutMs === undefined,
|
|
490
|
+
});
|
|
491
|
+
telemetry.track('checkpoint', { phase: 'authenticated' });
|
|
492
|
+
saveConnectContext('openclaw', telemetry.attemptId);
|
|
493
|
+
// For openclaw the skill install and auth ARE functional; only the walkthrough is cosmetic.
|
|
494
|
+
settle(state, telemetry, 'completed');
|
|
495
|
+
if (!options.launch) {
|
|
496
|
+
// Under --all the summary already lists OpenClaw.
|
|
497
|
+
if (!options.deferOutro) {
|
|
498
|
+
errLine('SpringBrand is connected. Open OpenClaw; if it is already running, ' +
|
|
499
|
+
`${RELOAD_ACTION.openclaw} to pick up the skill.`);
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
state.stage = 'walkthrough_launch';
|
|
504
|
+
try {
|
|
505
|
+
await launchOpenClawWalkthrough(async (outcome) => {
|
|
506
|
+
telemetry.track('checkpoint', { phase: walkthroughPhase(outcome) });
|
|
507
|
+
// Flushed pre-block; the session-long handover would strand the delivery.
|
|
508
|
+
await telemetry.flush();
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
trackPostInstallFailure('OpenClaw', state, telemetry, error);
|
|
513
|
+
errLine(finishSetupHint('openclaw'));
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
const PI_RELOAD_COPY = 'SpringBrand is configured in pi. Restart pi, then run `/mcp-auth springbrand` there if it asks you ' +
|
|
518
|
+
'to sign in.';
|
|
519
|
+
// The skill is the working path either way, so the note must not read as a failure.
|
|
520
|
+
const PI_ADAPTER_DORMANT_NOTE = 'Note: pi has no built-in MCP support, so the entry in its mcp.json stays dormant until you ' +
|
|
521
|
+
`run \`${PI_ADAPTER_INSTALL_COMMAND}\`. The SpringBrand skill works without it.`;
|
|
522
|
+
// Only the dormant case was spoken for, leaving a working MCP install silent.
|
|
523
|
+
const PI_ADAPTER_LIVE_NOTE = 'pi-mcp-adapter is installed, so pi loads the SpringBrand MCP tools as well as the skill once ' +
|
|
524
|
+
'you restart it.';
|
|
525
|
+
function piAdapterNote() {
|
|
526
|
+
const state = piMcpAdapterState();
|
|
527
|
+
if (state === 'installed')
|
|
528
|
+
return PI_ADAPTER_LIVE_NOTE;
|
|
529
|
+
return state === 'absent' ? PI_ADAPTER_DORMANT_NOTE : undefined;
|
|
530
|
+
}
|
|
531
|
+
// The `skills` CLI writes to the default dir even when pi reads another one.
|
|
532
|
+
function piPostConnectNotes() {
|
|
533
|
+
const notes = [piAdapterNote()];
|
|
534
|
+
const skills = piSkillDirMismatch();
|
|
535
|
+
if (skills) {
|
|
536
|
+
notes.push(`Note: PI_CODING_AGENT_DIR points pi at ${skills.readFrom}, but the SpringBrand skill ` +
|
|
537
|
+
`installed to ${skills.installedTo}. Copy or symlink it across for pi to load it.`);
|
|
538
|
+
}
|
|
539
|
+
const printable = notes.filter(Boolean);
|
|
540
|
+
return printable.length > 0 ? printable.join('\n') : undefined;
|
|
541
|
+
}
|
|
542
|
+
// Only interactive omp wires the OAuth handler; headless cannot sign in.
|
|
543
|
+
const OMP_KEYLESS_COPY = 'SpringBrand is configured in omp. Open omp and sign in to SpringBrand when prompted, or run ' +
|
|
544
|
+
'`/mcp reauth springbrand` there.';
|
|
545
|
+
const CONFIG_FILE_HARNESSES = {
|
|
546
|
+
cursor: {
|
|
547
|
+
skillTarget: CURSOR_SKILL_TARGET,
|
|
548
|
+
registeredUrl: () => readCursorSpringbrandEntry().url,
|
|
549
|
+
write: writeCursorMcpConfig,
|
|
550
|
+
configPath: cursorMcpPath,
|
|
551
|
+
tryLaunch: launchCursorDeeplink,
|
|
552
|
+
copy: {
|
|
553
|
+
verified: "SpringBrand is connected and verified (health+auth). Reload the Cursor window (or restart cursor-agent) and it's live.",
|
|
554
|
+
authFailed: (reason) => `SpringBrand is configured, but the authenticated check failed (${reason}). ` +
|
|
555
|
+
'Fix: rotate/re-enter your key with `sp auth login`, then re-run: sp connect cursor',
|
|
556
|
+
launched: 'Cursor should pop a SpringBrand install confirmation — approve it, then sign in when prompted.',
|
|
557
|
+
reload: 'SpringBrand is connected. Reload the Cursor window, then approve/sign in under Settings → MCP.',
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
omp: {
|
|
561
|
+
// No skill exists for omp; installWebSkill returns without one.
|
|
562
|
+
skillTarget: { displayName: HARNESS_DISPLAY_NAMES.omp },
|
|
563
|
+
registeredUrl: () => readOmpSpringbrandEntry()?.url,
|
|
564
|
+
write: writeOmpMcpConfig,
|
|
565
|
+
configPath: ompMcpPath,
|
|
566
|
+
// No install deeplink exists; the keyless copy carries the sign-in.
|
|
567
|
+
tryLaunch: () => false,
|
|
568
|
+
copy: {
|
|
569
|
+
verified: "SpringBrand is connected and verified (health+auth). Restart omp and it's live.",
|
|
570
|
+
authFailed: (reason) => `SpringBrand is configured, but the authenticated check failed (${reason}). ` +
|
|
571
|
+
'Fix: rotate/re-enter your key with `sp auth login`, then re-run: sp connect omp',
|
|
572
|
+
launched: OMP_KEYLESS_COPY,
|
|
573
|
+
reload: OMP_KEYLESS_COPY,
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
pi: {
|
|
577
|
+
skillTarget: { skillAgent: 'pi', displayName: HARNESS_DISPLAY_NAMES.pi },
|
|
578
|
+
registeredUrl: () => readPiSpringbrandEntry().url,
|
|
579
|
+
write: writePiMcpConfig,
|
|
580
|
+
configPath: piMcpPath,
|
|
581
|
+
// No install deeplink exists; the copy carries the restart instead.
|
|
582
|
+
tryLaunch: () => false,
|
|
583
|
+
postConnectNote: piPostConnectNotes,
|
|
584
|
+
copy: {
|
|
585
|
+
verified: "SpringBrand is connected and verified (health+auth). Restart pi and it's live.",
|
|
586
|
+
authFailed: (reason) => `SpringBrand is configured, but the authenticated check failed (${reason}). ` +
|
|
587
|
+
'Fix: rotate/re-enter your key with `sp auth login`, then re-run: sp connect pi',
|
|
588
|
+
launched: PI_RELOAD_COPY,
|
|
589
|
+
reload: PI_RELOAD_COPY,
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
/** Cosmetic and post-settle: a skill failure must not fail the connection. */
|
|
594
|
+
function installSkillPostSettle(spec, displayName, state, telemetry, options) {
|
|
595
|
+
if (!spec.skillTarget.skillAgent)
|
|
596
|
+
return false;
|
|
597
|
+
state.stage = 'skill_install';
|
|
598
|
+
try {
|
|
599
|
+
installWebSkill(spec.skillTarget, { verbose: options.verbose ?? false });
|
|
600
|
+
telemetry.track('checkpoint', { phase: 'skill_installed' });
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
catch (error) {
|
|
604
|
+
trackPostInstallFailure(displayName, state, telemetry, error);
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/** Prints last: the outro must own the final line. */
|
|
609
|
+
function reportPostInstallFailure(harness, failed, onPostInstallFailed) {
|
|
610
|
+
if (!failed)
|
|
611
|
+
return;
|
|
612
|
+
errLine(finishSetupHint(harness));
|
|
613
|
+
onPostInstallFailed?.();
|
|
614
|
+
}
|
|
615
|
+
/** Writes the harness's MCP config file itself; no `mcp add` exists. */
|
|
616
|
+
async function connectConfigFileHarness(harness, options) {
|
|
617
|
+
const spec = CONFIG_FILE_HARNESSES[harness];
|
|
618
|
+
const displayName = HARNESS_DISPLAY_NAMES[harness];
|
|
619
|
+
let fallbackAttemptId;
|
|
620
|
+
const registeredUrl = spec.registeredUrl();
|
|
621
|
+
if (registeredUrl) {
|
|
622
|
+
try {
|
|
623
|
+
const registered = new URL(registeredUrl);
|
|
624
|
+
const requested = new URL(options.mcpUrl);
|
|
625
|
+
const registeredId = registered.searchParams.get('connect_attempt_id');
|
|
626
|
+
const registeredSource = registered.searchParams.get('source');
|
|
627
|
+
const registeredClient = registered.searchParams.get('client');
|
|
628
|
+
for (const key of ['source', 'client', 'connect_attempt_id']) {
|
|
629
|
+
registered.searchParams.delete(key);
|
|
630
|
+
requested.searchParams.delete(key);
|
|
631
|
+
}
|
|
632
|
+
registered.searchParams.sort();
|
|
633
|
+
requested.searchParams.sort();
|
|
634
|
+
if (registered.toString() === requested.toString() &&
|
|
635
|
+
registeredSource === CONNECT_SOURCE &&
|
|
636
|
+
registeredClient === harness &&
|
|
637
|
+
z.uuid().safeParse(registeredId).success) {
|
|
638
|
+
fallbackAttemptId = registeredId ?? undefined;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
catch {
|
|
642
|
+
// A hand-edited invalid URL is repaired below with a fresh attempt id.
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const telemetry = createConnectTelemetry(options.mcpUrl, harness, {
|
|
646
|
+
...options,
|
|
647
|
+
fallbackAttemptId,
|
|
648
|
+
});
|
|
649
|
+
// Never degraded: no sign-in command to be missing. False, not absent, so a `= false`
|
|
650
|
+
// filter still catches these harnesses.
|
|
651
|
+
const state = {
|
|
652
|
+
stage: 'prerequisite_check',
|
|
653
|
+
settled: false,
|
|
654
|
+
harnessDegraded: false,
|
|
655
|
+
};
|
|
656
|
+
await runGuarded(state, telemetry, async () => {
|
|
657
|
+
telemetry.track('started');
|
|
658
|
+
telemetry.track('checkpoint', { phase: 'prerequisite_ok' });
|
|
659
|
+
// Auth first: a key lets the config carry an Authorization header, removing the sign-in hand-back.
|
|
660
|
+
state.stage = 'authentication';
|
|
661
|
+
// A set timeout means headless; never shell an interactive login.
|
|
662
|
+
ensureCliAuthenticated(harness, options.apiKey, {
|
|
663
|
+
interactiveLogin: options.authTimeoutMs === undefined,
|
|
664
|
+
});
|
|
665
|
+
telemetry.track('checkpoint', { phase: 'authenticated' });
|
|
666
|
+
const resolvedKey = validatedApiKey(options.apiKey);
|
|
667
|
+
// ensureCliAuthenticated leaves a key stored, and `auth status` exits 0 on a malformed one.
|
|
668
|
+
if (!resolvedKey) {
|
|
669
|
+
errLine('Ignoring the stored API key: invalid format. Run: sp auth login');
|
|
670
|
+
}
|
|
671
|
+
state.stage = 'registration';
|
|
672
|
+
errLine(`Adding SpringBrand to ${displayName}...`);
|
|
673
|
+
const mcpUrl = new URL(options.mcpUrl);
|
|
674
|
+
mcpUrl.searchParams.set('source', CONNECT_SOURCE);
|
|
675
|
+
mcpUrl.searchParams.set('client', harness);
|
|
676
|
+
mcpUrl.searchParams.set('connect_attempt_id', telemetry.attemptId);
|
|
677
|
+
const result = spec.write(mcpUrl.toString(), resolvedKey);
|
|
678
|
+
if (result.status === 'corrupt_skip') {
|
|
679
|
+
// The message fallback would leak the config path; tag instead.
|
|
680
|
+
throw new ConnectStepError(`Could not update ${spec.configPath()}: existing file could not be read or is not valid JSON (${result.error}). ` +
|
|
681
|
+
`Fix the file, then re-run: sp connect ${harness}`, 'invalid_config', { failureDetail: `${harness}_mcp_json_corrupt` });
|
|
682
|
+
}
|
|
683
|
+
if (result.backupPath) {
|
|
684
|
+
errLine(`Backed up existing ${displayName} MCP config to ${result.backupPath}`);
|
|
685
|
+
}
|
|
686
|
+
if (result.repaired) {
|
|
687
|
+
errLine('Repaired the existing SpringBrand entry (updated auth/config to current).');
|
|
688
|
+
}
|
|
689
|
+
telemetry.track('checkpoint', { phase: 'registered' });
|
|
690
|
+
saveConnectContext(harness, telemetry.attemptId);
|
|
691
|
+
settle(state, telemetry, 'completed', { authMode: resolvedKey ? AuthMode.ApiKey : 'deferred' });
|
|
692
|
+
const postInstallFailed = installSkillPostSettle(spec, displayName, state, telemetry, options);
|
|
693
|
+
if (resolvedKey) {
|
|
694
|
+
// Deeplink would embed the key in a URL (process args, LaunchServices logs) — reload instead.
|
|
695
|
+
// `/mcp` is the gateway's only endpoint, so `--url` also decides where the key is checked.
|
|
696
|
+
const verify = await verifyMcpAuth(resolvedKey, apiBaseFromMcpUrl(mcpUrl.toString()));
|
|
697
|
+
if (verify.ok) {
|
|
698
|
+
errLine(spec.copy.verified);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
errLine(spec.copy.authFailed(verify.reason ?? 'unknown'));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
else if (detectHumanInitiated() && spec.tryLaunch(mcpUrl.toString())) {
|
|
705
|
+
errLine(spec.copy.launched);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
errLine(spec.copy.reload);
|
|
709
|
+
}
|
|
710
|
+
const note = spec.postConnectNote?.();
|
|
711
|
+
if (note)
|
|
712
|
+
errLine(note);
|
|
713
|
+
reportPostInstallFailure(harness, postInstallFailed, options.onPostInstallFailed);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
/** Writes mcp.json directly — no `cursor mcp add` exists. */
|
|
717
|
+
export async function connectCursor(options) {
|
|
718
|
+
await connectConfigFileHarness('cursor', options);
|
|
719
|
+
}
|
|
720
|
+
/** Best-effort: false when no handler/open fails — caller falls back to reload copy. */
|
|
721
|
+
function launchCursorDeeplink(mcpUrl) {
|
|
722
|
+
const result = openExternalUrl(cursorInstallDeeplink(mcpUrl));
|
|
723
|
+
return !result.error && result.status === 0;
|
|
724
|
+
}
|
|
725
|
+
/** Total over the non-native harnesses: a new one fails to compile until wired here. */
|
|
726
|
+
const LAUNCH_OVERRIDES = {
|
|
727
|
+
omp: launchOmpWalkthrough,
|
|
728
|
+
pi: launchPiWalkthrough,
|
|
729
|
+
openclaw: launchOpenClawWalkthrough,
|
|
730
|
+
// Cursor has no chat CLI to launch.
|
|
731
|
+
cursor: () => {
|
|
732
|
+
throw new Error('Cursor has no launchable walkthrough. Open Cursor and start chatting.');
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
const isNonNative = (harness) => harness in LAUNCH_OVERRIDES;
|
|
736
|
+
export async function launchAgent(client) {
|
|
737
|
+
if (isNonNative(client))
|
|
738
|
+
return void (await LAUNCH_OVERRIDES[client]());
|
|
739
|
+
await launchNativeMcpClient(requireNative(client));
|
|
740
|
+
}
|
|
741
|
+
/** Total over the non-native harnesses; native harnesses use the descriptor engine. */
|
|
742
|
+
const CONNECTOR_OVERRIDES = {
|
|
743
|
+
openclaw: async (options) => {
|
|
744
|
+
await connectOpenClaw(options);
|
|
745
|
+
return undefined;
|
|
746
|
+
},
|
|
747
|
+
cursor: async (options) => {
|
|
748
|
+
await connectCursor(options);
|
|
749
|
+
// `connect cursor --launch` still connects; it just cannot launch after.
|
|
750
|
+
if (options.launch) {
|
|
751
|
+
errLine('Cursor has no launchable walkthrough. Open Cursor and start chatting.');
|
|
752
|
+
}
|
|
753
|
+
return undefined;
|
|
754
|
+
},
|
|
755
|
+
omp: async (options) => {
|
|
756
|
+
await connectConfigFileHarness('omp', options);
|
|
757
|
+
if (options.launch) {
|
|
758
|
+
try {
|
|
759
|
+
await launchOmpWalkthrough();
|
|
760
|
+
}
|
|
761
|
+
catch (error) {
|
|
762
|
+
// Post-settle, like the native walkthrough step: never fail the connect.
|
|
763
|
+
errLine(error instanceof Error ? error.message : String(error));
|
|
764
|
+
errLine(finishSetupHint('omp'));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return undefined;
|
|
768
|
+
},
|
|
769
|
+
pi: async (options) => {
|
|
770
|
+
await connectConfigFileHarness('pi', options);
|
|
771
|
+
if (options.launch) {
|
|
772
|
+
try {
|
|
773
|
+
await launchPiWalkthrough();
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
errLine(error instanceof Error ? error.message : String(error));
|
|
777
|
+
errLine(finishSetupHint('pi'));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return undefined;
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
function requireNative(harness) {
|
|
784
|
+
const client = NATIVE_BY_HARNESS.get(harness);
|
|
785
|
+
if (!client)
|
|
786
|
+
throw new Error(`No native MCP descriptor for ${harness}`);
|
|
787
|
+
return client;
|
|
788
|
+
}
|
|
789
|
+
export async function connectHarness(harness, options) {
|
|
790
|
+
if (isNonNative(harness))
|
|
791
|
+
return CONNECTOR_OVERRIDES[harness](options);
|
|
792
|
+
return connectNativeMcpClient(requireNative(harness), options);
|
|
793
|
+
}
|
|
794
|
+
function parseMcpUrlOption(options) {
|
|
795
|
+
let mcpUrl = options.url ?? DEFAULT_MCP_URL;
|
|
796
|
+
const parsedUrl = URL.parse(mcpUrl);
|
|
797
|
+
if (mcpUrl.startsWith('-') || !parsedUrl || !['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
798
|
+
throw new Error(`Invalid --url value: ${mcpUrl}`);
|
|
799
|
+
}
|
|
800
|
+
// Page-minted attempt id rides in on --url. Strip before the URL becomes the MCP
|
|
801
|
+
// base — registration re-stamps it, and nothing else may carry it.
|
|
802
|
+
const pageAttemptId = parsedUrl.searchParams.get('connect_attempt_id');
|
|
803
|
+
let attemptId;
|
|
804
|
+
if (pageAttemptId !== null) {
|
|
805
|
+
parsedUrl.searchParams.delete('connect_attempt_id');
|
|
806
|
+
mcpUrl = parsedUrl.toString();
|
|
807
|
+
if (z.uuid().safeParse(pageAttemptId).success)
|
|
808
|
+
attemptId = pageAttemptId;
|
|
809
|
+
}
|
|
810
|
+
return { mcpUrl, attemptId };
|
|
811
|
+
}
|
|
812
|
+
/** Bare `connect` offers the picker; `--all` takes the dry-run/uninstall/skip-launch flags. */
|
|
813
|
+
async function connectManyPath(options, mcpUrl, attemptId) {
|
|
814
|
+
if (options.launch) {
|
|
815
|
+
throw new Error('--launch is not supported with --all. Connect a single client to launch it.');
|
|
816
|
+
}
|
|
817
|
+
const pick = !options.all;
|
|
818
|
+
const humanInitiated = pick && detectHumanInitiated();
|
|
819
|
+
if (pick && !humanInitiated) {
|
|
820
|
+
errLine('No terminal detected: connecting every detected harness (same as sp connect --all).');
|
|
821
|
+
}
|
|
822
|
+
const exitCode = await runConnectAll({
|
|
823
|
+
apiKey: options.apiKey,
|
|
824
|
+
mcpUrl,
|
|
825
|
+
attemptId,
|
|
826
|
+
dryRun: options.dryRun ?? false,
|
|
827
|
+
uninstall: options.uninstall ?? false,
|
|
828
|
+
noLaunch: options.skipLaunch === true,
|
|
829
|
+
pick: humanInitiated,
|
|
830
|
+
verbose: options.verbose ?? false,
|
|
831
|
+
});
|
|
832
|
+
// Preserve the 130 runGuarded set on Ctrl+C.
|
|
833
|
+
if (!process.exitCode)
|
|
834
|
+
process.exitCode = exitCode;
|
|
835
|
+
}
|
|
836
|
+
async function connectSingleClient(client, options, mcpUrl, attemptId) {
|
|
837
|
+
const connectOptions = {
|
|
838
|
+
apiKey: options.apiKey,
|
|
839
|
+
mcpUrl,
|
|
840
|
+
launch: options.launch ?? false,
|
|
841
|
+
attemptId,
|
|
842
|
+
// A key that the install cannot use leaves only a browser hop no agent can finish.
|
|
843
|
+
keyAuthOnly: !detectHumanInitiated() && !!validatedApiKey(options.apiKey),
|
|
844
|
+
// Only here: a missing harness rescues via the CLI (PF-3707).
|
|
845
|
+
fallbackWhenMissing: true,
|
|
846
|
+
verbose: options.verbose ?? false,
|
|
847
|
+
};
|
|
848
|
+
if (!ALL_HARNESSES.includes(client)) {
|
|
849
|
+
throw new Error('Unsupported client: ' + client + '. Supported clients: ' + ALL_HARNESSES.join(', '));
|
|
850
|
+
}
|
|
851
|
+
const gate = await gateApiKey({
|
|
852
|
+
apiKey: options.apiKey,
|
|
853
|
+
mcpUrl,
|
|
854
|
+
retryCommand: `sp connect ${client}`,
|
|
855
|
+
mode: 'single',
|
|
856
|
+
attemptId,
|
|
857
|
+
});
|
|
858
|
+
if (gate === 'blocked') {
|
|
859
|
+
process.exitCode = 1;
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
await connectHarness(client, connectOptions);
|
|
863
|
+
}
|
|
864
|
+
export function registerConnect(program) {
|
|
865
|
+
program
|
|
866
|
+
.command('connect')
|
|
867
|
+
.description('Connect SpringBrand to an AI agent')
|
|
868
|
+
.argument('[client]', `Agent client to connect (${ALL_HARNESSES.join(', ')}); omit to pick from the agents detected on this machine`)
|
|
869
|
+
.option('--all', 'Detect and connect every supported harness found on this machine')
|
|
870
|
+
.option('--dry-run', 'Print planned writes per harness without touching anything (--all only)')
|
|
871
|
+
.option('--uninstall', 'Remove SpringBrand entries written by connect --all')
|
|
872
|
+
.option('--skip-launch', 'Skip the end-of-install agent launch (--all only)')
|
|
873
|
+
.option('--api-key <apiKey>', 'SpringBrand API key to persist for the installed skill')
|
|
874
|
+
.option('--launch', 'Launch the agent and start the SpringBrand walkthrough')
|
|
875
|
+
.option('--url <mcpUrl>', 'MCP endpoint override')
|
|
876
|
+
.option('--verbose', 'Print the full npm and skills output instead of only on failure')
|
|
877
|
+
.action(async (client, options) => {
|
|
878
|
+
if (options.apiKey && !validateKeyFormat(options.apiKey)) {
|
|
879
|
+
throw new Error('Invalid --api-key value');
|
|
880
|
+
}
|
|
881
|
+
const { mcpUrl, attemptId } = parseMcpUrlOption(options);
|
|
882
|
+
if (options.all) {
|
|
883
|
+
if (client) {
|
|
884
|
+
throw new Error('Pass either a client or --all, not both.');
|
|
885
|
+
}
|
|
886
|
+
await connectManyPath(options, mcpUrl, attemptId);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (options.dryRun || options.uninstall || options.skipLaunch) {
|
|
890
|
+
throw new Error('--dry-run, --uninstall and --skip-launch are only supported with --all.');
|
|
891
|
+
}
|
|
892
|
+
if (!client) {
|
|
893
|
+
await connectManyPath(options, mcpUrl, attemptId);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
await connectSingleClient(client, options, mcpUrl, attemptId);
|
|
897
|
+
// Deferred to here so the notice lands after install output, not interleaved with it.
|
|
898
|
+
emitNotice();
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
//# sourceMappingURL=connect.js.map
|