ollie/data/scripts/o

470 lines
14 KiB
Bash
Executable File

#!/bin/bash
# o — unified CLI for the ollie 9P namespace.
#
# Context is determined by the first argument:
# o <cmd> Root level
# o <session> <cmd> Session level
# o <session>/<agent> <cmd> Agent level
#
# The slash in session/agent disambiguates context level.
#
# Subcommands:
# read <path> Read a file (-l to loop)
# write <path> [data] Write to a file (args or stdin)
# ls [path] List namespace entries
# new [name] [cwd] Create session or agent (context-dependent)
# ctl <command> Send a control command
# prompt Interactive REPL for submitting prompts (agent-level)
# tui Launch tmux TUI (agent-level)
# generate <prompt> 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 new coding Create agent 'coding' in 'myproj'
# o myproj/coding prompt Interactive prompt
# o myproj/coding read chat Read chat output
set -euo pipefail
# ── 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 ls new ctl prompt log tui env generate 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] <path>" >&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 <path> [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_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 <prompt>" >&2
exit 1
fi
local prompt=""
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_ctl() {
if [[ $# -lt 1 ]]; then
echo "usage: o ctl <command> [args...]" >&2
if [[ -n "$CTX_AGENT" ]]; then
echo " Agent: stop, compact, clear, inject, agent, model, cwd, name" >&2
elif [[ -n "$CTX_SESSION" ]]; then
echo " Session: kill, pause, resume" >&2
else
echo " (requires session or agent context)" >&2
fi
exit 1
fi
if [[ -z "$PREFIX" ]]; then
echo "error: ctl requires session or agent context" >&2
echo " usage: o <session> ctl <cmd>" >&2
echo " or: o <session>/<agent> ctl <cmd>" >&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 <session>/<agent> 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, /inject X"
echo "!q exits the TUI."
echo ""
local buf="" line
while true; do
local prompt_str
if [[ -z "$buf" ]]; then
prompt_str=$'\033[1m>\033[0m '
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 <session>/<agent> log [-n N]" >&2
exit 1
fi
ollie-9p read "${PREFIX}/log" | tail -n "$lines"
}
cmd_new() {
if [[ -n "$CTX_AGENT" ]]; then
# o session/agent new [cwd] — create the agent in that session
local agent_cwd="${1:-$PWD}"
printf 'name=%s\ncwd=%s\n' "$CTX_AGENT" "$agent_cwd" | ollie-9p write "session/$CTX_SESSION/agent/new"
echo "created agent: $CTX_SESSION/$CTX_AGENT"
return
fi
if [[ -n "$CTX_SESSION" ]]; then
# Create agent in current session
if [[ $# -lt 1 ]]; then
echo "usage: o <session> new <agent> [cwd]" >&2
exit 1
fi
local agent_name="$1"
local agent_cwd="${2:-$PWD}"
printf 'name=%s\ncwd=%s\n' "$agent_name" "$agent_cwd" | ollie-9p write "${PREFIX}/agent/new"
echo "created agent: $CTX_SESSION/$agent_name"
else
# Create session and agent: o new sname/aname [cwd]
if [[ $# -lt 1 ]]; then
echo "usage: o new <session>/<agent> [cwd]" >&2
exit 1
fi
local spec="$1"
local cwd="${2:-$PWD}"
if [[ "$spec" != */* ]]; then
echo "usage: o new <session>/<agent> [cwd]" >&2
echo "error: must specify session/agent (e.g., work/code)" >&2
exit 1
fi
local session_name="${spec%%/*}"
local agent_name="${spec#*/}"
# Create session (ignore if already exists)
printf 'name=%s\ncwd=%s\n' "$session_name" "$cwd" | ollie-9p write "session/new" 2>/dev/null || true
# Create agent in that session
printf 'name=%s\ncwd=%s\n' "$agent_name" "$cwd" | ollie-9p write "session/$session_name/agent/new"
echo "created: $session_name/$agent_name"
fi
}
cmd_tui() {
if [[ -z "$CTX_AGENT" ]]; then
echo "error: tui requires agent context" >&2
echo " usage: o <session>/<agent> tui" >&2
exit 1
fi
local s="$CTX_SESSION"
local a="$CTX_AGENT"
# Check for subcommands
if [[ "${1:-}" == "exit" ]]; then
tmux kill-session -t "o-${s}" 2>/dev/null && echo "[killed o-${s}]" || echo "no tui session: o-${s}"
return
fi
local tmux_session="o-${s}"
tmux kill-session -t "$tmux_session" 2>/dev/null || true
# Layout: chat (top 80%), prompt (bottom 20%)
tmux -CC new-session -d -s "$tmux_session" -c "$HOME"
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
while true; do
ollie-9p read "${PREFIX}/statewait" 2>/dev/null | while read -r 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
tmux set-option -t "$tmux_session" status-right "$state | %H:%M" 2>/dev/null
done
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_help() {
cat <<'EOF'
usage: o [context] <command> [args...]
Context is determined by the first argument:
o <cmd> Root level
o <session> <cmd> Session level
o <session>/<agent> <cmd> Agent level
The slash in <session>/<agent> 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:
ls List sessions, models, etc.
new <session> [cwd] Create a new session
generate <prompt> One-shot generation (no session needed)
Session level (o <session> ...):
ls List session contents (agents, config)
new <agent> [cwd] Create a new agent
ctl <cmd> Session control (kill, pause, resume)
read/write <path> Access session files
Agent level (o <session>/<agent> ...):
ls List agent files
ctl <cmd> 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 <path> Access agent files (chat, cfg, etc.)
Common:
read [-l] <path> Read a file (-l: loop)
write <path> [data] Write to a file
env Print export statements for context
help Show this help
Examples:
o ls # list root (sessions, models)
o new myproj # create session 'myproj'
o myproj ls # list session contents
o myproj new coding # create agent 'coding'
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 "$@" ;;
ls) cmd_ls "$@" ;;
generate) cmd_generate "$@" ;;
ctl) cmd_ctl "$@" ;;
prompt) cmd_prompt "$@" ;;
log) cmd_log "$@" ;;
new) cmd_new "$@" ;;
tui) cmd_tui "$@" ;;
env) cmd_env "$@" ;;
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_help
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