#!/usr/bin/env bash # kei-agent-cli — invoke a KeiSeiKit agent via an external LLM CLI backend. # # Two entry points (both route through this script): # # kei run-via "" # explicit backend # kei agent "" # backend resolved from DNA: # # 1. --on= flag # # 2. agent manifest's `provider` # # 3. ~/.claude/config/primary.toml # # 4. fallback: claude # # Other forms: # kei run-via list # show backends + agents # kei agent --on= "" # override DNA backend # kei primary # print current primary # kei primary # set primary provider # kei run-via --help # # Backends (SSoT: _primitives/cli-backends.toml): # claude grok agy copilot kimi codex # # Reads assembled prompt from ~/.claude/agents/.md. # Strips YAML frontmatter, composes with task, execs the CLI. # # Env overrides: # KEI_AGENTS_DIR agent .md dir (default: ~/.claude/agents) # KEI_MANIFESTS_DIR manifest .toml dir (default: ~/.claude/_manifests) # KEI_PRIMARY override primary backend (beats config file) # KEI_NATIVE_AGENT=1 prefer backend's native --agent flag (grok/claude) set -euo pipefail KEI_AGENTS_DIR="${KEI_AGENTS_DIR:-$HOME/.claude/agents}" KEI_MANIFESTS_DIR="${KEI_MANIFESTS_DIR:-$HOME/.claude/_manifests}" KEI_PRIMARY_CFG="${KEI_PRIMARY_CFG:-$HOME/.claude/config/primary.toml}" KEI_NATIVE_AGENT="${KEI_NATIVE_AGENT:-0}" usage() { sed -n '2,32p' "$0" | sed 's|^# \{0,1\}||'; } # ---- backend table (SSoT mirror; kept in sync with cli-backends.toml) ----- backend_bin() { case "$1" in claude) echo "claude" ;; grok) echo "grok" ;; agy|antigravity) echo "agy" ;; copilot) echo "copilot" ;; kimi) echo "kimi" ;; codex) echo "codex" ;; *) return 1 ;; esac } backend_supports_native_agent() { case "$1" in claude|grok) return 0 ;; *) return 1 ;; esac } # ---- DNA resolver: agent → preferred backend -------------------------------- # Reads `provider = "..."` line from the manifest TOML if present. manifest_provider() { local agent="$1" tomlf="$KEI_MANIFESTS_DIR/$1.toml" [ -f "$tomlf" ] || return 1 awk -F'=' ' /^provider[[:space:]]*=/ { gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2) gsub(/^"|"$/, "", $2) print $2; exit } ' "$tomlf" } # Reads primary from config file (or KEI_PRIMARY env override). config_primary() { if [ -n "${KEI_PRIMARY:-}" ]; then printf '%s\n' "$KEI_PRIMARY"; return 0 fi [ -f "$KEI_PRIMARY_CFG" ] || return 1 awk -F'=' ' /^provider[[:space:]]*=/ { gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2) gsub(/^"|"$/, "", $2) print $2; exit } ' "$KEI_PRIMARY_CFG" } # Resolution order: explicit --on= → manifest provider → primary → claude. resolve_backend() { local agent="$1" explicit="${2:-}" out="" if [ -n "$explicit" ]; then printf '%s\n' "$explicit"; return 0; fi out=$(manifest_provider "$agent" 2>/dev/null) || true if [ -n "$out" ]; then printf '%s\n' "$out"; return 0; fi out=$(config_primary 2>/dev/null) || true if [ -n "$out" ]; then printf '%s\n' "$out"; return 0; fi printf 'claude\n' } # ---- backend invocation --------------------------------------------------- backend_invoke() { local backend="$1" prompt="$2" agent_name="${3:-}" bin bin=$(backend_bin "$backend") || { printf '[kei-agent-cli] unknown backend: %s\n' "$backend" >&2 return 2 } command -v "$bin" >/dev/null 2>&1 || { printf '[kei-agent-cli] %s not on PATH. Install it or pick another backend.\n' "$bin" >&2 return 127 } # Native --agent path (grok/claude) — pass agent name + task directly. if [ "$KEI_NATIVE_AGENT" = "1" ] \ && [ -n "$agent_name" ] \ && backend_supports_native_agent "$backend"; then printf '[kei-agent-cli] %s --agent %s\n' "$bin" "$agent_name" >&2 exec "$bin" --agent "$agent_name" --print "${prompt##*TASK FOR THIS RUN:}" fi case "$backend" in claude) exec "$bin" -p "$prompt" ;; grok|agy|antigravity) exec "$bin" --print "$prompt" ;; copilot) exec "$bin" --prompt "$prompt" ;; kimi) # Kimi has NO one-shot print mode (smoke-tested 2026-05-26): bare `kimi` # opens an interactive TUI that ignores piped stdin and exits with "Bye!". # For headless invocation we'd need an ACP client (`kimi acp` is a JSON-RPC # stdio server). Until KeiSeiKit ships that client, dump the composed # prompt to a tmpfile and open the TUI so the user can paste it in. tmp=$(mktemp -t kei-agent-kimi.XXXX.md) printf '%s\n' "$prompt" > "$tmp" printf '[kei-agent-cli] kimi non-interactive is unsupported (TUI only).\n' >&2 printf '[kei-agent-cli] composed prompt saved: %s\n' "$tmp" >&2 printf '[kei-agent-cli] copy-paste it into Kimi after the TUI opens.\n' >&2 printf '[kei-agent-cli] (or pipe via `kimi acp` if you have an ACP client.)\n' >&2 exec "$bin" ;; codex) exec "$bin" -p "$prompt" ;; esac } # ---- agent loader ------------------------------------------------------- load_agent() { local name="$1" path case "$name" in --file=*) path="${name#--file=}" ;; /*|./*|*/*) path="$name" ;; *) path="$KEI_AGENTS_DIR/$name.md" ;; esac if [ ! -f "$path" ]; then printf '[kei-agent-cli] agent not found: %s\n' "$path" >&2 if [ -d "$KEI_AGENTS_DIR" ]; then printf ' Available (%s): %s\n' "$KEI_AGENTS_DIR" \ "$(find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' 2>/dev/null \ | xargs -n1 basename 2>/dev/null | sed 's/\.md$//' \ | sort | head -8 | tr '\n' ' ')..." >&2 fi return 1 fi awk ' BEGIN { in_fm=0 } NR==1 && /^---$/ { in_fm=1; next } in_fm && /^---$/ { in_fm=0; next } in_fm { next } { print } ' "$path" } # ---- primary subcommand ------------------------------------------------ handle_primary() { local arg="${1:-}" if [ -z "$arg" ]; then cur=$(config_primary 2>/dev/null || true) printf 'primary provider: %s\n' "${cur:-claude (default fallback)}" [ -f "$KEI_PRIMARY_CFG" ] && printf 'config: %s\n' "$KEI_PRIMARY_CFG" return 0 fi backend_bin "$arg" >/dev/null || { printf '[kei-primary] unknown backend: %s\n' "$arg" >&2 printf 'valid: claude grok agy copilot kimi codex\n' >&2 return 2 } mkdir -p "$(dirname "$KEI_PRIMARY_CFG")" printf '# kei primary — written %s\nprovider = "%s"\n' \ "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$arg" > "$KEI_PRIMARY_CFG" printf 'primary provider set: %s → %s\n' "$arg" "$KEI_PRIMARY_CFG" } # ---- subcommands -------------------------------------------------------- case "${1:-}" in ""|-h|--help|help) usage; exit 0 ;; list|--list) printf 'Backends (✓ installed, ✗ missing):\n' for b in claude grok agy copilot kimi codex; do bin=$(backend_bin "$b") if p=$(command -v "$bin" 2>/dev/null); then printf ' %-10s ✓ %s\n' "$b" "$p" else printf ' %-10s ✗ (not on PATH)\n' "$b" fi done cur=$(config_primary 2>/dev/null || true) printf '\nprimary: %s\n' "${cur:-claude (default)}" printf '\nAgents (%s):\n' "$KEI_AGENTS_DIR" if [ -d "$KEI_AGENTS_DIR" ]; then find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' 2>/dev/null \ | xargs -n1 basename 2>/dev/null | sed 's/\.md$/ /' | sort | column 2>/dev/null \ || (find "$KEI_AGENTS_DIR" -maxdepth 1 -name '*.md' -not -name '_*' \ | xargs -n1 basename | sed 's/\.md$//' | sort | head -20) fi exit 0 ;; primary) shift handle_primary "${1:-}" exit $? ;; agent) # Direct-invocation passthrough: `kei-agent-cli.sh agent "task"` # behaves identically to `kei-agent-cli.sh "task"` (DNA mode). # Lets users call either form without surprise. shift ;; esac # ---- main: DNA mode (no leading backend) OR explicit run-via ------------ # Detect call shape: # "$1" is a known backend → run-via flow (kei run-via "task") # "$1" starts with --on= → DNA flow with override # "$1" is anything else → DNA flow (kei agent "task") EXPLICIT_BACKEND="" case "${1:-}" in --on=*) EXPLICIT_BACKEND="${1#--on=}" shift ;; *) if [ $# -ge 1 ] && backend_bin "$1" >/dev/null 2>&1; then EXPLICIT_BACKEND="$1" shift fi ;; esac if [ $# -lt 2 ]; then usage exit 2 fi AGENT_REF="$1"; shift TASK="$*" AGENT_NAME=$(basename "${AGENT_REF#--file=}") AGENT_NAME="${AGENT_NAME%.md}" BACKEND=$(resolve_backend "$AGENT_NAME" "$EXPLICIT_BACKEND") if ! AGENT_PROMPT=$(load_agent "$AGENT_REF"); then exit 1 fi COMPOSED=$(printf '%s\n\n---\n\nTASK FOR THIS RUN:\n%s\n' "$AGENT_PROMPT" "$TASK") printf '[kei-agent-cli] agent=%s backend=%s (via %s)\n' \ "$AGENT_NAME" "$BACKEND" \ "$([ -n "$EXPLICIT_BACKEND" ] && echo explicit \ || ([ -n "$(manifest_provider "$AGENT_NAME" 2>/dev/null)" ] && echo manifest \ || ([ -n "$(config_primary 2>/dev/null)" ] && echo primary || echo default)))" >&2 backend_invoke "$BACKEND" "$COMPOSED" "$AGENT_NAME"