#!/bin/bash # o — unified CLI for the ollie 9P namespace. # # Path resolution: # Paths are resolved based on context: # - Absolute paths (starting with session/, agents, models, etc.) → used as-is # - Relative paths with $session set but no $agent → session/$session/ # - Relative paths with both $session and $agent → session/$session/agent/$agent/ # - No context set → path used as-is (root-relative) # # Subcommands: # read Read a file # write [data] Write to a file (args or stdin) # readloop Read in a loop (re-reads after each return) # ls [path] List namespace entries # ctl Send a control command (stop, compact, kill, ...) # prompt Interactive REPL for submitting prompts (requires agent) # tail Tail chat output (requires agent) # tui Launch tmux TUI (requires agent) # env Print resolved session/agent environment # stop Interrupt running agent (requires agent) # kill Kill session (requires session) # # Environment: # session Session name (set via 'eval $(o env)') # agent Agent name set -euo pipefail # ── Path classification ───────────────────────────────────────── # Root-level entries (no context needed) ROOT_ENTRIES="session elevate agents backends models help ctl eventwait complete generate route aliases tools" # Session-level files (exist directly under session/{name}/) SESSION_FILES="env paused connected ctl name id elevate" # Agent-level files (exist under session/{name}/agent/{name}/) AGENT_FILES="prompt prompt.prev fifo.in fifo.out chat connection state statewait log offset plan cfg ctl cwd usage cost ctxsz models systemprompt context tail tools name id proc" is_root_path() { local path="$1" local first="${path%%/*}" for entry in $ROOT_ENTRIES; do [[ "$first" == "$entry" ]] && return 0 done return 1 } is_session_file() { local name="$1" for f in $SESSION_FILES; do [[ "$name" == "$f" ]] && return 0 done [[ "$name" == "agent" || "$name" == agent/* ]] && return 0 return 1 } is_agent_file() { local name="$1" local base="${name%%/*}" for f in $AGENT_FILES; do [[ "$base" == "$f" ]] && return 0 done return 1 } # ── Path resolution with validation ───────────────────────────── # Resolve a path based on current context, with meaningful errors. # Returns 0 on success (path on stdout), 1 on error (message on stderr). resolve_path() { local path="$1" # Root-level paths used as-is if is_root_path "$path"; then echo "$path" return 0 fi # Get context local s="${session:-}" local a="${agent:-}" # Determine what level this path requires local base="${path%%/*}" # Check if it's an agent-level file if is_agent_file "$base"; then if [[ -z "$s" ]]; then echo "error: '$path' requires session context" >&2 echo " export session= or use full path: session//agent//$path" >&2 return 1 fi if [[ -z "$a" ]]; then echo "error: '$path' requires agent context" >&2 echo " export agent= or use full path: session/$s/agent//$path" >&2 return 1 fi echo "session/$s/agent/$a/$path" return 0 fi # Check if it's a session-level file/dir if is_session_file "$base"; then if [[ -z "$s" ]]; then echo "error: '$path' requires session context" >&2 echo " export session= or use full path: session//$path" >&2 return 1 fi echo "session/$s/$path" return 0 fi # Unknown relative path — try with whatever context we have if [[ -n "$s" && -n "$a" ]]; then echo "session/$s/agent/$a/$path" elif [[ -n "$s" ]]; then echo "session/$s/$path" else echo "$path" fi return 0 } # ── Context requirements for specific commands ────────────────── require_session() { local s="${session:-}" if [[ -z "$s" ]]; then echo "error: no session set" >&2 echo " export session=" >&2 return 1 fi echo "$s" } require_agent() { local s="$1" local a="${agent:-}" if [[ -z "$a" ]]; then echo "error: no agent set" >&2 echo " export agent=" >&2 return 1 fi echo "$a" } agent_path() { local s a s=$(require_session) || return 1 a=$(require_agent "$s") || return 1 echo "session/$s/agent/$a" } # ── Subcommands ────────────────────────────────────────────────── cmd_read() { if [[ $# -lt 1 ]]; then echo "usage: o read " >&2 exit 1 fi local path path=$(resolve_path "$1") || exit 1 ollie-9p read "$path" } cmd_write() { if [[ $# -lt 1 ]]; then echo "usage: o write [data...]" >&2 exit 1 fi local file="$1"; shift local path path=$(resolve_path "$file") || exit 1 if [[ $# -gt 0 ]]; then printf '%s\n' "$*" | ollie-9p write "$path" else ollie-9p write "$path" fi } cmd_ls() { if [[ $# -ge 1 ]]; then local path path=$(resolve_path "$1") || exit 1 ollie-9p ls "$path" else # No arg: list based on context level local s="${session:-}" local a="${agent:-}" if [[ -n "$s" && -n "$a" ]]; then ollie-9p ls "session/$s/agent/$a" elif [[ -n "$s" ]]; then ollie-9p ls "session/$s" else ollie-9p ls "" fi fi } cmd_generate() { # One-shot generation via /generate (no session/agent needed) # Usage: o generate "prompt" # echo "prompt" | o generate # o generate -1 "prompt" (same as above) local prompt="" # Handle -1 flag (ignored, for compatibility) [[ "${1:-}" == "-1" ]] && shift if [[ $# -gt 0 ]]; then prompt="$*" else prompt=$(cat) fi [[ -z "$prompt" ]] && { echo "usage: o generate " >&2; exit 1; } printf '%s' "$prompt" | ollie-9p rdwr generate } cmd_readloop() { if [[ $# -lt 1 ]]; then echo "usage: o readloop " >&2 exit 1 fi local file="$1"; shift local path path=$(resolve_path "$file") || exit 1 while true; do ollie-9p read "$path" || break done } cmd_ctl() { if [[ $# -lt 1 ]]; then echo "usage: o ctl [args...]" >&2 echo " commands: stop, compact, kill, clear, model , backend , cwd " >&2 exit 1 fi local path path=$(agent_path) || exit 1 echo "$*" | ollie-9p write "$path/ctl" } cmd_prompt() { local s a path s=$(require_session) || exit 1 a=$(require_agent "$s") || exit 1 path="session/$s/agent/$a" echo "[$s/$a]" echo "Enter prompts (blank line sends, Ctrl+D exits)." echo "Slash commands: /stop /compact /kill /clear /model /backend /cwd /help" echo "" local buf="" line while true; do local prompt_str if [[ -z "$buf" ]]; then prompt_str=$'\001\033[1m\002>\001\033[0m\002 ' else prompt_str=" " fi if ! IFS= read -e -r -p "$prompt_str" line; then if [[ -n "$buf" ]]; then history -s "$buf" printf '%s\n' "$buf" | ollie-9p write "$path/prompt" fi printf "\n" break fi if [[ -z "$buf" && "$line" == /* ]]; then history -s "$line" local cmd="${line#/}" case "$cmd" in stop) echo "stop" | ollie-9p write "$path/ctl"; echo "[stopped]" ;; compact) echo "compact" | ollie-9p write "$path/ctl"; echo "[compacted]" ;; kill) echo "kill" | ollie-9p write "session/$s/ctl"; echo "[session killed]"; break ;; clear) echo "clear" | ollie-9p write "$path/ctl"; echo "[cleared]" ;; model*) echo "$cmd" | ollie-9p write "$path/ctl"; echo "[${cmd}]" ;; backend*) echo "$cmd" | ollie-9p write "$path/ctl"; echo "[${cmd}]" ;; cwd*) echo "$cmd" | ollie-9p write "$path/ctl"; echo "[${cmd}]" ;; quit|q) tmux kill-session -t "o-${s}" 2>/dev/null; exit 0 ;; help) echo " /stop — interrupt running agent" echo " /compact — compact context" echo " /kill — kill the session" echo " /clear — clear history" echo " /model X — switch model" echo " /backend X — switch backend" echo " /cwd X — change working directory" echo " /quit /q — kill the tmux TUI" ;; *) echo "unknown command: /$cmd (try /help)" ;; esac continue fi if [[ -z "$line" && -n "$buf" ]]; then history -s "$buf" printf '%s\n' "$buf" | ollie-9p write "$path/prompt" buf="" elif [[ -z "$line" ]]; then continue else if [[ -n "$buf" ]]; then buf="$buf"$'\n'"$line" else buf="$line" fi fi done } cmd_tail() { local path path=$(agent_path) || exit 1 exec ollie-9p read "$path/chat" } cmd_env() { # Accept optional positional args to set context if [[ $# -ge 1 ]]; then echo "export session=$1" shift elif [[ -n "${session:-}" ]]; then echo "export session=$session" fi if [[ $# -ge 1 ]]; then echo "export agent=$1" elif [[ -n "${agent:-}" ]]; then echo "export agent=$agent" fi } cmd_stop() { local path path=$(agent_path) || exit 1 echo "stop" | ollie-9p write "$path/ctl" } cmd_kill() { local s s=$(require_session) || exit 1 echo "kill" | ollie-9p write "session/$s/ctl" } cmd_tui() { local s a s=$(require_session) || exit 1 a=$(require_agent "$s") || exit 1 local tmux_session="o-${s}" if [[ "${1:-}" == "exit" ]]; then tmux kill-session -t "$tmux_session" 2>/dev/null && echo "[killed $tmux_session]" || echo "no tui session: $tmux_session" return fi tmux kill-session -t "$tmux_session" 2>/dev/null || true # Layout: # ┌──────────────────────────────────┐ # │ chat │ # │ │ # ├──────────────────────────────────┤ # │ prompt (20%) │ # └──────────────────────────────────┘ # State shown in tmux status-right. # Pane 0: chat (top, 80%) tmux new-session -d -s "$tmux_session" -c "$HOME" \ -e "session=$s" -e "agent=$a" tmux send-keys -t "$tmux_session" "clear; o tail | grep -v '\\[\\[\\[.*\\]\\]\\]'" Enter # Pane 1: prompt REPL (bottom, 20% height) tmux split-window -t "$tmux_session" -v -l 20% \ -e "session=$s" -e "agent=$a" tmux send-keys -t "$tmux_session" "o prompt" Enter # Show agent state in tmux status bar (live updates via statewait) tmux set-option -t "$tmux_session" status-right "starting... | %H:%M" tmux set-option -t "$tmux_session" status-right-length 64 # Background: loops on statewait, updates status bar instantly on each change while true; do ollie-9p read "session/$s/agent/$a/statewait" 2>/dev/null | while read -r state; do bg="" case "$state" in idle) bg="colour70" ;; # green calling*) bg="colour214" ;; # orange thinking) bg="colour33" ;; # blue paused) bg="colour245" ;; # gray *) bg="default" ;; esac tmux set-option -t "$tmux_session" status-right-style "fg=default,bg=$bg" tmux set-option -t "$tmux_session" status-right "$state | %H:%M" done done & # Focus the prompt pane tmux select-pane -t "$tmux_session:0.1" exec tmux attach -t "$tmux_session" } cmd_help() { cat <<'EOF' usage: o [args...] Path resolution: - Root paths (session/*, agents, models, etc.) → used as-is - Agent files (chat, state, prompt, etc.) → require $session + $agent - Session files (env, ctl, name, etc.) → require $session Commands: read Read a file write [data] Write to a file (args or stdin) readloop Read in a loop (re-reads after return) ls [path] List namespace entries generate One-shot generation (no session needed) ctl Send control command to agent stop Interrupt running agent kill Kill active session prompt Interactive multi-line prompt REPL tail Stream chat output tui Launch tmux layout tui exit Kill tmux TUI session env [session] [agent] Print export statements for context help Show this help Environment: session Session name agent Agent name Examples: o ls # list at current context level o ls session # list all sessions o read models # read root-level file o generate "explain monads" # one-shot, no session export session=foo agent=bar # set context o read chat # read agent's chat o readloop statewait # blocks until agent state changes EOF } # ── Dispatch ───────────────────────────────────────────────────── if [[ $# -lt 1 ]]; then cmd_help exit 0 fi subcmd="$1"; shift case "$subcmd" in read) cmd_read "$@" ;; write) cmd_write "$@" ;; readloop) cmd_readloop "$@" ;; ls) cmd_ls "$@" ;; generate) cmd_generate "$@" ;; ctl) cmd_ctl "$@" ;; stop) cmd_stop "$@" ;; kill) cmd_kill "$@" ;; prompt) cmd_prompt "$@" ;; tail) cmd_tail "$@" ;; tui) cmd_tui "$@" ;; env) cmd_env "$@" ;; help|-h|--help) cmd_help ;; *) echo "unknown command: $subcmd" >&2 echo "run 'o help' for usage" >&2 exit 1 ;; esac