463 lines
14 KiB
Bash
Executable File
463 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
|
|
echo "error: cannot create nested agents" >&2
|
|
exit 1
|
|
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
|
|
printf 'name=%s\ncwd=%s\n' "$session_name" "$cwd" | ollie-9p write "session/new"
|
|
# 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
|
|
}
|
|
|
|
# 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
|