bashbros 0.1.2 → 0.1.4

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 (65) hide show
  1. package/README.md +727 -265
  2. package/dist/adapters-JAZGGNVP.js +9 -0
  3. package/dist/chunk-4XZ64P4V.js +47 -0
  4. package/dist/chunk-4XZ64P4V.js.map +1 -0
  5. package/dist/{chunk-XCZMQRSX.js → chunk-7OEWYFN3.js} +745 -541
  6. package/dist/chunk-7OEWYFN3.js.map +1 -0
  7. package/dist/{chunk-SQCP6IYB.js → chunk-CG6VEHJM.js} +3 -2
  8. package/dist/chunk-CG6VEHJM.js.map +1 -0
  9. package/dist/{chunk-DLP2O6PN.js → chunk-EMLEJVJZ.js} +102 -1
  10. package/dist/chunk-EMLEJVJZ.js.map +1 -0
  11. package/dist/chunk-IUUBCPMV.js +166 -0
  12. package/dist/chunk-IUUBCPMV.js.map +1 -0
  13. package/dist/chunk-J6ONXY6N.js +146 -0
  14. package/dist/chunk-J6ONXY6N.js.map +1 -0
  15. package/dist/chunk-KYDMPE4N.js +224 -0
  16. package/dist/chunk-KYDMPE4N.js.map +1 -0
  17. package/dist/chunk-LJE4EPIU.js +56 -0
  18. package/dist/chunk-LJE4EPIU.js.map +1 -0
  19. package/dist/chunk-LZYW7XQO.js +339 -0
  20. package/dist/chunk-LZYW7XQO.js.map +1 -0
  21. package/dist/{chunk-YUMNBQAY.js → chunk-RDNSS3ME.js} +587 -12
  22. package/dist/chunk-RDNSS3ME.js.map +1 -0
  23. package/dist/{chunk-BW6XCOJH.js → chunk-RTZ4QWG2.js} +2 -2
  24. package/dist/chunk-RTZ4QWG2.js.map +1 -0
  25. package/dist/chunk-SDN6TAGD.js +157 -0
  26. package/dist/chunk-SDN6TAGD.js.map +1 -0
  27. package/dist/chunk-T5ONCUHZ.js +198 -0
  28. package/dist/chunk-T5ONCUHZ.js.map +1 -0
  29. package/dist/cli.js +1182 -251
  30. package/dist/cli.js.map +1 -1
  31. package/dist/{config-JLLOTFLI.js → config-I5NCK3RJ.js} +2 -2
  32. package/dist/copilot-cli-5WJWK5YT.js +9 -0
  33. package/dist/{db-OBKEXRTP.js → db-ETWTBXAE.js} +2 -2
  34. package/dist/db-checks-2YOVECD4.js +133 -0
  35. package/dist/db-checks-2YOVECD4.js.map +1 -0
  36. package/dist/{display-6LZ2HBCU.js → display-UH7KEHOW.js} +3 -3
  37. package/dist/display-UH7KEHOW.js.map +1 -0
  38. package/dist/gemini-cli-3563EELZ.js +9 -0
  39. package/dist/gemini-cli-3563EELZ.js.map +1 -0
  40. package/dist/index.d.ts +195 -72
  41. package/dist/index.js +119 -398
  42. package/dist/index.js.map +1 -1
  43. package/dist/{ollama-HY35OHW4.js → ollama-5JVKNFOV.js} +2 -2
  44. package/dist/ollama-5JVKNFOV.js.map +1 -0
  45. package/dist/opencode-DRCY275R.js +9 -0
  46. package/dist/opencode-DRCY275R.js.map +1 -0
  47. package/dist/profiles-7CLN6TAT.js +9 -0
  48. package/dist/profiles-7CLN6TAT.js.map +1 -0
  49. package/dist/setup-YS27MOPE.js +124 -0
  50. package/dist/setup-YS27MOPE.js.map +1 -0
  51. package/dist/static/index.html +4815 -2007
  52. package/dist/store-WJ5Y7MOE.js +9 -0
  53. package/dist/store-WJ5Y7MOE.js.map +1 -0
  54. package/dist/writer-3NAVABN6.js +12 -0
  55. package/dist/writer-3NAVABN6.js.map +1 -0
  56. package/package.json +77 -68
  57. package/dist/chunk-BW6XCOJH.js.map +0 -1
  58. package/dist/chunk-DLP2O6PN.js.map +0 -1
  59. package/dist/chunk-SQCP6IYB.js.map +0 -1
  60. package/dist/chunk-XCZMQRSX.js.map +0 -1
  61. package/dist/chunk-YUMNBQAY.js.map +0 -1
  62. /package/dist/{config-JLLOTFLI.js.map → adapters-JAZGGNVP.js.map} +0 -0
  63. /package/dist/{db-OBKEXRTP.js.map → config-I5NCK3RJ.js.map} +0 -0
  64. /package/dist/{display-6LZ2HBCU.js.map → copilot-cli-5WJWK5YT.js.map} +0 -0
  65. /package/dist/{ollama-HY35OHW4.js.map → db-ETWTBXAE.js.map} +0 -0
@@ -4,10 +4,16 @@ import {
4
4
  } from "./chunk-SG752FZC.js";
5
5
  import {
6
6
  OllamaClient
7
- } from "./chunk-DLP2O6PN.js";
7
+ } from "./chunk-EMLEJVJZ.js";
8
+ import {
9
+ AdapterRegistry
10
+ } from "./chunk-4XZ64P4V.js";
11
+ import {
12
+ ProfileManager
13
+ } from "./chunk-LJE4EPIU.js";
8
14
  import {
9
15
  loadConfig
10
- } from "./chunk-BW6XCOJH.js";
16
+ } from "./chunk-RTZ4QWG2.js";
11
17
  import {
12
18
  PolicyEngine
13
19
  } from "./chunk-QWZGB4V3.js";
@@ -414,202 +420,6 @@ function resetBashgymIntegration() {
414
420
  _integration = null;
415
421
  }
416
422
 
