@switchboard.spot/cli 0.2.1 → 0.2.2
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 +19 -6
- 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 +49 -0
- package/lib/commands/init.js +4 -2
- package/lib/commands/launch.js +192 -0
- package/lib/commands/projects.js +50 -4
- 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,34 @@ 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 projects create --name "My App" --slug my-app
|
|
57
58
|
switchboard setup --target client --json
|
|
58
|
-
switchboard
|
|
59
|
+
switchboard projects update <project-id> --allowed-origins http://localhost:5173 --virtual-microservice-enabled true
|
|
60
|
+
switchboard projects provision-turnstile <project-id>
|
|
61
|
+
switchboard verify setup
|
|
62
|
+
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"
|
|
63
|
+
switchboard verify publish
|
|
59
64
|
```
|
|
60
65
|
|
|
61
66
|
Use `--json` for automation, CI, and coding agents.
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
`setup --target client` writes public Client Gateway environment values such as `VITE_SWITCHBOARD_CLIENT_URL`; it does not prove chat or session readiness. Before SDK browser testing, configure the exact local or preview origin and provision Switchboard-managed Turnstile.
|
|
64
69
|
|
|
65
|
-
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
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`.
|
|
75
|
+
|
|
76
|
+
Switchboard-managed Turnstile is the default production path. Developers do not paste Cloudflare secrets into the CLI or repo files:
|
|
66
77
|
|
|
67
78
|
```bash
|
|
68
|
-
switchboard projects turnstile <project-id>
|
|
79
|
+
switchboard projects provision-turnstile <project-id>
|
|
69
80
|
switchboard projects turnstile <project-id> --clear
|
|
70
81
|
```
|
|
71
82
|
|
|
83
|
+
If `switchboard projects provision-turnstile --help` is missing, upgrade with `npm install -g @switchboard.spot/cli@latest` before trying dashboard automation or manual Cloudflare keys.
|
|
84
|
+
|
|
72
85
|
## Configuration
|
|
73
86
|
|
|
74
87
|
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
|
+
}
|
package/lib/commands/doctor.js
CHANGED
|
@@ -67,8 +67,57 @@ export function registerDoctorCommand(program) {
|
|
|
67
67
|
checks.push({ name: "project", ok: false, error: "No project selected" });
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
if (config.virtualMicroserviceUrl) {
|
|
71
|
+
checks.push(
|
|
72
|
+
await check("client_gateway_config", async () => {
|
|
73
|
+
const res = await fetch(new URL("client/config", ensureTrailingSlash(config.virtualMicroserviceUrl)).href, {
|
|
74
|
+
headers: { Accept: "application/json" },
|
|
75
|
+
});
|
|
76
|
+
const data = await readJson(res);
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
throw new Error(data?.error?.message || data?.message || `HTTP ${res.status}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const productionSafety = data?.production_safety ?? null;
|
|
82
|
+
const productionBlocked = productionSafety?.status === "blocked";
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
status: res.status,
|
|
86
|
+
clientUrl: config.virtualMicroserviceUrl,
|
|
87
|
+
browserChallengeProvider: data?.browser_challenge?.provider ?? null,
|
|
88
|
+
production_safety: productionSafety,
|
|
89
|
+
warning: productionBlocked,
|
|
90
|
+
warningCode: productionBlocked ? "production_safety_blocked" : null,
|
|
91
|
+
warningMessage: productionBlocked
|
|
92
|
+
? "Sandbox Client Gateway config is reachable, but production launch blockers remain."
|
|
93
|
+
: null,
|
|
94
|
+
};
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
checks.push({
|
|
99
|
+
name: "client_gateway_config",
|
|
100
|
+
ok: false,
|
|
101
|
+
error: "No SWITCHBOARD_CLIENT_URL or VITE_SWITCHBOARD_CLIENT_URL configured",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
70
105
|
const ok = checks.every((item) => item.ok);
|
|
71
106
|
emit({ object: "doctor_report", ok, baseUrl: config.baseUrl, checks }, flags);
|
|
72
107
|
if (!ok) process.exit(1);
|
|
108
|
+
process.exit(0);
|
|
73
109
|
});
|
|
74
110
|
}
|
|
111
|
+
|
|
112
|
+
function ensureTrailingSlash(value) {
|
|
113
|
+
return String(value).endsWith("/") ? String(value) : `${value}/`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function readJson(response) {
|
|
117
|
+
const text = await response.text();
|
|
118
|
+
try {
|
|
119
|
+
return text ? JSON.parse(text) : null;
|
|
120
|
+
} catch {
|
|
121
|
+
return { raw: text };
|
|
122
|
+
}
|
|
123
|
+
}
|
package/lib/commands/init.js
CHANGED
|
@@ -33,10 +33,12 @@ export function agentManifest(config) {
|
|
|
33
33
|
env: envBlock(clientUrl),
|
|
34
34
|
checklist: [
|
|
35
35
|
"switchboard setup --target client --json",
|
|
36
|
-
"export VITE_SWITCHBOARD_CLIENT_URL=<client_url from
|
|
36
|
+
"export VITE_SWITCHBOARD_CLIENT_URL=<client_url from integration kit>",
|
|
37
37
|
"Use mountSwitchboardWidget({ clientUrl, target }) in browser apps",
|
|
38
38
|
"Use createSwitchboardClient({ clientUrl, storage }) only for custom UI",
|
|
39
|
-
"switchboard
|
|
39
|
+
"switchboard verify setup",
|
|
40
|
+
"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\"",
|
|
41
|
+
"switchboard verify publish",
|
|
40
42
|
],
|
|
41
43
|
browser_smoke_test: `import { mountSwitchboardWidget } from "@switchboard.spot/sdk";
|
|
42
44
|
|