bashbros 0.1.1 → 0.1.3
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/dist/{chunk-VVSCAH2B.js → chunk-2RPTM6EQ.js} +211 -8
- package/dist/chunk-2RPTM6EQ.js.map +1 -0
- package/dist/chunk-EYO44OMN.js +181 -0
- package/dist/chunk-EYO44OMN.js.map +1 -0
- package/dist/chunk-FRMAIRQ2.js +89 -0
- package/dist/chunk-FRMAIRQ2.js.map +1 -0
- package/dist/chunk-JYWQT2B4.js +866 -0
- package/dist/chunk-JYWQT2B4.js.map +1 -0
- package/dist/{chunk-GD5VNHIN.js → chunk-QWZGB4V3.js} +4 -85
- package/dist/chunk-QWZGB4V3.js.map +1 -0
- package/dist/cli.js +520 -23
- package/dist/cli.js.map +1 -1
- package/dist/{db-EHQDB5OL.js → db-SWJUUSFX.js} +2 -2
- package/dist/engine-EGPAS2EX.js +10 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/session-Y4MICATZ.js +15 -0
- package/dist/session-Y4MICATZ.js.map +1 -0
- package/dist/static/index.html +1873 -276
- package/dist/writer-4ZEAKUFD.js +12 -0
- package/dist/writer-4ZEAKUFD.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-CSRPOGHY.js +0 -354
- package/dist/chunk-CSRPOGHY.js.map +0 -1
- package/dist/chunk-GD5VNHIN.js.map +0 -1
- package/dist/chunk-VVSCAH2B.js.map +0 -1
- package/dist/engine-PKLXW6OF.js +0 -9
- /package/dist/{db-EHQDB5OL.js.map → db-SWJUUSFX.js.map} +0 -0
- /package/dist/{engine-PKLXW6OF.js.map → engine-EGPAS2EX.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DashboardWriter
|
|
4
|
+
} from "./chunk-EYO44OMN.js";
|
|
5
|
+
import {
|
|
6
|
+
DashboardDB
|
|
7
|
+
} from "./chunk-JYWQT2B4.js";
|
|
2
8
|
import {
|
|
3
9
|
formatAllAgentsInfo,
|
|
4
10
|
formatPermissionsTable
|
|
@@ -14,7 +20,7 @@ import {
|
|
|
14
20
|
UndoStack,
|
|
15
21
|
gateCommand,
|
|
16
22
|
getBashgymIntegration
|
|
17
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-2RPTM6EQ.js";
|
|
18
24
|
import "./chunk-SG752FZC.js";
|
|
19
25
|
import "./chunk-DLP2O6PN.js";
|
|
20
26
|
import {
|
|
@@ -22,9 +28,10 @@ import {
|
|
|
22
28
|
getDefaultConfig,
|
|
23
29
|
loadConfig
|
|
24
30
|
} from "./chunk-A535VV7N.js";
|
|
31
|
+
import "./chunk-QWZGB4V3.js";
|
|
25
32
|
import {
|
|
26
33
|
allowForSession
|
|
27
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-FRMAIRQ2.js";
|
|
28
35
|
import {
|
|
29
36
|
RiskScorer
|
|
30
37
|
} from "./chunk-DEAF6PYM.js";
|
|
@@ -35,9 +42,6 @@ import {
|
|
|
35
42
|
import {
|
|
36
43
|
MoltbotHooks
|
|
37
44
|
} from "./chunk-J37RHCFJ.js";
|
|
38
|
-
import {
|
|
39
|
-
DashboardDB
|
|
40
|
-
} from "./chunk-CSRPOGHY.js";
|
|
41
45
|
import "./chunk-7OCVIDC7.js";
|
|
42
46
|
|
|
43
47
|
// src/cli.ts
|
|
@@ -1014,7 +1018,7 @@ var EgressPatternMatcher = class {
|
|
|
1014
1018
|
// src/policy/ward/egress.ts
|
|
1015
1019
|
var DashboardDB2 = null;
|
|
1016
1020
|
try {
|
|
1017
|
-
const dbModule = await import("./db-
|
|
1021
|
+
const dbModule = await import("./db-SWJUUSFX.js");
|
|
1018
1022
|
DashboardDB2 = dbModule.DashboardDB;
|
|
1019
1023
|
} catch {
|
|
1020
1024
|
}
|
|
@@ -1422,6 +1426,10 @@ ${passed} passed, ${failed} failed. Fix issues above.
|
|
|
1422
1426
|
|
|
1423
1427
|
// src/watch.ts
|
|
1424
1428
|
import chalk3 from "chalk";
|
|
1429
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1430
|
+
import { parse, stringify as stringify2 } from "yaml";
|
|
1431
|
+
var dashboardWriter = null;
|
|
1432
|
+
var riskScorer = null;
|
|
1425
1433
|
function cleanup() {
|
|
1426
1434
|
if (process.stdin.isTTY) {
|
|
1427
1435
|
process.stdin.setRawMode(false);
|
|
@@ -1440,7 +1448,24 @@ async function startWatch(options) {
|
|
|
1440
1448
|
console.log(chalk3.dim(" Press Ctrl+C to stop"));
|
|
1441
1449
|
console.log();
|
|
1442
1450
|
const bashbros = new BashBros(configPath);
|
|
1443
|
-
bashbros.
|
|
1451
|
+
const config = bashbros.getConfig();
|
|
1452
|
+
try {
|
|
1453
|
+
dashboardWriter = new DashboardWriter();
|
|
1454
|
+
riskScorer = new RiskScorer();
|
|
1455
|
+
const sessionId = dashboardWriter.startSession(config.agent, process.cwd());
|
|
1456
|
+
if (options.verbose) {
|
|
1457
|
+
console.log(chalk3.dim(` Session: ${sessionId}`));
|
|
1458
|
+
}
|
|
1459
|
+
} catch (error) {
|
|
1460
|
+
if (options.verbose) {
|
|
1461
|
+
console.log(chalk3.yellow(" Dashboard recording disabled"));
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
let pendingCommand = null;
|
|
1465
|
+
let awaitingPromptResponse = false;
|
|
1466
|
+
function showBlockedPrompt(command, violations) {
|
|
1467
|
+
pendingCommand = command;
|
|
1468
|
+
awaitingPromptResponse = true;
|
|
1444
1469
|
console.log();
|
|
1445
1470
|
console.log(chalk3.red("\u{1F6E1}\uFE0F BashBros blocked a command"));
|
|
1446
1471
|
console.log();
|
|
@@ -1448,12 +1473,72 @@ async function startWatch(options) {
|
|
|
1448
1473
|
console.log(chalk3.dim(" Reason:"), violations[0].message);
|
|
1449
1474
|
console.log(chalk3.dim(" Policy:"), violations[0].rule);
|
|
1450
1475
|
console.log();
|
|
1451
|
-
console.log(chalk3.
|
|
1452
|
-
console.log(chalk3.cyan(
|
|
1453
|
-
console.log(chalk3.cyan(
|
|
1454
|
-
console.log();
|
|
1476
|
+
console.log(chalk3.yellow(" Allow this command?"));
|
|
1477
|
+
console.log(chalk3.cyan(" [y]"), "Allow once");
|
|
1478
|
+
console.log(chalk3.cyan(" [s]"), "Allow for session");
|
|
1479
|
+
console.log(chalk3.cyan(" [p]"), "Allow permanently");
|
|
1480
|
+
console.log(chalk3.cyan(" [n]"), "Block (default)");
|
|
1481
|
+
process.stdout.write(chalk3.dim("\n Choice: "));
|
|
1482
|
+
}
|
|
1483
|
+
function handlePromptResponse(choice) {
|
|
1484
|
+
if (!pendingCommand) return;
|
|
1485
|
+
const command = pendingCommand;
|
|
1486
|
+
pendingCommand = null;
|
|
1487
|
+
awaitingPromptResponse = false;
|
|
1488
|
+
console.log(choice);
|
|
1489
|
+
switch (choice.toLowerCase()) {
|
|
1490
|
+
case "y":
|
|
1491
|
+
console.log(chalk3.green(" \u2713 Allowed once"));
|
|
1492
|
+
console.log();
|
|
1493
|
+
bashbros.write(command + "\r");
|
|
1494
|
+
break;
|
|
1495
|
+
case "s":
|
|
1496
|
+
allowForSession(command);
|
|
1497
|
+
console.log(chalk3.green(" \u2713 Allowed for session"));
|
|
1498
|
+
console.log();
|
|
1499
|
+
bashbros.write(command + "\r");
|
|
1500
|
+
break;
|
|
1501
|
+
case "p":
|
|
1502
|
+
try {
|
|
1503
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
1504
|
+
const config2 = parse(content);
|
|
1505
|
+
if (!config2.commands) {
|
|
1506
|
+
config2.commands = { allow: [], block: [] };
|
|
1507
|
+
}
|
|
1508
|
+
if (!config2.commands.allow) {
|
|
1509
|
+
config2.commands.allow = [];
|
|
1510
|
+
}
|
|
1511
|
+
if (!config2.commands.allow.includes(command)) {
|
|
1512
|
+
config2.commands.allow.push(command);
|
|
1513
|
+
writeFileSync2(configPath, stringify2(config2));
|
|
1514
|
+
}
|
|
1515
|
+
console.log(chalk3.green(" \u2713 Added to allowlist permanently"));
|
|
1516
|
+
console.log();
|
|
1517
|
+
bashbros.write(command + "\r");
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
console.log(chalk3.red(" \u2717 Failed to update config"));
|
|
1520
|
+
console.log();
|
|
1521
|
+
}
|
|
1522
|
+
break;
|
|
1523
|
+
case "n":
|
|
1524
|
+
default:
|
|
1525
|
+
console.log(chalk3.yellow(" \u2717 Blocked"));
|
|
1526
|
+
console.log();
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
bashbros.on("blocked", (command, violations) => {
|
|
1531
|
+
if (dashboardWriter && riskScorer) {
|
|
1532
|
+
const risk = riskScorer.score(command);
|
|
1533
|
+
dashboardWriter.recordCommand(command, false, risk, violations, 0);
|
|
1534
|
+
}
|
|
1535
|
+
showBlockedPrompt(command, violations);
|
|
1455
1536
|
});
|
|
1456
1537
|
bashbros.on("allowed", (result) => {
|
|
1538
|
+
if (dashboardWriter && riskScorer) {
|
|
1539
|
+
const risk = riskScorer.score(result.command);
|
|
1540
|
+
dashboardWriter.recordCommand(result.command, true, risk, [], result.duration);
|
|
1541
|
+
}
|
|
1457
1542
|
if (options.verbose) {
|
|
1458
1543
|
console.log(chalk3.green("\u2713"), chalk3.dim(result.command));
|
|
1459
1544
|
}
|
|
@@ -1465,6 +1550,10 @@ async function startWatch(options) {
|
|
|
1465
1550
|
console.error(chalk3.red("Error:"), error.message);
|
|
1466
1551
|
});
|
|
1467
1552
|
bashbros.on("exit", (exitCode) => {
|
|
1553
|
+
if (dashboardWriter) {
|
|
1554
|
+
dashboardWriter.endSession();
|
|
1555
|
+
dashboardWriter.close();
|
|
1556
|
+
}
|
|
1468
1557
|
cleanup();
|
|
1469
1558
|
process.exit(exitCode ?? 0);
|
|
1470
1559
|
});
|
|
@@ -1472,14 +1561,32 @@ async function startWatch(options) {
|
|
|
1472
1561
|
cleanup();
|
|
1473
1562
|
console.log();
|
|
1474
1563
|
console.log(chalk3.yellow("Stopping BashBros..."));
|
|
1564
|
+
if (dashboardWriter) {
|
|
1565
|
+
dashboardWriter.endSession();
|
|
1566
|
+
dashboardWriter.close();
|
|
1567
|
+
}
|
|
1475
1568
|
bashbros.stop();
|
|
1476
1569
|
process.exit(0);
|
|
1477
1570
|
});
|
|
1478
1571
|
process.on("SIGTERM", () => {
|
|
1479
1572
|
cleanup();
|
|
1573
|
+
if (dashboardWriter) {
|
|
1574
|
+
dashboardWriter.endSession();
|
|
1575
|
+
dashboardWriter.close();
|
|
1576
|
+
}
|
|
1480
1577
|
bashbros.stop();
|
|
1481
1578
|
process.exit(0);
|
|
1482
1579
|
});
|
|
1580
|
+
process.on("uncaughtException", (error) => {
|
|
1581
|
+
console.error(chalk3.red("Unexpected error:"), error.message);
|
|
1582
|
+
if (dashboardWriter) {
|
|
1583
|
+
dashboardWriter.crashSession();
|
|
1584
|
+
dashboardWriter.close();
|
|
1585
|
+
}
|
|
1586
|
+
cleanup();
|
|
1587
|
+
bashbros.stop();
|
|
1588
|
+
process.exit(1);
|
|
1589
|
+
});
|
|
1483
1590
|
bashbros.start();
|
|
1484
1591
|
let commandBuffer = "";
|
|
1485
1592
|
if (process.stdout.isTTY) {
|
|
@@ -1498,11 +1605,31 @@ async function startWatch(options) {
|
|
|
1498
1605
|
const str = data.toString();
|
|
1499
1606
|
for (const char of str) {
|
|
1500
1607
|
const code = char.charCodeAt(0);
|
|
1608
|
+
if (awaitingPromptResponse) {
|
|
1609
|
+
if (code === 3) {
|
|
1610
|
+
pendingCommand = null;
|
|
1611
|
+
awaitingPromptResponse = false;
|
|
1612
|
+
console.log(chalk3.yellow("Cancelled"));
|
|
1613
|
+
console.log();
|
|
1614
|
+
} else if (char === "\r" || char === "\n") {
|
|
1615
|
+
handlePromptResponse("n");
|
|
1616
|
+
} else if (code >= 32) {
|
|
1617
|
+
handlePromptResponse(char);
|
|
1618
|
+
}
|
|
1619
|
+
continue;
|
|
1620
|
+
}
|
|
1501
1621
|
if (char === "\r" || char === "\n") {
|
|
1502
1622
|
const command = commandBuffer.trim();
|
|
1503
1623
|
commandBuffer = "";
|
|
1504
1624
|
if (command) {
|
|
1505
|
-
bashbros.
|
|
1625
|
+
const violations = bashbros.validateOnly(command);
|
|
1626
|
+
if (violations.length > 0) {
|
|
1627
|
+
bashbros.write("");
|
|
1628
|
+
bashbros.write("\r");
|
|
1629
|
+
bashbros.emit("blocked", command, violations);
|
|
1630
|
+
} else {
|
|
1631
|
+
bashbros.write("\r");
|
|
1632
|
+
}
|
|
1506
1633
|
} else {
|
|
1507
1634
|
bashbros.write("\r");
|
|
1508
1635
|
}
|
|
@@ -1533,8 +1660,8 @@ async function startWatch(options) {
|
|
|
1533
1660
|
|
|
1534
1661
|
// src/allow.ts
|
|
1535
1662
|
import chalk4 from "chalk";
|
|
1536
|
-
import { readFileSync as
|
|
1537
|
-
import { parse, stringify as
|
|
1663
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1664
|
+
import { parse as parse2, stringify as stringify3 } from "yaml";
|
|
1538
1665
|
async function handleAllow(command, options) {
|
|
1539
1666
|
if (options.once) {
|
|
1540
1667
|
allowForSession(command);
|
|
@@ -1549,8 +1676,8 @@ async function handleAllow(command, options) {
|
|
|
1549
1676
|
process.exit(1);
|
|
1550
1677
|
}
|
|
1551
1678
|
try {
|
|
1552
|
-
const content =
|
|
1553
|
-
const config =
|
|
1679
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
1680
|
+
const config = parse2(content);
|
|
1554
1681
|
if (!config.commands) {
|
|
1555
1682
|
config.commands = { allow: [], block: [] };
|
|
1556
1683
|
}
|
|
@@ -1559,7 +1686,7 @@ async function handleAllow(command, options) {
|
|
|
1559
1686
|
}
|
|
1560
1687
|
if (!config.commands.allow.includes(command)) {
|
|
1561
1688
|
config.commands.allow.push(command);
|
|
1562
|
-
|
|
1689
|
+
writeFileSync3(configPath, stringify3(config));
|
|
1563
1690
|
console.log(chalk4.green("\u2713"), `Added to allowlist: ${command}`);
|
|
1564
1691
|
console.log(chalk4.dim(` Updated ${configPath}`));
|
|
1565
1692
|
} else {
|
|
@@ -1586,6 +1713,16 @@ import { WebSocketServer } from "ws";
|
|
|
1586
1713
|
import { createServer } from "http";
|
|
1587
1714
|
import { fileURLToPath } from "url";
|
|
1588
1715
|
import { dirname, join as join4 } from "path";
|
|
1716
|
+
import { homedir as homedir4 } from "os";
|
|
1717
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
1718
|
+
import { parse as parse3, stringify as stringify4 } from "yaml";
|
|
1719
|
+
function getDefaultDbPath() {
|
|
1720
|
+
const bashbrosDir = join4(homedir4(), ".bashbros");
|
|
1721
|
+
if (!existsSync5(bashbrosDir)) {
|
|
1722
|
+
mkdirSync2(bashbrosDir, { recursive: true });
|
|
1723
|
+
}
|
|
1724
|
+
return join4(bashbrosDir, "dashboard.db");
|
|
1725
|
+
}
|
|
1589
1726
|
var DashboardServer = class {
|
|
1590
1727
|
app;
|
|
1591
1728
|
server = null;
|
|
@@ -1597,7 +1734,7 @@ var DashboardServer = class {
|
|
|
1597
1734
|
constructor(config = {}) {
|
|
1598
1735
|
this.port = config.port ?? 17800;
|
|
1599
1736
|
this.bind = config.bind ?? "127.0.0.1";
|
|
1600
|
-
this.db = new DashboardDB(config.dbPath ??
|
|
1737
|
+
this.db = new DashboardDB(config.dbPath ?? getDefaultDbPath());
|
|
1601
1738
|
this.app = express();
|
|
1602
1739
|
this.setupMiddleware();
|
|
1603
1740
|
this.setupRoutes();
|
|
@@ -1696,6 +1833,226 @@ var DashboardServer = class {
|
|
|
1696
1833
|
res.status(500).json({ error: "Failed to deny block" });
|
|
1697
1834
|
}
|
|
1698
1835
|
});
|
|
1836
|
+
this.app.get("/api/sessions", (req, res) => {
|
|
1837
|
+
try {
|
|
1838
|
+
const filter = {};
|
|
1839
|
+
if (req.query.status) filter.status = req.query.status;
|
|
1840
|
+
if (req.query.agent) filter.agent = req.query.agent;
|
|
1841
|
+
if (req.query.limit) filter.limit = parseInt(req.query.limit, 10);
|
|
1842
|
+
if (req.query.offset) filter.offset = parseInt(req.query.offset, 10);
|
|
1843
|
+
if (req.query.since) filter.since = new Date(req.query.since);
|
|
1844
|
+
if (req.query.until) filter.until = new Date(req.query.until);
|
|
1845
|
+
const sessions = this.db.getSessions(filter);
|
|
1846
|
+
res.json(sessions);
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
res.status(500).json({ error: "Failed to fetch sessions" });
|
|
1849
|
+
}
|
|
1850
|
+
});
|
|
1851
|
+
this.app.get("/api/sessions/active", (_req, res) => {
|
|
1852
|
+
try {
|
|
1853
|
+
const session = this.db.getActiveSession();
|
|
1854
|
+
if (!session) {
|
|
1855
|
+
res.json(null);
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
res.json(session);
|
|
1859
|
+
} catch (error) {
|
|
1860
|
+
res.status(500).json({ error: "Failed to fetch active session" });
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
this.app.get("/api/sessions/:id", (req, res) => {
|
|
1864
|
+
try {
|
|
1865
|
+
const session = this.db.getSession(req.params.id);
|
|
1866
|
+
if (!session) {
|
|
1867
|
+
res.status(404).json({ error: "Session not found" });
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
res.json(session);
|
|
1871
|
+
} catch (error) {
|
|
1872
|
+
res.status(500).json({ error: "Failed to fetch session" });
|
|
1873
|
+
}
|
|
1874
|
+
});
|
|
1875
|
+
this.app.get("/api/sessions/:id/commands", (req, res) => {
|
|
1876
|
+
try {
|
|
1877
|
+
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 100;
|
|
1878
|
+
const commands = this.db.getCommandsBySession(req.params.id, limit);
|
|
1879
|
+
res.json(commands);
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
res.status(500).json({ error: "Failed to fetch session commands" });
|
|
1882
|
+
}
|
|
1883
|
+
});
|
|
1884
|
+
this.app.get("/api/sessions/:id/metrics", (req, res) => {
|
|
1885
|
+
try {
|
|
1886
|
+
const session = this.db.getSession(req.params.id);
|
|
1887
|
+
if (!session) {
|
|
1888
|
+
res.status(404).json({ error: "Session not found" });
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
const metrics = this.db.getSessionMetrics(req.params.id);
|
|
1892
|
+
res.json(metrics);
|
|
1893
|
+
} catch (error) {
|
|
1894
|
+
res.status(500).json({ error: "Failed to fetch session metrics" });
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
this.app.get("/api/commands/live", (req, res) => {
|
|
1898
|
+
try {
|
|
1899
|
+
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 20;
|
|
1900
|
+
const commands = this.db.getLiveCommands(limit);
|
|
1901
|
+
res.json(commands);
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
res.status(500).json({ error: "Failed to fetch live commands" });
|
|
1904
|
+
}
|
|
1905
|
+
});
|
|
1906
|
+
this.app.get("/api/commands", (req, res) => {
|
|
1907
|
+
try {
|
|
1908
|
+
const filter = {};
|
|
1909
|
+
if (req.query.sessionId) filter.sessionId = req.query.sessionId;
|
|
1910
|
+
if (req.query.allowed !== void 0) filter.allowed = req.query.allowed === "true";
|
|
1911
|
+
if (req.query.riskLevel) filter.riskLevel = req.query.riskLevel;
|
|
1912
|
+
if (req.query.afterId) filter.afterId = req.query.afterId;
|
|
1913
|
+
if (req.query.since) filter.since = new Date(req.query.since);
|
|
1914
|
+
if (req.query.limit) filter.limit = parseInt(req.query.limit, 10);
|
|
1915
|
+
if (req.query.offset) filter.offset = parseInt(req.query.offset, 10);
|
|
1916
|
+
const commands = this.db.getCommands(filter);
|
|
1917
|
+
res.json(commands);
|
|
1918
|
+
} catch (error) {
|
|
1919
|
+
res.status(500).json({ error: "Failed to fetch commands" });
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
this.app.get("/api/tools/live", (req, res) => {
|
|
1923
|
+
try {
|
|
1924
|
+
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 50;
|
|
1925
|
+
const tools = this.db.getLiveToolUses(limit);
|
|
1926
|
+
res.json(tools);
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
res.status(500).json({ error: "Failed to fetch live tool uses" });
|
|
1929
|
+
}
|
|
1930
|
+
});
|
|
1931
|
+
this.app.get("/api/tools", (req, res) => {
|
|
1932
|
+
try {
|
|
1933
|
+
const filter = {};
|
|
1934
|
+
if (req.query.toolName) filter.toolName = req.query.toolName;
|
|
1935
|
+
if (req.query.since) filter.since = new Date(req.query.since);
|
|
1936
|
+
if (req.query.limit) filter.limit = parseInt(req.query.limit, 10);
|
|
1937
|
+
if (req.query.offset) filter.offset = parseInt(req.query.offset, 10);
|
|
1938
|
+
const tools = this.db.getToolUses(filter);
|
|
1939
|
+
res.json(tools);
|
|
1940
|
+
} catch (error) {
|
|
1941
|
+
res.status(500).json({ error: "Failed to fetch tool uses" });
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
this.app.get("/api/tools/stats", (_req, res) => {
|
|
1945
|
+
try {
|
|
1946
|
+
const stats = this.db.getToolUseStats();
|
|
1947
|
+
res.json(stats);
|
|
1948
|
+
} catch (error) {
|
|
1949
|
+
res.status(500).json({ error: "Failed to fetch tool use stats" });
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
this.app.get("/api/bro/status", (_req, res) => {
|
|
1953
|
+
try {
|
|
1954
|
+
const status = this.db.getLatestBroStatus();
|
|
1955
|
+
res.json(status);
|
|
1956
|
+
} catch (error) {
|
|
1957
|
+
res.status(500).json({ error: "Failed to fetch Bro status" });
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
this.app.get("/api/bro/events", (req, res) => {
|
|
1961
|
+
try {
|
|
1962
|
+
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 100;
|
|
1963
|
+
const sessionId = req.query.sessionId;
|
|
1964
|
+
const events = this.db.getBroEvents(limit, sessionId);
|
|
1965
|
+
res.json(events);
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
res.status(500).json({ error: "Failed to fetch Bro events" });
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
this.app.get("/api/bro/models", async (_req, res) => {
|
|
1971
|
+
try {
|
|
1972
|
+
const controller = new AbortController();
|
|
1973
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
1974
|
+
const response = await fetch("http://localhost:11434/api/tags", {
|
|
1975
|
+
signal: controller.signal
|
|
1976
|
+
});
|
|
1977
|
+
clearTimeout(timeout);
|
|
1978
|
+
if (!response.ok) {
|
|
1979
|
+
res.json({ available: false, models: [] });
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
const data = await response.json();
|
|
1983
|
+
const models = data.models?.map((m) => m.name) || [];
|
|
1984
|
+
res.json({ available: true, models });
|
|
1985
|
+
} catch (error) {
|
|
1986
|
+
res.json({ available: false, models: [] });
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
this.app.post("/api/bro/model", (req, res) => {
|
|
1990
|
+
try {
|
|
1991
|
+
const { model } = req.body;
|
|
1992
|
+
if (!model) {
|
|
1993
|
+
res.status(400).json({ error: "Model name required" });
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
const controlPath = join4(homedir4(), ".bashbros", "model-control.json");
|
|
1997
|
+
writeFileSync4(controlPath, JSON.stringify({
|
|
1998
|
+
model,
|
|
1999
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2000
|
+
}), "utf-8");
|
|
2001
|
+
res.json({ success: true, model });
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
res.status(500).json({ error: "Failed to change model" });
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
this.app.post("/api/bro/scan", (_req, res) => {
|
|
2007
|
+
try {
|
|
2008
|
+
const controlPath = join4(homedir4(), ".bashbros", "scan-control.json");
|
|
2009
|
+
writeFileSync4(controlPath, JSON.stringify({
|
|
2010
|
+
action: "scan",
|
|
2011
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2012
|
+
}), "utf-8");
|
|
2013
|
+
res.json({ success: true, message: "Scan requested" });
|
|
2014
|
+
} catch (error) {
|
|
2015
|
+
res.status(500).json({ error: "Failed to trigger scan" });
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
this.app.get("/api/exposures", (req, res) => {
|
|
2019
|
+
try {
|
|
2020
|
+
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 100;
|
|
2021
|
+
const exposures = this.db.getRecentExposures(limit);
|
|
2022
|
+
res.json(exposures);
|
|
2023
|
+
} catch (error) {
|
|
2024
|
+
res.status(500).json({ error: "Failed to fetch exposures" });
|
|
2025
|
+
}
|
|
2026
|
+
});
|
|
2027
|
+
this.app.get("/api/config", (_req, res) => {
|
|
2028
|
+
try {
|
|
2029
|
+
const configPath = findConfig();
|
|
2030
|
+
if (!configPath) {
|
|
2031
|
+
res.status(404).json({ error: "No config file found" });
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
const content = readFileSync4(configPath, "utf-8");
|
|
2035
|
+
const config = parse3(content);
|
|
2036
|
+
res.json(config);
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
res.status(500).json({ error: "Failed to load config" });
|
|
2039
|
+
}
|
|
2040
|
+
});
|
|
2041
|
+
this.app.post("/api/config", (req, res) => {
|
|
2042
|
+
try {
|
|
2043
|
+
const configPath = findConfig();
|
|
2044
|
+
if (!configPath) {
|
|
2045
|
+
res.status(404).json({ error: "No config file found" });
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
const config = req.body;
|
|
2049
|
+
const content = stringify4(config);
|
|
2050
|
+
writeFileSync4(configPath, content, "utf-8");
|
|
2051
|
+
res.json({ success: true });
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
res.status(500).json({ error: "Failed to save config" });
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
1699
2056
|
const __filename = fileURLToPath(import.meta.url);
|
|
1700
2057
|
const __dirname = dirname(__filename);
|
|
1701
2058
|
const staticPath = join4(__dirname, "static");
|
|
@@ -1962,8 +2319,8 @@ program.command("script <description>").description("Generate a shell script fro
|
|
|
1962
2319
|
if (script) {
|
|
1963
2320
|
console.log(chalk5.cyan(script));
|
|
1964
2321
|
if (options.output) {
|
|
1965
|
-
const { writeFileSync:
|
|
1966
|
-
|
|
2322
|
+
const { writeFileSync: writeFileSync5 } = await import("fs");
|
|
2323
|
+
writeFileSync5(options.output, script, { mode: 493 });
|
|
1967
2324
|
console.log(chalk5.green(`
|
|
1968
2325
|
\u2713 Saved to ${options.output}`));
|
|
1969
2326
|
}
|
|
@@ -2079,11 +2436,30 @@ hookCmd.command("status").description("Check Claude Code hook status").action(()
|
|
|
2079
2436
|
console.log();
|
|
2080
2437
|
console.log(` Claude Code: ${status.claudeInstalled ? chalk5.green("installed") : chalk5.yellow("not found")}`);
|
|
2081
2438
|
console.log(` BashBros hooks: ${status.hooksInstalled ? chalk5.green("active") : chalk5.dim("not installed")}`);
|
|
2439
|
+
console.log(` All-tools recording: ${status.allToolsInstalled ? chalk5.green("active") : chalk5.dim("not installed")}`);
|
|
2082
2440
|
if (status.hooks.length > 0) {
|
|
2083
2441
|
console.log(` Active hooks: ${status.hooks.join(", ")}`);
|
|
2084
2442
|
}
|
|
2085
2443
|
console.log();
|
|
2086
2444
|
});
|
|
2445
|
+
hookCmd.command("install-all-tools").description("Install hook to record ALL Claude Code tool uses (not just Bash)").action(() => {
|
|
2446
|
+
const result = ClaudeCodeHooks.installAllTools();
|
|
2447
|
+
if (result.success) {
|
|
2448
|
+
console.log(chalk5.green("\u2713"), result.message);
|
|
2449
|
+
} else {
|
|
2450
|
+
console.log(chalk5.red("\u2717"), result.message);
|
|
2451
|
+
process.exit(1);
|
|
2452
|
+
}
|
|
2453
|
+
});
|
|
2454
|
+
hookCmd.command("uninstall-all-tools").description("Remove all-tools recording hook").action(() => {
|
|
2455
|
+
const result = ClaudeCodeHooks.uninstallAllTools();
|
|
2456
|
+
if (result.success) {
|
|
2457
|
+
console.log(chalk5.green("\u2713"), result.message);
|
|
2458
|
+
} else {
|
|
2459
|
+
console.log(chalk5.red("\u2717"), result.message);
|
|
2460
|
+
process.exit(1);
|
|
2461
|
+
}
|
|
2462
|
+
});
|
|
2087
2463
|
var moltbotCmd = program.command("moltbot").alias("clawdbot").description("Manage Moltbot/Clawdbot integration");
|
|
2088
2464
|
moltbotCmd.command("install").description("Install BashBros hooks into Moltbot").action(() => {
|
|
2089
2465
|
const result = MoltbotHooks.install();
|
|
@@ -2223,14 +2599,135 @@ program.command("permissions").description("Show combined permissions view acros
|
|
|
2223
2599
|
console.log(formatPermissionsTable(agents));
|
|
2224
2600
|
console.log();
|
|
2225
2601
|
});
|
|
2226
|
-
program.command("gate <command>").description("Check if a command should be allowed (used by hooks)").action(async (command) => {
|
|
2602
|
+
program.command("gate <command>").description("Check if a command should be allowed (used by hooks)").option("-y, --yes", "Skip interactive prompt and block").action(async (command, options) => {
|
|
2227
2603
|
const result = await gateCommand(command);
|
|
2228
2604
|
if (!result.allowed) {
|
|
2229
|
-
|
|
2230
|
-
|
|
2605
|
+
if (process.stdin.isTTY && !options.yes) {
|
|
2606
|
+
const { allowForSession: allowForSession2 } = await import("./session-Y4MICATZ.js");
|
|
2607
|
+
const { readFileSync: readFileSync5, writeFileSync: writeFileSync5 } = await import("fs");
|
|
2608
|
+
const { parse: parse4, stringify: stringify5 } = await import("yaml");
|
|
2609
|
+
const { findConfig: findConfig2 } = await import("./config-43SK6SFI.js");
|
|
2610
|
+
console.error();
|
|
2611
|
+
console.error(chalk5.red("\u{1F6E1}\uFE0F BashBros blocked a command"));
|
|
2612
|
+
console.error();
|
|
2613
|
+
console.error(chalk5.dim(" Command:"), command);
|
|
2614
|
+
console.error(chalk5.dim(" Reason:"), result.reason);
|
|
2615
|
+
console.error();
|
|
2616
|
+
console.error(chalk5.yellow(" Allow this command?"));
|
|
2617
|
+
console.error(chalk5.cyan(" [y]"), "Allow once");
|
|
2618
|
+
console.error(chalk5.cyan(" [s]"), "Allow for session");
|
|
2619
|
+
console.error(chalk5.cyan(" [p]"), "Allow permanently");
|
|
2620
|
+
console.error(chalk5.cyan(" [n]"), "Block (default)");
|
|
2621
|
+
process.stderr.write(chalk5.dim("\n Choice: "));
|
|
2622
|
+
const choice = await new Promise((resolve) => {
|
|
2623
|
+
if (process.stdin.isTTY) {
|
|
2624
|
+
process.stdin.setRawMode(true);
|
|
2625
|
+
}
|
|
2626
|
+
process.stdin.resume();
|
|
2627
|
+
process.stdin.once("data", (data) => {
|
|
2628
|
+
if (process.stdin.isTTY) {
|
|
2629
|
+
process.stdin.setRawMode(false);
|
|
2630
|
+
}
|
|
2631
|
+
const char = data.toString().toLowerCase();
|
|
2632
|
+
console.error(char);
|
|
2633
|
+
resolve(char);
|
|
2634
|
+
});
|
|
2635
|
+
});
|
|
2636
|
+
switch (choice) {
|
|
2637
|
+
case "y":
|
|
2638
|
+
console.error(chalk5.green(" \u2713 Allowed once"));
|
|
2639
|
+
process.exit(0);
|
|
2640
|
+
break;
|
|
2641
|
+
case "s":
|
|
2642
|
+
allowForSession2(command);
|
|
2643
|
+
console.error(chalk5.green(" \u2713 Allowed for session"));
|
|
2644
|
+
process.exit(0);
|
|
2645
|
+
break;
|
|
2646
|
+
case "p":
|
|
2647
|
+
try {
|
|
2648
|
+
const configPath = findConfig2();
|
|
2649
|
+
if (configPath) {
|
|
2650
|
+
const content = readFileSync5(configPath, "utf-8");
|
|
2651
|
+
const config = parse4(content);
|
|
2652
|
+
if (!config.commands) config.commands = { allow: [], block: [] };
|
|
2653
|
+
if (!config.commands.allow) config.commands.allow = [];
|
|
2654
|
+
if (!config.commands.allow.includes(command)) {
|
|
2655
|
+
config.commands.allow.push(command);
|
|
2656
|
+
writeFileSync5(configPath, stringify5(config));
|
|
2657
|
+
}
|
|
2658
|
+
console.error(chalk5.green(" \u2713 Added to allowlist permanently"));
|
|
2659
|
+
process.exit(0);
|
|
2660
|
+
}
|
|
2661
|
+
} catch {
|
|
2662
|
+
console.error(chalk5.red(" \u2717 Failed to update config"));
|
|
2663
|
+
}
|
|
2664
|
+
process.exit(2);
|
|
2665
|
+
break;
|
|
2666
|
+
default:
|
|
2667
|
+
console.error(chalk5.yellow(" \u2717 Blocked"));
|
|
2668
|
+
process.exit(2);
|
|
2669
|
+
}
|
|
2670
|
+
} else {
|
|
2671
|
+
console.error(`Blocked: ${result.reason}`);
|
|
2672
|
+
process.exit(2);
|
|
2673
|
+
}
|
|
2231
2674
|
}
|
|
2232
2675
|
process.exit(0);
|
|
2233
2676
|
});
|
|
2677
|
+
program.command("record-tool").description("Record a Claude Code tool execution (used by hooks)").option("--marker <marker>", "Hook marker (ignored, used for identification)").action(async () => {
|
|
2678
|
+
const eventJson = process.env.CLAUDE_HOOK_EVENT || "";
|
|
2679
|
+
if (!eventJson) {
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
try {
|
|
2683
|
+
const event = JSON.parse(eventJson);
|
|
2684
|
+
const events = Array.isArray(event) ? event : [event];
|
|
2685
|
+
const { DashboardWriter: DashboardWriter2 } = await import("./writer-4ZEAKUFD.js");
|
|
2686
|
+
const writer = new DashboardWriter2();
|
|
2687
|
+
for (const evt of events) {
|
|
2688
|
+
const toolName = evt.tool_name || evt.tool || "unknown";
|
|
2689
|
+
const toolInput = evt.tool_input || evt.input || {};
|
|
2690
|
+
const toolOutput = evt.tool_output || evt.output || "";
|
|
2691
|
+
let inputStr;
|
|
2692
|
+
if (typeof toolInput === "string") {
|
|
2693
|
+
inputStr = toolInput;
|
|
2694
|
+
} else if (typeof toolInput === "object") {
|
|
2695
|
+
inputStr = JSON.stringify(toolInput, null, 2);
|
|
2696
|
+
} else {
|
|
2697
|
+
inputStr = String(toolInput);
|
|
2698
|
+
}
|
|
2699
|
+
let outputStr;
|
|
2700
|
+
let exitCode = null;
|
|
2701
|
+
let success = null;
|
|
2702
|
+
if (typeof toolOutput === "object" && toolOutput !== null) {
|
|
2703
|
+
outputStr = (toolOutput.stdout || "") + (toolOutput.stderr || "");
|
|
2704
|
+
exitCode = toolOutput.exit_code ?? toolOutput.exitCode ?? null;
|
|
2705
|
+
if (exitCode !== null) {
|
|
2706
|
+
success = exitCode === 0;
|
|
2707
|
+
}
|
|
2708
|
+
} else {
|
|
2709
|
+
outputStr = String(toolOutput || "");
|
|
2710
|
+
}
|
|
2711
|
+
const repoName = evt.repo?.name ?? null;
|
|
2712
|
+
const repoPath = evt.repo?.path ?? null;
|
|
2713
|
+
writer.recordToolUse({
|
|
2714
|
+
toolName,
|
|
2715
|
+
toolInput: inputStr,
|
|
2716
|
+
toolOutput: outputStr,
|
|
2717
|
+
exitCode,
|
|
2718
|
+
success,
|
|
2719
|
+
cwd: evt.cwd || process.cwd(),
|
|
2720
|
+
repoName,
|
|
2721
|
+
repoPath
|
|
2722
|
+
});
|
|
2723
|
+
const preview = inputStr.substring(0, 40).replace(/\n/g, " ");
|
|
2724
|
+
console.log(`[BashBros] ${toolName}: ${preview}${inputStr.length > 40 ? "..." : ""}`);
|
|
2725
|
+
}
|
|
2726
|
+
writer.close();
|
|
2727
|
+
} catch (e) {
|
|
2728
|
+
console.error(`[BashBros] Error recording tool: ${e instanceof Error ? e.message : e}`);
|
|
2729
|
+
}
|
|
2730
|
+
});
|
|
2234
2731
|
program.command("record <command>").description("Record a command execution (used by hooks)").option("-o, --output <output>", "Command output").option("-e, --exit-code <code>", "Exit code", "0").action(async (command, options) => {
|
|
2235
2732
|
if (!metricsCollector) metricsCollector = new MetricsCollector();
|
|
2236
2733
|
if (!costEstimator) costEstimator = new CostEstimator();
|