toys: add ollie.sh, a minimal shell chat interface for ollie

~110-line bash script providing a fully functional chat interface by
reading/writing the 9p-mounted session filesystem directly. Includes
readline key bindings (read -e) and input history (set -o history).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-04-12 22:35:45 +02:00
parent 9cfc437bc1
commit 4be45bc628
2 changed files with 126 additions and 21 deletions

View File

@ -1,11 +1,18 @@
# Ollie Monorepo
# ollie
This repository contains all Ollie projects as submodules:
What happens if agent primitives are exposed as an ordinary filesystem? How much can be subtracted from AI agent implementations without reducing their usefulness? These are some of the motivating questions behind `ollie`.
- **core/** – The core Ollie agent library ([ollie-core](https://github.com/lneely/ollie-core))
- **tui/** – Terminal UI frontend ([ollie-tui](https://github.com/lneely/ollie-tui))
- **9p/** – 9p filesystem server ([ollie-9p](https://github.com/lneely/ollie-9p))
- **el/** – Emacs Lisp interface ([ollie-el](https://github.com/lneely/ollie-el))
- The [core system](https://github.com/lneely/ollie-core) provides a public library of AI agent primitives.
- The [9p filesystem server (Go)](https://github.com/lneely/ollie-9p) creates a virtual filesystem exposing the important state and behaviors as ordinary files and operations.
- A reference [TUI](https://github.com/lneely/ollie-tui) implementation proves that it is possible to build a functioning and user-friendly terminal front-end using only the Go standard library.
- And, just because I like my Emacs friends, a reference [Emacs interface](https://github.com/lneely/ollie-el) front-end implementation.
## Project Structure
- `core/`:
- `tui/`:
- `9p/`:
- `el/`:
## Getting Started
@ -37,7 +44,11 @@ mk tui # Build terminal UI
mk 9p # Build 9p filesystem server
```
For Emacs Lisp, load `el/ollie.el` in Emacs.
For Emacs Lisp, copy `el/ollie.el` into your own Emacs configuration directory, and:
```lisp
(require 'ollie)
```
## Development
@ -47,20 +58,6 @@ Update all submodules to latest:
git submodule update --remote
```
## Project Links
- **Monorepo**: [github.com/lneely/ollie](https://github.com/lneely/ollie)
- **Core Library**: [github.com/lneely/ollie-core](https://github.com/lneely/ollie-core)
- **Terminal UI**: [github.com/lneely/ollie-tui](https://github.com/lneely/ollie-tui)
- **9p Server**: [github.com/lneely/ollie-9p](https://github.com/lneely/ollie-9p)
- **Emacs Lisp**: [github.com/lneely/ollie-el](https://github.com/lneely/ollie-el)
## Project Structure
- `core/`: Core agent library (Go)
- `tui/`: Terminal UI (Go)
- `9p/`: 9p filesystem server (Go)
- `el/`: Emacs Lisp interface (Emacs Lisp)
## Credits
Many sources of inspiration:

108
toys/ollie.sh Executable file
View File

@ -0,0 +1,108 @@
#!/usr/bin/env bash
# ollie.sh: a simplified, fully-functional chat interface for ollie AI agents.
set -o history
MOUNT=${OLLIE_9MOUNT:-$HOME/mnt/ollie}
LAST=$HOME/.config/ollie/last-session
die() { printf 'error: %s\n' "$*" >&2; exit 1; }
is_idle() { [ "$(tr -d '[:space:]' < "$S/state" 2>/dev/null)" = "idle" ]; }
drain() {
local n
n=$(wc -c < "$S/chat" 2>/dev/null || echo 0)
[ "$n" -gt "$chat_off" ] && tail -c "+$((chat_off+1))" "$S/chat" | head -c "$((n-chat_off))"
chat_off=$n
}
# Parse flags
new=0; resume=0; id_flag=
backend=; model=; agent=; workdir=
while [ $# -gt 0 ]; do
case $1 in
-new) new=1 ;;
-resume) resume=1 ;;
-session) id_flag=$2; shift ;;
-backend) backend=$2; shift ;;
-model) model=$2; shift ;;
-agent) agent=$2; shift ;;
-workdir) workdir=$2; shift ;;
*) die "unknown flag: $1" ;;
esac
shift
done
# Build "new" ctl command with opts
new_cmd() {
local cmd=new
[ -n "$backend" ] && cmd="$cmd backend=$backend"
[ -n "$model" ] && cmd="$cmd model=$model"
[ -n "$agent" ] && cmd="$cmd agent=$agent"
cmd="$cmd workdir=${workdir:-$(pwd)}"
printf '%s\n' "$cmd"
}
# Create a fresh session, sets $id
create_session() {
local before d deadline
before=$(ls "$MOUNT/s" 2>/dev/null)
new_cmd > "$MOUNT/ctl" || die "write ctl"
deadline=$(($(date +%s) + 5))
while [ "$(date +%s)" -lt "$deadline" ]; do
for d in $(ls "$MOUNT/s" 2>/dev/null); do
printf '%s\n' "$before" | grep -qxF "$d" || { id=$d; return; }
done
sleep 0.05
done
die "timeout waiting for new session"
}
# Resolve session
if [ -n "$id_flag" ]; then
[ -d "$MOUNT/s/$id_flag" ] || die "session not found: $id_flag"
id=$id_flag; printf 'session: %s (resumed)\n' "$id" >&2
elif [ "$resume" = 1 ]; then
id=$(cat "$LAST" 2>/dev/null | tr -d '[:space:]')
[ -n "$id" ] && [ -d "$MOUNT/s/$id" ] || die "no resumable session"
printf 'session: %s (resumed)\n' "$id" >&2
elif [ "$new" = 1 ]; then
create_session
printf '%s\n' "$id" > "$LAST"
printf 'session: %s\n' "$id" >&2
else
id=$(cat "$LAST" 2>/dev/null | tr -d '[:space:]')
if [ -n "$id" ] && [ -d "$MOUNT/s/$id" ]; then
printf 'session: %s (resumed)\n' "$id" >&2
else
create_session
printf '%s\n' "$id" > "$LAST"
printf 'session: %s\n' "$id" >&2
fi
fi
S=$MOUNT/s/$id
chat_off=$(wc -c < "$S/chat" 2>/dev/null || echo 0)
trap '
[ "$(tr -d "[:space:]" < "$S/state" 2>/dev/null)" = "idle" ] && exit
printf "\n" >&2; printf "stop\n" > "$S/ctl"
' INT
while true; do
printf '> '
IFS= read -re input || break
[ -z "$input" ] && continue
history -s "$input"
case "$input" in
/kill) printf 'kill %s\n' "$id" > "$MOUNT/ctl"; break ;;
"/q "*) printf '%s\n' "${input#/q }" > "$S/enqueue"
printf 'queued\n' >&2; continue ;;
/*) printf '%s\n' "$input" > "$S/ctl" ;;
*) printf '%s' "$input" > "$S/prompt" || { printf 'submit failed\n' >&2; continue; } ;;
esac
sleep 0.15
while true; do
drain
is_idle && { drain; printf '\n'; break; }
sleep 0.05
done
done