@vibecodr/cli 0.1.8

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/LICENSE +201 -0
  3. package/README.md +66 -0
  4. package/dist/auth/official-client.js +10 -0
  5. package/dist/auth/token-manager.js +424 -0
  6. package/dist/bin/vibecodr-mcp.js +101 -0
  7. package/dist/cli/errors.js +38 -0
  8. package/dist/cli/output.js +52 -0
  9. package/dist/cli/parse.js +84 -0
  10. package/dist/clients/base.js +30 -0
  11. package/dist/clients/codex.js +136 -0
  12. package/dist/clients/cursor.js +91 -0
  13. package/dist/clients/vscode.js +138 -0
  14. package/dist/clients/windsurf.js +81 -0
  15. package/dist/commands/call.js +123 -0
  16. package/dist/commands/config.js +124 -0
  17. package/dist/commands/context.js +5 -0
  18. package/dist/commands/doctor.js +17 -0
  19. package/dist/commands/install.js +63 -0
  20. package/dist/commands/login.js +41 -0
  21. package/dist/commands/logout.js +26 -0
  22. package/dist/commands/pulse-setup.js +82 -0
  23. package/dist/commands/status.js +64 -0
  24. package/dist/commands/tools.js +82 -0
  25. package/dist/commands/uninstall.js +55 -0
  26. package/dist/core/interactive-input.js +114 -0
  27. package/dist/core/mcp-client.js +82 -0
  28. package/dist/core/renderers.js +34 -0
  29. package/dist/doctor/run.js +132 -0
  30. package/dist/platform/browser.js +79 -0
  31. package/dist/platform/exec.js +23 -0
  32. package/dist/platform/paths.js +36 -0
  33. package/dist/platform/prompt.js +19 -0
  34. package/dist/storage/config-store.js +72 -0
  35. package/dist/storage/file-lock.js +41 -0
  36. package/dist/storage/install-manifest.js +80 -0
  37. package/dist/storage/secret-store.js +301 -0
  38. package/dist/types/auth.js +1 -0
  39. package/dist/types/config.js +21 -0
  40. package/dist/types/install.js +1 -0
  41. package/docs/architecture.md +35 -0
  42. package/docs/auth.md +66 -0
  43. package/docs/clients.md +42 -0
  44. package/docs/commands.md +134 -0
  45. package/docs/contributors.md +68 -0
  46. package/docs/install.md +90 -0
  47. package/docs/licensing.md +20 -0
  48. package/docs/troubleshooting.md +97 -0
  49. package/package.json +40 -0
