@phuetz/code-buddy 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/agent/execution/agent-executor.js +2 -2
  2. package/dist/agent/execution/agent-executor.js.map +1 -1
  3. package/dist/agent/lessons-tracker.d.ts +10 -0
  4. package/dist/agent/lessons-tracker.js +53 -0
  5. package/dist/agent/lessons-tracker.js.map +1 -1
  6. package/dist/commands/client-dispatcher.js +22 -1
  7. package/dist/commands/client-dispatcher.js.map +1 -1
  8. package/dist/commands/enhanced-command-handler.js +3 -1
  9. package/dist/commands/enhanced-command-handler.js.map +1 -1
  10. package/dist/commands/handlers/index.d.ts +1 -0
  11. package/dist/commands/handlers/index.js +2 -0
  12. package/dist/commands/handlers/index.js.map +1 -1
  13. package/dist/commands/handlers/lessons-handler.d.ts +2 -0
  14. package/dist/commands/handlers/lessons-handler.js +63 -0
  15. package/dist/commands/handlers/lessons-handler.js.map +1 -0
  16. package/dist/commands/handlers/persona-handler.d.ts +11 -0
  17. package/dist/commands/handlers/persona-handler.js +117 -0
  18. package/dist/commands/handlers/persona-handler.js.map +1 -0
  19. package/dist/commands/lessons.js +58 -0
  20. package/dist/commands/lessons.js.map +1 -1
  21. package/dist/commands/slash/builtin-commands.js +27 -0
  22. package/dist/commands/slash/builtin-commands.js.map +1 -1
  23. package/dist/i18n/index.d.ts +5 -1
  24. package/dist/i18n/index.js +377 -5
  25. package/dist/i18n/index.js.map +1 -1
  26. package/dist/index.js +0 -0
  27. package/dist/interpreter/computer/skills.d.ts +4 -0
  28. package/dist/interpreter/computer/skills.js +56 -4
  29. package/dist/interpreter/computer/skills.js.map +1 -1
  30. package/dist/memory/semantic-memory-search.js +6 -4
  31. package/dist/memory/semantic-memory-search.js.map +1 -1
  32. package/dist/observability/run-store.d.ts +10 -1
  33. package/dist/observability/run-store.js +21 -0
  34. package/dist/observability/run-store.js.map +1 -1
  35. package/dist/optimization/cache-breakpoints.d.ts +2 -2
  36. package/dist/personas/persona-manager.d.ts +6 -1
  37. package/dist/personas/persona-manager.js +49 -1
  38. package/dist/personas/persona-manager.js.map +1 -1
  39. package/dist/prompts/system-base.js +8 -18
  40. package/dist/prompts/system-base.js.map +1 -1
  41. package/dist/tools/registry/attention-tools.d.ts +2 -2
  42. package/dist/tools/registry/attention-tools.js +14 -10
  43. package/dist/tools/registry/attention-tools.js.map +1 -1
  44. package/dist/tools/registry/lessons-tools.d.ts +4 -4
  45. package/dist/tools/registry/lessons-tools.js +36 -19
  46. package/dist/tools/registry/lessons-tools.js.map +1 -1
  47. package/dist/tools/registry/tool-aliases.js +16 -10
  48. package/dist/tools/registry/tool-aliases.js.map +1 -1
  49. package/dist/tools/web-search.js +34 -5
  50. package/dist/tools/web-search.js.map +1 -1
  51. package/package.json +1 -1
  52. package/dist/tools/bash.d.ts +0 -128
  53. package/dist/tools/bash.js +0 -973
  54. package/dist/tools/bash.js.map +0 -1
