bashbros 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +453 -0
  3. package/dist/audit-MCFNGOIM.js +11 -0
  4. package/dist/audit-MCFNGOIM.js.map +1 -0
  5. package/dist/chunk-43W3RVEL.js +2910 -0
  6. package/dist/chunk-43W3RVEL.js.map +1 -0
  7. package/dist/chunk-4R4GV5V2.js +213 -0
  8. package/dist/chunk-4R4GV5V2.js.map +1 -0
  9. package/dist/chunk-7OCVIDC7.js +12 -0
  10. package/dist/chunk-7OCVIDC7.js.map +1 -0
  11. package/dist/chunk-CSRPOGHY.js +354 -0
  12. package/dist/chunk-CSRPOGHY.js.map +1 -0
  13. package/dist/chunk-DEAF6PYM.js +212 -0
  14. package/dist/chunk-DEAF6PYM.js.map +1 -0
  15. package/dist/chunk-DLP2O6PN.js +273 -0
  16. package/dist/chunk-DLP2O6PN.js.map +1 -0
  17. package/dist/chunk-GD5VNHIN.js +519 -0
  18. package/dist/chunk-GD5VNHIN.js.map +1 -0
  19. package/dist/chunk-ID2O2QTI.js +269 -0
  20. package/dist/chunk-ID2O2QTI.js.map +1 -0
  21. package/dist/chunk-J37RHCFJ.js +357 -0
  22. package/dist/chunk-J37RHCFJ.js.map +1 -0
  23. package/dist/chunk-SB4JS3GU.js +456 -0
  24. package/dist/chunk-SB4JS3GU.js.map +1 -0
  25. package/dist/chunk-SG752FZC.js +200 -0
  26. package/dist/chunk-SG752FZC.js.map +1 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +2448 -0
  29. package/dist/cli.js.map +1 -0
  30. package/dist/config-CZMIGNPF.js +13 -0
  31. package/dist/config-CZMIGNPF.js.map +1 -0
  32. package/dist/config-parser-XHE7BC7H.js +13 -0
  33. package/dist/config-parser-XHE7BC7H.js.map +1 -0
  34. package/dist/db-EHQDB5OL.js +11 -0
  35. package/dist/db-EHQDB5OL.js.map +1 -0
  36. package/dist/display-IN4NRJJS.js +18 -0
  37. package/dist/display-IN4NRJJS.js.map +1 -0
  38. package/dist/engine-PKLXW6OF.js +9 -0
  39. package/dist/engine-PKLXW6OF.js.map +1 -0
  40. package/dist/index.d.ts +1498 -0
  41. package/dist/index.js +552 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/moltbot-DXZFVK3X.js +11 -0
  44. package/dist/moltbot-DXZFVK3X.js.map +1 -0
  45. package/dist/ollama-HY35OHW4.js +9 -0
  46. package/dist/ollama-HY35OHW4.js.map +1 -0
  47. package/dist/risk-scorer-Y6KF2XCZ.js +9 -0
  48. package/dist/risk-scorer-Y6KF2XCZ.js.map +1 -0
  49. package/dist/static/index.html +410 -0
  50. package/package.json +68 -0
