bashbros 0.1.0

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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +453 -0
  3. package/dist/audit-MCFNGOIM.js +11 -0
  4. package/dist/audit-MCFNGOIM.js.map +1 -0
  5. package/dist/chunk-43W3RVEL.js +2910 -0
  6. package/dist/chunk-43W3RVEL.js.map +1 -0
  7. package/dist/chunk-4R4GV5V2.js +213 -0
  8. package/dist/chunk-4R4GV5V2.js.map +1 -0
  9. package/dist/chunk-7OCVIDC7.js +12 -0
  10. package/dist/chunk-7OCVIDC7.js.map +1 -0
  11. package/dist/chunk-CSRPOGHY.js +354 -0
  12. package/dist/chunk-CSRPOGHY.js.map +1 -0
  13. package/dist/chunk-DEAF6PYM.js +212 -0
  14. package/dist/chunk-DEAF6PYM.js.map +1 -0
  15. package/dist/chunk-DLP2O6PN.js +273 -0
  16. package/dist/chunk-DLP2O6PN.js.map +1 -0
  17. package/dist/chunk-GD5VNHIN.js +519 -0
  18. package/dist/chunk-GD5VNHIN.js.map +1 -0
  19. package/dist/chunk-ID2O2QTI.js +269 -0
  20. package/dist/chunk-ID2O2QTI.js.map +1 -0
  21. package/dist/chunk-J37RHCFJ.js +357 -0
  22. package/dist/chunk-J37RHCFJ.js.map +1 -0
  23. package/dist/chunk-SB4JS3GU.js +456 -0
  24. package/dist/chunk-SB4JS3GU.js.map +1 -0
  25. package/dist/chunk-SG752FZC.js +200 -0
  26. package/dist/chunk-SG752FZC.js.map +1 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +2448 -0
  29. package/dist/cli.js.map +1 -0
  30. package/dist/config-CZMIGNPF.js +13 -0
  31. package/dist/config-CZMIGNPF.js.map +1 -0
  32. package/dist/config-parser-XHE7BC7H.js +13 -0
  33. package/dist/config-parser-XHE7BC7H.js.map +1 -0
  34. package/dist/db-EHQDB5OL.js +11 -0
  35. package/dist/db-EHQDB5OL.js.map +1 -0
  36. package/dist/display-IN4NRJJS.js +18 -0
  37. package/dist/display-IN4NRJJS.js.map +1 -0
  38. package/dist/engine-PKLXW6OF.js +9 -0
  39. package/dist/engine-PKLXW6OF.js.map +1 -0
  40. package/dist/index.d.ts +1498 -0
  41. package/dist/index.js +552 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/moltbot-DXZFVK3X.js +11 -0
  44. package/dist/moltbot-DXZFVK3X.js.map +1 -0
  45. package/dist/ollama-HY35OHW4.js +9 -0
  46. package/dist/ollama-HY35OHW4.js.map +1 -0
  47. package/dist/risk-scorer-Y6KF2XCZ.js +9 -0
  48. package/dist/risk-scorer-Y6KF2XCZ.js.map +1 -0
  49. package/dist/static/index.html +410 -0
  50. package/package.json +68 -0
