@@ -0,0 +1,10 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.
+
+In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, please refer to <http://unlicense.org/>
@@ -0,0 +1,223 @@
+<!--
+SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+
+SPDX-License-Identifier: Unlicense
+-->
+
+# agentsh
+
+[](https://api.reuse.software/info/git.secluded.site/agentsh)
+[](https://fishshell.com/)
+[](https://www.zsh.org/)
+[](https://scratchanitch.dev)
+[](http://unlicense.org/)
+
+_Poke [Crush 💘](https://github.com/charmbracelet/crush)—or any other CLI
+agent—from your shell. Inspired by
+[zsh-kimi-cli](https://github.com/MoonshotAI/zsh-kimi-cli)_
+
+## Features
+
+- **Prefix-based activation**: Mark commands with `✨` to send them to your agent
+- **Quick toggle**: Press `Ctrl-X` to add/remove the prefix from your current
+ command
+- **Customizable prefix**: Configure your own prefix character and formatting
+- **Configurable runner**: Defaults to `crush run --quiet`, but you can point it
+ at any CLI agent like Claude Code or Kimi CLI or Gemini.
+
+## Attribution
+
+The Zsh version started as a copy of
+[zsh-kimi-cli](https://github.com/MoonshotAI/zsh-kimi-cli) by MoonshotAI with
+`kimi -c "prompt"` swapped out for `crush run --quiet "prompt"`. The Fish plugin
+was translated and adapted from the Zsh plugin. Both have since diverged.
+
+## Requirements
+
+- **Zsh**: Version 5.4 or later
+- **Fish**: Version 3.0 or later
+- **Runner**: [`crush`](https://github.com/charmbracelet/crush) by default, or
+ any executable/function referenced by `__AGENTSH_RUNNER`
+
+## Installation
+
+### Fish
+
+#### Manual Installation
+
+```fish
+# Clone the repository
+git clone https://git.secluded.site/agentsh ~/.config/fish/agentsh
+
+# Source the plugin in your config.fish
+source ~/.config/fish/agentsh/conf.d/agentsh.fish
+```
+
+#### Fundle (recommended)
+
+Fundle works directly with git repositories (unlike Fisher), so it can pull from
+any host:
+
+```fish
+fundle plugin 'agentsh' --url 'https://git.secluded.site/agentsh'
+fundle install
+```
+
+### Zsh
+
+#### Manual (`.zshrc`)
+
+```zsh
+# Clone anywhere you prefer
+git clone https://git.secluded.site/agentsh ~/.zsh/agentsh
+
+# Load the plugin in .zshrc
+source ~/.zsh/agentsh/agentsh.zsh
+```
+
+#### Oh My Zsh
+
+```zsh
+git clone https://git.secluded.site/agentsh \
+ ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/agentsh
+
+# Add to your ~/.zshrc plugins list
+plugins=(... agentsh)
+```
+
+#### Antigen
+
+```zsh
+antigen bundle https://git.secluded.site/agentsh path:agentsh.zsh
+antigen apply
+```
+
+#### Zinit
+
+```zsh
+zinit light https://git.secluded.site/agentsh
+```
+
+#### Zplug
+
+```zsh
+zplug "https://git.secluded.site/agentsh", use:agentsh.zsh
+```
+
+## Usage
+
+Press `Ctrl-X` to add the prefix before typing your message
+
+```bash
+✨ find all TODO comments in this project
+✨ what is using port 8080?
+```
+
+Type the message, add the prefix, remove the prefix
+
+```bash
+# Type your command first
+show system resource usage
+
+# Press Ctrl-X to add the prefix
+✨ show system resource usage
+
+# Press Ctrl-X again to remove it
+show system resource usage
+```
+
+## Configuration
+
+### Custom Prefix Character
+
+Set these variables before loading the plugin to pick your own prefix.
+
+**Fish:**
+
+```fish
+set -g __AGENTSH_PREFIX_CHAR "🤖"
+set -g __AGENTSH_PREFIX "$__AGENTSH_PREFIX_CHAR "
+```
+
+**Zsh:**
+
+```zsh
+typeset -g __AGENTSH_PREFIX_CHAR
+__AGENTSH_PREFIX_CHAR="🤖"
+__AGENTSH_PREFIX="${__AGENTSH_PREFIX_CHAR} "
+```
+
+### Custom Runner Command
+
+Override the runner to talk to anything other than Crush. The variable is an
+array/list so you can include extra flags—set it before sourcing `agentsh`.
+
+**Fish:**
+
+```fish
+# Example overrides
+set -g __AGENTSH_RUNNER claude -p
+set -g __AGENTSH_RUNNER kimi -c
+```
+
+**Zsh:**
+
+```zsh
+typeset -ga __AGENTSH_RUNNER
+__AGENTSH_RUNNER=(claude -p)
+# or
+__AGENTSH_RUNNER=(kimi -c)
+```
+
+### Prefix Protection
+
+Both Zsh and Fish plugins prevent accidentally deleting the prefix with
+backspace or `Ctrl-W`.
+
+## How It Works
+
+Both plugins work by:
+
+1. **Intercepting command-not-found**: Commands starting with the prefix
+ character trigger the shell's command-not-found handler
+2. **Forwarding to your runner**: The handler strips the prefix and passes the
+ remaining text to the configured runner (defaults to `crush run --quiet`)
+3. **Preserving fallback**: Non-prefixed commands continue to use the shell's
+ normal command-not-found behavior
+
+## Differences from zsh-kimi-cli
+
+While inspired by zsh-kimi-cli, these plugins have several notable differences:
+
+- Defaults to Crush but can target any CLI agent via `__AGENTSH_RUNNER`
+- Support for both Fish and Zsh shells
+- Configurable prefix character and format
+
+## Contributions
+
+Patch requests are in [amolith/llm-projects] on [pr.pico.sh]. You don't need a
+new account to contribute, you don't need to fork this repo, you don't need to
+fiddle with `git send-email`, you don't need to faff with your email client to
+get `git request-pull` working...
+
+You just need:
+
+- Git
+- SSH
+- An SSH key
+
+```sh
+# Clone this repo, make your changes, and commit them
+# Create a new patch request with
+git format-patch origin/main --stdout | ssh pr.pico.sh pr create amolith/llm-projects
+# After potential feedback, submit a revision to an existing patch request with
+git format-patch origin/main --stdout | ssh pr.pico.sh pr add {prID}
+# List patch requests
+ssh pr.pico.sh pr ls amolith/llm-projects
+```
+
+See "How do Patch Requests work?" on [pr.pico.sh]'s home page for a more
+complete example workflow.
+
+[amolith/llm-projects]: https://pr.pico.sh/r/amolith/llm-projects
+[pr.pico.sh]: https://pr.pico.sh
@@ -0,0 +1,246 @@
+# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+#
+# SPDX-License-Identifier: Unlicense
+
+# Fallback to the configured agent command (Crush by default) when a command is not found.
+
+# Configure the prefix that marks commands for routing to the agent.
+typeset -g __AGENTSH_PREFIX_CHAR
+: "${__AGENTSH_PREFIX_CHAR:="✨"}"
+
+typeset -g __AGENTSH_PREFIX
+: "${__AGENTSH_PREFIX:="${__AGENTSH_PREFIX_CHAR} "}"
+
+typeset -ga __AGENTSH_RUNNER
+if ((${#__AGENTSH_RUNNER[@]} == 0)); then
+ __AGENTSH_RUNNER=("crush" "run" "--quiet")
+fi
+
+typeset -g __AGENTSH_PREFIX_ACTIVE=0
+typeset -g __AGENTSH_WIDGETS_INSTALLED=0
+typeset -g __AGENTSH_HAS_PREV_LINE_INIT=0
+typeset -g __AGENTSH_HAS_PREV_LINE_PRE_REDRAW=0
+typeset -g __AGENTSH_HAS_PREV_LINE_FINISH=0
+typeset -gA __AGENTSH_GUARD_WIDGET_ALIASES=()
+
+# Preserve any previously defined handler so we can delegate if needed.
+if (($ + functions[command_not_found_handler])); then
+ functions[__agentsh_original_command_not_found_handler]=$functions[command_not_found_handler]
+fi
+
+command_not_found_handler() {
+ emulate -L zsh
+
+ local missing_command="$1"
+ local -a cmd_with_args=("$@")
+
+ shift
+ local -a remaining_args=("$@")
+
+ # Nothing to do without a command name.
+ if [[ -z "$missing_command" ]]; then
+ if (($ + functions[__agentsh_original_command_not_found_handler])); then
+ __agentsh_original_command_not_found_handler "${cmd_with_args[@]}"
+ return $?
+ fi
+ return 127
+ fi
+
+ local prefix_char="${__AGENTSH_PREFIX_CHAR:-✨}"
+
+ local handled=false
+ local -a effective_cmd=()
+
+ if [[ "$missing_command" == "$prefix_char" ]]; then
+ handled=true
+ effective_cmd=("${remaining_args[@]}")
+ elif [[ "$missing_command" == ${prefix_char}* ]]; then
+ handled=true
+ local stripped="${missing_command#$prefix_char}"
+ if [[ -n "$stripped" ]]; then
+ effective_cmd=("$stripped" "${remaining_args[@]}")
+ else
+ effective_cmd=("${remaining_args[@]}")
+ fi
+ fi
+
+ if [[ "$handled" != true ]]; then
+ if (($ + functions[__agentsh_original_command_not_found_handler])); then
+ __agentsh_original_command_not_found_handler "${cmd_with_args[@]}"
+ return $?
+ fi
+ print -u2 "zsh: command not found: ${missing_command}"
+ return 127
+ fi
+
+ if ((${#effective_cmd[@]} == 0)); then
+ print -u2 "agentsh: nothing to run after '${prefix_char}'."
+ return 127
+ fi
+
+ local runner_exe="${__AGENTSH_RUNNER[1]:-}"
+ if [[ -z "$runner_exe" ]]; then
+ print -u2 "agentsh: no runner configured; set __AGENTSH_RUNNER before loading the plugin."
+ return 127
+ fi
+
+ if ! command -v "$runner_exe" >/dev/null 2>&1; then
+ if (($ + functions[__agentsh_original_command_not_found_handler])); then
+ __agentsh_original_command_not_found_handler "${cmd_with_args[@]}"
+ return $?
+ fi
+ print -u2 "agentsh: '${runner_exe}' command not found; unable to handle '${effective_cmd[1]}'."
+ return 127
+ fi
+
+ local full_cmd
+ full_cmd="${(j: :)effective_cmd}"
+
+ "${__AGENTSH_RUNNER[@]}" "$full_cmd"
+ return $?
+}
+
+__agentsh_toggle_prefix() {
+ emulate -L zsh
+
+ local prefix="${__AGENTSH_PREFIX:-${__AGENTSH_PREFIX_CHAR} }"
+ local prefix_len=${#prefix}
+
+ if [[ "$BUFFER" == "$prefix"* ]]; then
+ BUFFER="${BUFFER#$prefix}"
+ if ((CURSOR > prefix_len)); then
+ CURSOR=$((CURSOR - prefix_len))
+ else
+ CURSOR=0
+ fi
+ __AGENTSH_PREFIX_ACTIVE=0
+ else
+ BUFFER="${prefix}${BUFFER}"
+ CURSOR=$((CURSOR + prefix_len))
+ __AGENTSH_PREFIX_ACTIVE=1
+ fi
+}
+
+__agentsh_line_init() {
+ emulate -L zsh
+
+ # Add prefix at the start of a new line if prefix mode is active
+ if ((__AGENTSH_PREFIX_ACTIVE)); then
+ local prefix="${__AGENTSH_PREFIX:-${__AGENTSH_PREFIX_CHAR} }"
+ BUFFER="${prefix}"
+ CURSOR=${#prefix}
+ fi
+
+ if ((__AGENTSH_HAS_PREV_LINE_INIT)); then
+ zle __agentsh_prev_line_init
+ fi
+}
+
+__agentsh_line_pre_redraw() {
+ emulate -L zsh
+
+ if ((__AGENTSH_PREFIX_ACTIVE)); then
+ local prefix="${__AGENTSH_PREFIX:-${__AGENTSH_PREFIX_CHAR} }"
+ local prefix_len=${#prefix}
+
+ if ((CURSOR < prefix_len)); then
+ CURSOR=$prefix_len
+ fi
+
+ local buffer_len=${#BUFFER}
+ if ((CURSOR > buffer_len)); then
+ CURSOR=$buffer_len
+ fi
+ fi
+
+ if ((__AGENTSH_HAS_PREV_LINE_PRE_REDRAW)); then
+ zle __agentsh_prev_line_pre_redraw
+ fi
+}
+
+__agentsh_line_finish() {
+ emulate -L zsh
+
+ if ((__AGENTSH_HAS_PREV_LINE_FINISH)); then
+ zle __agentsh_prev_line_finish
+ fi
+}
+
+__agentsh_guard_backward_action() {
+ emulate -L zsh
+
+ if ((!__AGENTSH_PREFIX_ACTIVE)); then
+ __agentsh_call_guarded_original
+ return
+ fi
+
+ local prefix="${__AGENTSH_PREFIX:-${__AGENTSH_PREFIX_CHAR} }"
+ local prefix_len=${#prefix}
+
+ if [[ "$BUFFER" == "$prefix"* ]] && ((CURSOR <= prefix_len)); then
+ zle beep 2>/dev/null
+ return
+ fi
+
+ __agentsh_call_guarded_original
+}
+
+__agentsh_call_guarded_original() {
+ emulate -L zsh
+
+ local alias="${__AGENTSH_GUARD_WIDGET_ALIASES[$WIDGET]-}"
+ if [[ -n "$alias" ]]; then
+ zle "$alias" 2>/dev/null
+ else
+ zle ".${WIDGET}" 2>/dev/null
+ fi
+}
+
+__agentsh_register_guard_widget() {
+ emulate -L zsh
+
+ local widget="$1"
+ local alias="__agentsh_prev_${widget//-/_}"
+
+ if zle -A "$widget" "$alias" 2>/dev/null; then
+ __AGENTSH_GUARD_WIDGET_ALIASES[$widget]="$alias"
+ else
+ __AGENTSH_GUARD_WIDGET_ALIASES[$widget]=""
+ fi
+
+ zle -N "$widget" __agentsh_guard_backward_action
+}
+
+if [[ -o interactive ]]; then
+ zle -N __agentsh_toggle_prefix
+
+ local -a __agentsh_keymaps=("emacs" "viins")
+ local keymap
+ for keymap in "${__agentsh_keymaps[@]}"; do
+ bindkey -M "$keymap" '^X' __agentsh_toggle_prefix 2>/dev/null
+ done
+ unset keymap __agentsh_keymaps
+
+ if ((!__AGENTSH_WIDGETS_INSTALLED)); then
+ if zle -A zle-line-init __agentsh_prev_line_init 2>/dev/null; then
+ __AGENTSH_HAS_PREV_LINE_INIT=1
+ fi
+ zle -N zle-line-init __agentsh_line_init
+
+ if zle -A zle-line-pre-redraw __agentsh_prev_line_pre_redraw 2>/dev/null; then
+ __AGENTSH_HAS_PREV_LINE_PRE_REDRAW=1
+ fi
+ zle -N zle-line-pre-redraw __agentsh_line_pre_redraw
+
+ if zle -A zle-line-finish __agentsh_prev_line_finish 2>/dev/null; then
+ __AGENTSH_HAS_PREV_LINE_FINISH=1
+ fi
+ zle -N zle-line-finish __agentsh_line_finish
+ __agentsh_register_guard_widget backward-delete-char
+ __agentsh_register_guard_widget backward-kill-word
+ __agentsh_register_guard_widget vi-backward-delete-char
+ __agentsh_register_guard_widget vi-backward-kill-word
+
+ __AGENTSH_WIDGETS_INSTALLED=1
+ fi
+fi
@@ -0,0 +1,189 @@
+# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+#
+# SPDX-License-Identifier: Unlicense
+
+# Fallback to the configured agent command (Crush by default) when a command is not found.
+
+# Configure the prefix that marks commands for routing to the agent.
+if not set -q __AGENTSH_PREFIX_CHAR
+ set -g __AGENTSH_PREFIX_CHAR "✨"
+end
+
+if not set -q __AGENTSH_PREFIX
+ set -g __AGENTSH_PREFIX "$__AGENTSH_PREFIX_CHAR "
+end
+
+if not set -q __AGENTSH_RUNNER
+ set -g __AGENTSH_RUNNER crush run --quiet
+end
+
+set -g __AGENTSH_PREFIX_ACTIVE 0
+
+# Preserve any previously defined handler so we can delegate if needed
+if functions -q fish_command_not_found
+ functions -c fish_command_not_found __agentsh_prev_fish_command_not_found
+end
+
+# Command not found handler
+function fish_command_not_found
+ set -l missing_command $argv[1]
+ set -l remaining_args $argv[2..-1]
+
+ # Nothing to do without a command name
+ if test -z "$missing_command"
+ echo "fish: command not found" >&2
+ return 127
+ end
+
+ set -l prefix_char $__AGENTSH_PREFIX_CHAR
+ set -l handled false
+ set -l effective_cmd
+
+ # Check if command starts with prefix
+ if test "$missing_command" = "$prefix_char"
+ set handled true
+ set effective_cmd $remaining_args
+ else if string match -q "$prefix_char*" -- "$missing_command"
+ set handled true
+ set -l stripped (string replace -- "$prefix_char" "" -- "$missing_command")
+ if test -n "$stripped"
+ set effective_cmd $stripped $remaining_args
+ else
+ set effective_cmd $remaining_args
+ end
+ end
+
+ if test "$handled" != true
+ if functions -q __agentsh_prev_fish_command_not_found
+ __agentsh_prev_fish_command_not_found $argv
+ return $status
+ end
+ echo "fish: command not found: $missing_command" >&2
+ return 127
+ end
+
+ if test (count $effective_cmd) -eq 0
+ echo "agentsh: nothing to run after '$prefix_char'." >&2
+ return 127
+ end
+
+ if not set -q __AGENTSH_RUNNER[1]
+ echo "agentsh: no runner configured; set __AGENTSH_RUNNER before sourcing agentsh." >&2
+ return 127
+ end
+
+ set -l runner $__AGENTSH_RUNNER
+ set -l runner_exe $runner[1]
+
+ if not type -q -- $runner_exe
+ if functions -q __agentsh_prev_fish_command_not_found
+ __agentsh_prev_fish_command_not_found $argv
+ return $status
+ end
+ echo "agentsh: $runner_exe: command not found; unable to handle '$effective_cmd[1]'." >&2
+ return 127
+ end
+
+ # Build command string as single argument
+ set -l full_cmd (string join ' ' -- $effective_cmd)
+
+ $runner $full_cmd
+ return $status
+end
+
+# Toggle prefix function
+function __agentsh_toggle_prefix
+ set -l prefix $__AGENTSH_PREFIX
+ set -l prefix_len (string length -- $prefix)
+ set -l buffer (commandline)
+ set -l cursor_pos (commandline -C)
+
+ if string match -q "$prefix*" -- "$buffer"
+ # Remove prefix
+ set -l new_buffer (string sub --start (math $prefix_len + 1) -- $buffer)
+ commandline -r -- $new_buffer
+
+ if test $cursor_pos -gt $prefix_len
+ commandline -C (math $cursor_pos - $prefix_len)
+ else
+ commandline -C 0
+ end
+ set -g __AGENTSH_PREFIX_ACTIVE 0
+ else
+ # Add prefix
+ commandline -r -- "$prefix$buffer"
+ commandline -C (math $cursor_pos + $prefix_len)
+ set -g __AGENTSH_PREFIX_ACTIVE 1
+ end
+end
+
+# Hook to persist prefix across prompts
+function __agentsh_postexec --on-event fish_postexec
+ if test $__AGENTSH_PREFIX_ACTIVE -eq 1
+ commandline -f repaint
+ end
+end
+
+# Guard backward deletion to prevent removing prefix
+function __agentsh_guard_backward_delete
+ if test $__AGENTSH_PREFIX_ACTIVE -eq 0
+ commandline -f backward-delete-char
+ return
+ end
+
+ set -l prefix $__AGENTSH_PREFIX
+ set -l prefix_len (string length -- $prefix)
+ set -l buffer (commandline)
+ set -l cursor_pos (commandline -C)
+
+ # Don't delete if we're at or before the prefix boundary
+ if string match -q "$prefix*" -- "$buffer"; and test $cursor_pos -le $prefix_len
+ return
+ end
+
+ commandline -f backward-delete-char
+end
+
+function __agentsh_guard_backward_kill_word
+ if test $__AGENTSH_PREFIX_ACTIVE -eq 0
+ commandline -f backward-kill-word
+ return
+ end
+
+ set -l prefix $__AGENTSH_PREFIX
+ set -l prefix_len (string length -- $prefix)
+ set -l buffer (commandline)
+ set -l cursor_pos (commandline -C)
+
+ if string match -q "$prefix*" -- "$buffer"; and test $cursor_pos -le $prefix_len
+ return
+ end
+
+ commandline -f backward-kill-word
+end
+
+# Set up key bindings
+if status is-interactive
+ # Bind Ctrl+X to toggle prefix
+ bind \cx __agentsh_toggle_prefix
+
+ # Bind multiple backspace variants
+ bind \b __agentsh_guard_backward_delete
+ bind \x7f __agentsh_guard_backward_delete
+ bind -k backspace __agentsh_guard_backward_delete
+
+ # Bind Alt+Backspace variants
+ bind \eb __agentsh_guard_backward_kill_word
+ bind \e\x7f __agentsh_guard_backward_kill_word
+
+ # Bind Ctrl+W
+ bind \cw __agentsh_guard_backward_kill_word
+
+ # Bind in insert mode too
+ bind -M insert \b __agentsh_guard_backward_delete
+ bind -M insert \x7f __agentsh_guard_backward_delete
+ bind -M insert -k backspace __agentsh_guard_backward_delete
+ bind -M insert \eb __agentsh_guard_backward_kill_word
+ bind -M insert \e\x7f __agentsh_guard_backward_kill_word
+ bind -M insert \cw __agentsh_guard_backward_kill_word
+end