#!/bin/bash # o — unified CLI for the ollie 9P namespace. # # Context is determined by the first argument: # o Root level # o Session level # o / Agent level # # The slash in session/agent disambiguates context level. # # Subcommands: # read Read a file (-l to loop) # write [data] Write to a file (args or stdin) # rdwr [data] Write then read (request-response files) # ls [path] List namespace entries # new [cwd] Create session (if needed) and agent (agent-level) # ctl Send a control command # prompt Interactive REPL for submitting prompts (agent-level) # tui Launch tmux TUI (agent-level) # generate One-shot generation (root-level only) # env Print export statements for context # # Examples: # o ls List root (sessions, models, etc.) # o myproj ls List session contents # o myproj/coding new Create session+agent (cwd=$PWD) # o myproj/coding prompt Interactive prompt # o myproj/coding read chat Read chat output set -euo pipefail # ── Terminal capability ────────────────────────────────────────── USE_COLOR=true if [[ "${TERM:-dumb}" == "dumb" ]] || [[ ! -t 1 ]]; then USE_COLOR=false fi # ── Globals set by dispatch ───────────────────────────────────── PREFIX="" # Path prefix based on context CTX_SESSION="" # Session name if in session/agent context CTX_AGENT="" # Agent name if in agent context # ── Known commands (to distinguish from context) ───────────────── COMMANDS="read write rdwr ls new ctl prompt log tui env generate overview help" is_command() { local arg="$1" for cmd in $COMMANDS; do [[ "$arg" == "$cmd" ]] && return 0 done [[ "$arg" == "-h" || "$arg" == "--help" ]] && return 0 return 1 } # ── Path resolution helper ────────────────────────────────────── # Resolves a path: if it starts with /, treat as root-level (strip /) # Otherwise, prepend PREFIX. resolve_path() { local p="$1" if [[ "$p" == /* ]]; then # Absolute path - strip leading / and use as root-relative echo "${p#/}" else # Relative path - prepend PREFIX echo "${PREFIX}${PREFIX:+/}$p" fi } # ── Subcommands ───────────────────────────────────────────────── cmd_read() { local loop=false while [[ "${1:-}" == -* ]]; do case "$1" in -l|--loop) loop=true; shift ;; *) break ;; esac done if [[ $# -lt 1 ]]; then echo "usage: o read [-l] " >&2 exit 1 fi local path path=$(resolve_path "$1") if $loop; then while true; do ollie-9p read "$path" || break done else ollie-9p read "$path" fi } 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") if [[ $# -gt 0 ]]; then printf '%s\n' "$*" | ollie-9p write "$path" else ollie-9p write "$path" fi } cmd_rdwr() { if [[ $# -lt 1 ]]; then echo "usage: o rdwr [data...]" >&2 exit 1 fi local file="$1"; shift local path path=$(resolve_path "$file") if [[ $# -gt 0 ]]; then printf '%s\n' "$*" | ollie-9p rdwr "$path" else ollie-9p rdwr "$path" fi } cmd_ls() { local path="${PREFIX}" if [[ $# -ge 1 ]]; then path=$(resolve_path "$1") fi ollie-9p ls "$path" } cmd_generate() { # One-shot generation via /generate (root-level only) if [[ -n "$PREFIX" ]]; then echo "error: generate is a root-level command" >&2 echo " usage: o generate " >&2 exit 1 fi local prompt="" 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_ctl() { if [[ $# -lt 1 ]]; then if [[ -z "$PREFIX" ]]; then echo "usage: o ctl (or o / ctl )" >&2 exit 1 fi # The ctl file is self-describing: an empty request returns the # valid commands with descriptions. echo "usage: o ctl [args...]" >&2 printf '' | ollie-9p rdwr "${PREFIX}/ctl" >&2 exit 1 fi if [[ -z "$PREFIX" ]]; then echo "error: ctl requires session or agent context" >&2 echo " usage: o ctl " >&2 echo " or: o / ctl " >&2 exit 1 fi if [[ -n "$CTX_AGENT" ]]; then # Agent-level: rdwr to get response printf '%s' "$*" | ollie-9p rdwr "${PREFIX}/ctl" else # Session-level: write only echo "$*" | ollie-9p write "${PREFIX}/ctl" fi } cmd_prompt() { if [[ -z "$CTX_AGENT" ]]; then echo "error: prompt requires agent context" >&2 echo " usage: o / prompt" >&2 exit 1 fi # If stdin is not a terminal, read it all and send as a single prompt if [[ ! -t 0 ]]; then cat | ollie-9p write "${PREFIX}/prompt" return fi # Interactive REPL mode echo "[$CTX_SESSION/$CTX_AGENT]" echo "Type . on a blank line to send, Ctrl+D to exit." echo "/ prefix runs ctl: /stop, /compact, /model X, /i X (type /help for all)" echo "!q exits the TUI." echo "" local buf="" line while true; do local prompt_str if [[ -z "$buf" ]]; then if $USE_COLOR; then prompt_str=$'\033[1m>\033[0m ' else prompt_str='> ' fi else prompt_str=" " fi printf '%s' "$prompt_str" if ! IFS= read -r line; then if [[ -n "$buf" ]]; then history -s "$buf" printf '%s\n' "$buf" | ollie-9p write "${PREFIX}/prompt" fi printf "\n" break fi # Drain any additional lines that arrived quickly (multi-line paste) while IFS= read -r -t 0.1 extra_line; do line="$line"$'\n'"$extra_line" done if [[ -z "$buf" && "$line" == /* ]]; then history -s "$line" cmd_ctl "${line#/}" || true continue fi if [[ -z "$buf" && "$line" == !* ]]; then history -s "$line" local bang="${line#!}" case "$bang" in q|quit) tmux kill-session -t "o-${CTX_SESSION}" 2>/dev/null; exit 0 ;; esac continue fi if [[ "$line" == "." ]]; then if [[ -n "$buf" ]]; then history -s "$buf" printf '%s\n' "$buf" | ollie-9p write "${PREFIX}/prompt" buf="" fi continue fi if [[ -z "$line" && -n "$buf" ]]; then buf="$buf"$'\n' elif [[ -z "$line" ]]; then continue else if [[ -n "$buf" ]]; then buf="$buf"$'\n'"$line" else buf="$line" fi fi done } cmd_log() { local lines=1000 while [[ $# -gt 0 && "$1" == -* ]]; do case "$1" in -n) shift; lines="${1:-1000}"; shift ;; -n*) lines="${1#-n}"; shift ;; *) break ;; esac done if [[ -z "$CTX_AGENT" ]]; then echo "error: log requires agent context" >&2 echo " usage: o / log [-n N]" >&2 exit 1 fi ollie-9p read "${PREFIX}/log" | tail -n "$lines" } cmd_new() { if [[ -z "$CTX_AGENT" ]]; then echo "usage: o / new [cwd]" >&2 exit 1 fi local agent_cwd="${1:-$PWD}" # session/new is get-or-create: creating an existing session is a no-op # success, so real failures now surface instead of being suppressed. printf 'name=%s\ncwd=%s\n' "$CTX_SESSION" "$agent_cwd" | ollie-9p write "session/new" # Create the agent printf 'name=%s\ncwd=%s\n' "$CTX_AGENT" "$agent_cwd" | ollie-9p write "session/$CTX_SESSION/agent/new" echo "created: $CTX_SESSION/$CTX_AGENT" } cmd_tui() { if [[ -z "$CTX_AGENT" ]]; then echo "error: tui requires agent context" >&2 echo " usage: o / tui" >&2 exit 1 fi local s="$CTX_SESSION" local a="$CTX_AGENT" local tmux_session="o-${s}" # Check for subcommands if [[ "${1:-}" == "exit" ]]; then # Use = prefix for exact match (prevents dot interpretation as window.pane) 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 (top 80%), prompt (bottom 20%) tmux new-session -d -s "$tmux_session" -c "$HOME" tmux set-option -g mouse on tmux send-keys -t "=${tmux_session}" "o $s/$a log -n 1000 | grep -v '\\[\\[\\[.*\\]\\]\\]' | grep -v '^\`\`\`'; o $s/$a read chat" Enter tmux split-window -t "=${tmux_session}" -v -l 20% tmux send-keys -t "=${tmux_session}" "clear; o $s/$a prompt" Enter tmux set-option -t "=${tmux_session}" status-right "starting... | %H:%M" tmux set-option -t "=${tmux_session}" status-right-length 64 # Subscribe to state events for this agent only echo "session.${s}.agent.${a}.state" | ollie-9p rdwrs event 2>/dev/null | while read -r topic state; do bg="" case "$state" in idle) bg="colour70" ;; calling*) bg="colour214" ;; thinking) bg="colour33" ;; paused) bg="colour245" ;; *) bg="default" ;; esac tmux set-option -t "=${tmux_session}" status-right-style "fg=default,bg=$bg" 2>/dev/null status=$(ollie-9p read "${PREFIX}/status" 2>/dev/null | head -n1) tmux set-option -t "=${tmux_session}" status-right "${status:-$state} | %H:%M" 2>/dev/null done & local bg_pid=$! trap "kill $bg_pid 2>/dev/null" EXIT tmux select-pane -t "=${tmux_session}:0.1" exec tmux attach -t "=${tmux_session}" } cmd_env() { # Print export statements for current context [[ -n "$CTX_SESSION" ]] && echo "export OLLIE_SESSION=$CTX_SESSION" [[ -n "$CTX_AGENT" ]] && echo "export OLLIE_AGENT=$CTX_AGENT" } # cmd_overview shows all running sessions and their agents with a human-readable # status, so you can see what's live without knowing any names in advance. cmd_overview() { local idx idx=$(ollie-9p read "session/idx" 2>/dev/null || true) if [[ -z "$idx" ]]; then echo "no active sessions" echo "start one: o / new [cwd]" return fi # session/idx: id \t name \t paused \t connected \t remote \t cwd while IFS=$'\t' read -r _id sname paused _conn remote cwd; do [[ -z "$sname" ]] && continue local tag="" [[ "$paused" == "true" ]] && tag=" (paused)" [[ -n "$remote" ]] && tag="$tag [remote: $remote]" printf '%s%s\t%s\n' "$sname" "$tag" "$cwd" # agent idx: sess-id \t agent-id \t agent-name \t parent-id \t depth \t state local aidx aidx=$(ollie-9p read "session/$sname/agent/idx" 2>/dev/null || true) while IFS=$'\t' read -r _sid _aid aname _pid _depth _state; do [[ -z "$aname" ]] && continue local status status=$(ollie-9p read "session/$sname/agent/$aname/status" 2>/dev/null | head -n1) printf ' %s/%s\t%s\n' "$sname" "$aname" "${status:-?}" done <<< "$aidx" done <<< "$idx" } cmd_help() { cat <<'EOF' usage: o [context] [args...] Context is determined by the first argument: o Root level o Session level o / Agent level The slash in / disambiguates context level. Paths starting with / are root-relative (ignore context): o myproj ls /bypass List root bypass/ even in session context Commands (availability depends on context): Root level: overview Show running sessions/agents and their status ls List sessions, models, etc. generate One-shot generation (no session needed) Session level (o ...): ls List session contents (agents, config) ctl Session control (kill, pause, resume) read/write Access session files Agent level (o / ...): new [cwd] Create session (if needed) and agent ls List agent files ctl Agent control (stop, compact, model, etc.) prompt Interactive multi-line prompt REPL log [-n N] Show last N lines of agent log tui Launch tmux layout read/write Access agent files (chat, cfg, etc.) Common: read [-l] Read a file (-l: loop) write [data] Write to a file rdwr [data] Write then read (request-response) env Print export statements for context help Show this help Examples: o ls # list root (sessions, models) o myproj/coding new # create session+agent (cwd=$PWD) o myproj/coding new ~/src # create with specific cwd o myproj ls # list session contents o myproj/coding prompt # interactive prompt o myproj/coding read chat # read chat stream o myproj/coding ls /bypass # list root bypass/ from agent context o generate "explain monads" # one-shot, no session EOF } # ── Dispatch ──────────────────────────────────────────────────── dispatch_cmd() { local cmd="$1"; shift case "$cmd" in read) cmd_read "$@" ;; write) cmd_write "$@" ;; rdwr) cmd_rdwr "$@" ;; ls) cmd_ls "$@" ;; generate) cmd_generate "$@" ;; ctl) cmd_ctl "$@" ;; prompt) cmd_prompt "$@" ;; log) cmd_log "$@" ;; new) cmd_new "$@" ;; tui) cmd_tui "$@" ;; env) cmd_env "$@" ;; overview) cmd_overview "$@" ;; help|-h|--help) cmd_help ;; *) echo "unknown command: $cmd" >&2 echo "run 'o help' for usage" >&2 exit 1 ;; esac } # Parse context from first argument parse_context() { local arg="$1" if [[ "$arg" == */* ]]; then # session/agent format CTX_SESSION="${arg%%/*}" CTX_AGENT="${arg#*/}" PREFIX="session/$CTX_SESSION/agent/$CTX_AGENT" else # session only CTX_SESSION="$arg" PREFIX="session/$CTX_SESSION" fi } # Re-split arguments: handles quoted invocations (e.g., acme passes # the entire command as a single-quoted string). set -- $* # Main dispatch if [[ $# -lt 1 ]]; then cmd_overview echo "" echo "run 'o help' for usage" exit 0 fi # Check if first arg is a command (root level) or context if is_command "$1"; then # Root level - no context dispatch_cmd "$@" else # First arg is context (session or session/agent) parse_context "$1" shift if [[ $# -lt 1 ]]; then # No command after context - default to ls cmd_ls else dispatch_cmd "$@" fi fi