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.
- package/LICENSE +21 -0
- package/README.md +453 -0
- package/dist/audit-MCFNGOIM.js +11 -0
- package/dist/audit-MCFNGOIM.js.map +1 -0
- package/dist/chunk-43W3RVEL.js +2910 -0
- package/dist/chunk-43W3RVEL.js.map +1 -0
- package/dist/chunk-4R4GV5V2.js +213 -0
- package/dist/chunk-4R4GV5V2.js.map +1 -0
- package/dist/chunk-7OCVIDC7.js +12 -0
- package/dist/chunk-7OCVIDC7.js.map +1 -0
- package/dist/chunk-CSRPOGHY.js +354 -0
- package/dist/chunk-CSRPOGHY.js.map +1 -0
- package/dist/chunk-DEAF6PYM.js +212 -0
- package/dist/chunk-DEAF6PYM.js.map +1 -0
- package/dist/chunk-DLP2O6PN.js +273 -0
- package/dist/chunk-DLP2O6PN.js.map +1 -0
- package/dist/chunk-GD5VNHIN.js +519 -0
- package/dist/chunk-GD5VNHIN.js.map +1 -0
- package/dist/chunk-ID2O2QTI.js +269 -0
- package/dist/chunk-ID2O2QTI.js.map +1 -0
- package/dist/chunk-J37RHCFJ.js +357 -0
- package/dist/chunk-J37RHCFJ.js.map +1 -0
- package/dist/chunk-SB4JS3GU.js +456 -0
- package/dist/chunk-SB4JS3GU.js.map +1 -0
- package/dist/chunk-SG752FZC.js +200 -0
- package/dist/chunk-SG752FZC.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2448 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-CZMIGNPF.js +13 -0
- package/dist/config-CZMIGNPF.js.map +1 -0
- package/dist/config-parser-XHE7BC7H.js +13 -0
- package/dist/config-parser-XHE7BC7H.js.map +1 -0
- package/dist/db-EHQDB5OL.js +11 -0
- package/dist/db-EHQDB5OL.js.map +1 -0
- package/dist/display-IN4NRJJS.js +18 -0
- package/dist/display-IN4NRJJS.js.map +1 -0
- package/dist/engine-PKLXW6OF.js +9 -0
- package/dist/engine-PKLXW6OF.js.map +1 -0
- package/dist/index.d.ts +1498 -0
- package/dist/index.js +552 -0
- package/dist/index.js.map +1 -0
- package/dist/moltbot-DXZFVK3X.js +11 -0
- package/dist/moltbot-DXZFVK3X.js.map +1 -0
- package/dist/ollama-HY35OHW4.js +9 -0
- package/dist/ollama-HY35OHW4.js.map +1 -0
- package/dist/risk-scorer-Y6KF2XCZ.js +9 -0
- package/dist/risk-scorer-Y6KF2XCZ.js.map +1 -0
- package/dist/static/index.html +410 -0
- 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
|