@switchboard.spot/cli 0.2.0 → 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.
@@ -9,12 +9,16 @@
9
9
 
10
10
  import { execFile } from "node:child_process";
11
11
  import { spawn } from "node:child_process";
12
+ import fs from "node:fs";
13
+ import os from "node:os";
14
+ import path from "node:path";
12
15
  import { promisify } from "node:util";
13
16
 
14
17
  const execFileAsync = promisify(execFile);
15
18
  const SERVICE = "Switchboard CLI";
16
19
  const ACCOUNT = "account-session";
17
20
  const PROJECT_SECRET_PREFIX = "project-secret";
21
+ const ACCOUNT_SESSION_FILE = "account-session.json";
18
22
 
19
23
  /**
20
24
  * Reads the current account session token from the OS keychain.
@@ -54,6 +58,30 @@ export async function getAccountToken() {
54
58
  }
55
59
  }
56
60
 
61
+ /**
62
+ * Reads the current account session token from the isolated CLI config dir.
63
+ */
64
+ export function getConfigDirAccountToken(configDir = switchboardConfigDir()) {
65
+ const file = accountSessionFile(configDir);
66
+
67
+ if (!fs.existsSync(file)) {
68
+ return null;
69
+ }
70
+
71
+ let data;
72
+ try {
73
+ data = JSON.parse(fs.readFileSync(file, "utf8"));
74
+ } catch (error) {
75
+ throw new Error(`Could not read Switchboard config-dir account session: ${error.message}`);
76
+ }
77
+
78
+ if (!data || typeof data.token !== "string" || data.token.trim() === "") {
79
+ throw new Error("Switchboard config-dir account session is invalid");
80
+ }
81
+
82
+ return data.token;
83
+ }
84
+
57
85
  /**
58
86
  * Writes the current account session token to the OS keychain.
59
87
  */
@@ -92,6 +120,23 @@ export async function setAccountToken(token) {
92
120
  }
93
121
  }
94
122
 
123
+ /**
124
+ * Writes the current account session token to the isolated CLI config dir.
125
+ */
126
+ export function setConfigDirAccountToken(token, configDir = switchboardConfigDir()) {
127
+ if (!token) {
128
+ throw new Error("Cannot store an empty Switchboard account token");
129
+ }
130
+
131
+ fs.mkdirSync(configDir, { recursive: true });
132
+ fs.writeFileSync(
133
+ accountSessionFile(configDir),
134
+ `${JSON.stringify({ token }, null, 2)}\n`,
135
+ { mode: 0o600 },
136
+ );
137
+ chmodAccountSessionFile(configDir);
138
+ }
139
+
95
140
  /**
96
141
  * Deletes the current account session token from the OS keychain.
97
142
  */
@@ -129,6 +174,29 @@ export async function deleteAccountToken() {
129
174
  }
130
175
  }
131
176
 