package/dist/cli.js ADDED
@@ -0,0 +1,2448 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ formatAllAgentsInfo,
4
+ formatPermissionsTable
5
+ } from "./chunk-4R4GV5V2.js";
6
+ import {
7
+ BashBro,
8
+ BashBros,
9
+ ClaudeCodeHooks,
10
+ CostEstimator,
11
+ LoopDetector,
12
+ MetricsCollector,
13
+ ReportGenerator,
14
+ UndoStack,
15
+ gateCommand,
16
+ getBashgymIntegration
17
+ } from "./chunk-43W3RVEL.js";
18
+ import "./chunk-SG752FZC.js";
19
+ import "./chunk-DLP2O6PN.js";
20
+ import {
21
+ findConfig,
22
+ getDefaultConfig,
23
+ loadConfig
24
+ } from "./chunk-SB4JS3GU.js";
25
+ import {
26
+ allowForSession
27
+ } from "./chunk-GD5VNHIN.js";
28
+ import {
29
+ RiskScorer
30
+ } from "./chunk-DEAF6PYM.js";
31
+ import {
32
+ formatRedactedConfig,
33
+ parseAgentConfig
34
+ } from "./chunk-ID2O2QTI.js";
35
+ import {
36
+ MoltbotHooks
37
+ } from "./chunk-J37RHCFJ.js";
38
+ import {
39
+ DashboardDB
40
+ } from "./chunk-CSRPOGHY.js";
41
+ import "./chunk-7OCVIDC7.js";
42
+
43
+ // src/cli.ts
44
+ import { Command } from "commander";
45
+ import chalk5 from "chalk";
46
+
47
+ // src/onboarding.ts
48
+ import inquirer from "inquirer";
49
+ import chalk from "chalk";
50
+ import { writeFileSync, existsSync, mkdirSync } from "fs";
51
+ import { join } from "path";
52
+ import { homedir } from "os";
53
+ import { stringify } from "yaml";
54
+ async function runOnboarding() {
55
+ console.log(chalk.dim(` "I watch your agent's back so you don't have to."
56
+ `));
57
+ const bashgymAvailable = existsSync(join(homedir(), ".bashgym", "integration"));
58
+ const questions = [
59
+ {
60
+ type: "list",
61
+ name: "agent",
62
+ message: "What agent are you protecting?",
63
+ choices: [
64
+ { name: "Claude Code", value: "claude-code" },
65
+ { name: "Moltbot (clawd.bot)", value: "moltbot" },
66
+ { name: "Clawdbot (legacy)", value: "clawdbot" },
67
+ { name: "Gemini CLI", value: "gemini-cli" },
68
+ { name: "Aider", value: "aider" },
69
+ { name: "OpenCode", value: "opencode" },
70
+ { name: "Other (custom)", value: "custom" }
71
+ ]
72
+ },
73
+ {
74
+ type: "list",
75
+ name: "projectType",
76
+ message: "What's this project about? (helps tune defaults)",
77
+ choices: [
78
+ { name: "Web development", value: "web" },
79
+ { name: "DevOps / Infrastructure", value: "devops" },
80
+ { name: "Data engineering", value: "data" },
81
+ { name: "General coding", value: "general" },
82
+ { name: "Sensitive/regulated work", value: "sensitive" }
83
+ ]
84
+ },
85
+ {
86
+ type: "list",
87
+ name: "profile",
88
+ message: "Security posture:",
89
+ choices: [
90
+ {
91
+ name: "Balanced (recommended) - Block dangerous, allow common dev tools",
92
+ value: "balanced"
93
+ },
94
+ {
95
+ name: "Strict - Allowlist only, explicit approval for new commands",
96
+ value: "strict"
97
+ },
98
+ {
99
+ name: "Permissive - Log everything, block only critical threats",
100
+ value: "permissive"
101
+ },
102
+ {
103
+ name: "Custom - I'll configure manually",
104
+ value: "custom"
105
+ }
106
+ ]
107
+ },
108
+ {
109
+ type: "list",
110
+ name: "secrets",
111
+ message: "Protect secrets? (scans for .env, credentials, SSH keys)",
112
+ choices: [
113
+ { name: "Yes, block access and warn (recommended)", value: "block" },
114
+ { name: "Yes, but allow read with audit log", value: "audit" },
115
+ { name: "No", value: "disabled" }
116
+ ]
117
+ },
118
+ {
119
+ type: "list",
120
+ name: "audit",
121
+ message: "Enable audit logging?",
122
+ choices: [
123
+ { name: "Local file (~/.bashbros/audit.log)", value: "local" },
124
+ { name: "Send to remote (Datadog, Splunk, webhook)", value: "remote" },
125
+ { name: "Both", value: "both" },
126
+ { name: "None", value: "disabled" }
127
+ ]
128
+ }
129
+ ];
130
+ if (bashgymAvailable) {
131
+ questions.push({
132
+ type: "list",
133
+ name: "bashgym",
134
+ message: "Link to BashGym? (enables self-improving AI sidekick)",
135
+ choices: [
136
+ {
137
+ name: "Yes (recommended) - Export traces for training, get smarter sidekick",
138
+ value: "link"
139
+ },
140
+ {
141
+ name: "No - Use bashbros standalone",
142
+ value: "skip"
143
+ }
144
+ ]
145
+ });
146
+ }
147
+ const answers = await inquirer.prompt(questions);
148
+ const config = buildConfig(answers);
149
+ const configYaml = stringify(config);
150
+ writeFileSync(".bashbros.yml", configYaml);
151
+ console.log();
152
+ console.log(chalk.green("\u2713"), "Config written to", chalk.cyan(".bashbros.yml"));
153
+ console.log(chalk.green("\u2713"), "PTY wrapper ready");
154
+ console.log(chalk.green("\u2713"), "Audit logging", answers.audit !== "disabled" ? "enabled" : "disabled");
155
+ if (answers.bashgym === "link") {
156
+ const linked = await linkBashgym();
157
+ if (linked) {
158
+ console.log(chalk.green("\u2713"), "BashGym integration", chalk.cyan("linked"));
159
+ console.log(chalk.dim(" Traces will be exported for training"));
160
+ console.log(chalk.dim(" AI sidekick will improve over time"));
161
+ } else {
162
+ console.log(chalk.yellow("\u26A0"), "BashGym integration", chalk.dim("not linked (bashgym not running?)"));
163
+ }
164
+ } else if (bashgymAvailable) {
165
+ console.log(chalk.dim("\u25CB"), "BashGym integration", chalk.dim("skipped"));
166
+ }
167
+ console.log();
168
+ console.log(chalk.dim("Run"), chalk.cyan("'bashbros doctor'"), chalk.dim("to verify setup"));
169
+ console.log(chalk.dim("Run"), chalk.cyan("'bashbros watch'"), chalk.dim("to start protection"));
170
+ console.log();
171
+ }
172
+ async function linkBashgym() {
173
+ try {
174
+ const integration = getBashgymIntegration();
175
+ if (!integration.isAvailable()) {
176
+ const integrationDir = join(homedir(), ".bashgym", "integration");
177
+ const dirs = [
178
+ join(integrationDir, "traces", "pending"),
179
+ join(integrationDir, "traces", "processed"),
180
+ join(integrationDir, "traces", "failed"),
181
+ join(integrationDir, "models", "latest"),
182
+ join(integrationDir, "config"),
183
+ join(integrationDir, "status")
184
+ ];
185
+ for (const dir of dirs) {
186
+ mkdirSync(dir, { recursive: true });
187
+ }
188
+ const settingsPath = join(integrationDir, "config", "settings.json");
189
+ const settings = {
190
+ version: "1.0",
191
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
192
+ updated_by: "bashbros",
193
+ integration: {
194
+ enabled: true,
195
+ linked_at: (/* @__PURE__ */ new Date()).toISOString()
196
+ },
197
+ capture: {
198
+ mode: "successful_only",
199
+ auto_stream: true
200
+ },
201
+ training: {
202
+ auto_enabled: false,
203
+ quality_threshold: 50,
204
+ trigger: "quality_based"
205
+ },
206
+ security: {
207
+ bashbros_primary: true,
208
+ policy_path: null
209
+ },
210
+ model_sync: {
211
+ auto_export_ollama: true,
212
+ ollama_model_name: "bashgym-sidekick",
213
+ notify_on_update: true
214
+ }
215
+ };
216
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
217
+ } else {
218
+ integration.updateSettings({
219
+ integration: {
220
+ enabled: true,
221
+ linked_at: (/* @__PURE__ */ new Date()).toISOString()
222
+ }
223
+ });
224
+ }
225
+ return true;
226
+ } catch (error) {
227
+ console.error("Failed to link bashgym:", error);
228
+ return false;
229
+ }
230
+ }
231
+ function buildConfig(answers) {
232
+ const defaults = getDefaultConfig();
233
+ const config = {
234
+ ...defaults,
235
+ agent: answers.agent,
236
+ profile: answers.profile,
237
+ secrets: {
238
+ ...defaults.secrets,
239
+ enabled: answers.secrets !== "disabled",
240
+ mode: answers.secrets === "audit" ? "audit" : "block"
241
+ },
242
+ audit: {
243
+ ...defaults.audit,
244
+ enabled: answers.audit !== "disabled",
245
+ destination: answers.audit === "disabled" ? "local" : answers.audit
246
+ }
247
+ };
248
+ if (answers.projectType === "sensitive") {
249
+ config.profile = "strict";
250
+ config.rateLimit.maxPerMinute = 50;
251
+ }
252
+ if (answers.projectType === "devops") {
253
+ config.commands.allow.push("docker *", "kubectl *", "terraform *", "aws *");
254
+ }
255
+ if (answers.projectType === "data") {
256
+ config.commands.allow.push("python *", "jupyter *", "pandas *", "psql *");
257
+ }
258
+ return config;
259
+ }
260
+
261
+ // src/doctor.ts
262
+ import chalk2 from "chalk";
263
+ import { existsSync as existsSync4 } from "fs";
264
+ import { join as join3 } from "path";
265
+ import { homedir as homedir3 } from "os";
266
+
267
+ // src/transparency/agent-config.ts
268
+ import { existsSync as existsSync2, statSync } from "fs";
269
+ import { join as join2 } from "path";
270
+ import { homedir as homedir2, platform } from "os";
271
+ import { execFileSync } from "child_process";
272
+ var AGENT_CONFIG_PATHS = {
273
+ "claude-code": [
274
+ join2(homedir2(), ".claude", "settings.json")
275
+ ],
276
+ "clawdbot": [
277
+ join2(homedir2(), ".clawdbot", "moltbot.json"),
278
+ // New primary (moltbot format)
279
+ join2(homedir2(), ".clawdbot", "config.yml"),
280
+ // Legacy
281
+ join2(homedir2(), ".config", "clawdbot", "config.yml")
282
+ ],
283
+ "moltbot": [
284
+ join2(homedir2(), ".moltbot", "config.json"),
285
+ join2(homedir2(), ".clawdbot", "moltbot.json"),
286
+ // Common location
287
+ join2(homedir2(), ".config", "moltbot", "config.json")
288
+ ],
289
+ "aider": [
290
+ join2(homedir2(), ".aider.conf.yml"),
291
+ join2(homedir2(), ".config", "aider", "aider.conf.yml")
292
+ ],
293
+ "gemini-cli": [
294
+ join2(homedir2(), ".config", "gemini-cli", "config.json")
295
+ ],
296
+ "opencode": [
297
+ join2(homedir2(), ".opencode", "config.yml"),
298
+ join2(homedir2(), ".config", "opencode", "config.yml")
299
+ ],
300
+ "custom": []
301
+ };
302
+ var AGENT_COMMANDS = {
303
+ "claude-code": "claude",
304
+ "clawdbot": "clawdbot",
305
+ "moltbot": "moltbot",
306
+ "aider": "aider",
307
+ "gemini-cli": "gemini",
308
+ "opencode": "opencode",
309
+ "custom": ""
310
+ };
311
+ function commandExists(cmd) {
312
+ if (!cmd) return false;
313
+ try {
314
+ const whichCmd = platform() === "win32" ? "where" : "which";
315
+ execFileSync(whichCmd, [cmd], {
316
+ stdio: "pipe",
317
+ timeout: 3e3,
318
+ windowsHide: true
319
+ });
320
+ return true;
321
+ } catch {
322
+ return false;
323
+ }
324
+ }
325
+ function getAgentVersion(agent) {
326
+ const cmd = AGENT_COMMANDS[agent];
327
+ if (!cmd) return void 0;
328
+ try {
329
+ const output = execFileSync(cmd, ["--version"], {
330
+ encoding: "utf-8",
331
+ timeout: 5e3,
332
+ stdio: ["pipe", "pipe", "pipe"],
333
+ windowsHide: true
334
+ }).trim();
335
+ const match = output.match(/(\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)/i);
336
+ return match ? match[1] : output.split("\n")[0].slice(0, 50);
337
+ } catch {
338
+ return void 0;
339
+ }
340
+ }
341
+ function findAgentConfigPath(agent) {
342
+ const paths = AGENT_CONFIG_PATHS[agent];
343
+ for (const configPath of paths) {
344
+ if (existsSync2(configPath)) {
345
+ return configPath;
346
+ }
347
+ }
348
+ return void 0;
349
+ }
350
+ function getLastModified(filePath) {
351
+ try {
352
+ const stats = statSync(filePath);
353
+ return stats.mtime;
354
+ } catch {
355
+ return void 0;
356
+ }
357
+ }
358
+ async function isBashbrosIntegrated(agent) {
359
+ if (agent === "claude-code") {
360
+ const status = ClaudeCodeHooks.getStatus();
361
+ return status.hooksInstalled;
362
+ }
363
+ if (agent === "moltbot" || agent === "clawdbot") {
364
+ try {
365
+ const { MoltbotHooks: MoltbotHooks2 } = await import("./moltbot-DXZFVK3X.js");
366
+ return MoltbotHooks2.isInstalled();
367
+ } catch {
368
+ return false;
369
+ }
370
+ }
371
+ return false;
372
+ }
373
+ async function getAgentConfigInfo(agent) {
374
+ const cmd = AGENT_COMMANDS[agent];
375
+ const installed = cmd ? commandExists(cmd) : false;
376
+ const configPath = findAgentConfigPath(agent);
377
+ const configExists = configPath ? existsSync2(configPath) : false;
378
+ const info = {
379
+ agent,
380
+ installed,
381
+ configPath,
382
+ configExists,
383
+ bashbrosIntegrated: await isBashbrosIntegrated(agent)
384
+ };
385
+ if (installed) {
386
+ info.version = getAgentVersion(agent);
387
+ }
388
+ if (configExists && configPath) {
389
+ info.lastModified = getLastModified(configPath);
390
+ const parsed = await parseAgentConfig(agent, configPath);
391
+ if (parsed) {
392
+ info.permissions = parsed.permissions;
393
+ info.hooks = parsed.hooks;
394
+ }
395
+ }
396
+ return info;
397
+ }
398
+ async function getAllAgentConfigs() {
399
+ const agents = ["claude-code", "moltbot", "clawdbot", "aider", "gemini-cli", "opencode"];
400
+ const results = [];
401
+ for (const agent of agents) {
402
+ const info = await getAgentConfigInfo(agent);
403
+ results.push(info);
404
+ }
405
+ return results;
406
+ }
407
+
408
+ // src/policy/ward/exposure.ts
409
+ import { exec } from "child_process";
410
+ import { promisify } from "util";
411
+ import { existsSync as existsSync3, readFileSync } from "fs";
412
+ import { platform as platform2 } from "os";
413
+ var execAsync = promisify(exec);
414
+ var DEFAULT_AGENT_SIGNATURES = [
415
+ {
416
+ name: "claude-code",
417
+ processNames: ["claude", "claude-code", "claude_code"],
418
+ defaultPorts: [3e3, 3001, 8080],
419
+ configPaths: [
420
+ "~/.claude/config.json",
421
+ "~/.config/claude/config.json"
422
+ ],
423
+ authIndicators: ["api_key", "auth_token", "authorization"]
424
+ },
425
+ {
426
+ name: "aider",
427
+ processNames: ["aider", "aider-chat"],
428
+ defaultPorts: [8501, 8e3],
429
+ configPaths: [
430
+ "~/.aider.conf.yml",
431
+ ".aider.conf.yml"
432
+ ],
433
+ authIndicators: ["api_key", "openai_api_key"]
434
+ },
435
+ {
436
+ name: "continue",
437
+ processNames: ["continue", "continue-server"],
438
+ defaultPorts: [65432, 65433],
439
+ configPaths: [
440
+ "~/.continue/config.json",
441
+ ".continue/config.json"
442
+ ],
443
+ authIndicators: ["apiKey", "auth"]
444
+ },
445
+ {
446
+ name: "cursor",
447
+ processNames: ["cursor", "Cursor", "cursor-server"],
448
+ defaultPorts: [3e3, 8080, 9e3],
449
+ configPaths: [
450
+ "~/.cursor/config.json",
451
+ "%APPDATA%/Cursor/config.json"
452
+ ],
453
+ authIndicators: ["apiKey", "token", "auth"]
454
+ }
455
+ ];
456
+ var DEFAULT_SEVERITY_ACTIONS = {
457
+ low: "alert",
458
+ medium: "alert",
459
+ high: "block",
460
+ critical: "block_and_kill"
461
+ };
462
+ var ExposureScanner = class {
463
+ config;
464
+ agents;
465
+ constructor(config) {
466
+ this.agents = [...DEFAULT_AGENT_SIGNATURES];
467
+ this.config = {
468
+ enabled: true,
469
+ scanInterval: 6e4,
470
+ externalProbe: false,
471
+ severityActions: { ...DEFAULT_SEVERITY_ACTIONS },
472
+ agents: this.agents,
473
+ ...config
474
+ };
475
+ if (config?.agents) {
476
+ for (const agent of config.agents) {
477
+ if (!this.agents.find((a) => a.name === agent.name)) {
478
+ this.agents.push(agent);
479
+ }
480
+ }
481
+ }
482
+ }
483
+ /**
484
+ * Scan for exposed agent servers
485
+ */
486
+ async scan() {
487
+ const results = [];
488
+ const listeningPorts = await this.getListeningPorts();
489
+ for (const port of listeningPorts) {
490
+ for (const agent of this.agents) {
491
+ if (agent.defaultPorts.includes(port.localPort)) {
492
+ const hasAuth = await this.checkAuthConfig(agent);
493
+ const externallyReachable = this.isExternallyReachable(port.localAddress);
494
+ const severity = this.assessSeverity({
495
+ bindAddress: port.localAddress,
496
+ hasAuth,
497
+ externallyReachable,
498
+ hasActiveSessions: false
499
+ });
500
+ const action = this.getActionForSeverity(severity);
501
+ results.push({
502
+ agent: agent.name,
503
+ pid: port.pid,
504
+ port: port.localPort,
505
+ bindAddress: port.localAddress,
506
+ hasAuth,
507
+ severity,
508
+ action,
509
+ message: this.generateMessage(agent.name, port, severity, hasAuth),
510
+ timestamp: /* @__PURE__ */ new Date()
511
+ });
512
+ }
513
+ }
514
+ }
515
+ return results;
516
+ }
517
+ /**
518
+ * Assess severity based on exposure factors
519
+ */
520
+ assessSeverity(input) {
521
+ const { bindAddress, hasAuth, externallyReachable, hasActiveSessions } = input;
522
+ if (externallyReachable && hasAuth !== true) {
523
+ return "critical";
524
+ }
525
+ if (externallyReachable && hasActiveSessions) {
526
+ return "critical";
527
+ }
528
+ const isWildcardBind = bindAddress === "0.0.0.0" || bindAddress === "::" || bindAddress === "*";
529
+ if (isWildcardBind && hasAuth !== true) {
530
+ return "high";
531
+ }
532
+ if (isWildcardBind && hasAuth === true) {
533
+ return "medium";
534
+ }
535
+ const isLocalhost = bindAddress === "127.0.0.1" || bindAddress === "::1" || bindAddress === "localhost";
536
+ if (isLocalhost && hasAuth !== true) {
537
+ return "medium";
538
+ }
539
+ return "low";
540
+ }
541
+ /**
542
+ * Get action for a given severity level
543
+ */
544
+ getActionForSeverity(severity) {
545
+ return this.config.severityActions[severity];
546
+ }
547
+ /**
548
+ * Parse a Windows netstat output line
549
+ */
550
+ parseNetstatLine(line) {
551
+ const trimmed = line.trim();
552
+ if (!trimmed || !trimmed.includes("LISTENING")) {
553
+ return null;
554
+ }
555
+ const parts = trimmed.split(/\s+/);
556
+ if (parts.length < 5) {
557
+ return null;
558
+ }
559
+ const protocol = parts[0];
560
+ if (protocol !== "TCP" && protocol !== "UDP") {
561
+ return null;
562
+ }
563
+ const localAddressPart = parts[1];
564
+ const state = parts[3];
565
+ const pid = parseInt(parts[4], 10);
566
+ if (state !== "LISTENING" || isNaN(pid)) {
567
+ return null;
568
+ }
569
+ let localAddress;
570
+ let localPort;
571
+ if (localAddressPart.startsWith("[")) {
572
+ const match = localAddressPart.match(/^\[([^\]]+)\]:(\d+)$/);
573
+ if (!match) {
574
+ return null;
575
+ }
576
+ localAddress = match[1];
577
+ localPort = parseInt(match[2], 10);
578
+ } else {
579
+ const lastColonIndex = localAddressPart.lastIndexOf(":");
580
+ if (lastColonIndex === -1) {
581
+ return null;
582
+ }
583
+ localAddress = localAddressPart.substring(0, lastColonIndex);
584
+ localPort = parseInt(localAddressPart.substring(lastColonIndex + 1), 10);
585
+ }
586
+ if (isNaN(localPort)) {
587
+ return null;
588
+ }
589
+ return {
590
+ protocol,
591
+ localAddress,
592
+ localPort,
593
+ state,
594
+ pid
595
+ };
596
+ }
597
+ /**
598
+ * Get all listening TCP ports (cross-platform)
599
+ */
600
+ async getListeningPorts() {
601
+ const results = [];
602
+ try {
603
+ if (platform2() === "win32") {
604
+ const { stdout } = await execAsync("netstat -ano -p TCP");
605
+ const lines = stdout.split("\n");
606
+ for (const line of lines) {
607
+ const parsed = this.parseNetstatLine(line);
608
+ if (parsed) {
609
+ results.push(parsed);
610
+ }
611
+ }
612
+ } else {
613
+ try {
614
+ const { stdout } = await execAsync("ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null");
615
+ const lines = stdout.split("\n");
616
+ for (const line of lines) {
617
+ const parsed = this.parseUnixListeningLine(line);
618
+ if (parsed) {
619
+ results.push(parsed);
620
+ }
621
+ }
622
+ } catch {
623
+ const { stdout } = await execAsync("lsof -iTCP -sTCP:LISTEN -n -P 2>/dev/null || true");
624
+ const lines = stdout.split("\n");
625
+ for (const line of lines) {
626
+ const parsed = this.parseLsofLine(line);
627
+ if (parsed) {
628
+ results.push(parsed);
629
+ }
630
+ }
631
+ }
632
+ }
633
+ } catch {
634
+ }
635
+ return results;
636
+ }
637
+ /**
638
+ * Parse Unix ss/netstat output line
639
+ */
640
+ parseUnixListeningLine(line) {
641
+ const trimmed = line.trim();
642
+ if (!trimmed || trimmed.startsWith("State") || trimmed.startsWith("Proto")) {
643
+ return null;
644
+ }
645
+ const ssMatch = trimmed.match(/LISTEN\s+\d+\s+\d+\s+([^\s]+):(\d+)\s+.*?pid=(\d+)/);
646
+ if (ssMatch) {
647
+ return {
648
+ protocol: "TCP",
649
+ localAddress: ssMatch[1] === "*" ? "0.0.0.0" : ssMatch[1],
650
+ localPort: parseInt(ssMatch[2], 10),
651
+ state: "LISTENING",
652
+ pid: parseInt(ssMatch[3], 10)
653
+ };
654
+ }
655
+ const netstatMatch = trimmed.match(/tcp\s+\d+\s+\d+\s+([^\s]+):(\d+)\s+.*?LISTEN\s+(\d+)/);
656
+ if (netstatMatch) {
657
+ return {
658
+ protocol: "TCP",
659
+ localAddress: netstatMatch[1] === "*" ? "0.0.0.0" : netstatMatch[1],
660
+ localPort: parseInt(netstatMatch[2], 10),
661
+ state: "LISTENING",
662
+ pid: parseInt(netstatMatch[3], 10)
663
+ };
664
+ }
665
+ return null;
666
+ }
667
+ /**
668
+ * Parse lsof output line (macOS fallback)
669
+ */
670
+ parseLsofLine(line) {
671
+ const trimmed = line.trim();
672
+ if (!trimmed || trimmed.startsWith("COMMAND")) {
673
+ return null;
674
+ }
675
+ const parts = trimmed.split(/\s+/);
676
+ if (parts.length < 9) {
677
+ return null;
678
+ }
679
+ const pid = parseInt(parts[1], 10);
680
+ if (isNaN(pid)) {
681
+ return null;
682
+ }
683
+ const tcpIndex = parts.findIndex((p) => p === "TCP" || p === "TCP6");
684
+ if (tcpIndex === -1) {
685
+ return null;
686
+ }
687
+ const addressPart = parts[tcpIndex + 1];
688
+ if (!addressPart) {
689
+ return null;
690
+ }
691
+ const match = addressPart.match(/^([^:]+):(\d+)$/);
692
+ if (!match) {
693
+ return null;
694
+ }
695
+ return {
696
+ protocol: "TCP",
697
+ localAddress: match[1] === "*" ? "0.0.0.0" : match[1],
698
+ localPort: parseInt(match[2], 10),
699
+ state: "LISTENING",
700
+ pid
701
+ };
702
+ }
703
+ /**
704
+ * Check if an agent has auth configured
705
+ */
706
+ async checkAuthConfig(agent) {
707
+ for (const configPath of agent.configPaths) {
708
+ const expandedPath = this.expandPath(configPath);
709
+ if (existsSync3(expandedPath)) {
710
+ try {
711
+ const content = readFileSync(expandedPath, "utf-8");
712
+ for (const indicator of agent.authIndicators) {
713
+ if (content.includes(indicator)) {
714
+ return true;
715
+ }
716
+ }
717
+ return false;
718
+ } catch {
719
+ }
720
+ }
721
+ }
722
+ return "unknown";
723
+ }
724
+ /**
725
+ * Check if an address is externally reachable
726
+ */
727
+ isExternallyReachable(bindAddress) {
728
+ if (bindAddress === "0.0.0.0" || bindAddress === "::" || bindAddress === "*") {
729
+ return this.config.externalProbe;
730
+ }
731
+ if (bindAddress === "127.0.0.1" || bindAddress === "::1" || bindAddress === "localhost") {
732
+ return false;
733
+ }
734
+ if (this.isPrivateIP(bindAddress)) {
735
+ return this.config.externalProbe;
736
+ }
737
+ return true;
738
+ }
739
+ /**
740
+ * Check if an IP is in a private range
741
+ */
742
+ isPrivateIP(ip) {
743
+ if (ip.startsWith("10.")) return true;
744
+ if (ip.startsWith("172.")) {
745
+ const second = parseInt(ip.split(".")[1], 10);
746
+ if (second >= 16 && second <= 31) return true;
747
+ }
748
+ if (ip.startsWith("192.168.")) return true;
749
+ if (ip.toLowerCase().startsWith("fc") || ip.toLowerCase().startsWith("fd")) {
750
+ return true;
751
+ }
752
+ return false;
753
+ }
754
+ /**
755
+ * Expand path with home directory and environment variables
756
+ */
757
+ expandPath(path) {
758
+ let expanded = path;
759
+ if (expanded.startsWith("~")) {
760
+ const home = process.env.HOME || process.env.USERPROFILE || "";
761
+ expanded = expanded.replace("~", home);
762
+ }
763
+ expanded = expanded.replace(/%([^%]+)%/g, (_, name) => process.env[name] || "");
764
+ expanded = expanded.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name) => process.env[name] || "");
765
+ return expanded;
766
+ }
767
+ /**
768
+ * Generate human-readable message for an exposure
769
+ */
770
+ generateMessage(agentName, port, severity, hasAuth) {
771
+ const authStatus = hasAuth === true ? "with authentication" : hasAuth === false ? "without authentication" : "authentication status unknown";
772
+ const bindDesc = port.localAddress === "0.0.0.0" || port.localAddress === "::" ? "all interfaces" : port.localAddress;
773
+ return `${agentName} server detected on port ${port.localPort} (${bindDesc}) ${authStatus}. Severity: ${severity}`;
774
+ }
775
+ /**
776
+ * Add a custom agent signature
777
+ */
778
+ addAgent(agent) {
779
+ this.agents.push(agent);
780
+ }
781
+ /**
782
+ * Get all configured agent signatures
783
+ */
784
+ getAgents() {
785
+ return [...this.agents];
786
+ }
787
+ };
788
+
789
+ // src/policy/ward/patterns.ts
790
+ var DEFAULT_PATTERNS = [
791
+ // Credential patterns
792
+ {
793
+ name: "api_key",
794
+ regex: `(?:api[_-]?key|apikey)[\\s]*[=:][\\s]*["']?([a-zA-Z0-9_\\-]{20,})["']?`,
795
+ severity: "high",
796
+ action: "block",
797
+ category: "credentials",
798
+ description: "Generic API key pattern"
799
+ },
800
+ {
801
+ name: "aws_secret",
802
+ regex: `(?:AWS_SECRET_ACCESS_KEY|aws_secret_access_key)[\\s]*[=:][\\s]*["']?([a-zA-Z0-9/+=]{40})["']?`,
803
+ severity: "critical",
804
+ action: "block",
805
+ category: "credentials",
806
+ description: "AWS Secret Access Key"
807
+ },
808
+ {
809
+ name: "private_key",
810
+ regex: "-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----",
811
+ severity: "critical",
812
+ action: "block",
813
+ category: "credentials",
814
+ description: "Private key header"
815
+ },
816
+ {
817
+ name: "github_token",
818
+ regex: "(?:gh[pousr]_[a-zA-Z0-9]{36,}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})",
819
+ severity: "critical",
820
+ action: "block",
821
+ category: "credentials",
822
+ description: "GitHub personal access token"
823
+ },
824
+ {
825
+ name: "openai_key",
826
+ regex: "sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}",
827
+ severity: "critical",
828
+ action: "block",
829
+ category: "credentials",
830
+ description: "OpenAI API key"
831
+ },
832
+ {
833
+ name: "jwt_token",
834
+ regex: "eyJ[a-zA-Z0-9_-]*\\.eyJ[a-zA-Z0-9_-]*\\.[a-zA-Z0-9_-]*",
835
+ severity: "high",
836
+ action: "alert",
837
+ category: "credentials",
838
+ description: "JSON Web Token"
839
+ },
840
+ // PII patterns
841
+ {
842
+ name: "ssn",
843
+ regex: "\\b\\d{3}[- ]?\\d{2}[- ]?\\d{4}\\b",
844
+ severity: "critical",
845
+ action: "block",
846
+ category: "pii",
847
+ description: "US Social Security Number"
848
+ },
849
+ {
850
+ name: "credit_card",
851
+ regex: "\\b(?:4[0-9]{3}|5[1-5][0-9]{2}|6(?:011|5[0-9]{2})|3[47][0-9]{2})[- ]?[0-9]{4}[- ]?[0-9]{4}[- ]?[0-9]{4}\\b",
852
+ severity: "critical",
853
+ action: "block",
854
+ category: "pii",
855
+ description: "Credit card number (Visa, MasterCard, Amex, Discover)"
856
+ },
857
+ {
858
+ name: "email",
859
+ regex: "\\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\\b",
860
+ severity: "low",
861
+ action: "log",
862
+ category: "pii",
863
+ description: "Email address"
864
+ },
865
+ {
866
+ name: "phone_us",
867
+ regex: "\\b(?:\\+1[- ]?)?(?:\\([0-9]{3}\\)|[0-9]{3})[- ]?[0-9]{3}[- ]?[0-9]{4}\\b",
868
+ severity: "medium",
869
+ action: "alert",
870
+ category: "pii",
871
+ description: "US phone number"
872
+ }
873
+ ];
874
+ var SEVERITY_ORDER = {
875
+ low: 1,
876
+ medium: 2,
877
+ high: 3,
878
+ critical: 4
879
+ };
880
+ var EgressPatternMatcher = class {
881
+ patterns;
882
+ compiledPatterns;
883
+ constructor(customPatterns) {
884
+ this.patterns = [...DEFAULT_PATTERNS];
885
+ this.compiledPatterns = /* @__PURE__ */ new Map();
886
+ if (customPatterns) {
887
+ for (const pattern of customPatterns) {
888
+ this.patterns.push(pattern);
889
+ }
890
+ }
891
+ this.compilePatterns();
892
+ }
893
+ /**
894
+ * Compile regex patterns for efficient matching
895
+ */
896
+ compilePatterns() {
897
+ for (const pattern of this.patterns) {
898
+ if (!this.compiledPatterns.has(pattern.name)) {
899
+ try {
900
+ this.compiledPatterns.set(pattern.name, new RegExp(pattern.regex, "gi"));
901
+ } catch {
902
+ console.warn(`Invalid regex for pattern ${pattern.name}: ${pattern.regex}`);
903
+ }
904
+ }
905
+ }
906
+ }
907
+ /**
908
+ * Find all pattern matches in text
909
+ */
910
+ match(text) {
911
+ const matches = [];
912
+ for (const pattern of this.patterns) {
913
+ const regex = this.compiledPatterns.get(pattern.name);
914
+ if (!regex) continue;
915
+ regex.lastIndex = 0;
916
+ let match;
917
+ while ((match = regex.exec(text)) !== null) {
918
+ matches.push({
919
+ pattern,
920
+ matchedText: match[0],
921
+ index: match.index
922
+ });
923
+ }
924
+ }
925
+ return matches;
926
+ }
927
+ /**
928
+ * Check if any blocking patterns match the text
929
+ */
930
+ shouldBlock(text) {
931
+ const matches = this.match(text);
932
+ return matches.some((m) => m.pattern.action === "block");
933
+ }
934
+ /**
935
+ * Get the highest severity level from all matches
936
+ */
937
+ getHighestSeverity(text) {
938
+ const matches = this.match(text);
939
+ if (matches.length === 0) return null;
940
+ let highest = "low";
941
+ for (const m of matches) {
942
+ if (SEVERITY_ORDER[m.pattern.severity] > SEVERITY_ORDER[highest]) {
943
+ highest = m.pattern.severity;
944
+ }
945
+ }
946
+ return highest;
947
+ }
948
+ /**
949
+ * Redact sensitive data from text
950
+ */
951
+ redact(text) {
952
+ const matches = this.match(text);
953
+ const redactions = [];
954
+ let redacted = text;
955
+ const sortedMatches = [...matches].sort((a, b) => b.index - a.index);
956
+ for (const m of sortedMatches) {
957
+ const replacement = `[REDACTED:${m.pattern.name}]`;
958
+ let redactionType;
959
+ if (m.pattern.name === "api_key" || m.pattern.name === "aws_secret" || m.pattern.name === "private_key" || m.pattern.name === "github_token" || m.pattern.name === "openai_key" || m.pattern.name === "jwt_token") {
960
+ redactionType = "api_key";
961
+ } else if (m.pattern.name === "email") {
962
+ redactionType = "email";
963
+ } else if (m.pattern.name === "phone_us") {
964
+ redactionType = "phone";
965
+ } else if (m.pattern.name === "ssn") {
966
+ redactionType = "ssn";
967
+ } else if (m.pattern.name === "credit_card") {
968
+ redactionType = "credit_card";
969
+ } else {
970
+ redactionType = "custom";
971
+ }
972
+ redactions.push({
973
+ type: redactionType,
974
+ replacement
975
+ });
976
+ redacted = redacted.substring(0, m.index) + replacement + redacted.substring(m.index + m.matchedText.length);
977
+ }
978
+ return {
979
+ original: text,
980
+ redacted,
981
+ redactions: redactions.reverse()
982
+ // Return in original order
983
+ };
984
+ }
985
+ /**
986
+ * Add a custom pattern
987
+ */
988
+ addPattern(pattern) {
989
+ this.patterns.push(pattern);
990
+ try {
991
+ this.compiledPatterns.set(pattern.name, new RegExp(pattern.regex, "gi"));
992
+ } catch {
993
+ console.warn(`Invalid regex for pattern ${pattern.name}: ${pattern.regex}`);
994
+ }
995
+ }
996
+ /**
997
+ * Get all configured patterns
998
+ */
999
+ getPatterns() {
1000
+ return [...this.patterns];
1001
+ }
1002
+ /**
1003
+ * Test helper - returns matches, shouldBlock, and redacted result
1004
+ */
1005
+ test(text) {
1006
+ return {
1007
+ matches: this.match(text),
1008
+ shouldBlock: this.shouldBlock(text),
1009
+ redacted: this.redact(text)
1010
+ };
1011
+ }
1012
+ };
1013
+
1014
+ // src/policy/ward/egress.ts
1015
+ var DashboardDB2 = null;
1016
+ try {
1017
+ const dbModule = await import("./db-EHQDB5OL.js");
1018
+ DashboardDB2 = dbModule.DashboardDB;
1019
+ } catch {
1020
+ }
1021
+ var DEFAULT_EGRESS_CONFIG = {
1022
+ enabled: true,
1023
+ defaultAction: "block",
1024
+ allowlist: []
1025
+ };
1026
+ var EgressMonitor = class {
1027
+ config;
1028
+ matcher;
1029
+ db = null;
1030
+ allowlist;
1031
+ constructor(config) {
1032
+ this.config = { ...DEFAULT_EGRESS_CONFIG, ...config };
1033
+ this.matcher = new EgressPatternMatcher();
1034
+ this.allowlist = [...this.config.allowlist];
1035
+ if (DashboardDB2) {
1036
+ try {
1037
+ this.db = new DashboardDB2();
1038
+ } catch {
1039
+ }
1040
+ }
1041
+ }
1042
+ /**
1043
+ * Inspect content for sensitive data
1044
+ *
1045
+ * @param content - The content to inspect
1046
+ * @param connector - Optional connector name for allowlist checking
1047
+ * @param destination - Optional destination for allowlist checking
1048
+ * @returns Inspection result with blocking decision and matches
1049
+ */
1050
+ inspect(content, connector, destination) {
1051
+ if (this.isAllowlisted(connector, destination)) {
1052
+ return {
1053
+ blocked: false,
1054
+ allowlisted: true,
1055
+ matches: [],
1056
+ redacted: content
1057
+ };
1058
+ }
1059
+ const allMatches = this.matcher.match(content);
1060
+ const effectiveMatches = allMatches.filter((match) => {
1061
+ return !this.isPatternAllowlisted(match.pattern.name, connector, destination);
1062
+ });
1063
+ const shouldBlock = effectiveMatches.some((m) => m.pattern.action === "block");
1064
+ let redacted = content;
1065
+ if (effectiveMatches.length > 0) {
1066
+ const sortedMatches = [...effectiveMatches].sort((a, b) => b.index - a.index);
1067
+ for (const m of sortedMatches) {
1068
+ const replacement = `[REDACTED:${m.pattern.name}]`;
1069
+ redacted = redacted.substring(0, m.index) + replacement + redacted.substring(m.index + m.matchedText.length);
1070
+ }
1071
+ }
1072
+ const wasAllowlisted = allMatches.length > 0 && effectiveMatches.length === 0;
1073
+ const result = {
1074
+ blocked: shouldBlock,
1075
+ allowlisted: wasAllowlisted,
1076
+ matches: effectiveMatches,
1077
+ redacted
1078
+ };
1079
+ if (shouldBlock && this.db) {
1080
+ try {
1081
+ for (const match of effectiveMatches.filter((m) => m.pattern.action === "block")) {
1082
+ const blockId = this.db.insertEgressBlock({
1083
+ pattern: match.pattern,
1084
+ matchedText: match.matchedText.substring(0, 100),
1085
+ // Truncate for safety
1086
+ redactedText: redacted,
1087
+ connector,
1088
+ destination
1089
+ });
1090
+ result.blockId = blockId;
1091
+ }
1092
+ this.db.insertEvent({
1093
+ source: "ward",
1094
+ level: "warn",
1095
+ category: "egress",
1096
+ message: `Blocked egress: ${effectiveMatches.map((m) => m.pattern.name).join(", ")}`,
1097
+ data: {
1098
+ connector,
1099
+ destination,
1100
+ patternNames: effectiveMatches.map((m) => m.pattern.name)
1101
+ }
1102
+ });
1103
+ } catch {
1104
+ }
1105
+ }
1106
+ return result;
1107
+ }
1108
+ /**
1109
+ * Check if connector/destination combination is fully allowlisted
1110
+ */
1111
+ isAllowlisted(connector, destination) {
1112
+ return this.allowlist.some((entry) => {
1113
+ if (entry.pattern) return false;
1114
+ if (entry.connector && entry.destination) {
1115
+ return entry.connector === connector && entry.destination === destination;
1116
+ }
1117
+ if (entry.connector) {
1118
+ return entry.connector === connector;
1119
+ }
1120
+ if (entry.destination) {
1121
+ return entry.destination === destination;
1122
+ }
1123
+ return false;
1124
+ });
1125
+ }
1126
+ /**
1127
+ * Check if a specific pattern is allowlisted for the connector/destination
1128
+ */
1129
+ isPatternAllowlisted(patternName, connector, destination) {
1130
+ return this.allowlist.some((entry) => {
1131
+ if (!entry.pattern || entry.pattern !== patternName) return false;
1132
+ if (entry.connector && entry.destination) {
1133
+ return entry.connector === connector && entry.destination === destination;
1134
+ }
1135
+ if (entry.connector) {
1136
+ return entry.connector === connector;
1137
+ }
1138
+ if (entry.destination) {
1139
+ return entry.destination === destination;
1140
+ }
1141
+ return true;
1142
+ });
1143
+ }
1144
+ /**
1145
+ * Add an allowlist entry
1146
+ */
1147
+ addAllowlistEntry(entry) {
1148
+ this.allowlist.push(entry);
1149
+ }
1150
+ /**
1151
+ * Add a custom pattern
1152
+ */
1153
+ addPattern(pattern) {
1154
+ this.matcher.addPattern(pattern);
1155
+ }
1156
+ /**
1157
+ * Get pending blocks from database
1158
+ */
1159
+ getPendingBlocks() {
1160
+ if (!this.db) return [];
1161
+ try {
1162
+ return this.db.getPendingBlocks();
1163
+ } catch {
1164
+ return [];
1165
+ }
1166
+ }
1167
+ /**
1168
+ * Approve a pending block
1169
+ */
1170
+ approveBlock(id, approvedBy) {
1171
+ if (!this.db) return;
1172
+ try {
1173
+ this.db.approveBlock(id, approvedBy ?? "system");
1174
+ } catch {
1175
+ }
1176
+ }
1177
+ /**
1178
+ * Deny a pending block
1179
+ */
1180
+ denyBlock(id) {
1181
+ if (!this.db) return;
1182
+ try {
1183
+ this.db.denyBlock(id, "system");
1184
+ } catch {
1185
+ }
1186
+ }
1187
+ /**
1188
+ * Test content without recording to database
1189
+ * Useful for CLI testing of patterns
1190
+ */
1191
+ test(content) {
1192
+ const matches = this.matcher.match(content);
1193
+ const shouldBlock = matches.some((m) => m.pattern.action === "block");
1194
+ let redacted = content;
1195
+ if (matches.length > 0) {
1196
+ const sortedMatches = [...matches].sort((a, b) => b.index - a.index);
1197
+ for (const m of sortedMatches) {
1198
+ const replacement = `[REDACTED:${m.pattern.name}]`;
1199
+ redacted = redacted.substring(0, m.index) + replacement + redacted.substring(m.index + m.matchedText.length);
1200
+ }
1201
+ }
1202
+ return {
1203
+ blocked: shouldBlock,
1204
+ matches,
1205
+ redacted
1206
+ };
1207
+ }
1208
+ };
1209
+
1210
+ // src/doctor.ts
1211
+ async function checkAgentConfigs(agents) {
1212
+ const checks = [];
1213
+ for (const agent of agents) {
1214
+ const agentName = agent.agent.charAt(0).toUpperCase() + agent.agent.slice(1).replace("-", " ");
1215
+ if (agent.configPath && !agent.configExists) {
1216
+ checks.push({
1217
+ name: `${agentName} config`,
1218
+ passed: false,
1219
+ message: `Config path known (${agent.configPath}) but file not found`
1220
+ });
1221
+ } else if (agent.configExists) {
1222
+ checks.push({
1223
+ name: `${agentName} config`,
1224
+ passed: true,
1225
+ message: `Found at ${agent.configPath}`
1226
+ });
1227
+ }
1228
+ if (agent.agent === "claude-code") {
1229
+ checks.push({
1230
+ name: `${agentName} integration`,
1231
+ passed: agent.bashbrosIntegrated,
1232
+ message: agent.bashbrosIntegrated ? "BashBros hooks installed" : 'Hooks not installed. Run "bashbros hook install" to protect Claude Code'
1233
+ });
1234
+ }
1235
+ if (agent.agent === "moltbot" || agent.agent === "clawdbot") {
1236
+ checks.push({
1237
+ name: `${agentName} integration`,
1238
+ passed: agent.bashbrosIntegrated,
1239
+ message: agent.bashbrosIntegrated ? "BashBros hooks installed" : 'Hooks not installed. Run "bashbros moltbot install" to protect moltbot'
1240
+ });
1241
+ try {
1242
+ const { MoltbotHooks: MoltbotHooks2 } = await import("./moltbot-DXZFVK3X.js");
1243
+ const status = MoltbotHooks2.getStatus();
1244
+ if (status.sandboxMode) {
1245
+ checks.push({
1246
+ name: `${agentName} sandbox`,
1247
+ passed: status.sandboxMode === "strict",
1248
+ message: status.sandboxMode === "strict" ? "Sandbox mode enabled (strict)" : `Sandbox mode: ${status.sandboxMode} (consider "strict" for better security)`
1249
+ });
1250
+ }
1251
+ if (status.gatewayRunning) {
1252
+ const gatewayStatus = await MoltbotHooks2.getGatewayStatus();
1253
+ checks.push({
1254
+ name: `${agentName} gateway`,
1255
+ passed: gatewayStatus.running,
1256
+ message: gatewayStatus.running ? `Gateway running on port ${gatewayStatus.port}` : "Gateway configured but not running"
1257
+ });
1258
+ }
1259
+ } catch {
1260
+ }
1261
+ }
1262
+ if (agent.permissions) {
1263
+ const config = loadConfig();
1264
+ if ((!agent.permissions.allowedPaths || agent.permissions.allowedPaths.length === 0) && config.paths.allow.length > 0 && !config.paths.allow.includes("*")) {
1265
+ checks.push({
1266
+ name: `${agentName} path policy`,
1267
+ passed: true,
1268
+ // Not a failure, just a note
1269
+ message: `Agent has no path restrictions; bashbros will enforce: ${config.paths.allow.slice(0, 3).join(", ")}`
1270
+ });
1271
+ }
1272
+ }
1273
+ }
1274
+ return checks;
1275
+ }
1276
+ async function runDoctor() {
1277
+ console.log(chalk2.bold("\nRunning diagnostics...\n"));
1278
+ const checks = [];
1279
+ const configPath = findConfig();
1280
+ checks.push({
1281
+ name: "Config file",
1282
+ passed: configPath !== null,
1283
+ message: configPath ? `Found at ${configPath}` : 'Not found. Run "bashbros init" to create one.'
1284
+ });
1285
+ if (configPath) {
1286
+ try {
1287
+ const config = loadConfig(configPath);
1288
+ checks.push({
1289
+ name: "Config valid",
1290
+ passed: true,
1291
+ message: `Profile: ${config.profile}, Agent: ${config.agent}`
1292
+ });
1293
+ } catch (error) {
1294
+ checks.push({
1295
+ name: "Config valid",
1296
+ passed: false,
1297
+ message: `Parse error: ${error}`
1298
+ });
1299
+ }
1300
+ }
1301
+ const auditDir = join3(homedir3(), ".bashbros");
1302
+ checks.push({
1303
+ name: "Audit directory",
1304
+ passed: existsSync4(auditDir),
1305
+ message: existsSync4(auditDir) ? `Found at ${auditDir}` : "Will be created on first run"
1306
+ });
1307
+ try {
1308
+ await import("node-pty");
1309
+ checks.push({
1310
+ name: "PTY support",
1311
+ passed: true,
1312
+ message: "node-pty loaded successfully"
1313
+ });
1314
+ } catch (error) {
1315
+ checks.push({
1316
+ name: "PTY support",
1317
+ passed: false,
1318
+ message: 'node-pty not available. Run "npm install" to install dependencies.'
1319
+ });
1320
+ }
1321
+ if (configPath) {
1322
+ const config = loadConfig(configPath);
1323
+ const secretsEnabled = config.secrets.enabled;
1324
+ checks.push({
1325
+ name: "Secrets protection",
1326
+ passed: secretsEnabled,
1327
+ message: secretsEnabled ? `Enabled with ${config.secrets.patterns.length} patterns` : "Disabled - credentials may be exposed"
1328
+ });
1329
+ }
1330
+ if (configPath) {
1331
+ const config = loadConfig(configPath);
1332
+ checks.push({
1333
+ name: "Rate limiting",
1334
+ passed: config.rateLimit.enabled,
1335
+ message: config.rateLimit.enabled ? `${config.rateLimit.maxPerMinute}/min, ${config.rateLimit.maxPerHour}/hr` : "Disabled - runaway agents possible"
1336
+ });
1337
+ }
1338
+ const agents = await getAllAgentConfigs();
1339
+ const installedAgents = agents.filter((a) => a.installed);
1340
+ if (installedAgents.length > 0) {
1341
+ const agentChecks = await checkAgentConfigs(installedAgents);
1342
+ checks.push(...agentChecks);
1343
+ } else {
1344
+ checks.push({
1345
+ name: "Agent detection",
1346
+ passed: true,
1347
+ message: "No agents detected (install claude, aider, etc. to use bashbros protection)"
1348
+ });
1349
+ }
1350
+ if (configPath) {
1351
+ const config = loadConfig(configPath);
1352
+ const wardEnabled = config.ward?.enabled ?? false;
1353
+ checks.push({
1354
+ name: "Ward security",
1355
+ passed: wardEnabled,
1356
+ message: wardEnabled ? `Enabled (exposure scan: ${config.ward.exposure.scanInterval}ms)` : "Disabled - network exposure monitoring off"
1357
+ });
1358
+ }
1359
+ if (configPath) {
1360
+ const config = loadConfig(configPath);
1361
+ const dashEnabled = config.dashboard?.enabled ?? false;
1362
+ checks.push({
1363
+ name: "Dashboard",
1364
+ passed: dashEnabled,
1365
+ message: dashEnabled ? `Enabled on ${config.dashboard.bind}:${config.dashboard.port}` : 'Disabled - run "bashbros dashboard" to start'
1366
+ });
1367
+ }
1368
+ try {
1369
+ const scanner = new ExposureScanner();
1370
+ const agentSignatures = scanner.getAgents();
1371
+ checks.push({
1372
+ name: "Exposure scanner",
1373
+ passed: true,
1374
+ message: `Ready, monitoring ${agentSignatures.length} agent signatures`
1375
+ });
1376
+ } catch {
1377
+ checks.push({
1378
+ name: "Exposure scanner",
1379
+ passed: false,
1380
+ message: "Failed to initialize exposure scanner"
1381
+ });
1382
+ }
1383
+ try {
1384
+ const matcher = new EgressPatternMatcher();
1385
+ const patterns = matcher.getPatterns();
1386
+ const credPatterns = patterns.filter((p) => p.category === "credentials").length;
1387
+ const piiPatterns = patterns.filter((p) => p.category === "pii").length;
1388
+ checks.push({
1389
+ name: "Egress patterns",
1390
+ passed: true,
1391
+ message: `Loaded ${patterns.length} patterns (${credPatterns} credential, ${piiPatterns} PII)`
1392
+ });
1393
+ } catch {
1394
+ checks.push({
1395
+ name: "Egress patterns",
1396
+ passed: false,
1397
+ message: "Failed to initialize egress patterns"
1398
+ });
1399
+ }
1400
+ let passed = 0;
1401
+ let failed = 0;
1402
+ for (const check of checks) {
1403
+ const icon = check.passed ? chalk2.green("\u2713") : chalk2.red("\u2717");
1404
+ const status = check.passed ? chalk2.green("OK") : chalk2.red("FAIL");
1405
+ console.log(` ${icon} ${chalk2.bold(check.name)}: ${status}`);
1406
+ console.log(chalk2.dim(` ${check.message}`));
1407
+ console.log();
1408
+ if (check.passed) passed++;
1409
+ else failed++;
1410
+ }
1411
+ console.log(chalk2.bold("\u2500".repeat(40)));
1412
+ if (failed === 0) {
1413
+ console.log(chalk2.green(`
1414
+ \u2713 All ${passed} checks passed. BashBros is ready.
1415
+ `));
1416
+ } else {
1417
+ console.log(chalk2.yellow(`
1418
+ ${passed} passed, ${failed} failed. Fix issues above.
1419
+ `));
1420
+ }
1421
+ }
1422
+
1423
+ // src/watch.ts
1424
+ import chalk3 from "chalk";
1425
+ async function startWatch(options) {
1426
+ const configPath = findConfig();
1427
+ if (!configPath) {
1428
+ console.log(chalk3.red('No config found. Run "bashbros init" first.'));
1429
+ process.exit(1);
1430
+ }
1431
+ console.log(chalk3.dim(` "I watch your agent's back so you don't have to."`));
1432
+ console.log();
1433
+ console.log(chalk3.green("\u2713"), "Protection active");
1434
+ console.log(chalk3.dim(` Config: ${configPath}`));
1435
+ console.log(chalk3.dim(" Press Ctrl+C to stop"));
1436
+ console.log();
1437
+ const bashbros = new BashBros(configPath);
1438
+ bashbros.on("blocked", (command, violations) => {
1439
+ console.log();
1440
+ console.log(chalk3.red("\u{1F6E1}\uFE0F BashBros blocked a command"));
1441
+ console.log();
1442
+ console.log(chalk3.dim(" Command:"), command);
1443
+ console.log(chalk3.dim(" Reason:"), violations[0].message);
1444
+ console.log(chalk3.dim(" Policy:"), violations[0].rule);
1445
+ console.log();
1446
+ console.log(chalk3.dim(" To allow this command:"));
1447
+ console.log(chalk3.cyan(` bashbros allow "${command}" --once`));
1448
+ console.log(chalk3.cyan(` bashbros allow "${command}" --persist`));
1449
+ console.log();
1450
+ });
1451
+ bashbros.on("allowed", (result) => {
1452
+ if (options.verbose) {
1453
+ console.log(chalk3.green("\u2713"), chalk3.dim(result.command));
1454
+ }
1455
+ });
1456
+ bashbros.on("output", (data) => {
1457
+ process.stdout.write(data);
1458
+ });
1459
+ bashbros.on("error", (error) => {
1460
+ console.error(chalk3.red("Error:"), error.message);
1461
+ });
1462
+ process.on("SIGINT", () => {
1463
+ console.log();
1464
+ console.log(chalk3.yellow("Stopping BashBros..."));
1465
+ bashbros.stop();
1466
+ process.exit(0);
1467
+ });
1468
+ process.on("SIGTERM", () => {
1469
+ bashbros.stop();
1470
+ process.exit(0);
1471
+ });
1472
+ bashbros.start();
1473
+ await new Promise(() => {
1474
+ });
1475
+ }
1476
+
1477
+ // src/allow.ts
1478
+ import chalk4 from "chalk";
1479
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1480
+ import { parse, stringify as stringify2 } from "yaml";
1481
+ async function handleAllow(command, options) {
1482
+ if (options.once) {
1483
+ allowForSession(command);
1484
+ console.log(chalk4.green("\u2713"), `Allowed for this session: ${command}`);
1485
+ console.log(chalk4.dim(" This will reset when BashBros restarts."));
1486
+ return;
1487
+ }
1488
+ if (options.persist) {
1489
+ const configPath = findConfig();
1490
+ if (!configPath) {
1491
+ console.log(chalk4.red('No config found. Run "bashbros init" first.'));
1492
+ process.exit(1);
1493
+ }
1494
+ try {
1495
+ const content = readFileSync2(configPath, "utf-8");
1496
+ const config = parse(content);
1497
+ if (!config.commands) {
1498
+ config.commands = { allow: [], block: [] };
1499
+ }
1500
+ if (!config.commands.allow) {
1501
+ config.commands.allow = [];
1502
+ }
1503
+ if (!config.commands.allow.includes(command)) {
1504
+ config.commands.allow.push(command);
1505
+ writeFileSync2(configPath, stringify2(config));
1506
+ console.log(chalk4.green("\u2713"), `Added to allowlist: ${command}`);
1507
+ console.log(chalk4.dim(` Updated ${configPath}`));
1508
+ } else {
1509
+ console.log(chalk4.yellow("Already in allowlist:"), command);
1510
+ }
1511
+ } catch (error) {
1512
+ console.error(chalk4.red("Failed to update config:"), error);
1513
+ process.exit(1);
1514
+ }
1515
+ return;
1516
+ }
1517
+ console.log(chalk4.yellow("Specify how to allow this command:"));
1518
+ console.log();
1519
+ console.log(chalk4.cyan(` bashbros allow "${command}" --once`));
1520
+ console.log(chalk4.dim(" Allow for current session only"));
1521
+ console.log();
1522
+ console.log(chalk4.cyan(` bashbros allow "${command}" --persist`));
1523
+ console.log(chalk4.dim(" Add to config file permanently"));
1524
+ }
1525
+
1526
+ // src/dashboard/server.ts
1527
+ import express from "express";
1528
+ import { WebSocketServer } from "ws";
1529
+ import { createServer } from "http";
1530
+ import { fileURLToPath } from "url";
1531
+ import { dirname, join as join4 } from "path";
1532
+ var DashboardServer = class {
1533
+ app;
1534
+ server = null;
1535
+ wss = null;
1536
+ db;
1537
+ port;
1538
+ bind;
1539
+ clients = /* @__PURE__ */ new Set();
1540
+ constructor(config = {}) {
1541
+ this.port = config.port ?? 17800;
1542
+ this.bind = config.bind ?? "127.0.0.1";
1543
+ this.db = new DashboardDB(config.dbPath ?? ":memory:");
1544
+ this.app = express();
1545
+ this.setupMiddleware();
1546
+ this.setupRoutes();
1547
+ }
1548
+ setupMiddleware() {
1549
+ this.app.use(express.json());
1550
+ this.app.use((_req, res, next) => {
1551
+ res.header("Access-Control-Allow-Origin", "*");
1552
+ res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1553
+ res.header("Access-Control-Allow-Headers", "Content-Type");
1554
+ next();
1555
+ });
1556
+ }
1557
+ setupRoutes() {
1558
+ this.app.get("/api/health", (_req, res) => {
1559
+ res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
1560
+ });
1561
+ this.app.get("/api/stats", (_req, res) => {
1562
+ try {
1563
+ const stats = this.db.getStats();
1564
+ res.json(stats);
1565
+ } catch (error) {
1566
+ res.status(500).json({ error: "Failed to fetch stats" });
1567
+ }
1568
+ });
1569
+ this.app.get("/api/events", (req, res) => {
1570
+ try {
1571
+ const filter = {};
1572
+ if (req.query.source) filter.source = req.query.source;
1573
+ if (req.query.level) filter.level = req.query.level;
1574
+ if (req.query.category) filter.category = req.query.category;
1575
+ if (req.query.limit) filter.limit = parseInt(req.query.limit, 10);
1576
+ if (req.query.offset) filter.offset = parseInt(req.query.offset, 10);
1577
+ if (req.query.since) filter.since = new Date(req.query.since);
1578
+ const events = this.db.getEvents(filter);
1579
+ res.json(events);
1580
+ } catch (error) {
1581
+ res.status(500).json({ error: "Failed to fetch events" });
1582
+ }
1583
+ });
1584
+ this.app.get("/api/connectors", (_req, res) => {
1585
+ try {
1586
+ const events = this.db.getAllConnectorEvents(1e3);
1587
+ const connectors = [...new Set(events.map((e) => e.connector))];
1588
+ res.json(connectors);
1589
+ } catch (error) {
1590
+ res.status(500).json({ error: "Failed to fetch connectors" });
1591
+ }
1592
+ });
1593
+ this.app.get("/api/connectors/:name/events", (req, res) => {
1594
+ try {
1595
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 100;
1596
+ const events = this.db.getConnectorEvents(req.params.name, limit);
1597
+ res.json(events);
1598
+ } catch (error) {
1599
+ res.status(500).json({ error: "Failed to fetch connector events" });
1600
+ }
1601
+ });
1602
+ this.app.get("/api/blocked", (_req, res) => {
1603
+ try {
1604
+ const blocks = this.db.getPendingBlocks();
1605
+ res.json(blocks);
1606
+ } catch (error) {
1607
+ res.status(500).json({ error: "Failed to fetch blocked items" });
1608
+ }
1609
+ });
1610
+ this.app.post("/api/blocked/:id/approve", (req, res) => {
1611
+ try {
1612
+ const { id } = req.params;
1613
+ const approvedBy = req.body?.approvedBy ?? "dashboard-user";
1614
+ const block = this.db.getBlock(id);
1615
+ if (!block) {
1616
+ res.status(404).json({ error: "Block not found" });
1617
+ return;
1618
+ }
1619
+ this.db.approveBlock(id, approvedBy);
1620
+ this.broadcast({ type: "block-approved", id });
1621
+ res.json({ success: true });
1622
+ } catch (error) {
1623
+ res.status(500).json({ error: "Failed to approve block" });
1624
+ }
1625
+ });
1626
+ this.app.post("/api/blocked/:id/deny", (req, res) => {
1627
+ try {
1628
+ const { id } = req.params;
1629
+ const deniedBy = req.body?.deniedBy ?? "dashboard-user";
1630
+ const block = this.db.getBlock(id);
1631
+ if (!block) {
1632
+ res.status(404).json({ error: "Block not found" });
1633
+ return;
1634
+ }
1635
+ this.db.denyBlock(id, deniedBy);
1636
+ this.broadcast({ type: "block-denied", id });
1637
+ res.json({ success: true });
1638
+ } catch (error) {
1639
+ res.status(500).json({ error: "Failed to deny block" });
1640
+ }
1641
+ });
1642
+ const __filename = fileURLToPath(import.meta.url);
1643
+ const __dirname = dirname(__filename);
1644
+ const staticPath = join4(__dirname, "static");
1645
+ this.app.use(express.static(staticPath));
1646
+ this.app.get("/{*path}", (_req, res) => {
1647
+ res.sendFile(join4(staticPath, "index.html"));
1648
+ });
1649
+ }
1650
+ setupWebSocket() {
1651
+ if (!this.server) return;
1652
+ this.wss = new WebSocketServer({ server: this.server });
1653
+ this.wss.on("connection", (ws) => {
1654
+ this.clients.add(ws);
1655
+ ws.on("close", () => {
1656
+ this.clients.delete(ws);
1657
+ });
1658
+ ws.on("error", () => {
1659
+ this.clients.delete(ws);
1660
+ });
1661
+ try {
1662
+ const stats = this.db.getStats();
1663
+ ws.send(JSON.stringify({ type: "stats", data: stats }));
1664
+ } catch {
1665
+ }
1666
+ });
1667
+ }
1668
+ /**
1669
+ * Start the dashboard server
1670
+ */
1671
+ async start() {
1672
+ return new Promise((resolve, reject) => {
1673
+ this.server = createServer(this.app);
1674
+ this.setupWebSocket();
1675
+ this.server.on("error", (error) => {
1676
+ if (error.code === "EADDRINUSE") {
1677
+ reject(new Error(`Port ${this.port} is already in use`));
1678
+ } else {
1679
+ reject(error);
1680
+ }
1681
+ });
1682
+ this.server.listen(this.port, this.bind, () => {
1683
+ resolve();
1684
+ });
1685
+ });
1686
+ }
1687
+ /**
1688
+ * Stop the dashboard server
1689
+ */
1690
+ async stop() {
1691
+ return new Promise((resolve) => {
1692
+ for (const client of this.clients) {
1693
+ client.close();
1694
+ }
1695
+ this.clients.clear();
1696
+ if (this.wss) {
1697
+ this.wss.close();
1698
+ this.wss = null;
1699
+ }
1700
+ if (this.server) {
1701
+ this.server.close(() => {
1702
+ this.server = null;
1703
+ this.db.close();
1704
+ resolve();
1705
+ });
1706
+ } else {
1707
+ this.db.close();
1708
+ resolve();
1709
+ }
1710
+ });
1711
+ }
1712
+ /**
1713
+ * Broadcast a message to all connected WebSocket clients
1714
+ */
1715
+ broadcast(message) {
1716
+ const data = JSON.stringify(message);
1717
+ for (const client of this.clients) {
1718
+ if (client.readyState === 1) {
1719
+ client.send(data);
1720
+ }
1721
+ }
1722
+ }
1723
+ /**
1724
+ * Get the database instance for external use
1725
+ */
1726
+ getDB() {
1727
+ return this.db;
1728
+ }
1729
+ /**
1730
+ * Get the port the server is running on
1731
+ */
1732
+ getPort() {
1733
+ return this.port;
1734
+ }
1735
+ };
1736
+
1737
+ // src/cli.ts
1738
+ var metricsCollector = null;
1739
+ var costEstimator = null;
1740
+ var loopDetector = null;
1741
+ var undoStack = null;
1742
+ var logo = `
1743
+ \u2571BashBros \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
1744
+ \u{1F91D} Your Friendly Bash Agent Helper
1745
+ `;
1746
+ var program = new Command();
1747
+ program.name("bashbros").description("The Bash Agent Helper").version("0.1.0");
1748
+ program.command("init").description("Set up BashBros for your project").action(async () => {
1749
+ console.log(chalk5.cyan(logo));
1750
+ await runOnboarding();
1751
+ });
1752
+ program.command("watch").description("Start protecting your agent").option("-v, --verbose", "Show all commands as they run").action(async (options) => {
1753
+ console.log(chalk5.cyan(logo));
1754
+ await startWatch(options);
1755
+ });
1756
+ program.command("doctor").description("Check your BashBros configuration").action(async () => {
1757
+ console.log(chalk5.cyan(logo));
1758
+ await runDoctor();
1759
+ });
1760
+ program.command("allow <command>").description("Allow a specific command").option("--once", "Allow only for current session").option("--persist", "Add to config permanently").action(async (command, options) => {
1761
+ await handleAllow(command, options);
1762
+ });
1763
+ program.command("audit").description("View recent command history").option("-n, --lines <number>", "Number of lines to show", "50").option("--violations", "Show only blocked commands").action(async (options) => {
1764
+ const { viewAudit } = await import("./audit-MCFNGOIM.js");
1765
+ await viewAudit(options);
1766
+ });
1767
+ program.command("scan").description("Scan your system and project environment").option("-p, --project <path>", "Project path to scan", ".").action(async (options) => {
1768
+ console.log(chalk5.cyan(logo));
1769
+ console.log(chalk5.dim(" Scanning your environment...\n"));
1770
+ const bro = new BashBro();
1771
+ await bro.initialize();
1772
+ if (options.project) {
1773
+ bro.scanProject(options.project);
1774
+ }
1775
+ console.log(bro.getSystemContext());
1776
+ console.log();
1777
+ console.log(chalk5.bold("\n## Agent Configurations\n"));
1778
+ const { formatAgentSummary } = await import("./display-IN4NRJJS.js");
1779
+ const agents = await getAllAgentConfigs();
1780
+ console.log(formatAgentSummary(agents));
1781
+ console.log();
1782
+ console.log(chalk5.green("\u2713"), "System profile saved to ~/.bashbros/system-profile.json");
1783
+ });
1784
+ program.command("status").alias("bro").description("Show Bash Bro status and system info").action(async () => {
1785
+ console.log(chalk5.cyan(logo));
1786
+ const bro = new BashBro();
1787
+ await bro.initialize();
1788
+ console.log(bro.status());
1789
+ });
1790
+ program.command("suggest").description("Get command suggestions based on context").option("-c, --command <cmd>", "Last command for context").option("-e, --error <msg>", "Last error message").action(async (options) => {
1791
+ const bro = new BashBro();
1792
+ await bro.initialize();
1793
+ const suggestions = bro.suggest({
1794
+ lastCommand: options.command,
1795
+ lastError: options.error,
1796
+ cwd: process.cwd()
1797
+ });
1798
+ if (suggestions.length === 0) {
1799
+ console.log(chalk5.dim("No suggestions available."));
1800
+ return;
1801
+ }
1802
+ console.log(chalk5.bold("\u{1F91D} Bash Bro suggests:\n"));
1803
+ for (const s of suggestions) {
1804
+ const confidence = Math.round(s.confidence * 100);
1805
+ console.log(` ${chalk5.cyan(s.command)}`);
1806
+ console.log(chalk5.dim(` ${s.description} (${confidence}% confidence)`));
1807
+ console.log();
1808
+ }
1809
+ });
1810
+ program.command("route <command>").description("Check how a command would be routed").action(async (command) => {
1811
+ const bro = new BashBro();
1812
+ await bro.initialize();
1813
+ const result = bro.route(command);
1814
+ const icon = result.decision === "bro" ? "\u{1F91D}" : result.decision === "main" ? "\u{1F916}" : "\u26A1";
1815
+ const label = result.decision === "bro" ? "Bash Bro" : result.decision === "main" ? "Main Agent" : "Both (parallel)";
1816
+ console.log();
1817
+ console.log(`${icon} Route: ${chalk5.bold(label)}`);
1818
+ console.log(chalk5.dim(` Reason: ${result.reason}`));
1819
+ console.log(chalk5.dim(` Confidence: ${Math.round(result.confidence * 100)}%`));
1820
+ console.log();
1821
+ });
1822
+ program.command("run <command>").description("Run a command through Bash Bro").option("-b, --background", "Run in background").action(async (command, options) => {
1823
+ const bro = new BashBro();
1824
+ await bro.initialize();
1825
+ if (options.background) {
1826
+ const task = bro.runBackground(command);
1827
+ console.log(chalk5.green("\u2713"), `Started background task: ${task.id}`);
1828
+ console.log(chalk5.dim(` Command: ${command}`));
1829
+ console.log(chalk5.dim(` Run 'bashbros tasks' to check status`));
1830
+ } else {
1831
+ console.log(chalk5.dim(`\u{1F91D} Bash Bro executing: ${command}
1832
+ `));
1833
+ const output = await bro.execute(command);
1834
+ console.log(output);
1835
+ }
1836
+ });
1837
+ program.command("tasks").description("List background tasks").option("-a, --all", "Show all tasks (not just running)").action(async (options) => {
1838
+ const bro = new BashBro();
1839
+ const tasks = options.all ? bro.getBackgroundTasks() : bro.getBackgroundTasks().filter((t) => t.status === "running");
1840
+ if (tasks.length === 0) {
1841
+ console.log(chalk5.dim("No background tasks."));
1842
+ return;
1843
+ }
1844
+ console.log(chalk5.bold("\u{1F91D} Background Tasks:\n"));
1845
+ for (const task of tasks) {
1846
+ const elapsed = Math.round((Date.now() - task.startTime.getTime()) / 1e3);
1847
+ const statusIcon = task.status === "running" ? "\u23F3" : task.status === "completed" ? "\u2713" : task.status === "failed" ? "\u2717" : "\u25CB";
1848
+ console.log(` ${statusIcon} [${task.id}] ${task.command}`);
1849
+ console.log(chalk5.dim(` Status: ${task.status}, Elapsed: ${elapsed}s`));
1850
+ console.log();
1851
+ }
1852
+ });
1853
+ program.command("explain <command>").description("Ask Bash Bro to explain a command").action(async (command) => {
1854
+ const bro = new BashBro();
1855
+ await bro.initialize();
1856
+ if (!bro.isOllamaAvailable()) {
1857
+ console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
1858
+ return;
1859
+ }
1860
+ console.log(chalk5.dim("\u{1F91D} Bash Bro is thinking...\n"));
1861
+ const explanation = await bro.aiExplain(command);
1862
+ console.log(explanation);
1863
+ });
1864
+ program.command("fix <command>").description("Ask Bash Bro to fix a failed command").option("-e, --error <message>", "Error message from the failed command").action(async (command, options) => {
1865
+ const bro = new BashBro();
1866
+ await bro.initialize();
1867
+ if (!bro.isOllamaAvailable()) {
1868
+ console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
1869
+ return;
1870
+ }
1871
+ const error = options.error || "Command failed";
1872
+ console.log(chalk5.dim("\u{1F91D} Bash Bro is analyzing...\n"));
1873
+ const fixed = await bro.aiFix(command, error);
1874
+ if (fixed) {
1875
+ console.log(chalk5.green("Suggested fix:"));
1876
+ console.log(chalk5.cyan(` ${fixed}`));
1877
+ } else {
1878
+ console.log(chalk5.yellow("Could not suggest a fix."));
1879
+ }
1880
+ });
1881
+ program.command("ai <prompt>").description("Ask Bash Bro anything").action(async (prompt) => {
1882
+ const bro = new BashBro();
1883
+ await bro.initialize();
1884
+ if (!bro.isOllamaAvailable()) {
1885
+ console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
1886
+ return;
1887
+ }
1888
+ console.log(chalk5.dim("\u{1F91D} Bash Bro is thinking...\n"));
1889
+ const suggestion = await bro.aiSuggest(prompt);
1890
+ if (suggestion) {
1891
+ console.log(chalk5.cyan(suggestion));
1892
+ } else {
1893
+ console.log(chalk5.dim("No suggestion available."));
1894
+ }
1895
+ });
1896
+ program.command("script <description>").description("Generate a shell script from description").option("-o, --output <file>", "Save script to file").action(async (description, options) => {
1897
+ const bro = new BashBro();
1898
+ await bro.initialize();
1899
+ if (!bro.isOllamaAvailable()) {
1900
+ console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
1901
+ return;
1902
+ }
1903
+ console.log(chalk5.dim("\u{1F91D} Bash Bro is generating script...\n"));
1904
+ const script = await bro.aiGenerateScript(description);
1905
+ if (script) {
1906
+ console.log(chalk5.cyan(script));
1907
+ if (options.output) {
1908
+ const { writeFileSync: writeFileSync3 } = await import("fs");
1909
+ writeFileSync3(options.output, script, { mode: 493 });
1910
+ console.log(chalk5.green(`
1911
+ \u2713 Saved to ${options.output}`));
1912
+ }
1913
+ } else {
1914
+ console.log(chalk5.yellow("Could not generate script."));
1915
+ }
1916
+ });
1917
+ program.command("safety <command>").description("Analyze a command for security risks").action(async (command) => {
1918
+ const bro = new BashBro();
1919
+ await bro.initialize();
1920
+ if (!bro.isOllamaAvailable()) {
1921
+ console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
1922
+ return;
1923
+ }
1924
+ console.log(chalk5.dim("\u{1F91D} Bash Bro is analyzing...\n"));
1925
+ const analysis = await bro.aiAnalyzeSafety(command);
1926
+ const riskColors = {
1927
+ low: chalk5.green,
1928
+ medium: chalk5.yellow,
1929
+ high: chalk5.red,
1930
+ critical: chalk5.bgRed.white
1931
+ };
1932
+ const icon = analysis.safe ? "\u2713" : "\u26A0";
1933
+ const color = riskColors[analysis.risk];
1934
+ console.log(`${icon} Risk Level: ${color(analysis.risk.toUpperCase())}`);
1935
+ console.log();
1936
+ console.log(chalk5.bold("Explanation:"));
1937
+ console.log(` ${analysis.explanation}`);
1938
+ if (analysis.suggestions.length > 0) {
1939
+ console.log();
1940
+ console.log(chalk5.bold("Suggestions:"));
1941
+ for (const suggestion of analysis.suggestions) {
1942
+ console.log(` \u2022 ${suggestion}`);
1943
+ }
1944
+ }
1945
+ });
1946
+ program.command("help-ai <topic>").alias("h").description("Get AI help for a command or topic").action(async (topic) => {
1947
+ const bro = new BashBro();
1948
+ await bro.initialize();
1949
+ if (!bro.isOllamaAvailable()) {
1950
+ console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
1951
+ return;
1952
+ }
1953
+ console.log(chalk5.dim("\u{1F91D} Bash Bro is looking that up...\n"));
1954
+ const help = await bro.aiHelp(topic);
1955
+ console.log(help);
1956
+ });
1957
+ program.command("do <description>").description("Convert natural language to a command").option("-x, --execute", "Execute the command after showing it").action(async (description, options) => {
1958
+ const bro = new BashBro();
1959
+ await bro.initialize();
1960
+ if (!bro.isOllamaAvailable()) {
1961
+ console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
1962
+ return;
1963
+ }
1964
+ console.log(chalk5.dim("\u{1F91D} Bash Bro is translating...\n"));
1965
+ const command = await bro.aiToCommand(description);
1966
+ if (command) {
1967
+ console.log(chalk5.bold("Command:"));
1968
+ console.log(chalk5.cyan(` $ ${command}`));
1969
+ if (options.execute) {
1970
+ console.log();
1971
+ console.log(chalk5.dim("Executing..."));
1972
+ const output = await bro.execute(command);
1973
+ console.log(output);
1974
+ }
1975
+ } else {
1976
+ console.log(chalk5.yellow("Could not translate to a command."));
1977
+ }
1978
+ });
1979
+ program.command("models").description("List available Ollama models").action(async () => {
1980
+ console.log(chalk5.cyan(logo));
1981
+ const { OllamaClient } = await import("./ollama-HY35OHW4.js");
1982
+ const ollama = new OllamaClient();
1983
+ const available = await ollama.isAvailable();
1984
+ if (!available) {
1985
+ console.log(chalk5.yellow("Ollama not running. Start Ollama to see available models."));
1986
+ return;
1987
+ }
1988
+ const models = await ollama.listModels();
1989
+ if (models.length === 0) {
1990
+ console.log(chalk5.dim("No models installed. Run: ollama pull qwen2.5-coder:7b"));
1991
+ return;
1992
+ }
1993
+ console.log(chalk5.bold("\u{1F91D} Available Models:\n"));
1994
+ for (const model of models) {
1995
+ const current = model === ollama.getModel() ? chalk5.green(" (current)") : "";
1996
+ console.log(` \u2022 ${model}${current}`);
1997
+ }
1998
+ });
1999
+ var hookCmd = program.command("hook").description("Manage Claude Code hook integration");
2000
+ hookCmd.command("install").description("Install BashBros hooks into Claude Code").action(() => {
2001
+ const result = ClaudeCodeHooks.install();
2002
+ if (result.success) {
2003
+ console.log(chalk5.green("\u2713"), result.message);
2004
+ } else {
2005
+ console.log(chalk5.red("\u2717"), result.message);
2006
+ process.exit(1);
2007
+ }
2008
+ });
2009
+ hookCmd.command("uninstall").description("Remove BashBros hooks from Claude Code").action(() => {
2010
+ const result = ClaudeCodeHooks.uninstall();
2011
+ if (result.success) {
2012
+ console.log(chalk5.green("\u2713"), result.message);
2013
+ } else {
2014
+ console.log(chalk5.red("\u2717"), result.message);
2015
+ process.exit(1);
2016
+ }
2017
+ });
2018
+ hookCmd.command("status").description("Check Claude Code hook status").action(() => {
2019
+ const status = ClaudeCodeHooks.getStatus();
2020
+ console.log();
2021
+ console.log(chalk5.bold("Claude Code Integration Status"));
2022
+ console.log();
2023
+ console.log(` Claude Code: ${status.claudeInstalled ? chalk5.green("installed") : chalk5.yellow("not found")}`);
2024
+ console.log(` BashBros hooks: ${status.hooksInstalled ? chalk5.green("active") : chalk5.dim("not installed")}`);
2025
+ if (status.hooks.length > 0) {
2026
+ console.log(` Active hooks: ${status.hooks.join(", ")}`);
2027
+ }
2028
+ console.log();
2029
+ });
2030
+ var moltbotCmd = program.command("moltbot").alias("clawdbot").description("Manage Moltbot/Clawdbot integration");
2031
+ moltbotCmd.command("install").description("Install BashBros hooks into Moltbot").action(() => {
2032
+ const result = MoltbotHooks.install();
2033
+ if (result.success) {
2034
+ console.log(chalk5.green("\u2713"), result.message);
2035
+ } else {
2036
+ console.log(chalk5.red("\u2717"), result.message);
2037
+ process.exit(1);
2038
+ }
2039
+ });
2040
+ moltbotCmd.command("uninstall").description("Remove BashBros hooks from Moltbot").action(() => {
2041
+ const result = MoltbotHooks.uninstall();
2042
+ if (result.success) {
2043
+ console.log(chalk5.green("\u2713"), result.message);
2044
+ } else {
2045
+ console.log(chalk5.red("\u2717"), result.message);
2046
+ process.exit(1);
2047
+ }
2048
+ });
2049
+ moltbotCmd.command("status").description("Check Moltbot integration status").action(async () => {
2050
+ const status = MoltbotHooks.getStatus();
2051
+ console.log();
2052
+ console.log(chalk5.bold("Moltbot Integration Status"));
2053
+ console.log();
2054
+ if (status.moltbotInstalled) {
2055
+ console.log(` Moltbot: ${chalk5.green("installed")}`);
2056
+ } else if (status.clawdbotInstalled) {
2057
+ console.log(` Clawdbot: ${chalk5.green("installed")} ${chalk5.dim("(legacy)")}`);
2058
+ } else {
2059
+ console.log(` Moltbot: ${chalk5.yellow("not found")}`);
2060
+ }
2061
+ if (status.configPath) {
2062
+ console.log(` Config: ${chalk5.green(status.configPath)}`);
2063
+ } else {
2064
+ console.log(` Config: ${chalk5.dim("not found")}`);
2065
+ }
2066
+ console.log(` BashBros hooks: ${status.hooksInstalled ? chalk5.green("active") : chalk5.dim("not installed")}`);
2067
+ if (status.hooks.length > 0) {
2068
+ console.log(` Active hooks: ${status.hooks.join(", ")}`);
2069
+ }
2070
+ if (status.sandboxMode) {
2071
+ const sandboxColor = status.sandboxMode === "strict" ? chalk5.green : chalk5.yellow;
2072
+ console.log(` Sandbox mode: ${sandboxColor(status.sandboxMode)}`);
2073
+ }
2074
+ console.log();
2075
+ });
2076
+ moltbotCmd.command("gateway").description("Check Moltbot gateway status").action(async () => {
2077
+ console.log();
2078
+ console.log(chalk5.bold("Moltbot Gateway Status"));
2079
+ console.log();
2080
+ const gatewayStatus = await MoltbotHooks.getGatewayStatus();
2081
+ if (gatewayStatus.running) {
2082
+ console.log(` Status: ${chalk5.green("running")}`);
2083
+ console.log(` Host: ${gatewayStatus.host}`);
2084
+ console.log(` Port: ${gatewayStatus.port}`);
2085
+ console.log(` Sandbox: ${gatewayStatus.sandboxMode ? chalk5.green("enabled") : chalk5.yellow("disabled")}`);
2086
+ } else {
2087
+ console.log(` Status: ${chalk5.yellow("not running")}`);
2088
+ console.log(` Expected: ${gatewayStatus.host}:${gatewayStatus.port}`);
2089
+ if (gatewayStatus.error) {
2090
+ console.log(` Error: ${chalk5.dim(gatewayStatus.error)}`);
2091
+ }
2092
+ }
2093
+ console.log();
2094
+ });
2095
+ moltbotCmd.command("audit").description("Run Moltbot security audit").option("--json", "Output as JSON").action(async (options) => {
2096
+ console.log(chalk5.dim("Running security audit...\n"));
2097
+ const result = await MoltbotHooks.runSecurityAudit();
2098
+ if (options.json) {
2099
+ console.log(JSON.stringify(result, null, 2));
2100
+ return;
2101
+ }
2102
+ const statusIcon = result.passed ? chalk5.green("\u2713") : chalk5.red("\u2717");
2103
+ const statusText = result.passed ? chalk5.green("PASSED") : chalk5.red("FAILED");
2104
+ console.log(`${statusIcon} Security Audit: ${statusText}`);
2105
+ console.log();
2106
+ if (result.findings.length === 0) {
2107
+ console.log(chalk5.dim(" No findings."));
2108
+ } else {
2109
+ const severityColors = {
2110
+ critical: chalk5.bgRed.white,
2111
+ warning: chalk5.yellow,
2112
+ info: chalk5.dim
2113
+ };
2114
+ for (const finding of result.findings) {
2115
+ const color = severityColors[finding.severity] || chalk5.white;
2116
+ console.log(` ${color(`[${finding.severity.toUpperCase()}]`)} ${finding.message}`);
2117
+ console.log(chalk5.dim(` Category: ${finding.category}`));
2118
+ if (finding.recommendation) {
2119
+ console.log(chalk5.dim(` Fix: ${finding.recommendation}`));
2120
+ }
2121
+ console.log();
2122
+ }
2123
+ }
2124
+ console.log(chalk5.dim(`Audit completed at ${result.timestamp.toLocaleString()}`));
2125
+ console.log();
2126
+ });
2127
+ program.command("agent-info [agent]").description("Show detailed info about installed agents and their configurations").option("-r, --raw", "Show raw (redacted) config contents").action(async (agent, options) => {
2128
+ console.log(chalk5.cyan(logo));
2129
+ if (agent) {
2130
+ const validAgents = ["claude-code", "moltbot", "clawdbot", "aider", "gemini-cli", "opencode"];
2131
+ if (!validAgents.includes(agent)) {
2132
+ console.log(chalk5.red(`Unknown agent: ${agent}`));
2133
+ console.log(chalk5.dim(`Valid agents: ${validAgents.join(", ")}`));
2134
+ return;
2135
+ }
2136
+ const info = await getAgentConfigInfo(agent);
2137
+ const { formatAgentInfo: formatAgentInfo2 } = await import("./display-IN4NRJJS.js");
2138
+ console.log();
2139
+ console.log(formatAgentInfo2(info));
2140
+ if (options.raw && info.configExists && info.configPath) {
2141
+ const { parseAgentConfig: parseAgentConfig2 } = await import("./config-parser-XHE7BC7H.js");
2142
+ const parsed = await parseAgentConfig2(agent, info.configPath);
2143
+ if (parsed?.rawRedacted) {
2144
+ console.log();
2145
+ console.log(chalk5.bold("Configuration (sensitive data redacted):"));
2146
+ console.log(formatRedactedConfig(parsed.rawRedacted));
2147
+ }
2148
+ }
2149
+ } else {
2150
+ const agents = await getAllAgentConfigs();
2151
+ console.log();
2152
+ console.log(formatAllAgentsInfo(agents));
2153
+ }
2154
+ console.log();
2155
+ });
2156
+ program.command("permissions").description("Show combined permissions view across bashbros and agents").action(async () => {
2157
+ console.log(chalk5.cyan(logo));
2158
+ const agents = await getAllAgentConfigs();
2159
+ const installed = agents.filter((a) => a.installed);
2160
+ if (installed.length === 0) {
2161
+ console.log(chalk5.yellow("No agents installed to compare permissions with."));
2162
+ console.log(chalk5.dim("Install an agent (claude, aider, etc.) to see combined permissions."));
2163
+ return;
2164
+ }
2165
+ console.log();
2166
+ console.log(formatPermissionsTable(agents));
2167
+ console.log();
2168
+ });
2169
+ program.command("gate <command>").description("Check if a command should be allowed (used by hooks)").action(async (command) => {
2170
+ const result = await gateCommand(command);
2171
+ if (!result.allowed) {
2172
+ console.error(`BLOCKED: ${result.reason}`);
2173
+ process.exit(2);
2174
+ }
2175
+ process.exit(0);
2176
+ });
2177
+ program.command("record <command>").description("Record a command execution (used by hooks)").option("-o, --output <output>", "Command output").option("-e, --exit-code <code>", "Exit code", "0").action(async (command, options) => {
2178
+ if (!metricsCollector) metricsCollector = new MetricsCollector();
2179
+ if (!costEstimator) costEstimator = new CostEstimator();
2180
+ if (!loopDetector) loopDetector = new LoopDetector();
2181
+ if (!undoStack) undoStack = new UndoStack();
2182
+ const scorer = new RiskScorer();
2183
+ const risk = scorer.score(command);
2184
+ metricsCollector.record({
2185
+ command,
2186
+ timestamp: /* @__PURE__ */ new Date(),
2187
+ duration: 0,
2188
+ // Not available in hook
2189
+ allowed: true,
2190
+ riskScore: risk,
2191
+ violations: [],
2192
+ exitCode: parseInt(options.exitCode) || 0
2193
+ });
2194
+ costEstimator.recordToolCall(command, options.output || "");
2195
+ const loopAlert = loopDetector.check(command);
2196
+ if (loopAlert) {
2197
+ console.error(chalk5.yellow(`\u26A0 Loop detected: ${loopAlert.message}`));
2198
+ }
2199
+ const paths = command.match(/(?:^|\s)(\.\/|\.\.\/|\/|~\/)[^\s]+/g) || [];
2200
+ const cleanPaths = paths.map((p) => p.trim());
2201
+ if (cleanPaths.length > 0) {
2202
+ undoStack.recordFromCommand(command, cleanPaths);
2203
+ }
2204
+ });
2205
+ program.command("session-end").description("Generate session report (used by hooks)").option("-f, --format <format>", "Output format (text, markdown, json)", "text").action((options) => {
2206
+ if (!metricsCollector) {
2207
+ console.log(chalk5.dim("No session data to report."));
2208
+ return;
2209
+ }
2210
+ const metrics = metricsCollector.getMetrics();
2211
+ const cost = costEstimator?.getEstimate();
2212
+ const report = ReportGenerator.generate(metrics, cost, { format: options.format });
2213
+ console.log();
2214
+ console.log(report);
2215
+ console.log();
2216
+ });
2217
+ program.command("report").description("Generate a session report").option("-f, --format <format>", "Output format (text, markdown, json)", "text").option("--no-cost", "Hide cost estimate").option("--no-risk", "Hide risk distribution").action((options) => {
2218
+ if (!metricsCollector) {
2219
+ console.log(chalk5.dim("No session data. Run some commands first."));
2220
+ return;
2221
+ }
2222
+ const metrics = metricsCollector.getMetrics();
2223
+ const cost = options.cost ? costEstimator?.getEstimate() : void 0;
2224
+ const report = ReportGenerator.generate(metrics, cost, {
2225
+ format: options.format,
2226
+ showCost: options.cost,
2227
+ showRisk: options.risk
2228
+ });
2229
+ console.log();
2230
+ console.log(report);
2231
+ console.log();
2232
+ });
2233
+ program.command("risk <command>").description("Score a command for security risk").action((command) => {
2234
+ const scorer = new RiskScorer();
2235
+ const result = scorer.score(command);
2236
+ const colors = {
2237
+ safe: chalk5.green,
2238
+ caution: chalk5.yellow,
2239
+ dangerous: chalk5.red,
2240
+ critical: chalk5.bgRed.white
2241
+ };
2242
+ const color = colors[result.level];
2243
+ console.log();
2244
+ console.log(` Risk Score: ${color(`${result.score}/10`)} (${color(result.level.toUpperCase())})`);
2245
+ console.log();
2246
+ console.log(chalk5.bold(" Factors:"));
2247
+ for (const factor of result.factors) {
2248
+ console.log(` \u2022 ${factor}`);
2249
+ }
2250
+ console.log();
2251
+ });
2252
+ var undoCmd = program.command("undo").description("Undo file operations");
2253
+ undoCmd.command("last").alias("pop").description("Undo the last file operation").action(() => {
2254
+ if (!undoStack) {
2255
+ console.log(chalk5.dim("No operations to undo."));
2256
+ return;
2257
+ }
2258
+ const result = undoStack.undo();
2259
+ if (result.success) {
2260
+ console.log(chalk5.green("\u2713"), result.message);
2261
+ } else {
2262
+ console.log(chalk5.red("\u2717"), result.message);
2263
+ }
2264
+ });
2265
+ undoCmd.command("all").description("Undo all file operations in session").action(() => {
2266
+ if (!undoStack || undoStack.size() === 0) {
2267
+ console.log(chalk5.dim("No operations to undo."));
2268
+ return;
2269
+ }
2270
+ const results = undoStack.undoAll();
2271
+ let success = 0, failed = 0;
2272
+ for (const result of results) {
2273
+ if (result.success) {
2274
+ console.log(chalk5.green("\u2713"), result.message);
2275
+ success++;
2276
+ } else {
2277
+ console.log(chalk5.red("\u2717"), result.message);
2278
+ failed++;
2279
+ }
2280
+ }
2281
+ console.log();
2282
+ console.log(`Undone: ${success} successful, ${failed} failed`);
2283
+ });
2284
+ undoCmd.command("list").description("Show undo stack").action(() => {
2285
+ if (!undoStack) {
2286
+ undoStack = new UndoStack();
2287
+ }
2288
+ console.log();
2289
+ console.log(undoStack.formatStack());
2290
+ console.log();
2291
+ });
2292
+ var dashboardServer = null;
2293
+ program.command("dashboard").description("Start the BashBros dashboard").option("-p, --port <port>", "Port to run on", "7890").option("-b, --bind <address>", "Address to bind to", "127.0.0.1").action(async (options) => {
2294
+ console.log(chalk5.cyan(logo));
2295
+ console.log(chalk5.dim(" Starting dashboard...\n"));
2296
+ dashboardServer = new DashboardServer({
2297
+ port: parseInt(options.port),
2298
+ bind: options.bind
2299
+ });
2300
+ await dashboardServer.start();
2301
+ console.log(chalk5.green("\u2713"), `Dashboard running at http://${options.bind}:${options.port}`);
2302
+ console.log(chalk5.dim(" Press Ctrl+C to stop"));
2303
+ process.on("SIGINT", async () => {
2304
+ console.log(chalk5.dim("\n Stopping dashboard..."));
2305
+ await dashboardServer?.stop();
2306
+ process.exit(0);
2307
+ });
2308
+ });
2309
+ var wardCmd = program.command("ward").description("Network and connector security");
2310
+ wardCmd.command("status").description("Show ward security status").action(async () => {
2311
+ console.log(chalk5.cyan(logo));
2312
+ console.log(chalk5.bold("Ward Security Status\n"));
2313
+ const scanner = new ExposureScanner();
2314
+ const results = await scanner.scan();
2315
+ if (results.length === 0) {
2316
+ console.log(chalk5.green("\u2713"), "No exposed agent servers detected");
2317
+ } else {
2318
+ console.log(chalk5.yellow("\u26A0"), `Found ${results.length} exposure(s):
2319
+ `);
2320
+ for (const r of results) {
2321
+ const color = r.severity === "critical" ? chalk5.bgRed.white : r.severity === "high" ? chalk5.red : r.severity === "medium" ? chalk5.yellow : chalk5.dim;
2322
+ console.log(` ${color(r.severity.toUpperCase().padEnd(8))} ${r.message}`);
2323
+ }
2324
+ }
2325
+ console.log();
2326
+ });
2327
+ wardCmd.command("scan").description("Run exposure scan").action(async () => {
2328
+ console.log(chalk5.cyan(logo));
2329
+ console.log(chalk5.dim(" Scanning for exposed agent servers...\n"));
2330
+ const scanner = new ExposureScanner();
2331
+ const results = await scanner.scan();
2332
+ if (results.length === 0) {
2333
+ console.log(chalk5.green("\u2713"), "No exposed agent servers detected");
2334
+ } else {
2335
+ for (const r of results) {
2336
+ const icon = r.severity === "critical" ? "\u{1F6A8}" : r.severity === "high" ? "\u26A0\uFE0F" : r.severity === "medium" ? "\u26A1" : "\u2139\uFE0F";
2337
+ console.log(`${icon} ${r.agent}`);
2338
+ console.log(chalk5.dim(` Port: ${r.port}`));
2339
+ console.log(chalk5.dim(` Bind: ${r.bindAddress}`));
2340
+ console.log(chalk5.dim(` Auth: ${r.hasAuth}`));
2341
+ console.log(chalk5.dim(` Severity: ${r.severity}`));
2342
+ console.log(chalk5.dim(` Action: ${r.action}`));
2343
+ console.log();
2344
+ }
2345
+ }
2346
+ });
2347
+ var exposureCmd = wardCmd.command("exposure").description("Exposure scanner commands");
2348
+ exposureCmd.command("list").description("List monitored agents").action(() => {
2349
+ const scanner = new ExposureScanner();
2350
+ const agents = scanner.getAgents();
2351
+ console.log(chalk5.bold("\nMonitored Agent Signatures:\n"));
2352
+ for (const agent of agents) {
2353
+ console.log(` ${chalk5.cyan(agent.name)}`);
2354
+ console.log(chalk5.dim(` Processes: ${agent.processNames.join(", ")}`));
2355
+ console.log(chalk5.dim(` Ports: ${agent.defaultPorts.join(", ")}`));
2356
+ console.log();
2357
+ }
2358
+ });
2359
+ exposureCmd.command("scan").description("Run immediate exposure scan").action(async () => {
2360
+ const scanner = new ExposureScanner();
2361
+ const results = await scanner.scan();
2362
+ console.log(chalk5.bold("\nExposure Scan Results:\n"));
2363
+ if (results.length === 0) {
2364
+ console.log(chalk5.green(" \u2713 No exposures detected"));
2365
+ } else {
2366
+ for (const r of results) {
2367
+ const color = r.severity === "critical" ? chalk5.bgRed.white : r.severity === "high" ? chalk5.red : r.severity === "medium" ? chalk5.yellow : chalk5.green;
2368
+ console.log(` ${color(r.severity.toUpperCase().padEnd(10))} ${r.message}`);
2369
+ }
2370
+ }
2371
+ console.log();
2372
+ });
2373
+ wardCmd.command("blocked").description("Show pending blocked egress items").action(() => {
2374
+ const monitor = new EgressMonitor();
2375
+ const pending = monitor.getPendingBlocks();
2376
+ console.log(chalk5.bold("\nPending Egress Blocks:\n"));
2377
+ if (pending.length === 0) {
2378
+ console.log(chalk5.dim(" No pending blocks"));
2379
+ } else {
2380
+ for (const block of pending) {
2381
+ const severityColor = block.pattern.severity === "critical" ? chalk5.bgRed.white : block.pattern.severity === "high" ? chalk5.red : block.pattern.severity === "medium" ? chalk5.yellow : chalk5.dim;
2382
+ console.log(` ${chalk5.cyan(block.id)} ${severityColor(block.pattern.severity.toUpperCase())}`);
2383
+ console.log(chalk5.dim(` Pattern: ${block.pattern.name} (${block.pattern.category})`));
2384
+ console.log(chalk5.dim(` Matched: ${block.matchedText.substring(0, 50)}${block.matchedText.length > 50 ? "..." : ""}`));
2385
+ if (block.connector) console.log(chalk5.dim(` Connector: ${block.connector}`));
2386
+ if (block.destination) console.log(chalk5.dim(` Destination: ${block.destination}`));
2387
+ console.log();
2388
+ }
2389
+ console.log(chalk5.dim(` Use 'bashbros ward approve <id>' or 'bashbros ward deny <id>' to resolve`));
2390
+ }
2391
+ console.log();
2392
+ });
2393
+ wardCmd.command("approve <id>").description("Approve a blocked egress item").option("--by <name>", "Name of approver", "user").action((id, options) => {
2394
+ const monitor = new EgressMonitor();
2395
+ monitor.approveBlock(id, options.by);
2396
+ console.log(chalk5.green("\u2713"), `Approved block ${id}`);
2397
+ });
2398
+ wardCmd.command("deny <id>").description("Deny a blocked egress item").action((id) => {
2399
+ const monitor = new EgressMonitor();
2400
+ monitor.denyBlock(id);
2401
+ console.log(chalk5.green("\u2713"), `Denied block ${id}`);
2402
+ });
2403
+ var patternsCmd = wardCmd.command("patterns").description("Egress pattern detection commands");
2404
+ patternsCmd.command("list").description("List active detection patterns").option("--category <cat>", "Filter by category (credentials, pii)").action((options) => {
2405
+ const matcher = new EgressPatternMatcher();
2406
+ let patterns = matcher.getPatterns();
2407
+ if (options.category) {
2408
+ patterns = patterns.filter((p) => p.category === options.category);
2409
+ }
2410
+ console.log(chalk5.bold("\nActive Egress Patterns:\n"));
2411
+ const byCategory = {};
2412
+ for (const p of patterns) {
2413
+ const cat = p.category;
2414
+ if (!byCategory[cat]) byCategory[cat] = [];
2415
+ byCategory[cat].push(p);
2416
+ }
2417
+ for (const [category, categoryPatterns] of Object.entries(byCategory)) {
2418
+ console.log(chalk5.cyan(` ${category.toUpperCase()}`));
2419
+ for (const p of categoryPatterns) {
2420
+ const severityColor = p.severity === "critical" ? chalk5.red : p.severity === "high" ? chalk5.yellow : p.severity === "medium" ? chalk5.blue : chalk5.dim;
2421
+ const actionColor = p.action === "block" ? chalk5.red : p.action === "alert" ? chalk5.yellow : chalk5.dim;
2422
+ console.log(` ${chalk5.bold(p.name.padEnd(16))} ${severityColor(p.severity.padEnd(10))} ${actionColor(p.action.padEnd(6))} ${p.description}`);
2423
+ }
2424
+ console.log();
2425
+ }
2426
+ });
2427
+ patternsCmd.command("test <text>").description("Test if text matches any detection pattern").action((text) => {
2428
+ const monitor = new EgressMonitor();
2429
+ const result = monitor.test(text);
2430
+ console.log(chalk5.bold("\nPattern Test Results:\n"));
2431
+ if (result.matches.length === 0) {
2432
+ console.log(chalk5.green(" \u2713 No patterns matched"));
2433
+ } else {
2434
+ console.log(` ${result.blocked ? chalk5.red("WOULD BLOCK") : chalk5.yellow("WOULD ALERT")}`);
2435
+ console.log();
2436
+ console.log(chalk5.bold(" Matches:"));
2437
+ for (const m of result.matches) {
2438
+ const severityColor = m.pattern.severity === "critical" ? chalk5.red : m.pattern.severity === "high" ? chalk5.yellow : m.pattern.severity === "medium" ? chalk5.blue : chalk5.dim;
2439
+ console.log(` ${chalk5.cyan(m.pattern.name)} ${severityColor(`[${m.pattern.severity}]`)} - "${m.matchedText.substring(0, 30)}${m.matchedText.length > 30 ? "..." : ""}"`);
2440
+ }
2441
+ console.log();
2442
+ console.log(chalk5.bold(" Redacted output:"));
2443
+ console.log(chalk5.dim(` ${result.redacted.substring(0, 100)}${result.redacted.length > 100 ? "..." : ""}`));
2444
+ }
2445
+ console.log();
2446
+ });
2447
+ program.parse();
2448
+ //# sourceMappingURL=cli.js.map