@quint-security/cli 0.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.
Files changed (52) hide show
  1. package/dist/commands/auth.d.ts +3 -0
  2. package/dist/commands/auth.d.ts.map +1 -0
  3. package/dist/commands/auth.js +87 -0
  4. package/dist/commands/auth.js.map +1 -0
  5. package/dist/commands/http-proxy.d.ts +3 -0
  6. package/dist/commands/http-proxy.d.ts.map +1 -0
  7. package/dist/commands/http-proxy.js +35 -0
  8. package/dist/commands/http-proxy.js.map +1 -0
  9. package/dist/commands/init.d.ts +3 -0
  10. package/dist/commands/init.d.ts.map +1 -0
  11. package/dist/commands/init.js +343 -0
  12. package/dist/commands/init.js.map +1 -0
  13. package/dist/commands/keys.d.ts +3 -0
  14. package/dist/commands/keys.d.ts.map +1 -0
  15. package/dist/commands/keys.js +58 -0
  16. package/dist/commands/keys.js.map +1 -0
  17. package/dist/commands/logs.d.ts +3 -0
  18. package/dist/commands/logs.d.ts.map +1 -0
  19. package/dist/commands/logs.js +50 -0
  20. package/dist/commands/logs.js.map +1 -0
  21. package/dist/commands/policy.d.ts +3 -0
  22. package/dist/commands/policy.d.ts.map +1 -0
  23. package/dist/commands/policy.js +39 -0
  24. package/dist/commands/policy.js.map +1 -0
  25. package/dist/commands/proxy.d.ts +3 -0
  26. package/dist/commands/proxy.d.ts.map +1 -0
  27. package/dist/commands/proxy.js +24 -0
  28. package/dist/commands/proxy.js.map +1 -0
  29. package/dist/commands/status.d.ts +3 -0
  30. package/dist/commands/status.d.ts.map +1 -0
  31. package/dist/commands/status.js +46 -0
  32. package/dist/commands/status.js.map +1 -0
  33. package/dist/commands/verify.d.ts +3 -0
  34. package/dist/commands/verify.d.ts.map +1 -0
  35. package/dist/commands/verify.js +104 -0
  36. package/dist/commands/verify.js.map +1 -0
  37. package/dist/index.d.ts +3 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +28 -0
  40. package/dist/index.js.map +1 -0
  41. package/package.json +27 -0
  42. package/src/commands/auth.ts +103 -0
  43. package/src/commands/http-proxy.ts +34 -0
  44. package/src/commands/init.ts +408 -0
  45. package/src/commands/keys.ts +71 -0
  46. package/src/commands/logs.ts +59 -0
  47. package/src/commands/policy.ts +39 -0
  48. package/src/commands/proxy.ts +22 -0
  49. package/src/commands/status.ts +51 -0
  50. package/src/commands/verify.ts +117 -0
  51. package/src/index.ts +29 -0
  52. package/tsconfig.json +12 -0
