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.
- 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-XCZMQRSX.js → chunk-7OEWYFN3.js} +745 -541
- package/dist/chunk-7OEWYFN3.js.map +1 -0
- package/dist/{chunk-SQCP6IYB.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-KYDMPE4N.js +224 -0
- 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-YUMNBQAY.js → chunk-RDNSS3ME.js} +587 -12
- package/dist/chunk-RDNSS3ME.js.map +1 -0
- package/dist/{chunk-BW6XCOJH.js → chunk-RTZ4QWG2.js} +2 -2
- 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 +1182 -251
- package/dist/cli.js.map +1 -1
- package/dist/{config-JLLOTFLI.js → config-I5NCK3RJ.js} +2 -2
- package/dist/copilot-cli-5WJWK5YT.js +9 -0
- package/dist/{db-OBKEXRTP.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-6LZ2HBCU.js → display-UH7KEHOW.js} +3 -3
- package/dist/display-UH7KEHOW.js.map +1 -0
- package/dist/gemini-cli-3563EELZ.js +9 -0
- package/dist/gemini-cli-3563EELZ.js.map +1 -0
- package/dist/index.d.ts +195 -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-3NAVABN6.js +12 -0
- package/dist/writer-3NAVABN6.js.map +1 -0
- package/package.json +77 -68
- package/dist/chunk-BW6XCOJH.js.map +0 -1
- package/dist/chunk-DLP2O6PN.js.map +0 -1
- package/dist/chunk-SQCP6IYB.js.map +0 -1
- package/dist/chunk-XCZMQRSX.js.map +0 -1
- package/dist/chunk-YUMNBQAY.js.map +0 -1
- /package/dist/{config-JLLOTFLI.js.map → adapters-JAZGGNVP.js.map} +0 -0
- /package/dist/{db-OBKEXRTP.js.map → config-I5NCK3RJ.js.map} +0 -0
- /package/dist/{display-6LZ2HBCU.js.map → copilot-cli-5WJWK5YT.js.map} +0 -0
- /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-
|
|
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,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
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
712
|
-
|
|
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
|
|
719
|
-
import { homedir as
|
|
720
|
-
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";
|
|
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 =
|
|
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 =
|
|
938
|
-
if (
|
|
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 =
|
|
953
|
-
if (
|
|
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(
|
|
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 =
|
|
965
|
-
if (
|
|
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 =
|
|
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 (
|
|
1351
|
+
if (existsSync2(this.profilePath)) {
|
|
980
1352
|
try {
|
|
981
|
-
const data =
|
|
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:
|
|
993
|
-
const dir =
|
|
994
|
-
if (!
|
|
995
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2543
|
-
import { join as
|
|
2544
|
-
import { homedir as
|
|
2545
|
-
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 = {
|
|
2546
2884
|
maxStackSize: 100,
|
|
2547
2885
|
maxFileSize: 10 * 1024 * 1024,
|
|
2548
2886
|
// 10MB
|
|
2549
2887
|
ttlMinutes: 60,
|
|
2550
|
-
backupPath:
|
|
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 = { ...
|
|
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("~",
|
|
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 (!
|
|
2576
|
-
|
|
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 =
|
|
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 || !
|
|
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 =
|
|
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 || !
|
|
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 =
|
|
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 &&
|
|
3049
|
+
if (isDelete && existsSync3(path)) {
|
|
2712
3050
|
entry = this.recordDelete(path, command);
|
|
2713
|
-
} else if (isModify &&
|
|
3051
|
+
} else if (isModify && existsSync3(path)) {
|
|
2714
3052
|
entry = this.recordModify(path, command);
|
|
2715
|
-
} else if (isCreate && !
|
|
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 (
|
|
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 &&
|
|
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 &&
|
|
3107
|
+
if (entry.backupPath && existsSync3(entry.backupPath)) {
|
|
2770
3108
|
const dir = dirname(entry.path);
|
|
2771
|
-
if (!
|
|
2772
|
-
|
|
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 &&
|
|
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 &&
|
|
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-
|
|
3229
|
+
//# sourceMappingURL=chunk-7OEWYFN3.js.map
|