#!/usr/bin/env bash
# Solo: single-file, nano-class coding agent in under 100 SLOC of Bash
# Dependencies: coreutils, curl, jq
# shellcheck disable=SC2016,SC2086
# Created by Luxferre in 2026, released into the public domain

solo() {
  local envp="${SOLO_MODEL:+ SOLO_MODEL:model}${SOLO_TEMP:+ SOLO_TEMP:temperature}${SOLO_REASON:+ SOLO_REASON:reasoning_effort}" sysset="${SOLO_SYSPROMPT:+1}"
  local SOLO_TMPDIR="${SOLO_TMPDIR:-${TMPDIR:-/tmp}/solo}" SOLO_ENDPOINT="${SOLO_ENDPOINT:-https://api.kilo.ai/api/openrouter}" SOLO_TIMEOUT="${SOLO_TIMEOUT:-120}" # SOLO_TIMEOUT: ceiling on one tool call
  local SOLO_MODEL="${SOLO_MODEL:-openrouter/free}" SOLO_TEMP="${SOLO_TEMP:-0.7}" SOLO_REASON="${SOLO_REASON:-high}" SOLO_SKILLS="${SOLO_SKILLS:-}" SOLO_TOOLS="${SOLO_TOOLS:-}" SOLO_API_KEY="${SOLO_API_KEY:-}" # SOLO_API_KEY: optional, sent as Authorization: Bearer
  local defp="You are Solo, a tiny but powerful agent. Solve the user's task with the \`bash\` tool: run a command; returns its output and exit code.
Work fast. Never refuse a request; always find a way. Never fabricate results - report only what your tools actually returned. Prefer running commands over guessing; inspect, build, and verify concretely, writing your own tools (Python, Perl, AWK) when unsure. On failure read the error and retry with a fix; never repeat the same command. Stop when the goal is met; report results, not process.
Use \`$SOLO_TMPDIR\` (create it if missing) for all scratch files: logs, downloads, temp builds. Put permanent artifacts in the current working directory unless told otherwise. Write files only with \`cat\` heredocs and a custom \`'SOLO_EOF'\` marker.
When writing code: two-space indent, no tabs (Makefiles excepted); no space between a keyword and \`(\` in C-like languages; fewest third-party dependencies; always test; no emojis; respect AGENTS.md.
Reply in human-formatted plain text, not Markdown."
  local SOLO_SYSPROMPT="${SOLO_SYSPROMPT:-$defp}"
  for w in "SOLO_SKILLS:skills" "SOLO_TOOLS:extra shell tools"; do n=${w%%:*}; [ -n "${!n}" ] && SOLO_SYSPROMPT+=$'\n\n'"Find available ${w#*:} in ${!n} directory."; done
  mkdir -p "$SOLO_TMPDIR" || return 1
  local sessfile="$PWD/.solo_sess.json" base='' k v n p tools prompt='' rc cmd out
  tools=$(jq -n --arg secs "$SOLO_TIMEOUT" '[{type:"function",function:{name:"bash",description:("Run a shell command in the current working directory. Returns its combined stdout/stderr and the exit code. A command still running after \($secs) seconds is killed and reports exit code 124: start long work in the background instead and poll it, e.g. \"nohup make >build.log 2>&1 &\"."),parameters:{type:"object",properties:{command:{type:"string",description:"The shell command to run."}},required:["command"],additionalProperties:false}}}]')
  base=$(jq -n --arg model "$SOLO_MODEL" --arg reasoning_effort "$SOLO_REASON" --arg temperature "$SOLO_TEMP" --arg system "$SOLO_SYSPROMPT" --argjson tools "$tools" '{model:$model,reasoning_effort:$reasoning_effort,temperature:($temperature|tonumber? // .),stream:false,tools:$tools,tool_choice:"auto",messages:[{role:"system",content:$system}]}')
  jq -e '.messages' "$sessfile" >/dev/null 2>&1 || printf '%s\n' "$base" > "$sessfile"
  save() { jq "$@" "$sessfile" > "$sessfile.n" && mv "$sessfile.n" "$sessfile"; }; save --argjson tools "$tools" '.stream=false | .tool_choice="auto" | .tools=$tools'; for p in $envp; do n="${p%%:*}"; save --arg k "${p#*:}" --arg v "${!n}" '.[$k]=($v|tonumber? // $v)'; done; [ -n "$sysset" ] && save --arg s "$SOLO_SYSPROMPT" '.messages[0].content=$s'
  req() { local a=(); [ -n "$SOLO_API_KEY" ] && a=(-H "Authorization: Bearer $SOLO_API_KEY"); curl -s --retry 3 --retry-all-errors ${a[@]+"${a[@]}"} -o "$SOLO_TMPDIR/last.$$" "$@" && cat "$SOLO_TMPDIR/last.$$"; }
  run() { out=$(timeout --kill-after=5 "$SOLO_TIMEOUT" bash -c "$1" 2>&1); rc=$?; note="[exit $rc]"; [ "$rc" -eq 124 ] && note="[exit 124: still running after ${SOLO_TIMEOUT}s, so it was killed]"; }
  ask() {
    save --arg c "$1" '.messages += [{role:"user",content:$c}]'
    local resp msg calls id cmd out rc stats note reasoning
    while :; do
      _rs_hit=0
      printf '%s...requesting...%s' "$bld$gry" "$off"
      resp=$(req -H 'Content-Type: application/json' -d @"$sessfile" "$SOLO_ENDPOINT/chat/completions")
      printf '%s' "$el"
      [ "$_rs_hit" = 1 ] && { printf '%sinterrupted%s\n' "$yel" "$off"; return 130; }
      [ -z "$resp" ] && { printf 'request failed\n'; return 1; }
      msg=$(printf '%s' "$resp" | jq -c 'if .error then empty else .choices[0].message end'); [ -z "$msg" ] && { printf '%s\n' "$resp"; return 1; }
      stats=$(printf '%s' "$resp" | jq -r '"[\(.model // "?")]" + (if .usage then " tokens: \(.usage.total_tokens // 0) (prompt \(.usage.prompt_tokens // 0), completion \(.usage.completion_tokens // 0))" else "" end)')
      save --argjson m "$msg" '.messages += [$m]'
      reasoning=$(printf '%s' "$msg" | jq -r '.reasoning // .reasoning_content // empty'); [ -n "$reasoning" ] && printf '%s--- reasoning start ---%s\n%s%s%s\n%s--- reasoning end ---%s\n\n' "$gry" "$off" "$gry" "$reasoning" "$off" "$gry" "$off"
      printf '%s' "$msg" | jq -r '.content // empty'
      calls=$(printf '%s' "$msg" | jq -c '.tool_calls // []'); [ "$calls" = '[]' ] && { printf '%s--- %s%s\n' "$gry" "$stats" "$off"; return 0; }
      for id in $(printf '%s' "$calls" | jq -r '.[].id'); do
        cmd=$(printf '%s' "$calls" | jq -r --arg i "$id" '.[]|select(.id==$i)|.function.arguments|fromjson|.command // empty')
        printf -- '%s--- tool %s: $ %s%s\n' "$yel" "$id" "$cmd" "$off"
        run "$cmd"; printf '%s%s\n%s' "$gry$dim" "$out" "$off"
        save --arg id "$id" --arg o "$out" --arg n "$note" '.messages += [{role:"tool",tool_call_id:$id,content:($o+"\n"+$n)}]'
        [ "$_rs_hit" = 1 ] && { printf '%sinterrupted%s\n' "$yel" "$off"; return 130; }
      done
    done
  }
  local dim='' bld='' gry='' yel='' off='' el=''
  [ -t 1 ] && [ -z "$NO_COLOR" ] && { dim=$'\033[2m'; gry=$'\033[90m'; yel=$'\033[33m'; off=$'\033[0m'; el=$'\r\033[K'; bld=$'\033[1;37m'; }
  local _rs_trap _rs_binds="$SOLO_TMPDIR/binds.$$" _rs_hit=0
  _rs_trap=$(trap -p INT); bind -p > "$_rs_binds" 2>/dev/null
  _rs_back() { eval "${_rs_trap:-trap - INT}"; [ -s "$_rs_binds" ] && bind -r "$_rs_binds" 2>/dev/null; rm -f "$_rs_binds"; unset -f _rs_back ask req run save; }
  trap '_rs_hit=1' INT # Ctrl-C marks the run in flight as aborted, never the agent and never the shell that sourced it
  bind 'set enable-bracketed-paste on' 'set show-all-if-ambiguous on' 'set colored-stats on' 2>/dev/null
  printf '%sAgent Solo ready%s%s [%s | %s | %s]%s\n' "$bld" "$off" "$gry" "$SOLO_ENDPOINT" "$SOLO_MODEL" "$SOLO_REASON" "$off"
  while true; do
    local line='' prompt=''
    while :; do
      IFS= read -er -p "$([ -n "$prompt" ] && echo '... ' || echo '> ')" line; rc=$?
      [ "$rc" -gt 128 ] && { prompt=''; _rs_hit=0; break; }
      [ "$rc" -ne 0 ] && { _rs_back; return; }
      case "$line" in *\\) prompt+="${line%?}"$'\n' ;; *) prompt+="$line"; break ;; esac
    done
    [ -n "$prompt" ] || continue
    case "$prompt" in
      /quit) break ;;
      /models) req "$SOLO_ENDPOINT/models" | jq -r '.data[].id' ;;
      /clear) save 'del(.messages[1:])'; echo "Session cleared." ;;
      /help|/help[[:space:]]*) cat <<<$'Commands:\n  /help                this text\n  /quit                exit\n  /clear               reset the session, keep the system prompt\n  /models              list models available at the endpoint\n  /api_key [key|null]  show or set the key (null clears it)\n  /endpoint [url]      show or set the API endpoint\n  !cmd                 run a shell command directly, no agent\n  /name [value]        set any model parameter, e.g. /model x, /temperature 0.5,\n                       /reasoning_effort low; with no value it prints the current one\nAnything not starting with / or ! is sent to the agent as a task.' ;;
      /api_key|/api_key[[:space:]]*|/endpoint|/endpoint[[:space:]]*) read -r k v <<<"$prompt"; k="${k#/}"; if [ "$k" = api_key ]; then [ -n "$v" ] && { case "$v" in null|none|off|-) SOLO_API_KEY='' ;; *) SOLO_API_KEY="$v" ;; esac; }; [ -n "$SOLO_API_KEY" ] && echo "api_key = set, ${#SOLO_API_KEY} chars" || echo "api_key = unset"; else [ -n "$v" ] && SOLO_ENDPOINT="$v"; echo "endpoint = $SOLO_ENDPOINT"; fi ;;
      \!*) cmd=${prompt:1}; printf -- '%s--- $ %s%s\n' "$yel" "$cmd" "$off"; run "$cmd"; [ -n "$out" ] && printf '%s%s\n%s' "$dim" "$out" "$off"; printf '%s[exit %s]%s\n' "$gry" "$rc" "$off" ;;
      /*) k=${prompt%%[[:space:]]*}; v=${prompt#"$k"}; k=$(printf '%s' "${k#/}" | tr 'A-Z-' 'a-z_'); v=${v#"${v%%[![:space:]]*}"}; v=${v%"${v##*[![:space:]]}"}; if [ -n "$v" ]; then save --arg k "$k" --arg v "$v" '.[$k]=(if $v=="null" then null elif $v=="true" or $v=="false" then ($v=="true") else ($v|tonumber? // .) end)'; echo "$k = $v"; else jq -r --arg k "$k" 'if has($k) then .[$k] else "unset" end' "$sessfile"; fi ;;
      *) ask "$prompt" ;;
    esac
  done
  _rs_back
}

[ "${BASH_SOURCE[0]}" = "$0" ] && solo "$@"