417
- // src/hooks/claude-code.ts
418
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
419
- import { join as join2 } from "path";
420
- import { homedir as homedir2 } from "os";
421
- var CLAUDE_SETTINGS_PATH = join2(homedir2(), ".claude", "settings.json");
422
- var CLAUDE_DIR = join2(homedir2(), ".claude");
423
- var BASHBROS_HOOK_MARKER = "# bashbros-managed";
424
- var ClaudeCodeHooks = class {
425
- /**
426
- * Check if Claude Code is installed
427
- */
428
- static isClaudeInstalled() {
429
- return existsSync2(CLAUDE_DIR);
430
- }
431
- /**
432
- * Load current Claude settings
433
- */
434
- static loadSettings() {
435
- if (!existsSync2(CLAUDE_SETTINGS_PATH)) {
436
- return {};
437
- }
438
- try {
439
- const content = readFileSync2(CLAUDE_SETTINGS_PATH, "utf-8");
440
- return JSON.parse(content);
441
- } catch {
442
- return {};
443
- }
444
- }
445
- /**
446
- * Save Claude settings
447
- */
448
- static saveSettings(settings) {
449
- if (!existsSync2(CLAUDE_DIR)) {
450
- mkdirSync2(CLAUDE_DIR, { recursive: true });
451
- }
452
- writeFileSync2(
453
- CLAUDE_SETTINGS_PATH,
454
- JSON.stringify(settings, null, 2),
455
- "utf-8"
456
- );
457
- }
458
- /**
459
- * Install BashBros hooks into Claude Code
460
- */
461
- static install() {
462
- if (!this.isClaudeInstalled()) {
463
- return {
464
- success: false,
465
- message: "Claude Code not found. Install Claude Code first."
466
- };
467
- }
468
- const settings = this.loadSettings();
469
- if (!settings.hooks) {
470
- settings.hooks = {};
471
- }
472
- if (this.isInstalled(settings)) {
473
- return {
474
- success: true,
475
- message: "BashBros hooks already installed."
476
- };
477
- }
478
- const preToolUseHook = {
479
- matcher: "Bash",
480
- hooks: [{
481
- type: "command",
482
- command: `bashbros gate "$TOOL_INPUT" ${BASHBROS_HOOK_MARKER}`
483
- }]
484
- };
485
- const postToolUseHook = {
486
- matcher: "Bash",
487
- hooks: [{
488
- type: "command",
489
- command: `bashbros record "$TOOL_INPUT" "$TOOL_OUTPUT" ${BASHBROS_HOOK_MARKER}`
490
- }]
491
- };
492
- const sessionEndHook = {
493
- hooks: [{
494
- type: "command",
495
- command: `bashbros session-end ${BASHBROS_HOOK_MARKER}`
496
- }]
497
- };
498
- settings.hooks.PreToolUse = [
499
- ...settings.hooks.PreToolUse || [],
500
- preToolUseHook
501
- ];
502
- settings.hooks.PostToolUse = [
503
- ...settings.hooks.PostToolUse || [],
504
- postToolUseHook
505
- ];
506
- settings.hooks.SessionEnd = [
507
- ...settings.hooks.SessionEnd || [],
508
- sessionEndHook
509
- ];
510
- this.saveSettings(settings);
511
- return {
512
- success: true,
513
- message: "BashBros hooks installed successfully."
514
- };
515
- }
516
- /**
517
- * Uninstall BashBros hooks from Claude Code
518
- */
519
- static uninstall() {
520
- if (!this.isClaudeInstalled()) {
521
- return {
522
- success: false,
523
- message: "Claude Code not found."
524
- };
525
- }
526
- const settings = this.loadSettings();
527
- if (!settings.hooks) {
528
- return {
529
- success: true,
530
- message: "No hooks to uninstall."
531
- };
532
- }
533
- const filterHooks = (hooks) => {
534
- if (!hooks) return [];
535
- return hooks.filter(
536
- (h) => !h.hooks.some((hook) => hook.command.includes(BASHBROS_HOOK_MARKER))
537
- );
538
- };
539
- settings.hooks.PreToolUse = filterHooks(settings.hooks.PreToolUse);
540
- settings.hooks.PostToolUse = filterHooks(settings.hooks.PostToolUse);
541
- settings.hooks.SessionEnd = filterHooks(settings.hooks.SessionEnd);
542
- if (settings.hooks.PreToolUse?.length === 0) delete settings.hooks.PreToolUse;
543
- if (settings.hooks.PostToolUse?.length === 0) delete settings.hooks.PostToolUse;
544
- if (settings.hooks.SessionEnd?.length === 0) delete settings.hooks.SessionEnd;
545
- if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
546
- this.saveSettings(settings);
547
- return {
548
- success: true,
549
- message: "BashBros hooks uninstalled successfully."
550
- };
551
- }
552
- /**
553
- * Check if BashBros hooks are installed
554
- */
555
- static isInstalled(settings) {
556
- const s = settings || this.loadSettings();
557
- if (!s.hooks) return false;
558
- const hasMarker = (hooks) => {
559
- if (!hooks) return false;
560
- return hooks.some(
561
- (h) => h.hooks.some((hook) => hook.command.includes(BASHBROS_HOOK_MARKER))
562
- );
563
- };
564
- return hasMarker(s.hooks.PreToolUse) || hasMarker(s.hooks.PostToolUse) || hasMarker(s.hooks.SessionEnd);
565
- }
566
- /**
567
- * Get hook status
568
- */
569
- static getStatus() {
570
- const claudeInstalled = this.isClaudeInstalled();
571
- const settings = claudeInstalled ? this.loadSettings() : {};
572
- const hooksInstalled = this.isInstalled(settings);
573
- const hooks = [];
574
- if (settings.hooks?.PreToolUse) hooks.push("PreToolUse (gate)");
575
- if (settings.hooks?.PostToolUse) hooks.push("PostToolUse (record)");
576
- if (settings.hooks?.SessionEnd) hooks.push("SessionEnd (report)");
577
- return {
578
- claudeInstalled,
579
- hooksInstalled,
580
- hooks
581
- };
582
- }
583
- };
584
- async function gateCommand(command) {
585
- const { PolicyEngine: PolicyEngine2 } = await import("./engine-EGPAS2EX.js");
586
- const { RiskScorer } = await import("./risk-scorer-Y6KF2XCZ.js");
587
- const { loadConfig: loadConfig2 } = await import("./config-JLLOTFLI.js");
588
- const config = loadConfig2();
589
- const engine = new PolicyEngine2(config);
590
- const scorer = new RiskScorer();
591
- const violations = engine.validate(command);
592
- const risk = scorer.score(command);
593
- if (violations.length > 0) {
594
- return {
595
- allowed: false,
596
- reason: violations[0].message,
597
- riskScore: risk.score
598
- };
599
- }
600
- if (risk.level === "critical") {
601
- return {
602
- allowed: false,
603
- reason: `Critical risk: ${risk.factors.join(", ")}`,
604
- riskScore: risk.score
605
- };
606
- }
607
- return {
608
- allowed: true,
609
- riskScore: risk.score
610
- };
611
- }
612
-
613
423
  // src/core.ts
614
424
  import * as pty from "node-pty";
615
425
  import { EventEmitter as EventEmitter2 } from "events";
