bashbros 0.1.3 → 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.
- package/README.md +727 -265
- package/dist/adapters-JAZGGNVP.js +9 -0
- package/dist/chunk-4XZ64P4V.js +47 -0
- package/dist/chunk-4XZ64P4V.js.map +1 -0
- package/dist/{chunk-2RPTM6EQ.js → chunk-7OEWYFN3.js} +745 -629
- package/dist/chunk-7OEWYFN3.js.map +1 -0
- package/dist/{chunk-WPJJZLT6.js → chunk-CG6VEHJM.js} +3 -2
- package/dist/chunk-CG6VEHJM.js.map +1 -0
- package/dist/{chunk-DLP2O6PN.js → chunk-EMLEJVJZ.js} +102 -1
- package/dist/chunk-EMLEJVJZ.js.map +1 -0
- package/dist/chunk-IUUBCPMV.js +166 -0
- package/dist/chunk-IUUBCPMV.js.map +1 -0
- package/dist/chunk-J6ONXY6N.js +146 -0
- package/dist/chunk-J6ONXY6N.js.map +1 -0
- package/dist/{chunk-EYO44OMN.js → chunk-KYDMPE4N.js} +60 -17
- package/dist/chunk-KYDMPE4N.js.map +1 -0
- package/dist/chunk-LJE4EPIU.js +56 -0
- package/dist/chunk-LJE4EPIU.js.map +1 -0
- package/dist/chunk-LZYW7XQO.js +339 -0
- package/dist/chunk-LZYW7XQO.js.map +1 -0
- package/dist/{chunk-JYWQT2B4.js → chunk-RDNSS3ME.js} +489 -14
- package/dist/chunk-RDNSS3ME.js.map +1 -0
- package/dist/{chunk-A535VV7N.js → chunk-RTZ4QWG2.js} +5 -4
- package/dist/chunk-RTZ4QWG2.js.map +1 -0
- package/dist/chunk-SDN6TAGD.js +157 -0
- package/dist/chunk-SDN6TAGD.js.map +1 -0
- package/dist/chunk-T5ONCUHZ.js +198 -0
- package/dist/chunk-T5ONCUHZ.js.map +1 -0
- package/dist/cli.js +1069 -88
- package/dist/cli.js.map +1 -1
- package/dist/{config-43SK6SFI.js → config-I5NCK3RJ.js} +2 -2
- package/dist/copilot-cli-5WJWK5YT.js +9 -0
- package/dist/{db-SWJUUSFX.js → db-ETWTBXAE.js} +2 -2
- package/dist/db-checks-2YOVECD4.js +133 -0
- package/dist/db-checks-2YOVECD4.js.map +1 -0
- package/dist/{display-HFIFXOOL.js → display-UH7KEHOW.js} +3 -3
- package/dist/gemini-cli-3563EELZ.js +9 -0
- package/dist/gemini-cli-3563EELZ.js.map +1 -0
- package/dist/index.d.ts +176 -72
- package/dist/index.js +119 -398
- package/dist/index.js.map +1 -1
- package/dist/{ollama-HY35OHW4.js → ollama-5JVKNFOV.js} +2 -2
- package/dist/ollama-5JVKNFOV.js.map +1 -0
- package/dist/opencode-DRCY275R.js +9 -0
- package/dist/opencode-DRCY275R.js.map +1 -0
- package/dist/profiles-7CLN6TAT.js +9 -0
- package/dist/profiles-7CLN6TAT.js.map +1 -0
- package/dist/setup-YS27MOPE.js +124 -0
- package/dist/setup-YS27MOPE.js.map +1 -0
- package/dist/static/index.html +4815 -2007
- package/dist/store-WJ5Y7MOE.js +9 -0
- package/dist/store-WJ5Y7MOE.js.map +1 -0
- package/dist/{writer-4ZEAKUFD.js → writer-3NAVABN6.js} +3 -3
- package/dist/writer-3NAVABN6.js.map +1 -0
- package/package.json +77 -68
- package/dist/chunk-2RPTM6EQ.js.map +0 -1
- package/dist/chunk-A535VV7N.js.map +0 -1
- package/dist/chunk-DLP2O6PN.js.map +0 -1
- package/dist/chunk-EYO44OMN.js.map +0 -1
- package/dist/chunk-JYWQT2B4.js.map +0 -1
- package/dist/chunk-WPJJZLT6.js.map +0 -1
- /package/dist/{config-43SK6SFI.js.map → adapters-JAZGGNVP.js.map} +0 -0
- /package/dist/{db-SWJUUSFX.js.map → config-I5NCK3RJ.js.map} +0 -0
- /package/dist/{display-HFIFXOOL.js.map → copilot-cli-5WJWK5YT.js.map} +0 -0
- /package/dist/{ollama-HY35OHW4.js.map → db-ETWTBXAE.js.map} +0 -0
- /package/dist/{writer-4ZEAKUFD.js.map → display-UH7KEHOW.js.map} +0 -0
|
@@ -4,10 +4,16 @@ import {
|
|
|
4
4
|
} from "./chunk-SG752FZC.js";
|
|
5
5
|
import {
|
|
6
6
|
OllamaClient
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
16
|
+
} from "./chunk-RTZ4QWG2.js";
|
|
11
17
|
import {
|
|
12
18
|
PolicyEngine
|
|
13
19
|
} from "./chunk-QWZGB4V3.js";
|
|
@@ -414,290 +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 BASHBROS_ALL_TOOLS_MARKER = "--marker=bashbros-all-tools";
|
|
425
|
-
var ClaudeCodeHooks = class {
|
|
426
|
-
/**
|
|
427
|
-
* Check if Claude Code is installed
|
|
428
|
-
*/
|
|
429
|
-
static isClaudeInstalled() {
|
|
430
|
-
return existsSync2(CLAUDE_DIR);
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
* Load current Claude settings
|
|
434
|
-
*/
|
|
435
|
-
static loadSettings() {
|
|
436
|
-
if (!existsSync2(CLAUDE_SETTINGS_PATH)) {
|
|
437
|
-
return {};
|
|
438
|
-
}
|
|
439
|
-
try {
|
|
440
|
-
const content = readFileSync2(CLAUDE_SETTINGS_PATH, "utf-8");
|
|
441
|
-
return JSON.parse(content);
|
|
442
|
-
} catch {
|
|
443
|
-
return {};
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
/**
|
|
447
|
-
* Save Claude settings
|
|
448
|
-
*/
|
|
449
|
-
static saveSettings(settings) {
|
|
450
|
-
if (!existsSync2(CLAUDE_DIR)) {
|
|
451
|
-
mkdirSync2(CLAUDE_DIR, { recursive: true });
|
|
452
|
-
}
|
|
453
|
-
writeFileSync2(
|
|
454
|
-
CLAUDE_SETTINGS_PATH,
|
|
455
|
-
JSON.stringify(settings, null, 2),
|
|
456
|
-
"utf-8"
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Install BashBros hooks into Claude Code
|
|
461
|
-
*/
|
|
462
|
-
static install() {
|
|
463
|
-
if (!this.isClaudeInstalled()) {
|
|
464
|
-
return {
|
|
465
|
-
success: false,
|
|
466
|
-
message: "Claude Code not found. Install Claude Code first."
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
const settings = this.loadSettings();
|
|
470
|
-
if (!settings.hooks) {
|
|
471
|
-
settings.hooks = {};
|
|
472
|
-
}
|
|
473
|
-
if (this.isInstalled(settings)) {
|
|
474
|
-
return {
|
|
475
|
-
success: true,
|
|
476
|
-
message: "BashBros hooks already installed."
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
const preToolUseHook = {
|
|
480
|
-
matcher: "Bash",
|
|
481
|
-
hooks: [{
|
|
482
|
-
type: "command",
|
|
483
|
-
command: `bashbros gate "$TOOL_INPUT" ${BASHBROS_HOOK_MARKER}`
|
|
484
|
-
}]
|
|
485
|
-
};
|
|
486
|
-
const postToolUseHook = {
|
|
487
|
-
matcher: "Bash",
|
|
488
|
-
hooks: [{
|
|
489
|
-
type: "command",
|
|
490
|
-
command: `bashbros record "$TOOL_INPUT" "$TOOL_OUTPUT" ${BASHBROS_HOOK_MARKER}`
|
|
491
|
-
}]
|
|
492
|
-
};
|
|
493
|
-
const sessionEndHook = {
|
|
494
|
-
hooks: [{
|
|
495
|
-
type: "command",
|
|
496
|
-
command: `bashbros session-end ${BASHBROS_HOOK_MARKER}`
|
|
497
|
-
}]
|
|
498
|
-
};
|
|
499
|
-
settings.hooks.PreToolUse = [
|
|
500
|
-
...settings.hooks.PreToolUse || [],
|
|
501
|
-
preToolUseHook
|
|
502
|
-
];
|
|
503
|
-
settings.hooks.PostToolUse = [
|
|
504
|
-
...settings.hooks.PostToolUse || [],
|
|
505
|
-
postToolUseHook
|
|
506
|
-
];
|
|
507
|
-
settings.hooks.SessionEnd = [
|
|
508
|
-
...settings.hooks.SessionEnd || [],
|
|
509
|
-
sessionEndHook
|
|
510
|
-
];
|
|
511
|
-
this.saveSettings(settings);
|
|
512
|
-
return {
|
|
513
|
-
success: true,
|
|
514
|
-
message: "BashBros hooks installed successfully."
|
|
515
|
-
};
|
|
516
|
-
}
|
|
517
|
-
/**
|
|
518
|
-
* Uninstall BashBros hooks from Claude Code
|
|
519
|
-
*/
|
|
520
|
-
static uninstall() {
|
|
521
|
-
if (!this.isClaudeInstalled()) {
|
|
522
|
-
return {
|
|
523
|
-
success: false,
|
|
524
|
-
message: "Claude Code not found."
|
|
525
|
-
};
|
|
526
|
-
}
|
|
527
|
-
const settings = this.loadSettings();
|
|
528
|
-
if (!settings.hooks) {
|
|
529
|
-
return {
|
|
530
|
-
success: true,
|
|
531
|
-
message: "No hooks to uninstall."
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
const filterHooks = (hooks) => {
|
|
535
|
-
if (!hooks) return [];
|
|
536
|
-
return hooks.filter(
|
|
537
|
-
(h) => !h.hooks.some((hook) => hook.command.includes(BASHBROS_HOOK_MARKER))
|
|
538
|
-
);
|
|
539
|
-
};
|
|
540
|
-
settings.hooks.PreToolUse = filterHooks(settings.hooks.PreToolUse);
|
|
541
|
-
settings.hooks.PostToolUse = filterHooks(settings.hooks.PostToolUse);
|
|
542
|
-
settings.hooks.SessionEnd = filterHooks(settings.hooks.SessionEnd);
|
|
543
|
-
if (settings.hooks.PreToolUse?.length === 0) delete settings.hooks.PreToolUse;
|
|
544
|
-
if (settings.hooks.PostToolUse?.length === 0) delete settings.hooks.PostToolUse;
|
|
545
|
-
if (settings.hooks.SessionEnd?.length === 0) delete settings.hooks.SessionEnd;
|
|
546
|
-
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
547
|
-
this.saveSettings(settings);
|
|
548
|
-
return {
|
|
549
|
-
success: true,
|
|
550
|
-
message: "BashBros hooks uninstalled successfully."
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
/**
|
|
554
|
-
* Check if BashBros hooks are installed
|
|
555
|
-
*/
|
|
556
|
-
static isInstalled(settings) {
|
|
557
|
-
const s = settings || this.loadSettings();
|
|
558
|
-
if (!s.hooks) return false;
|
|
559
|
-
const hasMarker = (hooks) => {
|
|
560
|
-
if (!hooks) return false;
|
|
561
|
-
return hooks.some(
|
|
562
|
-
(h) => h.hooks.some((hook) => hook.command.includes(BASHBROS_HOOK_MARKER))
|
|
563
|
-
);
|
|
564
|
-
};
|
|
565
|
-
return hasMarker(s.hooks.PreToolUse) || hasMarker(s.hooks.PostToolUse) || hasMarker(s.hooks.SessionEnd);
|
|
566
|
-
}
|
|
567
|
-
/**
|
|
568
|
-
* Get hook status
|
|
569
|
-
*/
|
|
570
|
-
static getStatus() {
|
|
571
|
-
const claudeInstalled = this.isClaudeInstalled();
|
|
572
|
-
const settings = claudeInstalled ? this.loadSettings() : {};
|
|
573
|
-
const hooksInstalled = this.isInstalled(settings);
|
|
574
|
-
const allToolsInstalled = this.isAllToolsInstalled(settings);
|
|
575
|
-
const hooks = [];
|
|
576
|
-
if (settings.hooks?.PreToolUse) hooks.push("PreToolUse (gate)");
|
|
577
|
-
if (settings.hooks?.PostToolUse) hooks.push("PostToolUse (record)");
|
|
578
|
-
if (settings.hooks?.SessionEnd) hooks.push("SessionEnd (report)");
|
|
579
|
-
if (allToolsInstalled) hooks.push("PostToolUse (all-tools)");
|
|
580
|
-
return {
|
|
581
|
-
claudeInstalled,
|
|
582
|
-
hooksInstalled,
|
|
583
|
-
allToolsInstalled,
|
|
584
|
-
hooks
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
/**
|
|
588
|
-
* Check if all-tools recording is installed
|
|
589
|
-
*/
|
|
590
|
-
static isAllToolsInstalled(settings) {
|
|
591
|
-
const s = settings || this.loadSettings();
|
|
592
|
-
if (!s.hooks?.PostToolUse) return false;
|
|
593
|
-
return s.hooks.PostToolUse.some(
|
|
594
|
-
(h) => h.hooks.some(
|
|
595
|
-
(hook) => hook.command.includes(BASHBROS_ALL_TOOLS_MARKER) || hook.command.includes("bashbros-all-tools")
|
|
596
|
-
)
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Install all-tools recording hook (records ALL Claude Code tools, not just Bash)
|
|
601
|
-
*/
|
|
602
|
-
static installAllTools() {
|
|
603
|
-
if (!this.isClaudeInstalled()) {
|
|
604
|
-
return {
|
|
605
|
-
success: false,
|
|
606
|
-
message: "Claude Code not found. Install Claude Code first."
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
const settings = this.loadSettings();
|
|
610
|
-
if (!settings.hooks) {
|
|
611
|
-
settings.hooks = {};
|
|
612
|
-
}
|
|
613
|
-
if (this.isAllToolsInstalled(settings)) {
|
|
614
|
-
return {
|
|
615
|
-
success: true,
|
|
616
|
-
message: "BashBros all-tools recording already installed."
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
const allToolsHook = {
|
|
620
|
-
matcher: "",
|
|
621
|
-
// Empty matcher matches ALL tools
|
|
622
|
-
hooks: [{
|
|
623
|
-
type: "command",
|
|
624
|
-
command: `bashbros record-tool ${BASHBROS_ALL_TOOLS_MARKER}`
|
|
625
|
-
}]
|
|
626
|
-
};
|
|
627
|
-
settings.hooks.PostToolUse = [
|
|
628
|
-
allToolsHook,
|
|
629
|
-
...settings.hooks.PostToolUse || []
|
|
630
|
-
];
|
|
631
|
-
this.saveSettings(settings);
|
|
632
|
-
return {
|
|
633
|
-
success: true,
|
|
634
|
-
message: "BashBros all-tools recording installed. All Claude Code tools will now be recorded."
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* Uninstall all-tools recording hook
|
|
639
|
-
*/
|
|
640
|
-
static uninstallAllTools() {
|
|
641
|
-
if (!this.isClaudeInstalled()) {
|
|
642
|
-
return {
|
|
643
|
-
success: false,
|
|
644
|
-
message: "Claude Code not found."
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
const settings = this.loadSettings();
|
|
648
|
-
if (!settings.hooks?.PostToolUse) {
|
|
649
|
-
return {
|
|
650
|
-
success: true,
|
|
651
|
-
message: "No all-tools hook to uninstall."
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
|
|
655
|
-
(h) => !h.hooks.some(
|
|
656
|
-
(hook) => hook.command.includes(BASHBROS_ALL_TOOLS_MARKER) || hook.command.includes("bashbros-all-tools")
|
|
657
|
-
)
|
|
658
|
-
);
|
|
659
|
-
if (settings.hooks.PostToolUse.length === 0) {
|
|
660
|
-
delete settings.hooks.PostToolUse;
|
|
661
|
-
}
|
|
662
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
663
|
-
delete settings.hooks;
|
|
664
|
-
}
|
|
665
|
-
this.saveSettings(settings);
|
|
666
|
-
return {
|
|
667
|
-
success: true,
|
|
668
|
-
message: "BashBros all-tools recording uninstalled."
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
};
|
|
672
|
-
async function gateCommand(command) {
|
|
673
|
-
const { PolicyEngine: PolicyEngine2 } = await import("./engine-EGPAS2EX.js");
|
|
674
|
-
const { RiskScorer } = await import("./risk-scorer-Y6KF2XCZ.js");
|
|
675
|
-
const { loadConfig: loadConfig2 } = await import("./config-43SK6SFI.js");
|
|
676
|
-
const config = loadConfig2();
|
|
677
|
-
const engine = new PolicyEngine2(config);
|
|
678
|
-
const scorer = new RiskScorer();
|
|
679
|
-
const violations = engine.validate(command);
|
|
680
|
-
const risk = scorer.score(command);
|
|
681
|
-
if (violations.length > 0) {
|
|
682
|
-
return {
|
|
683
|
-
allowed: false,
|
|
684
|
-
reason: violations[0].message,
|
|
685
|
-
riskScore: risk.score
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
if (risk.level === "critical") {
|
|
689
|
-
return {
|
|
690
|
-
allowed: false,
|
|
691
|
-
reason: `Critical risk: ${risk.factors.join(", ")}`,
|
|
692
|
-
riskScore: risk.score
|
|
693
|
-
};
|
|
694
|
-
}
|
|
695
|
-
return {
|
|
696
|
-
allowed: true,
|
|
697
|
-
riskScore: risk.score
|
|
698
|
-
};
|
|
699
|
-
}
|
|
700
|
-
|
|
701
423
|
// src/core.ts
|
|
702
424
|
import * as pty from "node-pty";
|
|
703
425
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
@@ -780,32 +502,594 @@ var BashBros = class extends EventEmitter2 {
|
|
|
780
502
|
isAllowed(command) {
|
|
781
503
|
return this.policy.isAllowed(command);
|
|
782
504
|
}
|
|
783
|
-
resize(cols, rows) {
|
|
784
|
-
if (this.ptyProcess) {
|
|
785
|
-
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
|
+
}
|
|
786
1041
|
}
|
|
1042
|
+
return findings;
|
|
787
1043
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
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}]`);
|
|
791
1051
|
}
|
|
1052
|
+
for (const pattern of this.redactPatterns) {
|
|
1053
|
+
redacted = redacted.replace(pattern, "[REDACTED]");
|
|
1054
|
+
}
|
|
1055
|
+
return redacted;
|
|
792
1056
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
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
|
+
}
|
|
797
1065
|
}
|
|
1066
|
+
return false;
|
|
798
1067
|
}
|
|
799
|
-
|
|
800
|
-
|
|
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(", ");
|
|
801
1085
|
}
|
|
802
1086
|
};
|
|
803
1087
|
|
|
804
1088
|
// src/bro/profiler.ts
|
|
805
1089
|
import { execFileSync } from "child_process";
|
|
806
|
-
import { existsSync as
|
|
807
|
-
import { homedir as
|
|
808
|
-
import { join as
|
|
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";
|
|
809
1093
|
var SAFE_VERSION_COMMANDS = {
|
|
810
1094
|
python: ["--version"],
|
|
811
1095
|
python3: ["--version"],
|
|
@@ -838,7 +1122,7 @@ var SystemProfiler = class {
|
|
|
838
1122
|
profile = null;
|
|
839
1123
|
profilePath;
|
|
840
1124
|
constructor() {
|
|
841
|
-
this.profilePath =
|
|
1125
|
+
this.profilePath = join2(homedir2(), ".bashbros", "system-profile.json");
|
|
842
1126
|
}
|
|
843
1127
|
async scan() {
|
|
844
1128
|
const profile = {
|
|
@@ -1022,8 +1306,8 @@ var SystemProfiler = class {
|
|
|
1022
1306
|
["composer.json", "php"]
|
|
1023
1307
|
];
|
|
1024
1308
|
for (const [file, type] of checks) {
|
|
1025
|
-
const filePath =
|
|
1026
|
-
if (
|
|
1309
|
+
const filePath = join2(projectPath, file);
|
|
1310
|
+
if (existsSync2(filePath)) {
|
|
1027
1311
|
try {
|
|
1028
1312
|
const realPath = realpathSync(filePath);
|
|
1029
1313
|
if (realPath.startsWith(realpathSync(projectPath))) {
|
|
@@ -1037,24 +1321,24 @@ var SystemProfiler = class {
|
|
|
1037
1321
|
}
|
|
1038
1322
|
detectDependencies(projectPath) {
|
|
1039
1323
|
const deps = [];
|
|
1040
|
-
const pkgPath =
|
|
1041
|
-
if (
|
|
1324
|
+
const pkgPath = join2(projectPath, "package.json");
|
|
1325
|
+
if (existsSync2(pkgPath)) {
|
|
1042
1326
|
try {
|
|
1043
1327
|
const realPkgPath = realpathSync(pkgPath);
|
|
1044
1328
|
if (realPkgPath.startsWith(realpathSync(projectPath))) {
|
|
1045
|
-
const pkg = JSON.parse(
|
|
1329
|
+
const pkg = JSON.parse(readFileSync2(realPkgPath, "utf-8"));
|
|
1046
1330
|
deps.push(...Object.keys(pkg.dependencies || {}));
|
|
1047
1331
|
deps.push(...Object.keys(pkg.devDependencies || {}));
|
|
1048
1332
|
}
|
|
1049
1333
|
} catch {
|
|
1050
1334
|
}
|
|
1051
1335
|
}
|
|
1052
|
-
const reqPath =
|
|
1053
|
-
if (
|
|
1336
|
+
const reqPath = join2(projectPath, "requirements.txt");
|
|
1337
|
+
if (existsSync2(reqPath)) {
|
|
1054
1338
|
try {
|
|
1055
1339
|
const realReqPath = realpathSync(reqPath);
|
|
1056
1340
|
if (realReqPath.startsWith(realpathSync(projectPath))) {
|
|
1057
|
-
const reqs =
|
|
1341
|
+
const reqs = readFileSync2(realReqPath, "utf-8");
|
|
1058
1342
|
const packages = reqs.split("\n").map((line) => line.split(/[=<>]/)[0].trim()).filter(Boolean);
|
|
1059
1343
|
deps.push(...packages);
|
|
1060
1344
|
}
|
|
@@ -1064,9 +1348,9 @@ var SystemProfiler = class {
|
|
|
1064
1348
|
return deps.slice(0, 100);
|
|
1065
1349
|
}
|
|
1066
1350
|
load() {
|
|
1067
|
-
if (
|
|
1351
|
+
if (existsSync2(this.profilePath)) {
|
|
1068
1352
|
try {
|
|
1069
|
-
const data =
|
|
1353
|
+
const data = readFileSync2(this.profilePath, "utf-8");
|
|
1070
1354
|
this.profile = JSON.parse(data);
|
|
1071
1355
|
return this.profile;
|
|
1072
1356
|
} catch {
|
|
@@ -1077,13 +1361,13 @@ var SystemProfiler = class {
|
|
|
1077
1361
|
}
|
|
1078
1362
|
save() {
|
|
1079
1363
|
try {
|
|
1080
|
-
const { writeFileSync:
|
|
1081
|
-
const dir =
|
|
1082
|
-
if (!
|
|
1083
|
-
|
|
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 });
|
|
1084
1368
|
}
|
|
1085
1369
|
const filePath = this.profilePath;
|
|
1086
|
-
|
|
1370
|
+
writeFileSync3(filePath, JSON.stringify(this.profile, null, 2));
|
|
1087
1371
|
try {
|
|
1088
1372
|
chmodSync(filePath, 384);
|
|
1089
1373
|
} catch {
|
|
@@ -1131,8 +1415,10 @@ var SystemProfiler = class {
|
|
|
1131
1415
|
var TaskRouter = class {
|
|
1132
1416
|
rules;
|
|
1133
1417
|
profile;
|
|
1134
|
-
|
|
1418
|
+
ollama;
|
|
1419
|
+
constructor(profile = null, ollama = null) {
|
|
1135
1420
|
this.profile = profile;
|
|
1421
|
+
this.ollama = ollama;
|
|
1136
1422
|
this.rules = this.buildDefaultRules();
|
|
1137
1423
|
}
|
|
1138
1424
|
buildDefaultRules() {
|
|
@@ -1212,6 +1498,25 @@ var TaskRouter = class {
|
|
|
1212
1498
|
confidence: 0.5
|
|
1213
1499
|
};
|
|
1214
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
|
+
}
|
|
1215
1520
|
looksSimple(command) {
|
|
1216
1521
|
const words = command.split(/\s+/);
|
|
1217
1522
|
if (words.length <= 3) return true;
|
|
@@ -1240,8 +1545,11 @@ var CommandSuggester = class {
|
|
|
1240
1545
|
history = [];
|
|
1241
1546
|
profile = null;
|
|
1242
1547
|
patterns = /* @__PURE__ */ new Map();
|
|
1243
|
-
|
|
1548
|
+
ollama;
|
|
1549
|
+
aiCache = /* @__PURE__ */ new Map();
|
|
1550
|
+
constructor(profile = null, ollama = null) {
|
|
1244
1551
|
this.profile = profile;
|
|
1552
|
+
this.ollama = ollama;
|
|
1245
1553
|
this.initPatterns();
|
|
1246
1554
|
}
|
|
1247
1555
|
initPatterns() {
|
|
@@ -1274,6 +1582,34 @@ var CommandSuggester = class {
|
|
|
1274
1582
|
const unique = this.dedupeAndRank(suggestions);
|
|
1275
1583
|
return unique.slice(0, 5);
|
|
1276
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
|
+
}
|
|
1277
1613
|
suggestFromPatterns(lastCommand) {
|
|
1278
1614
|
const suggestions = [];
|
|
1279
1615
|
for (const [key, commands] of this.patterns) {
|
|
@@ -1692,6 +2028,9 @@ var BashBro = class extends EventEmitter4 {
|
|
|
1692
2028
|
config;
|
|
1693
2029
|
ollamaAvailable = false;
|
|
1694
2030
|
bashgymModelVersion = null;
|
|
2031
|
+
adapterRegistry;
|
|
2032
|
+
profileManager;
|
|
2033
|
+
activeProfile = null;
|
|
1695
2034
|
constructor(config = {}) {
|
|
1696
2035
|
super();
|
|
1697
2036
|
this.config = {
|
|
@@ -1703,8 +2042,6 @@ var BashBro = class extends EventEmitter4 {
|
|
|
1703
2042
|
...config
|
|
1704
2043
|
};
|
|
1705
2044
|
this.profiler = new SystemProfiler();
|
|
1706
|
-
this.router = new TaskRouter();
|
|
1707
|
-
this.suggester = new CommandSuggester();
|
|
1708
2045
|
this.worker = new BackgroundWorker();
|
|
1709
2046
|
if (this.config.enableOllama) {
|
|
1710
2047
|
this.ollama = new OllamaClient({
|
|
@@ -1712,12 +2049,19 @@ var BashBro = class extends EventEmitter4 {
|
|
|
1712
2049
|
model: this.config.modelName
|
|
1713
2050
|
});
|
|
1714
2051
|
}
|
|
2052
|
+
this.router = new TaskRouter(null, this.ollama);
|
|
2053
|
+
this.suggester = new CommandSuggester(null, this.ollama);
|
|
1715
2054
|
this.worker.on("complete", (data) => this.emit("task:complete", data));
|
|
1716
2055
|
this.worker.on("output", (data) => this.emit("task:output", data));
|
|
1717
2056
|
this.worker.on("error", (data) => this.emit("task:error", data));
|
|
1718
2057
|
if (this.config.enableBashgymIntegration) {
|
|
1719
2058
|
this.initBashgymIntegration();
|
|
1720
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
|
+
}
|
|
1721
2065
|
}
|
|
1722
2066
|
/**
|
|
1723
2067
|
* Initialize bashgym integration for model hot-swap
|
|
@@ -1800,6 +2144,22 @@ var BashBro = class extends EventEmitter4 {
|
|
|
1800
2144
|
}
|
|
1801
2145
|
return this.suggester.suggest(context);
|
|
1802
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
|
+
}
|
|
1803
2163
|
/**
|
|
1804
2164
|
* SECURITY FIX: Safe command execution with validation
|
|
1805
2165
|
*/
|
|
@@ -2084,6 +2444,41 @@ var BashBro = class extends EventEmitter4 {
|
|
|
2084
2444
|
}
|
|
2085
2445
|
return false;
|
|
2086
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
|
+
}
|
|
2087
2482
|
// Format a nice status message
|
|
2088
2483
|
status() {
|
|
2089
2484
|
const lines = [
|
|
@@ -2138,151 +2533,6 @@ var BashBro = class extends EventEmitter4 {
|
|
|
2138
2533
|
}
|
|
2139
2534
|
};
|
|
2140
2535
|
|
|
2141
|
-
// src/observability/metrics.ts
|
|
2142
|
-
var MetricsCollector = class {
|
|
2143
|
-
sessionId;
|
|
2144
|
-
startTime;
|
|
2145
|
-
commands = [];
|
|
2146
|
-
filesModified = /* @__PURE__ */ new Set();
|
|
2147
|
-
pathsAccessed = /* @__PURE__ */ new Set();
|
|
2148
|
-
constructor() {
|
|
2149
|
-
this.sessionId = this.generateSessionId();
|
|
2150
|
-
this.startTime = /* @__PURE__ */ new Date();
|
|
2151
|
-
}
|
|
2152
|
-
generateSessionId() {
|
|
2153
|
-
const now = /* @__PURE__ */ new Date();
|
|
2154
|
-
const date = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
2155
|
-
const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
|
|
2156
|
-
const rand = Math.random().toString(36).slice(2, 6);
|
|
2157
|
-
return `${date}-${time}-${rand}`;
|
|
2158
|
-
}
|
|
2159
|
-
/**
|
|
2160
|
-
* Record a command execution
|
|
2161
|
-
*/
|
|
2162
|
-
record(metric) {
|
|
2163
|
-
this.commands.push(metric);
|
|
2164
|
-
const paths = this.extractPaths(metric.command);
|
|
2165
|
-
for (const path of paths) {
|
|
2166
|
-
this.pathsAccessed.add(path);
|
|
2167
|
-
}
|
|
2168
|
-
if (this.isWriteCommand(metric.command)) {
|
|
2169
|
-
for (const path of paths) {
|
|
2170
|
-
this.filesModified.add(path);
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
/**
|
|
2175
|
-
* Get current session metrics
|
|
2176
|
-
*/
|
|
2177
|
-
getMetrics() {
|
|
2178
|
-
const now = /* @__PURE__ */ new Date();
|
|
2179
|
-
const duration = now.getTime() - this.startTime.getTime();
|
|
2180
|
-
const riskDist = { safe: 0, caution: 0, dangerous: 0, critical: 0 };
|
|
2181
|
-
let totalRisk = 0;
|
|
2182
|
-
for (const cmd of this.commands) {
|
|
2183
|
-
riskDist[cmd.riskScore.level]++;
|
|
2184
|
-
totalRisk += cmd.riskScore.score;
|
|
2185
|
-
}
|
|
2186
|
-
const cmdFreq = /* @__PURE__ */ new Map();
|
|
2187
|
-
for (const cmd of this.commands) {
|
|
2188
|
-
const base = cmd.command.split(/\s+/)[0];
|
|
2189
|
-
cmdFreq.set(base, (cmdFreq.get(base) || 0) + 1);
|
|
2190
|
-
}
|
|
2191
|
-
const topCommands = [...cmdFreq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
2192
|
-
const violationsByType = {};
|
|
2193
|
-
for (const cmd of this.commands) {
|
|
2194
|
-
for (const v of cmd.violations) {
|
|
2195
|
-
violationsByType[v.type] = (violationsByType[v.type] || 0) + 1;
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
const totalExecTime = this.commands.reduce((sum, c) => sum + c.duration, 0);
|
|
2199
|
-
const avgExecTime = this.commands.length > 0 ? totalExecTime / this.commands.length : 0;
|
|
2200
|
-
return {
|
|
2201
|
-
sessionId: this.sessionId,
|
|
2202
|
-
startTime: this.startTime,
|
|
2203
|
-
duration,
|
|
2204
|
-
commandCount: this.commands.length,
|
|
2205
|
-
blockedCount: this.commands.filter((c) => !c.allowed).length,
|
|
2206
|
-
uniqueCommands: cmdFreq.size,
|
|
2207
|
-
topCommands,
|
|
2208
|
-
riskDistribution: riskDist,
|
|
2209
|
-
avgRiskScore: this.commands.length > 0 ? totalRisk / this.commands.length : 0,
|
|
2210
|
-
avgExecutionTime: avgExecTime,
|
|
2211
|
-
totalExecutionTime: totalExecTime,
|
|
2212
|
-
filesModified: [...this.filesModified],
|
|
2213
|
-
pathsAccessed: [...this.pathsAccessed],
|
|
2214
|
-
violationsByType
|
|
2215
|
-
};
|
|
2216
|
-
}
|
|
2217
|
-
/**
|
|
2218
|
-
* Extract paths from a command
|
|
2219
|
-
*/
|
|
2220
|
-
extractPaths(command) {
|
|
2221
|
-
const paths = [];
|
|
2222
|
-
const tokens = command.split(/\s+/);
|
|
2223
|
-
for (const token of tokens) {
|
|
2224
|
-
if (token.startsWith("-")) continue;
|
|
2225
|
-
if (token.startsWith("/") || token.startsWith("./") || token.startsWith("../") || token.startsWith("~/") || token.includes(".")) {
|
|
2226
|
-
paths.push(token);
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
return paths;
|
|
2230
|
-
}
|
|
2231
|
-
/**
|
|
2232
|
-
* Check if command modifies files
|
|
2233
|
-
*/
|
|
2234
|
-
isWriteCommand(command) {
|
|
2235
|
-
const writePatterns = [
|
|
2236
|
-
/^(vim|vi|nano|emacs|code)\s/,
|
|
2237
|
-
/^(touch|mkdir|cp|mv|rm)\s/,
|
|
2238
|
-
/^(echo|cat|printf).*>/,
|
|
2239
|
-
/^(git\s+(add|commit|checkout|reset))/,
|
|
2240
|
-
/^(npm|yarn|pnpm)\s+(install|uninstall)/,
|
|
2241
|
-
/^(pip|pip3)\s+(install|uninstall)/,
|
|
2242
|
-
/^chmod\s/,
|
|
2243
|
-
/^chown\s/
|
|
2244
|
-
];
|
|
2245
|
-
return writePatterns.some((p) => p.test(command));
|
|
2246
|
-
}
|
|
2247
|
-
/**
|
|
2248
|
-
* Get recent commands
|
|
2249
|
-
*/
|
|
2250
|
-
getRecentCommands(n = 10) {
|
|
2251
|
-
return this.commands.slice(-n);
|
|
2252
|
-
}
|
|
2253
|
-
/**
|
|
2254
|
-
* Get blocked commands
|
|
2255
|
-
*/
|
|
2256
|
-
getBlockedCommands() {
|
|
2257
|
-
return this.commands.filter((c) => !c.allowed);
|
|
2258
|
-
}
|
|
2259
|
-
/**
|
|
2260
|
-
* Get high-risk commands
|
|
2261
|
-
*/
|
|
2262
|
-
getHighRiskCommands(threshold = 6) {
|
|
2263
|
-
return this.commands.filter((c) => c.riskScore.score >= threshold);
|
|
2264
|
-
}
|
|
2265
|
-
/**
|
|
2266
|
-
* Format duration for display
|
|
2267
|
-
*/
|
|
2268
|
-
static formatDuration(ms) {
|
|
2269
|
-
if (ms < 1e3) return `${ms}ms`;
|
|
2270
|
-
if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
2271
|
-
if (ms < 36e5) return `${Math.floor(ms / 6e4)}m ${Math.floor(ms % 6e4 / 1e3)}s`;
|
|
2272
|
-
return `${Math.floor(ms / 36e5)}h ${Math.floor(ms % 36e5 / 6e4)}m`;
|
|
2273
|
-
}
|
|
2274
|
-
/**
|
|
2275
|
-
* Reset collector
|
|
2276
|
-
*/
|
|
2277
|
-
reset() {
|
|
2278
|
-
this.sessionId = this.generateSessionId();
|
|
2279
|
-
this.startTime = /* @__PURE__ */ new Date();
|
|
2280
|
-
this.commands = [];
|
|
2281
|
-
this.filesModified.clear();
|
|
2282
|
-
this.pathsAccessed.clear();
|
|
2283
|
-
}
|
|
2284
|
-
};
|
|
2285
|
-
|
|
2286
2536
|
// src/observability/cost.ts
|
|
2287
2537
|
var MODEL_PRICING = {
|
|
2288
2538
|
"claude-opus-4": { inputPer1k: 0.015, outputPer1k: 0.075 },
|
|
@@ -2627,15 +2877,15 @@ var ReportGenerator = class {
|
|
|
2627
2877
|
};
|
|
2628
2878
|
|
|
2629
2879
|
// src/safety/undo-stack.ts
|
|
2630
|
-
import { existsSync as
|
|
2631
|
-
import { join as
|
|
2632
|
-
import { homedir as
|
|
2633
|
-
var
|
|
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 = {
|
|
2634
2884
|
maxStackSize: 100,
|
|
2635
2885
|
maxFileSize: 10 * 1024 * 1024,
|
|
2636
2886
|
// 10MB
|
|
2637
2887
|
ttlMinutes: 60,
|
|
2638
|
-
backupPath:
|
|
2888
|
+
backupPath: join3(homedir3(), ".bashbros", "undo"),
|
|
2639
2889
|
enabled: true
|
|
2640
2890
|
};
|
|
2641
2891
|
var UndoStack = class {
|
|
@@ -2644,13 +2894,13 @@ var UndoStack = class {
|
|
|
2644
2894
|
config;
|
|
2645
2895
|
undoDir;
|
|
2646
2896
|
constructor(policy) {
|
|
2647
|
-
this.config = { ...
|
|
2897
|
+
this.config = { ...DEFAULT_CONFIG3 };
|
|
2648
2898
|
if (policy) {
|
|
2649
2899
|
if (typeof policy.maxStackSize === "number") this.config.maxStackSize = policy.maxStackSize;
|
|
2650
2900
|
if (typeof policy.maxFileSize === "number") this.config.maxFileSize = policy.maxFileSize;
|
|
2651
2901
|
if (typeof policy.ttlMinutes === "number") this.config.ttlMinutes = policy.ttlMinutes;
|
|
2652
2902
|
if (typeof policy.backupPath === "string") {
|
|
2653
|
-
this.config.backupPath = policy.backupPath.replace("~",
|
|
2903
|
+
this.config.backupPath = policy.backupPath.replace("~", homedir3());
|
|
2654
2904
|
}
|
|
2655
2905
|
if (typeof policy.enabled === "boolean") this.config.enabled = policy.enabled;
|
|
2656
2906
|
}
|
|
@@ -2660,8 +2910,8 @@ var UndoStack = class {
|
|
|
2660
2910
|
this.cleanupOldBackups();
|
|
2661
2911
|
}
|
|
2662
2912
|
ensureUndoDir() {
|
|
2663
|
-
if (!
|
|
2664
|
-
|
|
2913
|
+
if (!existsSync3(this.undoDir)) {
|
|
2914
|
+
mkdirSync2(this.undoDir, { recursive: true, mode: 448 });
|
|
2665
2915
|
}
|
|
2666
2916
|
}
|
|
2667
2917
|
/**
|
|
@@ -2675,7 +2925,7 @@ var UndoStack = class {
|
|
|
2675
2925
|
const files = readdirSync(this.undoDir);
|
|
2676
2926
|
for (const file of files) {
|
|
2677
2927
|
if (!file.endsWith(".backup")) continue;
|
|
2678
|
-
const filePath =
|
|
2928
|
+
const filePath = join3(this.undoDir, file);
|
|
2679
2929
|
try {
|
|
2680
2930
|
const stats = statSync(filePath);
|
|
2681
2931
|
if (stats.mtimeMs < cutoff) {
|
|
@@ -2716,7 +2966,7 @@ var UndoStack = class {
|
|
|
2716
2966
|
* Record a file modification (backs up original)
|
|
2717
2967
|
*/
|
|
2718
2968
|
recordModify(path, command) {
|
|
2719
|
-
if (!this.config.enabled || !
|
|
2969
|
+
if (!this.config.enabled || !existsSync3(path)) {
|
|
2720
2970
|
return null;
|
|
2721
2971
|
}
|
|
2722
2972
|
const stats = statSync(path);
|
|
@@ -2732,7 +2982,7 @@ var UndoStack = class {
|
|
|
2732
2982
|
return entry;
|
|
2733
2983
|
}
|
|
2734
2984
|
const id = this.generateId();
|
|
2735
|
-
const backupPath =
|
|
2985
|
+
const backupPath = join3(this.undoDir, `${id}.backup`);
|
|
2736
2986
|
try {
|
|
2737
2987
|
copyFileSync(path, backupPath);
|
|
2738
2988
|
const entry = {
|
|
@@ -2753,7 +3003,7 @@ var UndoStack = class {
|
|
|
2753
3003
|
* Record a file deletion (backs up content)
|
|
2754
3004
|
*/
|
|
2755
3005
|
recordDelete(path, command) {
|
|
2756
|
-
if (!this.config.enabled || !
|
|
3006
|
+
if (!this.config.enabled || !existsSync3(path)) {
|
|
2757
3007
|
return null;
|
|
2758
3008
|
}
|
|
2759
3009
|
const stats = statSync(path);
|
|
@@ -2769,7 +3019,7 @@ var UndoStack = class {
|
|
|
2769
3019
|
return entry;
|
|
2770
3020
|
}
|
|
2771
3021
|
const id = this.generateId();
|
|
2772
|
-
const backupPath =
|
|
3022
|
+
const backupPath = join3(this.undoDir, `${id}.backup`);
|
|
2773
3023
|
try {
|
|
2774
3024
|
copyFileSync(path, backupPath);
|
|
2775
3025
|
const entry = {
|
|
@@ -2796,11 +3046,11 @@ var UndoStack = class {
|
|
|
2796
3046
|
const isModify = /^(sed|awk|vim|vi|nano|code)\s/.test(command) || /^(echo|cat).*>>/.test(command);
|
|
2797
3047
|
for (const path of paths) {
|
|
2798
3048
|
let entry = null;
|
|
2799
|
-
if (isDelete &&
|
|
3049
|
+
if (isDelete && existsSync3(path)) {
|
|
2800
3050
|
entry = this.recordDelete(path, command);
|
|
2801
|
-
} else if (isModify &&
|
|
3051
|
+
} else if (isModify && existsSync3(path)) {
|
|
2802
3052
|
entry = this.recordModify(path, command);
|
|
2803
|
-
} else if (isCreate && !
|
|
3053
|
+
} else if (isCreate && !existsSync3(path)) {
|
|
2804
3054
|
entry = this.recordCreate(path, command);
|
|
2805
3055
|
}
|
|
2806
3056
|
if (entry) {
|
|
@@ -2826,7 +3076,7 @@ var UndoStack = class {
|
|
|
2826
3076
|
try {
|
|
2827
3077
|
switch (entry.operation) {
|
|
2828
3078
|
case "create":
|
|
2829
|
-
if (
|
|
3079
|
+
if (existsSync3(entry.path)) {
|
|
2830
3080
|
unlinkSync(entry.path);
|
|
2831
3081
|
return {
|
|
2832
3082
|
success: true,
|
|
@@ -2840,7 +3090,7 @@ var UndoStack = class {
|
|
|
2840
3090
|
entry
|
|
2841
3091
|
};
|
|
2842
3092
|
case "modify":
|
|
2843
|
-
if (entry.backupPath &&
|
|
3093
|
+
if (entry.backupPath && existsSync3(entry.backupPath)) {
|
|
2844
3094
|
copyFileSync(entry.backupPath, entry.path);
|
|
2845
3095
|
return {
|
|
2846
3096
|
success: true,
|
|
@@ -2854,10 +3104,10 @@ var UndoStack = class {
|
|
|
2854
3104
|
entry
|
|
2855
3105
|
};
|
|
2856
3106
|
case "delete":
|
|
2857
|
-
if (entry.backupPath &&
|
|
3107
|
+
if (entry.backupPath && existsSync3(entry.backupPath)) {
|
|
2858
3108
|
const dir = dirname(entry.path);
|
|
2859
|
-
if (!
|
|
2860
|
-
|
|
3109
|
+
if (!existsSync3(dir)) {
|
|
3110
|
+
mkdirSync2(dir, { recursive: true });
|
|
2861
3111
|
}
|
|
2862
3112
|
copyFileSync(entry.backupPath, entry.path);
|
|
2863
3113
|
return {
|
|
@@ -2913,7 +3163,7 @@ var UndoStack = class {
|
|
|
2913
3163
|
*/
|
|
2914
3164
|
clear() {
|
|
2915
3165
|
for (const entry of this.stack) {
|
|
2916
|
-
if (entry.backupPath &&
|
|
3166
|
+
if (entry.backupPath && existsSync3(entry.backupPath)) {
|
|
2917
3167
|
try {
|
|
2918
3168
|
unlinkSync(entry.backupPath);
|
|
2919
3169
|
} catch {
|
|
@@ -2929,7 +3179,7 @@ var UndoStack = class {
|
|
|
2929
3179
|
this.stack.push(entry);
|
|
2930
3180
|
if (this.stack.length > this.config.maxStackSize) {
|
|
2931
3181
|
const removed = this.stack.shift();
|
|
2932
|
-
if (removed?.backupPath &&
|
|
3182
|
+
if (removed?.backupPath && existsSync3(removed.backupPath)) {
|
|
2933
3183
|
try {
|
|
2934
3184
|
unlinkSync(removed.backupPath);
|
|
2935
3185
|
} catch {
|
|
@@ -2959,155 +3209,21 @@ var UndoStack = class {
|
|
|
2959
3209
|
}
|
|
2960
3210
|
};
|
|
2961
3211
|
|
|
2962
|
-
// src/policy/loop-detector.ts
|
|
2963
|
-
var DEFAULT_CONFIG2 = {
|
|
2964
|
-
maxRepeats: 3,
|
|
2965
|
-
maxTurns: 100,
|
|
2966
|
-
similarityThreshold: 0.85,
|
|
2967
|
-
cooldownMs: 1e3,
|
|
2968
|
-
windowSize: 20
|
|
2969
|
-
};
|
|
2970
|
-
var LoopDetector = class {
|
|
2971
|
-
config;
|
|
2972
|
-
history = [];
|
|
2973
|
-
turnCount = 0;
|
|
2974
|
-
constructor(config = {}) {
|
|
2975
|
-
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
2976
|
-
}
|
|
2977
|
-
/**
|
|
2978
|
-
* Record a command and check for loops
|
|
2979
|
-
*/
|
|
2980
|
-
check(command) {
|
|
2981
|
-
const now = Date.now();
|
|
2982
|
-
const normalized = this.normalize(command);
|
|
2983
|
-
this.turnCount++;
|
|
2984
|
-
if (this.turnCount >= this.config.maxTurns) {
|
|
2985
|
-
return {
|
|
2986
|
-
type: "max_turns",
|
|
2987
|
-
command,
|
|
2988
|
-
count: this.turnCount,
|
|
2989
|
-
message: `Maximum turns reached (${this.config.maxTurns}). Session may be stuck.`
|
|
2990
|
-
};
|
|
2991
|
-
}
|
|
2992
|
-
const exactMatches = this.history.filter((h) => h.command === command);
|
|
2993
|
-
if (exactMatches.length >= this.config.maxRepeats) {
|
|
2994
|
-
return {
|
|
2995
|
-
type: "exact_repeat",
|
|
2996
|
-
command,
|
|
2997
|
-
count: exactMatches.length + 1,
|
|
2998
|
-
message: `Command repeated ${exactMatches.length + 1} times: "${command.slice(0, 50)}..."`
|
|
2999
|
-
};
|
|
3000
|
-
}
|
|
3001
|
-
const lastSame = exactMatches[exactMatches.length - 1];
|
|
3002
|
-
if (lastSame && now - lastSame.timestamp < this.config.cooldownMs) {
|
|
3003
|
-
return {
|
|
3004
|
-
type: "exact_repeat",
|
|
3005
|
-
command,
|
|
3006
|
-
count: 2,
|
|
3007
|
-
message: `Rapid repeat detected (${now - lastSame.timestamp}ms apart)`
|
|
3008
|
-
};
|
|
3009
|
-
}
|
|
3010
|
-
const recentWindow = this.history.slice(-this.config.windowSize);
|
|
3011
|
-
const similarCount = recentWindow.filter(
|
|
3012
|
-
(h) => this.similarity(h.normalized, normalized) >= this.config.similarityThreshold
|
|
3013
|
-
).length;
|
|
3014
|
-
if (similarCount >= this.config.maxRepeats) {
|
|
3015
|
-
return {
|
|
3016
|
-
type: "semantic_repeat",
|
|
3017
|
-
command,
|
|
3018
|
-
count: similarCount + 1,
|
|
3019
|
-
message: `Similar commands repeated ${similarCount + 1} times`
|
|
3020
|
-
};
|
|
3021
|
-
}
|
|
3022
|
-
const baseCommand = command.split(/\s+/)[0];
|
|
3023
|
-
const toolCount = recentWindow.filter(
|
|
3024
|
-
(h) => h.command.split(/\s+/)[0] === baseCommand
|
|
3025
|
-
).length;
|
|
3026
|
-
if (toolCount >= this.config.maxRepeats * 2) {
|
|
3027
|
-
return {
|
|
3028
|
-
type: "tool_hammering",
|
|
3029
|
-
command,
|
|
3030
|
-
count: toolCount + 1,
|
|
3031
|
-
message: `Tool "${baseCommand}" called ${toolCount + 1} times in last ${this.config.windowSize} commands`
|
|
3032
|
-
};
|
|
3033
|
-
}
|
|
3034
|
-
this.history.push({ command, timestamp: now, normalized });
|
|
3035
|
-
if (this.history.length > this.config.windowSize * 2) {
|
|
3036
|
-
this.history = this.history.slice(-this.config.windowSize);
|
|
3037
|
-
}
|
|
3038
|
-
return null;
|
|
3039
|
-
}
|
|
3040
|
-
/**
|
|
3041
|
-
* Normalize command for comparison
|
|
3042
|
-
*/
|
|
3043
|
-
normalize(command) {
|
|
3044
|
-
return command.toLowerCase().replace(/["']/g, "").replace(/\s+/g, " ").replace(/\d+/g, "N").replace(/[a-f0-9]{8,}/gi, "H").trim();
|
|
3045
|
-
}
|
|
3046
|
-
/**
|
|
3047
|
-
* Calculate similarity between two strings (Jaccard index on words)
|
|
3048
|
-
*/
|
|
3049
|
-
similarity(a, b) {
|
|
3050
|
-
const wordsA = new Set(a.split(/\s+/));
|
|
3051
|
-
const wordsB = new Set(b.split(/\s+/));
|
|
3052
|
-
const intersection = new Set([...wordsA].filter((x) => wordsB.has(x)));
|
|
3053
|
-
const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
|
|
3054
|
-
if (union.size === 0) return 1;
|
|
3055
|
-
return intersection.size / union.size;
|
|
3056
|
-
}
|
|
3057
|
-
/**
|
|
3058
|
-
* Get current turn count
|
|
3059
|
-
*/
|
|
3060
|
-
getTurnCount() {
|
|
3061
|
-
return this.turnCount;
|
|
3062
|
-
}
|
|
3063
|
-
/**
|
|
3064
|
-
* Get command frequency map
|
|
3065
|
-
*/
|
|
3066
|
-
getFrequencyMap() {
|
|
3067
|
-
const freq = /* @__PURE__ */ new Map();
|
|
3068
|
-
for (const entry of this.history) {
|
|
3069
|
-
const base = entry.command.split(/\s+/)[0];
|
|
3070
|
-
freq.set(base, (freq.get(base) || 0) + 1);
|
|
3071
|
-
}
|
|
3072
|
-
return freq;
|
|
3073
|
-
}
|
|
3074
|
-
/**
|
|
3075
|
-
* Reset detector state
|
|
3076
|
-
*/
|
|
3077
|
-
reset() {
|
|
3078
|
-
this.history = [];
|
|
3079
|
-
this.turnCount = 0;
|
|
3080
|
-
}
|
|
3081
|
-
/**
|
|
3082
|
-
* Get stats for reporting
|
|
3083
|
-
*/
|
|
3084
|
-
getStats() {
|
|
3085
|
-
const freq = this.getFrequencyMap();
|
|
3086
|
-
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
|
|
3087
|
-
return {
|
|
3088
|
-
turnCount: this.turnCount,
|
|
3089
|
-
uniqueCommands: freq.size,
|
|
3090
|
-
topCommands: sorted.slice(0, 5)
|
|
3091
|
-
};
|
|
3092
|
-
}
|
|
3093
|
-
};
|
|
3094
|
-
|
|
3095
3212
|
export {
|
|
3096
3213
|
BashgymIntegration,
|
|
3097
3214
|
getBashgymIntegration,
|
|
3098
3215
|
resetBashgymIntegration,
|
|
3099
|
-
ClaudeCodeHooks,
|
|
3100
|
-
gateCommand,
|
|
3101
3216
|
BashBros,
|
|
3217
|
+
LoopDetector,
|
|
3218
|
+
AnomalyDetector,
|
|
3219
|
+
OutputScanner,
|
|
3102
3220
|
SystemProfiler,
|
|
3103
3221
|
TaskRouter,
|
|
3104
3222
|
CommandSuggester,
|
|
3105
3223
|
BackgroundWorker,
|
|
3106
3224
|
BashBro,
|
|
3107
|
-
MetricsCollector,
|
|
3108
3225
|
CostEstimator,
|
|
3109
3226
|
ReportGenerator,
|
|
3110
|
-
UndoStack
|
|
3111
|
-
LoopDetector
|
|
3227
|
+
UndoStack
|
|
3112
3228
|
};
|
|
3113
|
-
//# sourceMappingURL=chunk-
|
|
3229
|
+
//# sourceMappingURL=chunk-7OEWYFN3.js.map
|