mirror of https://github.com/buster-so/buster.git
feat: implement bash_execute tool for BUS-1466
- Add bash_execute tool in packages/ai/src/tools/file-tools/bash-execute-tool.ts - Support both single commands and arrays of commands - Use Node's child_process with proper timeout and error handling - Capture stdout, stderr, and exit codes in structured format - Add unit tests for schema validation - Export tool in file-tools index and main tools index Implements BUS-1466: TypeScript-based bash execution tool with graceful error handling Co-Authored-By: Dallin Bentley <dallinbentley98@gmail.com>
This commit is contained in:
parent
951e142c6f
commit
973cdedc88
|
@ -0,0 +1,55 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { bashExecute } from './bash-execute-tool';
|
||||
|
||||
describe('bash-execute-tool', () => {
|
||||
it('should have correct tool configuration', () => {
|
||||
expect(bashExecute.id).toBe('bash_execute');
|
||||
expect(bashExecute.description).toBe('Executes bash commands and captures stdout, stderr, and exit codes');
|
||||
expect(bashExecute.inputSchema).toBeDefined();
|
||||
expect(bashExecute.outputSchema).toBeDefined();
|
||||
expect(bashExecute.execute).toBeDefined();
|
||||
});
|
||||
|
||||
it('should validate input schema for single command', () => {
|
||||
const singleCommandInput = {
|
||||
commands: {
|
||||
command: 'echo "hello"',
|
||||
description: 'Test command',
|
||||
timeout: 5000
|
||||
}
|
||||
};
|
||||
|
||||
const result = bashExecute.inputSchema.safeParse(singleCommandInput);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate input schema for array of commands', () => {
|
||||
const arrayCommandInput = {
|
||||
commands: [
|
||||
{ command: 'echo "hello"' },
|
||||
{ command: 'echo "world"', timeout: 1000 }
|
||||
]
|
||||
};
|
||||
|
||||
const result = bashExecute.inputSchema.safeParse(arrayCommandInput);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate output schema structure', () => {
|
||||
const outputExample = {
|
||||
results: [
|
||||
{
|
||||
command: 'echo "test"',
|
||||
stdout: 'test',
|
||||
stderr: undefined,
|
||||
exitCode: 0,
|
||||
success: true,
|
||||
error: undefined
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const result = bashExecute.outputSchema.safeParse(outputExample);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,128 @@
|
|||
import { createTool } from '@mastra/core';
|
||||
import type { RuntimeContext } from '@mastra/core/runtime-context';
|
||||
import { wrapTraced } from 'braintrust';
|
||||
import { z } from 'zod';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const bashCommandSchema = z.object({
|
||||
command: z.string().describe('The bash command to execute'),
|
||||
description: z.string().optional().describe('Description of what this command does'),
|
||||
timeout: z.number().optional().describe('Timeout in milliseconds')
|
||||
});
|
||||
|
||||
const inputSchema = z.object({
|
||||
commands: z.union([
|
||||
bashCommandSchema,
|
||||
z.array(bashCommandSchema)
|
||||
]).describe('Single command or array of bash commands to execute')
|
||||
});
|
||||
|
||||
const outputSchema = z.object({
|
||||
results: z.array(z.object({
|
||||
command: z.string(),
|
||||
stdout: z.string(),
|
||||
stderr: z.string().optional(),
|
||||
exitCode: z.number(),
|
||||
success: z.boolean(),
|
||||
error: z.string().optional()
|
||||
}))
|
||||
});
|
||||
|
||||
async function executeSingleBashCommand(
|
||||
command: string,
|
||||
timeout?: number
|
||||
): Promise<{
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
}> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('bash', ['-c', command], {
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let timeoutId: NodeJS.Timeout | undefined;
|
||||
|
||||
if (timeout) {
|
||||
timeoutId = setTimeout(() => {
|
||||
child.kill('SIGTERM');
|
||||
reject(new Error(`Command timed out after ${timeout}ms`));
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
resolve({
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
exitCode: code || 0
|
||||
});
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const executeBashCommands = wrapTraced(
|
||||
async (
|
||||
input: z.infer<typeof inputSchema>,
|
||||
runtimeContext?: RuntimeContext
|
||||
): Promise<z.infer<typeof outputSchema>> => {
|
||||
const commands = Array.isArray(input.commands) ? input.commands : [input.commands];
|
||||
const results = [];
|
||||
|
||||
for (const cmd of commands) {
|
||||
try {
|
||||
const result = await executeSingleBashCommand(cmd.command, cmd.timeout);
|
||||
|
||||
results.push({
|
||||
command: cmd.command,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr || undefined,
|
||||
exitCode: result.exitCode,
|
||||
success: result.exitCode === 0,
|
||||
error: result.exitCode !== 0 ? result.stderr || 'Command failed' : undefined
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
command: cmd.command,
|
||||
stdout: '',
|
||||
stderr: undefined,
|
||||
exitCode: 1,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown execution error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { results };
|
||||
},
|
||||
{ name: 'bash-execute-tool' }
|
||||
);
|
||||
|
||||
export const bashExecute = createTool({
|
||||
id: 'bash_execute',
|
||||
description: 'Executes bash commands and captures stdout, stderr, and exit codes',
|
||||
inputSchema,
|
||||
outputSchema,
|
||||
execute: async ({ context, runtimeContext }) => {
|
||||
return await executeBashCommands(context, runtimeContext);
|
||||
}
|
||||
});
|
|
@ -0,0 +1 @@
|
|||
export { bashExecute } from './bash-execute-tool';
|
|
@ -12,3 +12,4 @@ export { createTodoList } from './planning-thinking-tools/create-todo-item-tool'
|
|||
export { editFiles } from './file-tools/edit-files-tool/edit-files-tool';
|
||||
export { readFiles } from './file-tools/read-files-tool/read-files-tool';
|
||||
export { createFiles } from './file-tools/create-files-tool/create-file-tool';
|
||||
export { bashExecute } from './file-tools';
|
||||
|
|
Loading…
Reference in New Issue