@@ -692,32 +502,594 @@ var BashBros = class extends EventEmitter2 {
692
502
  isAllowed(command) {
693
503
  return this.policy.isAllowed(command);
694
504
  }
695
- resize(cols, rows) {
696
- if (this.ptyProcess) {
697
- this.ptyProcess.resize(cols, rows);
505
+ resize(cols, rows) {
506
+ if (this.ptyProcess) {
507
+ this.ptyProcess.resize(cols, rows);
508
+ }
509
+ }
510
+ write(data) {
511
+ if (this.ptyProcess) {
512
+ this.ptyProcess.write(data);
513
+ }
514
+ }
515
+ stop() {
516
+ if (this.ptyProcess) {
517
+ this.ptyProcess.kill();
518
+ this.ptyProcess = null;
519
+ }
520
+ }
521
+ getConfig() {
522
+ return this.config;
523
+ }
524
+ };
525
+
526
+ // src/policy/loop-detector.ts
527
+ var DEFAULT_CONFIG = {
528
+ maxRepeats: 3,
529
+ maxTurns: 100,
530
+ similarityThreshold: 0.85,
531
+ cooldownMs: 1e3,
532
+ windowSize: 20
533
+ };
534
+ var LoopDetector = class {
535
+ config;
536
+ history = [];
537
+ turnCount = 0;
538
+ constructor(config = {}) {
539
+ this.config = { ...DEFAULT_CONFIG, ...config };
540
+ }
541
+ /**
542
+ * Record a command and check for loops
543
+ */
544
+ check(command) {
545
+ const now = Date.now();
546
+ const normalized = this.normalize(command);
547
+ this.turnCount++;
548
+ if (this.turnCount >= this.config.maxTurns) {
549
+ return {
550
+ type: "max_turns",
551
+ command,
552
+ count: this.turnCount,
553
+ message: `Maximum turns reached (${this.config.maxTurns}). Session may be stuck.`
554
+ };
555
+ }
556
+ const exactMatches = this.history.filter((h) => h.command === command);
557
+ if (exactMatches.length >= this.config.maxRepeats) {
558
+ return {
559
+ type: "exact_repeat",
560
+ command,
561
+ count: exactMatches.length + 1,
562
+ message: `Command repeated ${exactMatches.length + 1} times: "${command.slice(0, 50)}..."`
563
+ };
564
+ }
565
+ const lastSame = exactMatches[exactMatches.length - 1];
566
+ if (lastSame && now - lastSame.timestamp < this.config.cooldownMs) {
567
+ return {
568
+ type: "exact_repeat",
569
+ command,
570
+ count: 2,
571
+ message: `Rapid repeat detected (${now - lastSame.timestamp}ms apart)`
572
+ };
573
+ }
574
+ const recentWindow = this.history.slice(-this.config.windowSize);
575
+ const similarCount = recentWindow.filter(
576
+ (h) => this.similarity(h.normalized, normalized) >= this.config.similarityThreshold
577
+ ).length;
578
+ if (similarCount >= this.config.maxRepeats) {
579
+ return {
580
+ type: "semantic_repeat",
581
+ command,
582
+ count: similarCount + 1,
583
+ message: `Similar commands repeated ${similarCount + 1} times`
584
+ };
585
+ }
586
+ const baseCommand = command.split(/\s+/)[0];
587
+ const toolCount = recentWindow.filter(
588
+ (h) => h.command.split(/\s+/)[0] === baseCommand
589
+ ).length;
590
+ if (toolCount >= this.config.maxRepeats * 2) {
591
+ return {
592
+ type: "tool_hammering",
593
+ command,
594
+ count: toolCount + 1,
595
+ message: `Tool "${baseCommand}" called ${toolCount + 1} times in last ${this.config.windowSize} commands`
596
+ };
597
+ }
598
+ this.history.push({ command, timestamp: now, normalized });
599
+ if (this.history.length > this.config.windowSize * 2) {
600
+ this.history = this.history.slice(-this.config.windowSize);
601
+ }
602
+ return null;
603
+ }
604
+ /**
605
+ * Normalize command for comparison
606
+ */
607
+ normalize(command) {
608
+ return command.toLowerCase().replace(/["']/g, "").replace(/\s+/g, " ").replace(/\d+/g, "N").replace(/[a-f0-9]{8,}/gi, "H").trim();
609
+ }
610
+ /**
611
+ * Calculate similarity between two strings (Jaccard index on words)
612
+ */
613
+ similarity(a, b) {
614
+ const wordsA = new Set(a.split(/\s+/));
615
+ const wordsB = new Set(b.split(/\s+/));
616
+ const intersection = new Set([...wordsA].filter((x) => wordsB.has(x)));
617
+ const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
618
+ if (union.size === 0) return 1;
619
+ return intersection.size / union.size;
620
+ }
621
+ /**
622
+ * Get current turn count
623
+ */
624
+ getTurnCount() {
625
+ return this.turnCount;
626
+ }
627
+ /**
628
+ * Get command frequency map
629
+ */
630
+ getFrequencyMap() {
631
+ const freq = /* @__PURE__ */ new Map();
632
+ for (const entry of this.history) {
633
+ const base = entry.command.split(/\s+/)[0];
634
+ freq.set(base, (freq.get(base) || 0) + 1);
635
+ }
636
+ return freq;
637
+ }
638
+ /**
639
+ * Reset detector state
640
+ */
641
+ reset() {
642
+ this.history = [];
643
+ this.turnCount = 0;
644
+ }
645
+ /**
646
+ * Get stats for reporting
647
+ */
648
+ getStats() {
649
+ const freq = this.getFrequencyMap();
650
+ const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
651
+ return {
652
+ turnCount: this.turnCount,
653
+ uniqueCommands: freq.size,
654
+ topCommands: sorted.slice(0, 5)
655
+ };
656
+ }
657
+ };
658
+
659
+ // src/policy/anomaly-detector.ts
660
+ var DEFAULT_CONFIG2 = {
661
+ workingHours: [6, 22],
662
+ // 6 AM to 10 PM
663
+ typicalCommandsPerMinute: 30,
664
+ knownPaths: [],
665
+ suspiciousPatterns: [],
666
+ enabled: true
667
+ };
668
+ var DEFAULT_SUSPICIOUS_PATTERNS = [
669
+ /\bpasswd\b/,
670
+ /\bshadow\b/,
671
+ /\/root\//,
672
+ /\.ssh\//,
673
+ /\.gnupg\//,
674
+ /\.aws\//,
675
+ /\.kube\//,
676
+ /wallet/i,
677
+ /crypto/i,
678
+ /bitcoin/i,
679
+ /ethereum/i,
680
+ /private.*key/i
681
+ ];
682
+ var AnomalyDetector = class {
683
+ config;
684
+ commands = [];
685
+ baselinePaths = /* @__PURE__ */ new Set();
686
+ baselineCommands = /* @__PURE__ */ new Set();
687
+ learningMode = true;
688
+ learningCount = 0;
689
+ LEARNING_THRESHOLD = 50;
690
+ constructor(config = {}) {
691
+ this.config = { ...DEFAULT_CONFIG2, ...config };
692
+ }
693
+ /**
694
+ * Check a command for anomalies
695
+ */
696
+ check(command, cwd) {
697
+ if (!this.config.enabled) return [];
698
+ const anomalies = [];
699
+ const now = Date.now();
700
+ this.commands.push({ command, timestamp: now, path: cwd });
701
+ if (this.commands.length > 1e3) {
702
+ this.commands = this.commands.slice(-500);
703
+ }
704
+ if (this.learningMode) {
705
+ this.learn(command, cwd);
706
+ if (this.learningCount >= this.LEARNING_THRESHOLD) {
707
+ this.learningMode = false;
708
+ }
709
+ return anomalies;
710
+ }
711
+ const timingAnomaly = this.checkTiming(now);
712
+ if (timingAnomaly) anomalies.push(timingAnomaly);
713
+ const freqAnomaly = this.checkFrequency(now);
714
+ if (freqAnomaly) anomalies.push(freqAnomaly);
715
+ if (cwd) {
716
+ const pathAnomaly = this.checkPath(cwd);
717
+ if (pathAnomaly) anomalies.push(pathAnomaly);
718
+ }
719
+ const patternAnomalies = this.checkPatterns(command);
720
+ anomalies.push(...patternAnomalies);
721
+ const behaviorAnomaly = this.checkBehavior(command);
722
+ if (behaviorAnomaly) anomalies.push(behaviorAnomaly);
723
+ return anomalies;
724
+ }
725
+ /**
726
+ * Learn from command (build baseline)
727
+ */
728
+ learn(command, cwd) {
729
+ this.learningCount++;
730
+ const baseCmd = command.split(/\s+/)[0];
731
+ this.baselineCommands.add(baseCmd);
732
+ if (cwd) {
733
+ this.baselinePaths.add(cwd);
734
+ const parts = cwd.split(/[/\\]/);
735
+ for (let i = 1; i <= parts.length; i++) {
736
+ this.baselinePaths.add(parts.slice(0, i).join("/"));
737
+ }
738
+ }
739
+ }
740
+ /**
741
+ * Check for timing anomalies
742
+ */
743
+ checkTiming(now) {
744
+ const hour = new Date(now).getHours();
745
+ const [start, end] = this.config.workingHours;
746
+ if (hour < start || hour >= end) {
747
+ return {
748
+ type: "timing",
749
+ severity: "info",
750
+ message: `Activity outside normal hours (${hour}:00)`,
751
+ details: { hour, workingHours: this.config.workingHours }
752
+ };
753
+ }
754
+ return null;
755
+ }
756
+ /**
757
+ * Check for frequency anomalies
758
+ */
759
+ checkFrequency(now) {
760
+ const oneMinuteAgo = now - 6e4;
761
+ const recentCommands = this.commands.filter((c) => c.timestamp > oneMinuteAgo);
762
+ const rate = recentCommands.length;
763
+ if (rate > this.config.typicalCommandsPerMinute * 2) {
764
+ return {
765
+ type: "frequency",
766
+ severity: "warning",
767
+ message: `High command rate: ${rate}/min (typical: ${this.config.typicalCommandsPerMinute})`,
768
+ details: { rate, typical: this.config.typicalCommandsPerMinute }
769
+ };
770
+ }
771
+ const fiveSecondsAgo = now - 5e3;
772
+ const burstCommands = this.commands.filter((c) => c.timestamp > fiveSecondsAgo);
773
+ if (burstCommands.length > 10) {
774
+ return {
775
+ type: "frequency",
776
+ severity: "alert",
777
+ message: `Burst detected: ${burstCommands.length} commands in 5 seconds`,
778
+ details: { count: burstCommands.length, window: "5s" }
779
+ };
780
+ }
781
+ return null;
782
+ }
783
+ /**
784
+ * Check for unusual path access
785
+ */
786
+ checkPath(path) {
787
+ if (this.config.knownPaths.length > 0) {
788
+ const isKnown = this.config.knownPaths.some(
789
+ (p) => path.startsWith(p) || path.includes(p)
790
+ );
791
+ if (!isKnown) {
792
+ return {
793
+ type: "path",
794
+ severity: "warning",
795
+ message: `Access to unexpected path: ${path}`,
796
+ details: { path, knownPaths: this.config.knownPaths }
797
+ };
798
+ }
799
+ }
800
+ if (!this.learningMode && this.baselinePaths.size > 0) {
801
+ const isBaseline = this.baselinePaths.has(path) || [...this.baselinePaths].some((p) => path.startsWith(p));
802
+ if (!isBaseline) {
803
+ return {
804
+ type: "path",
805
+ severity: "info",
806
+ message: `New path accessed: ${path}`,
807
+ details: { path, isNew: true }
808
+ };
809
+ }
810
+ }
811
+ return null;
812
+ }
813
+ /**
814
+ * Check for suspicious patterns
815
+ */
816
+ checkPatterns(command) {
817
+ const anomalies = [];
818
+ const allPatterns = [...DEFAULT_SUSPICIOUS_PATTERNS, ...this.config.suspiciousPatterns];
819
+ for (const pattern of allPatterns) {
820
+ if (pattern.test(command)) {
821
+ anomalies.push({
822
+ type: "pattern",
823
+ severity: "warning",
824
+ message: `Suspicious pattern detected: ${pattern.source}`,
825
+ details: { command: command.slice(0, 100), pattern: pattern.source }
826
+ });
827
+ }
828
+ }
829
+ return anomalies;
830
+ }
831
+ /**
832
+ * Check for behavioral anomalies
833
+ */
834
+ checkBehavior(command) {
835
+ const baseCmd = command.split(/\s+/)[0];
836
+ if (!this.learningMode && this.baselineCommands.size > 0) {
837
+ if (!this.baselineCommands.has(baseCmd)) {
838
+ const sensitiveCommands = /* @__PURE__ */ new Set([
839
+ "curl",
840
+ "wget",
841
+ "nc",
842
+ "netcat",
843
+ "ssh",
844
+ "scp",
845
+ "rsync",
846
+ "sudo",
847
+ "su",
848
+ "chmod",
849
+ "chown",
850
+ "mount",
851
+ "umount"
852
+ ]);
853
+ if (sensitiveCommands.has(baseCmd)) {
854
+ return {
855
+ type: "behavior",
856
+ severity: "warning",
857
+ message: `New sensitive command type: ${baseCmd}`,
858
+ details: { command: baseCmd, isNew: true }
859
+ };
860
+ }
861
+ }
862
+ }
863
+ return null;
864
+ }
865
+ /**
866
+ * Get anomaly stats
867
+ */
868
+ getStats() {
869
+ const now = Date.now();
870
+ const oneMinuteAgo = now - 6e4;
871
+ const recentRate = this.commands.filter((c) => c.timestamp > oneMinuteAgo).length;
872
+ return {
873
+ learningMode: this.learningMode,
874
+ learningProgress: Math.min(100, Math.round(this.learningCount / this.LEARNING_THRESHOLD * 100)),
875
+ baselineCommands: this.baselineCommands.size,
876
+ baselinePaths: this.baselinePaths.size,
877
+ recentCommandRate: recentRate
878
+ };
879
+ }
880
+ /**
881
+ * Force end learning mode
882
+ */
883
+ endLearning() {
884
+ this.learningMode = false;
885
+ }
886
+ /**
887
+ * Reset and restart learning
888
+ */
889
+ reset() {
890
+ this.commands = [];
891
+ this.baselinePaths.clear();
892
+ this.baselineCommands.clear();
893
+ this.learningMode = true;
894
+ this.learningCount = 0;
895
+ }
896
+ };
897
+
898
+ // src/policy/output-scanner.ts
899
+ var SECRET_PATTERNS = [
900
+ // API Keys
901
+ { pattern: /sk-[A-Za-z0-9]{20,}/, name: "OpenAI API Key" },
902
+ { pattern: /sk-ant-[A-Za-z0-9\-]{20,}/, name: "Anthropic API Key" },
903
+ { pattern: /ghp_[A-Za-z0-9]{36}/, name: "GitHub Token" },
904
+ { pattern: /gho_[A-Za-z0-9]{36}/, name: "GitHub OAuth Token" },
905
+ { pattern: /github_pat_[A-Za-z0-9_]{22,}/, name: "GitHub PAT" },
906
+ { pattern: /glpat-[A-Za-z0-9\-]{20,}/, name: "GitLab Token" },
907
+ { pattern: /xox[baprs]-[A-Za-z0-9\-]{10,}/, name: "Slack Token" },
908
+ { pattern: /sk_live_[A-Za-z0-9]{24,}/, name: "Stripe Secret Key" },
909
+ { pattern: /sq0atp-[A-Za-z0-9\-_]{22,}/, name: "Square Token" },
910
+ { pattern: /AKIA[A-Z0-9]{16}/, name: "AWS Access Key" },
911
+ { pattern: /amzn\.mws\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/, name: "Amazon MWS Key" },
912
+ // OAuth/JWT
913
+ { pattern: /Bearer\s+[A-Za-z0-9\-._~+/]+=*/, name: "Bearer Token" },
914
+ { pattern: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/, name: "JWT Token" },
915
+ // Credentials in output
916
+ { pattern: /password\s*[=:]\s*['"]?[^\s'"]{4,}['"]?/i, name: "Password" },
917
+ { pattern: /passwd\s*[=:]\s*['"]?[^\s'"]{4,}['"]?/i, name: "Password" },
918
+ { pattern: /api[_-]?key\s*[=:]\s*['"]?[^\s'"]{8,}['"]?/i, name: "API Key" },
919
+ { pattern: /secret\s*[=:]\s*['"]?[^\s'"]{8,}['"]?/i, name: "Secret" },
920
+ { pattern: /token\s*[=:]\s*['"]?[^\s'"]{8,}['"]?/i, name: "Token" },
921
+ // Private keys
922
+ { pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/, name: "Private Key" },
923
+ { pattern: /-----BEGIN\s+EC\s+PRIVATE\s+KEY-----/, name: "EC Private Key" },
924
+ { pattern: /-----BEGIN\s+OPENSSH\s+PRIVATE\s+KEY-----/, name: "SSH Private Key" },
925
+ { pattern: /-----BEGIN\s+PGP\s+PRIVATE\s+KEY\s+BLOCK-----/, name: "PGP Private Key" },
926
+ // Database URLs
927
+ { pattern: /mongodb(\+srv)?:\/\/[^:]+:[^@]+@/, name: "MongoDB Connection String" },
928
+ { pattern: /postgres(ql)?:\/\/[^:]+:[^@]+@/, name: "PostgreSQL Connection String" },
929
+ { pattern: /mysql:\/\/[^:]+:[^@]+@/, name: "MySQL Connection String" },
930
+ { pattern: /redis:\/\/[^:]+:[^@]+@/, name: "Redis Connection String" },
931
+ // SSH
932
+ { pattern: /ssh-rsa\s+[A-Za-z0-9+/]+[=]{0,2}/, name: "SSH Public Key" },
933
+ { pattern: /ssh-ed25519\s+[A-Za-z0-9+/]+/, name: "SSH ED25519 Key" }
934
+ ];
935
+ var ERROR_PATTERNS = [
936
+ { pattern: /EACCES|EPERM|permission denied/i, name: "Permission Error" },
937
+ { pattern: /ENOENT|no such file|not found/i, name: "File Not Found" },
938
+ { pattern: /ECONNREFUSED|connection refused/i, name: "Connection Refused" },
939
+ { pattern: /ETIMEDOUT|timed out/i, name: "Timeout Error" },
940
+ { pattern: /segmentation fault|core dumped/i, name: "Crash" },
941
+ { pattern: /out of memory|OOM|cannot allocate/i, name: "Memory Error" },
942
+ { pattern: /stack trace|traceback|at\s+\S+:\d+:\d+/i, name: "Stack Trace" },
943
+ { pattern: /error:|fatal:|failed:/i, name: "Error Message" }
944
+ ];
945
+ var OutputScanner = class {
946
+ secretPatterns;
947
+ redactPatterns;
948
+ policy;
949
+ constructor(policy) {
950
+ this.policy = policy;
951
+ this.secretPatterns = SECRET_PATTERNS.map((p) => p.pattern);
952
+ this.redactPatterns = (policy.redactPatterns || []).map((p) => {
953
+ try {
954
+ return new RegExp(p, "gi");
955
+ } catch {
956
+ return null;
957
+ }
958
+ }).filter((p) => p !== null);
959
+ }
960
+ /**
961
+ * Scan output for secrets and sensitive data
962
+ */
963
+ scan(output) {
964
+ if (!this.policy.enabled) {
965
+ return {
966
+ hasSecrets: false,
967
+ hasErrors: false,
968
+ redactedOutput: output,
969
+ findings: []
970
+ };
971
+ }
972
+ const findings = [];
973
+ let hasSecrets = false;
974
+ let hasErrors = false;
975
+ let processedOutput = output;
976
+ if (output.length > this.policy.maxOutputLength) {
977
+ processedOutput = output.slice(0, this.policy.maxOutputLength) + "\n... [truncated]";
978
+ }
979
+ if (this.policy.scanForSecrets) {
980
+ const secretFindings = this.scanForSecrets(processedOutput);
981
+ if (secretFindings.length > 0) {
982
+ hasSecrets = true;
983
+ findings.push(...secretFindings);
984
+ }
985
+ }
986
+ if (this.policy.scanForErrors) {
987
+ const errorFindings = this.scanForErrors(processedOutput);
988
+ if (errorFindings.length > 0) {
989
+ hasErrors = true;
990
+ findings.push(...errorFindings);
991
+ }
992
+ }
993
+ const redactedOutput = this.redact(processedOutput);
994
+ return {
995
+ hasSecrets,
996
+ hasErrors,
997
+ redactedOutput,
998
+ findings
999
+ };
1000
+ }
1001
+ /**
1002
+ * Scan for secrets in output
1003
+ */
1004
+ scanForSecrets(output) {
1005
+ const findings = [];
1006
+ const lines = output.split("\n");
1007
+ for (let i = 0; i < lines.length; i++) {
1008
+ const line = lines[i];
1009
+ for (const { pattern, name } of SECRET_PATTERNS) {
1010
+ if (pattern.test(line)) {
1011
+ findings.push({
1012
+ type: "secret",
1013
+ pattern: name,
1014
+ message: `Potential ${name} found in output`,
1015
+ line: i + 1
1016
+ });
1017
+ }
1018
+ }
1019
+ }
1020
+ return findings;
1021
+ }
1022
+ /**
1023
+ * Scan for error patterns in output
1024
+ */
1025
+ scanForErrors(output) {
1026
+ const findings = [];
1027
+ const lines = output.split("\n");
1028
+ for (let i = 0; i < lines.length; i++) {
1029
+ const line = lines[i];
1030
+ for (const { pattern, name } of ERROR_PATTERNS) {
1031
+ if (pattern.test(line)) {
1032
+ findings.push({
1033
+ type: "error",
1034
+ pattern: name,
1035
+ message: `${name} detected`,
1036
+ line: i + 1
1037
+ });
1038
+ break;
1039
+ }
1040
+ }
698
1041
  }
1042
+ return findings;
699
1043
  }
700
- write(data) {
701
- if (this.ptyProcess) {
702
- this.ptyProcess.write(data);
1044
+ /**
1045
+ * Redact sensitive data from output
1046
+ */
1047
+ redact(output) {
1048
+ let redacted = output;
1049
+ for (const { pattern, name } of SECRET_PATTERNS) {
1050
+ redacted = redacted.replace(new RegExp(pattern.source, "g"), `[REDACTED ${name}]`);
703
1051
  }
1052
+ for (const pattern of this.redactPatterns) {
1053
+ redacted = redacted.replace(pattern, "[REDACTED]");
1054
+ }
1055
+ return redacted;
704
1056
  }
705
- stop() {
706
- if (this.ptyProcess) {
707
- this.ptyProcess.kill();
708
- this.ptyProcess = null;
1057
+ /**
1058
+ * Check if output contains any secrets
1059
+ */
1060
+ hasSecrets(output) {
1061
+ for (const pattern of this.secretPatterns) {
1062
+ if (pattern.test(output)) {
1063
+ return true;
1064
+ }
709
1065
  }
1066
+ return false;
710
1067
  }
711
- getConfig() {
712
- return this.config;
1068
+ /**
1069
+ * Get summary of findings
1070
+ */
1071
+ static summarize(findings) {
1072
+ if (findings.length === 0) {
1073
+ return "No issues found";
1074
+ }
1075
+ const secrets = findings.filter((f) => f.type === "secret");
1076
+ const errors = findings.filter((f) => f.type === "error");
1077
+ const parts = [];
1078
+ if (secrets.length > 0) {
1079
+ parts.push(`${secrets.length} potential secret(s)`);
1080
+ }
1081
+ if (errors.length > 0) {
1082
+ parts.push(`${errors.length} error(s)`);
1083
+ }
1084
+ return parts.join(", ");
713
1085
  }
714
1086
  };
715
1087
 
716
1088
  // src/bro/profiler.ts
717
1089
  import { execFileSync } from "child_process";
718
- import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync } from "fs";
719
- import { homedir as homedir3, platform, arch, cpus, totalmem } from "os";
720
- import { join as join3 } from "path";
1090
+ import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "fs";
1091
+ import { homedir as homedir2, platform, arch, cpus, totalmem } from "os";
1092
+ import { join as join2 } from "path";
721
1093
  var SAFE_VERSION_COMMANDS = {
722
1094
  python: ["--version"],
723
1095
  python3: ["--version"],
@@ -750,7 +1122,7 @@ var SystemProfiler = class {
750
1122
  profile = null;
751
1123
  profilePath;
752
1124
  constructor() {
753
- this.profilePath = join3(homedir3(), ".bashbros", "system-profile.json");
1125
+ this.profilePath = join2(homedir2(), ".bashbros", "system-profile.json");
754
1126
  }
755
1127
  async scan() {
756
1128
  const profile = {
@@ -934,8 +1306,8 @@ var SystemProfiler = class {
934
1306
  ["composer.json", "php"]
935
1307
  ];
936
1308
  for (const [file, type] of checks) {
937
- const filePath = join3(projectPath, file);
938
- if (existsSync3(filePath)) {
1309
+ const filePath = join2(projectPath, file);
1310
+ if (existsSync2(filePath)) {
939
1311
  try {
940
1312
  const realPath = realpathSync(filePath);
941
1313
  if (realPath.startsWith(realpathSync(projectPath))) {
@@ -949,24 +1321,24 @@ var SystemProfiler = class {
949
1321
  }
950
1322
  detectDependencies(projectPath) {
951
1323
  const deps = [];
952
- const pkgPath = join3(projectPath, "package.json");
953
- if (existsSync3(pkgPath)) {
1324
+ const pkgPath = join2(projectPath, "package.json");
1325
+ if (existsSync2(pkgPath)) {
954
1326
  try {
955
1327
  const realPkgPath = realpathSync(pkgPath);
956
1328
  if (realPkgPath.startsWith(realpathSync(projectPath))) {
957
- const pkg = JSON.parse(readFileSync3(realPkgPath, "utf-8"));
1329
+ const pkg = JSON.parse(readFileSync2(realPkgPath, "utf-8"));
958
1330
  deps.push(...Object.keys(pkg.dependencies || {}));
959
1331
  deps.push(...Object.keys(pkg.devDependencies || {}));
960
1332
  }
961
1333
  } catch {
962
1334
  }
963
1335
  }
964
- const reqPath = join3(projectPath, "requirements.txt");
965
- if (existsSync3(reqPath)) {
1336
+ const reqPath = join2(projectPath, "requirements.txt");
1337
+ if (existsSync2(reqPath)) {
966
1338
  try {
967
1339
  const realReqPath = realpathSync(reqPath);
968
1340
  if (realReqPath.startsWith(realpathSync(projectPath))) {
969
- const reqs = readFileSync3(realReqPath, "utf-8");
1341
+ const reqs = readFileSync2(realReqPath, "utf-8");
970
1342
  const packages = reqs.split("\n").map((line) => line.split(/[=<>]/)[0].trim()).filter(Boolean);
971
1343
  deps.push(...packages);
972
1344
  }
@@ -976,9 +1348,9 @@ var SystemProfiler = class {
976
1348
  return deps.slice(0, 100);
977
1349
  }
978
1350
  load() {
979
- if (existsSync3(this.profilePath)) {
1351
+ if (existsSync2(this.profilePath)) {
980
1352
  try {
981
- const data = readFileSync3(this.profilePath, "utf-8");
1353
+ const data = readFileSync2(this.profilePath, "utf-8");
982
1354
  this.profile = JSON.parse(data);
983
1355
  return this.profile;
984
1356
  } catch {
@@ -989,13 +1361,13 @@ var SystemProfiler = class {
989
1361
  }
990
1362
  save() {
991
1363
  try {
992
- const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync4, chmodSync } = __require("fs");
993
- const dir = join3(homedir3(), ".bashbros");
994
- if (!existsSync3(dir)) {
995
- mkdirSync4(dir, { recursive: true, mode: 448 });
1364
+ const { writeFileSync: writeFileSync3, mkdirSync: mkdirSync3, chmodSync } = __require("fs");
1365
+ const dir = join2(homedir2(), ".bashbros");
1366
+ if (!existsSync2(dir)) {
1367
+ mkdirSync3(dir, { recursive: true, mode: 448 });
996
1368
  }
997
1369
  const filePath = this.profilePath;
998
- writeFileSync4(filePath, JSON.stringify(this.profile, null, 2));
1370
+ writeFileSync3(filePath, JSON.stringify(this.profile, null, 2));
999
1371
  try {
1000
1372
  chmodSync(filePath, 384);
1001
1373
  } catch {
@@ -1043,8 +1415,10 @@ var SystemProfiler = class {
1043
1415
  var TaskRouter = class {
1044
1416
  rules;
1045
1417
  profile;
1046
- constructor(profile = null) {
1418
+ ollama;
1419
+ constructor(profile = null, ollama = null) {
1047
1420
  this.profile = profile;
1421
+ this.ollama = ollama;
1048
1422
  this.rules = this.buildDefaultRules();
1049
1423
  }
1050
1424
  buildDefaultRules() {
@@ -1124,6 +1498,25 @@ var TaskRouter = class {
1124
1498
  confidence: 0.5
1125
1499
  };
1126
1500
  }
1501
+ async routeAsync(command) {
1502
+ const patternResult = this.route(command);
1503
+ if (patternResult.confidence >= 0.7) {
1504
+ return patternResult;
1505
+ }
1506
+ if (!this.ollama) {
1507
+ return patternResult;
1508
+ }
1509
+ try {
1510
+ const prompt = `Classify this command as one of: bro (simple, local task), main (complex, needs reasoning), both (can run in background). Command: "${command}". Respond with ONLY one word: bro, main, or both.`;
1511
+ const response = await this.ollama.generate(prompt, "You are a command classifier. Respond with exactly one word: bro, main, or both.");
1512
+ const decision = response.trim().toLowerCase();
1513
+ if (["bro", "main", "both"].includes(decision)) {
1514
+ return { decision, reason: "AI classification", confidence: 0.8 };
1515
+ }
1516
+ } catch {
1517
+ }
1518
+ return { decision: "main", reason: "AI fallback - defaulting to main", confidence: 0.5 };
1519
+ }
1127
1520
  looksSimple(command) {
1128
1521
  const words = command.split(/\s+/);
1129
1522
  if (words.length <= 3) return true;
@@ -1152,8 +1545,11 @@ var CommandSuggester = class {
1152
1545
  history = [];
1153
1546
  profile = null;
1154
1547
  patterns = /* @__PURE__ */ new Map();
1155
- constructor(profile = null) {
1548
+ ollama;
1549
+ aiCache = /* @__PURE__ */ new Map();
1550
+ constructor(profile = null, ollama = null) {
1156
1551
  this.profile = profile;
1552
+ this.ollama = ollama;
1157
1553
  this.initPatterns();
1158
1554
  }
1159
1555
  initPatterns() {
@@ -1186,6 +1582,34 @@ var CommandSuggester = class {
1186
1582
  const unique = this.dedupeAndRank(suggestions);
1187
1583
  return unique.slice(0, 5);
1188
1584
  }
1585
+ async suggestAsync(context) {
1586
+ const suggestions = this.suggest(context);
1587
+ if (!this.ollama) return suggestions;
1588
+ const cacheKey = JSON.stringify({ lc: context.lastCommand, lo: context.lastOutput?.slice(0, 100) });
1589
+ const cached = this.aiCache.get(cacheKey);
1590
+ if (cached && cached.expiry > Date.now()) {
1591
+ suggestions.push(...cached.suggestions);
1592
+ return this.dedupeAndRank(suggestions).slice(0, 5);
1593
+ }
1594
+ try {
1595
+ const contextStr = `Last command: ${context.lastCommand || "none"}
1596
+ Output: ${(context.lastOutput || "").slice(0, 200)}
1597
+ Project: ${context.projectType || "unknown"}`;
1598
+ const aiSuggestion = await this.ollama.suggestCommand(contextStr);
1599
+ if (aiSuggestion) {
1600
+ const aiSuggestions = [{
1601
+ command: aiSuggestion,
1602
+ description: "AI suggestion",
1603
+ confidence: 0.75,
1604
+ source: "model"
1605
+ }];
1606
+ this.aiCache.set(cacheKey, { suggestions: aiSuggestions, expiry: Date.now() + 5 * 60 * 1e3 });
1607
+ suggestions.push(...aiSuggestions);
1608
+ }
1609
+ } catch {
1610
+ }
1611
+ return this.dedupeAndRank(suggestions).slice(0, 5);
1612
+ }
1189
1613
  suggestFromPatterns(lastCommand) {
1190
1614
  const suggestions = [];
1191
1615
  for (const [key, commands] of this.patterns) {
@@ -1604,6 +2028,9 @@ var BashBro = class extends EventEmitter4 {
1604
2028
  config;
1605
2029
  ollamaAvailable = false;
1606
2030
  bashgymModelVersion = null;
2031
+ adapterRegistry;
2032
+ profileManager;
2033
+ activeProfile = null;
1607
2034
  constructor(config = {}) {
1608
2035
  super();
1609
2036
  this.config = {
@@ -1615,8 +2042,6 @@ var BashBro = class extends EventEmitter4 {
1615
2042
  ...config
1616
2043
  };
1617
2044
  this.profiler = new SystemProfiler();
1618
- this.router = new TaskRouter();
1619
- this.suggester = new CommandSuggester();
1620
2045
  this.worker = new BackgroundWorker();
1621
2046
  if (this.config.enableOllama) {
1622
2047
  this.ollama = new OllamaClient({
@@ -1624,12 +2049,19 @@ var BashBro = class extends EventEmitter4 {
1624
2049
  model: this.config.modelName
1625
2050
  });
1626
2051
  }
2052
+ this.router = new TaskRouter(null, this.ollama);
2053
+ this.suggester = new CommandSuggester(null, this.ollama);
1627
2054
  this.worker.on("complete", (data) => this.emit("task:complete", data));
1628
2055
  this.worker.on("output", (data) => this.emit("task:output", data));
1629
2056
  this.worker.on("error", (data) => this.emit("task:error", data));
1630
2057
  if (this.config.enableBashgymIntegration) {
1631
2058
  this.initBashgymIntegration();
1632
2059
  }
2060
+ this.adapterRegistry = new AdapterRegistry();
2061
+ this.profileManager = new ProfileManager();
2062
+ if (this.config.activeProfile) {
2063
+ this.activeProfile = this.profileManager.load(this.config.activeProfile);
2064
+ }
1633
2065
  }
1634
2066
  /**
1635
2067
  * Initialize bashgym integration for model hot-swap
@@ -1712,6 +2144,22 @@ var BashBro = class extends EventEmitter4 {
1712
2144
  }
1713
2145
  return this.suggester.suggest(context);
1714
2146
  }
2147
+ /**
2148
+ * AI-enhanced async routing - uses pattern matching first, falls back to Ollama
2149
+ */
2150
+ async routeAsync(command) {
2151
+ if (!this.config.enableRouting) {
2152
+ return { decision: "main", reason: "Routing disabled", confidence: 1 };
2153
+ }
2154
+ return this.router.routeAsync(command);
2155
+ }
2156
+ /**
2157
+ * AI-enhanced async suggestions - pattern matching + Ollama suggestions with caching
2158
+ */
2159
+ async suggestAsync(context) {
2160
+ if (!this.config.enableSuggestions) return [];
2161
+ return this.suggester.suggestAsync(context);
2162
+ }
1715
2163
  /**
1716
2164
  * SECURITY FIX: Safe command execution with validation
1717
2165
  */
@@ -1996,6 +2444,41 @@ var BashBro = class extends EventEmitter4 {
1996
2444
  }
1997
2445
  return false;
1998
2446
  }
2447
+ /**
2448
+ * Get model name for a specific purpose (checks active profile for adapter override)
2449
+ */
2450
+ getModelForPurpose(purpose) {
2451
+ if (!this.activeProfile) return null;
2452
+ return this.profileManager.getModelForPurpose(this.activeProfile, purpose);
2453
+ }
2454
+ /**
2455
+ * Get discovered LoRA adapters
2456
+ */
2457
+ getAdapters() {
2458
+ return this.adapterRegistry.discover();
2459
+ }
2460
+ /**
2461
+ * Get available model profiles
2462
+ */
2463
+ getProfiles() {
2464
+ return this.profileManager.list();
2465
+ }
2466
+ /**
2467
+ * Get the active model profile
2468
+ */
2469
+ getActiveProfile() {
2470
+ return this.activeProfile;
2471
+ }
2472
+ /**
2473
+ * Set the active model profile by name
2474
+ */
2475
+ setActiveProfile(name) {
2476
+ const profile = this.profileManager.load(name);
2477
+ if (!profile) return false;
2478
+ this.activeProfile = profile;
2479
+ this.emit("profile:changed", profile);
2480
+ return true;
2481
+ }
1999
2482
  // Format a nice status message
2000
2483
  status() {
2001
2484
  const lines = [
@@ -2050,151 +2533,6 @@ var BashBro = class extends EventEmitter4 {
2050
2533
  }
2051
2534
  };
2052
2535
 
2053
- // src/observability/metrics.ts
2054
- var MetricsCollector = class {
2055
- sessionId;
2056
- startTime;
2057
- commands = [];
2058
- filesModified = /* @__PURE__ */ new Set();
2059
- pathsAccessed = /* @__PURE__ */ new Set();
2060
- constructor() {
2061
- this.sessionId = this.generateSessionId();
2062
- this.startTime = /* @__PURE__ */ new Date();
2063
- }
2064
- generateSessionId() {
2065
- const now = /* @__PURE__ */ new Date();
2066
- const date = now.toISOString().slice(0, 10).replace(/-/g, "");
2067
- const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
2068
- const rand = Math.random().toString(36).slice(2, 6);
2069
- return `${date}-${time}-${rand}`;
2070
- }
2071
- /**
2072
- * Record a command execution
2073
- */
2074
- record(metric) {
2075
- this.commands.push(metric);
2076
- const paths = this.extractPaths(metric.command);
2077
- for (const path of paths) {
2078
- this.pathsAccessed.add(path);
2079
- }
2080
- if (this.isWriteCommand(metric.command)) {
2081
- for (const path of paths) {
2082
- this.filesModified.add(path);
2083
- }
2084
- }
2085
- }
2086
- /**
2087
- * Get current session metrics
2088
- */
2089
- getMetrics() {
2090
- const now = /* @__PURE__ */ new Date();
2091
- const duration = now.getTime() - this.startTime.getTime();
2092
- const riskDist = { safe: 0, caution: 0, dangerous: 0, critical: 0 };
2093
- let totalRisk = 0;
2094
- for (const cmd of this.commands) {
2095
- riskDist[cmd.riskScore.level]++;
2096
- totalRisk += cmd.riskScore.score;
2097
- }
2098
- const cmdFreq = /* @__PURE__ */ new Map();
2099
- for (const cmd of this.commands) {
2100
- const base = cmd.command.split(/\s+/)[0];
2101
- cmdFreq.set(base, (cmdFreq.get(base) || 0) + 1);
2102
- }
2103
- const topCommands = [...cmdFreq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
2104
- const violationsByType = {};
2105
- for (const cmd of this.commands) {
2106
- for (const v of cmd.violations) {
2107
- violationsByType[v.type] = (violationsByType[v.type] || 0) + 1;
2108
- }
2109
- }
2110
- const totalExecTime = this.commands.reduce((sum, c) => sum + c.duration, 0);
2111
- const avgExecTime = this.commands.length > 0 ? totalExecTime / this.commands.length : 0;
2112
- return {
2113
- sessionId: this.sessionId,
2114
- startTime: this.startTime,
2115
- duration,
2116
- commandCount: this.commands.length,
2117
- blockedCount: this.commands.filter((c) => !c.allowed).length,
2118
- uniqueCommands: cmdFreq.size,
2119
- topCommands,
2120
- riskDistribution: riskDist,
2121
- avgRiskScore: this.commands.length > 0 ? totalRisk / this.commands.length : 0,
2122
- avgExecutionTime: avgExecTime,
2123
- totalExecutionTime: totalExecTime,
2124
- filesModified: [...this.filesModified],
2125
- pathsAccessed: [...this.pathsAccessed],
2126
- violationsByType
2127
- };
2128
- }
2129
- /**
2130
- * Extract paths from a command
2131
- */
2132
- extractPaths(command) {
2133
- const paths = [];
2134
- const tokens = command.split(/\s+/);
2135
- for (const token of tokens) {
2136
- if (token.startsWith("-")) continue;
2137
- if (token.startsWith("/") || token.startsWith("./") || token.startsWith("../") || token.startsWith("~/") || token.includes(".")) {
2138
- paths.push(token);
2139
- }
2140
- }
2141
- return paths;
2142
- }
2143
- /**
2144
- * Check if command modifies files
2145
- */
2146
- isWriteCommand(command) {
2147
- const writePatterns = [
2148
- /^(vim|vi|nano|emacs|code)\s/,
2149
- /^(touch|mkdir|cp|mv|rm)\s/,
2150
- /^(echo|cat|printf).*>/,
2151
- /^(git\s+(add|commit|checkout|reset))/,
2152
- /^(npm|yarn|pnpm)\s+(install|uninstall)/,
2153
- /^(pip|pip3)\s+(install|uninstall)/,
2154
- /^chmod\s/,
2155
- /^chown\s/
2156
- ];
2157
- return writePatterns.some((p) => p.test(command));
2158
- }
2159
- /**
2160
- * Get recent commands
2161
- */
2162
- getRecentCommands(n = 10) {
2163
- return this.commands.slice(-n);
2164
- }
2165
- /**
2166
- * Get blocked commands
2167
- */
2168
- getBlockedCommands() {
2169
- return this.commands.filter((c) => !c.allowed);
2170
- }
2171
- /**
2172
- * Get high-risk commands
2173
- */
2174
- getHighRiskCommands(threshold = 6) {
2175
- return this.commands.filter((c) => c.riskScore.score >= threshold);
2176
- }
2177
- /**
2178
- * Format duration for display
2179
- */
2180
- static formatDuration(ms) {
2181
- if (ms < 1e3) return `${ms}ms`;
2182
- if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
2183
- if (ms < 36e5) return `${Math.floor(ms / 6e4)}m ${Math.floor(ms % 6e4 / 1e3)}s`;
2184
- return `${Math.floor(ms / 36e5)}h ${Math.floor(ms % 36e5 / 6e4)}m`;
2185
- }
2186
- /**
2187
- * Reset collector
2188
- */
2189
- reset() {
2190
- this.sessionId = this.generateSessionId();
2191
- this.startTime = /* @__PURE__ */ new Date();
2192
- this.commands = [];
2193
- this.filesModified.clear();
2194
- this.pathsAccessed.clear();
2195
- }
2196
- };
2197
-
2198
2536
  // src/observability/cost.ts
2199
2537
  var MODEL_PRICING = {
2200
2538
  "claude-opus-4": { inputPer1k: 0.015, outputPer1k: 0.075 },
@@ -2539,15 +2877,15 @@ var ReportGenerator = class {
2539
2877
  };
2540
2878
 
2541
2879
  // src/safety/undo-stack.ts
2542
- import { existsSync as existsSync4, unlinkSync, mkdirSync as mkdirSync3, copyFileSync, readdirSync, statSync } from "fs";
2543
- import { join as join4, dirname } from "path";
2544
- import { homedir as homedir4 } from "os";
2545
- var DEFAULT_CONFIG = {
2880
+ import { existsSync as existsSync3, unlinkSync, mkdirSync as mkdirSync2, copyFileSync, readdirSync, statSync } from "fs";
2881
+ import { join as join3, dirname } from "path";
2882
+ import { homedir as homedir3 } from "os";
2883
+ var DEFAULT_CONFIG3 = {
2546
2884
  maxStackSize: 100,
2547
2885
  maxFileSize: 10 * 1024 * 1024,
2548
2886
  // 10MB
2549
2887
  ttlMinutes: 60,
2550
- backupPath: join4(homedir4(), ".bashbros", "undo"),
2888
+ backupPath: join3(homedir3(), ".bashbros", "undo"),
2551
2889
  enabled: true
2552
2890
  };
2553
2891
  var UndoStack = class {
@@ -2556,13 +2894,13 @@ var UndoStack = class {
2556
2894
  config;
2557
2895
  undoDir;
2558
2896
  constructor(policy) {
2559
- this.config = { ...DEFAULT_CONFIG };
2897
+ this.config = { ...DEFAULT_CONFIG3 };
2560
2898
  if (policy) {
2561
2899
  if (typeof policy.maxStackSize === "number") this.config.maxStackSize = policy.maxStackSize;
2562
2900
  if (typeof policy.maxFileSize === "number") this.config.maxFileSize = policy.maxFileSize;
2563
2901
  if (typeof policy.ttlMinutes === "number") this.config.ttlMinutes = policy.ttlMinutes;
2564
2902
  if (typeof policy.backupPath === "string") {
2565
- this.config.backupPath = policy.backupPath.replace("~", homedir4());
2903
+ this.config.backupPath = policy.backupPath.replace("~", homedir3());
2566
2904
  }
2567
2905
  if (typeof policy.enabled === "boolean") this.config.enabled = policy.enabled;
2568
2906
  }
@@ -2572,8 +2910,8 @@ var UndoStack = class {
2572
2910
  this.cleanupOldBackups();
2573
2911
  }
2574
2912
  ensureUndoDir() {
2575
- if (!existsSync4(this.undoDir)) {
2576
- mkdirSync3(this.undoDir, { recursive: true, mode: 448 });
2913
+ if (!existsSync3(this.undoDir)) {
2914
+ mkdirSync2(this.undoDir, { recursive: true, mode: 448 });
2577
2915
  }
2578
2916
  }
2579
2917
  /**
@@ -2587,7 +2925,7 @@ var UndoStack = class {
2587
2925
  const files = readdirSync(this.undoDir);
2588
2926
  for (const file of files) {
2589
2927
  if (!file.endsWith(".backup")) continue;
2590
- const filePath = join4(this.undoDir, file);
2928
+ const filePath = join3(this.undoDir, file);
2591
2929
  try {
2592
2930
  const stats = statSync(filePath);
2593
2931
  if (stats.mtimeMs < cutoff) {
@@ -2628,7 +2966,7 @@ var UndoStack = class {
2628
2966
  * Record a file modification (backs up original)
2629
2967
  */
2630
2968
  recordModify(path, command) {
2631
- if (!this.config.enabled || !existsSync4(path)) {
2969
+ if (!this.config.enabled || !existsSync3(path)) {
2632
2970
  return null;
2633
2971
  }
2634
2972
  const stats = statSync(path);
@@ -2644,7 +2982,7 @@ var UndoStack = class {
2644
2982
  return entry;
2645
2983
  }
2646
2984
  const id = this.generateId();
2647
- const backupPath = join4(this.undoDir, `${id}.backup`);
2985
+ const backupPath = join3(this.undoDir, `${id}.backup`);
2648
2986
  try {
2649
2987
  copyFileSync(path, backupPath);
2650
2988
  const entry = {
@@ -2665,7 +3003,7 @@ var UndoStack = class {
2665
3003
  * Record a file deletion (backs up content)
2666
3004
  */
2667
3005
  recordDelete(path, command) {
2668
- if (!this.config.enabled || !existsSync4(path)) {
3006
+ if (!this.config.enabled || !existsSync3(path)) {
2669
3007
  return null;
2670
3008
  }
2671
3009
  const stats = statSync(path);
@@ -2681,7 +3019,7 @@ var UndoStack = class {
2681
3019
  return entry;
2682
3020
  }
2683
3021
  const id = this.generateId();
2684
- const backupPath = join4(this.undoDir, `${id}.backup`);
3022
+ const backupPath = join3(this.undoDir, `${id}.backup`);
2685
3023
  try {
2686
3024
  copyFileSync(path, backupPath);
2687
3025
  const entry = {
@@ -2708,11 +3046,11 @@ var UndoStack = class {
2708
3046
  const isModify = /^(sed|awk|vim|vi|nano|code)\s/.test(command) || /^(echo|cat).*>>/.test(command);
2709
3047
  for (const path of paths) {
2710
3048
  let entry = null;
2711
- if (isDelete && existsSync4(path)) {
3049
+ if (isDelete && existsSync3(path)) {
2712
3050
  entry = this.recordDelete(path, command);
2713
- } else if (isModify && existsSync4(path)) {
3051
+ } else if (isModify && existsSync3(path)) {
2714
3052
  entry = this.recordModify(path, command);
2715
- } else if (isCreate && !existsSync4(path)) {
3053
+ } else if (isCreate && !existsSync3(path)) {
2716
3054
  entry = this.recordCreate(path, command);
2717
3055
  }
2718
3056
  if (entry) {
@@ -2738,7 +3076,7 @@ var UndoStack = class {
2738
3076
  try {
2739
3077
  switch (entry.operation) {
2740
3078
  case "create":
2741
- if (existsSync4(entry.path)) {
3079
+ if (existsSync3(entry.path)) {
2742
3080
  unlinkSync(entry.path);
2743
3081
  return {
2744
3082
  success: true,
@@ -2752,7 +3090,7 @@ var UndoStack = class {
2752
3090
  entry
2753
3091
  };
2754
3092
  case "modify":
2755
- if (entry.backupPath && existsSync4(entry.backupPath)) {
3093
+ if (entry.backupPath && existsSync3(entry.backupPath)) {
2756
3094
  copyFileSync(entry.backupPath, entry.path);
2757
3095
  return {
2758
3096
  success: true,
@@ -2766,10 +3104,10 @@ var UndoStack = class {
2766
3104
  entry
2767
3105
  };
2768
3106
  case "delete":
2769
- if (entry.backupPath && existsSync4(entry.backupPath)) {
3107
+ if (entry.backupPath && existsSync3(entry.backupPath)) {
2770
3108
  const dir = dirname(entry.path);
2771
- if (!existsSync4(dir)) {
2772
- mkdirSync3(dir, { recursive: true });
3109
+ if (!existsSync3(dir)) {
3110
+ mkdirSync2(dir, { recursive: true });
2773
3111
  }
2774
3112
  copyFileSync(entry.backupPath, entry.path);
2775
3113
  return {
@@ -2825,7 +3163,7 @@ var UndoStack = class {
2825
3163
  */
2826
3164
  clear() {
2827
3165
  for (const entry of this.stack) {
2828
- if (entry.backupPath && existsSync4(entry.backupPath)) {
3166
+ if (entry.backupPath && existsSync3(entry.backupPath)) {
2829
3167
  try {
2830
3168
  unlinkSync(entry.backupPath);
2831
3169
  } catch {
@@ -2841,7 +3179,7 @@ var UndoStack = class {
2841
3179
  this.stack.push(entry);
2842
3180
  if (this.stack.length > this.config.maxStackSize) {
2843
3181
  const removed = this.stack.shift();
2844
- if (removed?.backupPath && existsSync4(removed.backupPath)) {
3182
+ if (removed?.backupPath && existsSync3(removed.backupPath)) {
2845
3183
  try {
2846
3184
  unlinkSync(removed.backupPath);
2847
3185
  } catch {
@@ -2871,155 +3209,21 @@ var UndoStack = class {
2871
3209
  }
2872
3210
  };
2873
3211
 
2874
- // src/policy/loop-detector.ts
2875
- var DEFAULT_CONFIG2 = {
2876
- maxRepeats: 3,
2877
- maxTurns: 100,
2878
- similarityThreshold: 0.85,
2879
- cooldownMs: 1e3,
2880
- windowSize: 20
2881
- };
2882
- var LoopDetector = class {
2883
- config;
2884
- history = [];
2885
- turnCount = 0;
2886
- constructor(config = {}) {
2887
- this.config = { ...DEFAULT_CONFIG2, ...config };
2888
- }
2889
- /**
2890
- * Record a command and check for loops
2891
- */
2892
- check(command) {
2893
- const now = Date.now();
2894
- const normalized = this.normalize(command);
2895
- this.turnCount++;
2896
- if (this.turnCount >= this.config.maxTurns) {
2897
- return {
2898
- type: "max_turns",
2899
- command,
2900
- count: this.turnCount,
2901
- message: `Maximum turns reached (${this.config.maxTurns}). Session may be stuck.`
2902
- };
2903
- }
2904
- const exactMatches = this.history.filter((h) => h.command === command);
2905
- if (exactMatches.length >= this.config.maxRepeats) {
2906
- return {
2907
- type: "exact_repeat",
2908
- command,
2909
- count: exactMatches.length + 1,
2910
- message: `Command repeated ${exactMatches.length + 1} times: "${command.slice(0, 50)}..."`
2911
- };
2912
- }
2913
- const lastSame = exactMatches[exactMatches.length - 1];
2914
- if (lastSame && now - lastSame.timestamp < this.config.cooldownMs) {
2915
- return {
2916
- type: "exact_repeat",
2917
- command,
2918
- count: 2,
2919
- message: `Rapid repeat detected (${now - lastSame.timestamp}ms apart)`
2920
- };
2921
- }
2922
- const recentWindow = this.history.slice(-this.config.windowSize);
2923
- const similarCount = recentWindow.filter(
2924
- (h) => this.similarity(h.normalized, normalized) >= this.config.similarityThreshold
2925
- ).length;
2926
- if (similarCount >= this.config.maxRepeats) {
2927
- return {
2928
- type: "semantic_repeat",
2929
- command,
2930
- count: similarCount + 1,
2931
- message: `Similar commands repeated ${similarCount + 1} times`
2932
- };
2933
- }
2934
- const baseCommand = command.split(/\s+/)[0];
2935
- const toolCount = recentWindow.filter(
2936
- (h) => h.command.split(/\s+/)[0] === baseCommand
2937
- ).length;
2938
- if (toolCount >= this.config.maxRepeats * 2) {
2939
- return {
2940
- type: "tool_hammering",
2941
- command,
2942
- count: toolCount + 1,
2943
- message: `Tool "${baseCommand}" called ${toolCount + 1} times in last ${this.config.windowSize} commands`
2944
- };
2945
- }
2946
- this.history.push({ command, timestamp: now, normalized });
2947
- if (this.history.length > this.config.windowSize * 2) {
2948
- this.history = this.history.slice(-this.config.windowSize);
2949
- }
2950
- return null;
2951
- }
2952
- /**
2953
- * Normalize command for comparison
2954
- */
2955
- normalize(command) {
2956
- return command.toLowerCase().replace(/["']/g, "").replace(/\s+/g, " ").replace(/\d+/g, "N").replace(/[a-f0-9]{8,}/gi, "H").trim();
2957
- }
2958
- /**
2959
- * Calculate similarity between two strings (Jaccard index on words)
2960
- */
2961
- similarity(a, b) {
2962
- const wordsA = new Set(a.split(/\s+/));
2963
- const wordsB = new Set(b.split(/\s+/));
2964
- const intersection = new Set([...wordsA].filter((x) => wordsB.has(x)));
2965
- const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
2966
- if (union.size === 0) return 1;
2967
- return intersection.size / union.size;
2968
- }
2969
- /**
2970
- * Get current turn count
2971
- */
2972
- getTurnCount() {
2973
- return this.turnCount;
2974
- }
2975
- /**
2976
- * Get command frequency map
2977
- */
2978
- getFrequencyMap() {
2979
- const freq = /* @__PURE__ */ new Map();
2980
- for (const entry of this.history) {
2981
- const base = entry.command.split(/\s+/)[0];
2982
- freq.set(base, (freq.get(base) || 0) + 1);
2983
- }
2984
- return freq;
2985
- }
2986
- /**
2987
- * Reset detector state
2988
- */
2989
- reset() {
2990
- this.history = [];
2991
- this.turnCount = 0;
2992
- }
2993
- /**
2994
- * Get stats for reporting
2995
- */
2996
- getStats() {
2997
- const freq = this.getFrequencyMap();
2998
- const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
2999
- return {
3000
- turnCount: this.turnCount,
3001
- uniqueCommands: freq.size,
3002
- topCommands: sorted.slice(0, 5)
3003
- };
3004
- }
3005
- };
3006
-
3007
3212
  export {
3008
3213
  BashgymIntegration,
3009
3214
  getBashgymIntegration,
3010
3215
  resetBashgymIntegration,
3011
- ClaudeCodeHooks,
3012
- gateCommand,
3013
3216
  BashBros,
3217
+ LoopDetector,
3218
+ AnomalyDetector,
3219
+ OutputScanner,
3014
3220
  SystemProfiler,
3015
3221
  TaskRouter,
3016
3222
  CommandSuggester,
3017
3223
  BackgroundWorker,
3018
3224
  BashBro,
3019
- MetricsCollector,
3020
3225
  CostEstimator,
3021
3226
  ReportGenerator,
3022
- UndoStack,
3023
- LoopDetector
3227
+ UndoStack
3024
3228
  };
3025
- //# sourceMappingURL=chunk-XCZMQRSX.js.map
3229
+ //# sourceMappingURL=chunk-7OEWYFN3.js.map