@mcp-guardian/cli 4.1.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/dist/index.js ADDED
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { scanServer, verifyToolDefinitions } from "@mcp-guardian/core";
7
+ import { fetchToolsFromStdio } from "@mcp-guardian/core";
8
+ import { fetchToolsFromHttp, fetchToolsFromSse } from "@mcp-guardian/core";
9
+ const { values: flags, positionals } = parseArgs({
10
+ args: process.argv.slice(2),
11
+ options: {
12
+ json: { type: "boolean", default: false },
13
+ "fail-on-critical": { type: "boolean", default: false },
14
+ "fail-on-warning": { type: "boolean", default: false },
15
+ "skip-semantic": { type: "boolean", default: false },
16
+ "skip-pinning": { type: "boolean", default: false },
17
+ verbose: { type: "boolean", short: "v", default: false },
18
+ url: { type: "string" },
19
+ transport: { type: "string" },
20
+ server: { type: "string" },
21
+ mcp: { type: "boolean", default: false },
22
+ },
23
+ allowPositionals: true,
24
+ });
25
+ async function resolveConfigPath() {
26
+ if (positionals[0])
27
+ return positionals[0];
28
+ const candidates = [
29
+ join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
30
+ join(homedir(), ".config", "Claude", "claude_desktop_config.json"),
31
+ join(process.env.APPDATA ?? "", "Claude", "claude_desktop_config.json"),
32
+ ];
33
+ return candidates.find(existsSync) ?? candidates[0];
34
+ }
35
+ function printReport(results, verbose) {
36
+ for (const server of results) {
37
+ const icon = server.status === "critical" ? "[CRIT]" :
38
+ server.status === "warning" ? "[WARN]" : "[OK]";
39
+ console.log(`\n${icon} ${server.serverName} [${server.transport}]`);
40
+ console.log(` ${server.summary.total} tools | ${server.summary.critical} critical | ${server.summary.warnings} warnings | ${server.summary.clean} clean`);
41
+ for (const tool of server.tools) {
42
+ if (tool.status === "clean" && !verbose)
43
+ continue;
44
+ const toolIcon = tool.status === "critical" ? " [CRIT]" : " [WARN]";
45
+ console.log(`${toolIcon} ${tool.toolName}`);
46
+ for (const issue of tool.issues) {
47
+ if (issue.severity === "info")
48
+ continue;
49
+ console.log(` [${issue.id}] ${issue.message}`);
50
+ if (verbose && issue.evidence)
51
+ console.log(` evidence: "${issue.evidence}"`);
52
+ if (verbose && issue.confidence < 1.0)
53
+ console.log(` confidence: ${(issue.confidence * 100).toFixed(0)}%`);
54
+ }
55
+ }
56
+ }
57
+ }
58
+ async function main() {
59
+ if (flags.mcp) {
60
+ const { startMcpServer } = await import("@mcp-guardian/server");
61
+ await startMcpServer();
62
+ return;
63
+ }
64
+ const results = [];
65
+ // Single URL mode
66
+ if (flags.url) {
67
+ const transport = (flags.transport ?? "http");
68
+ const fetchFn = transport === "sse" ? fetchToolsFromSse : fetchToolsFromHttp;
69
+ const tools = await fetchFn({ url: flags.url });
70
+ const scanResult = await scanServer(flags.url, tools, transport, { skipSemantic: flags["skip-semantic"] });
71
+ if (!flags["skip-pinning"]) {
72
+ const pinResult = verifyToolDefinitions(tools, flags.url);
73
+ if (pinResult.status === "changed" || pinResult.status === "tampered") {
74
+ scanResult.status = "critical";
75
+ }
76
+ }
77
+ results.push(scanResult);
78
+ }
79
+ else {
80
+ // Claude Desktop config mode
81
+ const configPath = await resolveConfigPath();
82
+ if (!existsSync(configPath)) {
83
+ console.error(`Config not found: ${configPath}`);
84
+ process.exit(1);
85
+ }
86
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
87
+ const servers = config.mcpServers ?? {};
88
+ for (const [serverName, serverConfig] of Object.entries(servers)) {
89
+ if (flags.server && serverName !== flags.server)
90
+ continue;
91
+ let tools;
92
+ let transport = "stdio";
93
+ if (serverConfig.url) {
94
+ const raw = serverConfig.transport ?? "http";
95
+ if (raw === "sse") {
96
+ transport = "sse";
97
+ tools = await fetchToolsFromSse({ url: serverConfig.url });
98
+ }
99
+ else {
100
+ transport = "http";
101
+ tools = await fetchToolsFromHttp({ url: serverConfig.url });
102
+ }
103
+ }
104
+ else if (serverConfig.command) {
105
+ tools = await fetchToolsFromStdio({ command: serverConfig.command, args: serverConfig.args, env: serverConfig.env });
106
+ }
107
+ else {
108
+ console.warn(` Skipping ${serverName}: no command or URL`);
109
+ continue;
110
+ }
111
+ const scanResult = await scanServer(serverName, tools, transport, { skipSemantic: flags["skip-semantic"] });
112
+ if (!flags["skip-pinning"]) {
113
+ const pinResult = verifyToolDefinitions(tools, serverName);
114
+ if (pinResult.status === "changed")
115
+ console.warn(` Tool definitions changed in "${serverName}" since last approval`);
116
+ if (pinResult.status === "tampered") {
117
+ console.error(` Manifest tampered for "${serverName}" - treat as critical`);
118
+ scanResult.status = "critical";
119
+ }
120
+ }
121
+ results.push(scanResult);
122
+ }
123
+ }
124
+ if (flags.json) {
125
+ console.log(JSON.stringify(results, null, 2));
126
+ }
127
+ else {
128
+ printReport(results, flags.verbose ?? false);
129
+ }
130
+ const hasCritical = results.some(r => r.status === "critical");
131
+ const hasWarning = results.some(r => r.status === "warning");
132
+ if (flags["fail-on-critical"] && hasCritical)
133
+ process.exit(2);
134
+ if (flags["fail-on-warning"] && (hasCritical || hasWarning))
135
+ process.exit(2);
136
+ }
137
+ main().catch(err => {
138
+ console.error("Fatal:", err.message);
139
+ process.exit(1);
140
+ });
@@ -0,0 +1 @@
1
+ export declare function startTui(dashboardUrl?: string): Promise<void>;
@@ -0,0 +1,430 @@
1
+ /**
2
+ * MCP Guardian Interactive Polished CLI/TUI
3
+ *
4
+ * A real-time terminal dashboard aggregating metrics, logs, and audit trails
5
+ * from all MCP Guardian instances into a unified view.
6
+ *
7
+ * Zero mock data — all values come from live dashboard API or PostgreSQL.
8
+ */
9
+ import { DataFetcher } from './data-fetcher.js';
10
+ import chalk from 'chalk';
11
+ // ── Color helpers ──────────────────────────────────────────────────
12
+ const C = {
13
+ dim: chalk.dim,
14
+ bold: chalk.bold,
15
+ green: chalk.green,
16
+ red: chalk.red,
17
+ yellow: chalk.yellow,
18
+ cyan: chalk.cyan,
19
+ magenta: chalk.magenta,
20
+ white: chalk.white,
21
+ gray: chalk.gray,
22
+ blue: chalk.blue,
23
+ bgRed: chalk.bgRed,
24
+ bgGreen: chalk.bgGreen,
25
+ bgYellow: chalk.bgYellow,
26
+ };
27
+ export async function startTui(dashboardUrl) {
28
+ const fetcher = new DataFetcher(dashboardUrl);
29
+ const state = { activeTab: 0, running: true, lastRefresh: '', frameCount: 0 };
30
+ // ── Terminal setup ───────────────────────────────────────────────
31
+ const stdout = process.stdout;
32
+ const stdin = process.stdin;
33
+ stdin.setRawMode(true);
34
+ stdin.resume();
35
+ stdin.setEncoding('utf-8');
36
+ // Hide cursor
37
+ stdout.write('\x1B[?25l');
38
+ // ── Key handler ──────────────────────────────────────────────────
39
+ stdin.on('data', (key) => {
40
+ const code = key.charCodeAt(0);
41
+ if (code === 3 || code === 27) {
42
+ // Ctrl+C or Escape → quit
43
+ state.running = false;
44
+ }
45
+ else if (code === 9) {
46
+ // Tab → next tab
47
+ state.activeTab = (state.activeTab + 1) % 8;
48
+ }
49
+ else if (key === '1')
50
+ state.activeTab = 0;
51
+ else if (key === '2')
52
+ state.activeTab = 1;
53
+ else if (key === '3')
54
+ state.activeTab = 2;
55
+ else if (key === '4')
56
+ state.activeTab = 3;
57
+ else if (key === '5')
58
+ state.activeTab = 4;
59
+ else if (key === '6')
60
+ state.activeTab = 5;
61
+ else if (key === '7')
62
+ state.activeTab = 6;
63
+ else if (key === '8')
64
+ state.activeTab = 7;
65
+ else if (key === 'r') {
66
+ // Manual refresh
67
+ fetcher.fetchAll().catch(() => { });
68
+ }
69
+ });
70
+ // ── Connect to dashboard ─────────────────────────────────────────
71
+ fetcher.connectWebSocket();
72
+ // Fallback polling
73
+ const pollInterval = fetcher.startPolling(3000);
74
+ // Initial fetch
75
+ try {
76
+ await fetcher.fetchAll();
77
+ }
78
+ catch {
79
+ // Will show empty dashboard until data arrives
80
+ }
81
+ // Subscribe to data changes
82
+ fetcher.onChange(() => {
83
+ render(state, fetcher.getData());
84
+ });
85
+ // ── Render loop ──────────────────────────────────────────────────
86
+ const renderInterval = setInterval(() => {
87
+ if (!state.running) {
88
+ clearInterval(renderInterval);
89
+ clearInterval(pollInterval);
90
+ fetcher.stop();
91
+ stdout.write('\x1B[?25h'); // Show cursor
92
+ stdout.write('\x1B[2J\x1B[H'); // Clear screen
93
+ process.exit(0);
94
+ }
95
+ render(state, fetcher.getData());
96
+ state.frameCount++;
97
+ }, 1000 / 15); // 15 FPS
98
+ // Initial render
99
+ render(state, fetcher.getData());
100
+ // Keep process alive
101
+ await new Promise((resolve) => {
102
+ const check = setInterval(() => {
103
+ if (!state.running) {
104
+ clearInterval(check);
105
+ resolve();
106
+ }
107
+ }, 100);
108
+ });
109
+ }
110
+ // ── TABS ────────────────────────────────────────────────────────────
111
+ const TABS = [
112
+ { name: 'Overview', key: '1' },
113
+ { name: 'Security', key: '2' },
114
+ { name: 'Cost', key: '3' },
115
+ { name: 'Health', key: '4' },
116
+ { name: 'AI Engine', key: '5' },
117
+ { name: 'Audit Trail', key: '6' },
118
+ { name: 'Policy', key: '7' },
119
+ { name: 'Instances', key: '8' },
120
+ ];
121
+ // ── Main render function ───────────────────────────────────────────
122
+ function render(state, data) {
123
+ const stdout = process.stdout;
124
+ const termWidth = stdout.columns || 120;
125
+ const termHeight = stdout.rows || 30;
126
+ // Clear screen
127
+ stdout.write('\x1B[2J\x1B[H');
128
+ // ── Header ──────────────────────────────────────────────────────
129
+ const header = [
130
+ C.bold.cyan('╔══════════════════════════════════════════════════════════════════════════════════╗'),
131
+ C.bold.cyan(`║ MCP GUARDIAN — UNIFIED OBSERVABILITY PLATFORM${' '.repeat(Math.max(0, termWidth - 39 - 39))}║`),
132
+ C.bold.magenta(`║ Adaptive AI-Driven Policy Engine | Real-Time Metrics | Multi-Instance Aggregation ║`),
133
+ ];
134
+ for (const line of header)
135
+ stdout.write(line + '\n');
136
+ // ── Top bar ─────────────────────────────────────────────────────
137
+ if (data) {
138
+ const instancesText = `${data.instances.length} instances`;
139
+ const blockedText = `${data.overview.blockedRequests} blocked`;
140
+ const costText = `$${data.overview.totalCostUsd.toFixed(4)}`;
141
+ const latencyText = `${data.overview.avgLatencyMs.toFixed(0)}ms avg`;
142
+ const bar = C.dim(` ${C.green(instancesText)} │ ${C.red(blockedText)} │ ${C.yellow(costText)} │ ${C.cyan(latencyText)} │ ` +
143
+ `Updated: ${data.overview.lastUpdated?.slice(11, 19) || 'N/A'}`);
144
+ stdout.write(bar + '\n');
145
+ }
146
+ stdout.write(C.dim('─'.repeat(termWidth - 1)) + '\n');
147
+ // ── Tab bar ──────────────────────────────────────────────────────
148
+ let tabBar = '';
149
+ for (let i = 0; i < TABS.length; i++) {
150
+ if (i === state.activeTab) {
151
+ tabBar += C.bgGreen.black(` ${TABS[i].key}. ${TABS[i].name} `) + ' ';
152
+ }
153
+ else {
154
+ tabBar += C.dim(`${TABS[i].key}.`) + ` ${TABS[i].name} `;
155
+ }
156
+ }
157
+ stdout.write(' ' + tabBar + '\n');
158
+ stdout.write(C.dim('─'.repeat(termWidth - 1)) + '\n\n');
159
+ // ── Tab content ─────────────────────────────────────────────────
160
+ if (!data) {
161
+ stdout.write(C.yellow('\n ⏳ Connecting to dashboard... Fetching real-time data.\n'));
162
+ stdout.write(C.dim(' Ensure mcp-guardian proxy is running with DASHBOARD_ENABLED=true\n\n'));
163
+ }
164
+ else {
165
+ switch (state.activeTab) {
166
+ case 0:
167
+ renderOverview(data);
168
+ break;
169
+ case 1:
170
+ renderSecurity(data);
171
+ break;
172
+ case 2:
173
+ renderCost(data);
174
+ break;
175
+ case 3:
176
+ renderHealth(data);
177
+ break;
178
+ case 4:
179
+ renderAi(data);
180
+ break;
181
+ case 5:
182
+ renderAudit(data);
183
+ break;
184
+ case 6:
185
+ renderPolicy(data);
186
+ break;
187
+ case 7:
188
+ renderInstances(data);
189
+ break;
190
+ }
191
+ }
192
+ // ── Footer ──────────────────────────────────────────────────────
193
+ stdout.write('\n' + C.dim('─'.repeat(termWidth - 1)) + '\n');
194
+ stdout.write(C.dim(' 1-8:Switch Tab Tab:Next r:Refresh Esc:Quit Frame: ' + state.frameCount + '\n'));
195
+ }
196
+ // ── PANEL RENDERERS ────────────────────────────────────────────────
197
+ function renderOverview(data) {
198
+ const stdout = process.stdout;
199
+ const o = data.overview;
200
+ // Score gauge
201
+ const score = data.security.overallScore;
202
+ const scoreColor = score >= 70 ? C.green : score >= 40 ? C.yellow : C.red;
203
+ const bar = '█'.repeat(Math.round(score / 5)) + '░'.repeat(20 - Math.round(score / 5));
204
+ stdout.write(C.bold.white(' 📊 EXECUTIVE SUMMARY\n\n'));
205
+ stdout.write(` Overall Security Score: ${scoreColor(`${score}/100`)} ${C.dim(`[${bar}]`)}\n`);
206
+ stdout.write(` Active Instances: ${C.green(o.activeInstances)} / ${C.white(o.totalInstances)}\n`);
207
+ stdout.write(` Total Requests: ${C.white(o.totalRequests.toLocaleString())}\n`);
208
+ stdout.write(` Blocked Requests: ${o.blockedRequests > 0 ? C.red(o.blockedRequests.toLocaleString()) : C.green('0')}\n`);
209
+ stdout.write(` Pass Rate: ${o.passRate > 90 ? C.green(`${o.passRate.toFixed(1)}%`) : C.yellow(`${o.passRate.toFixed(1)}%`)}\n`);
210
+ stdout.write(` Total Cost: ${C.yellow(`$${o.totalCostUsd.toFixed(4)}`)}\n`);
211
+ stdout.write(` Burn Rate: ${C.yellow(`$${o.burnRatePerHour.toFixed(4)}/hr`)}\n`);
212
+ stdout.write(` Avg Latency: ${o.avgLatencyMs < 200 ? C.green(`${o.avgLatencyMs.toFixed(0)}ms`) : C.yellow(`${o.avgLatencyMs.toFixed(0)}ms`)}\n`);
213
+ stdout.write(` Active Servers: ${C.cyan(o.activeServers)}\n`);
214
+ // Top risks
215
+ if (data.security.worstOffenders.length > 0) {
216
+ stdout.write(C.bold.red(`\n ⚠️ TOP RISKS:\n`));
217
+ for (const w of data.security.worstOffenders.slice(0, 5)) {
218
+ stdout.write(C.red(` • ${w}\n`));
219
+ }
220
+ }
221
+ // AI Insights
222
+ if (data.ai.report?.executiveSummary?.topRisks?.length > 0) {
223
+ stdout.write(C.bold.yellow(`\n 🤖 AI INSIGHTS:\n`));
224
+ for (const r of data.ai.report.executiveSummary.recommendations?.slice(0, 3) || []) {
225
+ stdout.write(C.yellow(` • ${r}\n`));
226
+ }
227
+ }
228
+ // Budget alerts
229
+ if (data.cost.budgetAlerts.length > 0) {
230
+ stdout.write(C.bold.red(`\n 💰 BUDGET ALERTS:\n`));
231
+ for (const alert of data.cost.budgetAlerts) {
232
+ stdout.write(C.red(` • ${alert}\n`));
233
+ }
234
+ }
235
+ }
236
+ function renderSecurity(data) {
237
+ const stdout = process.stdout;
238
+ const s = data.security;
239
+ stdout.write(C.bold.white(' 🔒 SECURITY POSTURE\n\n'));
240
+ stdout.write(` Overall Score: ${s.overallScore >= 70 ? C.green(s.overallScore) : C.red(s.overallScore)}/100\n`);
241
+ stdout.write(` Active Threats: ${s.activeThreats > 0 ? C.red(s.activeThreats) : C.green('0')}\n`);
242
+ stdout.write(` Last Scan: ${C.dim(s.lastScan)}\n\n`);
243
+ if (s.servers.length === 0) {
244
+ stdout.write(C.dim(' No security data available — run scan_security to populate.\n'));
245
+ return;
246
+ }
247
+ // Table header
248
+ stdout.write(C.dim(' ┌──────────────────────────┬─────────┬───────┬──────────┬────────┐\n'));
249
+ stdout.write(C.dim(' │ Server │ Score │ CVEs │ Critical │ Auth │\n'));
250
+ stdout.write(C.dim(' ├──────────────────────────┼─────────┼───────┼──────────┼────────┤\n'));
251
+ for (const server of s.servers) {
252
+ const scoreColor = server.score >= 70 ? C.green : server.score >= 40 ? C.yellow : C.red;
253
+ const name = server.name.slice(0, 24).padEnd(24);
254
+ const score = scoreColor(String(server.score).padEnd(7));
255
+ const cves = server.cves > 0 ? C.red(String(server.cves).padEnd(5)) : C.dim(String(server.cves).padEnd(5));
256
+ const critical = server.critical > 0 ? C.bgRed.white(` ${server.critical} `) : C.dim(' 0 ');
257
+ const auth = server.auth ? C.green('✅') : C.red('❌');
258
+ stdout.write(` │ ${C.white(name)}│ ${score}│ ${cves}│ ${critical} │ ${auth} │\n`);
259
+ }
260
+ stdout.write(C.dim(' └──────────────────────────┴─────────┴───────┴──────────┴────────┘\n'));
261
+ }
262
+ function renderCost(data) {
263
+ const stdout = process.stdout;
264
+ const c = data.cost;
265
+ stdout.write(C.bold.white(' 💰 COST ANALYSIS\n\n'));
266
+ stdout.write(` Total Cost: ${C.yellow(`$${c.totalCost.toFixed(4)}`)}\n`);
267
+ stdout.write(` Projected Monthly: ${C.yellow(`$${c.projectedMonthly.toFixed(2)}`)}\n`);
268
+ stdout.write(` Pricing Model: ${C.cyan(c.pricingModel)}\n\n`);
269
+ if (c.servers.length === 0) {
270
+ stdout.write(C.dim(' No cost data available — run audit_costs to populate.\n'));
271
+ return;
272
+ }
273
+ // Table
274
+ stdout.write(C.dim(' ┌──────────────────────────┬────────────┬──────────┬─────────┐\n'));
275
+ stdout.write(C.dim(' │ Server │ Tokens │ Cost USD │ Trend │\n'));
276
+ stdout.write(C.dim(' ├──────────────────────────┼────────────┼──────────┼─────────┤\n'));
277
+ for (const server of c.servers) {
278
+ const name = server.name.slice(0, 24).padEnd(24);
279
+ const tokens = String(server.tokens.toLocaleString()).padEnd(10);
280
+ const cost = `$${server.cost.toFixed(4)}`.padEnd(8);
281
+ const trendIcon = server.trend === 'increasing' ? '📈' : server.trend === 'decreasing' ? '📉' : '➡️';
282
+ stdout.write(` │ ${C.white(name)}│ ${C.cyan(tokens)}│ ${C.yellow(cost)}│ ${trendIcon} │\n`);
283
+ }
284
+ stdout.write(C.dim(' └──────────────────────────┴────────────┴──────────┴─────────┘\n'));
285
+ }
286
+ function renderHealth(data) {
287
+ const stdout = process.stdout;
288
+ const h = data.health;
289
+ stdout.write(C.bold.white(' ❤️ HEALTH STATUS\n\n'));
290
+ stdout.write(` Avg Latency: ${h.avgLatency < 200 ? C.green(`${h.avgLatency}ms`) : C.yellow(`${h.avgLatency}ms`)}\n`);
291
+ stdout.write(` Total Tools: ${C.cyan(h.totalTools)}\n`);
292
+ stdout.write(` At Risk: ${h.atRisk.length > 0 ? C.red(h.atRisk.join(', ')) : C.green('None')}\n\n`);
293
+ if (h.servers.length === 0) {
294
+ stdout.write(C.dim(' No health data available — run check_health to populate.\n'));
295
+ return;
296
+ }
297
+ // Table
298
+ stdout.write(C.dim(' ┌──────────────────────────┬──────────┬──────────┬───────┬───────────┐\n'));
299
+ stdout.write(C.dim(' │ Server │ Latency │ Success │ Tools │ Breaker │\n'));
300
+ stdout.write(C.dim(' ├──────────────────────────┼──────────┼──────────┼───────┼───────────┤\n'));
301
+ for (const server of h.servers) {
302
+ const name = server.name.slice(0, 24).padEnd(24);
303
+ const latency = server.latency < 200 ? C.green(`${server.latency}ms`.padEnd(8)) : C.yellow(`${server.latency}ms`.padEnd(8));
304
+ const success = server.successRate > 90 ? C.green(`${server.successRate.toFixed(0)}%`.padEnd(8)) : C.red(`${server.successRate.toFixed(0)}%`.padEnd(8));
305
+ const tools = String(server.tools).padEnd(5);
306
+ const breaker = server.circuitBreaker === 'open' ? C.red('OPEN') : server.circuitBreaker === 'half_open' ? C.yellow('HALF') : C.green('CLOSED');
307
+ stdout.write(` │ ${C.white(name)}│ ${latency}│ ${success}│ ${C.cyan(tools)}│ ${breaker} │\n`);
308
+ }
309
+ stdout.write(C.dim(' └──────────────────────────┴──────────┴──────────┴───────┴───────────┘\n'));
310
+ }
311
+ function renderAi(data) {
312
+ const stdout = process.stdout;
313
+ const ai = data.ai;
314
+ stdout.write(C.bold.white(' 🤖 ADAPTIVE AI ENGINE\n\n'));
315
+ // Learning state
316
+ const ls = ai.learningState;
317
+ stdout.write(C.bold.cyan(' Self-Improvement State:\n'));
318
+ stdout.write(` Adaptive Threshold: ${C.magenta(ls.adaptiveThreshold.toFixed(2))}\n`);
319
+ stdout.write(` True Positive Rate: ${C.green(`${(ls.truePositiveRate * 100).toFixed(0)}%`)}\n`);
320
+ stdout.write(` False Positive Rate: ${ls.falsePositiveRate > 0.3 ? C.red(`${(ls.falsePositiveRate * 100).toFixed(0)}%`) : C.green(`${(ls.falsePositiveRate * 100).toFixed(0)}%`)}\n`);
321
+ // Module weights
322
+ stdout.write(C.dim(`\n Module Weights:\n`));
323
+ const modules = ls.moduleWeights || {};
324
+ for (const [name, weight] of Object.entries(modules)) {
325
+ const w = typeof weight === 'number' ? weight : parseFloat(weight);
326
+ const bar = '█'.repeat(Math.round(w * 20)) + '░'.repeat(20 - Math.round(w * 20));
327
+ stdout.write(` ${C.cyan(name.padEnd(12))} ${bar} ${w.toFixed(2)}\n`);
328
+ }
329
+ // AI Suggestions
330
+ stdout.write(C.bold.cyan(`\n Active Suggestions (${ai.suggestions.length}):\n`));
331
+ if (ai.suggestions.length === 0) {
332
+ stdout.write(C.dim(' No suggestions yet — AI engine needs data to analyze.\n'));
333
+ }
334
+ else {
335
+ for (const s of ai.suggestions.slice(0, 5)) {
336
+ const conf = s.confidence || 0;
337
+ const confColor = conf >= 0.85 ? C.green : conf >= 0.5 ? C.yellow : C.dim;
338
+ stdout.write(` ${confColor(`${(conf * 100).toFixed(0)}%`)} ${C.white(s.reason || s.id || 'Unknown')}\n`);
339
+ }
340
+ }
341
+ // Threat intel
342
+ stdout.write(C.bold.cyan(`\n Threat Intelligence (${ai.threats.length} active):\n`));
343
+ if (ai.threats.length === 0) {
344
+ stdout.write(C.dim(' No active threats — feeds are clean.\n'));
345
+ }
346
+ else {
347
+ for (const t of ai.threats.slice(0, 5)) {
348
+ const sev = t.severity || t.entry?.severity;
349
+ const sevColor = sev === 'CRITICAL' ? C.bgRed.white : sev === 'HIGH' ? C.red : C.yellow;
350
+ stdout.write(` ${sevColor(` ${sev || 'UNKNOWN'} `)} ${C.white(t.reason || t.entry?.description || 'Unknown threat')}\n`);
351
+ }
352
+ }
353
+ // Baselines
354
+ stdout.write(C.bold.cyan(`\n Behavioral Baselines (${ai.baselines.length}):\n`));
355
+ if (ai.baselines.length === 0) {
356
+ stdout.write(C.dim(' No baselines learned yet.\n'));
357
+ }
358
+ else {
359
+ for (const b of ai.baselines.slice(0, 5)) {
360
+ stdout.write(` ${C.white(b.toolName || b.tool)} on ${C.cyan(b.serverName || b.server)}: ${b.sampleCount} samples, avg ${Math.round(b.avgTokens || b.avg_tokens || 0)} tokens\n`);
361
+ }
362
+ }
363
+ }
364
+ function renderAudit(data) {
365
+ const stdout = process.stdout;
366
+ const a = data.audit;
367
+ stdout.write(C.bold.white(' 📋 AUDIT TRAIL\n\n'));
368
+ stdout.write(` Total Events: ${C.white(a.total.toLocaleString())}\n`);
369
+ stdout.write(` Passed: ${C.green(a.passed.toLocaleString())}\n`);
370
+ stdout.write(` Blocked: ${a.blocked > 0 ? C.red(a.blocked.toLocaleString()) : C.green('0')}\n`);
371
+ stdout.write(` Flagged: ${a.flagged > 0 ? C.yellow(a.flagged.toLocaleString()) : C.green('0')}\n`);
372
+ stdout.write(C.dim(`\n Recent Events:\n`));
373
+ if (a.events.length === 0) {
374
+ stdout.write(C.dim(' No audit events recorded yet.\n'));
375
+ }
376
+ else {
377
+ for (const event of a.events.slice(0, 10)) {
378
+ const time = (event.timestamp || '').slice(11, 19) || '--:--:--';
379
+ const action = event.action === 'block' ? C.red('BLOCK') : event.action === 'flag' ? C.yellow('FLAG') : C.green('PASS');
380
+ const server = (event.server_name || event.serverName || '').slice(0, 20).padEnd(20);
381
+ const tool = (event.tool_name || event.toolName || '').slice(0, 15).padEnd(15);
382
+ const rule = (event.rule_name || event.ruleName || '-').slice(0, 25);
383
+ stdout.write(` ${C.dim(time)} ${action} ${C.cyan(server)} ${C.white(tool)} ${C.dim(rule)}\n`);
384
+ }
385
+ }
386
+ }
387
+ function renderPolicy(data) {
388
+ const stdout = process.stdout;
389
+ const p = data.policy;
390
+ stdout.write(C.bold.white(' 📜 ACTIVE POLICY\n\n'));
391
+ stdout.write(` Mode: ${p.mode === 'block' ? C.red('BLOCK') : p.mode === 'warn' ? C.yellow('WARN') : C.green('AUDIT')}\n`);
392
+ stdout.write(` Active Rules: ${C.cyan(p.activeRules)}\n`);
393
+ stdout.write(` Auto-Generated: ${C.magenta(p.autoGeneratedRules.length)}\n`);
394
+ if (p.autoGeneratedRules.length > 0) {
395
+ stdout.write(C.dim(`\n AI-Generated Rules:\n`));
396
+ for (const rule of p.autoGeneratedRules.slice(0, 10)) {
397
+ stdout.write(` ${C.magenta('✦')} ${rule}\n`);
398
+ }
399
+ }
400
+ if (p.rules.length === 0) {
401
+ stdout.write(C.dim('\n No policy rules loaded. Start proxy with --policy to activate.\n'));
402
+ }
403
+ }
404
+ function renderInstances(data) {
405
+ const stdout = process.stdout;
406
+ const instances = data.instances;
407
+ stdout.write(C.bold.white(' 🖥️ GUARDIAN INSTANCES\n\n'));
408
+ stdout.write(` Total Instances: ${C.white(instances.length)}\n`);
409
+ const active = instances.filter(i => i.status === 'active').length;
410
+ const degraded = instances.filter(i => i.status === 'degraded').length;
411
+ const offline = instances.filter(i => i.status === 'offline').length;
412
+ stdout.write(` Active: ${C.green(active)} Degraded: ${C.yellow(degraded)} Offline: ${C.red(offline)}\n\n`);
413
+ if (instances.length === 0) {
414
+ stdout.write(C.dim(' No instances registered. Start mcp-guardian proxy to register.\n'));
415
+ return;
416
+ }
417
+ // Table
418
+ stdout.write(C.dim(' ┌──────────────────────┬──────────┬───────────┬──────────┬───────────┐\n'));
419
+ stdout.write(C.dim(' │ Instance │ Status │ Requests │ Blocked │ Latency │\n'));
420
+ stdout.write(C.dim(' ├──────────────────────┼──────────┼───────────┼──────────┼───────────┤\n'));
421
+ for (const inst of instances) {
422
+ const name = inst.instanceId.slice(0, 20).padEnd(20);
423
+ const status = inst.status === 'active' ? C.green('ACTIVE') : inst.status === 'degraded' ? C.yellow('DEGRADED') : C.red('OFFLINE');
424
+ const requests = String(inst.totalRequests.toLocaleString()).padEnd(9);
425
+ const blocked = inst.blockedRequests > 0 ? C.red(String(inst.blockedRequests).padEnd(8)) : C.dim('0'.padEnd(8));
426
+ const latency = inst.avgLatencyMs ? `${inst.avgLatencyMs.toFixed(0)}ms`.padEnd(9) : 'N/A'.padEnd(9);
427
+ stdout.write(` │ ${C.white(name)}│ ${status} │ ${C.cyan(requests)}│ ${blocked}│ ${C.dim(latency)}│\n`);
428
+ }
429
+ stdout.write(C.dim(' └──────────────────────┴──────────┴───────────┴──────────┴───────────┘\n'));
430
+ }