@switchboard.spot/cli 0.2.1 → 0.2.3
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 +18 -7
- package/bin/switchboard.js +5 -1
- package/lib/commands/auth.js +147 -38
- package/lib/commands/billing.js +43 -0
- package/lib/commands/docs.js +176 -0
- package/lib/commands/doctor.js +108 -42
- package/lib/commands/endUsers.js +1 -1
- package/lib/commands/init.js +8 -6
- package/lib/commands/launch.js +213 -0
- package/lib/commands/projects.js +93 -5
- package/lib/commands/setup.js +282 -7
- package/lib/commands/verify.js +3 -3
- package/lib/config.js +12 -9
- package/lib/docsClient.js +157 -0
- package/lib/mcpServer.js +193 -0
- package/lib/output.js +65 -9
- package/lib/verify/index.js +32 -4
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -7,13 +7,13 @@ tests.
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
npm install -g @switchboard.spot/cli
|
|
10
|
+
npm install -g @switchboard.spot/cli@latest
|
|
11
11
|
```
|
|
12
12
|
|
|
13
13
|
You can also run commands without a global install:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx @switchboard.spot/cli auth login
|
|
16
|
+
npx @switchboard.spot/cli@latest auth login
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Build for local testing
|
|
@@ -54,21 +54,32 @@ If a first publish fails, `npm view @switchboard.spot/cli` will continue to retu
|
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
56
|
switchboard auth login
|
|
57
|
-
switchboard
|
|
58
|
-
switchboard
|
|
57
|
+
switchboard projects create --name "My App" --slug my-app
|
|
58
|
+
switchboard setup project --origin http://localhost:5173 --json
|
|
59
|
+
switchboard verify setup
|
|
60
|
+
switchboard launch prepare --production-origin https://app.example.com --end-user-terms-url https://app.example.com/terms --end-user-privacy-url https://app.example.com/privacy --support-email support@example.com --contact-email owner@example.com --use-case "Browser chat for signed-in customers"
|
|
61
|
+
switchboard verify publish
|
|
59
62
|
```
|
|
60
63
|
|
|
61
64
|
Use `--json` for automation, CI, and coding agents.
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
`setup project --origin <origin>` writes public Client Gateway environment values such as `VITE_SWITCHBOARD_CLIENT_URL`, configures the exact local or preview origin, enables Client Gateway, and provisions Switchboard-managed Turnstile. It reports browser chat verification as a manual SDK check.
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
CLI account login does not create Client Gateway end-user sessions. End-user sign-up, sign-in, and refresh require the SDK-managed browser challenge; curl, Node scripts, CI, and CLI account login cannot mint browser sessions without running that real browser/mobile flow.
|
|
69
|
+
|
|
70
|
+
For local Switchboard development, `switchboard verify setup --client-url http://localhost:4000/m/<slug>/v1` uses the built-in `dev_browser_challenge` token unless a scenario supplies an explicit `browserChallengeToken`. Hosted Switchboard should use managed Turnstile and the SDK-managed real browser challenge. A local sandbox smoke path is: configure allowed origin plus legal/support fields, create an anonymous session through the SDK or browser verification flow, then call Client Gateway chat.
|
|
71
|
+
|
|
72
|
+
Model discovery is global. Use `GET /v1/models` for OpenAI-compatible discovery or `GET /v1/catalog/models` for catalog/pricing metadata; Client Gateway chat is project-scoped at `/m/<slug>/v1/chat/completions`.
|
|
73
|
+
|
|
74
|
+
Switchboard-managed Turnstile is the default production path. Developers do not paste Cloudflare secrets into the CLI or repo files:
|
|
66
75
|
|
|
67
76
|
```bash
|
|
68
|
-
switchboard
|
|
77
|
+
switchboard setup project --origin <origin> --json
|
|
69
78
|
switchboard projects turnstile <project-id> --clear
|
|
70
79
|
```
|
|
71
80
|
|
|
81
|
+
`switchboard projects provision-turnstile` remains available as a low-level admin/debug command. If its help is missing, upgrade with `npm install -g @switchboard.spot/cli@latest` before trying dashboard automation or manual Cloudflare keys.
|
|
82
|
+
|
|
72
83
|
## Configuration
|
|
73
84
|
|
|
74
85
|
The CLI stores non-secret settings in `~/.switchboard/config.json` by default.
|
package/bin/switchboard.js
CHANGED
|
@@ -18,11 +18,13 @@ import { registerBillingCommands } from "../lib/commands/billing.js";
|
|
|
18
18
|
import { registerEnvCommands } from "../lib/commands/env.js";
|
|
19
19
|
import { registerUsageCommands } from "../lib/commands/usage.js";
|
|
20
20
|
import { registerIntegrationCommands } from "../lib/commands/integration.js";
|
|
21
|
+
import { registerDocsCommands } from "../lib/commands/docs.js";
|
|
21
22
|
import { registerInitCommand } from "../lib/commands/init.js";
|
|
22
23
|
import { registerSetupCommand } from "../lib/commands/setup.js";
|
|
23
24
|
import { registerHealthCommand } from "../lib/commands/health.js";
|
|
24
25
|
import { registerDoctorCommand } from "../lib/commands/doctor.js";
|
|
25
26
|
import { registerVerifyCommands } from "../lib/commands/verify.js";
|
|
27
|
+
import { registerLaunchCommands } from "../lib/commands/launch.js";
|
|
26
28
|
|
|
27
29
|
const program = new Command();
|
|
28
30
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -42,6 +44,7 @@ registerSetupCommand(program);
|
|
|
42
44
|
registerHealthCommand(program);
|
|
43
45
|
registerDoctorCommand(program);
|
|
44
46
|
registerVerifyCommands(program);
|
|
47
|
+
registerLaunchCommands(program);
|
|
45
48
|
registerAuth(program);
|
|
46
49
|
registerAccountCommands(program);
|
|
47
50
|
registerWorkspacesCommands(program);
|
|
@@ -53,8 +56,9 @@ registerBillingCommands(program);
|
|
|
53
56
|
registerEnvCommands(program);
|
|
54
57
|
registerUsageCommands(program);
|
|
55
58
|
registerIntegrationCommands(program);
|
|
59
|
+
registerDocsCommands(program);
|
|
56
60
|
|
|
57
|
-
program.
|
|
61
|
+
await program.parseAsync();
|
|
58
62
|
|
|
59
63
|
if (!process.argv.slice(2).length) {
|
|
60
64
|
program.outputHelp();
|
package/lib/commands/auth.js
CHANGED
|
@@ -317,9 +317,11 @@ function waitForCallback(callbackServer, timeoutSeconds, json) {
|
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
function callbackPage(title, message, tone) {
|
|
321
|
-
const
|
|
322
|
-
const
|
|
320
|
+
export function callbackPage(title, message, tone) {
|
|
321
|
+
const success = tone === "success";
|
|
322
|
+
const accent = success ? "#14b8a6" : "#ef4444";
|
|
323
|
+
const accentSoft = success ? "#ccfbf1" : "#fee2e2";
|
|
324
|
+
const mark = success ? "✓" : "!";
|
|
323
325
|
|
|
324
326
|
return `<!doctype html>
|
|
325
327
|
<html lang="en">
|
|
@@ -329,74 +331,181 @@ function callbackPage(title, message, tone) {
|
|
|
329
331
|
<title>${escapeHtml(title)}</title>
|
|
330
332
|
<style>
|
|
331
333
|
:root {
|
|
332
|
-
color-scheme: dark;
|
|
334
|
+
color-scheme: light dark;
|
|
333
335
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
334
|
-
background: #
|
|
335
|
-
color: #
|
|
336
|
+
background: #ffffff;
|
|
337
|
+
color: #1f2937;
|
|
336
338
|
}
|
|
339
|
+
* { box-sizing: border-box; }
|
|
337
340
|
body {
|
|
338
341
|
min-height: 100vh;
|
|
339
342
|
margin: 0;
|
|
340
|
-
|
|
341
|
-
place-items: center;
|
|
343
|
+
color: #1f2937;
|
|
342
344
|
background:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
345
|
+
repeating-linear-gradient(125deg, transparent, transparent 6px, #e8e8e8 6px, #e8e8e8 7px),
|
|
346
|
+
#ffffff;
|
|
347
|
+
}
|
|
348
|
+
.page {
|
|
349
|
+
min-height: 100vh;
|
|
350
|
+
width: min(100%, 80rem);
|
|
351
|
+
margin: 0 auto;
|
|
352
|
+
display: flex;
|
|
353
|
+
flex-direction: column;
|
|
354
|
+
border-inline: 1px solid #e5e7eb;
|
|
355
|
+
background:
|
|
356
|
+
repeating-linear-gradient(125deg, transparent, transparent 6px, #e8e8e8 6px, #e8e8e8 7px),
|
|
357
|
+
#ffffff;
|
|
358
|
+
}
|
|
359
|
+
header {
|
|
360
|
+
height: 4.5rem;
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: center;
|
|
363
|
+
justify-content: space-between;
|
|
364
|
+
padding: 0 1.5rem;
|
|
365
|
+
border-bottom: 1px solid #e5e7eb;
|
|
366
|
+
background: rgba(255, 255, 255, 0.88);
|
|
367
|
+
backdrop-filter: blur(12px);
|
|
368
|
+
}
|
|
369
|
+
.logo {
|
|
370
|
+
color: #1f2937;
|
|
371
|
+
font-size: 0.95rem;
|
|
372
|
+
font-weight: 800;
|
|
373
|
+
letter-spacing: 0;
|
|
374
|
+
}
|
|
375
|
+
.pill {
|
|
376
|
+
border: 1px solid #e5e7eb;
|
|
377
|
+
border-radius: 999px;
|
|
378
|
+
padding: 0.45rem 0.8rem;
|
|
379
|
+
background: #ffffff;
|
|
380
|
+
color: #4b5563;
|
|
381
|
+
font-size: 0.82rem;
|
|
382
|
+
font-weight: 600;
|
|
346
383
|
}
|
|
347
384
|
main {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
padding:
|
|
352
|
-
|
|
353
|
-
|
|
385
|
+
flex: 1;
|
|
386
|
+
display: grid;
|
|
387
|
+
place-items: center;
|
|
388
|
+
padding: 5rem 1.5rem;
|
|
389
|
+
}
|
|
390
|
+
.panel {
|
|
391
|
+
width: min(100%, 34rem);
|
|
392
|
+
border: 1px solid #e5e7eb;
|
|
393
|
+
border-radius: 0.5rem;
|
|
394
|
+
padding: clamp(1.5rem, 5vw, 2.25rem);
|
|
395
|
+
background: rgba(255, 255, 255, 0.94);
|
|
396
|
+
box-shadow:
|
|
397
|
+
0 1.34368px 0.537473px -0.625px rgba(0, 0, 0, 0.09),
|
|
398
|
+
0 15.5969px 6.23877px -3.125px rgba(0, 0, 0, 0.07),
|
|
399
|
+
0 43.962px 17.5848px -4.375px rgba(0, 0, 0, 0.04);
|
|
354
400
|
text-align: center;
|
|
355
|
-
backdrop-filter: blur(18px);
|
|
356
401
|
}
|
|
357
|
-
.
|
|
358
|
-
margin: 0 0 1.
|
|
359
|
-
color:
|
|
402
|
+
.eyebrow {
|
|
403
|
+
margin: 0 0 1.25rem;
|
|
404
|
+
color: #4b5563;
|
|
360
405
|
font-size: 0.78rem;
|
|
361
406
|
font-weight: 700;
|
|
362
|
-
letter-spacing: 0.
|
|
407
|
+
letter-spacing: 0.12em;
|
|
363
408
|
text-transform: uppercase;
|
|
364
409
|
}
|
|
365
410
|
.mark {
|
|
366
|
-
width:
|
|
367
|
-
height:
|
|
411
|
+
width: 4rem;
|
|
412
|
+
height: 4rem;
|
|
368
413
|
margin: 0 auto 1.25rem;
|
|
369
414
|
display: grid;
|
|
370
415
|
place-items: center;
|
|
371
|
-
border
|
|
416
|
+
border: 1px solid ${accent};
|
|
417
|
+
border-radius: 0.5rem;
|
|
372
418
|
background: ${accent};
|
|
373
|
-
color: #
|
|
374
|
-
font-size: 1.
|
|
419
|
+
color: #111827;
|
|
420
|
+
font-size: 1.9rem;
|
|
375
421
|
font-weight: 900;
|
|
376
|
-
box-shadow: 0
|
|
422
|
+
box-shadow: 0 18px 40px -20px ${accent};
|
|
377
423
|
}
|
|
378
424
|
h1 {
|
|
379
425
|
margin: 0;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
426
|
+
color: #1f2937;
|
|
427
|
+
font-size: clamp(2rem, 7vw, 3.5rem);
|
|
428
|
+
line-height: 0.98;
|
|
429
|
+
letter-spacing: 0;
|
|
383
430
|
}
|
|
384
431
|
p {
|
|
385
432
|
margin: 1rem auto 0;
|
|
386
433
|
max-width: 26rem;
|
|
387
|
-
color:
|
|
434
|
+
color: #4b5563;
|
|
388
435
|
font-size: 1rem;
|
|
389
436
|
line-height: 1.65;
|
|
390
437
|
}
|
|
438
|
+
.status {
|
|
439
|
+
display: inline-flex;
|
|
440
|
+
align-items: center;
|
|
441
|
+
gap: 0.5rem;
|
|
442
|
+
margin-top: 1.5rem;
|
|
443
|
+
border: 1px solid ${accent};
|
|
444
|
+
border-radius: 999px;
|
|
445
|
+
padding: 0.5rem 0.8rem;
|
|
446
|
+
background: ${accentSoft};
|
|
447
|
+
color: #1f2937;
|
|
448
|
+
font-size: 0.86rem;
|
|
449
|
+
font-weight: 700;
|
|
450
|
+
}
|
|
451
|
+
@media (prefers-color-scheme: dark) {
|
|
452
|
+
:root {
|
|
453
|
+
background: #020617;
|
|
454
|
+
color: #f3f4f6;
|
|
455
|
+
}
|
|
456
|
+
body {
|
|
457
|
+
color: #f3f4f6;
|
|
458
|
+
background:
|
|
459
|
+
repeating-linear-gradient(125deg, transparent, transparent 6px, rgba(55, 65, 81, 0.6) 6px, rgba(55, 65, 81, 0.6) 7px),
|
|
460
|
+
#020617;
|
|
461
|
+
}
|
|
462
|
+
.page {
|
|
463
|
+
border-color: #1f2937;
|
|
464
|
+
background:
|
|
465
|
+
repeating-linear-gradient(125deg, transparent, transparent 6px, rgba(55, 65, 81, 0.6) 6px, rgba(55, 65, 81, 0.6) 7px),
|
|
466
|
+
#020617;
|
|
467
|
+
}
|
|
468
|
+
header,
|
|
469
|
+
.panel {
|
|
470
|
+
border-color: #1f2937;
|
|
471
|
+
background: rgba(2, 6, 23, 0.94);
|
|
472
|
+
}
|
|
473
|
+
.logo,
|
|
474
|
+
h1 {
|
|
475
|
+
color: #f3f4f6;
|
|
476
|
+
}
|
|
477
|
+
.pill,
|
|
478
|
+
.eyebrow,
|
|
479
|
+
p {
|
|
480
|
+
color: #d1d5db;
|
|
481
|
+
}
|
|
482
|
+
.pill {
|
|
483
|
+
border-color: #1f2937;
|
|
484
|
+
background: #111827;
|
|
485
|
+
}
|
|
486
|
+
.mark,
|
|
487
|
+
.status {
|
|
488
|
+
color: #020617;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
391
491
|
</style>
|
|
392
492
|
</head>
|
|
393
493
|
<body>
|
|
394
|
-
<
|
|
395
|
-
<
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
494
|
+
<div class="page">
|
|
495
|
+
<header>
|
|
496
|
+
<div class="logo">Switchboard</div>
|
|
497
|
+
<div class="pill">CLI session</div>
|
|
498
|
+
</header>
|
|
499
|
+
<main>
|
|
500
|
+
<section class="panel" aria-labelledby="callback-title">
|
|
501
|
+
<p class="eyebrow">Switchboard CLI</p>
|
|
502
|
+
<div class="mark" aria-hidden="true">${mark}</div>
|
|
503
|
+
<h1 id="callback-title">${escapeHtml(title)}</h1>
|
|
504
|
+
<p>${escapeHtml(message)}</p>
|
|
505
|
+
<div class="status">${success ? "Session approved" : "Session not approved"}</div>
|
|
506
|
+
</section>
|
|
507
|
+
</main>
|
|
508
|
+
</div>
|
|
400
509
|
</body>
|
|
401
510
|
</html>`;
|
|
402
511
|
}
|
package/lib/commands/billing.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Developer billing commands.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
5
6
|
import { accountRequest } from "../client.js";
|
|
6
7
|
import { emit, globalFlags, printList } from "../output.js";
|
|
7
8
|
|
|
@@ -51,6 +52,40 @@ export function registerBillingCommands(program) {
|
|
|
51
52
|
emit(flags.json ? data : `Created top-up ${data.id}`, flags);
|
|
52
53
|
});
|
|
53
54
|
|
|
55
|
+
billing
|
|
56
|
+
.command("checkout")
|
|
57
|
+
.description("Create a Stripe-hosted developer billing checkout URL")
|
|
58
|
+
.requiredOption("--success-url <url>")
|
|
59
|
+
.requiredOption("--cancel-url <url>")
|
|
60
|
+
.option("--open", "Open checkout URL in the default browser")
|
|
61
|
+
.action(async (opts, cmd) => {
|
|
62
|
+
const flags = globalFlags(cmd);
|
|
63
|
+
const { data } = await accountRequest("POST", "/billing/checkout", {
|
|
64
|
+
body: {
|
|
65
|
+
success_url: opts.successUrl,
|
|
66
|
+
cancel_url: opts.cancelUrl,
|
|
67
|
+
},
|
|
68
|
+
json: flags.json,
|
|
69
|
+
});
|
|
70
|
+
if (opts.open) openUrl(data.checkout_url);
|
|
71
|
+
emit(flags.json ? data : data.checkout_url, flags);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
billing
|
|
75
|
+
.command("portal")
|
|
76
|
+
.description("Create a Stripe-hosted developer billing portal URL")
|
|
77
|
+
.requiredOption("--return-url <url>")
|
|
78
|
+
.option("--open", "Open portal URL in the default browser")
|
|
79
|
+
.action(async (opts, cmd) => {
|
|
80
|
+
const flags = globalFlags(cmd);
|
|
81
|
+
const { data } = await accountRequest("POST", "/billing/portal", {
|
|
82
|
+
body: { return_url: opts.returnUrl },
|
|
83
|
+
json: flags.json,
|
|
84
|
+
});
|
|
85
|
+
if (opts.open) openUrl(data.url);
|
|
86
|
+
emit(flags.json ? data : data.url, flags);
|
|
87
|
+
});
|
|
88
|
+
|
|
54
89
|
billing
|
|
55
90
|
.command("prepaid")
|
|
56
91
|
.description("Update prepaid wallet settings")
|
|
@@ -85,3 +120,11 @@ export function registerBillingCommands(program) {
|
|
|
85
120
|
emit(flags.json ? data : "Updated prepaid settings", flags);
|
|
86
121
|
});
|
|
87
122
|
}
|
|
123
|
+
|
|
124
|
+
function openUrl(url) {
|
|
125
|
+
const command =
|
|
126
|
+
process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
127
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
128
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
129
|
+
child.unref();
|
|
130
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hosted docs and embedded MCP commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
docsCapabilities,
|
|
7
|
+
listDocs,
|
|
8
|
+
readDoc,
|
|
9
|
+
searchDocs,
|
|
10
|
+
DocsClientError,
|
|
11
|
+
} from "../docsClient.js";
|
|
12
|
+
import { runMcpServer } from "../mcpServer.js";
|
|
13
|
+
import { emit, fail, globalFlags, redactSecrets } from "../output.js";
|
|
14
|
+
|
|
15
|
+
export function registerDocsCommands(program) {
|
|
16
|
+
const docs = program.command("docs").description("Public docs and MCP server");
|
|
17
|
+
|
|
18
|
+
docs
|
|
19
|
+
.command("list")
|
|
20
|
+
.description("List public Switchboard docs")
|
|
21
|
+
.option("--json", "Output JSON to stdout")
|
|
22
|
+
.action(async (_opts, cmd) => {
|
|
23
|
+
await runDocsAction(cmd, async (flags) => {
|
|
24
|
+
const result = await listDocs();
|
|
25
|
+
if (jsonMode(cmd)) return emit(result, { json: true });
|
|
26
|
+
|
|
27
|
+
for (const doc of result.data || []) {
|
|
28
|
+
emit(`${doc.id}\t${doc.title}\t${doc.url}`, flags);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
docs
|
|
34
|
+
.command("read")
|
|
35
|
+
.description("Read one public Switchboard doc")
|
|
36
|
+
.argument("<id>", "Stable public doc id")
|
|
37
|
+
.option("--json", "Output JSON to stdout")
|
|
38
|
+
.action(async (id, _opts, cmd) => {
|
|
39
|
+
await runDocsAction(cmd, async (flags) => {
|
|
40
|
+
const result = await readDoc(id);
|
|
41
|
+
if (jsonMode(cmd)) return emit(result, { json: true });
|
|
42
|
+
emit(result.data?.content || "", flags);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
docs
|
|
47
|
+
.command("search")
|
|
48
|
+
.description("Search public Switchboard docs")
|
|
49
|
+
.argument("<query>", "Search query")
|
|
50
|
+
.option("--limit <count>", "Maximum number of snippets", parseInteger)
|
|
51
|
+
.option("--json", "Output JSON to stdout")
|
|
52
|
+
.action(async (query, opts, cmd) => {
|
|
53
|
+
await runDocsAction(cmd, async (flags) => {
|
|
54
|
+
const result = await searchDocs(query, { limit: opts.limit });
|
|
55
|
+
if (jsonMode(cmd)) return emit(result, { json: true });
|
|
56
|
+
|
|
57
|
+
for (const match of result.data || []) {
|
|
58
|
+
emit(`${match.id}\t${match.title}\n${match.snippet}\n`, flags);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
docs
|
|
64
|
+
.command("mcp")
|
|
65
|
+
.description("Start the embedded Switchboard MCP stdio server")
|
|
66
|
+
.action(async () => {
|
|
67
|
+
await runMcpServer();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
docs
|
|
71
|
+
.command("mcp-config")
|
|
72
|
+
.description("Print MCP client configuration")
|
|
73
|
+
.requiredOption("--client <client>", "MCP client: codex, claude, or cursor")
|
|
74
|
+
.option("--json", "Output JSON to stdout")
|
|
75
|
+
.action(async (opts, cmd) => {
|
|
76
|
+
const client = opts.client.toLowerCase();
|
|
77
|
+
const config = mcpConfig(client);
|
|
78
|
+
|
|
79
|
+
if (!config) {
|
|
80
|
+
fail("Unsupported MCP client. Expected one of: codex, claude, cursor.", 1, jsonMode(cmd));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (jsonMode(cmd)) {
|
|
84
|
+
emit(config, { json: true });
|
|
85
|
+
} else {
|
|
86
|
+
emit(formatConfig(client, config), globalFlags(cmd));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
docs
|
|
91
|
+
.command("capabilities")
|
|
92
|
+
.description("Print hosted MCP capability metadata")
|
|
93
|
+
.option("--json", "Output JSON to stdout")
|
|
94
|
+
.action(async (_opts, cmd) => {
|
|
95
|
+
await runDocsAction(cmd, async (flags) => {
|
|
96
|
+
const result = await docsCapabilities();
|
|
97
|
+
emit(result, { json: jsonMode(cmd), quiet: flags.quiet });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runDocsAction(cmd, callback) {
|
|
103
|
+
const flags = globalFlags(cmd);
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await callback(flags);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error instanceof DocsClientError) {
|
|
109
|
+
fail(error.message, exitCode(error.status), jsonMode(cmd), error.type);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fail(error.message || "Switchboard docs command failed.", 1, jsonMode(cmd));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function jsonMode(cmd) {
|
|
117
|
+
return Boolean(cmd.opts?.().json || globalFlags(cmd).json);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function exitCode(status) {
|
|
121
|
+
if (status === 401 || status === 403) return 2;
|
|
122
|
+
if (status >= 500 || status === 3) return 3;
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseInteger(value) {
|
|
127
|
+
const parsed = Number.parseInt(value, 10);
|
|
128
|
+
if (!Number.isInteger(parsed)) {
|
|
129
|
+
throw new Error("Limit must be an integer.");
|
|
130
|
+
}
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function mcpConfig(client) {
|
|
135
|
+
const command = "switchboard";
|
|
136
|
+
const args = ["docs", "mcp"];
|
|
137
|
+
|
|
138
|
+
switch (client) {
|
|
139
|
+
case "codex":
|
|
140
|
+
return {
|
|
141
|
+
mcp_servers: {
|
|
142
|
+
switchboard: {
|
|
143
|
+
command,
|
|
144
|
+
args,
|
|
145
|
+
env: {
|
|
146
|
+
SWITCHBOARD_BASE_URL: "https://switchboard.spot",
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
case "claude":
|
|
152
|
+
case "cursor":
|
|
153
|
+
return {
|
|
154
|
+
mcpServers: {
|
|
155
|
+
switchboard: {
|
|
156
|
+
command,
|
|
157
|
+
args,
|
|
158
|
+
env: {
|
|
159
|
+
SWITCHBOARD_BASE_URL: "https://switchboard.spot",
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
default:
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function formatConfig(client, config) {
|
|
170
|
+
return [
|
|
171
|
+
`# ${client} MCP config`,
|
|
172
|
+
"```json",
|
|
173
|
+
JSON.stringify(redactSecrets(config), null, 2),
|
|
174
|
+
"```",
|
|
175
|
+
].join("\n");
|
|
176
|
+
}
|