/ tools / BashTool / commandSemantics.ts
commandSemantics.ts
  1  /**
  2   * Command semantics configuration for interpreting exit codes in different contexts.
  3   *
  4   * Many commands use exit codes to convey information other than just success/failure.
  5   * For example, grep returns 1 when no matches are found, which is not an error condition.
  6   */
  7  
  8  import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js'
  9  
 10  export type CommandSemantic = (
 11    exitCode: number,
 12    stdout: string,
 13    stderr: string,
 14  ) => {
 15    isError: boolean
 16    message?: string
 17  }
 18  
 19  /**
 20   * Default semantic: treat only 0 as success, everything else as error
 21   */
 22  const DEFAULT_SEMANTIC: CommandSemantic = (exitCode, _stdout, _stderr) => ({
 23    isError: exitCode !== 0,
 24    message:
 25      exitCode !== 0 ? `Command failed with exit code ${exitCode}` : undefined,
 26  })
 27  
 28  /**
 29   * Command-specific semantics
 30   */
 31  const COMMAND_SEMANTICS: Map<string, CommandSemantic> = new Map([
 32    // grep: 0=matches found, 1=no matches, 2+=error
 33    [
 34      'grep',
 35      (exitCode, _stdout, _stderr) => ({
 36        isError: exitCode >= 2,
 37        message: exitCode === 1 ? 'No matches found' : undefined,
 38      }),
 39    ],
 40  
 41    // ripgrep has same semantics as grep
 42    [
 43      'rg',
 44      (exitCode, _stdout, _stderr) => ({
 45        isError: exitCode >= 2,
 46        message: exitCode === 1 ? 'No matches found' : undefined,
 47      }),
 48    ],
 49  
 50    // find: 0=success, 1=partial success (some dirs inaccessible), 2+=error
 51    [
 52      'find',
 53      (exitCode, _stdout, _stderr) => ({
 54        isError: exitCode >= 2,
 55        message:
 56          exitCode === 1 ? 'Some directories were inaccessible' : undefined,
 57      }),
 58    ],
 59  
 60    // diff: 0=no differences, 1=differences found, 2+=error
 61    [
 62      'diff',
 63      (exitCode, _stdout, _stderr) => ({
 64        isError: exitCode >= 2,
 65        message: exitCode === 1 ? 'Files differ' : undefined,
 66      }),
 67    ],
 68  
 69    // test/[: 0=condition true, 1=condition false, 2+=error
 70    [
 71      'test',
 72      (exitCode, _stdout, _stderr) => ({
 73        isError: exitCode >= 2,
 74        message: exitCode === 1 ? 'Condition is false' : undefined,
 75      }),
 76    ],
 77  
 78    // [ is an alias for test
 79    [
 80      '[',
 81      (exitCode, _stdout, _stderr) => ({
 82        isError: exitCode >= 2,
 83        message: exitCode === 1 ? 'Condition is false' : undefined,
 84      }),
 85    ],
 86  
 87    // wc, head, tail, cat, etc.: these typically only fail on real errors
 88    // so we use default semantics
 89  ])
 90  
 91  /**
 92   * Get the semantic interpretation for a command
 93   */
 94  function getCommandSemantic(command: string): CommandSemantic {
 95    // Extract the base command (first word, handling pipes)
 96    const baseCommand = heuristicallyExtractBaseCommand(command)
 97    const semantic = COMMAND_SEMANTICS.get(baseCommand)
 98    return semantic !== undefined ? semantic : DEFAULT_SEMANTIC
 99  }
100  
101  /**
102   * Extract just the command name (first word) from a single command string.
103   */
104  function extractBaseCommand(command: string): string {
105    return command.trim().split(/\s+/)[0] || ''
106  }
107  
108  /**
109   * Extract the primary command from a complex command line;
110   * May get it super wrong - don't depend on this for security
111   */
112  function heuristicallyExtractBaseCommand(command: string): string {
113    const segments = splitCommand_DEPRECATED(command)
114  
115    // Take the last command as that's what determines the exit code
116    const lastCommand = segments[segments.length - 1] || command
117  
118    return extractBaseCommand(lastCommand)
119  }
120  
121  /**
122   * Interpret command result based on semantic rules
123   */
124  export function interpretCommandResult(
125    command: string,
126    exitCode: number,
127    stdout: string,
128    stderr: string,
129  ): {
130    isError: boolean
131    message?: string
132  } {
133    const semantic = getCommandSemantic(command)
134    const result = semantic(exitCode, stdout, stderr)
135  
136    return {
137      isError: result.isError,
138      message: result.message,
139    }
140  }