@@ -0,0 +1,424 @@
1
+ import { createServer } from "node:http";
2
+ import { randomBytes } from "node:crypto";
3
+ import { writeFile } from "node:fs/promises";
4
+ import { discoverOAuthServerInfo, exchangeAuthorization, refreshAuthorization, registerClient, startAuthorization } from "@modelcontextprotocol/sdk/client/auth.js";
5
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
6
+ import { openExternalUrl } from "../platform/browser.js";
7
+ import { isInteractiveTerminal, promptText } from "../platform/prompt.js";
8
+ import { ConfigStore } from "../storage/config-store.js";
9
+ import { SecretStore } from "../storage/secret-store.js";
10
+ import { isOfficialServer, officialClientInformation } from "./official-client.js";
11
+ const CIMD_CLIENT_ID = (process.env["VIBECDR_MCP_CIMD_CLIENT_ID"] || "").trim();
12
+ const MANUAL_CLIENT_ID = (process.env["VIBECDR_MCP_MANUAL_CLIENT_ID"] || "").trim();
13
+ function randomState() {
14
+ return randomBytes(32).toString("base64url");
15
+ }
16
+ function isExpiringSoon(session) {
17
+ if (!session.expiresAt)
18
+ return false;
19
+ return Date.parse(session.expiresAt) - Date.now() < 60_000;
20
+ }
21
+ function computeExpiresAt(expiresIn) {
22
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0)
23
+ return undefined;
24
+ return new Date(Date.now() + expiresIn * 1000).toISOString();
25
+ }
26
+ async function startLoopbackListener(timeoutSec) {
27
+ const callbackPath = "/oauth/callback/vibecodr";
28
+ const server = createServer();
29
+ let callbackResolve;
30
+ let callbackReject;
31
+ const callbackPromise = new Promise((resolve, reject) => {
32
+ callbackResolve = resolve;
33
+ callbackReject = reject;
34
+ });
35
+ server.on("request", (req, res) => {
36
+ const url = new URL(req.url || "/", "http://127.0.0.1");
37
+ if (url.pathname !== callbackPath) {
38
+ res.statusCode = 404;
39
+ res.end("Not found.");
40
+ return;
41
+ }
42
+ const result = {
43
+ code: url.searchParams.get("code") || undefined,
44
+ error: url.searchParams.get("error") || undefined,
45
+ errorDescription: url.searchParams.get("error_description") || undefined,
46
+ state: url.searchParams.get("state") || undefined
47
+ };
48
+ res.statusCode = 200;
49
+ res.setHeader("content-type", "text/plain; charset=utf-8");
50
+ res.end("Vibecodr MCP CLI login complete. You can close this tab.");
51
+ callbackResolve?.(result);
52
+ });
53
+ await new Promise((resolve, reject) => {
54
+ server.once("error", reject);
55
+ server.listen(0, "127.0.0.1", () => resolve());
56
+ }).catch((error) => {
57
+ throw new CliError("auth.loopback_bind_failed", "Failed to open a local loopback callback listener.", EXIT_CODES.authFailed, {
58
+ cause: error,
59
+ nextStep: "Close any conflicting local listener and retry."
60
+ });
61
+ });
62
+ const address = server.address();
63
+ if (!address || typeof address === "string") {
64
+ throw new CliError("auth.loopback_bind_failed", "Failed to determine the loopback callback address.", EXIT_CODES.authFailed);
65
+ }
66
+ const redirectUrl = new URL(`http://127.0.0.1:${address.port}${callbackPath}`);
67
+ const timer = setTimeout(() => {
68
+ callbackReject?.(new CliError("auth.timeout", "Timed out waiting for the OAuth callback.", EXIT_CODES.canceled, {
69
+ nextStep: "Retry login and complete the browser flow before the timeout expires."
70
+ }));
71
+ }, timeoutSec * 1000);
72
+ return {
73
+ redirectUrl,
74
+ awaitCallback: async () => {
75
+ try {
76
+ return await callbackPromise;
77
+ }
78
+ finally {
79
+ clearTimeout(timer);
80
+ }
81
+ },
82
+ close: async () => {
83
+ clearTimeout(timer);
84
+ await new Promise((resolve) => server.close(() => resolve()));
85
+ }
86
+ };
87
+ }
88
+ function assertPkceSupport(metadata) {
89
+ const methods = metadata?.code_challenge_methods_supported;
90
+ if (!Array.isArray(methods) || !methods.includes("S256")) {
91
+ throw new CliError("auth.pkce_missing", "Authorization server metadata does not advertise PKCE S256 support.", EXIT_CODES.protocol, {
92
+ nextStep: "Verify the MCP authorization server metadata includes code_challenge_methods_supported with S256."
93
+ });
94
+ }
95
+ }
96
+ function resolveBrowserMode(profile, override) {
97
+ return override || profile.browserMode;
98
+ }
99
+ export class TokenManager {
100
+ configStore;
101
+ secretStore;
102
+ constructor(configStore, secretStore) {
103
+ this.configStore = configStore;
104
+ this.secretStore = secretStore;
105
+ }
106
+ async resolveProfile(globalOptions) {
107
+ const { name, profile } = await this.configStore.getProfile(globalOptions.profile);
108
+ return {
109
+ profileName: name,
110
+ profile,
111
+ serverUrl: globalOptions.serverUrl || profile.serverUrl
112
+ };
113
+ }
114
+ async getSession(profileName) {
115
+ return await this.secretStore.get(profileName);
116
+ }
117
+ sessionState(session) {
118
+ if (!session)
119
+ return "none";
120
+ if (!session.expiresAt)
121
+ return session.refreshToken ? "refreshable" : "valid";
122
+ const expiresAt = Date.parse(session.expiresAt);
123
+ if (Number.isNaN(expiresAt))
124
+ return session.refreshToken ? "refreshable" : "valid";
125
+ if (expiresAt > Date.now())
126
+ return "valid";
127
+ return session.refreshToken ? "refreshable" : "expired";
128
+ }
129
+ async discover(serverUrl) {
130
+ try {
131
+ const result = await discoverOAuthServerInfo(serverUrl);
132
+ return {
133
+ authorizationServerUrl: result.authorizationServerUrl,
134
+ ...(result.authorizationServerMetadata ? { authorizationServerMetadata: result.authorizationServerMetadata } : {}),
135
+ ...(result.resourceMetadata ? { resourceMetadata: result.resourceMetadata } : {})
136
+ };
137
+ }
138
+ catch (error) {
139
+ throw new CliError("network.discovery_failed", "Failed to discover MCP OAuth metadata.", EXIT_CODES.network, {
140
+ cause: error,
141
+ nextStep: `Verify ${serverUrl} is reachable and its auth metadata endpoints are healthy.`
142
+ });
143
+ }
144
+ }
145
+ async login(globalOptions, options) {
146
+ const { profileName, profile, serverUrl } = await this.resolveProfile(globalOptions);
147
+ const timeoutSec = options?.timeoutSec || 900;
148
+ const loopback = await startLoopbackListener(timeoutSec);
149
+ options?.onLoopbackReady?.(loopback.redirectUrl.toString());
150
+ try {
151
+ const prepared = await this.prepareAuthorization({
152
+ serverUrl,
153
+ profile,
154
+ requestedMode: options?.registrationMode || profile.registrationMode,
155
+ globalOptions,
156
+ redirectUrl: loopback.redirectUrl,
157
+ ...(options?.scope ? { scope: options.scope } : {})
158
+ });
159
+ const browserMode = resolveBrowserMode(profile, options?.browserMode);
160
+ if (browserMode === "print" || globalOptions.nonInteractive) {
161
+ options?.onAuthorizationUrl?.(prepared.authorizationUrl.toString());
162
+ if (process.env["VIBECDR_MCP_TEST_AUTH_URL_FILE"]) {
163
+ await writeFile(process.env["VIBECDR_MCP_TEST_AUTH_URL_FILE"], prepared.authorizationUrl.toString(), "utf8");
164
+ }
165
+ }
166
+ else {
167
+ await openExternalUrl(prepared.authorizationUrl.toString());
168
+ }
169
+ const callback = await loopback.awaitCallback();
170
+ if (callback.error) {
171
+ throw new CliError("auth.authorization_failed", callback.errorDescription || callback.error, EXIT_CODES.authFailed);
172
+ }
173
+ if (!callback.code || callback.state !== prepared.state) {
174
+ throw new CliError("auth.state_mismatch", "OAuth callback state did not match the pending login session.", EXIT_CODES.authFailed, {
175
+ nextStep: "Retry login and complete only the most recent browser flow."
176
+ });
177
+ }
178
+ const resource = new URL(prepared.discovery.resourceMetadata?.resource || serverUrl);
179
+ const tokens = await exchangeAuthorization(prepared.discovery.authorizationServerUrl, {
180
+ clientInformation: prepared.clientInformation,
181
+ authorizationCode: callback.code,
182
+ codeVerifier: prepared.codeVerifier,
183
+ redirectUri: prepared.redirectUrl,
184
+ resource,
185
+ ...(prepared.discovery.authorizationServerMetadata ? { metadata: prepared.discovery.authorizationServerMetadata } : {})
186
+ }).catch((error) => {
187
+ throw new CliError("auth.exchange_failed", "Failed to exchange the authorization code for tokens.", EXIT_CODES.authFailed, {
188
+ cause: error
189
+ });
190
+ });
191
+ const session = {
192
+ schemaVersion: 1,
193
+ serverUrl,
194
+ accessToken: tokens.access_token,
195
+ refreshToken: tokens.refresh_token,
196
+ expiresAt: computeExpiresAt(tokens.expires_in),
197
+ scope: tokens.scope || options?.scope,
198
+ tokenType: tokens.token_type,
199
+ registrationMode: prepared.registrationMode,
200
+ authorizationServerUrl: prepared.discovery.authorizationServerUrl,
201
+ resourceUrl: resource.toString(),
202
+ clientInformation: prepared.clientInformation,
203
+ updatedAt: new Date().toISOString()
204
+ };
205
+ await this.secretStore.set(profileName, session);
206
+ return {
207
+ profile: profileName,
208
+ serverUrl,
209
+ registrationMode: prepared.registrationMode,
210
+ authenticated: true,
211
+ expiresAt: session.expiresAt,
212
+ hasRefreshToken: Boolean(session.refreshToken),
213
+ authorizationServerIssuer: prepared.discovery.authorizationServerMetadata?.issuer
214
+ };
215
+ }
216
+ finally {
217
+ await loopback.close();
218
+ }
219
+ }
220
+ async ensureSession(globalOptions, options) {
221
+ const { profileName } = await this.resolveProfile(globalOptions);
222
+ const current = await this.secretStore.get(profileName);
223
+ if (current && !isExpiringSoon(current))
224
+ return current;
225
+ if (current?.refreshToken) {
226
+ try {
227
+ const refresh = await this.refresh(profileName, current);
228
+ return refresh.session;
229
+ }
230
+ catch (error) {
231
+ if (!(error instanceof CliError) || (error.exitCode !== EXIT_CODES.authFailed && error.exitCode !== EXIT_CODES.authRequired)) {
232
+ throw error;
233
+ }
234
+ }
235
+ }
236
+ if (options?.allowInteractiveLogin && !globalOptions.nonInteractive && isInteractiveTerminal()) {
237
+ await this.login(globalOptions);
238
+ const next = await this.secretStore.get(profileName);
239
+ if (!next) {
240
+ throw new CliError("auth.missing_session", "Login completed but no local session was stored.", EXIT_CODES.authFailed);
241
+ }
242
+ return next;
243
+ }
244
+ throw new CliError("auth.required", "Authentication is required for this command.", EXIT_CODES.authRequired, {
245
+ nextStep: "Run vibecodr login, then retry. CLI auth is separate from Codex, editor, ChatGPT, and other MCP client auth."
246
+ });
247
+ }
248
+ async refresh(profileName, session) {
249
+ if (!session.refreshToken) {
250
+ throw new CliError("auth.refresh_unavailable", "No refresh token is available for this profile.", EXIT_CODES.authRequired);
251
+ }
252
+ const discovery = await this.discover(session.serverUrl);
253
+ const resource = session.resourceUrl ? new URL(session.resourceUrl) : new URL(session.serverUrl);
254
+ const tokens = await refreshAuthorization(discovery.authorizationServerUrl, {
255
+ clientInformation: session.clientInformation,
256
+ refreshToken: session.refreshToken,
257
+ resource,
258
+ ...(discovery.authorizationServerMetadata ? { metadata: discovery.authorizationServerMetadata } : {})
259
+ }).catch(async (error) => {
260
+ await this.secretStore.delete(profileName).catch(() => undefined);
261
+ throw new CliError("auth.refresh_failed", "Failed to refresh the stored session.", EXIT_CODES.authFailed, {
262
+ cause: error,
263
+ nextStep: "Run vibecodr login to re-authenticate."
264
+ });
265
+ });
266
+ const updated = {
267
+ ...session,
268
+ accessToken: tokens.access_token,
269
+ refreshToken: tokens.refresh_token || session.refreshToken,
270
+ expiresAt: computeExpiresAt(tokens.expires_in),
271
+ scope: tokens.scope || session.scope,
272
+ tokenType: tokens.token_type || session.tokenType,
273
+ authorizationServerUrl: discovery.authorizationServerUrl,
274
+ updatedAt: new Date().toISOString()
275
+ };
276
+ await this.secretStore.set(profileName, updated);
277
+ return { session: updated, previousSession: session };
278
+ }
279
+ async logout(profileName, options) {
280
+ const session = await this.secretStore.get(profileName);
281
+ if (!session) {
282
+ return {
283
+ localTokensDeleted: false,
284
+ revocationAttempted: false,
285
+ revocationConfirmed: false
286
+ };
287
+ }
288
+ let revocationAttempted = false;
289
+ let revocationConfirmed = false;
290
+ if (!options?.noRevoke && session.refreshToken) {
291
+ revocationAttempted = true;
292
+ try {
293
+ const discovery = await this.discover(session.serverUrl);
294
+ const endpoint = discovery.authorizationServerMetadata && "revocation_endpoint" in discovery.authorizationServerMetadata
295
+ ? discovery.authorizationServerMetadata.revocation_endpoint
296
+ : undefined;
297
+ if (endpoint) {
298
+ const form = new URLSearchParams({
299
+ token: session.refreshToken,
300
+ client_id: session.clientInformation.client_id
301
+ });
302
+ const clientSecret = "client_secret" in session.clientInformation ? session.clientInformation.client_secret : undefined;
303
+ if (typeof clientSecret === "string" && clientSecret)
304
+ form.set("client_secret", clientSecret);
305
+ const res = await fetch(endpoint, {
306
+ method: "POST",
307
+ headers: {
308
+ "content-type": "application/x-www-form-urlencoded"
309
+ },
310
+ body: form.toString()
311
+ });
312
+ revocationConfirmed = res.ok;
313
+ }
314
+ }
315
+ catch {
316
+ revocationConfirmed = false;
317
+ }
318
+ }
319
+ const localTokensDeleted = await this.secretStore.delete(profileName);
320
+ return {
321
+ localTokensDeleted,
322
+ revocationAttempted,
323
+ revocationConfirmed
324
+ };
325
+ }
326
+ async prepareAuthorization(args) {
327
+ const discovery = await this.discover(args.serverUrl);
328
+ assertPkceSupport(discovery.authorizationServerMetadata);
329
+ const registrationMode = await this.resolveRegistrationMode(args.requestedMode, args.serverUrl, discovery.authorizationServerMetadata, args.globalOptions);
330
+ const clientInformation = await this.resolveClientInformation({
331
+ serverUrl: args.serverUrl,
332
+ registrationMode,
333
+ authorizationServerUrl: discovery.authorizationServerUrl,
334
+ redirectUrl: args.redirectUrl,
335
+ ...(args.scope ? { scope: args.scope } : {}),
336
+ metadata: discovery.authorizationServerMetadata
337
+ });
338
+ const resource = new URL(discovery.resourceMetadata?.resource || args.serverUrl);
339
+ const state = randomState();
340
+ const { authorizationUrl, codeVerifier } = await startAuthorization(discovery.authorizationServerUrl, {
341
+ clientInformation,
342
+ redirectUrl: args.redirectUrl,
343
+ state,
344
+ resource,
345
+ ...(args.scope ? { scope: args.scope } : {}),
346
+ ...(discovery.authorizationServerMetadata ? { metadata: discovery.authorizationServerMetadata } : {})
347
+ });
348
+ return {
349
+ authorizationUrl,
350
+ codeVerifier,
351
+ state,
352
+ redirectUrl: args.redirectUrl,
353
+ clientInformation,
354
+ registrationMode,
355
+ discovery
356
+ };
357
+ }
358
+ async resolveRegistrationMode(requestedMode, serverUrl, metadata, globalOptions) {
359
+ if (requestedMode !== "auto")
360
+ return requestedMode;
361
+ if (isOfficialServer(serverUrl))
362
+ return "cimd";
363
+ if (metadata?.client_id_metadata_document_supported && CIMD_CLIENT_ID)
364
+ return "cimd";
365
+ if (metadata?.registration_endpoint)
366
+ return "dcr";
367
+ if (MANUAL_CLIENT_ID)
368
+ return "manual";
369
+ if (!globalOptions.nonInteractive && isInteractiveTerminal())
370
+ return "manual";
371
+ throw new CliError("auth.registration_unavailable", "No supported OAuth client registration mode is available for this server.", EXIT_CODES.protocol, {
372
+ nextStep: "Configure a preregistered client, CIMD client ID, or dynamic registration endpoint."
373
+ });
374
+ }
375
+ async resolveClientInformation(args) {
376
+ switch (args.registrationMode) {
377
+ case "preregistered":
378
+ if (isOfficialServer(args.serverUrl)) {
379
+ return officialClientInformation();
380
+ }
381
+ throw new CliError("auth.preregistered_missing", "No preregistered client is configured for this server.", EXIT_CODES.protocol);
382
+ case "cimd":
383
+ if (isOfficialServer(args.serverUrl)) {
384
+ return officialClientInformation();
385
+ }
386
+ if (!CIMD_CLIENT_ID) {
387
+ throw new CliError("auth.cimd_missing", "No Client ID Metadata Document URL is configured.", EXIT_CODES.protocol);
388
+ }
389
+ return {
390
+ client_id: CIMD_CLIENT_ID
391
+ };
392
+ case "dcr":
393
+ if (!args.metadata?.registration_endpoint) {
394
+ throw new CliError("auth.dcr_missing", "Authorization server metadata does not advertise a registration endpoint.", EXIT_CODES.protocol);
395
+ }
396
+ return await registerClient(args.authorizationServerUrl, {
397
+ metadata: args.metadata,
398
+ clientMetadata: {
399
+ redirect_uris: [args.redirectUrl.toString()],
400
+ token_endpoint_auth_method: "none",
401
+ grant_types: ["authorization_code", "refresh_token"],
402
+ response_types: ["code"],
403
+ client_name: "Vibecodr MCP CLI",
404
+ scope: args.scope
405
+ }
406
+ }).catch((error) => {
407
+ throw new CliError("auth.dcr_failed", "Dynamic client registration failed.", EXIT_CODES.protocol, {
408
+ cause: error
409
+ });
410
+ });
411
+ case "manual": {
412
+ const clientId = MANUAL_CLIENT_ID || (isInteractiveTerminal() ? await promptText("Public client_id: ") : "");
413
+ if (!clientId) {
414
+ throw new CliError("auth.manual_client_missing", "A public client_id is required for manual registration mode.", EXIT_CODES.authRequired, {
415
+ nextStep: "Provide a manual client_id or choose a different registration mode."
416
+ });
417
+ }
418
+ return { client_id: clientId };
419
+ }
420
+ default:
421
+ throw new CliError("auth.unsupported_mode", `Unsupported registration mode: ${args.registrationMode}`, EXIT_CODES.usage);
422
+ }
423
+ }
424
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ import { ConfigStore } from "../storage/config-store.js";
3
+ import { SecretStore } from "../storage/secret-store.js";
4
+ import { TokenManager } from "../auth/token-manager.js";
5
+ import { McpRuntimeClient } from "../core/mcp-client.js";
6
+ import { Output } from "../cli/output.js";
7
+ import { parseGlobalOptions } from "../cli/parse.js";
8
+ import { CliError, EXIT_CODES } from "../cli/errors.js";
9
+ import { runLoginCommand } from "../commands/login.js";
10
+ import { runLogoutCommand } from "../commands/logout.js";
11
+ import { runStatusCommand } from "../commands/status.js";
12
+ import { runToolsCommand } from "../commands/tools.js";
13
+ import { runCallCommand } from "../commands/call.js";
14
+ import { runDoctorCommand } from "../commands/doctor.js";
15
+ import { runConfigCommand } from "../commands/config.js";
16
+ import { runInstallCommand } from "../commands/install.js";
17
+ import { runUninstallCommand } from "../commands/uninstall.js";
18
+ import { runPulseSetupCommand } from "../commands/pulse-setup.js";
19
+ function helpText() {
20
+ return [
21
+ "vibecodr <command> [options]",
22
+ "Compatibility alias: vibecodr-mcp <command> [options]",
23
+ "",
24
+ "Commands:",
25
+ " login",
26
+ " logout",
27
+ " status",
28
+ " tools [tool-name]",
29
+ " call <tool-name>",
30
+ " doctor",
31
+ " install <client>",
32
+ " uninstall <client>",
33
+ " config",
34
+ " pulse-setup [--descriptor-setup-json <json> | --descriptor-setup-file <path>]",
35
+ "",
36
+ "Global flags:",
37
+ " --profile <name>",
38
+ " --server-url <url>",
39
+ " --json",
40
+ " --verbose",
41
+ " --non-interactive"
42
+ ].join("\n");
43
+ }
44
+ async function main() {
45
+ const { command, commandArgs, globalOptions } = parseGlobalOptions(process.argv.slice(2));
46
+ if (!command || command === "--help" || command === "help") {
47
+ process.stdout.write(helpText() + "\n");
48
+ return;
49
+ }
50
+ const configStore = new ConfigStore();
51
+ const secretStore = new SecretStore();
52
+ const tokenManager = new TokenManager(configStore, secretStore);
53
+ const runtimeClient = new McpRuntimeClient();
54
+ const output = new Output(globalOptions);
55
+ const context = {
56
+ globalOptions,
57
+ output,
58
+ configStore,
59
+ secretStore,
60
+ tokenManager,
61
+ runtimeClient
62
+ };
63
+ switch (command) {
64
+ case "login":
65
+ await runLoginCommand(commandArgs, context);
66
+ return;
67
+ case "logout":
68
+ await runLogoutCommand(commandArgs, context);
69
+ return;
70
+ case "status":
71
+ await runStatusCommand(commandArgs, context);
72
+ return;
73
+ case "tools":
74
+ await runToolsCommand(commandArgs, context);
75
+ return;
76
+ case "call":
77
+ await runCallCommand(commandArgs, context);
78
+ return;
79
+ case "doctor":
80
+ await runDoctorCommand(commandArgs, context);
81
+ return;
82
+ case "install":
83
+ await runInstallCommand(commandArgs, context);
84
+ return;
85
+ case "uninstall":
86
+ await runUninstallCommand(commandArgs, context);
87
+ return;
88
+ case "config":
89
+ await runConfigCommand(commandArgs, context);
90
+ return;
91
+ case "pulse-setup":
92
+ await runPulseSetupCommand(commandArgs, context);
93
+ return;
94
+ default:
95
+ throw new CliError("usage.command", `Unknown command: ${command}`, EXIT_CODES.usage);
96
+ }
97
+ }
98
+ main().catch((error) => {
99
+ const { globalOptions } = parseGlobalOptions(process.argv.slice(2));
100
+ new Output(globalOptions).failure(error);
101
+ });
@@ -0,0 +1,38 @@
1
+ export const EXIT_CODES = {
2
+ runtime: 1,
3
+ usage: 2,
4
+ config: 3,
5
+ authRequired: 4,
6
+ authFailed: 5,
7
+ network: 6,
8
+ protocol: 7,
9
+ toolFailed: 8,
10
+ unsupportedClient: 9,
11
+ installConflict: 10,
12
+ secretStoreUnavailable: 11,
13
+ canceled: 12
14
+ };
15
+ export class CliError extends Error {
16
+ exitCode;
17
+ machineCode;
18
+ nextStep;
19
+ debugDetails;
20
+ constructor(machineCode, message, exitCode, options) {
21
+ super(message, options?.cause ? { cause: options.cause } : undefined);
22
+ this.name = "CliError";
23
+ this.machineCode = machineCode;
24
+ this.exitCode = exitCode;
25
+ this.nextStep = options?.nextStep;
26
+ this.debugDetails = options?.debugDetails;
27
+ }
28
+ }
29
+ export function asCliError(error) {
30
+ if (error instanceof CliError)
31
+ return error;
32
+ if (error instanceof Error) {
33
+ return new CliError("runtime.unexpected", error.message, EXIT_CODES.runtime, {
34
+ cause: error
35
+ });
36
+ }
37
+ return new CliError("runtime.unexpected", String(error), EXIT_CODES.runtime);
38
+ }
@@ -0,0 +1,52 @@
1
+ import { asCliError } from "./errors.js";
2
+ export class Output {
3
+ options;
4
+ constructor(options) {
5
+ this.options = options;
6
+ }
7
+ write(value) {
8
+ process.stdout.write(value + "\n");
9
+ }
10
+ info(message) {
11
+ if (!this.options.json)
12
+ this.write(message);
13
+ }
14
+ warn(message) {
15
+ if (!this.options.json)
16
+ process.stderr.write(message + "\n");
17
+ }
18
+ json(value) {
19
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
20
+ }
21
+ success(value, humanLines) {
22
+ if (this.options.json) {
23
+ this.json(value);
24
+ return;
25
+ }
26
+ for (const line of humanLines)
27
+ this.write(line);
28
+ }
29
+ failure(error) {
30
+ const cliError = asCliError(error);
31
+ if (this.options.json) {
32
+ this.json({
33
+ schemaVersion: 1,
34
+ ok: false,
35
+ error: {
36
+ code: cliError.machineCode,
37
+ message: cliError.message,
38
+ nextStep: cliError.nextStep
39
+ }
40
+ });
41
+ }
42
+ else {
43
+ process.stderr.write(`${cliError.message}\n`);
44
+ if (cliError.nextStep)
45
+ process.stderr.write(`Next step: ${cliError.nextStep}\n`);
46
+ if (this.options.verbose && cliError.debugDetails != null) {
47
+ process.stderr.write(`${JSON.stringify(cliError.debugDetails, null, 2)}\n`);
48
+ }
49
+ }
50
+ process.exit(cliError.exitCode);
51
+ }
52
+ }