ollie/data/scripts/o

469 lines
14 KiB
Bash
Executable File

#!/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/<path>
# - Relative paths with both $session and $agent → session/$session/agent/$agent/<path>
# - No context set → path used as-is (root-relative)
#
# Subcommands:
# read <path> Read a file
# write <path> [data] Write to a file (args or stdin)
# readloop <path> Read in a loop (re-reads after each return)
# ls [path] List namespace entries
# ctl <command> 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=<name> or use full path: session/<name>/agent/<agent>/$path" >&2
return 1
fi
if [[ -z "$a" ]]; then
echo "error: '$path' requires agent context" >&2
echo " export agent=<name> or use full path: session/$s/agent/<name>/$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=<name> or use full path: session/<name>/$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=<name>" >&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=<name>" >&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 <path>" >&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 <path> [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 <prompt>" >&2; exit 1; }
printf '%s' "$prompt" | ollie-9p rdwr generate
}
cmd_readloop() {
if [[ $# -lt 1 ]]; then
echo "usage: o readloop <path>" >&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 <command> [args...]" >&2
echo " commands: stop, compact, kill, clear, model <m>, backend <b>, cwd <d>" >&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=$'\033[1m>\033[0m '
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}]" ;;
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 "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:
# ┌──────────────────────┬──────────┐
# │ │ state │
# │ chat (75%) │ (25%) │
# │ ├──────────┤
# │ │ shell │
# ├──────────────────────┴──────────┤
# │ prompt (20%) │
# └─────────────────────────────────┘
# Pane 0: chat (left, large)
tmux new-session -d -s "$tmux_session" -c "$HOME" \
-e "session=$s" -e "agent=$a"
tmux send-keys -t "$tmux_session" "o tail | grep -v '\\[\\[\\[.*\\]\\]\\]'" Enter
# Pane 1: state monitor (right column, 25% width)
tmux split-window -t "$tmux_session" -h -l 25% \
-e "session=$s" -e "agent=$a"
tmux send-keys -t "$tmux_session" "o readloop statewait" Enter
# Pane 2: shell (bottom of right column, 50% of right)
tmux split-window -t "$tmux_session" -v -l 50% \
-e "session=$s" -e "agent=$a"
# Pane 3: prompt REPL (bottom, 20% height, spans full width)
tmux split-window -t "$tmux_session:0.0" -v -l 20% \
-e "session=$s" -e "agent=$a"
tmux send-keys -t "$tmux_session" "o prompt" Enter
# Focus the prompt pane
tmux select-pane -t "$tmux_session:0.3"
exec tmux attach -t "$tmux_session"
}
cmd_help() {
cat <<'EOF'
usage: o <command> [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 <path> Read a file
write <path> [data] Write to a file (args or stdin)
readloop <path> Read in a loop (re-reads after return)
ls [path] List namespace entries
generate <prompt> One-shot generation (no session needed)
ctl <cmd> 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