Skip to content

Instantly share code, notes, and snippets.

@lloydzhou
Last active September 25, 2026 14:24
Show Gist options
  • Select an option

  • Save lloydzhou/c675546c6395fb4ac4365c2f111f503e to your computer and use it in GitHub Desktop.

Select an option

Save lloydzhou/c675546c6395fb4ac4365c2f111f503e to your computer and use it in GitHub Desktop.
550S — The Wandering Earth has 550W. Your terminal gets 550 lines of Shell. Powered by agent.sh.

550S

Shell. Single-file. Symlink.

The Wandering Earth has 550W. Your terminal gets 550S.

Not here to move the Earth. Just here to run your tools.

550S is the codename for agent.sh: an AI agent in 550 lines of Bash + awk. The model lives behind an API. The tools live in a directory. Give it a symlink, and it gets a new tool.

No SDK. No npm or pip. No build step. One script you can read end to end.

Quick start

Requires Bash, curl, awk, and standard shell utilities. Use a streaming Chat Completions API and a model with tool-calling support.

Download the script as 550s into your project directory:

curl -fL https://gist.githubusercontent.com/lloydzhou/c675546c6395fb4ac4365c2f111f503e/raw/agent.sh -o 550s && chmod +x 550s

export OPENAI_API_KEY="your-api-key"
export MODEL="your-model-name"
# Optional: export OPENAI_BASE_URL="https://your-provider.example/v1"

mkdir -p .agents/tools
ln -s "$(command -v cat)" .agents/tools/cat
./550s "Read AGENTS.md and summarize it."

Inspect the script before running it. The download command overwrites an existing 550s; back it up first if needed.

./550s "hello"       # one-shot
./550s              # interactive chat with a 550S> prompt
./550s < prompt.txt # prompt from stdin

Want to call it from anywhere? Put 550s in a directory on your PATH.

Make it yours

Startup creates missing AGENTS.md, .agents/tools/, and .agents/conv.jsonl without overwriting existing content. No separate initialization command is needed.

  • Rules: edit AGENTS.md in your current working directory.
  • Tools: place executables or symlinks in .agents/tools/.
  • Skills: optionally add .agents/skills/<name>/SKILL.md; the agent can read them with an available tool.
  • History: .agents/conv.jsonl persists between runs. Paths are relative to your working directory, not the script.

Back up the conversation to start fresh:

mv -i .agents/conv.jsonl ".agents/conv-$(date +%Y%m%d-%H%M%S).jsonl"

Tools are not sandboxed: they run with your shell privileges and model-supplied arguments. Only expose tools you trust.

Source and full documentation

550S is a playful nod to 550W from The Wandering Earth, not an affiliated project.

