373 lines
11 KiB
Bash
Executable File
373 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
# o — unified CLI for the ollie 9P namespace.
|
|
#
|
|
# Subcommands:
|
|
# read <file> Read a file from the active session/agent
|
|
# ls [path] List namespace entries
|
|
# ctl <command> Send a control command (stop, compact, kill, ...)
|
|
# prompt Interactive REPL for submitting prompts
|
|
# tail Tail chat output from the active agent
|
|
# env Print resolved session/agent environment
|
|
#
|
|
# Environment:
|
|
# session Session name (set via 'eval $(o env)')
|
|
# agent Agent name
|
|
#
|
|
# If session/agent are not set and there is exactly one session, it is
|
|
# selected automatically. If multiple exist, lists them and exits.
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Resolve session and agent ────────────────────────────────────
|
|
|
|
resolve_session() {
|
|
local s="${session:-}"
|
|
if [[ -n "$s" ]]; then
|
|
echo "$s"
|
|
return
|
|
fi
|
|
# Count sessions from idx
|
|
local idx
|
|
idx=$(ollie-9p read session/idx 2>/dev/null || true)
|
|
if [[ -z "$idx" ]]; then
|
|
return
|
|
fi
|
|
local names
|
|
names=$(echo "$idx" | cut -f1 | sort -u)
|
|
local count
|
|
count=$(echo "$names" | wc -l)
|
|
if [[ $count -eq 1 ]]; then
|
|
echo "$names"
|
|
else
|
|
echo "error: multiple sessions active, set one:" >&2
|
|
echo "$names" | sed 's/^/ /' >&2
|
|
echo "" >&2
|
|
echo " export session=<name> OR eval \$(o env <name>)" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
resolve_agent() {
|
|
local s="$1"
|
|
local a="${agent:-}"
|
|
if [[ -n "$a" ]]; then
|
|
echo "$a"
|
|
return
|
|
fi
|
|
if [[ -z "$s" ]]; then
|
|
return
|
|
fi
|
|
# List agents in session
|
|
local agents
|
|
agents=$(ollie-9p ls "session/$s/agent" 2>/dev/null | grep -v '^new$' || true)
|
|
if [[ -z "$agents" ]]; then
|
|
return
|
|
fi
|
|
local count
|
|
count=$(echo "$agents" | wc -l)
|
|
if [[ $count -eq 1 ]]; then
|
|
echo "$agents"
|
|
else
|
|
echo "error: multiple agents in session '$s', set one:" >&2
|
|
echo "$agents" | sed 's/^/ /' >&2
|
|
echo "" >&2
|
|
echo " export agent=<name> OR eval \$(o env $s <agent>)" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
require_session() {
|
|
local s
|
|
s=$(resolve_session)
|
|
if [[ -z "$s" ]]; then
|
|
echo "error: no session active (start one, or export session=<name>)" >&2
|
|
exit 1
|
|
fi
|
|
echo "$s"
|
|
}
|
|
|
|
require_agent() {
|
|
local s="$1"
|
|
local a
|
|
a=$(resolve_agent "$s")
|
|
if [[ -z "$a" ]]; then
|
|
echo "error: no agent set (export agent=<name>)" >&2
|
|
exit 1
|
|
fi
|
|
echo "$a"
|
|
}
|
|
|
|
agent_path() {
|
|
local s a
|
|
s=$(require_session)
|
|
a=$(require_agent "$s")
|
|
echo "session/$s/agent/$a"
|
|
}
|
|
|
|
# ── Subcommands ──────────────────────────────────────────────────
|
|
|
|
cmd_read() {
|
|
if [[ $# -lt 1 ]]; then
|
|
echo "usage: o read <file> [session [agent]]" >&2
|
|
exit 1
|
|
fi
|
|
local file="$1"; shift
|
|
[[ $# -ge 1 ]] && export session="${1}" && shift
|
|
[[ $# -ge 1 ]] && export agent="${1}" && shift
|
|
local path
|
|
path=$(agent_path)
|
|
ollie-9p read "$path/$file"
|
|
}
|
|
|
|
cmd_ls() {
|
|
local s
|
|
s=$(resolve_session) || true
|
|
if [[ $# -ge 1 ]]; then
|
|
local target="$1"; shift
|
|
if [[ "$target" == session/* || "$target" == elevate/* ]]; then
|
|
ollie-9p ls "$target"
|
|
elif [[ -n "$s" ]]; then
|
|
local a
|
|
a=$(resolve_agent "$s") || true
|
|
if [[ -n "$a" ]]; then
|
|
ollie-9p ls "session/$s/agent/$a/$target"
|
|
else
|
|
ollie-9p ls "session/$s/$target"
|
|
fi
|
|
else
|
|
ollie-9p ls "$target"
|
|
fi
|
|
elif [[ -n "$s" ]]; then
|
|
local a
|
|
a=$(resolve_agent "$s") || true
|
|
if [[ -n "$a" ]]; then
|
|
ollie-9p ls "session/$s/agent/$a"
|
|
else
|
|
ollie-9p ls "session/$s"
|
|
fi
|
|
else
|
|
ollie-9p ls session
|
|
fi
|
|
}
|
|
|
|
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)
|
|
echo "$*" | ollie-9p write "$path/ctl"
|
|
}
|
|
|
|
cmd_prompt() {
|
|
local path s a
|
|
path=$(agent_path)
|
|
s=$(require_session)
|
|
a=$(require_agent "$s")
|
|
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
|
|
# EOF — send remaining buffer if any
|
|
if [[ -n "$buf" ]]; then
|
|
history -s "$buf"
|
|
printf '%s\n' "$buf" | ollie-9p write "$path/prompt"
|
|
fi
|
|
printf "\n"
|
|
break
|
|
fi
|
|
# Slash commands (only when buffer is empty — first line)
|
|
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
|
|
# Blank line sends accumulated buffer
|
|
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)
|
|
exec ollie-9p read "$path/chat"
|
|
}
|
|
|
|
cmd_readloop() {
|
|
if [[ $# -lt 1 ]]; then
|
|
echo "usage: o readloop <file>" >&2
|
|
echo " read in a loop — re-reads after each return" >&2
|
|
exit 1
|
|
fi
|
|
local file="$1"; shift
|
|
local path
|
|
path=$(agent_path)
|
|
while true; do
|
|
ollie-9p read "$path/$file" || break
|
|
done
|
|
}
|
|
|
|
cmd_env() {
|
|
# Accept optional positional args to override
|
|
[[ $# -ge 1 ]] && export session="${1}" && shift
|
|
[[ $# -ge 1 ]] && export agent="${1}" && shift
|
|
|
|
local s a
|
|
s=$(require_session)
|
|
a=$(require_agent "$s")
|
|
echo "export session=$s"
|
|
echo "export agent=$a"
|
|
}
|
|
|
|
cmd_stop() {
|
|
local path
|
|
path=$(agent_path)
|
|
echo "stop" | ollie-9p write "$path/ctl"
|
|
}
|
|
|
|
cmd_kill() {
|
|
local s
|
|
s=$(require_session)
|
|
echo "kill" | ollie-9p write "session/$s/ctl"
|
|
}
|
|
|
|
cmd_tui() {
|
|
local s a
|
|
s=$(require_session)
|
|
a=$(require_agent "$s")
|
|
|
|
local tmux_session="o-${s}"
|
|
|
|
# Subcommand: o tui exit
|
|
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
|
|
|
|
# Kill existing tmux session if present
|
|
tmux kill-session -t "$tmux_session" 2>/dev/null || true
|
|
|
|
# Create new detached tmux session
|
|
# Pane 0: chat tail (left, large)
|
|
tmux new-session -d -s "$tmux_session" -c "$HOME" -x 200 -y 50 \
|
|
-e "session=$s" -e "agent=$a"
|
|
tmux send-keys -t "$tmux_session" "o tail" Enter
|
|
|
|
# Pane 1: state monitor (top-right)
|
|
tmux split-window -t "$tmux_session" -h -l 40 \
|
|
-e "session=$s" -e "agent=$a"
|
|
tmux send-keys -t "$tmux_session" "o readloop statewait" Enter
|
|
|
|
# Pane 2: spare shell (bottom-right, below state)
|
|
tmux split-window -t "$tmux_session" -v \
|
|
-e "session=$s" -e "agent=$a"
|
|
|
|
# Pane 3: prompt REPL (bottom, full width)
|
|
tmux split-window -t "$tmux_session:0.0" -v -l 8 \
|
|
-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"
|
|
|
|
# Attach
|
|
exec tmux attach -t "$tmux_session"
|
|
}
|
|
|
|
cmd_help() {
|
|
cat <<'EOF'
|
|
usage: o <command> [args...]
|
|
|
|
commands:
|
|
read <file> Read a file from the active agent's namespace
|
|
readloop <file> Read in a loop (re-reads after each return)
|
|
ls [path] List namespace entries (defaults to agent dir)
|
|
ctl <cmd> Send control command (stop, compact, kill, ...)
|
|
stop Interrupt the running agent
|
|
kill Kill the active session
|
|
prompt Interactive multi-line prompt REPL
|
|
tail Stream chat output
|
|
tui Launch tmux layout (chat + prompt + state + shell)
|
|
tui exit Kill the tmux TUI session
|
|
env [session] [agent] Print session/agent env vars (eval-able)
|
|
help Show this help
|
|
|
|
environment:
|
|
session Active session name
|
|
agent Active agent name
|
|
|
|
If only one session/agent exists, it is auto-selected.
|
|
If multiple exist, you must choose: eval $(o env <session> [agent])
|
|
EOF
|
|
}
|
|
|
|
# ── Dispatch ─────────────────────────────────────────────────────
|
|
|
|
if [[ $# -lt 1 ]]; then
|
|
cmd_help
|
|
exit 0
|
|
fi
|
|
|
|
subcmd="$1"; shift
|
|
case "$subcmd" in
|
|
read) cmd_read "$@" ;;
|
|
readloop) cmd_readloop "$@" ;;
|
|
ls) cmd_ls "$@" ;;
|
|
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
|