@@ -18,7 +18,7 @@ export function registerDefaultTools(registry: ToolRegistry): void {
1818 } ,
1919 timeout : {
2020 type : "number" ,
21- description : "Timeout in milliseconds (default: 30000 )"
21+ description : "Timeout in seconds (default: 30 )"
2222 } ,
2323 cwd : {
2424 type : "string" ,
@@ -29,25 +29,49 @@ export function registerDefaultTools(registry: ToolRegistry): void {
2929 } ,
3030 source : "local" as const
3131 } , async ( args ) => {
32- const { exec } = await import ( "child_process" ) ;
33- const { promisify } = await import ( "util" ) ;
34- const execAsync = promisify ( exec ) ;
32+ const { spawn } = await import ( "child_process" ) ;
3533
36- try {
37- const command = resolveBashCommand ( args ) ;
38- if ( ! command ) {
39- throw new Error ( "bash: missing required argument 'command'" ) ;
40- }
41- const timeout = resolveTimeout ( args . timeout ) ;
42- const cwd = resolveWorkingDirectory ( args ) ;
43- const { stdout, stderr } = await execAsync ( command , {
44- timeout : timeout ?? 30000 ,
45- cwd : cwd
46- } ) ;
47- return stdout || stderr || "Command executed successfully" ;
48- } catch ( error : any ) {
49- throw error ;
34+ const command = resolveBashCommand ( args ) ;
35+ if ( ! command ) {
36+ throw new Error ( "bash: missing required argument 'command'" ) ;
5037 }
38+ const timeoutMs = resolveTimeoutMs ( args . timeout ) ;
39+ const cwd = resolveWorkingDirectory ( args ) ;
40+
41+ return new Promise < string > ( ( resolve , reject ) => {
42+ const proc = spawn ( command , {
43+ shell : process . env . SHELL || "/bin/bash" ,
44+ cwd,
45+ } ) ;
46+
47+ const stdoutChunks : Buffer [ ] = [ ] ;
48+ const stderrChunks : Buffer [ ] = [ ] ;
49+ let timedOut = false ;
50+
51+ const timer = setTimeout ( ( ) => {
52+ timedOut = true ;
53+ proc . kill ( "SIGTERM" ) ;
54+ } , timeoutMs ) ;
55+
56+ proc . stdout . on ( "data" , ( chunk : Buffer ) => stdoutChunks . push ( chunk ) ) ;
57+ proc . stderr . on ( "data" , ( chunk : Buffer ) => stderrChunks . push ( chunk ) ) ;
58+
59+ proc . on ( "close" , ( code ) => {
60+ clearTimeout ( timer ) ;
61+ const stdout = Buffer . concat ( stdoutChunks ) . toString ( "utf8" ) ;
62+ const stderr = Buffer . concat ( stderrChunks ) . toString ( "utf8" ) ;
63+ const output = stdout || stderr || "Command executed successfully" ;
64+ if ( timedOut ) {
65+ resolve ( `Command timed out after ${ timeoutMs / 1000 } s\n${ output } ` ) ;
66+ } else if ( code !== 0 ) {
67+ resolve ( `${ output } \n[Exit code: ${ code } ]` ) ;
68+ } else {
69+ resolve ( output ) ;
70+ }
71+ } ) ;
72+
73+ proc . on ( "error" , reject ) ;
74+ } ) ;
5175 } ) ;
5276
5377 // 2. Read tool - Read file contents
@@ -598,6 +622,14 @@ function resolveTimeout(value: unknown): number | undefined {
598622 return undefined ;
599623}
600624
625+ // Convert model-supplied timeout (seconds) to milliseconds. Falls back to 30s.
626+ function resolveTimeoutMs ( value : unknown ) : number {
627+ const raw = resolveTimeout ( value ) ;
628+ if ( raw === undefined ) return 30_000 ;
629+ // Values ≤ 600 are treated as seconds (no real use case for a <600ms shell timeout).
630+ return raw <= 600 ? raw * 1000 : raw ;
631+ }
632+
601633function resolveBoolean ( value : unknown , defaultValue : boolean ) : boolean {
602634 if ( typeof value === "boolean" ) {
603635 return value ;
0 commit comments