aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2026-08-10 15:11:21 -0600
committerJosh Rahm <joshuarahm@gmail.com>2026-08-10 15:13:27 -0600
commite0e78ab1ce3920364ce6c46de38663bd9a978105 (patch)
treeadbec86f5f98ce3e4919f342af6cf3b792ca716c
parent2ff93a7cee97fc14b693c29899e53689654ce0d3 (diff)
downloadzshrcd-e0e78ab1ce3920364ce6c46de38663bd9a978105.tar.gz
zshrcd-e0e78ab1ce3920364ce6c46de38663bd9a978105.tar.bz2
zshrcd-e0e78ab1ce3920364ce6c46de38663bd9a978105.zip
[feat] Add "whatcmd"
This provides ai assistance when crafting a command.
-rw-r--r--97-whatcmd.sh543
1 files changed, 543 insertions, 0 deletions
diff --git a/97-whatcmd.sh b/97-whatcmd.sh
new file mode 100644
index 0000000..4d26fab
--- /dev/null
+++ b/97-whatcmd.sh
@@ -0,0 +1,543 @@
+# opencode-command.plugin.zsh
+
+_whatcmd_project_root() {
+ emulate -L zsh
+
+ local dir=$PWD
+ local marker
+
+ local -a markers=(
+ .git
+ .hg
+ Cargo.toml
+ go.mod
+ pyproject.toml
+ package.json
+ dune-project
+ CMakeLists.txt
+ meson.build
+ Makefile
+ build.zig
+ pom.xml
+ build.gradle
+ build.gradle.kts
+ WORKSPACE
+ WORKSPACE.bazel
+ MODULE.bazel
+ flake.nix
+ mix.exs
+ rebar.config
+ cabal.project
+ stack.yaml
+ Gemfile
+ composer.json
+ AGENTS.md
+ )
+
+ while true; do
+ for marker in $markers; do
+ if [[ -e "$dir/$marker" ]]; then
+ REPLY=$dir
+ return 0
+ fi
+ done
+
+ [[ $dir == / ]] && break
+ dir=${dir:h}
+ done
+
+ REPLY=''
+ return 1
+}
+
+
+#
+# Describe the local environment to the command-generating agent.
+#
+_whatcmd_environment() {
+ emulate -L zsh
+
+ local os='unknown'
+ local userland='unknown'
+ local package_manager='unknown'
+ local privilege='none'
+
+ #
+ # Distribution / OS.
+ #
+ if [[ -r /etc/os-release ]]; then
+ local NAME='' PRETTY_NAME='' ID='' VERSION_ID=''
+
+ . /etc/os-release
+
+ os="${PRETTY_NAME:-${NAME:-${ID:-unknown}}}"
+ elif [[ $OSTYPE == darwin* ]]; then
+ os="macOS $(command sw_vers -productVersion 2>/dev/null)"
+ else
+ os="$(command uname -s 2>/dev/null)"
+ fi
+
+ #
+ # Roughly identify the command-line userland.
+ #
+ if command ls --version >/dev/null 2>&1; then
+ userland="$(command ls --version 2>/dev/null | head -n1)"
+ elif (( $+commands[busybox] )); then
+ userland="$(command busybox 2>&1 | head -n1)"
+ elif [[ $OSTYPE == darwin* || $OSTYPE == *bsd* ]]; then
+ userland='BSD-style userland'
+ fi
+
+ #
+ # Prefer the native package manager.
+ #
+ local pm
+ for pm in emerge dnf apt apt-get pacman zypper apk brew; do
+ if (( $+commands[$pm] )); then
+ package_manager=$pm
+ break
+ fi
+ done
+
+ #
+ # This actually matters quite a bit for generated administrative
+ # commands.
+ #
+ if (( $+commands[doas] )); then
+ privilege=doas
+ elif (( $+commands[sudo] )); then
+ privilege=sudo
+ fi
+
+ print -r -- "OS: $os"
+ print -r -- "Kernel: $(command uname -sr 2>/dev/null)"
+ print -r -- "Architecture: $(command uname -m 2>/dev/null)"
+ print -r -- "Shell: zsh $ZSH_VERSION"
+ print -r -- "Userland: $userland"
+ print -r -- "Package manager: $package_manager"
+ print -r -- "Privilege command: $privilege"
+ print -r -- "Current Id: $(id)"
+ print -r -- "Working directory: $PWD"
+}
+
+
+#
+# Public interface.
+#
+# Examples:
+#
+# whatcmd find all files larger than 1GB
+# $(whatcmd get url of git origin)
+# whatcmd install imagemagick | wl-copy
+#
+whatcmd() {
+ emulate -L zsh
+
+ if (( $# == 0 )); then
+ print -u2 -r -- 'usage: whatcmd <request>'
+ return 2
+ fi
+
+ local request="$*"
+ local environment="$(_whatcmd_environment)"
+
+ local prompt
+ prompt=$'The command will be run in this environment:\n'
+ prompt+="$environment"
+ prompt+=$'\n\n'
+ prompt+="Request: $request"
+
+ local project_root=''
+ local permissions
+
+ if _whatcmd_project_root; then
+ project_root=$REPLY
+
+ permissions='
+ "*": "deny",
+ "read": {
+ "*": "allow",
+ "*.env": "deny",
+ "*.env.*": "deny",
+ "*.env.example": "allow"
+ },
+ "glob": "allow",
+ "grep": "allow",
+ "external_directory": "deny"
+ '
+ else
+ permissions='
+ "*": "deny"
+ '
+ fi
+
+ local opencode_config='
+{
+ "agent": {
+ "whatcmd": {
+ "description": "Generate exactly one shell command for a requested operation",
+ "mode": "primary",
+
+ "permission": {
+ '"$permissions"'
+ },
+
+ "prompt": "You are a shell-command generator, not a conversational assistant.\n\nThe user describes an operation they want to perform. Your job is to output the zsh command they should run to perform that operation.\n\nNEVER perform the requested operation yourself.\nNEVER return the result of the requested operation.\nNEVER answer the request directly.\nALWAYS return a command that the user can run to obtain the requested result or perform the requested action.\n\nFor example:\n- Request: get the URL of the git origin\n Output: git remote get-url origin\n\n- Request: find all executable files under the current directory\n Output: find . -type f -executable\n\n- Request: show processes listening on TCP ports\n Output: ss -ltnp\n\nYou may use read, glob, and grep ONLY when local project-specific information is necessary to determine which command should be run. Examples include:\n- compile this project\n- run this project tests\n- start this application\n- build the current package\n\nFor generic shell questions, do not inspect the filesystem.\n\nWhen you inspect a project, inspection is only evidence for choosing the command. Do not return information discovered during inspection as the answer.\n\nYou must never execute shell commands, modify files, access the network, invoke other agents, or perform the requested operation.\n\nYour final response must contain EXACTLY the shell command and nothing else.\nNo markdown.\nNo explanation.\nNo commentary.\nNo prefix or suffix."
+ }
+ }
+}
+'
+
+ OPENCODE_CONFIG_CONTENT="$opencode_config" \
+ command opencode run \
+ --agent whatcmd \
+ --format json \
+ --thinking \
+ "$prompt" |
+ _whatcmd_demux
+
+ local -a pipeline_status=("${pipestatus[@]}")
+
+ # Preserve an OpenCode failure rather than just returning the
+ # demuxer's status.
+ (( pipeline_status[1] != 0 )) && return $pipeline_status[1]
+ return $pipeline_status[2]
+}
+
+_whatcmd_demux() {
+ emulate -L zsh
+
+ local line type text tool target
+ local final=''
+
+ while IFS= read -r line; do
+ type=$(jq -r '.type // empty' <<< "$line") || continue
+
+ case "$type" in
+ text)
+ # Don't emit yet. The last completed text part is the
+ # command we care about.
+ final=$(jq -r '.part.text // empty' <<< "$line")
+ ;;
+
+ reasoning)
+ text=$(
+ jq -r '
+ (.part.text // "")
+ | gsub("[\r\n\t]+"; " ")
+ | gsub(" +"; " ")
+ ' <<< "$line"
+ )
+
+ [[ -n $text ]] && print -u2 -r -- "✦ $text"
+ ;;
+
+ tool_use)
+ tool=$(jq -r '.part.tool // "tool"' <<< "$line")
+
+ case "$tool" in
+ read)
+ target=$(
+ jq -r '
+ .part.state.input.filePath //
+ .part.state.input.path //
+ ""
+ ' <<< "$line"
+ )
+ print -u2 -r -- "󰈙 Reading ${target:t}"
+ ;;
+
+ glob)
+ print -u2 -r -- "󰱼 Looking through project files"
+ ;;
+
+ grep)
+ print -u2 -r -- "󰱼 Searching project"
+ ;;
+
+ *)
+ print -u2 -r -- "⚙ Using $tool"
+ ;;
+ esac
+ ;;
+
+ error)
+ text=$(
+ jq -r '
+ .error.data.message //
+ .error.message //
+ .error.name //
+ "OpenCode error"
+ ' <<< "$line"
+ )
+ print -u2 -r -- "✗ $text"
+ ;;
+ esac
+ done
+
+ [[ -n $final ]] && print -r -- "$final"
+}
+
+#
+# Minibuffer highlighting
+#
+# This replaces all region_highlight entries immediately before every
+# minibuffer redraw. That prevents zsh-syntax-highlighting and
+# fast-syntax-highlighting from leaking stale ranges into the recursive
+# editor.
+#
+_opencode_minibuffer_pre_redraw() {
+ emulate -L zsh
+
+ region_highlight=(
+ "P${_opencode_prompt_start} ${_opencode_prompt_end} fg=magenta,bold"
+ )
+}
+
+
+#
+# Customized read-from-minibuffer.
+#
+# - colored OpenCode prompt
+# - plain editable text
+# - no shell syntax highlighting
+# - preserves/restores original editing buffer
+#
+_opencode_minibuffer() {
+ emulate -L zsh
+
+ local readprompt="$1"
+
+ integer stat=1
+ integer savelim=$UNDO_LIMIT_NO
+ integer changeno=$UNDO_CHANGE_NO
+
+ {
+ () {
+ local pretext="$PREDISPLAY$LBUFFER$RBUFFER$POSTDISPLAY
+"
+
+ local +h LBUFFER=
+ local +h RBUFFER=
+ local +h PREDISPLAY="$pretext$readprompt"
+ local +h POSTDISPLAY=
+
+ #
+ # These are special ZLE parameters, so localize them for the
+ # recursive editor.
+ #
+ local +h -a region_highlight
+ local +h -a zle_highlight
+
+ #
+ # Don't apply any of ZLE's normal visual highlighting to the
+ # natural-language text.
+ #
+ zle_highlight=(
+ 'default:none'
+ 'region:none'
+ 'special:none'
+ 'suffix:none'
+ 'isearch:none'
+ 'paste:none'
+ )
+
+ #
+ # These are dynamically scoped, so
+ # _opencode_minibuffer_pre_redraw can see them.
+ #
+ integer _opencode_prompt_start=${#pretext}
+ integer _opencode_prompt_end=${#PREDISPLAY}
+
+ #
+ # Temporarily replace zle-line-pre-redraw.
+ #
+ # This is stronger than trying to configure individual syntax
+ # highlighters: regardless of what they put in region_highlight,
+ # our hook runs immediately before display and replaces it with
+ # exactly the one range we want.
+ #
+ integer had_pre_redraw=0
+
+ if zle -l zle-line-pre-redraw >/dev/null 2>&1; then
+ zle -A zle-line-pre-redraw _opencode_saved_pre_redraw
+ had_pre_redraw=1
+ fi
+
+ {
+ zle -N \
+ zle-line-pre-redraw \
+ _opencode_minibuffer_pre_redraw
+
+ #
+ # Clear any stale syntax-highlighting ranges before the
+ # recursive editor even starts.
+ #
+ region_highlight=(
+ "P${_opencode_prompt_start} ${_opencode_prompt_end} fg=magenta,bold"
+ )
+
+ local NUMERIC
+ unset NUMERIC
+
+ zle split-undo
+ UNDO_LIMIT_NO=$UNDO_CHANGE_NO
+
+ zle recursive-edit -K main
+ stat=$?
+
+ (( stat )) || REPLY=$BUFFER
+
+ } always {
+ #
+ # Restore the user's normal redraw widget, which in turn
+ # restores zsh-syntax-highlighting/F-Sy-H behavior at the
+ # normal shell prompt.
+ #
+ if (( had_pre_redraw )); then
+ zle -A \
+ _opencode_saved_pre_redraw \
+ zle-line-pre-redraw
+
+ zle -D _opencode_saved_pre_redraw
+ else
+ zle -D zle-line-pre-redraw
+ fi
+ }
+ }
+
+ } always {
+ #
+ # Remove the recursive editor's changes from the original command
+ # line's undo history.
+ #
+ zle undo $changeno
+ UNDO_LIMIT_NO=$savelim
+ }
+
+ return $stat
+}
+
+
+#
+# Ctrl-? widget
+#
+_opencode_command() {
+ emulate -L zsh
+
+ local request
+
+ #
+ # Give the natural-language minibuffer its own history.
+ #
+ () {
+ local history_file="${XDG_STATE_HOME:-$HOME/.local/state}/opencode-command/history"
+
+ mkdir -p -- "${history_file:h}" || return 1
+
+ #
+ # Push normal shell history and load our OpenCode history.
+ #
+ # -a restores the previous history automatically when this
+ # anonymous function returns.
+ #
+ fc -p -a "$history_file" 1000 1000
+
+ _opencode_minibuffer " ✦ What command " || return
+
+ request=$REPLY
+
+ [[ -z "${request//[[:space:]]/}" ]] && return 1
+
+ #
+ # Store the natural-language request in OpenCode history.
+ #
+ print -s -- "$request"
+
+ } || return
+
+ unsetopt MONITOR
+
+ #
+ # Show a temporary ZLE status line before blocking on OpenCode.
+ #
+ local tmpdir
+ tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/whatcmd.XXXXXX") || return 1
+
+ local result_file="$tmpdir/result"
+ local status_file="$tmpdir/status"
+ local stderr_fifo="$tmpdir/stderr"
+
+ mkfifo "$stderr_fifo" || {
+ rm -rf -- "$tmpdir"
+ return 1
+ }
+
+ #
+ # Open the FIFO read/write in the parent so opening either end
+ # never blocks waiting for the other side.
+ #
+ local fifo_fd
+ exec {fifo_fd}<>"$stderr_fifo"
+
+ (
+ whatcmd "$request" >| "$result_file" 2>"$stderr_fifo"
+ print -r -- "$?" >| "$status_file"
+ ) &
+
+ local -a spinner=(
+ '⠋' '⠙' '⠹' '⠸' '⠼'
+ '⠴' '⠦' '⠧' '⠇' '⠏'
+ )
+ local i=1
+ local line=''
+ local message='Asking agent ...'
+
+ while [[ ! -e "$status_file" ]]; do
+ #
+ # Grab an stderr line if one is available, but don't block
+ # longer than one animation frame.
+ #
+ if read -r -t 0.1 -u $fifo_fd line; then
+ [[ -n $line ]] && message=$line
+ fi
+
+ zle -R "${spinner[i]} $message"
+ (( i = i % ${#spinner} + 1 ))
+ done
+
+ local ec
+ read -r ec < "$status_file"
+
+ local result
+ result="$(<"$result_file")"
+
+ exec {fifo_fd}>&-
+ rm -rf -- "$tmpdir"
+
+ zle -R
+
+ if (( ec != 0 )); then
+ zle -I
+ print -u2 -r -- "✗ opencode failed: exit status $ec"
+ return $ec
+ fi
+
+ if [[ -z "${result//[[:space:]]/}" ]]; then
+ zle -I
+ print -u2 -r -- '✗ opencode returned nothing'
+ return 1
+ fi
+
+ #
+ # Replace the current command line but don't execute it.
+ #
+ BUFFER=$result
+ CURSOR=${#BUFFER}
+}
+
+
+zle -N _opencode_command
+
+# Ctrl-? as emitted by your terminal (0x1f / ^_).
+bindkey $'\x1f' _opencode_command