#!/usr/bin/env bash
# 550S — Shell. Single-file. Symlink.
#
# Built on the shell.
# Delivered as one file.
# Extended with symlinks.
#
# Source: https://github.com/lloydzhou/agent.sh
# License: MIT — see LICENSE in the source repository.
# agent.sh — filesystem-first AI agent in pure bash/awk
# Chat Completions only; dependencies: bash, curl, awk.
# Auto-create: AGENTS.md (rules), .agents/tools/ (executables), .agents/conv.jsonl (history).
set -uo pipefail
MODEL="${MODEL:-gpt-5.6-luna}"
API_KEY="${OPENAI_API_KEY:-}"
API_URL="${OPENAI_BASE_URL:-https://api.openai.com/v1}/chat/completions"
AGENT_DIR="${AGENT_DIR:-$PWD/.agents}"
TOOLS_DIR="$AGENT_DIR/tools"
CONV_FILE="$AGENT_DIR/conv.jsonl"
MAX_TURNS="${MAX_TURNS:-50}"
TOOL_TIMEOUT_SECS="${TOOL_TIMEOUT_SECS:-60}"
TOOL_RESULT_MAX_BYTES="${TOOL_RESULT_MAX_BYTES:-100000}"
USER_INPUT=""
TOOL_DEFS_JSON=""
DISPLAY_LAST_CHAR=$'\n'
PREV_WAS_REASONING=false
INTERRUPT_REQUESTED=false
# ================= inline awk program =================
# Quoted heredoc preserves backslashes; read returns nonzero at EOF.
IFS= read -r -d '' AWK_PROGRAM <<'AWK_PROGRAM' || true
# Well-formed JSON: direct fields, raw array elements, decoded strings.
# Read one JSON value, honoring strings and nested containers.
function jread_at(s, i, start, c, depth, quoted, escaped) {
while (substr(s, i, 1) ~ /[ \t\r\n]/) i++
start = i
for (; i <= length(s); i++) {
c = substr(s, i, 1)
if (quoted) {
if (escaped) escaped = 0
else if (c == "\\") escaped = 1
else if (c == "\"") {
quoted = 0
if (!depth) return substr(s, start, i - start + 1)
}
} else {
if (!depth && c ~ /[,}\] \t\r\n]/) break
if (c == "\"") quoted = 1
else if (c == "{" || c == "[") depth++
else if (c == "}" || c == "]") {
if (--depth == 0) return substr(s, start, i - start + 1)
}
}
}
return substr(s, start, i - start)
}
function jfield(s, key, i, k, v) {
sub(/^[ \t\r\n]+/, "", s)
if (substr(s, 1, 1) != "{") return ""
for (i = 2; i < length(s);) {
while (substr(s, i, 1) ~ /[, \t\r\n]/) i++
k = jread_at(s, i); i += length(k)
while (substr(s, i, 1) ~ /[: \t\r\n]/) i++
v = jread_at(s, i)
if (k == "\"" key "\"") return v
if (v == "") break
i += length(v)
}
return ""
}
function jstr(s, key, v) {
v = jfield(s, key)
return substr(v, 1, 1) == "\"" ? junesc(substr(v, 2, length(v) - 2)) : ""
}
function jsplit(s, out, i, v, n) {
split("", out)
sub(/^[ \t\r\n]+/, "", s)
if (substr(s, 1, 1) != "[") return 0
for (i = 2; i < length(s);) {
while (substr(s, i, 1) ~ /[, \t\r\n]/) i++
v = jread_at(s, i)
if (v == "") break
out[++n] = v; i += length(v)
}
return n + 0
}
# ---------- escape / unescape ----------
function jescape(s, out, i, c, code) {
for (i = 1; i <= length(s); i++) {
c = substr(s, i, 1)
if ((code = index("\"\\\b\f\n\r\t", c)) > 0)
out = out "\\" substr("\"\\bfnrt", code, 1)
else if ((code = index("\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037", c)) > 0)
out = out sprintf("\\u%04x", code)
else out = out c
}
return out
}
function junesc(s, out, i, c, pos, hex, cp, lo) {
for (i = 1; i <= length(s); i++) {
c = substr(s, i, 1)
if (c != "\\") { out = out c; continue }
if (++i > length(s)) break
c = substr(s, i, 1)
if (c != "u") {
pos = index("bfnrt\"\\/", c)
out = out (pos ? substr("\b\f\n\r\t\"\\/", pos, 1) : c)
continue
}
hex = substr(s, i + 1, 4)
cp = hex_to_int(hex)
if (length(hex) < 4 || cp < 0) { out = out "\\u"; continue }
if (cp >= 55296 && cp <= 56319 && i + 10 <= length(s) && substr(s, i + 5, 2) == "\\u") {
lo = hex_to_int(substr(s, i + 7, 4))
if (lo >= 56320 && lo <= 57343) {
out = out utf8(65536 + (cp - 55296) * 1024 + lo - 56320)
i += 10; continue
}
}
out = out (cp >= 55296 && cp <= 57343 ? "\\u" hex : utf8(cp))
i += 4
}
return out
}
function hex_to_int(hex, i, d, value) {
for (i = 1; i <= length(hex); i++) {
d = index("0123456789abcdef", tolower(substr(hex, i, 1))) - 1
if (d < 0) return -1
value = value * 16 + d
}
return value + 0
}
function utf8(cp) {
# Decoding runs under LC_ALL=C; build UTF-8 bytes explicitly.
if (cp <= 127) return sprintf("%c", cp)
if (cp <= 2047) return sprintf("%c%c", 192 + int(cp / 64), 128 + (cp % 64))
if (cp <= 65535) return sprintf("%c%c%c", 224 + int(cp / 4096), 128 + int((cp % 4096) / 64), 128 + (cp % 64))
return sprintf("%c%c%c%c", 240 + int(cp / 262144), 128 + int((cp % 262144) / 4096), 128 + int((cp % 4096) / 64), 128 + (cp % 64))
}
# ---------- CLI ----------
BEGIN {
if (json_mode == "escape_string") {
if ((getline json_input) < 0) json_input = ""
else while ((getline _line) > 0) json_input = json_input "\n" _line
printf "%s", jescape(substr(json_input, 1, length(json_input) - 1))
exit 0
}
}
# SSE -> TSV; tool_calls: name, id, call JSON, argv prefixed with '=' to preserve empty args.
function esc(s, out, i, c) {
for (i = 1; i <= length(s); i++) {
c = substr(s, i, 1)
out = out (c == "\\" ? "\\\\" : c == "\t" ? "\\t" : c == "\n" ? "\\n" : c == "\r" ? "\\r" : c)
}
return out
}
BEGIN { _reset() }
/^error\t/ { print "error\t" esc(substr($0, 7)); fflush(); done = 1; exit 0 }
/^retry$/ { _reset(); print "retry"; fflush(); next }
/^data: \[DONE\]/ {
_emit_tools()
if (pending_stop != "") print "finish_reason\t" pending_stop
fflush(); done = 1; next
}
/^data: / {
jsplit(jfield(substr($0, 7), "choices"), choices)
choice = choices[1]
delta = jfield(choice, "delta")
fr = jstr(choice, "finish_reason")
if (fr != "") pending_stop = fr
content = jstr(delta, "content")
if (content != "") print "content\t" esc(content)
reasoning = jstr(delta, "reasoning_content")
if (reasoning != "") print "reasoning\t" esc(reasoning)
_parse_tool_calls(jfield(delta, "tool_calls"))
if (fr == "tool_calls") _emit_tools()
fflush()
}
END {
if (json_mode != "escape_string" && !done && pending_stop == "") print "error\tStream interrupted (no [DONE] received)"
}
function _reset() {
done = 0; pending_stop = ""; tc_max = -1
split("", tc_name); split("", tc_id); split("", tc_args)
}
function _parse_tool_calls(json, n, p, tc, idx, f, id) {
n = jsplit(json, chunks)
for (p = 1; p <= n; p++) {
tc = chunks[p]
idx = jfield(tc, "index")
if (idx !~ /^[0-9]+$/) continue
idx += 0
if (idx > tc_max) tc_max = idx
f = jfield(tc, "function")
id = jstr(tc, "id")
if (id != "") tc_id[idx] = id
tc_name[idx] = tc_name[idx] jstr(f, "name")
tc_args[idx] = tc_args[idx] jstr(f, "arguments")
}
}
function _emit_tools( idx, n, i, call, line) {
for (idx = 0; idx <= tc_max; idx++) {
if (tc_args[idx] == "") continue
# Preserve the arguments JSON text; decode only the execution copy.
call = "{\"id\":\"" jescape(tc_id[idx]) "\",\"type\":\"function\",\"function\":{\"name\":\"" jescape(tc_name[idx]) "\",\"arguments\":\"" jescape(tc_args[idx]) "\"}}"
line = "tool_calls\t" esc(tc_name[idx]) "\t" esc(tc_id[idx]) "\t" esc(call)
n = jsplit(jfield(tc_args[idx], "args"), argv)
for (i = 1; i <= n; i++) line = line "\t=" esc(junesc(substr(argv[i], 2, length(argv[i]) - 2)))
print line
tc_args[idx] = ""
}
fflush()
}
AWK_PROGRAM
# ================= utils =================
util_awk_run() { LC_ALL=C LANG=C awk "$@"; }
util_unescape() {
# Decode line-protocol fields into REPLY without stripping trailing newlines.
local s="$1" c
REPLY=""
while [[ "$s" == *\\* ]]; do
REPLY+="${s%%\\*}"; s="${s#*\\}"
c="${s:0:1}"; s="${s:1}"
case "$c" in n) REPLY+=$'\n' ;; t) REPLY+=$'\t' ;; r) REPLY+=$'\r' ;; *) REPLY+="$c" ;; esac
done
REPLY+="$s"
}
util_run_timeout() {
local seconds="$1" pid watcher rc; shift
"$@" <&0 2>&1 & pid=$!
( sleep "$seconds"; kill -TERM "$pid" 2>/dev/null ) >/dev/null 2>&1 & watcher=$!
wait "$pid" 2>/dev/null; rc=$?
kill "$watcher" 2>/dev/null
return $(( rc == 143 ? 124 : rc ))
}
util_die() {
printf '\033[31mError: %s\033[0m\n' "$*" >&2
exit 1
}
util_json_escape() {
# A final sentinel preserves trailing newlines through awk's record reader.
printf '%s.' "${1:-}" | util_awk_run -v json_mode=escape_string "$AWK_PROGRAM"
}
# ================= system prompt =================
agent_build_prompt() {
cat <<'PROMPT'
You are agent.sh, a minimal filesystem-first agent running in a terminal.
Tools:
- Tools are plain executables (binaries, symlinks, scripts) in the agent tools directory; the tool name is the file name.
- Each call passes "args": an array of strings. Elements are handed to the command as separate arguments — like Docker CMD exec form. No shell expansion: quotes, spaces, and $ are literal. Pass [] when the tool takes no arguments.
- Read each tool's description before calling it.
- A tool returns stdout and stderr. Non-zero exit is prefixed "Error (exit N)" — read the message and self-correct on the next call. Long output is truncated; use head/tail/grep to narrow large reads.
- Shell composition (pipes, $(...), globs) does not apply to args. If a "bash" tool exists, call it with ["-c", "<command line>"] for full shell features.
- You may issue several independent tool calls in one turn.
PROMPT
local locale="${LC_ALL:-${LC_MESSAGES:-${LANG:-en_US}}}" lang_name skill name desc indexed=""
locale="${locale%%.*}"
case "$locale" in ""|C|C.*|POSIX) locale="en_US" ;; esac
case "$locale" in zh*) lang_name="Chinese" ;; *) lang_name="English" ;; esac
printf 'Environment:\nlang: %s\npwd: %s\nhome: %s\nplatform: %s\nshell: %s\n' \
"$locale" "$PWD" "${HOME:-?}" "$(uname -s 2>/dev/null || echo unknown)" "${SHELL:-unknown}"
printf '\nBy default, use %s (%s) for output, including reasoning/thinking, unless user instructions specify otherwise. Code, commands, and file content remain as-is.\n' "$lang_name" "$locale"
for skill in "$AGENT_DIR"/skills/*/SKILL.md; do
[[ -f "$skill" ]] || continue
if [[ -z "$indexed" ]]; then
printf '\n<skill-index>\nWhen a skill matches the task, read its SKILL.md using any suitable available tool (e.g. cat or bash); no dedicated skill tool is needed. If none is available, do not assume its contents. Resolve relative paths from the skill directory.\n'
indexed=1
fi
name="${skill%/SKILL.md}"; name="${name##*/}"
desc=$(awk '
{ sub(/\r$/, "") }
NR == 1 { if ($0 != "---") exit; next }
/^---$/ { exit }
/^description:[ \t]*/ { sub(/^description:[ \t]*/, ""); sub(/[ \t]+$/, ""); if ($0 ~ /^[|>][-+0-9]*$/) exit; if ($0 ~ /^".*"$/ || $0 ~ /^\047.*\047$/) $0 = substr($0, 2, length($0) - 2); print; exit }
' "$skill")
printf -- '- %s%s\n path: %s\n' "$name" "${desc:+: $desc}" "$skill"
done
[[ -n "$indexed" ]] && printf '</skill-index>\n'
if [[ -f "$PWD/AGENTS.md" ]]; then
printf '\nUser instructions:\n'
cat "$PWD/AGENTS.md"
fi
}
# ================= tools (filesystem-discovered command templates) =================
tools_load() {
TOOL_DEFS_JSON=""
[[ -d "$TOOLS_DIR" ]] || return 0
local f name desc
for f in "$TOOLS_DIR"/*; do
[[ -x "$f" ]] || continue # tools are executables: binaries, symlinks, scripts
name="${f##*/}"
[[ "$name" =~ ^[A-Za-z0-9_-]{1,64}$ ]] || { printf 'skip tool (bad name): %s\n' "$name" >&2; continue; }
desc=$(tools_auto_desc "$f")
[[ -n "$desc" ]] || desc="Run $name"
TOOL_DEFS_JSON+="${TOOL_DEFS_JSON:+,}"'{"type":"function","function":{"name":"'"$name"'","description":"'"$(util_json_escape "$desc")"'","parameters":{"type":"object","properties":{"args":{"type":"array","items":{"type":"string"},"description":"command-line arguments for '"$name"', passed as separate arguments (exec form); [] if none"}}}}}'
done
}
tools_auto_desc() {
# Exact-name man match; fall back to --help with stdin=/dev/null and a timeout.
local name="${1##*/}" d
d=$(man -f "$name" 2>/dev/null | awk -v name="$name" '$0 ~ "^" name "\\([0-9]+\\)" { sub(/^[^(]*\([^)]*\)[[:space:]]*-[[:space:]]*/, ""); print; exit }')
[[ -z "$d" ]] && d=$(util_run_timeout 3 "$1" --help </dev/null 2>&1 | grep -m1 -v '^[[:space:]]*$')
printf '%s' "$d"
}
tool_execute() {
local name="$1" f="$TOOLS_DIR/$1" output rc; shift
[[ -x "$f" ]] || { printf 'Error: no such tool: %s' "$name"; return 0; }
output=$(util_run_timeout "$TOOL_TIMEOUT_SECS" bash -c 'exec "$0" "$@"' "$f" "$@" 2>&1)
rc=$?
(( rc != 0 )) && output="Error (exit $rc): $output"
if (( ${#output} > TOOL_RESULT_MAX_BYTES )); then
local head=$(( (TOOL_RESULT_MAX_BYTES + 1) / 2 )) tail=$(( TOOL_RESULT_MAX_BYTES / 2 ))
output="${output:0:head}"$'\n...[truncated]...\n'"${output: ${#output}-tail}"
fi
printf '%s' "$output"
}
# ================= conversation (conv.jsonl: one OpenAI message per line) =================
conv_append() { printf '%s\n' "$1" >> "$CONV_FILE"; }
# ================= llm =================
llm_call() {
local body='{"model":"'"$MODEL"'","messages":[{"role":"system","content":"'"$(util_json_escape "$(agent_build_prompt)")"'"}'
body+="$(awk '{printf ",%s", $0}' "$CONV_FILE")"']'
[[ -n "$TOOL_DEFS_JSON" ]] && body+=',"tools":['"$TOOL_DEFS_JSON"']'
body+=',"stream":true}'
curl -sS --no-buffer -D - --retry 2 --retry-delay 1 --retry-max-time 20 \
--connect-timeout 5 --speed-limit 1 --speed-time 60 \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d "$body" "$API_URL" 2>&1 \
| util_awk_run '
{ sub(/\r$/, "") }
/^curl: / { print "error\t" $0; fflush(); exit 1 }
/^HTTP\// {
if (body) { print "retry"; fflush() }
code = $2; body = 0; err = ""; next
}
/^[ \t]*$/ && !body { body = 1; next }
!body { next }
code >= 400 { err = err (err == "" ? "" : "\n") $0; next }
{ print; fflush() }
END {
if (code >= 400) {
print "error\tHTTP " code ": " (err == "" ? "(empty)" : err)
fflush()
}
}
' \
| util_awk_run "$AWK_PROGRAM"
}
# ================= display =================
display_ensure_newline() {
if [[ "$DISPLAY_LAST_CHAR" != $'\n' ]]; then printf '\n'; DISPLAY_LAST_CHAR=$'\n'; fi
return 0
}
display() {
local kind="$1"; shift
case "$kind" in
content|reasoning)
[[ -n "$1" ]] || return 0
if [[ "$kind" == reasoning ]]; then
PREV_WAS_REASONING=true
printf '\033[90m%s\033[0m' "$1"
else
[[ "$PREV_WAS_REASONING" == true ]] && printf '\n'
PREV_WAS_REASONING=false
printf '%s' "$1"
fi
DISPLAY_LAST_CHAR="${1: -1}"; return 0 ;;
tool) display_ensure_newline; printf '\033[33m[tool] %s %s\033[0m\n' "$1" "${2:0:120}" ;;
result)
local first="${2%%$'\n'*}"
printf '\033[90m[%s done] %s\033[0m\n' "$1" "${first:0:120}" ;;
error) display_ensure_newline; printf '\033[31mError: %s\033[0m\n' "$1" >&2 ;;
retry) printf '\033[90mretrying...\033[0m\n' >&2 ;;
esac
DISPLAY_LAST_CHAR=$'\n'
}
# ================= agent loop =================
agent_loop() {
local turn=0 stop="" text="" reasoning="" reasoning_field="" calls="" tool_messages="" _line _type _rest
conv_append '{"role":"user","content":"'"$(util_json_escape "$1")"'"}'
INTERRUPT_REQUESTED=false
trap 'INTERRUPT_REQUESTED=true' INT
while (( turn < MAX_TURNS )); do
(( turn++ )) || true
stop=""; text=""; reasoning=""; calls=""; tool_messages=""
exec 8< <(llm_call)
while IFS= read -r _line <&8; do
[[ "$INTERRUPT_REQUESTED" == true ]] && { stop="interrupted"; break; }
_type="${_line%%$'\t'*}"
_rest="${_line#*$'\t'}"
case "$_type" in
content)
util_unescape "$_rest"
display content "$REPLY"
text+="$REPLY" ;;
reasoning)
util_unescape "$_rest"; display reasoning "$REPLY"; reasoning+="$REPLY" ;;
tool_calls)
# name, id, complete call JSON, then '='-prefixed argv fields.
local _f=() _argv=() _name _id _call _output _i
IFS=$'\t' read -r -a _f <<<"$_rest"
util_unescape "${_f[0]}"; _name="$REPLY"
util_unescape "${_f[1]}"; _id="$REPLY"
util_unescape "${_f[2]}"; _call="$REPLY"
for _i in ${_f[@]+"${_f[@]:3}"}; do
util_unescape "${_i:1}"; _argv+=("$REPLY")
done
display tool "$_name" "${_argv[*]-}"
_output=$(tool_execute "$_name" ${_argv[@]+"${_argv[@]}"})
display result "$_name" "$_output"
calls+="${calls:+,}$_call"
tool_messages+='{"role":"tool","tool_call_id":"'"$(util_json_escape "$_id")"'","content":"'"$(util_json_escape "$_output")"'"}'$'\n' ;;
retry)
display retry
text=""; reasoning=""; calls=""; tool_messages="" ;;
finish_reason)
stop="$_rest" ;;
error)
util_unescape "$_rest"; display error "$REPLY"
stop="error"; break ;;
esac
done
exec 8<&-
[[ "$stop" == "interrupted" || "$stop" == "error" ]] && break
# Persist this assistant turn, then continue only when tools were called
reasoning_field=""
[[ -n "$reasoning" ]] && reasoning_field=',"reasoning_content":"'"$(util_json_escape "$reasoning")"'"'
if [[ -n "$calls" ]]; then
local content_field="null"
[[ -n "$text" ]] && content_field='"'"$(util_json_escape "$text")"'"'
conv_append '{"role":"assistant","content":'"$content_field"',"tool_calls":['"$calls"']'"$reasoning_field"'}'
printf '%s' "$tool_messages" >> "$CONV_FILE"
[[ "$stop" == "tool_calls" ]] && continue
else
[[ -n "$text$reasoning" ]] && conv_append '{"role":"assistant","content":"'"$(util_json_escape "$text")"'"'"$reasoning_field"'}'
fi
break
done
trap - INT
if (( turn >= MAX_TURNS )); then
display error "Max turns ($MAX_TURNS) reached"
return 1
fi
[[ "$stop" == error ]] && return 1
return 0
}
# ================= interactive =================
interactive_mode() {
local line
printf '\033[36magent.sh (%s) — type exit or Ctrl+D to quit\033[0m\n' "$MODEL"
while true; do
if ! IFS= read -e -r -p $'\033[32m550S>\033[0m ' line; then printf '\n'; break; fi
[[ "$line" == "exit" || "$line" == "quit" ]] && break
[[ -z "$line" ]] && continue
history -s "$line" 2>/dev/null || true
agent_loop "$line"
display_ensure_newline
done
printf '\033[36mGoodbye!\033[0m\n'
}
# ================= cli =================
usage() {
cat <<'EOF'
agent.sh — filesystem-first AI agent (OpenAI Chat Completions)
Usage:
agent.sh [prompt] chat; no prompt + tty opens interactive REPL
agent.sh < file read prompt from stdin
fresh conversation: mv -i .agents/conv.jsonl ".agents/conv-$(date +%Y%m%d-%H%M%S).jsonl"
Missing AGENTS.md, tools directory, and history file are created on startup.
Options:
-m, --model NAME model override (env MODEL)
-h, --help this help
Environment:
OPENAI_API_KEY required
OPENAI_BASE_URL default https://api.openai.com/v1
MODEL default gpt-5.6-luna
EOF
}
parse_args() {
while (( $# > 0 )); do
case "$1" in
-m|--model) [[ -n "${2:-}" ]] || util_die "$1 requires a model"; MODEL="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
-*) util_die "unknown option: $1 (see --help)" ;;
*) [[ -n "$USER_INPUT" ]] && util_die "unexpected extra argument: $1"
USER_INPUT="$1"; shift ;;
esac
done
return 0
}
main() {
parse_args "$@"
[[ -n "$API_KEY" ]] || util_die "OPENAI_API_KEY is not set"
mkdir -p "$TOOLS_DIR" && touch "$CONV_FILE" || util_die "Cannot initialize $AGENT_DIR"
if [[ ! -e "$PWD/AGENTS.md" ]]; then
printf '# Agent instructions\n\n' > "$PWD/AGENTS.md" || util_die "Cannot create AGENTS.md"
fi
tools_load
if [[ -z "$USER_INPUT" ]]; then
if [[ -t 0 ]]; then interactive_mode; return $?; fi
USER_INPUT=$(cat)
fi
agent_loop "$USER_INPUT"
local rc=$?
display_ensure_newline
return "$rc"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment