o: simplify path resolution, remove auto-session/agent
- Remove auto-resolution of session/agent (was slow, unexpected) - Path classification determines required context level immediately - Errors are fast and actionable with clear fix instructions - Root-level paths work without any context - o env now just prints export statements without requiring context
This commit is contained in:
parent
214242ba44
commit
05517c384a
388
data/scripts/o
388
data/scripts/o
|
|
@ -1,106 +1,155 @@
|
|||
#!/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 <file> Read a file from the active session/agent
|
||||
# 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
|
||||
# tail Tail chat output from the active agent
|
||||
# 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
|
||||
#
|
||||
# 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 ────────────────────────────────────
|
||||
# ── Path classification ─────────────────────────────────────────
|
||||
|
||||
resolve_session() {
|
||||
# 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:-}"
|
||||
if [[ -n "$s" ]]; then
|
||||
echo "$s"
|
||||
return
|
||||
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
|
||||
# Count sessions from idx
|
||||
local idx
|
||||
idx=$(ollie-9p read session/idx 2>/dev/null || true)
|
||||
if [[ -z "$idx" ]]; then
|
||||
return
|
||||
|
||||
# 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
|
||||
local names
|
||||
names=$(echo "$idx" | cut -f1 | sort -u)
|
||||
local count
|
||||
count=$(echo "$names" | wc -l)
|
||||
if [[ $count -eq 1 ]]; then
|
||||
echo "$names"
|
||||
|
||||
# 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 "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
|
||||
echo "$path"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
# ── Context requirements for specific commands ──────────────────
|
||||
|
||||
require_session() {
|
||||
local s
|
||||
s=$(resolve_session)
|
||||
local s="${session:-}"
|
||||
if [[ -z "$s" ]]; then
|
||||
echo "error: no session active (start one, or export session=<name>)" >&2
|
||||
exit 1
|
||||
echo "error: no session set" >&2
|
||||
echo " export session=<name>" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "$s"
|
||||
}
|
||||
|
||||
require_agent() {
|
||||
local s="$1"
|
||||
local a
|
||||
a=$(resolve_agent "$s")
|
||||
local a="${agent:-}"
|
||||
if [[ -z "$a" ]]; then
|
||||
echo "error: no agent set (export agent=<name>)" >&2
|
||||
exit 1
|
||||
echo "error: no agent set" >&2
|
||||
echo " export agent=<name>" >&2
|
||||
return 1
|
||||
fi
|
||||
echo "$a"
|
||||
}
|
||||
|
||||
agent_path() {
|
||||
local s a
|
||||
s=$(require_session)
|
||||
a=$(require_agent "$s")
|
||||
s=$(require_session) || return 1
|
||||
a=$(require_agent "$s") || return 1
|
||||
echo "session/$s/agent/$a"
|
||||
}
|
||||
|
||||
|
|
@ -108,64 +157,62 @@ agent_path() {
|
|||
|
||||
cmd_read() {
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "usage: o read <file> [session [agent]]" >&2
|
||||
echo "usage: o read <path>" >&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"
|
||||
path=$(resolve_path "$1") || exit 1
|
||||
ollie-9p read "$path"
|
||||
}
|
||||
|
||||
cmd_write() {
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "usage: o write <file> [data...]" >&2
|
||||
echo " writes stdin (or args) to session/\$s/agent/\$a/<file>" >&2
|
||||
echo "usage: o write <path> [data...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
local file="$1"; shift
|
||||
local path
|
||||
path=$(agent_path)
|
||||
path=$(resolve_path "$file") || exit 1
|
||||
if [[ $# -gt 0 ]]; then
|
||||
printf '%s\n' "$*" | ollie-9p write "$path/$file"
|
||||
printf '%s\n' "$*" | ollie-9p write "$path"
|
||||
else
|
||||
ollie-9p write "$path/$file"
|
||||
ollie-9p write "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
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
|
||||
local path
|
||||
path=$(resolve_path "$1") || exit 1
|
||||
ollie-9p ls "$path"
|
||||
else
|
||||
ollie-9p ls session
|
||||
# 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_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
|
||||
|
|
@ -173,15 +220,16 @@ cmd_ctl() {
|
|||
exit 1
|
||||
fi
|
||||
local path
|
||||
path=$(agent_path)
|
||||
path=$(agent_path) || exit 1
|
||||
echo "$*" | ollie-9p write "$path/ctl"
|
||||
}
|
||||
|
||||
cmd_prompt() {
|
||||
local path s a
|
||||
path=$(agent_path)
|
||||
s=$(require_session)
|
||||
a=$(require_agent "$s")
|
||||
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"
|
||||
|
|
@ -196,7 +244,6 @@ cmd_prompt() {
|
|||
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"
|
||||
|
|
@ -204,7 +251,6 @@ cmd_prompt() {
|
|||
printf "\n"
|
||||
break
|
||||
fi
|
||||
# Slash commands (only when buffer is empty — first line)
|
||||
if [[ -z "$buf" && "$line" == /* ]]; then
|
||||
history -s "$line"
|
||||
local cmd="${line#/}"
|
||||
|
|
@ -230,7 +276,6 @@ cmd_prompt() {
|
|||
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=""
|
||||
|
|
@ -248,88 +293,68 @@ cmd_prompt() {
|
|||
|
||||
cmd_tail() {
|
||||
local path
|
||||
path=$(agent_path)
|
||||
path=$(agent_path) || exit 1
|
||||
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"
|
||||
# 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)
|
||||
path=$(agent_path) || exit 1
|
||||
echo "stop" | ollie-9p write "$path/ctl"
|
||||
}
|
||||
|
||||
cmd_kill() {
|
||||
local s
|
||||
s=$(require_session)
|
||||
s=$(require_session) || exit 1
|
||||
echo "kill" | ollie-9p write "session/$s/ctl"
|
||||
}
|
||||
|
||||
cmd_tui() {
|
||||
local s a
|
||||
s=$(require_session)
|
||||
a=$(require_agent "$s")
|
||||
s=$(require_session) || exit 1
|
||||
a=$(require_agent "$s") || exit 1
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
|
|
@ -337,27 +362,40 @@ cmd_help() {
|
|||
cat <<'EOF'
|
||||
usage: o <command> [args...]
|
||||
|
||||
commands:
|
||||
read <file> Read a file from the active agent's namespace
|
||||
write <file> [data] Write to a file (args or stdin)
|
||||
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
|
||||
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
|
||||
|
||||
environment:
|
||||
session Active session name
|
||||
agent Active agent name
|
||||
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
|
||||
|
||||
If only one session/agent exists, it is auto-selected.
|
||||
If multiple exist, you must choose: eval $(o env <session> [agent])
|
||||
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
|
||||
export session=foo agent=bar # set context
|
||||
o read chat # read agent's chat
|
||||
o readloop statewait # blocks until agent state changes
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -370,17 +408,17 @@ fi
|
|||
|
||||
subcmd="$1"; shift
|
||||
case "$subcmd" in
|
||||
read) cmd_read "$@" ;;
|
||||
write) cmd_write "$@" ;;
|
||||
read) cmd_read "$@" ;;
|
||||
write) cmd_write "$@" ;;
|
||||
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 "$@" ;;
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue