{"slug": "550s-the-wandering-earth-has-550w-your-terminal-gets-550-lines-of-shell-powered", "title": "550S — The Wandering Earth has 550W. Your terminal gets 550 lines of Shell. Powered by agent.sh.", "summary": "A developer released agent.sh, a single-file AI agent written in pure bash and awk that runs on the shell with only bash, curl and awk as dependencies. The script talks to OpenAI-compatible Chat Completions endpoints, auto-creates an AGENTS.md rules file, a .agents/tools/ directory of executables and a .agents/conv.jsonl conversation history, and includes an inline awk JSON parser for handling tool calls. It is published under the MIT license on GitHub.", "body_md": "|  | #!/usr/bin/env bash | \n|  | # 550S — Shell. Single-file. Symlink. | \n|  | # | \n|  | # Built on the shell. | \n|  | # Delivered as one file. | \n|  | # Extended with symlinks. | \n|  | # | \n|  | # Source: https://github.com/lloydzhou/agent.sh | \n|  | # License: MIT — see LICENSE in the source repository. | \n|  | # agent.sh — filesystem-first AI agent in pure bash/awk | \n|  | # Chat Completions only; dependencies: bash, curl, awk. | \n|  | # Auto-create: AGENTS.md (rules), .agents/tools/ (executables), .agents/conv.jsonl (history). | \n|  |  | \n|  | set -uo pipefail | \n|  |  | \n|  | MODEL=\"${MODEL:-gpt-5.6-luna}\" | \n|  | API_KEY=\"${OPENAI_API_KEY:-}\" | \n|  | API_URL=\"${OPENAI_BASE_URL:-https://api.openai.com/v1}/chat/completions\" | \n|  | AGENT_DIR=\"${AGENT_DIR:-$PWD/.agents}\" | \n|  | TOOLS_DIR=\"$AGENT_DIR/tools\" | \n|  | CONV_FILE=\"$AGENT_DIR/conv.jsonl\" | \n|  | MAX_TURNS=\"${MAX_TURNS:-50}\" | \n|  | TOOL_TIMEOUT_SECS=\"${TOOL_TIMEOUT_SECS:-60}\" | \n|  | TOOL_RESULT_MAX_BYTES=\"${TOOL_RESULT_MAX_BYTES:-100000}\" | \n|  | USER_INPUT=\"\" | \n|  |  | \n|  | TOOL_DEFS_JSON=\"\" | \n|  | DISPLAY_LAST_CHAR=$'\\n' | \n|  | PREV_WAS_REASONING=false | \n|  | INTERRUPT_REQUESTED=false | \n|  |  | \n|  | # ================= inline awk program ================= | \n|  | # Quoted heredoc preserves backslashes; read returns nonzero at EOF. | \n|  | IFS= read -r -d '' AWK_PROGRAM <<'AWK_PROGRAM' \\|\\| true | \n|  | # Well-formed JSON: direct fields, raw array elements, decoded strings. | \n|  |  | \n|  | # Read one JSON value, honoring strings and nested containers. | \n|  | function jread_at(s, i,    start, c, depth, quoted, escaped) { | \n|  | while (substr(s, i, 1) ~ /[ \\t\\r\\n]/) i++ | \n|  | start = i | \n|  | for (; i <= length(s); i++) { | \n|  | c = substr(s, i, 1) | \n|  | if (quoted) { | \n|  | if (escaped) escaped = 0 | \n|  | else if (c == \"\\\\\") escaped = 1 | \n|  | else if (c == \"\\\"\") { | \n|  | quoted = 0 | \n|  | if (!depth) return substr(s, start, i - start + 1) | \n|  | } | \n|  | } else { | \n|  | if (!depth && c ~ /[,}\\] \\t\\r\\n]/) break | \n|  | if (c == \"\\\"\") quoted = 1 | \n|  | else if (c == \"{\" \\|\\| c == \"[\") depth++ | \n|  | else if (c == \"}\" \\|\\| c == \"]\") { | \n|  | if (--depth == 0) return substr(s, start, i - start + 1) | \n|  | } | \n|  | } | \n|  | } | \n|  | return substr(s, start, i - start) | \n|  | } | \n|  |  | \n|  | function jfield(s, key,    i, k, v) { | \n|  | sub(/^[ \\t\\r\\n]+/, \"\", s) | \n|  | if (substr(s, 1, 1) != \"{\") return \"\" | \n|  | for (i = 2; i < length(s);) { | \n|  | while (substr(s, i, 1) ~ /[, \\t\\r\\n]/) i++ | \n|  | k = jread_at(s, i); i += length(k) | \n|  | while (substr(s, i, 1) ~ /[: \\t\\r\\n]/) i++ | \n|  | v = jread_at(s, i) | \n|  | if (k == \"\\\"\" key \"\\\"\") return v | \n|  | if (v == \"\") break | \n|  | i += length(v) | \n|  | } | \n|  | return \"\" | \n|  | } | \n|  |  | \n|  | function jstr(s, key,    v) { | \n|  | v = jfield(s, key) | \n|  | return substr(v, 1, 1) == \"\\\"\" ? junesc(substr(v, 2, length(v) - 2)) : \"\" | \n|  | } | \n|  |  | \n|  | function jsplit(s, out,    i, v, n) { | \n|  | split(\"\", out) | \n|  | sub(/^[ \\t\\r\\n]+/, \"\", s) | \n|  | if (substr(s, 1, 1) != \"[\") return 0 | \n|  | for (i = 2; i < length(s);) { | \n|  | while (substr(s, i, 1) ~ /[, \\t\\r\\n]/) i++ | \n|  | v = jread_at(s, i) | \n|  | if (v == \"\") break | \n|  | out[++n] = v; i += length(v) | \n|  | } | \n|  | return n + 0 | \n|  | } | \n|  |  | \n|  | # ---------- escape / unescape ---------- | \n|  | function jescape(s,    out, i, c, code) { | \n|  | for (i = 1; i <= length(s); i++) { | \n|  | c = substr(s, i, 1) | \n|  | if ((code = index(\"\\\"\\\\\\b\\f\\n\\r\\t\", c)) > 0) | \n|  | out = out \"\\\\\" substr(\"\\\"\\\\bfnrt\", code, 1) | \n|  | 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) | \n|  | out = out sprintf(\"\\\\u%04x\", code) | \n|  | else out = out c | \n|  | } | \n|  | return out | \n|  | } | \n|  |  | \n|  | function junesc(s,    out, i, c, pos, hex, cp, lo) { | \n|  | for (i = 1; i <= length(s); i++) { | \n|  | c = substr(s, i, 1) | \n|  | if (c != \"\\\\\") { out = out c; continue } | \n|  | if (++i > length(s)) break | \n|  | c = substr(s, i, 1) | \n|  | if (c != \"u\") { | \n|  | pos = index(\"bfnrt\\\"\\\\/\", c) | \n|  | out = out (pos ? substr(\"\\b\\f\\n\\r\\t\\\"\\\\/\", pos, 1) : c) | \n|  | continue | \n|  | } | \n|  | hex = substr(s, i + 1, 4) | \n|  | cp = hex_to_int(hex) | \n|  | if (length(hex) < 4 \\|\\| cp < 0) { out = out \"\\\\u\"; continue } | \n|  | if (cp >= 55296 && cp <= 56319 && i + 10 <= length(s) && substr(s, i + 5, 2) == \"\\\\u\") { | \n|  | lo = hex_to_int(substr(s, i + 7, 4)) | \n|  | if (lo >= 56320 && lo <= 57343) { | \n|  | out = out utf8(65536 + (cp - 55296) * 1024 + lo - 56320) | \n|  | i += 10; continue | \n|  | } | \n|  | } | \n|  | out = out (cp >= 55296 && cp <= 57343 ? \"\\\\u\" hex : utf8(cp)) | \n|  | i += 4 | \n|  | } | \n|  | return out | \n|  | } | \n|  |  | \n|  | function hex_to_int(hex,    i, d, value) { | \n|  | for (i = 1; i <= length(hex); i++) { | \n|  | d = index(\"0123456789abcdef\", tolower(substr(hex, i, 1))) - 1 | \n|  | if (d < 0) return -1 | \n|  | value = value * 16 + d | \n|  | } | \n|  | return value + 0 | \n|  | } | \n|  |  | \n|  | function utf8(cp) { | \n|  | # Decoding runs under LC_ALL=C; build UTF-8 bytes explicitly. | \n|  | if (cp <= 127) return sprintf(\"%c\", cp) | \n|  | if (cp <= 2047) return sprintf(\"%c%c\", 192 + int(cp / 64), 128 + (cp % 64)) | \n|  | if (cp <= 65535) return sprintf(\"%c%c%c\", 224 + int(cp / 4096), 128 + int((cp % 4096) / 64), 128 + (cp % 64)) | \n|  | return sprintf(\"%c%c%c%c\", 240 + int(cp / 262144), 128 + int((cp % 262144) / 4096), 128 + int((cp % 4096) / 64), 128 + (cp % 64)) | \n|  | } | \n|  |  | \n|  | # ---------- CLI ---------- | \n|  | BEGIN { | \n|  | if (json_mode == \"escape_string\") { | \n|  | if ((getline json_input) < 0) json_input = \"\" | \n|  | else while ((getline _line) > 0) json_input = json_input \"\\n\" _line | \n|  | printf \"%s\", jescape(substr(json_input, 1, length(json_input) - 1)) | \n|  | exit 0 | \n|  | } | \n|  | } | \n|  | # SSE -> TSV; tool_calls: name, id, call JSON, argv prefixed with '=' to preserve empty args. | \n|  |  | \n|  | function esc(s,    out, i, c) { | \n|  | for (i = 1; i <= length(s); i++) { | \n|  | c = substr(s, i, 1) | \n|  | out = out (c == \"\\\\\" ? \"\\\\\\\\\" : c == \"\\t\" ? \"\\\\t\" : c == \"\\n\" ? \"\\\\n\" : c == \"\\r\" ? \"\\\\r\" : c) | \n|  | } | \n|  | return out | \n|  | } | \n|  |  | \n|  | BEGIN { _reset() } | \n|  |  | \n|  | /^error\\t/ { print \"error\\t\" esc(substr($0, 7)); fflush(); done = 1; exit 0 } | \n|  | /^retry$/ { _reset(); print \"retry\"; fflush(); next } | \n|  | /^data:  $$ DONE $$ / { | \n|  | _emit_tools() | \n|  | if (pending_stop != \"\") print \"finish_reason\\t\" pending_stop | \n|  | fflush(); done = 1; next | \n|  | } | \n|  | /^data: / { | \n|  | jsplit(jfield(substr($0, 7), \"choices\"), choices) | \n|  | choice = choices[1] | \n|  | delta = jfield(choice, \"delta\") | \n|  | fr = jstr(choice, \"finish_reason\") | \n|  | if (fr != \"\") pending_stop = fr | \n|  | content = jstr(delta, \"content\") | \n|  | if (content != \"\") print \"content\\t\" esc(content) | \n|  | reasoning = jstr(delta, \"reasoning_content\") | \n|  | if (reasoning != \"\") print \"reasoning\\t\" esc(reasoning) | \n|  | _parse_tool_calls(jfield(delta, \"tool_calls\")) | \n|  | if (fr == \"tool_calls\") _emit_tools() | \n|  | fflush() | \n|  | } | \n|  | END { | \n|  | if (json_mode != \"escape_string\" && !done && pending_stop == \"\") print \"error\\tStream interrupted (no [DONE] received)\" | \n|  | } | \n|  |  | \n|  | function _reset() { | \n|  | done = 0; pending_stop = \"\"; tc_max = -1 | \n|  | split(\"\", tc_name); split(\"\", tc_id); split(\"\", tc_args) | \n|  | } | \n|  |  | \n|  | function _parse_tool_calls(json,    n, p, tc, idx, f, id) { | \n|  | n = jsplit(json, chunks) | \n|  | for (p = 1; p <= n; p++) { | \n|  | tc = chunks[p] | \n|  | idx = jfield(tc, \"index\") | \n|  | if (idx !~ /^[0-9]+$/) continue | \n|  | idx += 0 | \n|  | if (idx > tc_max) tc_max = idx | \n|  | f = jfield(tc, \"function\") | \n|  | id = jstr(tc, \"id\") | \n|  | if (id != \"\") tc_id[idx] = id | \n|  | tc_name[idx] = tc_name[idx] jstr(f, \"name\") | \n|  | tc_args[idx] = tc_args[idx] jstr(f, \"arguments\") | \n|  | } | \n|  | } | \n|  |  | \n|  | function _emit_tools(    idx, n, i, call, line) { | \n|  | for (idx = 0; idx <= tc_max; idx++) { | \n|  | if (tc_args[idx] == \"\") continue | \n|  | # Preserve the arguments JSON text; decode only the execution copy. | \n|  | call = \"{\\\"id\\\":\\\"\" jescape(tc_id[idx]) \"\\\",\\\"type\\\":\\\"function\\\",\\\"function\\\":{\\\"name\\\":\\\"\" jescape(tc_name[idx]) \"\\\",\\\"arguments\\\":\\\"\" jescape(tc_args[idx]) \"\\\"}}\" | \n|  | line = \"tool_calls\\t\" esc(tc_name[idx]) \"\\t\" esc(tc_id[idx]) \"\\t\" esc(call) | \n|  | n = jsplit(jfield(tc_args[idx], \"args\"), argv) | \n|  | for (i = 1; i <= n; i++) line = line \"\\t=\" esc(junesc(substr(argv[i], 2, length(argv[i]) - 2))) | \n|  | print line | \n|  | tc_args[idx] = \"\" | \n|  | } | \n|  | fflush() | \n|  | } | \n|  | AWK_PROGRAM | \n|  |  | \n|  | # ================= utils ================= | \n|  | util_awk_run() { LC_ALL=C LANG=C awk \"$@\"; } | \n|  |  | \n|  | util_unescape() { | \n|  | # Decode line-protocol fields into REPLY without stripping trailing newlines. | \n|  | local s=\"$1\" c | \n|  | REPLY=\"\" | \n|  | while [[ \"$s\" == *\\\\* ]]; do | \n|  | REPLY+=\"${s%%\\\\*}\"; s=\"${s#*\\\\}\" | \n|  | c=\"${s:0:1}\"; s=\"${s:1}\" | \n|  | case \"$c\" in n) REPLY+=$'\\n' ;; t) REPLY+=$'\\t' ;; r) REPLY+=$'\\r' ;; *) REPLY+=\"$c\" ;; esac | \n|  | done | \n|  | REPLY+=\"$s\" | \n|  | } | \n|  |  | \n|  | util_run_timeout() { | \n|  | local seconds=\"$1\" pid watcher rc; shift | \n|  | \"$@\" <&0 2>&1 & pid=$! | \n|  | ( sleep \"$seconds\"; kill -TERM \"$pid\" 2>/dev/null ) >/dev/null 2>&1 & watcher=$! | \n|  | wait \"$pid\" 2>/dev/null; rc=$? | \n|  | kill \"$watcher\" 2>/dev/null | \n|  | return $(( rc == 143 ? 124 : rc )) | \n|  | } | \n|  |  | \n|  | util_die() { | \n|  | printf '\\033[31mError: %s\\033[0m\\n' \"$*\" >&2 | \n|  | exit 1 | \n|  | } | \n|  |  | \n|  | util_json_escape() { | \n|  | # A final sentinel preserves trailing newlines through awk's record reader. | \n|  | printf '%s.' \"${1:-}\" \\| util_awk_run -v json_mode=escape_string \"$AWK_PROGRAM\" | \n|  | } | \n|  |  | \n|  | # ================= system prompt ================= | \n|  | agent_build_prompt() { | \n|  | cat <<'PROMPT' | \n|  | You are agent.sh, a minimal filesystem-first agent running in a terminal. | \n|  |  | \n|  | Tools: | \n|  | - Tools are plain executables (binaries, symlinks, scripts) in the agent tools directory; the tool name is the file name. | \n|  | - 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. | \n|  | - Read each tool's description before calling it. | \n|  | - 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. | \n|  | - Shell composition (pipes, $(...), globs) does not apply to args. If a \"bash\" tool exists, call it with [\"-c\", \"<command line>\"] for full shell features. | \n|  | - You may issue several independent tool calls in one turn. | \n|  | PROMPT | \n|  | local locale=\"${LC_ALL:-${LC_MESSAGES:-${LANG:-en_US}}}\" lang_name skill name desc indexed=\"\" | \n|  | locale=\"${locale%%.*}\" | \n|  | case \"$locale\" in \"\"\\|C\\|C.*\\|POSIX) locale=\"en_US\" ;; esac | \n|  | case \"$locale\" in zh*) lang_name=\"Chinese\" ;; *) lang_name=\"English\" ;; esac | \n|  | printf 'Environment:\\nlang: %s\\npwd: %s\\nhome: %s\\nplatform: %s\\nshell: %s\\n' \\ | \n|  | \"$locale\" \"$PWD\" \"${HOME:-?}\" \"$(uname -s 2>/dev/null \\|\\| echo unknown)\" \"${SHELL:-unknown}\" | \n|  | 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\" | \n|  | for skill in \"$AGENT_DIR\"/skills/*/SKILL.md; do | \n|  | [[ -f \"$skill\" ]] \\|\\| continue | \n|  | if [[ -z \"$indexed\" ]]; then | \n|  | 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' | \n|  | indexed=1 | \n|  | fi | \n|  | name=\"${skill%/SKILL.md}\"; name=\"${name##*/}\" | \n|  | desc=$(awk ' | \n|  | { sub(/\\r$/, \"\") } | \n|  | NR == 1 { if ($0 != \"---\") exit; next } | \n|  | /^---$/ { exit } | \n|  | /^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 } | \n|  | ' \"$skill\") | \n|  | printf -- '- %s%s\\n  path: %s\\n' \"$name\" \"${desc:+: $desc}\" \"$skill\" | \n|  | done | \n|  | [[ -n \"$indexed\" ]] && printf '</skill-index>\\n' | \n|  | if [[ -f \"$PWD/AGENTS.md\" ]]; then | \n|  | printf '\\nUser instructions:\\n' | \n|  | cat \"$PWD/AGENTS.md\" | \n|  | fi | \n|  | } | \n|  |  | \n|  | # ================= tools (filesystem-discovered command templates) ================= | \n|  | tools_load() { | \n|  | TOOL_DEFS_JSON=\"\" | \n|  | [[ -d \"$TOOLS_DIR\" ]] \\|\\| return 0 | \n|  | local f name desc | \n|  | for f in \"$TOOLS_DIR\"/*; do | \n|  | [[ -x \"$f\" ]] \\|\\| continue # tools are executables: binaries, symlinks, scripts | \n|  | name=\"${f##*/}\" | \n|  | [[ \"$name\" =~ ^[A-Za-z0-9_-]{1,64}$ ]] \\|\\| { printf 'skip tool (bad name): %s\\n' \"$name\" >&2; continue; } | \n|  | desc=$(tools_auto_desc \"$f\") | \n|  | [[ -n \"$desc\" ]] \\|\\| desc=\"Run $name\" | \n|  | 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\"}}}}}' | \n|  | done | \n|  | } | \n|  |  | \n|  | tools_auto_desc() { | \n|  | # Exact-name man match; fall back to --help with stdin=/dev/null and a timeout. | \n|  | local name=\"${1##*/}\" d | \n|  | d=$(man -f \"$name\" 2>/dev/null \\| awk -v name=\"$name\" '$0 ~ \"^\" name \"\\$[0-9]+\\$\" { sub(/^[^(]*$[^)]*$[[:space:]]*-[[:space:]]*/, \"\"); print; exit }') | \n|  | [[ -z \"$d\" ]] && d=$(util_run_timeout 3 \"$1\" --help </dev/null 2>&1 \\| grep -m1 -v '^[[:space:]]*$') | \n|  | printf '%s' \"$d\" | \n|  | } | \n|  |  | \n|  | tool_execute() { | \n|  | local name=\"$1\" f=\"$TOOLS_DIR/$1\" output rc; shift | \n|  | [[ -x \"$f\" ]] \\|\\| { printf 'Error: no such tool: %s' \"$name\"; return 0; } | \n|  | output=$(util_run_timeout \"$TOOL_TIMEOUT_SECS\" bash -c 'exec \"$0\" \"$@\"' \"$f\" \"$@\" 2>&1) | \n|  | rc=$? | \n|  | (( rc != 0 )) && output=\"Error (exit $rc): $output\" | \n|  | if (( ${#output} > TOOL_RESULT_MAX_BYTES )); then | \n|  | local head=$(( (TOOL_RESULT_MAX_BYTES + 1) / 2 )) tail=$(( TOOL_RESULT_MAX_BYTES / 2 )) | \n|  | output=\"${output:0:head}\"$'\\n...[truncated]...\\n'\"${output: ${#output}-tail}\" | \n|  | fi | \n|  | printf '%s' \"$output\" | \n|  | } | \n|  |  | \n|  | # ================= conversation (conv.jsonl: one OpenAI message per line) ================= | \n|  | conv_append() { printf '%s\\n' \"$1\" >> \"$CONV_FILE\"; } | \n|  |  | \n|  | # ================= llm ================= | \n|  | llm_call() { | \n|  | local body='{\"model\":\"'\"$MODEL\"'\",\"messages\":[{\"role\":\"system\",\"content\":\"'\"$(util_json_escape \"$(agent_build_prompt)\")\"'\"}' | \n|  | body+=\"$(awk '{printf \",%s\", $0}' \"$CONV_FILE\")\"']' | \n|  | [[ -n \"$TOOL_DEFS_JSON\" ]] && body+=',\"tools\":['\"$TOOL_DEFS_JSON\"']' | \n|  | body+=',\"stream\":true}' | \n|  | curl -sS --no-buffer -D - --retry 2 --retry-delay 1 --retry-max-time 20 \\ | \n|  | --connect-timeout 5 --speed-limit 1 --speed-time 60 \\ | \n|  | -H \"Authorization: Bearer $API_KEY\" -H \"Content-Type: application/json\" \\ | \n|  | -d \"$body\" \"$API_URL\" 2>&1 \\ | \n|  | \\| util_awk_run ' | \n|  | { sub(/\\r$/, \"\") } | \n|  | /^curl: / { print \"error\\t\" $0; fflush(); exit 1 } | \n|  | /^HTTP\\// { | \n|  | if (body) { print \"retry\"; fflush() } | \n|  | code = $2; body = 0; err = \"\"; next | \n|  | } | \n|  | /^[ \\t]*$/ && !body { body = 1; next } | \n|  | !body { next } | \n|  | code >= 400 { err = err (err == \"\" ? \"\" : \"\\n\") $0; next } | \n|  | { print; fflush() } | \n|  | END { | \n|  | if (code >= 400) { | \n|  | print \"error\\tHTTP \" code \": \" (err == \"\" ? \"(empty)\" : err) | \n|  | fflush() | \n|  | } | \n|  | } | \n|  | ' \\ | \n|  | \\| util_awk_run \"$AWK_PROGRAM\" | \n|  | } | \n|  |  | \n|  | # ================= display ================= | \n|  | display_ensure_newline() { | \n|  | if [[ \"$DISPLAY_LAST_CHAR\" != $'\\n' ]]; then printf '\\n'; DISPLAY_LAST_CHAR=$'\\n'; fi | \n|  | return 0 | \n|  | } | \n|  |  | \n|  | display() { | \n|  | local kind=\"$1\"; shift | \n|  | case \"$kind\" in | \n|  | content\\|reasoning) | \n|  | [[ -n \"$1\" ]] \\|\\| return 0 | \n|  | if [[ \"$kind\" == reasoning ]]; then | \n|  | PREV_WAS_REASONING=true | \n|  | printf '\\033[90m%s\\033[0m' \"$1\" | \n|  | else | \n|  | [[ \"$PREV_WAS_REASONING\" == true ]] && printf '\\n' | \n|  | PREV_WAS_REASONING=false | \n|  | printf '%s' \"$1\" | \n|  | fi | \n|  | DISPLAY_LAST_CHAR=\"${1: -1}\"; return 0 ;; | \n|  | tool) display_ensure_newline; printf '\\033[33m[tool] %s %s\\033[0m\\n' \"$1\" \"${2:0:120}\" ;; | \n|  | result) | \n|  | local first=\"${2%%$'\\n'*}\" | \n|  | printf '\\033[90m[%s done] %s\\033[0m\\n' \"$1\" \"${first:0:120}\" ;; | \n|  | error) display_ensure_newline; printf '\\033[31mError: %s\\033[0m\\n' \"$1\" >&2 ;; | \n|  | retry) printf '\\033[90mretrying...\\033[0m\\n' >&2 ;; | \n|  | esac | \n|  | DISPLAY_LAST_CHAR=$'\\n' | \n|  | } | \n|  |  | \n|  | # ================= agent loop ================= | \n|  | agent_loop() { | \n|  | local turn=0 stop=\"\" text=\"\" reasoning=\"\" reasoning_field=\"\" calls=\"\" tool_messages=\"\" _line _type _rest | \n|  | conv_append '{\"role\":\"user\",\"content\":\"'\"$(util_json_escape \"$1\")\"'\"}' | \n|  | INTERRUPT_REQUESTED=false | \n|  | trap 'INTERRUPT_REQUESTED=true' INT | \n|  | while (( turn < MAX_TURNS )); do | \n|  | (( turn++ )) \\|\\| true | \n|  | stop=\"\"; text=\"\"; reasoning=\"\"; calls=\"\"; tool_messages=\"\" | \n|  | exec 8< <(llm_call) | \n|  | while IFS= read -r _line <&8; do | \n|  | [[ \"$INTERRUPT_REQUESTED\" == true ]] && { stop=\"interrupted\"; break; } | \n|  | _type=\"${_line%%$'\\t'*}\" | \n|  | _rest=\"${_line#*$'\\t'}\" | \n|  | case \"$_type\" in | \n|  | content) | \n|  | util_unescape \"$_rest\" | \n|  | display content \"$REPLY\" | \n|  | text+=\"$REPLY\" ;; | \n|  | reasoning) | \n|  | util_unescape \"$_rest\"; display reasoning \"$REPLY\"; reasoning+=\"$REPLY\" ;; | \n|  | tool_calls) | \n|  | # name, id, complete call JSON, then '='-prefixed argv fields. | \n|  | local _f=() _argv=() _name _id _call _output _i | \n|  | IFS=$'\\t' read -r -a _f <<<\"$_rest\" | \n|  | util_unescape \"${_f[0]}\"; _name=\"$REPLY\" | \n|  | util_unescape \"${_f[1]}\"; _id=\"$REPLY\" | \n|  | util_unescape \"${_f[2]}\"; _call=\"$REPLY\" | \n|  | for _i in ${_f[@]+\"${_f[@]:3}\"}; do | \n|  | util_unescape \"${_i:1}\"; _argv+=(\"$REPLY\") | \n|  | done | \n|  | display tool \"$_name\" \"${_argv[*]-}\" | \n|  | _output=$(tool_execute \"$_name\" ${_argv[@]+\"${_argv[@]}\"}) | \n|  | display result \"$_name\" \"$_output\" | \n|  | calls+=\"${calls:+,}$_call\" | \n|  | tool_messages+='{\"role\":\"tool\",\"tool_call_id\":\"'\"$(util_json_escape \"$_id\")\"'\",\"content\":\"'\"$(util_json_escape \"$_output\")\"'\"}'$'\\n' ;; | \n|  | retry) | \n|  | display retry | \n|  | text=\"\"; reasoning=\"\"; calls=\"\"; tool_messages=\"\" ;; | \n|  | finish_reason) | \n|  | stop=\"$_rest\" ;; | \n|  | error) | \n|  | util_unescape \"$_rest\"; display error \"$REPLY\" | \n|  | stop=\"error\"; break ;; | \n|  | esac | \n|  | done | \n|  | exec 8<&- | \n|  | [[ \"$stop\" == \"interrupted\" \\|\\| \"$stop\" == \"error\" ]] && break | \n|  | # Persist this assistant turn, then continue only when tools were called | \n|  | reasoning_field=\"\" | \n|  | [[ -n \"$reasoning\" ]] && reasoning_field=',\"reasoning_content\":\"'\"$(util_json_escape \"$reasoning\")\"'\"' | \n|  | if [[ -n \"$calls\" ]]; then | \n|  | local content_field=\"null\" | \n|  | [[ -n \"$text\" ]] && content_field='\"'\"$(util_json_escape \"$text\")\"'\"' | \n|  | conv_append '{\"role\":\"assistant\",\"content\":'\"$content_field\"',\"tool_calls\":['\"$calls\"']'\"$reasoning_field\"'}' | \n|  | printf '%s' \"$tool_messages\" >> \"$CONV_FILE\" | \n|  | [[ \"$stop\" == \"tool_calls\" ]] && continue | \n|  | else | \n|  | [[ -n \"$text$reasoning\" ]] && conv_append '{\"role\":\"assistant\",\"content\":\"'\"$(util_json_escape \"$text\")\"'\"'\"$reasoning_field\"'}' | \n|  | fi | \n|  | break | \n|  | done | \n|  | trap - INT | \n|  | if (( turn >= MAX_TURNS )); then | \n|  | display error \"Max turns ($MAX_TURNS) reached\" | \n|  | return 1 | \n|  | fi | \n|  | [[ \"$stop\" == error ]] && return 1 | \n|  | return 0 | \n|  | } | \n|  |  | \n|  | # ================= interactive ================= | \n|  | interactive_mode() { | \n|  | local line | \n|  | printf '\\033[36magent.sh (%s) — type exit or Ctrl+D to quit\\033[0m\\n' \"$MODEL\" | \n|  | while true; do | \n|  | if ! IFS= read -e -r -p $'\\033[32m550S>\\033[0m ' line; then printf '\\n'; break; fi | \n|  | [[ \"$line\" == \"exit\" \\|\\| \"$line\" == \"quit\" ]] && break | \n|  | [[ -z \"$line\" ]] && continue | \n|  | history -s \"$line\" 2>/dev/null \\|\\| true | \n|  | agent_loop \"$line\" | \n|  | display_ensure_newline | \n|  | done | \n|  | printf '\\033[36mGoodbye!\\033[0m\\n' | \n|  | } | \n|  |  | \n|  | # ================= cli ================= | \n|  | usage() { | \n|  | cat <<'EOF' | \n|  | agent.sh — filesystem-first AI agent (OpenAI Chat Completions) | \n|  |  | \n|  | Usage: | \n|  | agent.sh [prompt]          chat; no prompt + tty opens interactive REPL | \n|  | agent.sh < file            read prompt from stdin | \n|  | fresh conversation: mv -i .agents/conv.jsonl \".agents/conv-$(date +%Y%m%d-%H%M%S).jsonl\" | \n|  |  | \n|  | Missing AGENTS.md, tools directory, and history file are created on startup. | \n|  |  | \n|  | Options: | \n|  | -m, --model NAME   model override (env MODEL) | \n|  | -h, --help         this help | \n|  |  | \n|  | Environment: | \n|  | OPENAI_API_KEY     required | \n|  | OPENAI_BASE_URL    default https://api.openai.com/v1 | \n|  | MODEL              default gpt-5.6-luna | \n|  | EOF | \n|  | } | \n|  |  | \n|  | parse_args() { | \n|  | while (( $# > 0 )); do | \n|  | case \"$1\" in | \n|  | -m\\|--model) [[ -n \"${2:-}\" ]] \\|\\| util_die \"$1 requires a model\"; MODEL=\"$2\"; shift 2 ;; | \n|  | -h\\|--help) usage; exit 0 ;; | \n|  | -*) util_die \"unknown option: $1 (see --help)\" ;; | \n|  | *) [[ -n \"$USER_INPUT\" ]] && util_die \"unexpected extra argument: $1\" | \n|  | USER_INPUT=\"$1\"; shift ;; | \n|  | esac | \n|  | done | \n|  | return 0 | \n|  | } | \n|  |  | \n|  | main() { | \n|  | parse_args \"$@\" | \n|  | [[ -n \"$API_KEY\" ]] \\|\\| util_die \"OPENAI_API_KEY is not set\" | \n|  | mkdir -p \"$TOOLS_DIR\" && touch \"$CONV_FILE\" \\|\\| util_die \"Cannot initialize $AGENT_DIR\" | \n|  | if [[ ! -e \"$PWD/AGENTS.md\" ]]; then | \n|  | printf '# Agent instructions\\n\\n' > \"$PWD/AGENTS.md\" \\|\\| util_die \"Cannot create AGENTS.md\" | \n|  | fi | \n|  | tools_load | \n|  | if [[ -z \"$USER_INPUT\" ]]; then | \n|  | if [[ -t 0 ]]; then interactive_mode; return $?; fi | \n|  | USER_INPUT=$(cat) | \n|  | fi | \n|  | agent_loop \"$USER_INPUT\" | \n|  | local rc=$? | \n|  | display_ensure_newline | \n|  | return \"$rc\" | \n|  | } | \n|  |  | \n|  | main \"$@\" |", "url": "https://wpnews.pro/news/550s-the-wandering-earth-has-550w-your-terminal-gets-550-lines-of-shell-powered", "canonical_source": "https://gist.github.com/lloydzhou/c675546c6395fb4ac4365c2f111f503e", "published_at": "2026-09-23 05:39:11+00:00", "updated_at": "2026-09-25 14:31:59.136742+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools", "large-language-models"], "entities": ["GitHub", "OpenAI", "lloydzhou"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/550s-the-wandering-earth-has-550w-your-terminal-gets-550-lines-of-shell-powered", "markdown": "https://wpnews.pro/news/550s-the-wandering-earth-has-550w-your-terminal-gets-550-lines-of-shell-powered.md", "text": "https://wpnews.pro/news/550s-the-wandering-earth-has-550w-your-terminal-gets-550-lines-of-shell-powered.txt", "jsonld": "https://wpnews.pro/news/550s-the-wandering-earth-has-550w-your-terminal-gets-550-lines-of-shell-powered.jsonld"}}