capsule-ai-cli
Version:
The AI Model Orchestrator - Intelligent multi-model workflows with device-locked licensing
206 lines • 7.36 kB
JavaScript
import { exec, spawn } from 'child_process';
import { promisify } from 'util';
import { BaseTool } from '../base.js';
const execAsync = promisify(exec);
export class BashTool extends BaseTool {
name = 'bash';
displayName = '🖥️ Bash';
description = 'Execute bash commands. LAST RESORT - prefer specialized tools (search, git, file_*) which are faster and smarter.';
category = 'system';
icon = '🖥️';
parameters = [
{
name: 'command',
type: 'string',
description: 'The bash command to execute',
required: true
},
{
name: 'cwd',
type: 'string',
description: 'Working directory for the command (optional)',
required: false
},
{
name: 'timeout',
type: 'number',
description: 'Command timeout in milliseconds (default: 30000)',
required: false,
default: 30000
},
{
name: 'stream',
type: 'boolean',
description: 'Stream output in real-time (default: false)',
required: false,
default: false
}
];
permissions = {
shell: true
};
ui = {
showProgress: true,
collapsible: true,
dangerous: true
};
async run(params, context) {
const { command, cwd, timeout = 30000, stream = false } = params;
if (!command || typeof command !== 'string') {
throw new Error('Command must be a non-empty string');
}
const workingDir = cwd || context.workingDirectory || process.cwd();
this.reportProgress(context, `Executing: ${command}`, 10);
try {
if (stream) {
return await this.runStreaming(command, workingDir, timeout, context);
}
else {
return await this.runNonStreaming(command, workingDir, timeout, context);
}
}
catch (error) {
if (error.killed && error.signal === 'SIGTERM') {
throw new Error(`Command timed out after ${timeout}ms`);
}
throw error;
}
}
async runNonStreaming(command, cwd, timeout, context) {
const startTime = Date.now();
try {
const { stdout, stderr } = await execAsync(command, {
cwd,
timeout,
maxBuffer: 1024 * 1024 * 10,
shell: '/bin/bash'
});
const endTime = Date.now();
const duration = endTime - startTime;
this.reportProgress(context, 'Command completed', 100);
const maxOutputLength = 1000;
let truncatedStdout = stdout.toString();
let truncatedStderr = stderr.toString();
let outputTruncated = false;
if (truncatedStdout.length > maxOutputLength) {
truncatedStdout = truncatedStdout.substring(0, maxOutputLength) + '\n... [output truncated]';
outputTruncated = true;
}
if (truncatedStderr.length > maxOutputLength) {
truncatedStderr = truncatedStderr.substring(0, maxOutputLength) + '\n... [error output truncated]';
outputTruncated = true;
}
return {
success: true,
stdout: truncatedStdout,
stderr: truncatedStderr,
exitCode: 0,
duration,
command,
cwd,
outputTruncated
};
}
catch (error) {
const endTime = Date.now();
const duration = endTime - startTime;
const maxOutputLength = 1000;
let truncatedStdout = error.stdout?.toString() || '';
let truncatedStderr = error.stderr?.toString() || error.message;
let outputTruncated = false;
if (truncatedStdout.length > maxOutputLength) {
truncatedStdout = truncatedStdout.substring(0, maxOutputLength) + '\n... [output truncated]';
outputTruncated = true;
}
if (truncatedStderr.length > maxOutputLength) {
truncatedStderr = truncatedStderr.substring(0, maxOutputLength) + '\n... [error output truncated]';
outputTruncated = true;
}
return {
success: false,
stdout: truncatedStdout,
stderr: truncatedStderr,
exitCode: error.code || 1,
duration,
command,
cwd,
error: error.message,
outputTruncated
};
}
}
async runStreaming(command, cwd, timeout, context) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const output = [];
const errorOutput = [];
const proc = spawn('/bin/bash', ['-c', command], {
cwd,
shell: false
});
const timeoutHandle = setTimeout(() => {
proc.kill('SIGTERM');
}, timeout);
proc.stdout.on('data', (data) => {
const text = data.toString();
output.push(text);
const preview = text.split('\n')[0].substring(0, 50);
this.reportProgress(context, `Output: ${preview}...`, 50);
});
proc.stderr.on('data', (data) => {
const text = data.toString();
errorOutput.push(text);
});
proc.on('close', (code) => {
clearTimeout(timeoutHandle);
const endTime = Date.now();
const duration = endTime - startTime;
this.reportProgress(context, 'Command completed', 100);
resolve({
success: code === 0,
stdout: output.join(''),
stderr: errorOutput.join(''),
exitCode: code || 0,
duration,
command,
cwd
});
});
proc.on('error', (error) => {
clearTimeout(timeoutHandle);
reject(error);
});
});
}
formatResult(result) {
if (!result)
return '';
let output = '';
output += `$ ${result.command}\n`;
if (result.cwd && result.cwd !== process.cwd()) {
output += `(in ${result.cwd})\n`;
}
output += '\n';
if (result.stdout) {
output += result.stdout;
if (!result.stdout.endsWith('\n')) {
output += '\n';
}
}
if (result.stderr && !result.success) {
output += '\n[ERROR]\n';
output += result.stderr;
if (!result.stderr.endsWith('\n')) {
output += '\n';
}
}
if (result.exitCode !== 0) {
output += `\nExit code: ${result.exitCode}`;
}
if (result.duration) {
output += `\n(Completed in ${result.duration}ms)`;
}
return output;
}
}
//# sourceMappingURL=bash.js.map