@@ -0,0 +1,408 @@
1
+ import { Command } from "commander";
2
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import {
6
+ ensureKeyPair,
7
+ resolveDataDir,
8
+ initPolicy,
9
+ publicKeyFingerprint,
10
+ type PolicyConfig,
11
+ type ServerPolicy,
12
+ } from "@quint-security/core";
13
+
14
+ // ── Role presets ────────────────────────────────────────────────
15
+
16
+ interface RolePreset {
17
+ name: string;
18
+ description: string;
19
+ defaultAction: "allow" | "deny";
20
+ tools: Array<{ tool: string; action: "allow" | "deny" }>;
21
+ }
22
+
23
+ const ROLE_PRESETS: Record<string, RolePreset> = {
24
+ "coding-assistant": {
25
+ name: "Coding Assistant",
26
+ description: "Read/write project files, run builds and tests. Block destructive ops, shell access outside project, and sensitive file access.",
27
+ defaultAction: "allow",
28
+ tools: [
29
+ { tool: "Delete*", action: "deny" },
30
+ { tool: "Remove*", action: "deny" },
31
+ { tool: "Drop*", action: "deny" },
32
+ { tool: "MechanicRun*", action: "deny" },
33
+ { tool: "TicketingWrite*", action: "deny" },
34
+ ],
35
+ },
36
+ "research-agent": {
37
+ name: "Research Agent",
38
+ description: "Read-only access. Can fetch web pages and search. All write/execute operations denied.",
39
+ defaultAction: "deny",
40
+ tools: [
41
+ { tool: "Read*", action: "allow" },
42
+ { tool: "Get*", action: "allow" },
43
+ { tool: "List*", action: "allow" },
44
+ { tool: "Search*", action: "allow" },
45
+ { tool: "Fetch*", action: "allow" },
46
+ { tool: "ReadInternalWebsites", action: "allow" },
47
+ { tool: "MechanicDiscoverTools", action: "allow" },
48
+ { tool: "MechanicDescribeTool", action: "allow" },
49
+ ],
50
+ },
51
+ "strict": {
52
+ name: "Strict",
53
+ description: "Deny everything by default. Manually allowlist tools as needed.",
54
+ defaultAction: "deny",
55
+ tools: [],
56
+ },
57
+ "permissive": {
58
+ name: "Permissive",
59
+ description: "Allow everything, deny only known-dangerous operations. Good for trusted environments.",
60
+ defaultAction: "allow",
61
+ tools: [
62
+ { tool: "Delete*", action: "deny" },
63
+ { tool: "Remove*", action: "deny" },
64
+ { tool: "Drop*", action: "deny" },
65
+ ],
66
+ },
67
+ };
68
+
69
+ // ── Detect MCP servers from Claude Code config ──────────────────
70
+
71
+ interface ClaudeMcpServer {
72
+ type: "stdio" | "http";
73
+ command?: string;
74
+ args?: string[];
75
+ url?: string;
76
+ env?: Record<string, string>;
77
+ }
78
+
79
+ interface DetectedServer {
80
+ name: string;
81
+ config: ClaudeMcpServer;
82
+ source: "global" | "project";
83
+ alreadyProxied: boolean;
84
+ }
85
+
86
+ function detectMcpServers(): DetectedServer[] {
87
+ const claudeConfigPath = join(homedir(), ".claude.json");
88
+ if (!existsSync(claudeConfigPath)) {
89
+ return [];
90
+ }
91
+
92
+ const raw = readFileSync(claudeConfigPath, "utf-8");
93
+ let config: Record<string, unknown>;
94
+ try {
95
+ config = JSON.parse(raw);
96
+ } catch {
97
+ return [];
98
+ }
99
+
100
+ const servers: DetectedServer[] = [];
101
+ const seen = new Set<string>();
102
+
103
+ // Global MCP servers
104
+ const globalServers = config.mcpServers as Record<string, ClaudeMcpServer> | undefined;
105
+ if (globalServers && typeof globalServers === "object") {
106
+ for (const [name, srv] of Object.entries(globalServers)) {
107
+ if (!seen.has(name)) {
108
+ seen.add(name);
109
+ servers.push({
110
+ name,
111
+ config: srv,
112
+ source: "global",
113
+ alreadyProxied: isAlreadyProxied(srv),
114
+ });
115
+ }
116
+ }
117
+ }
118
+
119
+ // Project-level MCP servers (current working directory)
120
+ const projects = config.projects as Record<string, { mcpServers?: Record<string, ClaudeMcpServer> }> | undefined;
121
+ if (projects) {
122
+ const cwd = process.cwd();
123
+ for (const [projectPath, proj] of Object.entries(projects)) {
124
+ if (cwd.startsWith(projectPath) && proj.mcpServers) {
125
+ for (const [name, srv] of Object.entries(proj.mcpServers)) {
126
+ if (!seen.has(name)) {
127
+ seen.add(name);
128
+ servers.push({
129
+ name,
130
+ config: srv,
131
+ source: "project",
132
+ alreadyProxied: isAlreadyProxied(srv),
133
+ });
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ return servers;
141
+ }
142
+
143
+ function isAlreadyProxied(srv: ClaudeMcpServer): boolean {
144
+ return srv.command === "quint" || srv.command === "node" && (srv.args ?? []).some(a => a.includes("quint"));
145
+ }
146
+
147
+ // ── Generate wrapped config ─────────────────────────────────────
148
+
149
+ function generateWrappedConfig(server: DetectedServer): ClaudeMcpServer | null {
150
+ if (server.alreadyProxied) return null;
151
+
152
+ if (server.config.type === "stdio") {
153
+ return {
154
+ type: "stdio",
155
+ command: "quint",
156
+ args: [
157
+ "proxy",
158
+ "--name", server.name,
159
+ "--",
160
+ server.config.command!,
161
+ ...(server.config.args ?? []),
162
+ ],
163
+ env: server.config.env ?? {},
164
+ };
165
+ }
166
+
167
+ if (server.config.type === "http" && server.config.url) {
168
+ return {
169
+ type: "stdio",
170
+ command: "quint",
171
+ args: [
172
+ "http-proxy",
173
+ "--name", server.name,
174
+ "--target", server.config.url,
175
+ ],
176
+ env: server.config.env ?? {},
177
+ };
178
+ }
179
+
180
+ return null;
181
+ }
182
+
183
+ // ── Apply changes to claude.json ────────────────────────────────
184
+
185
+ function applyToClaudeConfig(servers: DetectedServer[]): { applied: number; path: string } {
186
+ const claudeConfigPath = join(homedir(), ".claude.json");
187
+ const raw = readFileSync(claudeConfigPath, "utf-8");
188
+ const config = JSON.parse(raw);
189
+
190
+ let applied = 0;
191
+
192
+ for (const server of servers) {
193
+ if (server.alreadyProxied) continue;
194
+
195
+ const wrapped = generateWrappedConfig(server);
196
+ if (!wrapped) continue;
197
+
198
+ if (server.source === "global" && config.mcpServers?.[server.name]) {
199
+ config.mcpServers[server.name] = wrapped;
200
+ applied++;
201
+ } else if (server.source === "project") {
202
+ // Find the project entry
203
+ for (const [projectPath, proj] of Object.entries(config.projects as Record<string, { mcpServers?: Record<string, ClaudeMcpServer> }>)) {
204
+ if (proj.mcpServers?.[server.name]) {
205
+ proj.mcpServers[server.name] = wrapped;
206
+ applied++;
207
+ break;
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2) + "\n");
214
+ return { applied, path: claudeConfigPath };
215
+ }
216
+
217
+ // ── The init command ────────────────────────────────────────────
218
+
219
+ export const initCommand = new Command("init")
220
+ .description("Set up Quint: detect MCP servers, generate keys, create policy, and optionally wrap your config")
221
+ .option("--role <role>", "Use a pre-built role preset (coding-assistant, research-agent, strict, permissive)")
222
+ .option("--apply", "Apply changes to ~/.claude.json (wraps MCP servers through Quint)")
223
+ .option("--revert", "Revert all Quint-proxied MCP servers back to direct connections")
224
+ .option("--dry-run", "Show what would change without modifying anything")
225
+ .option("--list-roles", "Show available role presets")
226
+ .action((opts: { role?: string; apply?: boolean; revert?: boolean; dryRun?: boolean; listRoles?: boolean }) => {
227
+
228
+ // ── List roles ──
229
+ if (opts.listRoles) {
230
+ console.log("Available role presets:\n");
231
+ for (const [id, preset] of Object.entries(ROLE_PRESETS)) {
232
+ console.log(` ${id}`);
233
+ console.log(` ${preset.description}`);
234
+ console.log(` Default: ${preset.defaultAction}, ${preset.tools.length} tool rules`);
235
+ console.log("");
236
+ }
237
+ return;
238
+ }
239
+
240
+ // ── Revert ──
241
+ if (opts.revert) {
242
+ const servers = detectMcpServers();
243
+ const proxied = servers.filter(s => s.alreadyProxied);
244
+
245
+ if (proxied.length === 0) {
246
+ console.log("No Quint-proxied servers found. Nothing to revert.");
247
+ return;
248
+ }
249
+
250
+ if (opts.dryRun) {
251
+ console.log("Would revert these servers to direct connections:\n");
252
+ for (const s of proxied) {
253
+ // Extract the original command from quint proxy args
254
+ const args = s.config.args ?? [];
255
+ const dashDashIdx = args.indexOf("--");
256
+ if (dashDashIdx >= 0) {
257
+ const origCmd = args[dashDashIdx + 1];
258
+ const origArgs = args.slice(dashDashIdx + 2);
259
+ console.log(` ${s.name}: quint proxy → ${origCmd} ${origArgs.join(" ")}`);
260
+ }
261
+ }
262
+ return;
263
+ }
264
+
265
+ const claudeConfigPath = join(homedir(), ".claude.json");
266
+ const raw = readFileSync(claudeConfigPath, "utf-8");
267
+ const config = JSON.parse(raw);
268
+
269
+ let reverted = 0;
270
+ for (const s of proxied) {
271
+ const args = s.config.args ?? [];
272
+ const dashDashIdx = args.indexOf("--");
273
+ if (dashDashIdx < 0) continue;
274
+
275
+ const origCmd = args[dashDashIdx + 1];
276
+ const origArgs = args.slice(dashDashIdx + 2);
277
+ const restored = {
278
+ type: "stdio" as const,
279
+ command: origCmd,
280
+ args: origArgs,
281
+ env: s.config.env ?? {},
282
+ };
283
+
284
+ // Restore in the correct location
285
+ if (s.source === "global" && config.mcpServers?.[s.name]) {
286
+ config.mcpServers[s.name] = restored;
287
+ reverted++;
288
+ } else if (s.source === "project" && config.projects) {
289
+ for (const proj of Object.values(config.projects as Record<string, { mcpServers?: Record<string, ClaudeMcpServer> }>)) {
290
+ if (proj.mcpServers?.[s.name]) {
291
+ proj.mcpServers[s.name] = restored;
292
+ reverted++;
293
+ break;
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2) + "\n");
300
+ console.log(`Reverted ${reverted} server(s) to direct connections.`);
301
+ console.log("Restart Claude Code for changes to take effect.");
302
+ return;
303
+ }
304
+
305
+ // ── Normal init flow ──
306
+
307
+ console.log("Quint Setup\n");
308
+
309
+ // Step 1: Detect MCP servers
310
+ const servers = detectMcpServers();
311
+ if (servers.length === 0) {
312
+ console.log(" No MCP servers found in ~/.claude.json");
313
+ console.log(" Quint works by wrapping existing MCP servers.");
314
+ console.log(" Add MCP servers to Claude Code first, then run quint init again.");
315
+ return;
316
+ }
317
+
318
+ console.log(` Found ${servers.length} MCP server(s):\n`);
319
+ for (const s of servers) {
320
+ const status = s.alreadyProxied ? " (already proxied)" : "";
321
+ const type = s.config.type === "http" ? ` [HTTP: ${s.config.url}]` : ` [stdio: ${s.config.command}]`;
322
+ console.log(` ${s.name}${type} (${s.source})${status}`);
323
+ }
324
+
325
+ const toWrap = servers.filter(s => !s.alreadyProxied);
326
+ if (toWrap.length === 0) {
327
+ console.log("\n All servers are already proxied through Quint.");
328
+ }
329
+
330
+ // Step 2: Generate keys
331
+ console.log("");
332
+ const dataDir = resolveDataDir("~/.quint");
333
+ const kp = ensureKeyPair(dataDir);
334
+ console.log(` Keys: ${publicKeyFingerprint(kp.publicKey)} (ready)`);
335
+
336
+ // Step 3: Generate policy
337
+ const role = opts.role ? ROLE_PRESETS[opts.role] : undefined;
338
+ if (opts.role && !role) {
339
+ console.error(`\n Unknown role: ${opts.role}`);
340
+ console.error(` Available: ${Object.keys(ROLE_PRESETS).join(", ")}`);
341
+ process.exit(1);
342
+ }
343
+
344
+ const serverPolicies: ServerPolicy[] = [];
345
+
346
+ for (const s of servers) {
347
+ if (role) {
348
+ serverPolicies.push({
349
+ server: s.name,
350
+ default_action: role.defaultAction,
351
+ tools: [...role.tools],
352
+ });
353
+ } else {
354
+ serverPolicies.push({
355
+ server: s.name,
356
+ default_action: "allow",
357
+ tools: [],
358
+ });
359
+ }
360
+ }
361
+
362
+ // Always add wildcard fallback
363
+ serverPolicies.push({ server: "*", default_action: "allow", tools: [] });
364
+
365
+ const policy: PolicyConfig = {
366
+ version: 1,
367
+ data_dir: "~/.quint",
368
+ log_level: "info",
369
+ servers: serverPolicies,
370
+ };
371
+
372
+ const policyPath = join(dataDir, "policy.json");
373
+ if (!existsSync(policyPath)) {
374
+ writeFileSync(policyPath, JSON.stringify(policy, null, 2) + "\n");
375
+ console.log(` Policy: ${policyPath} (created${role ? `, role: ${role.name}` : ""})`);
376
+ } else {
377
+ console.log(` Policy: ${policyPath} (exists, not overwritten)`);
378
+ }
379
+
380
+ // Step 4: Show or apply config changes
381
+ if (toWrap.length > 0) {
382
+ console.log(`\n Config changes needed for ${toWrap.length} server(s):\n`);
383
+
384
+ for (const s of toWrap) {
385
+ const wrapped = generateWrappedConfig(s);
386
+ if (!wrapped) continue;
387
+
388
+ console.log(` ${s.name}:`);
389
+ console.log(` before: ${JSON.stringify({ command: s.config.command, args: s.config.args ?? [] })}`);
390
+ console.log(` after: ${JSON.stringify({ command: wrapped.command, args: wrapped.args })}`);
391
+ console.log("");
392
+ }
393
+
394
+ if (opts.apply && !opts.dryRun) {
395
+ const result = applyToClaudeConfig(toWrap);
396
+ console.log(` Applied ${result.applied} change(s) to ${result.path}`);
397
+ console.log(" Restart Claude Code for changes to take effect.");
398
+ } else if (opts.dryRun) {
399
+ console.log(" (dry run — no changes made)");
400
+ } else {
401
+ console.log(" Run with --apply to modify ~/.claude.json automatically.");
402
+ console.log(" Run with --dry-run to preview without changes.");
403
+ console.log(" Run with --revert to undo Quint proxying.");
404
+ }
405
+ }
406
+
407
+ console.log("\n Setup complete. Run `quint status` to verify.");
408
+ });
@@ -0,0 +1,71 @@
1
+ import { Command } from "commander";
2
+ import {
3
+ loadPolicy,
4
+ resolveDataDir,
5
+ generateKeyPair,
6
+ saveKeyPair,
7
+ loadKeyPair,
8
+ publicKeyFingerprint,
9
+ } from "@quint-security/core";
10
+ import { existsSync } from "node:fs";
11
+ import { join } from "node:path";
12
+
13
+ export const keysCommand = new Command("keys")
14
+ .description("Manage Ed25519 signing keys");
15
+
16
+ keysCommand
17
+ .command("generate")
18
+ .description("Generate a new Ed25519 keypair")
19
+ .option("--force", "Overwrite existing keys")
20
+ .action((opts: { force?: boolean }) => {
21
+ const policy = loadPolicy();
22
+ const dataDir = resolveDataDir(policy.data_dir);
23
+ const privPath = join(dataDir, "keys", "quint.key");
24
+
25
+ if (existsSync(privPath) && !opts.force) {
26
+ console.log("Keys already exist. Use --force to overwrite.");
27
+ return;
28
+ }
29
+
30
+ const kp = generateKeyPair();
31
+ saveKeyPair(dataDir, kp);
32
+ const fp = publicKeyFingerprint(kp.publicKey);
33
+ console.log(`Ed25519 keypair generated.`);
34
+ console.log(` Private key: ${join(dataDir, "keys", "quint.key")} (mode 0600)`);
35
+ console.log(` Public key: ${join(dataDir, "keys", "quint.pub")}`);
36
+ console.log(` Fingerprint: ${fp}`);
37
+ });
38
+
39
+ keysCommand
40
+ .command("show")
41
+ .description("Show current public key")
42
+ .action(() => {
43
+ const policy = loadPolicy();
44
+ const dataDir = resolveDataDir(policy.data_dir);
45
+ const kp = loadKeyPair(dataDir);
46
+
47
+ if (!kp) {
48
+ console.log("No keys found. Run `quint keys generate` first.");
49
+ return;
50
+ }
51
+
52
+ const fp = publicKeyFingerprint(kp.publicKey);
53
+ console.log(`Public key fingerprint: ${fp}`);
54
+ console.log(`\n${kp.publicKey.trim()}`);
55
+ });
56
+
57
+ keysCommand
58
+ .command("export")
59
+ .description("Export public key to stdout")
60
+ .action(() => {
61
+ const policy = loadPolicy();
62
+ const dataDir = resolveDataDir(policy.data_dir);
63
+ const kp = loadKeyPair(dataDir);
64
+
65
+ if (!kp) {
66
+ console.error("No keys found. Run `quint keys generate` first.");
67
+ process.exit(1);
68
+ }
69
+
70
+ process.stdout.write(kp.publicKey);
71
+ });
@@ -0,0 +1,59 @@
1
+ import { Command } from "commander";
2
+ import { loadPolicy, resolveDataDir, openAuditDb, type AuditEntry } from "@quint-security/core";
3
+
4
+ export const logsCommand = new Command("logs")
5
+ .description("Search and display the audit log")
6
+ .option("--server <name>", "Filter by server name")
7
+ .option("--tool <name>", "Filter by tool name")
8
+ .option("--denied", "Show only denied entries")
9
+ .option("--since <iso-date>", "Show entries since ISO-8601 date")
10
+ .option("-n, --limit <count>", "Max entries to show", "50")
11
+ .option("--json", "Output as JSON")
12
+ .action((opts: {
13
+ server?: string;
14
+ tool?: string;
15
+ denied?: boolean;
16
+ since?: string;
17
+ limit: string;
18
+ json?: boolean;
19
+ }) => {
20
+ const policy = loadPolicy();
21
+ const dataDir = resolveDataDir(policy.data_dir);
22
+ const db = openAuditDb(dataDir);
23
+
24
+ try {
25
+ const entries = db.query({
26
+ server: opts.server,
27
+ tool: opts.tool,
28
+ verdict: opts.denied ? "deny" : undefined,
29
+ since: opts.since,
30
+ limit: parseInt(opts.limit, 10),
31
+ });
32
+
33
+ if (opts.json) {
34
+ console.log(JSON.stringify(entries, null, 2));
35
+ return;
36
+ }
37
+
38
+ if (entries.length === 0) {
39
+ console.log("No audit log entries found.");
40
+ return;
41
+ }
42
+
43
+ console.log(`Showing ${entries.length} entries (newest first):\n`);
44
+
45
+ for (const entry of entries) {
46
+ printEntry(entry);
47
+ }
48
+ } finally {
49
+ db.close();
50
+ }
51
+ });
52
+
53
+ function printEntry(e: AuditEntry): void {
54
+ const icon = e.verdict === "deny" ? "✗" : e.verdict === "allow" ? "✓" : "→";
55
+ const ts = e.timestamp.replace("T", " ").replace(/\.\d+Z$/, "Z");
56
+ const tool = e.tool_name ? ` tool=${e.tool_name}` : "";
57
+ const risk = e.risk_score != null ? ` risk=${e.risk_score}(${e.risk_level})` : "";
58
+ console.log(` ${icon} [${ts}] ${e.direction} ${e.method}${tool} server=${e.server_name} verdict=${e.verdict}${risk} id=${e.id}`);
59
+ }
@@ -0,0 +1,39 @@
1
+ import { Command } from "commander";
2
+ import { loadPolicy, resolveDataDir, initPolicy, validatePolicy } from "@quint-security/core";
3
+
4
+ export const policyCommand = new Command("policy")
5
+ .description("Manage access control policy");
6
+
7
+ policyCommand
8
+ .command("init")
9
+ .description("Create a default policy.json if none exists")
10
+ .action(() => {
11
+ const path = initPolicy();
12
+ console.log(`Policy file: ${path}`);
13
+ });
14
+
15
+ policyCommand
16
+ .command("validate")
17
+ .description("Validate the current policy.json")
18
+ .action(() => {
19
+ const policy = loadPolicy();
20
+ const errors = validatePolicy(policy);
21
+
22
+ if (errors.length === 0) {
23
+ console.log("Policy is valid.");
24
+ } else {
25
+ console.log("Policy validation errors:");
26
+ for (const err of errors) {
27
+ console.log(` - ${err}`);
28
+ }
29
+ process.exit(1);
30
+ }
31
+ });
32
+
33
+ policyCommand
34
+ .command("show")
35
+ .description("Display the current policy")
36
+ .action(() => {
37
+ const policy = loadPolicy();
38
+ console.log(JSON.stringify(policy, null, 2));
39
+ });
@@ -0,0 +1,22 @@
1
+ import { Command } from "commander";
2
+ import { loadPolicy, resolveDataDir } from "@quint-security/core";
3
+ import { startProxy } from "@quint-security/proxy";
4
+
5
+ export const proxyCommand = new Command("proxy")
6
+ .description("Run as MCP stdio proxy wrapping another MCP server")
7
+ .requiredOption("--name <name>", "Name identifier for the proxied server")
8
+ .option("--policy <path>", "Path to policy.json (default: ~/.quint/policy.json)")
9
+ .argument("<command>", "Command to spawn the real MCP server")
10
+ .argument("[args...]", "Arguments for the real MCP server")
11
+ .allowExcessArguments(true)
12
+ .action((command: string, args: string[], opts: { name: string; policy?: string }) => {
13
+ const policy = loadPolicy(opts.policy ? opts.policy : undefined);
14
+ const dataDir = resolveDataDir(policy.data_dir);
15
+
16
+ startProxy({
17
+ serverName: opts.name,
18
+ command,
19
+ args,
20
+ policy,
21
+ });
22
+ });
@@ -0,0 +1,51 @@
1
+ import { Command } from "commander";
2
+ import {
3
+ loadPolicy,
4
+ resolveDataDir,
5
+ loadKeyPair,
6
+ publicKeyFingerprint,
7
+ openAuditDb,
8
+ } from "@quint-security/core";
9
+ import { existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ export const statusCommand = new Command("status")
13
+ .description("Show Quint configuration summary")
14
+ .action(() => {
15
+ const policy = loadPolicy();
16
+ const dataDir = resolveDataDir(policy.data_dir);
17
+
18
+ console.log("Quint Status");
19
+ console.log("============");
20
+ console.log(` Data dir: ${dataDir}`);
21
+ console.log(` Policy: ${join(dataDir, "policy.json")} ${existsSync(join(dataDir, "policy.json")) ? "(found)" : "(not found)"}`);
22
+ console.log(` Log level: ${policy.log_level}`);
23
+
24
+ // Keys
25
+ const kp = loadKeyPair(dataDir);
26
+ if (kp) {
27
+ console.log(` Keys: ${publicKeyFingerprint(kp.publicKey)} (loaded)`);
28
+ } else {
29
+ console.log(` Keys: not generated (run \`quint keys generate\`)`);
30
+ }
31
+
32
+ // Database
33
+ const dbPath = join(dataDir, "quint.db");
34
+ if (existsSync(dbPath)) {
35
+ const db = openAuditDb(dataDir);
36
+ const count = db.count();
37
+ db.close();
38
+ console.log(` Database: ${dbPath} (${count} entries)`);
39
+ } else {
40
+ console.log(` Database: not created yet`);
41
+ }
42
+
43
+ // Servers
44
+ console.log(`\n Servers (${policy.servers.length}):`);
45
+ for (const srv of policy.servers) {
46
+ const toolRules = srv.tools.length > 0
47
+ ? srv.tools.map(t => `${t.tool}:${t.action}`).join(", ")
48
+ : "no tool-specific rules";
49
+ console.log(` ${srv.server} → default:${srv.default_action} [${toolRules}]`);
50
+ }
51
+ });