2025-10-26 19:50:41 +00:00
import z from "zod"
2025-10-12 04:24:48 +00:00
import { spawn } from "child_process"
2025-05-31 18:41:00 +00:00
import { Tool } from "./tool"
2025-06-04 17:12:13 +00:00
import DESCRIPTION from "./bash.txt"
2025-08-01 00:40:05 +00:00
import { Log } from "../util/log"
2025-09-01 21:15:49 +00:00
import { Instance } from "../project/instance"
2025-10-31 19:07:36 +00:00
import { lazy } from "@/util/lazy"
import { Language } from "web-tree-sitter"
2026-01-01 22:54:11 +00:00
2025-10-31 19:07:36 +00:00
import { $ } from "bun"
import { Filesystem } from "@/util/filesystem"
2025-11-18 06:46:49 +00:00
import { fileURLToPath } from "url"
2025-11-20 16:45:13 +00:00
import { Flag } from "@/flag/flag.ts"
2025-12-12 22:11:07 +00:00
import { Shell } from "@/shell/shell"
2025-05-19 23:29:38 +00:00
2026-01-01 22:54:11 +00:00
import { BashArity } from "@/permission/arity"
2025-12-09 21:44:37 +00:00
const MAX_OUTPUT_LENGTH = Flag . OPENCODE_EXPERIMENTAL_BASH_MAX_OUTPUT_LENGTH || 30 _000
2025-12-09 23:28:34 +00:00
const DEFAULT_TIMEOUT = Flag . OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || 2 * 60 * 1000
2025-05-19 23:29:38 +00:00
2025-10-31 19:07:36 +00:00
export const log = Log . create ( { service : "bash-tool" } )
2025-08-01 01:41:48 +00:00
2025-11-18 06:46:49 +00:00
const resolveWasm = ( asset : string ) = > {
if ( asset . startsWith ( "file://" ) ) return fileURLToPath ( asset )
2025-11-18 20:06:45 +00:00
if ( asset . startsWith ( "/" ) || /^[a-z]:/i . test ( asset ) ) return asset
2025-11-18 06:46:49 +00:00
const url = new URL ( asset , import . meta . url )
return fileURLToPath ( url )
}
2025-10-07 03:24:07 +00:00
const parser = lazy ( async ( ) = > {
2025-10-31 19:07:36 +00:00
const { Parser } = await import ( "web-tree-sitter" )
const { default : treeWasm } = await import ( "web-tree-sitter/tree-sitter.wasm" as string , {
with : { type : "wasm" } ,
} )
2025-11-18 06:46:49 +00:00
const treePath = resolveWasm ( treeWasm )
2025-10-31 19:07:36 +00:00
await Parser . init ( {
locateFile() {
2025-11-18 06:46:49 +00:00
return treePath
2025-10-31 19:07:36 +00:00
} ,
} )
const { default : bashWasm } = await import ( "tree-sitter-bash/tree-sitter-bash.wasm" as string , {
with : { type : "wasm" } ,
} )
2025-11-18 06:46:49 +00:00
const bashPath = resolveWasm ( bashWasm )
const bashLanguage = await Language . load ( bashPath )
2025-10-31 19:07:36 +00:00
const p = new Parser ( )
p . setLanguage ( bashLanguage )
return p
2025-07-31 21:19:56 +00:00
} )
2025-07-31 00:57:52 +00:00
2025-11-21 22:29:47 +00:00
// TODO: we may wanna rename this tool so it works better on other shells
export const BashTool = Tool . define ( "bash" , async ( ) = > {
2025-12-12 22:11:07 +00:00
const shell = Shell . acceptable ( )
2025-11-21 22:29:47 +00:00
log . info ( "bash tool using shell" , { shell } )
return {
2025-12-11 20:58:44 +00:00
description : DESCRIPTION.replaceAll ( "${directory}" , Instance . directory ) ,
2025-11-21 22:29:47 +00:00
parameters : z.object ( {
command : z.string ( ) . describe ( "The command to execute" ) ,
timeout : z.number ( ) . describe ( "Optional timeout in milliseconds" ) . optional ( ) ,
2025-12-07 05:55:07 +00:00
workdir : z
. string ( )
. describe (
` The working directory to run the command in. Defaults to ${ Instance . directory } . Use this instead of 'cd' commands. ` ,
)
. optional ( ) ,
2025-11-21 22:29:47 +00:00
description : z
. string ( )
. describe (
"Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'" ,
) ,
} ) ,
async execute ( params , ctx ) {
2025-12-07 05:55:07 +00:00
const cwd = params . workdir || Instance . directory
2025-11-21 22:29:47 +00:00
if ( params . timeout !== undefined && params . timeout < 0 ) {
throw new Error ( ` Invalid timeout value: ${ params . timeout } . Timeout must be a positive number. ` )
}
2025-12-07 05:55:07 +00:00
const timeout = params . timeout ? ? DEFAULT_TIMEOUT
2025-11-21 22:29:47 +00:00
const tree = await parser ( ) . then ( ( p ) = > p . parse ( params . command ) )
if ( ! tree ) {
throw new Error ( "Failed to parse command" )
2025-07-31 00:57:52 +00:00
}
2026-01-01 22:54:11 +00:00
const directories = new Set < string > ( )
if ( ! Filesystem . contains ( Instance . directory , cwd ) ) directories . add ( cwd )
const patterns = new Set < string > ( )
const always = new Set < string > ( )
2025-12-07 05:55:07 +00:00
2025-11-21 22:29:47 +00:00
for ( const node of tree . rootNode . descendantsOfType ( "command" ) ) {
if ( ! node ) continue
const command = [ ]
for ( let i = 0 ; i < node . childCount ; i ++ ) {
const child = node . child ( i )
if ( ! child ) continue
if (
child . type !== "command_name" &&
child . type !== "word" &&
child . type !== "string" &&
child . type !== "raw_string" &&
child . type !== "concatenation"
) {
continue
}
command . push ( child . text )
}
// not an exhaustive list, but covers most common cases
if ( [ "cd" , "rm" , "cp" , "mv" , "mkdir" , "touch" , "chmod" , "chown" ] . includes ( command [ 0 ] ) ) {
for ( const arg of command . slice ( 1 ) ) {
if ( arg . startsWith ( "-" ) || ( command [ 0 ] === "chmod" && arg . startsWith ( "+" ) ) ) continue
const resolved = await $ ` realpath ${ arg } `
. quiet ( )
. nothrow ( )
. text ( )
. then ( ( x ) = > x . trim ( ) )
log . info ( "resolved path" , { arg , resolved } )
if ( resolved ) {
// Git Bash on Windows returns Unix-style paths like /c/Users/...
const normalized =
process . platform === "win32" && resolved . match ( /^\/[a-z]\// )
? resolved . replace ( /^\/([a-z])\// , ( _ , drive ) = > ` ${ drive . toUpperCase ( ) } : \\ ` ) . replace ( /\//g , "\\" )
: resolved
2026-01-01 23:27:23 +00:00
if ( ! Filesystem . contains ( Instance . directory , normalized ) ) directories . add ( normalized )
2025-11-17 07:06:44 +00:00
}
2025-07-31 00:57:52 +00:00
}
}
2026-01-01 22:54:11 +00:00
// cd covered by above check
if ( command . length && command [ 0 ] !== "cd" ) {
patterns . add ( command . join ( " " ) )
always . add ( BashArity . prefix ( command ) . join ( " " ) + "*" )
2025-09-14 14:01:57 +00:00
}
2025-07-31 00:57:52 +00:00
}
2026-01-01 22:54:11 +00:00
if ( directories . size > 0 ) {
await ctx . ask ( {
permission : "external_directory" ,
patterns : Array.from ( directories ) ,
always : Array.from ( directories ) . map ( ( x ) = > x + "*" ) ,
metadata : { } ,
} )
}
if ( patterns . size > 0 ) {
await ctx . ask ( {
permission : "bash" ,
patterns : Array.from ( patterns ) ,
always : Array.from ( always ) ,
metadata : { } ,
2025-11-21 22:29:47 +00:00
} )
}
const proc = spawn ( params . command , {
shell ,
2025-12-07 05:55:07 +00:00
cwd ,
2025-11-21 22:29:47 +00:00
env : {
. . . process . env ,
2025-07-31 00:57:52 +00:00
} ,
2025-11-21 22:29:47 +00:00
stdio : [ "ignore" , "pipe" , "pipe" ] ,
detached : process.platform !== "win32" ,
2025-07-31 00:57:52 +00:00
} )
2025-07-31 14:34:43 +00:00
2025-11-21 22:29:47 +00:00
let output = ""
// Initialize metadata with empty output
2025-08-11 05:23:00 +00:00
ctx . metadata ( {
metadata : {
2025-11-21 22:29:47 +00:00
output : "" ,
2025-08-11 05:23:00 +00:00
description : params.description ,
} ,
} )
2025-11-21 22:29:47 +00:00
const append = ( chunk : Buffer ) = > {
2025-12-05 19:56:56 +00:00
if ( output . length <= MAX_OUTPUT_LENGTH ) {
output += chunk . toString ( )
ctx . metadata ( {
metadata : {
output ,
description : params.description ,
} ,
} )
}
2025-11-21 22:29:47 +00:00
}
2025-10-16 19:39:36 +00:00
2025-11-21 22:29:47 +00:00
proc . stdout ? . on ( "data" , append )
proc . stderr ? . on ( "data" , append )
2025-08-03 19:34:37 +00:00
2025-11-21 22:29:47 +00:00
let timedOut = false
let aborted = false
let exited = false
2025-10-16 19:39:36 +00:00
2025-12-12 22:11:07 +00:00
const kill = ( ) = > Shell . killTree ( proc , { exited : ( ) = > exited } )
2025-10-16 19:39:36 +00:00
2025-11-21 22:29:47 +00:00
if ( ctx . abort . aborted ) {
aborted = true
2025-12-12 22:11:07 +00:00
await kill ( )
2025-11-21 22:29:47 +00:00
}
2025-10-16 19:39:36 +00:00
2025-11-21 22:29:47 +00:00
const abortHandler = ( ) = > {
aborted = true
2025-12-12 22:11:07 +00:00
void kill ( )
2025-11-21 22:29:47 +00:00
}
2025-10-16 19:39:36 +00:00
2025-11-21 22:29:47 +00:00
ctx . abort . addEventListener ( "abort" , abortHandler , { once : true } )
2025-10-16 19:39:36 +00:00
2025-11-21 22:29:47 +00:00
const timeoutTimer = setTimeout ( ( ) = > {
timedOut = true
2025-12-12 22:11:07 +00:00
void kill ( )
2025-12-05 19:56:56 +00:00
} , timeout + 100 )
2025-10-16 19:39:36 +00:00
2025-11-21 22:29:47 +00:00
await new Promise < void > ( ( resolve , reject ) = > {
const cleanup = ( ) = > {
clearTimeout ( timeoutTimer )
ctx . abort . removeEventListener ( "abort" , abortHandler )
}
2025-10-16 19:39:36 +00:00
2025-11-21 22:29:47 +00:00
proc . once ( "exit" , ( ) = > {
exited = true
cleanup ( )
resolve ( )
} )
2025-08-03 19:34:37 +00:00
2025-11-21 22:29:47 +00:00
proc . once ( "error" , ( error ) = > {
exited = true
cleanup ( )
reject ( error )
} )
2025-10-16 19:39:36 +00:00
} )
2025-06-03 17:08:47 +00:00
2025-12-04 17:33:00 +00:00
let resultMetadata : String [ ] = [ "<bash_metadata>" ]
2025-11-21 22:29:47 +00:00
if ( output . length > MAX_OUTPUT_LENGTH ) {
output = output . slice ( 0 , MAX_OUTPUT_LENGTH )
2025-12-05 19:56:56 +00:00
resultMetadata . push ( ` bash tool truncated output as it exceeded ${ MAX_OUTPUT_LENGTH } char limit ` )
2025-11-21 22:29:47 +00:00
}
2025-08-12 18:51:13 +00:00
2025-11-21 22:29:47 +00:00
if ( timedOut ) {
2025-12-30 15:42:04 +00:00
resultMetadata . push ( ` bash tool terminated command after exceeding timeout ${ timeout } ms ` )
2025-11-21 22:29:47 +00:00
}
2025-10-06 04:55:01 +00:00
2025-11-21 22:29:47 +00:00
if ( aborted ) {
2025-12-05 19:56:56 +00:00
resultMetadata . push ( "User aborted the command" )
2025-12-04 17:33:00 +00:00
}
if ( resultMetadata . length > 1 ) {
resultMetadata . push ( "</bash_metadata>" )
output += "\n\n" + resultMetadata . join ( "\n" )
2025-11-21 22:29:47 +00:00
}
2025-10-16 19:39:36 +00:00
2025-11-21 22:29:47 +00:00
return {
title : params.description ,
metadata : {
output ,
exit : proc.exitCode ,
description : params.description ,
} ,
2025-08-11 05:23:00 +00:00
output ,
2025-11-21 22:29:47 +00:00
}
} ,
}
2025-05-31 18:41:00 +00:00
} )