177
+ /**
178
+ * Deletes the current account session token from the isolated CLI config dir.
179
+ */
180
+ export function deleteConfigDirAccountToken(configDir = switchboardConfigDir()) {
181
+ fs.rmSync(accountSessionFile(configDir), { force: true });
182
+ }
183
+
184
+ export function accountSessionFile(configDir = switchboardConfigDir()) {
185
+ return path.join(configDir, ACCOUNT_SESSION_FILE);
186
+ }
187
+
188
+ function switchboardConfigDir() {
189
+ return process.env.SWITCHBOARD_CONFIG_DIR || path.join(os.homedir(), ".switchboard");
190
+ }
191
+
192
+ function chmodAccountSessionFile(configDir) {
193
+ try {
194
+ fs.chmodSync(accountSessionFile(configDir), 0o600);
195
+ } catch {
196
+ /* best effort on platforms that do not support chmod */
197
+ }
198
+ }
199
+
132
200
  /**
133
201
  * Reads a stored project secret key from the OS keychain.
134
202
  */
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Public docs client used by CLI docs commands and the embedded MCP server.
3
+ */
4
+
5
+ import { gatewayApiUrl, resolveAccountConfig, resolveConfig, accountApiUrl } from "./config.js";
6
+ import { redactSecrets } from "./output.js";
7
+
8
+ const DEFAULT_TIMEOUT_MS = 15_000;
9
+
10
+ export function docsBaseUrl(config = resolveConfig()) {
11
+ return gatewayApiUrl(config);
12
+ }
13
+
14
+ export async function listDocs({ config } = {}) {
15
+ const data = await publicJson("/docs", { config });
16
+ return redactSecrets(data);
17
+ }
18
+
19
+ export async function readDoc(id, { config } = {}) {
20
+ const data = await publicJson(`/docs/${encodeURIComponent(id)}`, { config });
21
+ return redactSecrets(data);
22
+ }
23
+
24
+ export async function searchDocs(query, { limit, config } = {}) {
25
+ const params = new URLSearchParams({ q: query });
26
+ if (limit != null) params.set("limit", String(limit));
27
+ const data = await publicJson(`/docs/search?${params}`, { config });
28
+ return redactSecrets(data);
29
+ }
30
+
31
+ export async function docsCapabilities({ config } = {}) {
32
+ const data = await publicJson("/docs/capabilities", { config });
33
+ return redactSecrets(data);
34
+ }
35
+
36
+ export async function openApi({ config } = {}) {
37
+ const data = await publicJson("/openapi.json", { config });
38
+ return redactSecrets(data);
39
+ }
40
+
41
+ export async function models({ config } = {}) {
42
+ const data = await publicJson("/models", { config });
43
+ return redactSecrets(data);
44
+ }
45
+
46
+ export async function integrationKit({ stack, config } = {}) {
47
+ let cfg;
48
+ try {
49
+ cfg = await resolveAccountConfig(config || resolveConfig());
50
+ } catch (error) {
51
+ return {
52
+ ok: false,
53
+ error: {
54
+ type: "keychain_unavailable",
55
+ message: error.message || "Could not read Switchboard account session from the OS keychain.",
56
+ },
57
+ };
58
+ }
59
+
60
+ if (!cfg.accountToken) {
61
+ return {
62
+ ok: false,
63
+ error: {
64
+ type: "authentication_required",
65
+ message: "Run `switchboard auth login`, then select a project with `switchboard projects use <id>`.",
66
+ },
67
+ };
68
+ }
69
+
70
+ if (!cfg.projectId) {
71
+ return {
72
+ ok: false,
73
+ error: {
74
+ type: "project_required",
75
+ message: "Select a project with `switchboard projects use <id>` or set SWITCHBOARD_PROJECT_ID.",
76
+ },
77
+ };
78
+ }
79
+
80
+ const params = new URLSearchParams({ project_id: cfg.projectId });
81
+ if (stack) params.set("stack", stack);
82
+
83
+ const url = `${accountApiUrl(cfg)}/integration_kit?${params}`;
84
+ const data = await fetchJson(url, {
85
+ headers: {
86
+ Authorization: `Bearer ${cfg.accountToken}`,
87
+ Accept: "application/json",
88
+ },
89
+ });
90
+
91
+ return redactSecrets(data);
92
+ }
93
+
94
+ export async function publicResource(name, options = {}) {
95
+ switch (name) {
96
+ case "docs":
97
+ return listDocs(options);
98
+ case "llms":
99
+ return readDoc("llms", options);
100
+ case "knowledge":
101
+ return readDoc("knowledge", options);
102
+ case "openapi":
103
+ return openApi(options);
104
+ case "integration-kit":
105
+ return integrationKit(options);
106
+ case "capabilities":
107
+ return docsCapabilities(options);
108
+ default:
109
+ throw new DocsClientError(`Unknown Switchboard resource: ${name}`, "not_found", 404);
110
+ }
111
+ }
112
+
113
+ async function publicJson(path, { config } = {}) {
114
+ const cfg = config || resolveConfig();
115
+ return fetchJson(`${docsBaseUrl(cfg)}${path}`, {
116
+ headers: { Accept: "application/json" },
117
+ });
118
+ }
119
+
120
+ async function fetchJson(url, init = {}) {
121
+ const controller = new AbortController();
122
+ const timeout = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
123
+
124
+ let res;
125
+ try {
126
+ res = await fetch(url, { ...init, signal: controller.signal });
127
+ } catch (error) {
128
+ throw new DocsClientError(error.message || "Switchboard request failed.", "network_error", 3);
129
+ } finally {
130
+ clearTimeout(timeout);
131
+ }
132
+
133
+ const text = await res.text();
134
+ let data;
135
+ try {
136
+ data = text ? JSON.parse(text) : null;
137
+ } catch {
138
+ data = { raw: text };
139
+ }
140
+
141
+ if (!res.ok) {
142
+ const message = data?.error?.message || text || `HTTP ${res.status}`;
143
+ const type = data?.error?.type || "http_error";
144
+ throw new DocsClientError(message, type, res.status);
145
+ }
146
+
147
+ return data;
148
+ }
149
+
150
+ export class DocsClientError extends Error {
151
+ constructor(message, type = "error", status = 1) {
152
+ super(message);
153
+ this.name = "DocsClientError";
154
+ this.type = type;
155
+ this.status = status;
156
+ }
157
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Embedded Switchboard MCP stdio server.
3
+ */
4
+
5
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+ import { z } from "zod";
8
+ import {
9
+ docsCapabilities,
10
+ integrationKit,
11
+ listDocs,
12
+ models,
13
+ openApi,
14
+ publicResource,
15
+ readDoc,
16
+ searchDocs,
17
+ } from "./docsClient.js";
18
+ import { redactSecrets } from "./output.js";
19
+
20
+ const SERVER_VERSION = "0.1.0";
21
+
22
+ export async function runMcpServer() {
23
+ const server = createMcpServer();
24
+ await server.connect(new StdioServerTransport());
25
+ }
26
+
27
+ export function createMcpServer() {
28
+ const server = new McpServer({
29
+ name: "@switchboard.spot/cli",
30
+ version: SERVER_VERSION,
31
+ });
32
+
33
+ registerResources(server);
34
+ registerTools(server);
35
+
36
+ return server;
37
+ }
38
+
39
+ function registerResources(server) {
40
+ registerJsonResource(server, "switchboard_docs", "switchboard://docs", "Public Switchboard docs catalog", () =>
41
+ listDocs(),
42
+ );
43
+ registerJsonResource(server, "switchboard_llms", "switchboard://llms", "Public llms.txt context", () =>
44
+ publicResource("llms"),
45
+ );
46
+ registerJsonResource(
47
+ server,
48
+ "switchboard_knowledge",
49
+ "switchboard://knowledge",
50
+ "Public Switchboard knowledge context",
51
+ () => publicResource("knowledge"),
52
+ );
53
+ registerJsonResource(server, "switchboard_openapi", "switchboard://openapi", "Switchboard OpenAPI schema", () =>
54
+ openApi(),
55
+ );
56
+ registerJsonResource(
57
+ server,
58
+ "switchboard_integration_kit",
59
+ "switchboard://integration-kit",
60
+ "Project Integration Kit for the selected CLI project",
61
+ () => integrationKit(),
62
+ );
63
+ registerJsonResource(
64
+ server,
65
+ "switchboard_capabilities",
66
+ "switchboard://capabilities",
67
+ "Switchboard hosted MCP capabilities",
68
+ () => docsCapabilities(),
69
+ );
70
+
71
+ server.registerResource(
72
+ "switchboard_doc",
73
+ new ResourceTemplate("switchboard://docs/{id}", {
74
+ list: async () => {
75
+ const catalog = await listDocs();
76
+ return {
77
+ resources: (catalog.data || []).map((doc) => ({
78
+ uri: `switchboard://docs/${doc.id}`,
79
+ name: doc.id,
80
+ title: doc.title,
81
+ description: doc.description,
82
+ mimeType: "text/markdown",
83
+ })),
84
+ };
85
+ },
86
+ }),
87
+ {
88
+ title: "Switchboard doc",
89
+ description: "Read one public Switchboard doc by stable id.",
90
+ mimeType: "text/markdown",
91
+ },
92
+ async (_uri, variables) => {
93
+ const doc = await readDoc(String(variables.id));
94
+ return {
95
+ contents: [
96
+ {
97
+ uri: `switchboard://docs/${doc.data.id}`,
98
+ mimeType: doc.data.content_type || "text/markdown",
99
+ text: redactText(doc.data.content),
100
+ },
101
+ ],
102
+ };
103
+ },
104
+ );
105
+ }
106
+
107
+ function registerTools(server) {
108
+ server.registerTool(
109
+ "switchboard_docs_search",
110
+ {
111
+ title: "Search Switchboard docs",
112
+ description: "Search public Switchboard docs and return bounded snippets.",
113
+ inputSchema: {
114
+ query: z.string().min(1),
115
+ limit: z.number().int().min(1).max(20).optional(),
116
+ },
117
+ annotations: { readOnlyHint: true },
118
+ },
119
+ async ({ query, limit }) => jsonTool(await searchDocs(query, { limit })),
120
+ );
121
+
122
+ server.registerTool(
123
+ "switchboard_docs_read",
124
+ {
125
+ title: "Read Switchboard doc",
126
+ description: "Read one public Switchboard doc by stable id.",
127
+ inputSchema: {
128
+ id: z.string().min(1),
129
+ },
130
+ annotations: { readOnlyHint: true },
131
+ },
132
+ async ({ id }) => jsonTool(await readDoc(id)),
133
+ );
134
+
135
+ server.registerTool(
136
+ "switchboard_integration_kit",
137
+ {
138
+ title: "Switchboard Integration Kit",
139
+ description: "Return Integration Kit data for the logged-in selected CLI project.",
140
+ inputSchema: {
141
+ stack: z.string().optional(),
142
+ },
143
+ annotations: { readOnlyHint: true },
144
+ },
145
+ async ({ stack }) => jsonTool(await integrationKit({ stack })),
146
+ );
147
+
148
+ server.registerTool(
149
+ "switchboard_models",
150
+ {
151
+ title: "Switchboard models",
152
+ description: "List public OpenAI-compatible Switchboard models.",
153
+ annotations: { readOnlyHint: true },
154
+ },
155
+ async () => jsonTool(await models()),
156
+ );
157
+ }
158
+
159
+ function registerJsonResource(server, name, uri, description, loader) {
160
+ server.registerResource(
161
+ name,
162
+ uri,
163
+ {
164
+ title: name,
165
+ description,
166
+ mimeType: "application/json",
167
+ },
168
+ async () => ({
169
+ contents: [
170
+ {
171
+ uri,
172
+ mimeType: "application/json",
173
+ text: JSON.stringify(redactSecrets(await loader()), null, 2),
174
+ },
175
+ ],
176
+ }),
177
+ );
178
+ }
179
+
180
+ function jsonTool(data) {
181
+ return {
182
+ content: [
183
+ {
184
+ type: "text",
185
+ text: JSON.stringify(redactSecrets(data), null, 2),
186
+ },
187
+ ],
188
+ };
189
+ }
190
+
191
+ function redactText(text) {
192
+ return typeof text === "string" ? redactSecrets(text) : JSON.stringify(redactSecrets(text), null, 2);
193
+ }
package/lib/output.js CHANGED
@@ -2,16 +2,30 @@
2
2
  * Human-readable and JSON output helpers for the Switchboard CLI.
3
3
  */
4
4
 
5
+ const REDACTED = "[REDACTED]";
6
+
7
+ const SECRET_PATTERNS = [
8
+ /\bsb_(?:test|live|sess|eusr)_[A-Za-z0-9._-]+\b/g,
9
+ /\bsk-(?:proj-|ant-)?[A-Za-z0-9._-]{20,}\b/g,
10
+ /\bsk_(?:live|test)_[A-Za-z0-9._-]+\b/g,
11
+ /\brk_(?:live|test)_[A-Za-z0-9._-]+\b/g,
12
+ /\bpk_(?:live|test)_[A-Za-z0-9._-]+\b/g,
13
+ /\bwhsec_[A-Za-z0-9._-]+\b/g,
14
+ /\b0x[0-9A-Za-z_-]{20,}\b/g,
15
+ /\b[A-Za-z0-9_-]{24,}\.[A-Za-z0-9_-]{24,}\.[A-Za-z0-9_-]{24,}\b/g,
16
+ /\b(?:OPENAI|ANTHROPIC|GROQ|MISTRAL|GOOGLE|STRIPE|CLOUDFLARE)_[A-Z0-9_]*(?:KEY|TOKEN|SECRET)\s*=\s*[^\s]+/gi,
17
+ ];
18
+
5
19
  /**
6
20
  * Prints data as JSON or a human message depending on global flags.
7
21
  */
8
22
  export function emit(data, { json, quiet } = {}) {
9
23
  if (json) {
10
- console.log(JSON.stringify(data, null, 2));
24
+ console.log(JSON.stringify(redactSecrets(data), null, 2));
11
25
  return;
12
26
  }
13
27
  if (!quiet && typeof data === "string") {
14
- console.log(data);
28
+ console.log(redactSecrets(data));
15
29
  }
16
30
  }
17
31
 
@@ -35,13 +49,13 @@ export function fail(message, code = 1, json = false, type = "invalid_request")
35
49
  if (json) {
36
50
  console.log(
37
51
  JSON.stringify(
38
- {
52
+ redactSecrets({
39
53
  ok: false,
40
54
  error: {
41
55
  type,
42
56
  message,
43
57
  },
44
- },
58
+ }),
45
59
  null,
46
60
  2,
47
61
  ),
@@ -49,7 +63,7 @@ export function fail(message, code = 1, json = false, type = "invalid_request")
49
63
  process.exit(code);
50
64
  }
51
65
 
52
- console.error(message);
66
+ console.error(redactSecrets(message));
53
67
  process.exit(code);
54
68
  }
55
69
 
@@ -93,9 +107,9 @@ export function normalizeError(data, text, status) {
93
107
 
94
108
  export function emitHttpError(error, status, flags = {}) {
95
109
  if (flags.json) {
96
- console.log(JSON.stringify({ ok: false, status, error }, null, 2));
110
+ console.log(JSON.stringify(redactSecrets({ ok: false, status, error }), null, 2));
97
111
  } else {
98
- console.error(error.message);
112
+ console.error(redactSecrets(error.message));
99
113
  }
100
114
 
101
115
  process.exit(exitCodeForStatus(status));
@@ -105,8 +119,50 @@ export function emitHttpError(error, status, flags = {}) {
105
119
  * Formats a simple key-value listing for human output.
106
120
  */
107
121
  export function printList(title, items, formatter) {
108
- console.log(title);
122
+ console.log(redactSecrets(title));
109
123
  for (const item of items) {
110
- console.log(formatter(item));
124
+ console.log(redactSecrets(formatter(item)));
125
+ }
126
+ }
127
+
128
+ export function redactSecrets(value) {
129
+ if (typeof value === "string") return redactString(value);
130
+ if (Array.isArray(value)) return value.map((entry) => redactSecrets(entry));
131
+ if (!value || typeof value !== "object") return value;
132
+
133
+ const redacted = {};
134
+ for (const [key, entry] of Object.entries(value)) {
135
+ redacted[key] = isSecretKeyName(key) ? redactKnownPublicKey(key, entry) : redactSecrets(entry);
111
136
  }
137
+
138
+ return redacted;
139
+ }
140
+
141
+ function redactKnownPublicKey(key, value) {
142
+ if (key === "site_key" || key === "turnstile_site_key") return redactSecrets(value);
143
+ return typeof value === "boolean" || value == null ? value : REDACTED;
144
+ }
145
+
146
+ function redactString(value) {
147
+ return SECRET_PATTERNS.reduce(
148
+ (text, pattern) => text.replace(pattern, (match) => redactAssignment(match)),
149
+ value,
150
+ );
151
+ }
152
+
153
+ function redactAssignment(match) {
154
+ const index = match.indexOf("=");
155
+ if (index === -1) return REDACTED;
156
+ return `${match.slice(0, index + 1)}${REDACTED}`;
157
+ }
158
+
159
+ function isSecretKeyName(key) {
160
+ const normalized = key.replace(/[A-Z]/g, "_$&").toLowerCase();
161
+ if (normalized.endsWith("_count") || normalized.endsWith("_configured")) return false;
162
+ if (normalized === "site_key" || normalized === "turnstile_site_key") return false;
163
+
164
+ return (
165
+ /(^|_)(secret|token|password|plaintext|session)($|_)/.test(normalized) ||
166
+ /(^|_)api_key($|_)/.test(normalized)
167
+ );
112
168
  }
@@ -70,6 +70,8 @@ const PROTECTED_AUTH_HOST_PATTERNS = [
70
70
  ];
71
71
 
72
72
  const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "0.0.0.0"]);
73
+ const DEV_BROWSER_CHALLENGE_TOKEN = "dev_browser_challenge";
74
+ const DEFAULT_BROWSER_CHALLENGE_TOKEN = "switchboard-verification";
73
75
 
74
76
  export function loadScenario(filePath) {
75
77
  if (!filePath) return DEFAULT_SCENARIO;
@@ -427,7 +429,7 @@ async function executeScenarioCheck({
427
429
  const result = await browserFetchJson(page, clientUrl, "/auth/anonymous/session", {
428
430
  method: "POST",
429
431
  body: {
430
- browser_challenge_token: check.browserChallengeToken ?? "switchboard-verification",
432
+ browser_challenge_token: browserChallengeTokenFor(check, clientUrl),
431
433
  },
432
434
  });
433
435
  if (result.data?.token) {
@@ -478,14 +480,16 @@ async function executeScenarioCheck({
478
480
  }
479
481
 
480
482
  async function browserFetchJson(page, clientUrl, endpointPath, init) {
483
+ const endpointUrl = clientEndpointUrl(clientUrl, endpointPath);
484
+
481
485
  return page.evaluate(
482
- async ({ clientUrl, endpointPath, init }) => {
486
+ async ({ endpointUrl, init }) => {
483
487
  try {
484
488
  const headers = new Headers(init.headers ?? {});
485
489
  headers.set("Accept", "application/json");
486
490
  if (init.body) headers.set("Content-Type", "application/json");
487
491
 
488
- const response = await fetch(new URL(endpointPath, clientUrl).href, {
492
+ const response = await fetch(endpointUrl, {
489
493
  method: init.method,
490
494
  headers,
491
495
  body: init.body ? JSON.stringify(init.body) : undefined,
@@ -508,10 +512,18 @@ async function browserFetchJson(page, clientUrl, endpointPath, init) {
508
512
  return { ok: false, status: 0, data: null, error: error.message };
509
513
  }
510
514
  },
511
- { clientUrl: clientUrl.href, endpointPath, init },
515
+ { endpointUrl, init },
512
516
  );
513
517
  }
514
518
 
519
+ function clientEndpointUrl(clientUrl, endpointPath) {
520
+ const endpoint = endpointPath.startsWith("/") ? endpointPath.slice(1) : endpointPath;
521
+ const basePath = clientUrl.pathname.endsWith("/") ? clientUrl.pathname : `${clientUrl.pathname}/`;
522
+ const url = new URL(clientUrl.href);
523
+ url.pathname = `${basePath}${endpoint}`.replace(/\/{2,}/g, "/");
524
+ return url.href;
525
+ }
526
+
515
527
  function wireEvidence(page, evidence) {
516
528
  page.on("console", (message) => {
517
529
  evidence.consoleMessages.push({
@@ -709,6 +721,22 @@ function normalizedHostname(url) {
709
721
  return url.hostname.replace(/^\[|\]$/g, "");
710
722
  }
711
723
 
724
+ function browserChallengeTokenFor(check, clientUrl) {
725
+ if (Object.prototype.hasOwnProperty.call(check, "browserChallengeToken")) {
726
+ return check.browserChallengeToken;
727
+ }
728
+
729
+ if (isLocalSwitchboardUrl(clientUrl)) {
730
+ return DEV_BROWSER_CHALLENGE_TOKEN;
731
+ }
732
+
733
+ return DEFAULT_BROWSER_CHALLENGE_TOKEN;
734
+ }
735
+
736
+ function isLocalSwitchboardUrl(url) {
737
+ return url instanceof URL && LOCAL_HOSTS.has(normalizedHostname(url));
738
+ }
739
+
712
740
  function addFailure(report, checkId, message, finding) {
713
741
  addCheck(report, { id: checkId, status: "failed", message });
714
742
  report.findings.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@switchboard.spot/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Switchboard CLI — full dashboard parity for agents and testing",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,8 +17,10 @@
17
17
  "coverage": "node --test --experimental-test-coverage --test-coverage-include='bin/**/*.js' --test-coverage-include='lib/**/*.js' test/*.test.js"
18
18
  },
19
19
  "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.29.0",
20
21
  "commander": "^13.1.0",
21
- "playwright": "^1.61.0"
22
+ "playwright": "^1.61.0",
23
+ "zod": "^4.4.3"
22
24
  },
23
25
  "files": [
24
26
  "bin/",