@@ -1,973 +0,0 @@
1
- import { spawn } from 'child_process';
2
- import { ConfirmationService } from '../utils/confirmation-service.js';
3
- import { getSandboxManager } from '../security/sandbox.js';
4
- import { getSelfHealingEngine } from '../utils/self-healing.js';
5
- import { parseTestOutput, isLikelyTestOutput } from '../utils/test-output-parser.js';
6
- import { registerDisposable } from '../utils/disposable.js';
7
- import { bashToolSchemas, validateWithSchema, validateCommand as validateCommandSafety, sanitizeForShell } from '../utils/input-validator.js';
8
- import { rgPath } from '@vscode/ripgrep';
9
- import path from 'path';
10
- import os from 'os';
11
- /**
12
- * Dangerous command patterns that are always blocked
13
- */
14
- const BLOCKED_PATTERNS = [
15
- // Filesystem destruction
16
- /rm\s+(-rf?|--recursive)\s+[/~]/i, // rm -rf / or ~
17
- /rm\s+.*\/\s*$/i, // rm something/
18
- />\s*\/dev\/sd[a-z]/i, // Write to disk device
19
- /dd\s+.*if=.*of=\/dev/i, // dd to device
20
- /mkfs/i, // Format filesystem
21
- /:\(\)\s*\{\s*:\|:&\s*\};:/, // Fork bomb :(){ :|:& };:
22
- /chmod\s+-R\s+777\s+\//i, // chmod 777 /
23
- // Remote code execution via pipe to shell
24
- /wget.*\|\s*(ba)?sh/i, // wget | sh
25
- /curl.*\|\s*(ba)?sh/i, // curl | sh
26
- /sudo\s+(rm|dd|mkfs)/i, // sudo dangerous commands
27
- // Command injection via command substitution
28
- /\$\([^)]*(?:rm|dd|mkfs|chmod|chown|curl|wget|nc|netcat|bash|sh|eval|exec)/i, // $(dangerous_cmd)
29
- /`[^`]*(?:rm|dd|mkfs|chmod|chown|curl|wget|nc|netcat|bash|sh|eval|exec)/i, // `dangerous_cmd`
30
- // Dangerous variable expansion that could leak secrets
31
- /\$\{?(?:GROK_API_KEY|AWS_SECRET|AWS_ACCESS_KEY|AWS_SESSION_TOKEN|GITHUB_TOKEN|NPM_TOKEN|MORPH_API_KEY|DATABASE_URL|DB_PASSWORD|SECRET_KEY|PRIVATE_KEY|API_KEY|API_SECRET|AUTH_TOKEN|ACCESS_TOKEN|OPENAI_API_KEY|ANTHROPIC_API_KEY|SLACK_TOKEN|DISCORD_TOKEN)\}?/i,
32
- // Eval and exec injection
33
- /\beval\s+.*\$/i, // eval with variable expansion
34
- /\bexec\s+\d*[<>]/i, // exec with redirections
35
- // Hex/octal encoded dangerous commands (bypass attempts)
36
- /\\x[0-9a-f]{2}/i, // Hex escape sequences
37
- /\\[0-7]{3}/, // Octal escape sequences
38
- /\$'\\x/i, // ANSI-C quoting with hex
39
- /\$'\\[0-7]/, // ANSI-C quoting with octal
40
- /\$'[^']*\\[nrtbfv]/i, // ANSI-C with escape sequences
41
- // Base64 decode piped to shell
42
- /base64\s+(-d|--decode).*\|\s*(ba)?sh/i,
43
- // Network exfiltration patterns
44
- /\|\s*(nc|netcat|curl|wget)\s+[^|]*(>|>>)/i, // pipe to network tool with redirect
45
- />\s*\/dev\/(tcp|udp)\//i, // bash network redirection
46
- /\bnc\s+-[elp]/i, // netcat listen/exec modes
47
- /\bbash\s+-i\s+>&?\s*\/dev\/(tcp|udp)/i, // bash reverse shell
48
- // Additional bypass patterns
49
- /\bprintf\s+['"]%b['"].*\\x/i, // printf %b with hex (bypass)
50
- /\becho\s+-e\s+.*\\x/i, // echo -e with hex
51
- /\becho\s+\$'\\x/i, // echo with ANSI-C quoting
52
- /\bxxd\s+-r.*\|\s*(ba)?sh/i, // xxd decode to shell
53
- /\bpython[23]?\s+-c\s+['"].*(?:exec|eval|os\.system|subprocess|__import__)/i, // Python code exec
54
- /\bperl\s+-e\s+['"].*(?:system|exec|`)/i, // Perl code exec
55
- /\bruby\s+-e\s+['"].*(?:system|exec|`)/i, // Ruby code exec
56
- /\bnode\s+-e\s+['"].*(?:exec|spawn|child_process)/i, // Node.js code exec
57
- /\bawk\s+.*\bsystem\s*\(/i, // awk system() call
58
- // Unicode/special character bypass attempts
59
- // eslint-disable-next-line no-control-regex
60
- /[\u0000-\u001f]/, // Control characters (except common whitespace handled separately)
61
- /[\u007f-\u009f]/, // Delete and C1 control codes
62
- /[\u200b-\u200f]/, // Zero-width and directional chars
63
- /[\u2028\u2029]/, // Line/paragraph separators
64
- /[\ufeff]/, // BOM
65
- /[\ufff0-\uffff]/, // Specials block
66
- ];
67
- /**
68
- * Control characters that are never allowed in commands
69
- * These could be used to manipulate terminal output or bypass validation
70
- */
71
- // eslint-disable-next-line no-control-regex
72
- const BLOCKED_CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
73
- /**
74
- * ANSI escape sequences that could manipulate terminal display
75
- */
76
- // eslint-disable-next-line no-control-regex
77
- const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[a-zA-Z]|\x1b[PX^_][^\x1b]*\x1b\\|\x1b\][^\x07]*\x07/;
78
- /**
79
- * Allowlist of safe base commands
80
- * Only commands starting with these are allowed in strict mode
81
- * Reserved for future strict mode implementation
82
- */
83
- const _ALLOWED_COMMANDS = new Set([
84
- // File operations (read-only or safe)
85
- 'ls', 'cat', 'head', 'tail', 'less', 'more', 'file', 'stat', 'wc',
86
- 'find', 'locate', 'which', 'whereis', 'type',
87
- // Text processing
88
- 'grep', 'egrep', 'fgrep', 'rg', 'ag', 'ack',
89
- 'sed', 'awk', 'cut', 'sort', 'uniq', 'tr', 'diff', 'comm',
90
- // Development tools
91
- 'git', 'npm', 'npx', 'yarn', 'pnpm', 'bun',
92
- 'node', 'deno', 'python', 'python3', 'pip', 'pip3',
93
- 'cargo', 'rustc', 'go', 'java', 'javac', 'mvn', 'gradle',
94
- 'make', 'cmake', 'gcc', 'g++', 'clang',
95
- // Build and test
96
- 'jest', 'vitest', 'mocha', 'pytest', 'tsc', 'esbuild', 'vite', 'webpack',
97
- 'eslint', 'prettier', 'biome',
98
- // System info (safe read-only)
99
- 'echo', 'printf', 'pwd', 'date', 'whoami', 'hostname', 'uname',
100
- 'env', 'printenv', 'id', 'groups',
101
- // Process info
102
- 'ps', 'top', 'htop', 'pgrep',
103
- // Network diagnostics (read-only)
104
- 'ping', 'dig', 'nslookup', 'host',
105
- // Archives (read operations)
106
- 'tar', 'zip', 'unzip', 'gzip', 'gunzip', 'bzip2', 'xz',
107
- // Directory operations
108
- 'mkdir', 'rmdir', 'cd',
109
- // Safe file operations
110
- 'cp', 'mv', 'touch', 'ln',
111
- // Docker (controlled)
112
- 'docker', 'docker-compose', 'podman',
113
- // Kubernetes (controlled)
114
- 'kubectl', 'helm',
115
- // Cloud CLI (controlled)
116
- 'aws', 'gcloud', 'az',
117
- // Misc safe commands
118
- 'jq', 'yq', 'tree', 'realpath', 'basename', 'dirname',
119
- 'sleep', 'true', 'false', 'test', '[',
120
- // Package managers
121
- 'apt', 'apt-get', 'brew', 'dnf', 'yum', 'pacman',
122
- ]);
123
- /**
124
- * Commands that should be completely blocked even in non-strict mode
125
- */
126
- const BLOCKED_COMMANDS = new Set([
127
- 'rm', 'shred', 'wipefs', // Destructive file operations (blocked without confirmation path)
128
- 'mkfs', 'fdisk', 'parted', // Disk operations
129
- 'dd', // Raw disk operations
130
- 'chmod', 'chown', 'chgrp', // Permission changes (blocked at base level)
131
- 'sudo', 'su', 'doas', // Privilege escalation
132
- 'nc', 'netcat', 'ncat', // Network tools that can be dangerous
133
- 'socat', // Socket relay
134
- 'telnet', 'ftp', // Insecure protocols
135
- 'nmap', 'masscan', // Port scanning
136
- 'tcpdump', 'wireshark', 'tshark', // Packet capture
137
- 'strace', 'ltrace', 'ptrace', // Process tracing
138
- 'gdb', 'lldb', // Debuggers (can be abused)
139
- 'reboot', 'shutdown', 'poweroff', 'halt', // System control
140
- 'init', 'systemctl', 'service', // Service control
141
- 'iptables', 'nft', 'firewall-cmd', // Firewall
142
- 'mount', 'umount', // Mount operations
143
- 'insmod', 'rmmod', 'modprobe', // Kernel modules
144
- 'sysctl', // Kernel parameters
145
- 'crontab', 'at', // Scheduled tasks
146
- 'useradd', 'userdel', 'usermod', // User management
147
- 'passwd', 'chpasswd', // Password changes
148
- 'visudo', // Sudoers editing
149
- 'ssh-keygen', 'ssh-add', // SSH key operations
150
- 'gpg', // GPG operations
151
- 'openssl', // Certificate operations (can leak keys)
152
- ]);
153
- /**
154
- * Whitelist of safe environment variables to pass to child processes
155
- * All other env vars (especially secrets) are filtered out
156
- */
157
- const SAFE_ENV_VARS = new Set([
158
- // System paths and locale
159
- 'PATH',
160
- 'HOME',
161
- 'USER',
162
- 'SHELL',
163
- 'LANG',
164
- 'LC_ALL',
165
- 'LC_CTYPE',
166
- 'TERM',
167
- 'TZ',
168
- 'TMPDIR',
169
- 'TEMP',
170
- 'TMP',
171
- // Node.js
172
- 'NODE_ENV',
173
- 'NODE_PATH',
174
- 'NODE_OPTIONS',
175
- // Development tools
176
- 'EDITOR',
177
- 'VISUAL',
178
- 'PAGER',
179
- 'LESS',
180
- // Git (non-sensitive)
181
- 'GIT_AUTHOR_NAME',
182
- 'GIT_AUTHOR_EMAIL',
183
- 'GIT_COMMITTER_NAME',
184
- 'GIT_COMMITTER_EMAIL',
185
- 'GIT_TERMINAL_PROMPT',
186
- // CI/CD flags (non-sensitive)
187
- 'CI',
188
- 'CONTINUOUS_INTEGRATION',
189
- // Display
190
- 'DISPLAY',
191
- 'COLORTERM',
192
- // Python
193
- 'PYTHONPATH',
194
- 'PYTHONIOENCODING',
195
- 'VIRTUAL_ENV',
196
- // Package managers (non-sensitive config)
197
- 'NPM_CONFIG_YES',
198
- 'YARN_ENABLE_PROGRESS_BARS',
199
- 'DEBIAN_FRONTEND',
200
- // History control
201
- 'HISTFILE',
202
- 'HISTSIZE',
203
- // Output control
204
- 'NO_COLOR',
205
- 'FORCE_COLOR',
206
- 'NO_TTY',
207
- // Current working directory
208
- 'PWD',
209
- 'OLDPWD',
210
- ]);
211
- /**
212
- * Extract the base command from a command string
213
- * Handles paths, env var prefixes, and common shell constructs
214
- */
215
- function extractBaseCommand(command) {
216
- // Trim and handle empty
217
- const trimmed = command.trim();
218
- if (!trimmed)
219
- return null;
220
- // Skip leading environment variable assignments (VAR=value cmd)
221
- let remaining = trimmed;
222
- while (/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/.test(remaining)) {
223
- remaining = remaining.replace(/^[A-Za-z_][A-Za-z0-9_]*=\S*\s+/, '');
224
- }
225
- // Get the first token
226
- const match = remaining.match(/^(\S+)/);
227
- if (!match)
228
- return null;
229
- let cmd = match[1];
230
- // Remove path prefix (e.g., /usr/bin/ls -> ls)
231
- if (cmd.includes('/')) {
232
- cmd = cmd.split('/').pop() || cmd;
233
- }
234
- // Handle ./ prefix
235
- if (cmd.startsWith('./')) {
236
- cmd = cmd.slice(2);
237
- }
238
- return cmd.toLowerCase();
239
- }
240
- /**
241
- * Check if command uses shell features that could bypass validation
242
- */
243
- function hasShellBypassFeatures(command) {
244
- // Check for multiple commands via && || ; |
245
- // But allow single pipes for grep, etc.
246
- const multiCommandPatterns = [
247
- { pattern: /;\s*\S/, reason: 'Command chaining with semicolon' },
248
- { pattern: /&&\s*\S/, reason: 'Command chaining with &&' },
249
- { pattern: /\|\|\s*\S/, reason: 'Command chaining with ||' },
250
- { pattern: /\|\s*(?:bash|sh|zsh|ksh|csh|fish|dash)\b/i, reason: 'Pipe to shell' },
251
- ];
252
- for (const { pattern, reason } of multiCommandPatterns) {
253
- if (pattern.test(command)) {
254
- // Check if this is a safe pipe (e.g., grep | wc)
255
- if (reason === 'Pipe to shell') {
256
- return { bypass: true, reason };
257
- }
258
- // For other chaining, check if the second command is safe
259
- // For now, we'll allow chaining but each command gets validated separately
260
- }
261
- }
262
- // Check for process substitution
263
- if (/[<>]\(/.test(command)) {
264
- return { bypass: true, reason: 'Process substitution detected' };
265
- }
266
- // Check for here-string/here-doc that could contain encoded payloads
267
- if (/<<</.test(command)) {
268
- return { bypass: true, reason: 'Here-string detected' };
269
- }
270
- return { bypass: false };
271
- }
272
- /**
273
- * Paths that should never be accessed
274
- */
275
- const BLOCKED_PATHS = [
276
- path.join(os.homedir(), '.ssh'),
277
- path.join(os.homedir(), '.gnupg'),
278
- path.join(os.homedir(), '.aws'),
279
- path.join(os.homedir(), '.docker'),
280
- path.join(os.homedir(), '.npmrc'),
281
- path.join(os.homedir(), '.gitconfig'),
282
- path.join(os.homedir(), '.netrc'),
283
- path.join(os.homedir(), '.env'),
284
- path.join(os.homedir(), '.config/gh'),
285
- path.join(os.homedir(), '.config/gcloud'),
286
- path.join(os.homedir(), '.kube'),
287
- '/etc/passwd',
288
- '/etc/shadow',
289
- '/etc/sudoers',
290
- ];
291
- /**
292
- * Bash Tool
293
- *
294
- * Executes shell commands with comprehensive security measures:
295
- * - Blocked dangerous patterns (rm -rf /, fork bombs, etc.)
296
- * - Protected paths (~/.ssh, ~/.aws, /etc/shadow, etc.)
297
- * - User confirmation for commands (unless session-approved)
298
- * - Self-healing: automatic error recovery for common failures
299
- * - Process isolation via spawn with process group management
300
- * - Graceful termination with SIGTERM before SIGKILL
301
- *
302
- * Security modes are controlled by SandboxManager configuration.
303
- * Self-healing can be disabled via --no-self-heal flag.
304
- */
305
- export class BashTool {
306
- currentDirectory = process.cwd();
307
- confirmationService = ConfirmationService.getInstance();
308
- sandboxManager = getSandboxManager();
309
- selfHealingEngine = getSelfHealingEngine();
310
- selfHealingEnabled = true;
311
- runningProcesses = new Set();
312
- constructor() {
313
- registerDisposable(this);
314
- }
315
- /**
316
- * Clean up resources - kill any running processes
317
- */
318
- dispose() {
319
- for (const proc of this.runningProcesses) {
320
- try {
321
- proc.kill('SIGTERM');
322
- }
323
- catch {
324
- // Process may already be dead
325
- }
326
- }
327
- this.runningProcesses.clear();
328
- }
329
- /**
330
- * Validate command for dangerous patterns
331
- *
332
- * Security checks performed (in order):
333
- * 1. Control characters - blocks terminal manipulation
334
- * 2. ANSI escape sequences - blocks display manipulation
335
- * 3. Shell bypass features - blocks process substitution, here-strings, etc.
336
- * 4. Base command blocklist - blocks known dangerous commands
337
- * 5. Blocked command patterns - blocks known dangerous patterns
338
- * 6. Protected paths - blocks access to sensitive directories
339
- * 7. Sandbox manager validation - additional runtime checks
340
- */
341
- validateCommand(command) {
342
- // Check for dangerous control characters
343
- if (BLOCKED_CONTROL_CHARS.test(command)) {
344
- return {
345
- valid: false,
346
- reason: 'Command contains blocked control characters'
347
- };
348
- }
349
- // Check for ANSI escape sequences that could manipulate terminal
350
- if (ANSI_ESCAPE_PATTERN.test(command)) {
351
- return {
352
- valid: false,
353
- reason: 'Command contains blocked ANSI escape sequences'
354
- };
355
- }
356
- // Check for shell bypass features
357
- const bypassCheck = hasShellBypassFeatures(command);
358
- if (bypassCheck.bypass) {
359
- return {
360
- valid: false,
361
- reason: `Shell bypass blocked: ${bypassCheck.reason}`
362
- };
363
- }
364
- // Extract base command and check against blocklist
365
- const baseCmd = extractBaseCommand(command);
366
- if (baseCmd && BLOCKED_COMMANDS.has(baseCmd)) {
367
- return {
368
- valid: false,
369
- reason: `Blocked command: ${baseCmd}`
370
- };
371
- }
372
- // Check for blocked patterns
373
- for (const pattern of BLOCKED_PATTERNS) {
374
- if (pattern.test(command)) {
375
- return {
376
- valid: false,
377
- reason: `Blocked command pattern detected: ${pattern.source}`
378
- };
379
- }
380
- }
381
- // Check for access to blocked paths
382
- for (const blockedPath of BLOCKED_PATHS) {
383
- if (command.includes(blockedPath)) {
384
- return {
385
- valid: false,
386
- reason: `Access to protected path blocked: ${blockedPath}`
387
- };
388
- }
389
- }
390
- // Also use sandbox manager validation
391
- const sandboxValidation = this.sandboxManager.validateCommand(command);
392
- if (!sandboxValidation.valid) {
393
- return sandboxValidation;
394
- }
395
- return { valid: true };
396
- }
397
- /**
398
- * Filter environment variables to only include safe ones
399
- * This prevents credential leakage to child processes
400
- *
401
- * Security measures:
402
- * - Only allowlisted variable names are passed through
403
- * - Values containing shell metacharacters are sanitized
404
- * - Values that look like secrets are excluded
405
- */
406
- getFilteredEnv() {
407
- const filtered = {};
408
- // Patterns that suggest a value is a secret (even if var name is allowed)
409
- const secretPatterns = [
410
- /^sk-[a-zA-Z0-9]{20,}$/, // OpenAI-style keys
411
- /^xai-[a-zA-Z0-9]{20,}$/, // xAI keys
412
- /^ghp_[a-zA-Z0-9]{36}$/, // GitHub PAT
413
- /^gho_[a-zA-Z0-9]{36}$/, // GitHub OAuth
414
- /^github_pat_/i, // GitHub fine-grained PAT
415
- /^AKIA[A-Z0-9]{16}$/, // AWS Access Key
416
- /^npm_[a-zA-Z0-9]{36}$/, // NPM token
417
- /^eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/, // JWT
418
- /^[a-f0-9]{64}$/i, // Hex-encoded secrets (64 chars)
419
- /^-----BEGIN.*PRIVATE KEY-----/m, // Private keys
420
- ];
421
- for (const [key, value] of Object.entries(process.env)) {
422
- if (value === undefined)
423
- continue;
424
- // Only allow safe variable names
425
- if (!SAFE_ENV_VARS.has(key))
426
- continue;
427
- // Check if value looks like a secret
428
- const looksLikeSecret = secretPatterns.some(pattern => pattern.test(value));
429
- if (looksLikeSecret)
430
- continue;
431
- // Sanitize value - remove control characters
432
- // eslint-disable-next-line no-control-regex
433
- const sanitized = value.replace(/[\x00-\x1f\x7f]/g, '');
434
- filtered[key] = sanitized;
435
- }
436
- return filtered;
437
- }
438
- /**
439
- * Execute a command with streaming output.
440
- * Yields each line of stdout/stderr as it arrives.
441
- * Validates and confirms the command before execution.
442
- */
443
- async *executeStreaming(command, timeout = 30000) {
444
- // Validate command
445
- const validation = this.validateCommand(command);
446
- if (!validation.valid) {
447
- return { success: false, error: `Command blocked: ${validation.reason}` };
448
- }
449
- const commandSafetyValidation = validateCommandSafety(command);
450
- if (!commandSafetyValidation.valid) {
451
- return { success: false, error: `Command blocked: ${commandSafetyValidation.error}` };
452
- }
453
- // Check confirmation
454
- const sessionFlags = this.confirmationService.getSessionFlags();
455
- if (!sessionFlags.bashCommands && !sessionFlags.allOperations) {
456
- const confirmationResult = await this.confirmationService.requestConfirmation({
457
- operation: 'Run bash command (streaming)',
458
- filename: command,
459
- showVSCodeOpen: false,
460
- content: `Command: ${command}\nWorking directory: ${this.currentDirectory}`,
461
- }, 'bash');
462
- if (!confirmationResult.confirmed) {
463
- return { success: false, error: confirmationResult.feedback || 'Cancelled by user' };
464
- }
465
- }
466
- // Spawn the process
467
- const isWindows = process.platform === 'win32';
468
- const filteredEnv = this.getFilteredEnv();
469
- const controlledEnv = {
470
- ...filteredEnv,
471
- HISTFILE: '/dev/null',
472
- HISTSIZE: '0',
473
- CI: 'true',
474
- NO_COLOR: '1',
475
- TERM: 'dumb',
476
- NO_TTY: '1',
477
- GIT_TERMINAL_PROMPT: '0',
478
- NPM_CONFIG_YES: 'true',
479
- LC_ALL: 'C.UTF-8',
480
- LANG: 'C.UTF-8',
481
- PYTHONIOENCODING: 'utf-8',
482
- DEBIAN_FRONTEND: 'noninteractive',
483
- };
484
- const proc = spawn('bash', ['-c', command], {
485
- shell: false,
486
- cwd: this.currentDirectory,
487
- env: controlledEnv,
488
- detached: !isWindows,
489
- stdio: ['ignore', 'pipe', 'pipe'],
490
- });
491
- this.runningProcesses.add(proc);
492
- let stdout = '';
493
- let stderr = '';
494
- let timedOut = false;
495
- const timer = setTimeout(() => {
496
- timedOut = true;
497
- try {
498
- proc.kill('SIGTERM');
499
- }
500
- catch { /* ignore */ }
501
- }, timeout);
502
- try {
503
- // Create a readable stream from stdout and stderr combined
504
- const chunks = [];
505
- let resolve = null;
506
- let done = false;
507
- const onData = (data, isStderr) => {
508
- const text = data.toString();
509
- if (isStderr)
510
- stderr += text;
511
- else
512
- stdout += text;
513
- chunks.push(text);
514
- if (resolve) {
515
- resolve();
516
- resolve = null;
517
- }
518
- };
519
- proc.stdout?.on('data', (data) => onData(data, false));
520
- proc.stderr?.on('data', (data) => onData(data, true));
521
- proc.on('close', () => { done = true; if (resolve) {
522
- resolve();
523
- resolve = null;
524
- } });
525
- while (!done) {
526
- if (chunks.length > 0) {
527
- while (chunks.length > 0) {
528
- yield chunks.shift();
529
- }
530
- }
531
- else {
532
- await new Promise(r => { resolve = r; });
533
- }
534
- }
535
- // Yield remaining chunks
536
- while (chunks.length > 0) {
537
- yield chunks.shift();
538
- }
539
- }
540
- finally {
541
- clearTimeout(timer);
542
- this.runningProcesses.delete(proc);
543
- }
544
- if (timedOut) {
545
- return { success: false, error: `Command timed out after ${timeout}ms` };
546
- }
547
- const exitCode = proc.exitCode ?? 0;
548
- if (exitCode !== 0) {
549
- return { success: false, error: stderr || `Exit code ${exitCode}`, output: stdout };
550
- }
551
- return { success: true, output: stdout };
552
- }
553
- /**
554
- * Execute a command using spawn with process group isolation (safer than exec)
555
- * Inspired by mistral-vibe's robust process handling
556
- */
557
- executeWithSpawn(command, options) {
558
- return new Promise((resolve) => {
559
- let stdout = '';
560
- let stderr = '';
561
- let timedOut = false;
562
- const isWindows = process.platform === 'win32';
563
- // Start with filtered environment (only safe vars, no secrets)
564
- const filteredEnv = this.getFilteredEnv();
565
- // Controlled environment variables for deterministic output
566
- const controlledEnv = {
567
- ...filteredEnv,
568
- // Disable history to prevent command logging
569
- HISTFILE: '/dev/null',
570
- HISTSIZE: '0',
571
- // CI mode for consistent behavior
572
- CI: 'true',
573
- // Disable color output for clean parsing
574
- NO_COLOR: '1',
575
- TERM: 'dumb',
576
- // Disable TTY for non-interactive mode
577
- NO_TTY: '1',
578
- // Disable interactive features
579
- GIT_TERMINAL_PROMPT: '0',
580
- NPM_CONFIG_YES: 'true',
581
- YARN_ENABLE_PROGRESS_BARS: 'false',
582
- // Locale settings for consistent encoding
583
- LC_ALL: 'C.UTF-8',
584
- LANG: 'C.UTF-8',
585
- PYTHONIOENCODING: 'utf-8',
586
- // Force non-interactive for common tools
587
- DEBIAN_FRONTEND: 'noninteractive',
588
- };
589
- const spawnOptions = {
590
- // IMPORTANT: shell must be false when using bash -c
591
- // Using shell: true with bash -c creates double-shell that breaks commands
592
- shell: false,
593
- cwd: options.cwd,
594
- env: controlledEnv,
595
- // Process group isolation on Unix (allows killing entire process tree)
596
- detached: !isWindows,
597
- // Don't inherit stdin - commands should be non-interactive
598
- stdio: ['ignore', 'pipe', 'pipe'],
599
- };
600
- const proc = spawn('bash', ['-c', command], spawnOptions);
601
- // Store process group ID for cleanup
602
- const pgid = proc.pid;
603
- // Graceful termination: SIGTERM first, then SIGKILL after grace period
604
- const gracePeriod = 3000; // 3 seconds grace period
605
- let gracefulTerminationTimer = null;
606
- const killProcess = (signal = 'SIGKILL') => {
607
- try {
608
- if (!isWindows && pgid) {
609
- // Kill the entire process group
610
- process.kill(-pgid, signal);
611
- }
612
- else {
613
- proc.kill(signal);
614
- }
615
- }
616
- catch {
617
- // Process may have already exited
618
- try {
619
- proc.kill('SIGKILL');
620
- }
621
- catch {
622
- // Ignore - process is already gone
623
- }
624
- }
625
- };
626
- const timer = setTimeout(() => {
627
- timedOut = true;
628
- // Try graceful termination first (SIGTERM)
629
- killProcess('SIGTERM');
630
- // If still running after grace period, force kill
631
- gracefulTerminationTimer = setTimeout(() => {
632
- killProcess('SIGKILL');
633
- }, gracePeriod);
634
- }, options.timeout);
635
- const maxBuffer = 1024 * 1024; // 1MB limit
636
- proc.stdout?.on('data', (data) => {
637
- const chunk = data.toString();
638
- if (stdout.length + chunk.length <= maxBuffer) {
639
- stdout += chunk;
640
- }
641
- });
642
- proc.stderr?.on('data', (data) => {
643
- const chunk = data.toString();
644
- if (stderr.length + chunk.length <= maxBuffer) {
645
- stderr += chunk;
646
- }
647
- });
648
- proc.on('close', (exitCode) => {
649
- clearTimeout(timer);
650
- if (gracefulTerminationTimer) {
651
- clearTimeout(gracefulTerminationTimer);
652
- }
653
- if (timedOut) {
654
- resolve({
655
- stdout: stdout.trim(),
656
- stderr: 'Command timed out (graceful termination attempted)',
657
- exitCode: 124
658
- });
659
- }
660
- else {
661
- resolve({
662
- stdout: stdout.trim(),
663
- stderr: stderr.trim(),
664
- exitCode: exitCode ?? 1
665
- });
666
- }
667
- });
668
- proc.on('error', (error) => {
669
- clearTimeout(timer);
670
- if (gracefulTerminationTimer) {
671
- clearTimeout(gracefulTerminationTimer);
672
- }
673
- resolve({
674
- stdout: '',
675
- stderr: error.message,
676
- exitCode: 1
677
- });
678
- });
679
- });
680
- }
681
- /**
682
- * Execute a shell command
683
- *
684
- * Validates command safety, requests user confirmation, and executes
685
- * via spawn with process isolation. Failed commands trigger self-healing
686
- * attempts if enabled.
687
- *
688
- * Special handling for `cd` commands to update working directory state.
689
- *
690
- * @param command - Shell command to execute
691
- * @param timeout - Maximum execution time in ms (default: 30000)
692
- * @returns Command output or error message; test output is parsed and structured
693
- *
694
- * @example
695
- * // Simple command
696
- * await bash.execute('ls -la');
697
- *
698
- * // With custom timeout (2 minutes)
699
- * await bash.execute('npm install', 120000);
700
- */
701
- async execute(command, timeout = 30000) {
702
- try {
703
- // Validate input with schema (enhanced validation)
704
- const schemaValidation = validateWithSchema(bashToolSchemas.execute, { command, timeout }, 'execute');
705
- if (!schemaValidation.valid) {
706
- return {
707
- success: false,
708
- error: `Invalid input: ${schemaValidation.error}`,
709
- };
710
- }
711
- // Additional command safety validation
712
- const commandSafetyValidation = validateCommandSafety(command);
713
- if (!commandSafetyValidation.valid) {
714
- return {
715
- success: false,
716
- error: `Command blocked: ${commandSafetyValidation.error}`,
717
- };
718
- }
719
- // Validate command before any execution (legacy validation)
720
- const validation = this.validateCommand(command);
721
- if (!validation.valid) {
722
- return {
723
- success: false,
724
- error: `Command blocked: ${validation.reason}`,
725
- };
726
- }
727
- // Check if user has already accepted bash commands for this session
728
- const sessionFlags = this.confirmationService.getSessionFlags();
729
- if (!sessionFlags.bashCommands && !sessionFlags.allOperations) {
730
- // Request confirmation showing the command
731
- const confirmationResult = await this.confirmationService.requestConfirmation({
732
- operation: 'Run bash command',
733
- filename: command,
734
- showVSCodeOpen: false,
735
- content: `Command: ${command}\nWorking directory: ${this.currentDirectory}`,
736
- }, 'bash');
737
- if (!confirmationResult.confirmed) {
738
- return {
739
- success: false,
740
- error: confirmationResult.feedback || 'Command execution cancelled by user',
741
- };
742
- }
743
- }
744
- // Handle cd command separately
745
- if (command.startsWith('cd ')) {
746
- const newDir = command.substring(3).trim();
747
- // Remove quotes if present
748
- const cleanDir = newDir.replace(/^["']|["']$/g, '');
749
- try {
750
- process.chdir(cleanDir);
751
- this.currentDirectory = process.cwd();
752
- return {
753
- success: true,
754
- output: `Changed directory to: ${this.currentDirectory}`,
755
- };
756
- }
757
- catch (error) {
758
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
759
- return {
760
- success: false,
761
- error: `Cannot change directory: ${errorMessage}`,
762
- };
763
- }
764
- }
765
- // Execute using spawn (safer than exec)
766
- const result = await this.executeWithSpawn(command, {
767
- timeout,
768
- cwd: this.currentDirectory,
769
- });
770
- if (result.exitCode !== 0) {
771
- const errorMessage = result.stderr || `Command exited with code ${result.exitCode}`;
772
- // Attempt self-healing if enabled
773
- if (this.selfHealingEnabled) {
774
- const healingResult = await this.selfHealingEngine.attemptHealing(command, errorMessage, async (fixCmd) => {
775
- // Execute fix command without self-healing to avoid recursion
776
- const fixResult = await this.executeWithSpawn(fixCmd, {
777
- timeout: timeout * 2, // Give more time for fix commands
778
- cwd: this.currentDirectory,
779
- });
780
- if (fixResult.exitCode === 0) {
781
- return {
782
- success: true,
783
- output: fixResult.stdout || 'Fix applied successfully',
784
- };
785
- }
786
- return {
787
- success: false,
788
- error: fixResult.stderr || `Fix failed with code ${fixResult.exitCode}`,
789
- };
790
- });
791
- if (healingResult.success && healingResult.finalResult) {
792
- return {
793
- success: true,
794
- output: `🔧 Self-healed after ${healingResult.attempts.length} attempt(s)\n` +
795
- `Fix applied: ${healingResult.fixedCommand}\n\n` +
796
- (healingResult.finalResult.output || 'Success'),
797
- };
798
- }
799
- // If healing failed, return original error with healing info
800
- if (healingResult.attempts.length > 0) {
801
- return {
802
- success: false,
803
- error: `${errorMessage}\n\n🔧 Self-healing attempted ${healingResult.attempts.length} fix(es) but failed.`,
804
- };
805
- }
806
- }
807
- return {
808
- success: false,
809
- error: errorMessage,
810
- };
811
- }
812
- const output = result.stdout + (result.stderr ? `\nSTDERR: ${result.stderr}` : '');
813
- const trimmedOutput = output.trim() || 'Command executed successfully (no output)';
814
- // Check if this looks like test output and enrich it
815
- if (isLikelyTestOutput(trimmedOutput)) {
816
- const parsed = parseTestOutput(trimmedOutput);
817
- if (parsed.isTestOutput && parsed.data) {
818
- // Return structured test data as JSON for the renderer
819
- return {
820
- success: true,
821
- output: JSON.stringify(parsed.data),
822
- data: { type: 'test-results', framework: parsed.data.framework },
823
- };
824
- }
825
- }
826
- return {
827
- success: true,
828
- output: trimmedOutput,
829
- };
830
- }
831
- catch (error) {
832
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
833
- return {
834
- success: false,
835
- error: `Command failed: ${errorMessage}`,
836
- };
837
- }
838
- }
839
- /**
840
- * Enable or disable self-healing
841
- */
842
- setSelfHealing(enabled) {
843
- this.selfHealingEnabled = enabled;
844
- }
845
- /**
846
- * Check if self-healing is enabled
847
- */
848
- isSelfHealingEnabled() {
849
- return this.selfHealingEnabled;
850
- }
851
- /**
852
- * Get self-healing engine for configuration
853
- */
854
- getSelfHealingEngine() {
855
- return this.selfHealingEngine;
856
- }
857
- getCurrentDirectory() {
858
- return this.currentDirectory;
859
- }
860
- /**
861
- * Escape shell argument to prevent command injection
862
- */
863
- escapeShellArg(arg) {
864
- // Use single quotes and escape any single quotes in the string
865
- return `'${arg.replace(/'/g, "'\\''")}'`;
866
- }
867
- /**
868
- * List files in a directory (wrapper for `ls -la`)
869
- *
870
- * @param directory - Directory path to list (default: current directory)
871
- * @returns Formatted directory listing or error
872
- */
873
- async listFiles(directory = '.') {
874
- // Validate input with schema
875
- const validation = validateWithSchema(bashToolSchemas.listFiles, { directory }, 'listFiles');
876
- if (!validation.valid) {
877
- return {
878
- success: false,
879
- error: `Invalid input: ${validation.error}`,
880
- };
881
- }
882
- const safeDir = sanitizeForShell(directory);
883
- return this.execute(`ls -la ${safeDir}`);
884
- }
885
- /**
886
- * Find files matching a pattern (wrapper for `find -name -type f`)
887
- *
888
- * @param pattern - Glob pattern to match (e.g., "*.ts", "package.json")
889
- * @param directory - Directory to search in (default: current directory)
890
- * @returns List of matching file paths or error
891
- */
892
- async findFiles(pattern, directory = '.') {
893
- // Validate input with schema
894
- const validation = validateWithSchema(bashToolSchemas.findFiles, { pattern, directory }, 'findFiles');
895
- if (!validation.valid) {
896
- return {
897
- success: false,
898
- error: `Invalid input: ${validation.error}`,
899
- };
900
- }
901
- const safeDir = sanitizeForShell(directory);
902
- const safePattern = sanitizeForShell(pattern);
903
- return this.execute(`find ${safeDir} -name ${safePattern} -type f`);
904
- }
905
- /**
906
- * Search for a pattern in files using ripgrep
907
- *
908
- * Uses @vscode/ripgrep for ultra-fast searching. Results are limited
909
- * to 100 matches for performance.
910
- *
911
- * @param pattern - Regex pattern to search for
912
- * @param files - File or directory to search in (default: current directory)
913
- * @returns Matching lines with file paths and line numbers, or error
914
- */
915
- async grep(pattern, files = '.') {
916
- // Validate input with schema
917
- const validation = validateWithSchema(bashToolSchemas.grep, { pattern, files }, 'grep');
918
- if (!validation.valid) {
919
- return {
920
- success: false,
921
- error: `Invalid input: ${validation.error}`,
922
- };
923
- }
924
- // Use ripgrep for ultra-fast searching
925
- return new Promise((resolve) => {
926
- const args = [
927
- '--no-heading',
928
- '--line-number',
929
- '--color', 'never',
930
- '--max-count', '100', // Limit results for performance
931
- pattern,
932
- files
933
- ];
934
- const rg = spawn(rgPath, args, {
935
- cwd: this.currentDirectory,
936
- env: this.getFilteredEnv(),
937
- });
938
- let stdout = '';
939
- let stderr = '';
940
- rg.stdout?.on('data', (data) => {
941
- if (stdout.length < 5_000_000)
942
- stdout += data.toString();
943
- });
944
- rg.stderr?.on('data', (data) => {
945
- if (stderr.length < 100_000)
946
- stderr += data.toString();
947
- });
948
- rg.on('close', (code) => {
949
- // ripgrep returns 1 if no matches found (not an error)
950
- if (code === 0 || code === 1) {
951
- resolve({
952
- success: true,
953
- output: stdout || 'No matches found',
954
- });
955
- }
956
- else {
957
- resolve({
958
- success: false,
959
- error: stderr || `ripgrep exited with code ${code}`,
960
- output: stdout,
961
- });
962
- }
963
- });
964
- rg.on('error', (error) => {
965
- resolve({
966
- success: false,
967
- error: `ripgrep error: ${error.message}`,
968
- });
969
- });
970
- });
971
- }
972
- }
973
- //# sourceMappingURL=bash.js.map