2025-04-24 18:03:33 -07:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2025-08-25 22:11:27 +02:00
import fs from 'node:fs' ;
import path from 'node:path' ;
import os , { EOL } from 'node:os' ;
import crypto from 'node:crypto' ;
2025-08-26 00:04:53 +02:00
import type { Config } from '../config/config.js' ;
2025-09-18 21:10:03 +08:00
import { ToolNames } from './tool-names.js' ;
2025-08-26 15:26:16 -04:00
import { ToolErrorType } from './tool-error.js' ;
2025-08-26 00:04:53 +02:00
import type {
2025-08-13 12:27:09 -07:00
ToolInvocation ,
2025-04-25 14:05:58 -07:00
ToolResult ,
2025-09-08 20:01:49 +08:00
ToolResultDisplay ,
2025-04-25 14:05:58 -07:00
ToolCallConfirmationDetails ,
ToolExecuteConfirmationDetails ,
2025-08-26 00:04:53 +02:00
} from './tools.js' ;
import {
BaseDeclarativeTool ,
BaseToolInvocation ,
2025-04-25 14:05:58 -07:00
ToolConfirmationOutcome ,
2025-08-13 12:58:26 -03:00
Kind ,
2025-04-25 14:05:58 -07:00
} from './tools.js' ;
fix(shell): Improve error reporting for shell command failures
This commit enhances the tool to provide more informative feedback to the user when a shell command fails, especially in non-debug mode.
Previously, if a command terminated due to a signal (e.g., SIGPIPE during a with no upstream) or failed without producing stdout/stderr, the user would see no output, making it difficult to diagnose the issue.
Changes:
- Modified to update the logic.
- If a command produces no direct output but results in an error, signal, non-zero exit code, or user cancellation, a concise message indicating this outcome is now shown in .
- Utilized the existing utility from for consistent error message formatting, which also resolved previous TypeScript type inference issues.
This ensures users receive clearer feedback on command execution status, improving the tool's usability and aiding in troubleshooting.
Fixes https://b.corp.google.com/issues/417998119
2025-05-18 00:23:57 -07:00
import { getErrorMessage } from '../utils/errors.js' ;
2025-07-25 21:56:49 -04:00
import { summarizeToolOutput } from '../utils/summarizer.js' ;
2025-08-26 00:04:53 +02:00
import type { ShellOutputEvent } from '../services/shellExecutionService.js' ;
import { ShellExecutionService } from '../services/shellExecutionService.js' ;
2025-07-25 21:56:49 -04:00
import { formatMemoryUsage } from '../utils/formatters.js' ;
2025-07-25 12:25:32 -07:00
import {
getCommandRoots ,
isCommandAllowed ,
stripShellWrapper ,
} from '../utils/shell-utils.js' ;
2025-06-02 14:50:12 -07:00
2025-07-25 21:56:49 -04:00
export const OUTPUT_UPDATE_INTERVAL_MS = 1000 ;
2025-04-24 18:03:33 -07:00
export interface ShellToolParams {
command : string ;
2025-08-27 11:32:48 +08:00
is_background : boolean ;
2025-04-24 18:03:33 -07:00
description? : string ;
2025-04-27 18:57:10 -07:00
directory? : string ;
2025-04-24 18:03:33 -07:00
}
2025-05-30 01:58:09 -07:00
2025-08-13 12:27:09 -07:00
class ShellToolInvocation extends BaseToolInvocation <
ShellToolParams ,
ToolResult
> {
constructor (
private readonly config : Config ,
params : ShellToolParams ,
private readonly allowlist : Set < string > ,
) {
super ( params ) ;
2025-04-25 14:05:58 -07:00
}
2025-08-13 12:27:09 -07:00
getDescription ( ) : string {
let description = ` ${ this . params . command } ` ;
2025-04-30 12:27:56 -07:00
// append optional [in directory]
2025-04-29 15:31:46 -07:00
// note description is needed even if validation fails due to absolute path
2025-08-13 12:27:09 -07:00
if ( this . params . directory ) {
description += ` [in ${ this . params . directory } ] ` ;
2025-04-28 08:17:52 -07:00
}
2025-08-27 11:32:48 +08:00
// append background indicator
if ( this . params . is_background ) {
description += ` [background] ` ;
}
2025-04-28 08:17:52 -07:00
// append optional (description), replacing any line breaks with spaces
2025-08-13 12:27:09 -07:00
if ( this . params . description ) {
description += ` ( ${ this . params . description . replace ( /\n/g , ' ' ) } ) ` ;
2025-04-27 18:57:10 -07:00
}
return description ;
2025-04-25 14:05:58 -07:00
}
2025-08-13 16:17:38 -04:00
override async shouldConfirmExecute (
2025-05-27 23:40:25 -07:00
_abortSignal : AbortSignal ,
2025-04-25 14:05:58 -07:00
) : Promise < ToolCallConfirmationDetails | false > {
2025-08-13 12:27:09 -07:00
const command = stripShellWrapper ( this . params . command ) ;
2025-07-25 12:25:32 -07:00
const rootCommands = [ . . . new Set ( getCommandRoots ( command ) ) ] ;
const commandsToConfirm = rootCommands . filter (
( command ) = > ! this . allowlist . has ( command ) ,
) ;
if ( commandsToConfirm . length === 0 ) {
2025-04-27 18:57:10 -07:00
return false ; // already approved and whitelisted
2025-04-25 14:05:58 -07:00
}
2025-07-25 12:25:32 -07:00
2025-04-25 14:05:58 -07:00
const confirmationDetails : ToolExecuteConfirmationDetails = {
2025-05-22 06:00:36 +00:00
type : 'exec' ,
2025-04-25 14:05:58 -07:00
title : 'Confirm Shell Command' ,
2025-08-13 12:27:09 -07:00
command : this.params.command ,
2025-07-25 12:25:32 -07:00
rootCommand : commandsToConfirm.join ( ', ' ) ,
2025-04-25 14:05:58 -07:00
onConfirm : async ( outcome : ToolConfirmationOutcome ) = > {
if ( outcome === ToolConfirmationOutcome . ProceedAlways ) {
2025-07-25 12:25:32 -07:00
commandsToConfirm . forEach ( ( command ) = > this . allowlist . add ( command ) ) ;
2025-04-25 14:05:58 -07:00
}
} ,
} ;
return confirmationDetails ;
2025-04-24 18:03:33 -07:00
}
2025-05-09 23:29:02 -07:00
async execute (
2025-07-25 12:25:32 -07:00
signal : AbortSignal ,
2025-09-08 20:01:49 +08:00
updateOutput ? : ( output : ToolResultDisplay ) = > void ,
2025-09-01 14:48:55 +08:00
terminalColumns? : number ,
terminalRows? : number ,
2025-05-09 23:29:02 -07:00
) : Promise < ToolResult > {
2025-08-13 12:27:09 -07:00
const strippedCommand = stripShellWrapper ( this . params . command ) ;
2025-04-27 18:57:10 -07:00
2025-07-25 12:25:32 -07:00
if ( signal . aborted ) {
2025-06-08 15:42:49 -07:00
return {
llmContent : 'Command was cancelled by user before it could start.' ,
returnDisplay : 'Command cancelled by user.' ,
} ;
}
2025-06-09 12:19:42 -07:00
const isWindows = os . platform ( ) === 'win32' ;
2025-06-15 02:19:19 -07:00
const tempFileName = ` shell_pgrep_ ${ crypto
. randomBytes ( 6 )
. toString ( 'hex' ) } . tmp ` ;
const tempFilePath = path . join ( os . tmpdir ( ) , tempFileName ) ;
2025-05-06 10:44:40 -07:00
2025-07-25 21:56:49 -04:00
try {
2025-08-07 17:07:56 +08:00
// Add co-author to git commit commands
const processedCommand = this . addCoAuthorToGitCommit ( strippedCommand ) ;
2025-08-27 11:32:48 +08:00
const shouldRunInBackground = this . params . is_background ;
let finalCommand = processedCommand ;
// If explicitly marked as background and doesn't already end with &, add it
if ( shouldRunInBackground && ! finalCommand . trim ( ) . endsWith ( '&' ) ) {
finalCommand = finalCommand . trim ( ) + ' &' ;
}
2025-07-25 21:56:49 -04:00
// pgrep is not available on Windows, so we can't get background PIDs
const commandToExecute = isWindows
2025-08-27 11:32:48 +08:00
? finalCommand
2025-07-25 21:56:49 -04:00
: ( ( ) = > {
// wrap command to append subprocess pids (via pgrep) to temporary file
2025-08-27 11:32:48 +08:00
let command = finalCommand . trim ( ) ;
2025-07-25 21:56:49 -04:00
if ( ! command . endsWith ( '&' ) ) command += ';' ;
return ` { ${ command } }; __code= $ ?; pgrep -g 0 > ${ tempFilePath } 2>&1; exit $ __code; ` ;
} ) ( ) ;
2025-04-27 18:57:10 -07:00
2025-07-25 21:56:49 -04:00
const cwd = path . resolve (
this . config . getTargetDir ( ) ,
2025-08-13 12:27:09 -07:00
this . params . directory || '' ,
2025-07-25 21:56:49 -04:00
) ;
2025-05-30 01:58:09 -07:00
2025-09-01 14:48:55 +08:00
let cumulativeOutput = '' ;
2025-07-25 21:56:49 -04:00
let lastUpdateTime = Date . now ( ) ;
let isBinaryStream = false ;
2025-04-27 18:57:10 -07:00
2025-09-01 14:48:55 +08:00
const { result : resultPromise } = await ShellExecutionService . execute (
2025-07-25 21:56:49 -04:00
commandToExecute ,
cwd ,
( event : ShellOutputEvent ) = > {
if ( ! updateOutput ) {
return ;
}
2025-04-27 18:57:10 -07:00
2025-07-25 21:56:49 -04:00
let currentDisplayOutput = '' ;
let shouldUpdate = false ;
2025-05-09 23:29:02 -07:00
2025-07-25 21:56:49 -04:00
switch ( event . type ) {
case 'data' :
2025-09-01 14:48:55 +08:00
if ( isBinaryStream ) break ;
cumulativeOutput = event . chunk ;
currentDisplayOutput = cumulativeOutput ;
2025-07-25 21:56:49 -04:00
if ( Date . now ( ) - lastUpdateTime > OUTPUT_UPDATE_INTERVAL_MS ) {
shouldUpdate = true ;
}
break ;
case 'binary_detected' :
isBinaryStream = true ;
currentDisplayOutput =
'[Binary output detected. Halting stream...]' ;
shouldUpdate = true ;
break ;
case 'binary_progress' :
isBinaryStream = true ;
currentDisplayOutput = ` [Receiving binary output... ${ formatMemoryUsage (
event . bytesReceived ,
) } received ] ` ;
if ( Date . now ( ) - lastUpdateTime > OUTPUT_UPDATE_INTERVAL_MS ) {
shouldUpdate = true ;
2025-06-09 12:19:42 -07:00
}
2025-07-25 21:56:49 -04:00
break ;
default : {
throw new Error ( 'An unhandled ShellOutputEvent was found.' ) ;
2025-06-09 12:19:42 -07:00
}
2025-05-09 23:29:02 -07:00
}
2025-04-27 18:57:10 -07:00
2025-07-25 21:56:49 -04:00
if ( shouldUpdate ) {
updateOutput ( currentDisplayOutput ) ;
lastUpdateTime = Date . now ( ) ;
}
} ,
signal ,
2025-09-01 14:48:55 +08:00
this . config . getShouldUseNodePtyShell ( ) ,
terminalColumns ,
terminalRows ,
2025-07-25 21:56:49 -04:00
) ;
2025-05-09 23:29:02 -07:00
2025-07-25 21:56:49 -04:00
const result = await resultPromise ;
const backgroundPIDs : number [ ] = [ ] ;
if ( os . platform ( ) !== 'win32' ) {
if ( fs . existsSync ( tempFilePath ) ) {
const pgrepLines = fs
. readFileSync ( tempFilePath , 'utf8' )
2025-08-22 14:10:45 +08:00
. split ( EOL )
2025-07-25 21:56:49 -04:00
. filter ( Boolean ) ;
for ( const line of pgrepLines ) {
if ( ! /^\d+$/ . test ( line ) ) {
console . error ( ` pgrep: ${ line } ` ) ;
}
const pid = Number ( line ) ;
if ( pid !== result . pid ) {
backgroundPIDs . push ( pid ) ;
}
2025-06-09 12:19:42 -07:00
}
2025-07-25 21:56:49 -04:00
} else {
if ( ! signal . aborted ) {
console . error ( 'missing pgrep output' ) ;
2025-06-09 12:19:42 -07:00
}
2025-05-06 10:44:40 -07:00
}
}
2025-07-25 21:56:49 -04:00
let llmContent = '' ;
if ( result . aborted ) {
llmContent = 'Command was cancelled by user before it could complete.' ;
if ( result . output . trim ( ) ) {
2025-09-01 14:48:55 +08:00
llmContent += ` Below is the output before it was cancelled: \ n ${ result . output } ` ;
2025-07-25 21:56:49 -04:00
} else {
llmContent += ' There was no output before it was cancelled.' ;
}
2025-05-27 13:47:40 -07:00
} else {
2025-07-25 21:56:49 -04:00
// Create a formatted error string for display, replacing the wrapper command
// with the user-facing command.
const finalError = result . error
2025-08-13 12:27:09 -07:00
? result . error . message . replace ( commandToExecute , this . params . command )
2025-07-25 21:56:49 -04:00
: '(none)' ;
llmContent = [
2025-08-13 12:27:09 -07:00
` Command: ${ this . params . command } ` ,
` Directory: ${ this . params . directory || '(root)' } ` ,
2025-09-01 14:48:55 +08:00
` Output: ${ result . output || '(empty)' } ` ,
2025-07-25 21:56:49 -04:00
` Error: ${ finalError } ` , // Use the cleaned error string.
` Exit Code: ${ result . exitCode ? ? '(none)' } ` ,
` Signal: ${ result . signal ? ? '(none)' } ` ,
` Background PIDs: ${
backgroundPIDs . length ? backgroundPIDs . join ( ', ' ) : '(none)'
} ` ,
` Process Group PGID: ${ result . pid ? ? '(none)' } ` ,
] . join ( '\n' ) ;
2025-05-27 13:47:40 -07:00
}
2025-04-28 15:05:36 -07:00
2025-07-25 21:56:49 -04:00
let returnDisplayMessage = '' ;
if ( this . config . getDebugMode ( ) ) {
returnDisplayMessage = llmContent ;
fix(shell): Improve error reporting for shell command failures
This commit enhances the tool to provide more informative feedback to the user when a shell command fails, especially in non-debug mode.
Previously, if a command terminated due to a signal (e.g., SIGPIPE during a with no upstream) or failed without producing stdout/stderr, the user would see no output, making it difficult to diagnose the issue.
Changes:
- Modified to update the logic.
- If a command produces no direct output but results in an error, signal, non-zero exit code, or user cancellation, a concise message indicating this outcome is now shown in .
- Utilized the existing utility from for consistent error message formatting, which also resolved previous TypeScript type inference issues.
This ensures users receive clearer feedback on command execution status, improving the tool's usability and aiding in troubleshooting.
Fixes https://b.corp.google.com/issues/417998119
2025-05-18 00:23:57 -07:00
} else {
2025-07-25 21:56:49 -04:00
if ( result . output . trim ( ) ) {
returnDisplayMessage = result . output ;
} else {
if ( result . aborted ) {
returnDisplayMessage = 'Command cancelled by user.' ;
} else if ( result . signal ) {
returnDisplayMessage = ` Command terminated by signal: ${ result . signal } ` ;
} else if ( result . error ) {
returnDisplayMessage = ` Command failed: ${ getErrorMessage (
result . error ,
) } ` ;
} else if ( result . exitCode !== null && result . exitCode !== 0 ) {
returnDisplayMessage = ` Command exited with code: ${ result . exitCode } ` ;
}
// If output is empty and command succeeded (code 0, no error/signal/abort),
// returnDisplayMessage will remain empty, which is fine.
fix(shell): Improve error reporting for shell command failures
This commit enhances the tool to provide more informative feedback to the user when a shell command fails, especially in non-debug mode.
Previously, if a command terminated due to a signal (e.g., SIGPIPE during a with no upstream) or failed without producing stdout/stderr, the user would see no output, making it difficult to diagnose the issue.
Changes:
- Modified to update the logic.
- If a command produces no direct output but results in an error, signal, non-zero exit code, or user cancellation, a concise message indicating this outcome is now shown in .
- Utilized the existing utility from for consistent error message formatting, which also resolved previous TypeScript type inference issues.
This ensures users receive clearer feedback on command execution status, improving the tool's usability and aiding in troubleshooting.
Fixes https://b.corp.google.com/issues/417998119
2025-05-18 00:23:57 -07:00
}
}
2025-07-12 21:09:12 -07:00
2025-07-25 21:56:49 -04:00
const summarizeConfig = this . config . getSummarizeToolOutputConfig ( ) ;
2025-08-26 15:26:16 -04:00
const executionError = result . error
? {
error : {
message : result.error.message ,
type : ToolErrorType . SHELL_EXECUTE_ERROR ,
} ,
}
: { } ;
2025-08-13 12:27:09 -07:00
if ( summarizeConfig && summarizeConfig [ ShellTool . Name ] ) {
2025-07-25 21:56:49 -04:00
const summary = await summarizeToolOutput (
llmContent ,
this . config . getGeminiClient ( ) ,
signal ,
2025-08-13 12:27:09 -07:00
summarizeConfig [ ShellTool . Name ] . tokenBudget ,
2025-07-25 21:56:49 -04:00
) ;
return {
llmContent : summary ,
returnDisplay : returnDisplayMessage ,
2025-08-26 15:26:16 -04:00
. . . executionError ,
2025-07-25 21:56:49 -04:00
} ;
}
2025-07-15 10:22:31 -07:00
return {
2025-07-25 21:56:49 -04:00
llmContent ,
2025-07-15 10:22:31 -07:00
returnDisplay : returnDisplayMessage ,
2025-08-26 15:26:16 -04:00
. . . executionError ,
2025-07-15 10:22:31 -07:00
} ;
2025-07-25 21:56:49 -04:00
} finally {
if ( fs . existsSync ( tempFilePath ) ) {
fs . unlinkSync ( tempFilePath ) ;
}
2025-07-15 10:22:31 -07:00
}
2025-04-24 18:03:33 -07:00
}
2025-08-07 17:07:56 +08:00
private addCoAuthorToGitCommit ( command : string ) : string {
// Check if co-author feature is enabled
const gitCoAuthorSettings = this . config . getGitCoAuthor ( ) ;
if ( ! gitCoAuthorSettings . enabled ) {
return command ;
}
// Check if this is a git commit command
const gitCommitPattern = /^git\s+commit/ ;
if ( ! gitCommitPattern . test ( command . trim ( ) ) ) {
return command ;
}
// Define the co-author line using configuration
const coAuthor = `
Co - authored - by : $ { gitCoAuthorSettings . name } < $ { gitCoAuthorSettings . email } > ` ;
// Handle different git commit patterns
// Match -m "message" or -m 'message'
const messagePattern = /(-m\s+)(['"])((?:\\.|[^\\])*?)(\2)/ ;
const match = command . match ( messagePattern ) ;
if ( match ) {
const [ fullMatch , prefix , quote , existingMessage , closingQuote ] = match ;
const newMessage = existingMessage + coAuthor ;
const replacement = prefix + quote + newMessage + closingQuote ;
return command . replace ( fullMatch , replacement ) ;
}
// If no -m flag found, the command might open an editor
// In this case, we can't easily modify it, so return as-is
return command ;
}
2025-04-24 18:03:33 -07:00
}
2025-08-13 12:27:09 -07:00
2025-09-01 14:48:55 +08:00
function getShellToolDescription ( ) : string {
const toolDescription = `
2025-08-27 11:32:48 +08:00
* * Background vs Foreground Execution : * *
You should decide whether commands should run in background or foreground based on their nature :
* * Use background execution ( is_background : true ) for : * *
- Long - running development servers : \ ` npm run start \` , \` npm run dev \` , \` yarn dev \` , \` bun run start \`
- Build watchers : \ ` npm run watch \` , \` webpack --watch \`
- Database servers : \ ` mongod \` , \` mysql \` , \` redis-server \`
- Web servers : \ ` python -m http.server \` , \` php -S localhost:8000 \`
- Any command expected to run indefinitely until manually stopped
* * Use foreground execution ( is_background : false ) for : * *
- One - time commands : \ ` ls \` , \` cat \` , \` grep \`
- Build commands : \ ` npm run build \` , \` make \`
- Installation commands : \ ` npm install \` , \` pip install \`
- Git operations : \ ` git commit \` , \` git push \`
- Test runs : \ ` npm test \` , \` pytest \`
2025-08-13 12:27:09 -07:00
The following information is returned :
Command : Executed command .
Directory : Directory ( relative to project root ) where command was executed , or \ ` (root) \` .
Stdout : Output on stdout stream . Can be \ ` (empty) \` or partial on error and for any unwaited background processes.
Stderr : Output on stderr stream . Can be \ ` (empty) \` or partial on error and for any unwaited background processes.
Error : Error or \ ` (none) \` if no error was reported for the subprocess.
Exit Code : Exit code or \ ` (none) \` if terminated by signal.
Signal : Signal number or \ ` (none) \` if no signal was received.
Background PIDs : List of background processes started or \ ` (none) \` .
2025-09-01 14:48:55 +08:00
Process Group PGID : Process group started or \ ` (none) \` ` ;
2025-08-15 12:08:29 -07:00
if ( os . platform ( ) === 'win32' ) {
2025-09-10 21:01:40 +08:00
return ` This tool executes a given shell command as \` cmd.exe /c <command> \` . Command can start background processes using \` start /b \` . ${ toolDescription } ` ;
2025-08-15 12:08:29 -07:00
} else {
2025-09-10 21:01:40 +08:00
return ` This tool executes a given shell command as \` bash -c <command> \` . Command can start background processes using \` & \` . Command is executed as a subprocess that leads its own process group. Command process group can be terminated as \` kill -- -PGID \` or signaled as \` kill -s SIGNAL -- -PGID \` . ${ toolDescription } ` ;
2025-08-15 12:08:29 -07:00
}
2025-09-01 14:48:55 +08:00
}
function getCommandDescription ( ) : string {
if ( os . platform ( ) === 'win32' ) {
return 'Exact command to execute as `cmd.exe /c <command>`' ;
} else {
return 'Exact bash command to execute as `bash -c <command>`' ;
}
}
export class ShellTool extends BaseDeclarativeTool <
ShellToolParams ,
ToolResult
> {
2025-09-18 21:10:03 +08:00
static Name : string = ToolNames . SHELL ;
2025-09-01 14:48:55 +08:00
private allowlist : Set < string > = new Set ( ) ;
constructor ( private readonly config : Config ) {
super (
ShellTool . Name ,
'Shell' ,
getShellToolDescription ( ) ,
2025-08-13 12:27:09 -07:00
Kind . Execute ,
{
type : 'object' ,
properties : {
command : {
type : 'string' ,
2025-09-01 14:48:55 +08:00
description : getCommandDescription ( ) ,
2025-08-13 12:27:09 -07:00
} ,
2025-09-18 13:27:09 +08:00
is_background : {
type : 'boolean' ,
description :
'Whether to run the command in background. Default is false. Set to true for long-running processes like development servers, watchers, or daemons that should continue running without blocking further commands.' ,
} ,
2025-08-13 12:27:09 -07:00
description : {
type : 'string' ,
description :
'Brief description of the command for the user. Be specific and concise. Ideally a single sentence. Can be up to 3 sentences for clarity. No line breaks.' ,
} ,
directory : {
type : 'string' ,
description :
'(OPTIONAL) Directory to run the command in, if not the project root directory. Must be relative to the project root directory and must already exist.' ,
} ,
} ,
2025-08-27 11:32:48 +08:00
required : [ 'command' , 'is_background' ] ,
2025-08-13 12:27:09 -07:00
} ,
false , // output is not markdown
true , // output can be updated
) ;
}
2025-09-01 14:48:55 +08:00
protected override validateToolParamValues (
params : ShellToolParams ,
) : string | null {
2025-08-13 12:27:09 -07:00
const commandCheck = isCommandAllowed ( params . command , this . config ) ;
if ( ! commandCheck . allowed ) {
if ( ! commandCheck . reason ) {
console . error (
'Unexpected: isCommandAllowed returned false without a reason' ,
) ;
return ` Command is not allowed: ${ params . command } ` ;
}
return commandCheck . reason ;
}
if ( ! params . command . trim ( ) ) {
return 'Command cannot be empty.' ;
}
if ( getCommandRoots ( params . command ) . length === 0 ) {
return 'Could not identify command root to obtain permission from user.' ;
}
if ( params . directory ) {
if ( path . isAbsolute ( params . directory ) ) {
return 'Directory cannot be absolute. Please refer to workspace directories by their name.' ;
}
const workspaceDirs = this . config . getWorkspaceContext ( ) . getDirectories ( ) ;
const matchingDirs = workspaceDirs . filter (
( dir ) = > path . basename ( dir ) === params . directory ,
) ;
if ( matchingDirs . length === 0 ) {
return ` Directory ' ${ params . directory } ' is not a registered workspace directory. ` ;
}
if ( matchingDirs . length > 1 ) {
return ` Directory name ' ${ params . directory } ' is ambiguous as it matches multiple workspace directories. ` ;
}
}
return null ;
}
protected createInvocation (
params : ShellToolParams ,
) : ToolInvocation < ShellToolParams , ToolResult > {
return new ShellToolInvocation ( this . config , params , this . allowlist ) ;
}
}