// Shell-command display normalization, shared by every provider's shell tool // (Claude `Bash`, Codex/OpenClaw `exec`/`bash`, Hermes `terminal:` titles). // // Agents rarely hand us the command a person would have typed. Codex and // OpenClaw hand the model's script to the login shell, so the tool input is // the wrapper invocation: // // /bin/bash -c "sed -n '1,240p' SKILL.md && sed -n '1,240p' .moi/FIRMA.md" // // The wrapper is noise in a one-line brief: the shell path and `-lc` eat the // width, and the inner quoting arrives escaped, so the row shows `\"` where // the model wrote `"`. Rendering strips the wrapper and removes one quoting // level, leaving what actually ran: // // sed -n '1,240p' SKILL.md && sed -n '1,240p' .moi/FIRMA.md // // Word splitting is `shell-quote`'s, not ours — quoting rules have too many // corners (`'` inside `"`, `\d` staying literal, `$(…)` and backticks passing // through) to re-derive here. // // Display only — nothing here feeds execution, so an unrecognized shape is // never an error: every step falls back to the raw command unchanged. import { parse } from 'shell-quote' import type { Shorten } from './shared' // `shell-quote` resolves `$VAR` through this callback, defaulting to the empty // string. We render commands rather than run them, so put the variable back // verbatim. Braces only survive where they carry meaning (`${FOO:-x}`); a bare // `${FOO}` reads the same without them. A command or arithmetic substitution // (`$(pwd)`, `$((1+2))`) arrives here as an empty key — a bare `$` restores it. function keepVariable(key: string): string { if (!key) return '$' return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key) ? `$${key}` : `\${${key}}` } // `shell-quote` throws on a few malformed inputs (`${}`), and a streaming tool // call can always show us a half-written command. Unparseable means "render it // as it came". function splitWords(command: string): ReturnType | null { try { return parse(command, keepVariable) } catch { return null } } function basename(path: string): string { return path.split('/').pop() ?? path } // Shells whose `-c` argument is a command string we can lift out. Keep this to // real shells: unwrapping `docker run … sh -c …` would hide where the command // ran, which is the useful part of that row. const SHELLS = new Set([ 'bash', 'sh', 'zsh', 'fish', 'dash', 'ash', 'ksh', 'mksh', 'csh', 'tcsh', 'nu', 'pwsh', 'powershell' ]) // Flags that consume the next word, so it isn't mistaken for the script // (`bash -o pipefail -c …`). const FLAGS_WITH_VALUE = new Set(['-o', '+o', '-O', '+O', '--rcfile', '--init-file']) const ENV_ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/ // `-c`, plus the combined short forms shells are usually invoked with (`-lc`, // `-ic`, `-euc`) and the long spellings (`--command`, pwsh's `-Command`). function isCommandFlag(word: string): boolean { if (/^-[a-zA-Z]*c$/.test(word)) return true const lower = word.toLowerCase() return lower === '--command' || lower === '-command' } // One unwrap pass: `[VAR=v …] [env] [flags] -c