@@ -0,0 +1,2910 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AuditLogger
4
+ } from "./chunk-SG752FZC.js";
5
+ import {
6
+ OllamaClient
7
+ } from "./chunk-DLP2O6PN.js";
8
+ import {
9
+ loadConfig
10
+ } from "./chunk-SB4JS3GU.js";
11
+ import {
12
+ PolicyEngine
13
+ } from "./chunk-GD5VNHIN.js";
14
+ import {
15
+ __require
16
+ } from "./chunk-7OCVIDC7.js";
17
+
18
+ // src/integration/bashgym.ts
19
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, watch } from "fs";
20
+ import { join } from "path";
21
+ import { homedir } from "os";
22
+ import { EventEmitter } from "events";
23
+ var DEFAULT_SETTINGS = {
24
+ version: "1.0",
25
+ updated_at: null,
26
+ updated_by: null,
27
+ integration: {
28
+ enabled: false,
29
+ linked_at: null
30
+ },
31
+ capture: {
32
+ mode: "successful_only",
33
+ auto_stream: true
34
+ },
35
+ training: {
36
+ auto_enabled: false,
37
+ quality_threshold: 50,
38
+ trigger: "quality_based"
39
+ },
40
+ security: {
41
+ bashbros_primary: true,
42
+ policy_path: null
43
+ },
44
+ model_sync: {
45
+ auto_export_ollama: true,
46
+ ollama_model_name: "bashgym-sidekick",
47
+ notify_on_update: true
48
+ }
49
+ };
50
+ var BashgymIntegration = class extends EventEmitter {
51
+ integrationDir;
52
+ settings = null;
53
+ manifest = null;
54
+ settingsWatcher = null;
55
+ modelWatcher = null;
56
+ sessionId;
57
+ traceBuffer = [];
58
+ securityEvents = [];
59
+ currentPrompt = "";
60
+ // Directory paths
61
+ tracesDir;
62
+ pendingDir;
63
+ modelsDir;
64
+ configDir;
65
+ statusDir;
66
+ constructor(integrationDir) {
67
+ super();
68
+ this.integrationDir = integrationDir || join(homedir(), ".bashgym", "integration");
69
+ this.tracesDir = join(this.integrationDir, "traces");
70
+ this.pendingDir = join(this.tracesDir, "pending");
71
+ this.modelsDir = join(this.integrationDir, "models");
72
+ this.configDir = join(this.integrationDir, "config");
73
+ this.statusDir = join(this.integrationDir, "status");
74
+ this.sessionId = `bashbros-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
75
+ }
76
+ /**
77
+ * Initialize the integration
78
+ */
79
+ async initialize() {
80
+ if (!existsSync(this.integrationDir)) {
81
+ return false;
82
+ }
83
+ this.settings = this.loadSettings();
84
+ if (!this.settings?.integration.enabled) {
85
+ return false;
86
+ }
87
+ this.manifest = this.loadManifest();
88
+ this.startWatching();
89
+ this.updateStatus();
90
+ this.emit("connected");
91
+ return true;
92
+ }
93
+ /**
94
+ * Check if bashgym is available
95
+ */
96
+ isAvailable() {
97
+ return existsSync(this.integrationDir) && existsSync(this.configDir);
98
+ }
99
+ /**
100
+ * Check if integration is linked
101
+ */
102
+ isLinked() {
103
+ const settings = this.getSettings();
104
+ return !!(settings?.integration.enabled && settings.integration.linked_at !== null);
105
+ }
106
+ // =========================================================================
107
+ // Settings Management
108
+ // =========================================================================
109
+ getSettings() {
110
+ if (!this.settings) {
111
+ this.settings = this.loadSettings();
112
+ }
113
+ return this.settings;
114
+ }
115
+ loadSettings() {
116
+ const settingsPath = join(this.configDir, "settings.json");
117
+ if (!existsSync(settingsPath)) {
118
+ return null;
119
+ }
120
+ try {
121
+ const content = readFileSync(settingsPath, "utf-8");
122
+ return JSON.parse(content);
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+ updateSettings(updates) {
128
+ const current = this.getSettings() || { ...DEFAULT_SETTINGS };
129
+ const updated = {
130
+ ...current,
131
+ ...updates,
132
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
133
+ updated_by: "bashbros"
134
+ };
135
+ const settingsPath = join(this.configDir, "settings.json");
136
+ writeFileSync(settingsPath, JSON.stringify(updated, null, 2));
137
+ this.settings = updated;
138
+ this.emit("settings:changed", updated);
139
+ }
140
+ // =========================================================================
141
+ // Trace Export
142
+ // =========================================================================
143
+ /**
144
+ * Start a new trace session
145
+ */
146
+ startSession(prompt) {
147
+ this.currentPrompt = prompt;
148
+ this.traceBuffer = [];
149
+ this.securityEvents = [];
150
+ }
151
+ /**
152
+ * Add a step to the current trace
153
+ */
154
+ addStep(step) {
155
+ this.traceBuffer.push({
156
+ ...step,
157
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
158
+ });
159
+ }
160
+ /**
161
+ * Add a command result to the trace
162
+ */
163
+ addCommandResult(result) {
164
+ this.addStep({
165
+ tool_name: "Bash",
166
+ command: result.command,
167
+ output: result.output || "",
168
+ success: result.allowed && !result.error,
169
+ exit_code: result.exitCode,
170
+ cwd: process.cwd()
171
+ });
172
+ if (result.violations.length > 0) {
173
+ this.securityEvents.push({
174
+ type: "violation",
175
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
176
+ command: result.command,
177
+ violation: result.violations[0]
178
+ });
179
+ }
180
+ }
181
+ /**
182
+ * Add a file operation to the trace
183
+ */
184
+ addFileOperation(operation, path, success, output) {
185
+ this.addStep({
186
+ tool_name: operation,
187
+ command: path,
188
+ output: output || "",
189
+ success
190
+ });
191
+ }
192
+ /**
193
+ * End the session and export the trace
194
+ */
195
+ async endSession(verificationPassed = false) {
196
+ if (!this.currentPrompt || this.traceBuffer.length === 0) {
197
+ return null;
198
+ }
199
+ const settings = this.getSettings();
200
+ if (!settings?.integration.enabled) {
201
+ return null;
202
+ }
203
+ if (settings.capture.mode === "successful_only" && !verificationPassed) {
204
+ this.clearSession();
205
+ return null;
206
+ }
207
+ const traceData = {
208
+ version: "1.0",
209
+ metadata: {
210
+ user_initial_prompt: this.currentPrompt,
211
+ source_tool: "bashbros",
212
+ session_id: this.sessionId,
213
+ verification_passed: verificationPassed,
214
+ capture_mode: settings.capture.mode
215
+ },
216
+ trace: this.traceBuffer,
217
+ bashbros_extensions: {
218
+ security_events: this.securityEvents,
219
+ sidekick_annotations: {
220
+ teachable_moment: this.determineTeachableMoment(),
221
+ complexity: this.determineComplexity()
222
+ }
223
+ }
224
+ };
225
+ const filename = `${Date.now()}-${this.sessionId.slice(-8)}.json`;
226
+ const filepath = join(this.pendingDir, filename);
227
+ try {
228
+ if (!existsSync(this.pendingDir)) {
229
+ mkdirSync(this.pendingDir, { recursive: true });
230
+ }
231
+ writeFileSync(filepath, JSON.stringify(traceData, null, 2));
232
+ this.emit("trace:exported", filename);
233
+ this.clearSession();
234
+ return filename;
235
+ } catch (error) {
236
+ console.error("Failed to export trace:", error);
237
+ return null;
238
+ }
239
+ }
240
+ clearSession() {
241
+ this.currentPrompt = "";
242
+ this.traceBuffer = [];
243
+ this.securityEvents = [];
244
+ this.sessionId = `bashbros-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
245
+ }
246
+ determineTeachableMoment() {
247
+ const hasMultipleSteps = this.traceBuffer.length >= 3;
248
+ const hasErrorRecovery = this.traceBuffer.some(
249
+ (s, i) => !s.success && i < this.traceBuffer.length - 1 && this.traceBuffer[i + 1].success
250
+ );
251
+ const toolNames = new Set(this.traceBuffer.map((s) => s.tool_name));
252
+ const hasDiverseTools = toolNames.size >= 2;
253
+ return hasMultipleSteps && (hasErrorRecovery || hasDiverseTools);
254
+ }
255
+ determineComplexity() {
256
+ const stepCount = this.traceBuffer.length;
257
+ const toolCount = new Set(this.traceBuffer.map((s) => s.tool_name)).size;
258
+ const hasErrors = this.traceBuffer.some((s) => !s.success);
259
+ if (stepCount <= 3 && toolCount <= 2 && !hasErrors) {
260
+ return "easy";
261
+ }
262
+ if (stepCount >= 10 || toolCount >= 4 || this.securityEvents.length > 0) {
263
+ return "hard";
264
+ }
265
+ return "medium";
266
+ }
267
+ // =========================================================================
268
+ // Model Management
269
+ // =========================================================================
270
+ /**
271
+ * Get current model version
272
+ */
273
+ getCurrentModelVersion() {
274
+ const manifest = this.loadManifest();
275
+ return manifest?.latest || null;
276
+ }
277
+ /**
278
+ * Get model manifest
279
+ */
280
+ getModelManifest() {
281
+ return this.manifest || this.loadManifest();
282
+ }
283
+ loadManifest() {
284
+ const manifestPath = join(this.modelsDir, "manifest.json");
285
+ if (!existsSync(manifestPath)) {
286
+ return null;
287
+ }
288
+ try {
289
+ const content = readFileSync(manifestPath, "utf-8");
290
+ return JSON.parse(content);
291
+ } catch {
292
+ return null;
293
+ }
294
+ }
295
+ /**
296
+ * Get path to latest GGUF model
297
+ */
298
+ getLatestModelPath() {
299
+ const latestPath = join(this.modelsDir, "latest", "sidekick.gguf");
300
+ if (existsSync(latestPath)) {
301
+ return latestPath;
302
+ }
303
+ return null;
304
+ }
305
+ /**
306
+ * Get Ollama model name for sidekick
307
+ */
308
+ getOllamaModelName() {
309
+ const settings = this.getSettings();
310
+ return settings?.model_sync.ollama_model_name || "bashgym-sidekick";
311
+ }
312
+ // =========================================================================
313
+ // File Watching
314
+ // =========================================================================
315
+ startWatching() {
316
+ const settingsPath = join(this.configDir, "settings.json");
317
+ if (existsSync(settingsPath)) {
318
+ try {
319
+ this.settingsWatcher = watch(settingsPath, (eventType) => {
320
+ if (eventType === "change") {
321
+ const newSettings = this.loadSettings();
322
+ if (newSettings) {
323
+ this.settings = newSettings;
324
+ this.emit("settings:changed", newSettings);
325
+ }
326
+ }
327
+ });
328
+ } catch {
329
+ }
330
+ }
331
+ const manifestPath = join(this.modelsDir, "manifest.json");
332
+ if (existsSync(manifestPath)) {
333
+ try {
334
+ this.modelWatcher = watch(manifestPath, (eventType) => {
335
+ if (eventType === "change") {
336
+ const oldVersion = this.manifest?.latest;
337
+ const newManifest = this.loadManifest();
338
+ if (newManifest && newManifest.latest !== oldVersion) {
339
+ this.manifest = newManifest;
340
+ this.emit("model:updated", newManifest.latest, newManifest);
341
+ }
342
+ }
343
+ });
344
+ } catch {
345
+ }
346
+ }
347
+ }
348
+ stopWatching() {
349
+ if (this.settingsWatcher) {
350
+ this.settingsWatcher.close();
351
+ this.settingsWatcher = null;
352
+ }
353
+ if (this.modelWatcher) {
354
+ this.modelWatcher.close();
355
+ this.modelWatcher = null;
356
+ }
357
+ }
358
+ // =========================================================================
359
+ // Status Management
360
+ // =========================================================================
361
+ updateStatus() {
362
+ const statusPath = join(this.statusDir, "bashbros.json");
363
+ const status = {
364
+ heartbeat: (/* @__PURE__ */ new Date()).toISOString(),
365
+ version: "1.0",
366
+ session_id: this.sessionId,
367
+ active: true
368
+ };
369
+ try {
370
+ if (!existsSync(this.statusDir)) {
371
+ mkdirSync(this.statusDir, { recursive: true });
372
+ }
373
+ writeFileSync(statusPath, JSON.stringify(status, null, 2));
374
+ } catch {
375
+ }
376
+ }
377
+ /**
378
+ * Check if bashgym is actively running
379
+ */
380
+ isBashgymRunning() {
381
+ const statusPath = join(this.statusDir, "bashgym.json");
382
+ if (!existsSync(statusPath)) {
383
+ return false;
384
+ }
385
+ try {
386
+ const content = readFileSync(statusPath, "utf-8");
387
+ const status = JSON.parse(content);
388
+ const heartbeat = new Date(status.heartbeat);
389
+ const age = Date.now() - heartbeat.getTime();
390
+ return age < 5 * 60 * 1e3;
391
+ } catch {
392
+ return false;
393
+ }
394
+ }
395
+ // =========================================================================
396
+ // Cleanup
397
+ // =========================================================================
398
+ dispose() {
399
+ this.stopWatching();
400
+ this.emit("disconnected");
401
+ }
402
+ };
403
+ var _integration = null;
404
+ function getBashgymIntegration() {
405
+ if (!_integration) {
406
+ _integration = new BashgymIntegration();
407
+ }
408
+ return _integration;
409
+ }
410
+ function resetBashgymIntegration() {
411
+ if (_integration) {
412
+ _integration.dispose();
413
+ }
414
+ _integration = null;
415
+ }
416
+
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-PKLXW6OF.js");
586
+ const { RiskScorer } = await import("./risk-scorer-Y6KF2XCZ.js");
587
+ const { loadConfig: loadConfig2 } = await import("./config-CZMIGNPF.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
+ // src/core.ts
614
+ import * as pty from "node-pty";
615
+ import { EventEmitter as EventEmitter2 } from "events";
616
+ var BashBros = class extends EventEmitter2 {
617
+ config;
618
+ policy;
619
+ audit;
620
+ ptyProcess = null;
621
+ shell;
622
+ pendingCommand = "";
623
+ commandStartTime = 0;
624
+ constructor(configPath) {
625
+ super();
626
+ this.config = loadConfig(configPath);
627
+ this.policy = new PolicyEngine(this.config);
628
+ this.audit = new AuditLogger(this.config.audit);
629
+ this.shell = process.platform === "win32" ? "powershell.exe" : "bash";
630
+ }
631
+ start() {
632
+ this.ptyProcess = pty.spawn(this.shell, [], {
633
+ name: "xterm-color",
634
+ cols: 80,
635
+ rows: 30,
636
+ cwd: process.cwd(),
637
+ env: process.env
638
+ });
639
+ this.ptyProcess.onData((data) => {
640
+ this.emit("output", data);
641
+ });
642
+ this.ptyProcess.onExit(({ exitCode }) => {
643
+ this.emit("exit", exitCode);
644
+ });
645
+ }
646
+ execute(command) {
647
+ const startTime = Date.now();
648
+ const violations = this.policy.validate(command);
649
+ if (violations.length > 0) {
650
+ const result2 = {
651
+ command,
652
+ allowed: false,
653
+ duration: Date.now() - startTime,
654
+ violations
655
+ };
656
+ this.audit.log({
657
+ timestamp: /* @__PURE__ */ new Date(),
658
+ command,
659
+ allowed: false,
660
+ violations,
661
+ duration: result2.duration,
662
+ agent: this.config.agent
663
+ });
664
+ this.emit("blocked", command, violations);
665
+ return result2;
666
+ }
667
+ this.commandStartTime = startTime;
668
+ this.pendingCommand = command;
669
+ if (this.ptyProcess) {
670
+ this.ptyProcess.write(command + "\r");
671
+ }
672
+ const result = {
673
+ command,
674
+ allowed: true,
675
+ duration: Date.now() - startTime,
676
+ violations: []
677
+ };
678
+ this.audit.log({
679
+ timestamp: /* @__PURE__ */ new Date(),
680
+ command,
681
+ allowed: true,
682
+ violations: [],
683
+ duration: result.duration,
684
+ agent: this.config.agent
685
+ });
686
+ this.emit("allowed", result);
687
+ return result;
688
+ }
689
+ validateOnly(command) {
690
+ return this.policy.validate(command);
691
+ }
692
+ isAllowed(command) {
693
+ return this.policy.isAllowed(command);
694
+ }
695
+ resize(cols, rows) {
696
+ if (this.ptyProcess) {
697
+ this.ptyProcess.resize(cols, rows);
698
+ }
699
+ }
700
+ write(data) {
701
+ if (this.ptyProcess) {
702
+ this.ptyProcess.write(data);
703
+ }
704
+ }
705
+ stop() {
706
+ if (this.ptyProcess) {
707
+ this.ptyProcess.kill();
708
+ this.ptyProcess = null;
709
+ }
710
+ }
711
+ getConfig() {
712
+ return this.config;
713
+ }
714
+ };
715
+
716
+ // src/bro/profiler.ts
717
+ import { execFileSync } from "child_process";
718
+ import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync } from "fs";
719
+ import { homedir as homedir3, platform, arch, cpus, totalmem } from "os";
720
+ import { join as join3 } from "path";
721
+ var SAFE_VERSION_COMMANDS = {
722
+ python: ["--version"],
723
+ python3: ["--version"],
724
+ node: ["--version"],
725
+ rustc: ["--version"],
726
+ go: ["version"],
727
+ java: ["-version"],
728
+ ruby: ["--version"],
729
+ npm: ["--version"],
730
+ pnpm: ["--version"],
731
+ yarn: ["--version"],
732
+ pip: ["--version"],
733
+ pip3: ["--version"],
734
+ cargo: ["--version"],
735
+ brew: ["--version"],
736
+ git: ["--version"],
737
+ docker: ["--version"],
738
+ kubectl: ["version", "--client", "--short"],
739
+ aws: ["--version"],
740
+ gcloud: ["--version"],
741
+ ollama: ["--version"],
742
+ code: ["--version"],
743
+ cursor: ["--version"],
744
+ vim: ["--version"],
745
+ nvim: ["--version"],
746
+ nano: ["--version"],
747
+ emacs: ["--version"]
748
+ };
749
+ var SystemProfiler = class {
750
+ profile = null;
751
+ profilePath;
752
+ constructor() {
753
+ this.profilePath = join3(homedir3(), ".bashbros", "system-profile.json");
754
+ }
755
+ async scan() {
756
+ const profile = {
757
+ platform: platform(),
758
+ arch: arch(),
759
+ shell: this.detectShell(),
760
+ cpuCores: cpus().length,
761
+ memoryGB: Math.round(totalmem() / 1024 ** 3),
762
+ python: this.getVersionSafe("python") || this.getVersionSafe("python3"),
763
+ node: this.getVersionSafe("node"),
764
+ rust: this.getVersionSafe("rustc"),
765
+ go: this.getVersionSafe("go"),
766
+ java: this.getVersionSafe("java"),
767
+ ruby: this.getVersionSafe("ruby"),
768
+ npm: this.getVersionSafe("npm"),
769
+ pnpm: this.getVersionSafe("pnpm"),
770
+ yarn: this.getVersionSafe("yarn"),
771
+ pip: this.getVersionSafe("pip") || this.getVersionSafe("pip3"),
772
+ cargo: this.getVersionSafe("cargo"),
773
+ brew: this.getVersionSafe("brew"),
774
+ git: this.getVersionSafe("git"),
775
+ docker: this.getVersionSafe("docker"),
776
+ kubectl: this.getVersionSafe("kubectl"),
777
+ aws: this.getVersionSafe("aws"),
778
+ gcloud: this.getVersionSafe("gcloud"),
779
+ claude: this.commandExists("claude"),
780
+ clawdbot: this.commandExists("clawdbot"),
781
+ aider: this.commandExists("aider"),
782
+ ollama: this.getOllamaInfo(),
783
+ projectType: null,
784
+ projectDeps: [],
785
+ envVars: this.getEnvVarNames(),
786
+ commonCommands: [],
787
+ workingHours: null,
788
+ preferredEditor: this.detectEditor(),
789
+ timestamp: /* @__PURE__ */ new Date()
790
+ };
791
+ this.profile = profile;
792
+ this.save();
793
+ return profile;
794
+ }
795
+ scanProject(projectPath) {
796
+ let resolvedPath;
797
+ try {
798
+ resolvedPath = realpathSync(projectPath);
799
+ } catch {
800
+ resolvedPath = projectPath;
801
+ }
802
+ const updates = {
803
+ projectType: this.detectProjectType(resolvedPath),
804
+ projectDeps: this.detectDependencies(resolvedPath)
805
+ };
806
+ if (this.profile) {
807
+ this.profile = { ...this.profile, ...updates };
808
+ this.save();
809
+ }
810
+ return updates;
811
+ }
812
+ detectShell() {
813
+ if (platform() === "win32") {
814
+ return process.env.COMSPEC || "cmd.exe";
815
+ }
816
+ return process.env.SHELL || "/bin/bash";
817
+ }
818
+ /**
819
+ * SECURITY FIX: Use execFileSync with array args instead of string concatenation
820
+ */
821
+ getVersionSafe(cmd) {
822
+ const args = SAFE_VERSION_COMMANDS[cmd];
823
+ if (!args) {
824
+ return null;
825
+ }
826
+ try {
827
+ const output = execFileSync(cmd, args, {
828
+ encoding: "utf-8",
829
+ timeout: 5e3,
830
+ stdio: ["pipe", "pipe", "pipe"],
831
+ windowsHide: true
832
+ }).trim();
833
+ const version = this.parseVersion(output);
834
+ const path = this.getCommandPathSafe(cmd);
835
+ return { version, path };
836
+ } catch {
837
+ return null;
838
+ }
839
+ }
840
+ parseVersion(output) {
841
+ const match = output.match(/(\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)/i);
842
+ return match ? match[1] : output.split("\n")[0].slice(0, 50);
843
+ }
844
+ /**
845
+ * SECURITY FIX: Use execFileSync for which/where command
846
+ */
847
+ getCommandPathSafe(cmd) {
848
+ try {
849
+ const whichCmd = platform() === "win32" ? "where" : "which";
850
+ const result = execFileSync(whichCmd, [cmd], {
851
+ encoding: "utf-8",
852
+ timeout: 3e3,
853
+ stdio: ["pipe", "pipe", "pipe"],
854
+ windowsHide: true
855
+ }).trim().split("\n")[0];
856
+ return result;
857
+ } catch {
858
+ return cmd;
859
+ }
860
+ }
861
+ commandExists(cmd) {
862
+ try {
863
+ const whichCmd = platform() === "win32" ? "where" : "which";
864
+ execFileSync(whichCmd, [cmd], {
865
+ stdio: "pipe",
866
+ timeout: 3e3,
867
+ windowsHide: true
868
+ });
869
+ return true;
870
+ } catch {
871
+ return false;
872
+ }
873
+ }
874
+ getOllamaInfo() {
875
+ try {
876
+ const version = execFileSync("ollama", ["--version"], {
877
+ encoding: "utf-8",
878
+ timeout: 5e3,
879
+ windowsHide: true
880
+ }).trim();
881
+ let models = [];
882
+ try {
883
+ const modelList = execFileSync("ollama", ["list"], {
884
+ encoding: "utf-8",
885
+ timeout: 1e4,
886
+ windowsHide: true
887
+ });
888
+ models = modelList.split("\n").slice(1).map((line) => line.split(/\s+/)[0]).filter(Boolean);
889
+ } catch {
890
+ }
891
+ return { version, models };
892
+ } catch {
893
+ return null;
894
+ }
895
+ }
896
+ getEnvVarNames() {
897
+ const safePatterns = [
898
+ /^PATH$/i,
899
+ /^HOME$/i,
900
+ /^USER$/i,
901
+ /^SHELL$/i,
902
+ /^TERM$/i,
903
+ /^LANG$/i,
904
+ /^NODE_VERSION$/i,
905
+ /^PYTHON.*VERSION$/i,
906
+ /^JAVA_HOME$/i,
907
+ /^GOPATH$/i,
908
+ /^EDITOR$/i,
909
+ /^VISUAL$/i
910
+ ];
911
+ return Object.keys(process.env).filter(
912
+ (key) => safePatterns.some((pattern) => pattern.test(key))
913
+ );
914
+ }
915
+ detectEditor() {
916
+ const editor = process.env.EDITOR || process.env.VISUAL;
917
+ if (editor) return editor;
918
+ const editors = ["code", "cursor", "vim", "nvim", "nano", "emacs"];
919
+ for (const ed of editors) {
920
+ if (this.commandExists(ed)) return ed;
921
+ }
922
+ return null;
923
+ }
924
+ detectProjectType(projectPath) {
925
+ const checks = [
926
+ ["package.json", "node"],
927
+ ["pyproject.toml", "python"],
928
+ ["requirements.txt", "python"],
929
+ ["Cargo.toml", "rust"],
930
+ ["go.mod", "go"],
931
+ ["pom.xml", "java"],
932
+ ["build.gradle", "java"],
933
+ ["Gemfile", "ruby"],
934
+ ["composer.json", "php"]
935
+ ];
936
+ for (const [file, type] of checks) {
937
+ const filePath = join3(projectPath, file);
938
+ if (existsSync3(filePath)) {
939
+ try {
940
+ const realPath = realpathSync(filePath);
941
+ if (realPath.startsWith(realpathSync(projectPath))) {
942
+ return type;
943
+ }
944
+ } catch {
945
+ }
946
+ }
947
+ }
948
+ return null;
949
+ }
950
+ detectDependencies(projectPath) {
951
+ const deps = [];
952
+ const pkgPath = join3(projectPath, "package.json");
953
+ if (existsSync3(pkgPath)) {
954
+ try {
955
+ const realPkgPath = realpathSync(pkgPath);
956
+ if (realPkgPath.startsWith(realpathSync(projectPath))) {
957
+ const pkg = JSON.parse(readFileSync3(realPkgPath, "utf-8"));
958
+ deps.push(...Object.keys(pkg.dependencies || {}));
959
+ deps.push(...Object.keys(pkg.devDependencies || {}));
960
+ }
961
+ } catch {
962
+ }
963
+ }
964
+ const reqPath = join3(projectPath, "requirements.txt");
965
+ if (existsSync3(reqPath)) {
966
+ try {
967
+ const realReqPath = realpathSync(reqPath);
968
+ if (realReqPath.startsWith(realpathSync(projectPath))) {
969
+ const reqs = readFileSync3(realReqPath, "utf-8");
970
+ const packages = reqs.split("\n").map((line) => line.split(/[=<>]/)[0].trim()).filter(Boolean);
971
+ deps.push(...packages);
972
+ }
973
+ } catch {
974
+ }
975
+ }
976
+ return deps.slice(0, 100);
977
+ }
978
+ load() {
979
+ if (existsSync3(this.profilePath)) {
980
+ try {
981
+ const data = readFileSync3(this.profilePath, "utf-8");
982
+ this.profile = JSON.parse(data);
983
+ return this.profile;
984
+ } catch {
985
+ return null;
986
+ }
987
+ }
988
+ return null;
989
+ }
990
+ save() {
991
+ try {
992
+ const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync4, chmodSync } = __require("fs");
993
+ const dir = join3(homedir3(), ".bashbros");
994
+ if (!existsSync3(dir)) {
995
+ mkdirSync4(dir, { recursive: true, mode: 448 });
996
+ }
997
+ const filePath = this.profilePath;
998
+ writeFileSync4(filePath, JSON.stringify(this.profile, null, 2));
999
+ try {
1000
+ chmodSync(filePath, 384);
1001
+ } catch {
1002
+ }
1003
+ } catch {
1004
+ }
1005
+ }
1006
+ get() {
1007
+ return this.profile;
1008
+ }
1009
+ toContext() {
1010
+ if (!this.profile) return "System profile not available.";
1011
+ const p = this.profile;
1012
+ const lines = [
1013
+ `## System Context`,
1014
+ `- Platform: ${p.platform} (${p.arch})`,
1015
+ `- Shell: ${p.shell}`,
1016
+ `- CPU: ${p.cpuCores} cores, RAM: ${p.memoryGB}GB`,
1017
+ ""
1018
+ ];
1019
+ if (p.python) lines.push(`- Python: ${p.python.version}`);
1020
+ if (p.node) lines.push(`- Node: ${p.node.version}`);
1021
+ if (p.rust) lines.push(`- Rust: ${p.rust.version}`);
1022
+ if (p.go) lines.push(`- Go: ${p.go.version}`);
1023
+ if (p.git) lines.push(`- Git: ${p.git.version}`);
1024
+ if (p.docker) lines.push(`- Docker: ${p.docker.version}`);
1025
+ if (p.ollama) {
1026
+ lines.push(`- Ollama: ${p.ollama.version}`);
1027
+ if (p.ollama.models.length > 0) {
1028
+ lines.push(` Models: ${p.ollama.models.join(", ")}`);
1029
+ }
1030
+ }
1031
+ if (p.projectType) {
1032
+ lines.push("");
1033
+ lines.push(`## Project: ${p.projectType}`);
1034
+ if (p.projectDeps.length > 0) {
1035
+ lines.push(`Dependencies: ${p.projectDeps.slice(0, 20).join(", ")}`);
1036
+ }
1037
+ }
1038
+ return lines.join("\n");
1039
+ }
1040
+ };
1041
+
1042
+ // src/bro/router.ts
1043
+ var TaskRouter = class {
1044
+ rules;
1045
+ profile;
1046
+ constructor(profile = null) {
1047
+ this.profile = profile;
1048
+ this.rules = this.buildDefaultRules();
1049
+ }
1050
+ buildDefaultRules() {
1051
+ return [
1052
+ // Simple file operations → Bash Bro
1053
+ { pattern: /^ls\b/, route: "bro", reason: "Simple file listing" },
1054
+ { pattern: /^cat\s+\S+$/, route: "bro", reason: "Simple file read" },
1055
+ { pattern: /^head\b/, route: "bro", reason: "File head" },
1056
+ { pattern: /^tail\b/, route: "bro", reason: "File tail" },
1057
+ { pattern: /^wc\b/, route: "bro", reason: "Word count" },
1058
+ { pattern: /^pwd$/, route: "bro", reason: "Print directory" },
1059
+ { pattern: /^cd\s+/, route: "bro", reason: "Change directory" },
1060
+ { pattern: /^mkdir\s+/, route: "bro", reason: "Create directory" },
1061
+ { pattern: /^touch\s+/, route: "bro", reason: "Create file" },
1062
+ { pattern: /^cp\s+/, route: "bro", reason: "Copy file" },
1063
+ { pattern: /^mv\s+/, route: "bro", reason: "Move file" },
1064
+ { pattern: /^rm\s+(?!-rf)/, route: "bro", reason: "Remove file (safe)" },
1065
+ // Simple searches → Bash Bro
1066
+ { pattern: /^grep\s+-[ril]*\s+['"]?\w+['"]?\s+\S+$/, route: "bro", reason: "Simple grep" },
1067
+ { pattern: /^find\s+\.\s+-name\s+/, route: "bro", reason: "Simple find" },
1068
+ { pattern: /^which\s+/, route: "bro", reason: "Which command" },
1069
+ // Git simple operations → Bash Bro
1070
+ { pattern: /^git\s+status$/, route: "bro", reason: "Git status" },
1071
+ { pattern: /^git\s+branch$/, route: "bro", reason: "Git branch list" },
1072
+ { pattern: /^git\s+log\s+--oneline/, route: "bro", reason: "Git log" },
1073
+ { pattern: /^git\s+diff$/, route: "bro", reason: "Git diff" },
1074
+ { pattern: /^git\s+add\s+/, route: "bro", reason: "Git add" },
1075
+ // Package info → Bash Bro
1076
+ { pattern: /^npm\s+list/, route: "bro", reason: "NPM list" },
1077
+ { pattern: /^pip\s+list/, route: "bro", reason: "Pip list" },
1078
+ { pattern: /^pip\s+show\s+/, route: "bro", reason: "Pip show" },
1079
+ // Environment checks → Bash Bro
1080
+ { pattern: /^python\s+--version/, route: "bro", reason: "Python version" },
1081
+ { pattern: /^node\s+--version/, route: "bro", reason: "Node version" },
1082
+ { pattern: /^npm\s+--version/, route: "bro", reason: "NPM version" },
1083
+ { pattern: /^env$/, route: "bro", reason: "Environment vars" },
1084
+ { pattern: /^echo\s+\$\w+$/, route: "bro", reason: "Echo env var" },
1085
+ // Complex operations → Main agent
1086
+ { pattern: /refactor/i, route: "main", reason: "Refactoring requires reasoning" },
1087
+ { pattern: /implement/i, route: "main", reason: "Implementation requires reasoning" },
1088
+ { pattern: /explain/i, route: "main", reason: "Explanation requires reasoning" },
1089
+ { pattern: /debug/i, route: "main", reason: "Debugging requires reasoning" },
1090
+ { pattern: /fix\s+/i, route: "main", reason: "Fixing requires reasoning" },
1091
+ { pattern: /why/i, route: "main", reason: "Explanation required" },
1092
+ { pattern: /how\s+(do|can|should)/i, route: "main", reason: "Guidance required" },
1093
+ // Git complex → Main agent
1094
+ { pattern: /^git\s+rebase/, route: "main", reason: "Rebase needs oversight" },
1095
+ { pattern: /^git\s+merge/, route: "main", reason: "Merge needs oversight" },
1096
+ { pattern: /^git\s+reset/, route: "main", reason: "Reset needs oversight" },
1097
+ // Parallel tasks → Both
1098
+ { pattern: /^(npm|yarn|pnpm)\s+(test|run\s+test)/, route: "both", reason: "Tests can run in background" },
1099
+ { pattern: /^pytest/, route: "both", reason: "Tests can run in background" },
1100
+ { pattern: /^(npm|yarn|pnpm)\s+run\s+build/, route: "both", reason: "Build can run in background" },
1101
+ { pattern: /^docker\s+build/, route: "both", reason: "Docker build can run in background" }
1102
+ ];
1103
+ }
1104
+ route(command) {
1105
+ for (const rule of this.rules) {
1106
+ if (rule.pattern.test(command)) {
1107
+ return {
1108
+ decision: rule.route,
1109
+ reason: rule.reason,
1110
+ confidence: 0.9
1111
+ };
1112
+ }
1113
+ }
1114
+ if (this.looksSimple(command)) {
1115
+ return {
1116
+ decision: "bro",
1117
+ reason: "Appears to be a simple command",
1118
+ confidence: 0.6
1119
+ };
1120
+ }
1121
+ return {
1122
+ decision: "main",
1123
+ reason: "Complex or unknown command",
1124
+ confidence: 0.5
1125
+ };
1126
+ }
1127
+ looksSimple(command) {
1128
+ const words = command.split(/\s+/);
1129
+ if (words.length <= 3) return true;
1130
+ if (/[|><&;]/.test(command)) return false;
1131
+ if (/[$`(]/.test(command)) return false;
1132
+ return true;
1133
+ }
1134
+ addRule(pattern, route, reason) {
1135
+ this.rules.unshift({ pattern, route, reason });
1136
+ }
1137
+ updateProfile(profile) {
1138
+ this.profile = profile;
1139
+ if (profile.projectType === "python") {
1140
+ this.addRule(/^python\s+-c\s+/, "bro", "Simple Python one-liner");
1141
+ this.addRule(/^pip\s+install\s+/, "bro", "Pip install");
1142
+ }
1143
+ if (profile.projectType === "node") {
1144
+ this.addRule(/^npx\s+/, "bro", "NPX command");
1145
+ this.addRule(/^npm\s+install\s+/, "bro", "NPM install");
1146
+ }
1147
+ }
1148
+ };
1149
+
1150
+ // src/bro/suggester.ts
1151
+ var CommandSuggester = class {
1152
+ history = [];
1153
+ profile = null;
1154
+ patterns = /* @__PURE__ */ new Map();
1155
+ constructor(profile = null) {
1156
+ this.profile = profile;
1157
+ this.initPatterns();
1158
+ }
1159
+ initPatterns() {
1160
+ this.patterns.set("git status", ["git add .", "git diff", "git stash"]);
1161
+ this.patterns.set("git add", ['git commit -m ""', "git status"]);
1162
+ this.patterns.set("git commit", ["git push", "git log --oneline -5"]);
1163
+ this.patterns.set("git pull", ["git status", "git log --oneline -5"]);
1164
+ this.patterns.set("git checkout", ["git status", "git branch"]);
1165
+ this.patterns.set("npm install", ["npm run build", "npm test", "npm start"]);
1166
+ this.patterns.set("npm test", ["npm run build", "git add ."]);
1167
+ this.patterns.set("npm run build", ["npm start", "npm test"]);
1168
+ this.patterns.set("pip install", ["pip freeze", "python -m pytest"]);
1169
+ this.patterns.set("pytest", ["git add .", "python -m pytest -v"]);
1170
+ this.patterns.set("docker build", ["docker run", "docker images"]);
1171
+ this.patterns.set("docker run", ["docker ps", "docker logs"]);
1172
+ this.patterns.set("cd", ["ls", "ls -la", "git status"]);
1173
+ this.patterns.set("mkdir", ["cd", "touch"]);
1174
+ this.patterns.set("ls", ["cd", "cat", "vim"]);
1175
+ }
1176
+ suggest(context) {
1177
+ const suggestions = [];
1178
+ if (context.lastCommand) {
1179
+ const patternSuggestions = this.suggestFromPatterns(context.lastCommand);
1180
+ suggestions.push(...patternSuggestions);
1181
+ }
1182
+ const historySuggestions = this.suggestFromHistory(context);
1183
+ suggestions.push(...historySuggestions);
1184
+ const contextSuggestions = this.suggestFromContext(context);
1185
+ suggestions.push(...contextSuggestions);
1186
+ const unique = this.dedupeAndRank(suggestions);
1187
+ return unique.slice(0, 5);
1188
+ }
1189
+ suggestFromPatterns(lastCommand) {
1190
+ const suggestions = [];
1191
+ for (const [key, commands] of this.patterns) {
1192
+ if (lastCommand.startsWith(key)) {
1193
+ for (const cmd of commands) {
1194
+ suggestions.push({
1195
+ command: cmd,
1196
+ description: `Common follow-up to "${key}"`,
1197
+ confidence: 0.8,
1198
+ source: "pattern"
1199
+ });
1200
+ }
1201
+ break;
1202
+ }
1203
+ }
1204
+ return suggestions;
1205
+ }
1206
+ suggestFromHistory(context) {
1207
+ if (this.history.length < 3) return [];
1208
+ const suggestions = [];
1209
+ const recentCommands = this.history.slice(-20);
1210
+ const following = /* @__PURE__ */ new Map();
1211
+ for (let i = 0; i < recentCommands.length - 1; i++) {
1212
+ const current = recentCommands[i].command;
1213
+ const next = recentCommands[i + 1].command;
1214
+ if (context.lastCommand && current.startsWith(context.lastCommand.split(" ")[0])) {
1215
+ const count = following.get(next) || 0;
1216
+ following.set(next, count + 1);
1217
+ }
1218
+ }
1219
+ for (const [cmd, count] of following) {
1220
+ if (count >= 2) {
1221
+ suggestions.push({
1222
+ command: cmd,
1223
+ description: "Based on your history",
1224
+ confidence: Math.min(0.9, 0.5 + count * 0.1),
1225
+ source: "history"
1226
+ });
1227
+ }
1228
+ }
1229
+ return suggestions;
1230
+ }
1231
+ suggestFromContext(context) {
1232
+ const suggestions = [];
1233
+ if (context.projectType === "node" && context.cwd) {
1234
+ if (context.files?.includes("package.json")) {
1235
+ suggestions.push({
1236
+ command: "npm install",
1237
+ description: "Install dependencies",
1238
+ confidence: 0.7,
1239
+ source: "context"
1240
+ });
1241
+ }
1242
+ }
1243
+ if (context.projectType === "python" && context.cwd) {
1244
+ if (context.files?.includes("requirements.txt")) {
1245
+ suggestions.push({
1246
+ command: "pip install -r requirements.txt",
1247
+ description: "Install dependencies",
1248
+ confidence: 0.7,
1249
+ source: "context"
1250
+ });
1251
+ }
1252
+ }
1253
+ if (context.lastError) {
1254
+ if (context.lastError.includes("ModuleNotFoundError")) {
1255
+ const match = context.lastError.match(/No module named '(\w+)'/);
1256
+ if (match) {
1257
+ suggestions.push({
1258
+ command: `pip install ${match[1]}`,
1259
+ description: `Install missing module`,
1260
+ confidence: 0.9,
1261
+ source: "context"
1262
+ });
1263
+ }
1264
+ }
1265
+ if (context.lastError.includes("Cannot find module")) {
1266
+ suggestions.push({
1267
+ command: "npm install",
1268
+ description: "Install missing dependencies",
1269
+ confidence: 0.85,
1270
+ source: "context"
1271
+ });
1272
+ }
1273
+ }
1274
+ return suggestions;
1275
+ }
1276
+ dedupeAndRank(suggestions) {
1277
+ const seen = /* @__PURE__ */ new Set();
1278
+ const unique = [];
1279
+ suggestions.sort((a, b) => b.confidence - a.confidence);
1280
+ for (const s of suggestions) {
1281
+ if (!seen.has(s.command)) {
1282
+ seen.add(s.command);
1283
+ unique.push(s);
1284
+ }
1285
+ }
1286
+ return unique;
1287
+ }
1288
+ recordCommand(entry) {
1289
+ this.history.push(entry);
1290
+ if (this.history.length > 100) {
1291
+ this.history = this.history.slice(-100);
1292
+ }
1293
+ }
1294
+ updateProfile(profile) {
1295
+ this.profile = profile;
1296
+ }
1297
+ };
1298
+
1299
+ // src/bro/worker.ts
1300
+ import { spawn as spawn2 } from "child_process";
1301
+ import { EventEmitter as EventEmitter3 } from "events";
1302
+ var MAX_TASK_HISTORY = 100;
1303
+ function parseCommand(command) {
1304
+ const tokens = [];
1305
+ let current = "";
1306
+ let inQuote = null;
1307
+ for (let i = 0; i < command.length; i++) {
1308
+ const char = command[i];
1309
+ if (inQuote) {
1310
+ if (char === inQuote) {
1311
+ inQuote = null;
1312
+ } else {
1313
+ current += char;
1314
+ }
1315
+ } else if (char === '"' || char === "'") {
1316
+ inQuote = char;
1317
+ } else if (char === " " || char === " ") {
1318
+ if (current) {
1319
+ tokens.push(current);
1320
+ current = "";
1321
+ }
1322
+ } else {
1323
+ current += char;
1324
+ }
1325
+ }
1326
+ if (current) {
1327
+ tokens.push(current);
1328
+ }
1329
+ return {
1330
+ cmd: tokens[0] || "",
1331
+ args: tokens.slice(1)
1332
+ };
1333
+ }
1334
+ function validateCommand(command) {
1335
+ const dangerousPatterns = [
1336
+ /[;&|`$]/,
1337
+ // Shell operators and command substitution
1338
+ /\$\(/,
1339
+ // Command substitution
1340
+ />\s*>/,
1341
+ // Append redirect
1342
+ />\s*\//,
1343
+ // Redirect to absolute path
1344
+ /<\s*\//,
1345
+ // Input from absolute path
1346
+ /\|\s*\w+/
1347
+ // Pipe to command
1348
+ ];
1349
+ for (const pattern of dangerousPatterns) {
1350
+ if (pattern.test(command)) {
1351
+ return {
1352
+ valid: false,
1353
+ reason: `Command contains potentially dangerous pattern: ${pattern.source}`
1354
+ };
1355
+ }
1356
+ }
1357
+ return { valid: true };
1358
+ }
1359
+ var BackgroundWorker = class extends EventEmitter3 {
1360
+ tasks = /* @__PURE__ */ new Map();
1361
+ processes = /* @__PURE__ */ new Map();
1362
+ taskIdCounter = 0;
1363
+ spawn(command, cwd) {
1364
+ const validation = validateCommand(command);
1365
+ if (!validation.valid) {
1366
+ throw new Error(`Security: ${validation.reason}`);
1367
+ }
1368
+ const id = `task_${++this.taskIdCounter}`;
1369
+ const task = {
1370
+ id,
1371
+ command,
1372
+ status: "running",
1373
+ startTime: /* @__PURE__ */ new Date(),
1374
+ output: []
1375
+ };
1376
+ this.tasks.set(id, task);
1377
+ const { cmd, args } = parseCommand(command);
1378
+ if (!cmd) {
1379
+ task.status = "failed";
1380
+ task.endTime = /* @__PURE__ */ new Date();
1381
+ task.output.push("Error: Empty command");
1382
+ return task;
1383
+ }
1384
+ const proc = spawn2(cmd, args, {
1385
+ cwd: cwd || process.cwd(),
1386
+ shell: false,
1387
+ // CRITICAL: Never use shell: true
1388
+ stdio: ["pipe", "pipe", "pipe"],
1389
+ env: process.env
1390
+ });
1391
+ this.processes.set(id, proc);
1392
+ proc.stdout?.on("data", (data) => {
1393
+ const line = data.toString();
1394
+ task.output.push(line);
1395
+ this.emit("output", { taskId: id, data: line, stream: "stdout" });
1396
+ });
1397
+ proc.stderr?.on("data", (data) => {
1398
+ const line = data.toString();
1399
+ task.output.push(line);
1400
+ this.emit("output", { taskId: id, data: line, stream: "stderr" });
1401
+ });
1402
+ proc.on("close", (code) => {
1403
+ task.status = code === 0 ? "completed" : "failed";
1404
+ task.endTime = /* @__PURE__ */ new Date();
1405
+ task.exitCode = code ?? void 0;
1406
+ this.processes.delete(id);
1407
+ this.emit("complete", {
1408
+ taskId: id,
1409
+ exitCode: code,
1410
+ duration: task.endTime.getTime() - task.startTime.getTime()
1411
+ });
1412
+ this.notifyCompletion(task);
1413
+ this.cleanupOldTasks();
1414
+ });
1415
+ proc.on("error", (error) => {
1416
+ task.status = "failed";
1417
+ task.endTime = /* @__PURE__ */ new Date();
1418
+ task.output.push(`Error: ${error.message}`);
1419
+ this.processes.delete(id);
1420
+ this.emit("error", { taskId: id, error });
1421
+ });
1422
+ this.emit("started", { taskId: id, command });
1423
+ return task;
1424
+ }
1425
+ cancel(taskId) {
1426
+ const proc = this.processes.get(taskId);
1427
+ const task = this.tasks.get(taskId);
1428
+ if (proc && task) {
1429
+ proc.kill("SIGTERM");
1430
+ task.status = "cancelled";
1431
+ task.endTime = /* @__PURE__ */ new Date();
1432
+ this.processes.delete(taskId);
1433
+ return true;
1434
+ }
1435
+ return false;
1436
+ }
1437
+ getTask(taskId) {
1438
+ return this.tasks.get(taskId);
1439
+ }
1440
+ getRunningTasks() {
1441
+ return Array.from(this.tasks.values()).filter((t) => t.status === "running");
1442
+ }
1443
+ getAllTasks() {
1444
+ return Array.from(this.tasks.values());
1445
+ }
1446
+ getRecentTasks(limit = 10) {
1447
+ return Array.from(this.tasks.values()).sort((a, b) => b.startTime.getTime() - a.startTime.getTime()).slice(0, limit);
1448
+ }
1449
+ cleanupOldTasks() {
1450
+ const tasks = Array.from(this.tasks.entries()).filter(([_, t]) => t.status !== "running").sort((a, b) => b[1].startTime.getTime() - a[1].startTime.getTime());
1451
+ if (tasks.length > MAX_TASK_HISTORY) {
1452
+ const toRemove = tasks.slice(MAX_TASK_HISTORY);
1453
+ for (const [id] of toRemove) {
1454
+ this.tasks.delete(id);
1455
+ }
1456
+ }
1457
+ }
1458
+ notifyCompletion(task) {
1459
+ const duration = task.endTime ? Math.round((task.endTime.getTime() - task.startTime.getTime()) / 1e3) : 0;
1460
+ const icon = task.status === "completed" ? "\u2713" : "\u2717";
1461
+ const status = task.status === "completed" ? "completed" : "failed";
1462
+ console.log(`
1463
+ \u{1F91D} Bash Bro: Background task ${icon} ${status}`);
1464
+ console.log(` Command: ${task.command}`);
1465
+ console.log(` Duration: ${duration}s`);
1466
+ if (task.status === "failed" && task.output.length > 0) {
1467
+ const lastLines = task.output.slice(-3).join("").trim();
1468
+ if (lastLines) {
1469
+ console.log(` Last output: ${lastLines.slice(0, 100)}`);
1470
+ }
1471
+ }
1472
+ console.log();
1473
+ }
1474
+ formatStatus() {
1475
+ const running = this.getRunningTasks();
1476
+ if (running.length === 0) {
1477
+ return "No background tasks running.";
1478
+ }
1479
+ const lines = ["Background tasks:"];
1480
+ for (const task of running) {
1481
+ const elapsed = Math.round((Date.now() - task.startTime.getTime()) / 1e3);
1482
+ lines.push(` [${task.id}] ${task.command} (${elapsed}s)`);
1483
+ }
1484
+ return lines.join("\n");
1485
+ }
1486
+ };
1487
+
1488
+ // src/bro/bro.ts
1489
+ import { execFileSync as execFileSync2 } from "child_process";
1490
+ import { EventEmitter as EventEmitter4 } from "events";
1491
+ var SAFE_COMMANDS = /* @__PURE__ */ new Set([
1492
+ "ls",
1493
+ "dir",
1494
+ "cat",
1495
+ "head",
1496
+ "tail",
1497
+ "grep",
1498
+ "find",
1499
+ "wc",
1500
+ "pwd",
1501
+ "cd",
1502
+ "mkdir",
1503
+ "touch",
1504
+ "cp",
1505
+ "mv",
1506
+ "rm",
1507
+ "git",
1508
+ "npm",
1509
+ "npx",
1510
+ "pnpm",
1511
+ "yarn",
1512
+ "node",
1513
+ "python",
1514
+ "python3",
1515
+ "pip",
1516
+ "pip3",
1517
+ "pytest",
1518
+ "cargo",
1519
+ "go",
1520
+ "rustc",
1521
+ "docker",
1522
+ "kubectl",
1523
+ "echo",
1524
+ "which",
1525
+ "where",
1526
+ "type",
1527
+ "date",
1528
+ "whoami",
1529
+ "hostname",
1530
+ "env",
1531
+ "printenv"
1532
+ ]);
1533
+ function parseCommandSafe(command) {
1534
+ const tokens = [];
1535
+ let current = "";
1536
+ let inQuote = null;
1537
+ for (let i = 0; i < command.length; i++) {
1538
+ const char = command[i];
1539
+ if (inQuote) {
1540
+ if (char === inQuote) {
1541
+ inQuote = null;
1542
+ } else {
1543
+ current += char;
1544
+ }
1545
+ } else if (char === '"' || char === "'") {
1546
+ inQuote = char;
1547
+ } else if (char === " " || char === " ") {
1548
+ if (current) {
1549
+ tokens.push(current);
1550
+ current = "";
1551
+ }
1552
+ } else {
1553
+ current += char;
1554
+ }
1555
+ }
1556
+ if (current) {
1557
+ tokens.push(current);
1558
+ }
1559
+ if (tokens.length === 0) {
1560
+ return null;
1561
+ }
1562
+ const cmd = tokens[0];
1563
+ if (!SAFE_COMMANDS.has(cmd)) {
1564
+ return null;
1565
+ }
1566
+ return {
1567
+ cmd,
1568
+ args: tokens.slice(1)
1569
+ };
1570
+ }
1571
+ function validateCommandSafety(command) {
1572
+ const dangerousPatterns = [
1573
+ /[;&|`]/,
1574
+ // Shell operators
1575
+ /\$\(/,
1576
+ // Command substitution
1577
+ /\$\{/,
1578
+ // Variable expansion
1579
+ />\s*>/,
1580
+ // Append redirect
1581
+ /[<>]\s*\//,
1582
+ // Redirect to/from absolute path
1583
+ /\|\s*\w+/,
1584
+ // Pipe to command
1585
+ /\\x[0-9a-f]/i,
1586
+ // Hex escapes
1587
+ /\\[0-7]{3}/
1588
+ // Octal escapes
1589
+ ];
1590
+ for (const pattern of dangerousPatterns) {
1591
+ if (pattern.test(command)) {
1592
+ return { safe: false, reason: `Contains dangerous pattern` };
1593
+ }
1594
+ }
1595
+ return { safe: true };
1596
+ }
1597
+ var BashBro = class extends EventEmitter4 {
1598
+ profiler;
1599
+ router;
1600
+ suggester;
1601
+ worker;
1602
+ ollama = null;
1603
+ profile = null;
1604
+ config;
1605
+ ollamaAvailable = false;
1606
+ bashgymModelVersion = null;
1607
+ constructor(config = {}) {
1608
+ super();
1609
+ this.config = {
1610
+ enableSuggestions: true,
1611
+ enableRouting: true,
1612
+ enableBackground: true,
1613
+ enableOllama: true,
1614
+ enableBashgymIntegration: true,
1615
+ ...config
1616
+ };
1617
+ this.profiler = new SystemProfiler();
1618
+ this.router = new TaskRouter();
1619
+ this.suggester = new CommandSuggester();
1620
+ this.worker = new BackgroundWorker();
1621
+ if (this.config.enableOllama) {
1622
+ this.ollama = new OllamaClient({
1623
+ host: this.config.modelEndpoint,
1624
+ model: this.config.modelName
1625
+ });
1626
+ }
1627
+ this.worker.on("complete", (data) => this.emit("task:complete", data));
1628
+ this.worker.on("output", (data) => this.emit("task:output", data));
1629
+ this.worker.on("error", (data) => this.emit("task:error", data));
1630
+ if (this.config.enableBashgymIntegration) {
1631
+ this.initBashgymIntegration();
1632
+ }
1633
+ }
1634
+ /**
1635
+ * Initialize bashgym integration for model hot-swap
1636
+ */
1637
+ initBashgymIntegration() {
1638
+ try {
1639
+ const integration = getBashgymIntegration();
1640
+ integration.on("model:updated", (version, manifest) => {
1641
+ this.handleModelUpdate(version, manifest);
1642
+ });
1643
+ if (integration.isLinked()) {
1644
+ const modelName = integration.getOllamaModelName();
1645
+ const currentVersion = integration.getCurrentModelVersion();
1646
+ if (currentVersion && this.ollama) {
1647
+ this.ollama.setModel(`${modelName}:${currentVersion}`);
1648
+ this.bashgymModelVersion = currentVersion;
1649
+ console.log(`\u{1F91D} Bash Bro: Using bashgym sidekick model (${currentVersion})`);
1650
+ }
1651
+ }
1652
+ } catch {
1653
+ }
1654
+ }
1655
+ /**
1656
+ * Handle model update from bashgym (hot-swap)
1657
+ */
1658
+ handleModelUpdate(version, manifest) {
1659
+ if (!this.ollama) return;
1660
+ if (version === this.bashgymModelVersion) return;
1661
+ try {
1662
+ const integration = getBashgymIntegration();
1663
+ const modelName = integration.getOllamaModelName();
1664
+ this.ollama.setModel(`${modelName}:${version}`);
1665
+ this.bashgymModelVersion = version;
1666
+ console.log(`\u{1F91D} Bash Bro: Model hot-swapped to ${version}`);
1667
+ this.emit("model:updated", version);
1668
+ } catch (error) {
1669
+ console.error("Failed to hot-swap model:", error);
1670
+ }
1671
+ }
1672
+ async initialize() {
1673
+ this.profile = this.profiler.load();
1674
+ if (!this.profile || this.isProfileStale()) {
1675
+ console.log("\u{1F91D} Bash Bro: Scanning your system...");
1676
+ this.profile = await this.profiler.scan();
1677
+ console.log("\u{1F91D} Bash Bro: System profile updated!");
1678
+ }
1679
+ this.router.updateProfile(this.profile);
1680
+ this.suggester.updateProfile(this.profile);
1681
+ if (this.ollama) {
1682
+ this.ollamaAvailable = await this.ollama.isAvailable();
1683
+ if (this.ollamaAvailable) {
1684
+ console.log("\u{1F91D} Bash Bro: Ollama connected");
1685
+ }
1686
+ }
1687
+ this.emit("ready", this.profile);
1688
+ }
1689
+ isProfileStale() {
1690
+ if (!this.profile) return true;
1691
+ const age = Date.now() - new Date(this.profile.timestamp).getTime();
1692
+ const oneDay = 24 * 60 * 60 * 1e3;
1693
+ return age > oneDay;
1694
+ }
1695
+ scanProject(projectPath) {
1696
+ this.profiler.scanProject(projectPath);
1697
+ this.profile = this.profiler.get();
1698
+ if (this.profile) {
1699
+ this.router.updateProfile(this.profile);
1700
+ this.suggester.updateProfile(this.profile);
1701
+ }
1702
+ }
1703
+ route(command) {
1704
+ if (!this.config.enableRouting) {
1705
+ return { decision: "main", reason: "Routing disabled", confidence: 1 };
1706
+ }
1707
+ return this.router.route(command);
1708
+ }
1709
+ suggest(context) {
1710
+ if (!this.config.enableSuggestions) {
1711
+ return [];
1712
+ }
1713
+ return this.suggester.suggest(context);
1714
+ }
1715
+ /**
1716
+ * SECURITY FIX: Safe command execution with validation
1717
+ */
1718
+ async execute(command) {
1719
+ const safety = validateCommandSafety(command);
1720
+ if (!safety.safe) {
1721
+ return `Security: Command blocked - ${safety.reason}`;
1722
+ }
1723
+ const parsed = parseCommandSafe(command);
1724
+ if (!parsed) {
1725
+ return `Security: Command not in allowlist. Only safe commands can be executed directly.`;
1726
+ }
1727
+ try {
1728
+ const output = execFileSync2(parsed.cmd, parsed.args, {
1729
+ encoding: "utf-8",
1730
+ timeout: 3e4,
1731
+ cwd: process.cwd(),
1732
+ windowsHide: true
1733
+ });
1734
+ return output;
1735
+ } catch (error) {
1736
+ return error.message || "Command failed";
1737
+ }
1738
+ }
1739
+ runBackground(command, cwd) {
1740
+ if (!this.config.enableBackground) {
1741
+ throw new Error("Background tasks disabled");
1742
+ }
1743
+ return this.worker.spawn(command, cwd);
1744
+ }
1745
+ cancelBackground(taskId) {
1746
+ return this.worker.cancel(taskId);
1747
+ }
1748
+ getBackgroundTasks() {
1749
+ return this.worker.getRunningTasks();
1750
+ }
1751
+ getSystemContext() {
1752
+ return this.profiler.toContext();
1753
+ }
1754
+ getProfile() {
1755
+ return this.profile;
1756
+ }
1757
+ /**
1758
+ * Check if Ollama is available for AI features
1759
+ */
1760
+ isOllamaAvailable() {
1761
+ return this.ollamaAvailable;
1762
+ }
1763
+ /**
1764
+ * Ask Bash Bro (via Ollama) to suggest the next command
1765
+ */
1766
+ async aiSuggest(context) {
1767
+ if (!this.ollama || !this.ollamaAvailable) {
1768
+ return null;
1769
+ }
1770
+ return this.ollama.suggestCommand(context);
1771
+ }
1772
+ /**
1773
+ * Ask Bash Bro to explain a command
1774
+ */
1775
+ async aiExplain(command) {
1776
+ if (!this.ollama || !this.ollamaAvailable) {
1777
+ return "Ollama not available for explanations.";
1778
+ }
1779
+ return this.ollama.explainCommand(command);
1780
+ }
1781
+ /**
1782
+ * Ask Bash Bro to fix a failed command
1783
+ */
1784
+ async aiFix(command, error) {
1785
+ if (!this.ollama || !this.ollamaAvailable) {
1786
+ return null;
1787
+ }
1788
+ return this.ollama.fixCommand(command, error);
1789
+ }
1790
+ /**
1791
+ * Set the Ollama model to use
1792
+ */
1793
+ setModel(model) {
1794
+ if (this.ollama) {
1795
+ this.ollama.setModel(model);
1796
+ }
1797
+ }
1798
+ /**
1799
+ * Generate a shell script from natural language description
1800
+ */
1801
+ async aiGenerateScript(description) {
1802
+ if (!this.ollama || !this.ollamaAvailable) {
1803
+ return null;
1804
+ }
1805
+ const shell = this.profile?.shell || "bash";
1806
+ return this.ollama.generateScript(description, shell);
1807
+ }
1808
+ /**
1809
+ * Analyze command for security risks using AI
1810
+ */
1811
+ async aiAnalyzeSafety(command) {
1812
+ if (!this.ollama || !this.ollamaAvailable) {
1813
+ return {
1814
+ safe: true,
1815
+ risk: "low",
1816
+ explanation: "AI analysis not available.",
1817
+ suggestions: []
1818
+ };
1819
+ }
1820
+ return this.ollama.analyzeCommandSafety(command);
1821
+ }
1822
+ /**
1823
+ * Summarize a terminal session
1824
+ */
1825
+ async aiSummarize(commands) {
1826
+ if (!this.ollama || !this.ollamaAvailable) {
1827
+ return "AI not available for summaries.";
1828
+ }
1829
+ return this.ollama.summarizeSession(commands);
1830
+ }
1831
+ /**
1832
+ * Get AI help for a topic or command
1833
+ */
1834
+ async aiHelp(topic) {
1835
+ if (!this.ollama || !this.ollamaAvailable) {
1836
+ return "AI not available for help.";
1837
+ }
1838
+ return this.ollama.getHelp(topic);
1839
+ }
1840
+ /**
1841
+ * Convert natural language to command
1842
+ */
1843
+ async aiToCommand(description) {
1844
+ if (!this.ollama || !this.ollamaAvailable) {
1845
+ return null;
1846
+ }
1847
+ return this.ollama.naturalToCommand(description);
1848
+ }
1849
+ /**
1850
+ * Get bashgym sidekick model version (if using)
1851
+ */
1852
+ getBashgymModelVersion() {
1853
+ return this.bashgymModelVersion;
1854
+ }
1855
+ /**
1856
+ * Check if using bashgym-trained sidekick model
1857
+ */
1858
+ isUsingBashgymModel() {
1859
+ return this.bashgymModelVersion !== null;
1860
+ }
1861
+ /**
1862
+ * Force refresh the bashgym model (check for updates)
1863
+ */
1864
+ refreshBashgymModel() {
1865
+ if (!this.config.enableBashgymIntegration) {
1866
+ return false;
1867
+ }
1868
+ try {
1869
+ const integration = getBashgymIntegration();
1870
+ const currentVersion = integration.getCurrentModelVersion();
1871
+ if (currentVersion && currentVersion !== this.bashgymModelVersion) {
1872
+ const modelName = integration.getOllamaModelName();
1873
+ if (this.ollama) {
1874
+ this.ollama.setModel(`${modelName}:${currentVersion}`);
1875
+ this.bashgymModelVersion = currentVersion;
1876
+ this.emit("model:updated", currentVersion);
1877
+ return true;
1878
+ }
1879
+ }
1880
+ } catch {
1881
+ }
1882
+ return false;
1883
+ }
1884
+ // Format a nice status message
1885
+ status() {
1886
+ const lines = [
1887
+ "\u{1F91D} Bash Bro Status",
1888
+ "\u2500".repeat(40)
1889
+ ];
1890
+ if (this.profile) {
1891
+ lines.push(`Platform: ${this.profile.platform} (${this.profile.arch})`);
1892
+ lines.push(`Shell: ${this.profile.shell}`);
1893
+ if (this.profile.python) {
1894
+ lines.push(`Python: ${this.profile.python.version}`);
1895
+ }
1896
+ if (this.profile.node) {
1897
+ lines.push(`Node: ${this.profile.node.version}`);
1898
+ }
1899
+ if (this.profile.ollama) {
1900
+ lines.push(`Ollama: ${this.profile.ollama.version}`);
1901
+ if (this.profile.ollama.models.length > 0) {
1902
+ lines.push(` Models: ${this.profile.ollama.models.join(", ")}`);
1903
+ }
1904
+ }
1905
+ if (this.profile.projectType) {
1906
+ lines.push(`Project: ${this.profile.projectType}`);
1907
+ }
1908
+ }
1909
+ lines.push("");
1910
+ if (this.ollamaAvailable) {
1911
+ const model = this.ollama?.getModel() || "default";
1912
+ if (this.bashgymModelVersion) {
1913
+ lines.push(`AI: Connected (bashgym sidekick ${this.bashgymModelVersion})`);
1914
+ } else {
1915
+ lines.push(`AI: Connected (${model})`);
1916
+ }
1917
+ } else {
1918
+ lines.push("AI: Not connected (run Ollama for AI features)");
1919
+ }
1920
+ if (this.config.enableBashgymIntegration) {
1921
+ try {
1922
+ const integration = getBashgymIntegration();
1923
+ if (integration.isLinked()) {
1924
+ lines.push(`BashGym: Linked`);
1925
+ if (integration.isBashgymRunning()) {
1926
+ lines.push(` Status: Running`);
1927
+ }
1928
+ }
1929
+ } catch {
1930
+ }
1931
+ }
1932
+ lines.push("");
1933
+ lines.push(this.worker.formatStatus());
1934
+ return lines.join("\n");
1935
+ }
1936
+ };
1937
+
1938
+ // src/observability/metrics.ts
1939
+ var MetricsCollector = class {
1940
+ sessionId;
1941
+ startTime;
1942
+ commands = [];
1943
+ filesModified = /* @__PURE__ */ new Set();
1944
+ pathsAccessed = /* @__PURE__ */ new Set();
1945
+ constructor() {
1946
+ this.sessionId = this.generateSessionId();
1947
+ this.startTime = /* @__PURE__ */ new Date();
1948
+ }
1949
+ generateSessionId() {
1950
+ const now = /* @__PURE__ */ new Date();
1951
+ const date = now.toISOString().slice(0, 10).replace(/-/g, "");
1952
+ const time = now.toTimeString().slice(0, 8).replace(/:/g, "");
1953
+ const rand = Math.random().toString(36).slice(2, 6);
1954
+ return `${date}-${time}-${rand}`;
1955
+ }
1956
+ /**
1957
+ * Record a command execution
1958
+ */
1959
+ record(metric) {
1960
+ this.commands.push(metric);
1961
+ const paths = this.extractPaths(metric.command);
1962
+ for (const path of paths) {
1963
+ this.pathsAccessed.add(path);
1964
+ }
1965
+ if (this.isWriteCommand(metric.command)) {
1966
+ for (const path of paths) {
1967
+ this.filesModified.add(path);
1968
+ }
1969
+ }
1970
+ }
1971
+ /**
1972
+ * Get current session metrics
1973
+ */
1974
+ getMetrics() {
1975
+ const now = /* @__PURE__ */ new Date();
1976
+ const duration = now.getTime() - this.startTime.getTime();
1977
+ const riskDist = { safe: 0, caution: 0, dangerous: 0, critical: 0 };
1978
+ let totalRisk = 0;
1979
+ for (const cmd of this.commands) {
1980
+ riskDist[cmd.riskScore.level]++;
1981
+ totalRisk += cmd.riskScore.score;
1982
+ }
1983
+ const cmdFreq = /* @__PURE__ */ new Map();
1984
+ for (const cmd of this.commands) {
1985
+ const base = cmd.command.split(/\s+/)[0];
1986
+ cmdFreq.set(base, (cmdFreq.get(base) || 0) + 1);
1987
+ }
1988
+ const topCommands = [...cmdFreq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
1989
+ const violationsByType = {};
1990
+ for (const cmd of this.commands) {
1991
+ for (const v of cmd.violations) {
1992
+ violationsByType[v.type] = (violationsByType[v.type] || 0) + 1;
1993
+ }
1994
+ }
1995
+ const totalExecTime = this.commands.reduce((sum, c) => sum + c.duration, 0);
1996
+ const avgExecTime = this.commands.length > 0 ? totalExecTime / this.commands.length : 0;
1997
+ return {
1998
+ sessionId: this.sessionId,
1999
+ startTime: this.startTime,
2000
+ duration,
2001
+ commandCount: this.commands.length,
2002
+ blockedCount: this.commands.filter((c) => !c.allowed).length,
2003
+ uniqueCommands: cmdFreq.size,
2004
+ topCommands,
2005
+ riskDistribution: riskDist,
2006
+ avgRiskScore: this.commands.length > 0 ? totalRisk / this.commands.length : 0,
2007
+ avgExecutionTime: avgExecTime,
2008
+ totalExecutionTime: totalExecTime,
2009
+ filesModified: [...this.filesModified],
2010
+ pathsAccessed: [...this.pathsAccessed],
2011
+ violationsByType
2012
+ };
2013
+ }
2014
+ /**
2015
+ * Extract paths from a command
2016
+ */
2017
+ extractPaths(command) {
2018
+ const paths = [];
2019
+ const tokens = command.split(/\s+/);
2020
+ for (const token of tokens) {
2021
+ if (token.startsWith("-")) continue;
2022
+ if (token.startsWith("/") || token.startsWith("./") || token.startsWith("../") || token.startsWith("~/") || token.includes(".")) {
2023
+ paths.push(token);
2024
+ }
2025
+ }
2026
+ return paths;
2027
+ }
2028
+ /**
2029
+ * Check if command modifies files
2030
+ */
2031
+ isWriteCommand(command) {
2032
+ const writePatterns = [
2033
+ /^(vim|vi|nano|emacs|code)\s/,
2034
+ /^(touch|mkdir|cp|mv|rm)\s/,
2035
+ /^(echo|cat|printf).*>/,
2036
+ /^(git\s+(add|commit|checkout|reset))/,
2037
+ /^(npm|yarn|pnpm)\s+(install|uninstall)/,
2038
+ /^(pip|pip3)\s+(install|uninstall)/,
2039
+ /^chmod\s/,
2040
+ /^chown\s/
2041
+ ];
2042
+ return writePatterns.some((p) => p.test(command));
2043
+ }
2044
+ /**
2045
+ * Get recent commands
2046
+ */
2047
+ getRecentCommands(n = 10) {
2048
+ return this.commands.slice(-n);
2049
+ }
2050
+ /**
2051
+ * Get blocked commands
2052
+ */
2053
+ getBlockedCommands() {
2054
+ return this.commands.filter((c) => !c.allowed);
2055
+ }
2056
+ /**
2057
+ * Get high-risk commands
2058
+ */
2059
+ getHighRiskCommands(threshold = 6) {
2060
+ return this.commands.filter((c) => c.riskScore.score >= threshold);
2061
+ }
2062
+ /**
2063
+ * Format duration for display
2064
+ */
2065
+ static formatDuration(ms) {
2066
+ if (ms < 1e3) return `${ms}ms`;
2067
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
2068
+ if (ms < 36e5) return `${Math.floor(ms / 6e4)}m ${Math.floor(ms % 6e4 / 1e3)}s`;
2069
+ return `${Math.floor(ms / 36e5)}h ${Math.floor(ms % 36e5 / 6e4)}m`;
2070
+ }
2071
+ /**
2072
+ * Reset collector
2073
+ */
2074
+ reset() {
2075
+ this.sessionId = this.generateSessionId();
2076
+ this.startTime = /* @__PURE__ */ new Date();
2077
+ this.commands = [];
2078
+ this.filesModified.clear();
2079
+ this.pathsAccessed.clear();
2080
+ }
2081
+ };
2082
+
2083
+ // src/observability/cost.ts
2084
+ var MODEL_PRICING = {
2085
+ "claude-opus-4": { inputPer1k: 0.015, outputPer1k: 0.075 },
2086
+ "claude-sonnet-4": { inputPer1k: 3e-3, outputPer1k: 0.015 },
2087
+ "claude-haiku-4": { inputPer1k: 25e-5, outputPer1k: 125e-5 },
2088
+ "gpt-4o": { inputPer1k: 5e-3, outputPer1k: 0.015 },
2089
+ "gpt-4o-mini": { inputPer1k: 15e-5, outputPer1k: 6e-4 },
2090
+ "default": { inputPer1k: 3e-3, outputPer1k: 0.015 }
2091
+ };
2092
+ var AVG_CHARS_PER_TOKEN = 4;
2093
+ var AVG_TOOL_CALL_TOKENS = 150;
2094
+ var AVG_TOOL_RESULT_TOKENS = 500;
2095
+ var CONTEXT_OVERHEAD_RATIO = 0.2;
2096
+ var CostEstimator = class {
2097
+ model;
2098
+ pricing;
2099
+ totalInputTokens = 0;
2100
+ totalOutputTokens = 0;
2101
+ toolCallCount = 0;
2102
+ constructor(model = "claude-sonnet-4") {
2103
+ this.model = model;
2104
+ this.pricing = MODEL_PRICING[model] || MODEL_PRICING["default"];
2105
+ }
2106
+ /**
2107
+ * Estimate tokens from text
2108
+ */
2109
+ estimateTokens(text) {
2110
+ return Math.ceil(text.length / AVG_CHARS_PER_TOKEN);
2111
+ }
2112
+ /**
2113
+ * Record a tool call with input and output
2114
+ */
2115
+ recordToolCall(input, output) {
2116
+ this.toolCallCount++;
2117
+ const inputTokens = this.estimateTokens(input) + AVG_TOOL_CALL_TOKENS;
2118
+ this.totalInputTokens += inputTokens;
2119
+ if (output) {
2120
+ const outputTokens = this.estimateTokens(output) + 50;
2121
+ this.totalOutputTokens += outputTokens;
2122
+ } else {
2123
+ this.totalOutputTokens += AVG_TOOL_RESULT_TOKENS;
2124
+ }
2125
+ }
2126
+ /**
2127
+ * Get current cost estimate
2128
+ */
2129
+ getEstimate() {
2130
+ const contextTokens = Math.ceil(
2131
+ (this.totalInputTokens + this.totalOutputTokens) * CONTEXT_OVERHEAD_RATIO
2132
+ );
2133
+ const totalInput = this.totalInputTokens + contextTokens;
2134
+ const totalOutput = this.totalOutputTokens;
2135
+ const inputCost = totalInput / 1e3 * this.pricing.inputPer1k;
2136
+ const outputCost = totalOutput / 1e3 * this.pricing.outputPer1k;
2137
+ const totalCost = inputCost + outputCost;
2138
+ let confidence;
2139
+ if (this.toolCallCount < 5) confidence = "low";
2140
+ else if (this.toolCallCount < 20) confidence = "medium";
2141
+ else confidence = "high";
2142
+ return {
2143
+ estimatedTokens: totalInput + totalOutput,
2144
+ estimatedCost: Math.round(totalCost * 1e4) / 1e4,
2145
+ // 4 decimal places
2146
+ breakdown: {
2147
+ inputTokens: totalInput,
2148
+ outputTokens: totalOutput,
2149
+ toolCalls: this.toolCallCount,
2150
+ contextTokens
2151
+ },
2152
+ model: this.model,
2153
+ confidence
2154
+ };
2155
+ }
2156
+ /**
2157
+ * Format cost for display
2158
+ */
2159
+ static formatCost(cost) {
2160
+ if (cost < 0.01) return `$${(cost * 100).toFixed(2)}\xA2`;
2161
+ if (cost < 1) return `$${cost.toFixed(3)}`;
2162
+ return `$${cost.toFixed(2)}`;
2163
+ }
2164
+ /**
2165
+ * Get cost projection for N more commands
2166
+ */
2167
+ projectCost(additionalCommands) {
2168
+ if (this.toolCallCount === 0) {
2169
+ const projectedInput2 = additionalCommands * (AVG_TOOL_CALL_TOKENS + 50);
2170
+ const projectedOutput2 = additionalCommands * AVG_TOOL_RESULT_TOKENS;
2171
+ const inputCost2 = projectedInput2 / 1e3 * this.pricing.inputPer1k;
2172
+ const outputCost2 = projectedOutput2 / 1e3 * this.pricing.outputPer1k;
2173
+ return {
2174
+ estimatedTokens: projectedInput2 + projectedOutput2,
2175
+ estimatedCost: inputCost2 + outputCost2,
2176
+ breakdown: {
2177
+ inputTokens: projectedInput2,
2178
+ outputTokens: projectedOutput2,
2179
+ toolCalls: additionalCommands,
2180
+ contextTokens: 0
2181
+ },
2182
+ model: this.model,
2183
+ confidence: "low"
2184
+ };
2185
+ }
2186
+ const avgInputPerCall = this.totalInputTokens / this.toolCallCount;
2187
+ const avgOutputPerCall = this.totalOutputTokens / this.toolCallCount;
2188
+ const projectedInput = this.totalInputTokens + additionalCommands * avgInputPerCall;
2189
+ const projectedOutput = this.totalOutputTokens + additionalCommands * avgOutputPerCall;
2190
+ const contextTokens = Math.ceil((projectedInput + projectedOutput) * CONTEXT_OVERHEAD_RATIO);
2191
+ const totalInput = projectedInput + contextTokens;
2192
+ const totalOutput = projectedOutput;
2193
+ const inputCost = totalInput / 1e3 * this.pricing.inputPer1k;
2194
+ const outputCost = totalOutput / 1e3 * this.pricing.outputPer1k;
2195
+ return {
2196
+ estimatedTokens: totalInput + totalOutput,
2197
+ estimatedCost: inputCost + outputCost,
2198
+ breakdown: {
2199
+ inputTokens: Math.ceil(totalInput),
2200
+ outputTokens: Math.ceil(totalOutput),
2201
+ toolCalls: this.toolCallCount + additionalCommands,
2202
+ contextTokens
2203
+ },
2204
+ model: this.model,
2205
+ confidence: this.toolCallCount >= 10 ? "high" : "medium"
2206
+ };
2207
+ }
2208
+ /**
2209
+ * Set model for pricing
2210
+ */
2211
+ setModel(model) {
2212
+ this.model = model;
2213
+ this.pricing = MODEL_PRICING[model] || MODEL_PRICING["default"];
2214
+ }
2215
+ /**
2216
+ * Reset counters
2217
+ */
2218
+ reset() {
2219
+ this.totalInputTokens = 0;
2220
+ this.totalOutputTokens = 0;
2221
+ this.toolCallCount = 0;
2222
+ }
2223
+ /**
2224
+ * Add custom model pricing
2225
+ */
2226
+ static addModelPricing(model, pricing) {
2227
+ MODEL_PRICING[model] = pricing;
2228
+ }
2229
+ };
2230
+
2231
+ // src/observability/report.ts
2232
+ var DEFAULT_OPTIONS = {
2233
+ showCommands: true,
2234
+ showBlocked: true,
2235
+ showRisk: true,
2236
+ showPaths: true,
2237
+ showCost: true,
2238
+ format: "text"
2239
+ };
2240
+ var ReportGenerator = class {
2241
+ /**
2242
+ * Generate a session report
2243
+ */
2244
+ static generate(metrics, cost, options = {}) {
2245
+ const opts = { ...DEFAULT_OPTIONS, ...options };
2246
+ switch (opts.format) {
2247
+ case "json":
2248
+ return this.generateJSON(metrics, cost);
2249
+ case "markdown":
2250
+ return this.generateMarkdown(metrics, cost, opts);
2251
+ default:
2252
+ return this.generateText(metrics, cost, opts);
2253
+ }
2254
+ }
2255
+ /**
2256
+ * Generate text report
2257
+ */
2258
+ static generateText(metrics, cost, opts = {}) {
2259
+ const lines = [];
2260
+ const duration = this.formatDuration(metrics.duration);
2261
+ lines.push(`Session Report (${duration})`);
2262
+ lines.push("\u2500".repeat(45));
2263
+ lines.push("");
2264
+ const blockedPct = metrics.commandCount > 0 ? Math.round(metrics.blockedCount / metrics.commandCount * 100) : 0;
2265
+ lines.push(`Commands: ${metrics.commandCount} total, ${metrics.blockedCount} blocked (${blockedPct}%)`);
2266
+ lines.push("");
2267
+ if (opts.showRisk) {
2268
+ const total = metrics.commandCount || 1;
2269
+ const { safe, caution, dangerous, critical } = metrics.riskDistribution;
2270
+ const safePct = Math.round(safe / total * 100);
2271
+ const cautionPct = Math.round(caution / total * 100);
2272
+ const dangerousPct = Math.round(dangerous / total * 100);
2273
+ const criticalPct = Math.round(critical / total * 100);
2274
+ lines.push("Risk Distribution:");
2275
+ lines.push(` ${this.progressBar(safePct, 20)} ${safePct}% safe`);
2276
+ lines.push(` ${this.progressBar(cautionPct, 20)} ${cautionPct}% caution`);
2277
+ lines.push(` ${this.progressBar(dangerousPct, 20)} ${dangerousPct}% dangerous`);
2278
+ if (critical > 0) {
2279
+ lines.push(` ${this.progressBar(criticalPct, 20)} ${criticalPct}% CRITICAL`);
2280
+ }
2281
+ lines.push(` Average risk score: ${metrics.avgRiskScore.toFixed(1)}/10`);
2282
+ lines.push("");
2283
+ }
2284
+ if (opts.showCommands && metrics.topCommands.length > 0) {
2285
+ lines.push("Top Commands:");
2286
+ for (const [cmd, count] of metrics.topCommands.slice(0, 5)) {
2287
+ const pct = Math.round(count / metrics.commandCount * 100);
2288
+ lines.push(` ${cmd.padEnd(15)} ${count.toString().padStart(3)} (${pct}%)`);
2289
+ }
2290
+ lines.push("");
2291
+ }
2292
+ if (opts.showBlocked && metrics.blockedCount > 0) {
2293
+ lines.push("Violations by Type:");
2294
+ for (const [type, count] of Object.entries(metrics.violationsByType)) {
2295
+ lines.push(` ${type}: ${count}`);
2296
+ }
2297
+ lines.push("");
2298
+ }
2299
+ if (opts.showPaths) {
2300
+ if (metrics.filesModified.length > 0) {
2301
+ lines.push(`Files Modified: ${metrics.filesModified.length}`);
2302
+ for (const file of metrics.filesModified.slice(0, 5)) {
2303
+ lines.push(` \u2022 ${file}`);
2304
+ }
2305
+ if (metrics.filesModified.length > 5) {
2306
+ lines.push(` ... and ${metrics.filesModified.length - 5} more`);
2307
+ }
2308
+ lines.push("");
2309
+ }
2310
+ lines.push(`Paths Accessed: ${metrics.pathsAccessed.length} unique`);
2311
+ lines.push("");
2312
+ }
2313
+ if (opts.showCost && cost) {
2314
+ lines.push("Cost Estimate:");
2315
+ lines.push(` Tokens: ~${cost.estimatedTokens.toLocaleString()} (${cost.confidence} confidence)`);
2316
+ lines.push(` Cost: ~${this.formatCost(cost.estimatedCost)} (${cost.model})`);
2317
+ lines.push("");
2318
+ }
2319
+ lines.push("Performance:");
2320
+ lines.push(` Avg execution time: ${metrics.avgExecutionTime.toFixed(0)}ms`);
2321
+ lines.push(` Total execution time: ${this.formatDuration(metrics.totalExecutionTime)}`);
2322
+ lines.push("");
2323
+ lines.push(`Session ID: ${metrics.sessionId}`);
2324
+ return lines.join("\n");
2325
+ }
2326
+ /**
2327
+ * Generate markdown report
2328
+ */
2329
+ static generateMarkdown(metrics, cost, opts = {}) {
2330
+ const lines = [];
2331
+ const duration = this.formatDuration(metrics.duration);
2332
+ lines.push(`# Session Report`);
2333
+ lines.push("");
2334
+ lines.push(`**Duration:** ${duration}`);
2335
+ lines.push(`**Session ID:** \`${metrics.sessionId}\``);
2336
+ lines.push("");
2337
+ lines.push("## Summary");
2338
+ lines.push("");
2339
+ lines.push("| Metric | Value |");
2340
+ lines.push("|--------|-------|");
2341
+ lines.push(`| Commands | ${metrics.commandCount} |`);
2342
+ lines.push(`| Blocked | ${metrics.blockedCount} |`);
2343
+ lines.push(`| Unique Commands | ${metrics.uniqueCommands} |`);
2344
+ lines.push(`| Avg Risk Score | ${metrics.avgRiskScore.toFixed(1)}/10 |`);
2345
+ lines.push("");
2346
+ if (opts.showRisk) {
2347
+ lines.push("## Risk Distribution");
2348
+ lines.push("");
2349
+ lines.push("| Level | Count | Percentage |");
2350
+ lines.push("|-------|-------|------------|");
2351
+ const total = metrics.commandCount || 1;
2352
+ for (const [level, count] of Object.entries(metrics.riskDistribution)) {
2353
+ const pct = Math.round(count / total * 100);
2354
+ lines.push(`| ${level} | ${count} | ${pct}% |`);
2355
+ }
2356
+ lines.push("");
2357
+ }
2358
+ if (opts.showCommands && metrics.topCommands.length > 0) {
2359
+ lines.push("## Top Commands");
2360
+ lines.push("");
2361
+ lines.push("| Command | Count |");
2362
+ lines.push("|---------|-------|");
2363
+ for (const [cmd, count] of metrics.topCommands.slice(0, 10)) {
2364
+ lines.push(`| \`${cmd}\` | ${count} |`);
2365
+ }
2366
+ lines.push("");
2367
+ }
2368
+ if (opts.showCost && cost) {
2369
+ lines.push("## Cost Estimate");
2370
+ lines.push("");
2371
+ lines.push(`- **Tokens:** ~${cost.estimatedTokens.toLocaleString()}`);
2372
+ lines.push(`- **Cost:** ~${this.formatCost(cost.estimatedCost)}`);
2373
+ lines.push(`- **Model:** ${cost.model}`);
2374
+ lines.push(`- **Confidence:** ${cost.confidence}`);
2375
+ lines.push("");
2376
+ }
2377
+ return lines.join("\n");
2378
+ }
2379
+ /**
2380
+ * Generate JSON report
2381
+ */
2382
+ static generateJSON(metrics, cost) {
2383
+ return JSON.stringify({ metrics, cost }, null, 2);
2384
+ }
2385
+ /**
2386
+ * Generate a simple progress bar
2387
+ */
2388
+ static progressBar(percent, width) {
2389
+ const filled = Math.round(percent / 100 * width);
2390
+ const empty = width - filled;
2391
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty);
2392
+ }
2393
+ /**
2394
+ * Format duration
2395
+ */
2396
+ static formatDuration(ms) {
2397
+ if (ms < 1e3) return `${ms}ms`;
2398
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
2399
+ if (ms < 36e5) {
2400
+ const mins2 = Math.floor(ms / 6e4);
2401
+ const secs = Math.floor(ms % 6e4 / 1e3);
2402
+ return `${mins2}m ${secs}s`;
2403
+ }
2404
+ const hours = Math.floor(ms / 36e5);
2405
+ const mins = Math.floor(ms % 36e5 / 6e4);
2406
+ return `${hours}h ${mins}m`;
2407
+ }
2408
+ /**
2409
+ * Format cost
2410
+ */
2411
+ static formatCost(cost) {
2412
+ if (cost < 0.01) return `$${(cost * 100).toFixed(2)}\xA2`;
2413
+ if (cost < 1) return `$${cost.toFixed(3)}`;
2414
+ return `$${cost.toFixed(2)}`;
2415
+ }
2416
+ /**
2417
+ * Generate a one-line summary
2418
+ */
2419
+ static oneLine(metrics) {
2420
+ const duration = this.formatDuration(metrics.duration);
2421
+ const blockedPct = metrics.commandCount > 0 ? Math.round(metrics.blockedCount / metrics.commandCount * 100) : 0;
2422
+ return `${metrics.commandCount} cmds (${blockedPct}% blocked) | risk: ${metrics.avgRiskScore.toFixed(1)}/10 | ${duration}`;
2423
+ }
2424
+ };
2425
+
2426
+ // src/safety/undo-stack.ts
2427
+ import { existsSync as existsSync4, unlinkSync, mkdirSync as mkdirSync3, copyFileSync, readdirSync, statSync } from "fs";
2428
+ import { join as join4, dirname } from "path";
2429
+ import { homedir as homedir4 } from "os";
2430
+ var DEFAULT_CONFIG = {
2431
+ maxStackSize: 100,
2432
+ maxFileSize: 10 * 1024 * 1024,
2433
+ // 10MB
2434
+ ttlMinutes: 60,
2435
+ backupPath: join4(homedir4(), ".bashbros", "undo"),
2436
+ enabled: true
2437
+ };
2438
+ var UndoStack = class {
2439
+ stack = [];
2440
+ sessionId;
2441
+ config;
2442
+ undoDir;
2443
+ constructor(policy) {
2444
+ this.config = { ...DEFAULT_CONFIG };
2445
+ if (policy) {
2446
+ if (typeof policy.maxStackSize === "number") this.config.maxStackSize = policy.maxStackSize;
2447
+ if (typeof policy.maxFileSize === "number") this.config.maxFileSize = policy.maxFileSize;
2448
+ if (typeof policy.ttlMinutes === "number") this.config.ttlMinutes = policy.ttlMinutes;
2449
+ if (typeof policy.backupPath === "string") {
2450
+ this.config.backupPath = policy.backupPath.replace("~", homedir4());
2451
+ }
2452
+ if (typeof policy.enabled === "boolean") this.config.enabled = policy.enabled;
2453
+ }
2454
+ this.undoDir = this.config.backupPath;
2455
+ this.sessionId = Date.now().toString(36);
2456
+ this.ensureUndoDir();
2457
+ this.cleanupOldBackups();
2458
+ }
2459
+ ensureUndoDir() {
2460
+ if (!existsSync4(this.undoDir)) {
2461
+ mkdirSync3(this.undoDir, { recursive: true, mode: 448 });
2462
+ }
2463
+ }
2464
+ /**
2465
+ * Clean up backups older than TTL
2466
+ */
2467
+ cleanupOldBackups() {
2468
+ if (!this.config.enabled || this.config.ttlMinutes <= 0) return 0;
2469
+ const cutoff = Date.now() - this.config.ttlMinutes * 60 * 1e3;
2470
+ let cleaned = 0;
2471
+ try {
2472
+ const files = readdirSync(this.undoDir);
2473
+ for (const file of files) {
2474
+ if (!file.endsWith(".backup")) continue;
2475
+ const filePath = join4(this.undoDir, file);
2476
+ try {
2477
+ const stats = statSync(filePath);
2478
+ if (stats.mtimeMs < cutoff) {
2479
+ unlinkSync(filePath);
2480
+ cleaned++;
2481
+ }
2482
+ } catch {
2483
+ }
2484
+ }
2485
+ } catch {
2486
+ }
2487
+ return cleaned;
2488
+ }
2489
+ generateId() {
2490
+ return `${this.sessionId}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
2491
+ }
2492
+ /**
2493
+ * Check if undo is enabled
2494
+ */
2495
+ isEnabled() {
2496
+ return this.config.enabled;
2497
+ }
2498
+ /**
2499
+ * Record a file creation
2500
+ */
2501
+ recordCreate(path, command) {
2502
+ const entry = {
2503
+ id: this.generateId(),
2504
+ timestamp: /* @__PURE__ */ new Date(),
2505
+ path,
2506
+ operation: "create",
2507
+ command
2508
+ };
2509
+ this.push(entry);
2510
+ return entry;
2511
+ }
2512
+ /**
2513
+ * Record a file modification (backs up original)
2514
+ */
2515
+ recordModify(path, command) {
2516
+ if (!this.config.enabled || !existsSync4(path)) {
2517
+ return null;
2518
+ }
2519
+ const stats = statSync(path);
2520
+ if (stats.size > this.config.maxFileSize) {
2521
+ const entry = {
2522
+ id: this.generateId(),
2523
+ timestamp: /* @__PURE__ */ new Date(),
2524
+ path,
2525
+ operation: "modify",
2526
+ command
2527
+ };
2528
+ this.push(entry);
2529
+ return entry;
2530
+ }
2531
+ const id = this.generateId();
2532
+ const backupPath = join4(this.undoDir, `${id}.backup`);
2533
+ try {
2534
+ copyFileSync(path, backupPath);
2535
+ const entry = {
2536
+ id,
2537
+ timestamp: /* @__PURE__ */ new Date(),
2538
+ path,
2539
+ operation: "modify",
2540
+ backupPath,
2541
+ command
2542
+ };
2543
+ this.push(entry);
2544
+ return entry;
2545
+ } catch {
2546
+ return null;
2547
+ }
2548
+ }
2549
+ /**
2550
+ * Record a file deletion (backs up content)
2551
+ */
2552
+ recordDelete(path, command) {
2553
+ if (!this.config.enabled || !existsSync4(path)) {
2554
+ return null;
2555
+ }
2556
+ const stats = statSync(path);
2557
+ if (stats.size > this.config.maxFileSize) {
2558
+ const entry = {
2559
+ id: this.generateId(),
2560
+ timestamp: /* @__PURE__ */ new Date(),
2561
+ path,
2562
+ operation: "delete",
2563
+ command
2564
+ };
2565
+ this.push(entry);
2566
+ return entry;
2567
+ }
2568
+ const id = this.generateId();
2569
+ const backupPath = join4(this.undoDir, `${id}.backup`);
2570
+ try {
2571
+ copyFileSync(path, backupPath);
2572
+ const entry = {
2573
+ id,
2574
+ timestamp: /* @__PURE__ */ new Date(),
2575
+ path,
2576
+ operation: "delete",
2577
+ backupPath,
2578
+ command
2579
+ };
2580
+ this.push(entry);
2581
+ return entry;
2582
+ } catch {
2583
+ return null;
2584
+ }
2585
+ }
2586
+ /**
2587
+ * Auto-detect operation from command
2588
+ */
2589
+ recordFromCommand(command, paths) {
2590
+ const entries = [];
2591
+ const isCreate = /^(touch|mkdir|cp|mv|>|>>)/.test(command) || /^(echo|cat|printf).*>/.test(command);
2592
+ const isDelete = /^rm\s/.test(command);
2593
+ const isModify = /^(sed|awk|vim|vi|nano|code)\s/.test(command) || /^(echo|cat).*>>/.test(command);
2594
+ for (const path of paths) {
2595
+ let entry = null;
2596
+ if (isDelete && existsSync4(path)) {
2597
+ entry = this.recordDelete(path, command);
2598
+ } else if (isModify && existsSync4(path)) {
2599
+ entry = this.recordModify(path, command);
2600
+ } else if (isCreate && !existsSync4(path)) {
2601
+ entry = this.recordCreate(path, command);
2602
+ }
2603
+ if (entry) {
2604
+ entries.push(entry);
2605
+ }
2606
+ }
2607
+ return entries;
2608
+ }
2609
+ /**
2610
+ * Undo the last operation
2611
+ */
2612
+ undo() {
2613
+ const entry = this.stack.pop();
2614
+ if (!entry) {
2615
+ return { success: false, message: "Nothing to undo" };
2616
+ }
2617
+ return this.undoEntry(entry);
2618
+ }
2619
+ /**
2620
+ * Undo a specific entry
2621
+ */
2622
+ undoEntry(entry) {
2623
+ try {
2624
+ switch (entry.operation) {
2625
+ case "create":
2626
+ if (existsSync4(entry.path)) {
2627
+ unlinkSync(entry.path);
2628
+ return {
2629
+ success: true,
2630
+ message: `Deleted created file: ${entry.path}`,
2631
+ entry
2632
+ };
2633
+ }
2634
+ return {
2635
+ success: false,
2636
+ message: `File already deleted: ${entry.path}`,
2637
+ entry
2638
+ };
2639
+ case "modify":
2640
+ if (entry.backupPath && existsSync4(entry.backupPath)) {
2641
+ copyFileSync(entry.backupPath, entry.path);
2642
+ return {
2643
+ success: true,
2644
+ message: `Restored: ${entry.path}`,
2645
+ entry
2646
+ };
2647
+ }
2648
+ return {
2649
+ success: false,
2650
+ message: `No backup available for: ${entry.path}`,
2651
+ entry
2652
+ };
2653
+ case "delete":
2654
+ if (entry.backupPath && existsSync4(entry.backupPath)) {
2655
+ const dir = dirname(entry.path);
2656
+ if (!existsSync4(dir)) {
2657
+ mkdirSync3(dir, { recursive: true });
2658
+ }
2659
+ copyFileSync(entry.backupPath, entry.path);
2660
+ return {
2661
+ success: true,
2662
+ message: `Restored deleted file: ${entry.path}`,
2663
+ entry
2664
+ };
2665
+ }
2666
+ return {
2667
+ success: false,
2668
+ message: `No backup available for: ${entry.path}`,
2669
+ entry
2670
+ };
2671
+ default:
2672
+ return {
2673
+ success: false,
2674
+ message: `Unknown operation: ${entry.operation}`,
2675
+ entry
2676
+ };
2677
+ }
2678
+ } catch (error) {
2679
+ return {
2680
+ success: false,
2681
+ message: `Undo failed: ${error.message}`,
2682
+ entry
2683
+ };
2684
+ }
2685
+ }
2686
+ /**
2687
+ * Undo all operations in the session
2688
+ */
2689
+ undoAll() {
2690
+ const results = [];
2691
+ while (this.stack.length > 0) {
2692
+ results.push(this.undo());
2693
+ }
2694
+ return results;
2695
+ }
2696
+ /**
2697
+ * Get the undo stack
2698
+ */
2699
+ getStack() {
2700
+ return [...this.stack];
2701
+ }
2702
+ /**
2703
+ * Get stack size
2704
+ */
2705
+ size() {
2706
+ return this.stack.length;
2707
+ }
2708
+ /**
2709
+ * Clear the stack (and backups)
2710
+ */
2711
+ clear() {
2712
+ for (const entry of this.stack) {
2713
+ if (entry.backupPath && existsSync4(entry.backupPath)) {
2714
+ try {
2715
+ unlinkSync(entry.backupPath);
2716
+ } catch {
2717
+ }
2718
+ }
2719
+ }
2720
+ this.stack = [];
2721
+ }
2722
+ /**
2723
+ * Push entry to stack
2724
+ */
2725
+ push(entry) {
2726
+ this.stack.push(entry);
2727
+ if (this.stack.length > this.config.maxStackSize) {
2728
+ const removed = this.stack.shift();
2729
+ if (removed?.backupPath && existsSync4(removed.backupPath)) {
2730
+ try {
2731
+ unlinkSync(removed.backupPath);
2732
+ } catch {
2733
+ }
2734
+ }
2735
+ }
2736
+ }
2737
+ /**
2738
+ * Format stack for display
2739
+ */
2740
+ formatStack() {
2741
+ if (this.stack.length === 0) {
2742
+ return "Undo stack is empty";
2743
+ }
2744
+ const lines = ["Undo Stack:", ""];
2745
+ for (let i = this.stack.length - 1; i >= 0; i--) {
2746
+ const entry = this.stack[i];
2747
+ const hasBackup = entry.backupPath ? "\u2713" : "\u2717";
2748
+ const time = entry.timestamp.toLocaleTimeString();
2749
+ const op = entry.operation.padEnd(6);
2750
+ lines.push(`${i + 1}. [${time}] ${op} ${entry.path} (backup: ${hasBackup})`);
2751
+ if (entry.command) {
2752
+ lines.push(` \u2514\u2500 ${entry.command.slice(0, 60)}...`);
2753
+ }
2754
+ }
2755
+ return lines.join("\n");
2756
+ }
2757
+ };
2758
+
2759
+ // src/policy/loop-detector.ts
2760
+ var DEFAULT_CONFIG2 = {
2761
+ maxRepeats: 3,
2762
+ maxTurns: 100,
2763
+ similarityThreshold: 0.85,
2764
+ cooldownMs: 1e3,
2765
+ windowSize: 20
2766
+ };
2767
+ var LoopDetector = class {
2768
+ config;
2769
+ history = [];
2770
+ turnCount = 0;
2771
+ constructor(config = {}) {
2772
+ this.config = { ...DEFAULT_CONFIG2, ...config };
2773
+ }
2774
+ /**
2775
+ * Record a command and check for loops
2776
+ */
2777
+ check(command) {
2778
+ const now = Date.now();
2779
+ const normalized = this.normalize(command);
2780
+ this.turnCount++;
2781
+ if (this.turnCount >= this.config.maxTurns) {
2782
+ return {
2783
+ type: "max_turns",
2784
+ command,
2785
+ count: this.turnCount,
2786
+ message: `Maximum turns reached (${this.config.maxTurns}). Session may be stuck.`
2787
+ };
2788
+ }
2789
+ const exactMatches = this.history.filter((h) => h.command === command);
2790
+ if (exactMatches.length >= this.config.maxRepeats) {
2791
+ return {
2792
+ type: "exact_repeat",
2793
+ command,
2794
+ count: exactMatches.length + 1,
2795
+ message: `Command repeated ${exactMatches.length + 1} times: "${command.slice(0, 50)}..."`
2796
+ };
2797
+ }
2798
+ const lastSame = exactMatches[exactMatches.length - 1];
2799
+ if (lastSame && now - lastSame.timestamp < this.config.cooldownMs) {
2800
+ return {
2801
+ type: "exact_repeat",
2802
+ command,
2803
+ count: 2,
2804
+ message: `Rapid repeat detected (${now - lastSame.timestamp}ms apart)`
2805
+ };
2806
+ }
2807
+ const recentWindow = this.history.slice(-this.config.windowSize);
2808
+ const similarCount = recentWindow.filter(
2809
+ (h) => this.similarity(h.normalized, normalized) >= this.config.similarityThreshold
2810
+ ).length;
2811
+ if (similarCount >= this.config.maxRepeats) {
2812
+ return {
2813
+ type: "semantic_repeat",
2814
+ command,
2815
+ count: similarCount + 1,
2816
+ message: `Similar commands repeated ${similarCount + 1} times`
2817
+ };
2818
+ }
2819
+ const baseCommand = command.split(/\s+/)[0];
2820
+ const toolCount = recentWindow.filter(
2821
+ (h) => h.command.split(/\s+/)[0] === baseCommand
2822
+ ).length;
2823
+ if (toolCount >= this.config.maxRepeats * 2) {
2824
+ return {
2825
+ type: "tool_hammering",
2826
+ command,
2827
+ count: toolCount + 1,
2828
+ message: `Tool "${baseCommand}" called ${toolCount + 1} times in last ${this.config.windowSize} commands`
2829
+ };
2830
+ }
2831
+ this.history.push({ command, timestamp: now, normalized });
2832
+ if (this.history.length > this.config.windowSize * 2) {
2833
+ this.history = this.history.slice(-this.config.windowSize);
2834
+ }
2835
+ return null;
2836
+ }
2837
+ /**
2838
+ * Normalize command for comparison
2839
+ */
2840
+ normalize(command) {
2841
+ return command.toLowerCase().replace(/["']/g, "").replace(/\s+/g, " ").replace(/\d+/g, "N").replace(/[a-f0-9]{8,}/gi, "H").trim();
2842
+ }
2843
+ /**
2844
+ * Calculate similarity between two strings (Jaccard index on words)
2845
+ */
2846
+ similarity(a, b) {
2847
+ const wordsA = new Set(a.split(/\s+/));
2848
+ const wordsB = new Set(b.split(/\s+/));
2849
+ const intersection = new Set([...wordsA].filter((x) => wordsB.has(x)));
2850
+ const union = /* @__PURE__ */ new Set([...wordsA, ...wordsB]);
2851
+ if (union.size === 0) return 1;
2852
+ return intersection.size / union.size;
2853
+ }
2854
+ /**
2855
+ * Get current turn count
2856
+ */
2857
+ getTurnCount() {
2858
+ return this.turnCount;
2859
+ }
2860
+ /**
2861
+ * Get command frequency map
2862
+ */
2863
+ getFrequencyMap() {
2864
+ const freq = /* @__PURE__ */ new Map();
2865
+ for (const entry of this.history) {
2866
+ const base = entry.command.split(/\s+/)[0];
2867
+ freq.set(base, (freq.get(base) || 0) + 1);
2868
+ }
2869
+ return freq;
2870
+ }
2871
+ /**
2872
+ * Reset detector state
2873
+ */
2874
+ reset() {
2875
+ this.history = [];
2876
+ this.turnCount = 0;
2877
+ }
2878
+ /**
2879
+ * Get stats for reporting
2880
+ */
2881
+ getStats() {
2882
+ const freq = this.getFrequencyMap();
2883
+ const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
2884
+ return {
2885
+ turnCount: this.turnCount,
2886
+ uniqueCommands: freq.size,
2887
+ topCommands: sorted.slice(0, 5)
2888
+ };
2889
+ }
2890
+ };
2891
+
2892
+ export {
2893
+ BashgymIntegration,
2894
+ getBashgymIntegration,
2895
+ resetBashgymIntegration,
2896
+ ClaudeCodeHooks,
2897
+ gateCommand,
2898
+ BashBros,
2899
+ SystemProfiler,
2900
+ TaskRouter,
2901
+ CommandSuggester,
2902
+ BackgroundWorker,
2903
+ BashBro,
2904
+ MetricsCollector,
2905
+ CostEstimator,
2906
+ ReportGenerator,
2907
+ UndoStack,
2908
+ LoopDetector
2909
+ };
2910
+ //# sourceMappingURL=chunk-43W3RVEL.js.map