#!/bin/sh

# cheesedanish: a small coding-agent harness for BusyBox systems.
# Runtime: BusyBox userland, awk, and curl.

set -u

CHEESEDANISH_VERSION="0.2.0"
DEFAULT_MODEL="deepseek/deepseek-v4-pro"
OPENROUTER_ENDPOINT="${CHEESEDANISH_OPENROUTER_ENDPOINT:-https://openrouter.ai/api/v1/chat/completions}"
OPENROUTER_KEY_ENDPOINT="${CHEESEDANISH_OPENROUTER_KEY_ENDPOINT:-https://openrouter.ai/api/v1/key}"
CURL_BIN="${CHEESEDANISH_CURL:-curl}"
MAX_TOOL_ROUNDS="${CHEESEDANISH_MAX_TOOL_ROUNDS:-20}"
MAX_TOOL_OUTPUT="${CHEESEDANISH_MAX_TOOL_OUTPUT:-50000}"
TOOL_TIMEOUT="${CHEESEDANISH_TOOL_TIMEOUT:-120}"
API_TIMEOUT="${CHEESEDANISH_API_TIMEOUT:-300}"
API_RETRIES="${CHEESEDANISH_API_RETRIES:-2}"

PROGRAM_PATH=$0
case "$PROGRAM_PATH" in
    /*) PROGRAM_DIR=$(dirname "$PROGRAM_PATH") ;;
    *) PROGRAM_DIR=$(cd "$(dirname "$PROGRAM_PATH")" 2>/dev/null && pwd) ;;
esac

if [ -f "$PROGRAM_DIR/lib/json.awk" ]; then
    LIB_DIR="$PROGRAM_DIR/lib"
elif [ -f "$PROGRAM_DIR/../lib/cheesedanish/json.awk" ]; then
    LIB_DIR=$(cd "$PROGRAM_DIR/../lib/cheesedanish" 2>/dev/null && pwd)
elif [ -n "${CHEESEDANISH_LIBDIR:-}" ] && [ -f "$CHEESEDANISH_LIBDIR/json.awk" ]; then
    LIB_DIR=$CHEESEDANISH_LIBDIR
else
    printf '%s\n' "cheesedanish: cannot find json.awk" >&2
    exit 1
fi

JSON_AWK="$LIB_DIR/json.awk"
QUOTE_AWK="$LIB_DIR/quote-bytes.awk"
REPLACE_AWK="$LIB_DIR/replace.awk"

RELEASE_URL=${CHEESEDANISH_RELEASE_URL:-}
if [ -z "$RELEASE_URL" ] && [ -s "$LIB_DIR/release-url" ]; then
    IFS= read -r RELEASE_URL < "$LIB_DIR/release-url"
fi
RELEASE_URL=${RELEASE_URL:-https://cheesedanish.nickmeslovich.com}
RELEASE_URL=${RELEASE_URL%/}

CONFIG_DIR="${CHEESEDANISH_HOME:-${HOME:-.}/.cheesedanish}"
AUTH_FILE="$CONFIG_DIR/auth.json"
MODEL_FILE="$CONFIG_DIR/model"
SESSIONS_DIR="$CONFIG_DIR/sessions"
WORKING_DIR=$(pwd)

TEMP_BASE=${TMPDIR:-/tmp}
TEMP_DIR=$(mktemp -d "$TEMP_BASE/cheesedanish.XXXXXX") || exit 1
cleanup() {
    rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
umask 077

mkdir -p "$CONFIG_DIR" "$SESSIONS_DIR" || exit 1

die() {
    printf 'cheesedanish: %s\n' "$*" >&2
    exit 1
}

warn() {
    printf 'cheesedanish: %s\n' "$*" >&2
}

need_command() {
    command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"
}

need_command awk
need_command base64
need_command od
need_command sha256sum
need_command "$CURL_BIN"

update_command() {
    local install_script update_prefix
    case "$RELEASE_URL" in
        http://*|https://*) ;;
        *) die "release URL must start with http:// or https://" ;;
    esac

    if [ -n "${CHEESEDANISH_INSTALL_PREFIX:-}" ]; then
        update_prefix=$CHEESEDANISH_INSTALL_PREFIX
    elif [ -f "$PROGRAM_DIR/../lib/cheesedanish/json.awk" ]; then
        update_prefix=$(cd "$PROGRAM_DIR/.." 2>/dev/null && pwd) || die "cannot determine install prefix"
    else
        update_prefix=${PREFIX:-${HOME:?HOME is not set}/.local}
    fi

    install_script="$TEMP_DIR/install.sh"
    printf 'Updating cheesedanish from %s\n' "$RELEASE_URL"
    "$CURL_BIN" --fail --silent --show-error --location \
        --connect-timeout 30 --max-time "$API_TIMEOUT" \
        --output "$install_script" "$RELEASE_URL/install.sh" \
        || die "could not download the installer"
    chmod 700 "$install_script"
    CHEESEDANISH_RELEASE_URL="$RELEASE_URL" \
    CHEESEDANISH_CURL="$CURL_BIN" \
    PREFIX="$update_prefix" \
        sh "$install_script"
}

json_get() {
    awk -f "$JSON_AWK" -v op=get -v path="$2" "$1"
}

json_to_file() {
    awk -f "$JSON_AWK" -v op=json -v path="$2" "$1" > "$3"
}

json_kind() {
    awk -f "$JSON_AWK" -v op=kind -v path="$2" "$1"
}

json_length() {
    awk -f "$JSON_AWK" -v op=length -v path="$2" "$1"
}

quote_file() {
    od -An -v -tu1 "$1" | awk -f "$QUOTE_AWK"
}

quote_text() {
    printf '%s' "$1" > "$TEMP_DIR/quote-input"
    quote_file "$TEMP_DIR/quote-input"
}

base64_file() {
    base64 "$1" | tr -d '\n'
}

decode_base64() {
    printf '%s' "$1" | base64 -d > "$2"
}

append_user() {
    printf 'U|%s\n' "$(base64_file "$1")" >> "$SESSION_FILE"
}

append_assistant() {
    local content_encoded calls_encoded
    content_encoded=$(base64_file "$1")
    calls_encoded=""
    if [ -n "$2" ] && [ -s "$2" ]; then
        calls_encoded=$(base64_file "$2")
    fi
    printf 'A|%s|%s\n' "$content_encoded" "$calls_encoded" >> "$SESSION_FILE"
}

append_tool() {
    printf 'T|%s|%s|%s\n' \
        "$(base64_file "$1")" \
        "$(base64_file "$2")" \
        "$(base64_file "$3")" >> "$SESSION_FILE"
}

write_context_section() {
    local label path size
    label=$1
    path=$2
    [ -f "$path" ] || return 0
    size=$(wc -c < "$path" | tr -d ' ')
    printf '\n# Context: %s\n\n' "$label" >> "$SYSTEM_PROMPT_FILE"
    if [ "$size" -gt 24000 ]; then
        head -c 24000 "$path" >> "$SYSTEM_PROMPT_FILE"
        printf '\n[Context file truncated by cheesedanish.]\n' >> "$SYSTEM_PROMPT_FILE"
    else
        cat "$path" >> "$SYSTEM_PROMPT_FILE"
    fi
    printf '\n' >> "$SYSTEM_PROMPT_FILE"
}

build_system_prompt() {
    local current parent picked depth
    SYSTEM_PROMPT_FILE="$TEMP_DIR/system-prompt"
    cat > "$SYSTEM_PROMPT_FILE" <<EOF
You are an expert coding assistant operating inside cheesedanish, a small shell-based coding agent harness. You help the user by reading files, running shell commands, editing exact text, and writing files.

Available tools:
- read: Read a text file with line numbers. Use offset and limit for large files.
- write: Create or overwrite a file with exact text.
- edit: Replace one exact occurrence of old_text in a file.
- bash: Run a command with the system's /bin/sh.

Guidelines:
- Be concise.
- Show file paths clearly.
- Inspect relevant files before changing them.
- Use the tools to complete the request instead of only describing commands.
- This environment can be BusyBox and Alpine Linux. Use portable commands unless the project requires something else.
- Do not assume GNU-only command options are present.
- The current working directory is $WORKING_DIR.
- The current date is $(date +%Y-%m-%d).
EOF

    write_context_section "global AGENTS.md" "$CONFIG_DIR/AGENTS.md"

    current=$WORKING_DIR
    depth=0
    while [ "$depth" -lt 32 ]; do
        picked=""
        if [ -f "$current/AGENTS.override.md" ]; then
            picked="$current/AGENTS.override.md"
        elif [ -f "$current/AGENTS.md" ]; then
            picked="$current/AGENTS.md"
        elif [ -f "$current/CLAUDE.md" ]; then
            picked="$current/CLAUDE.md"
        fi
        if [ -n "$picked" ]; then
            write_context_section "$picked" "$picked"
        fi
        [ "$current" = "/" ] && break
        parent=$(dirname "$current")
        [ "$parent" = "$current" ] && break
        current=$parent
        depth=$((depth + 1))
    done
}

print_tools_json() {
    cat <<'EOF'
[
  {
    "type":"function",
    "function":{
      "name":"read",
      "description":"Read a text file and return numbered lines.",
      "parameters":{
        "type":"object",
        "properties":{
          "path":{"type":"string","description":"File path, relative to the working directory or absolute."},
          "offset":{"type":"integer","description":"First line to read, starting at 1."},
          "limit":{"type":"integer","description":"Maximum number of lines to return."}
        },
        "required":["path"]
      }
    }
  },
  {
    "type":"function",
    "function":{
      "name":"write",
      "description":"Create or overwrite a file with exact text.",
      "parameters":{
        "type":"object",
        "properties":{
          "path":{"type":"string","description":"File path, relative to the working directory or absolute."},
          "content":{"type":"string","description":"Complete file content."}
        },
        "required":["path","content"]
      }
    }
  },
  {
    "type":"function",
    "function":{
      "name":"edit",
      "description":"Replace one exact occurrence of old_text in a text file.",
      "parameters":{
        "type":"object",
        "properties":{
          "path":{"type":"string","description":"File path, relative to the working directory or absolute."},
          "old_text":{"type":"string","description":"Exact text that occurs once."},
          "new_text":{"type":"string","description":"Replacement text."}
        },
        "required":["path","old_text","new_text"]
      }
    }
  },
  {
    "type":"function",
    "function":{
      "name":"bash",
      "description":"Run a shell command in the working directory and return combined output and status.",
      "parameters":{
        "type":"object",
        "properties":{
          "command":{"type":"string","description":"Command for /bin/sh -c."}
        },
        "required":["command"]
      }
    }
  }
]
EOF
}

emit_session_messages() {
    local kind field1 field2 field3 first record_file calls_file id_file name_file
    first=1
    record_file="$TEMP_DIR/record-content"
    calls_file="$TEMP_DIR/record-calls"
    id_file="$TEMP_DIR/record-id"
    name_file="$TEMP_DIR/record-name"

    while IFS='|' read -r kind field1 field2 field3; do
        [ -n "$kind" ] || continue
        if [ "$first" -eq 0 ]; then
            printf ','
        fi
        first=0
        case "$kind" in
            U)
                decode_base64 "$field1" "$record_file" || return 1
                printf '{"role":"user","content":'
                quote_file "$record_file"
                printf '}'
                ;;
            A)
                decode_base64 "$field1" "$record_file" || return 1
                printf '{"role":"assistant","content":'
                quote_file "$record_file"
                if [ -n "$field2" ]; then
                    decode_base64 "$field2" "$calls_file" || return 1
                    printf ',"tool_calls":'
                    cat "$calls_file"
                fi
                printf '}'
                ;;
            T)
                decode_base64 "$field1" "$id_file" || return 1
                decode_base64 "$field2" "$name_file" || return 1
                decode_base64 "$field3" "$record_file" || return 1
                printf '{"role":"tool","tool_call_id":'
                quote_file "$id_file"
                printf ',"name":'
                quote_file "$name_file"
                printf ',"content":'
                quote_file "$record_file"
                printf '}'
                ;;
            *)
                warn "ignored invalid session record: $kind"
                first=1
                ;;
        esac
    done < "$SESSION_FILE"
}

build_request() {
    local request_file
    request_file=$1
    {
        printf '{"model":'
        quote_text "$MODEL"
        printf ',"messages":[{"role":"system","content":'
        quote_file "$SYSTEM_PROMPT_FILE"
        printf '}'
        if [ -s "$SESSION_FILE" ]; then
            printf ','
            emit_session_messages
        fi
        printf '],"tools":'
        print_tools_json
        printf ',"tool_choice":"auto","parallel_tool_calls":false}'
    } > "$request_file"
}

load_api_key() {
    API_KEY=""
    if [ -n "${API_KEY_OVERRIDE:-}" ]; then
        API_KEY=$API_KEY_OVERRIDE
        return 0
    fi
    if [ -f "$AUTH_FILE" ]; then
        API_KEY=$(json_get "$AUTH_FILE" /openrouter/key 2>/dev/null || true)
        if [ -z "$API_KEY" ]; then
            API_KEY=$(json_get "$AUTH_FILE" /openrouter 2>/dev/null || true)
        fi
    fi
    if [ -z "$API_KEY" ] && [ -n "${OPENROUTER_API_KEY:-}" ]; then
        API_KEY=$OPENROUTER_API_KEY
    fi
    [ -n "$API_KEY" ] || return 1
    case "$API_KEY" in
        *[!A-Za-z0-9._-]*)
            warn "OpenRouter API key contains an invalid character"
            API_KEY=""
            return 1
            ;;
    esac
    return 0
}

make_curl_config() {
    local destination key
    destination=$1
    key=${2:-$API_KEY}
    {
        printf 'header = "Authorization: Bearer %s"\n' "$key"
        printf 'header = "Content-Type: application/json"\n'
        printf 'header = "HTTP-Referer: https://github.com/cheesedanish/cheesedanish"\n'
        printf 'header = "X-OpenRouter-Title: cheesedanish"\n'
    } > "$destination"
    chmod 600 "$destination"
}

validate_api_key() {
    local key response curl_config status curl_status
    key=$1
    case "$key" in
        ''|*[!A-Za-z0-9._-]*)
            warn "OpenRouter API key has an invalid format"
            return 1
            ;;
    esac

    response="$TEMP_DIR/key-validation-response.json"
    curl_config="$TEMP_DIR/key-validation-curl.conf"
    make_curl_config "$curl_config" "$key"
    status=$(
        "$CURL_BIN" --silent --show-error \
            --connect-timeout 30 --max-time "$API_TIMEOUT" \
            --request GET --config "$curl_config" \
            --output "$response" --write-out '%{http_code}' \
            "$OPENROUTER_KEY_ENDPOINT"
    )
    curl_status=$?
    if [ "$curl_status" -ne 0 ]; then
        warn "could not verify the OpenRouter API key (curl status $curl_status); key was not saved"
        return 2
    fi
    case "$status" in
        200) return 0 ;;
        401)
            warn "OpenRouter rejected that API key; key was not saved"
            return 1
            ;;
        *)
            warn "could not verify the OpenRouter API key (HTTP $status); key was not saved"
            if [ -s "$response" ]; then
                json_get "$response" /error/message >&2 2>/dev/null || true
                printf '\n' >&2
            fi
            return 2
            ;;
    esac
}

api_request() {
    local request_file response_file curl_config attempt status curl_status
    request_file=$1
    response_file=$2
    curl_config="$TEMP_DIR/curl.conf"
    make_curl_config "$curl_config"

    attempt=0
    while [ "$attempt" -le "$API_RETRIES" ]; do
        status=$(
            "$CURL_BIN" --silent --show-error \
                --connect-timeout 30 --max-time "$API_TIMEOUT" \
                --request POST --config "$curl_config" \
                --output "$response_file" --write-out '%{http_code}' \
                --data-binary "@$request_file" "$OPENROUTER_ENDPOINT"
        )
        curl_status=$?
        if [ "$curl_status" -ne 0 ]; then
            warn "network request failed with curl status $curl_status"
        elif [ "$status" = "200" ]; then
            return 0
        elif [ "$status" = "429" ] || [ "$status" = "502" ] || [ "$status" = "503" ]; then
            warn "OpenRouter returned HTTP $status; retrying"
        elif [ "$status" = "401" ]; then
            warn "OpenRouter rejected the credential; run /login to replace the saved API key"
            if [ -s "$response_file" ]; then
                json_get "$response_file" /error/message >&2 2>/dev/null || true
                printf '\n' >&2
            fi
            return 1
        else
            warn "OpenRouter returned HTTP $status"
            if [ -s "$response_file" ]; then
                json_get "$response_file" /error/message >&2 2>/dev/null || true
                printf '\n' >&2
            fi
            return 1
        fi
        attempt=$((attempt + 1))
        [ "$attempt" -le "$API_RETRIES" ] && sleep "$attempt"
    done
    return 1
}

truncate_output() {
    local path bytes shortened
    path=$1
    bytes=$(wc -c < "$path" | tr -d ' ')
    if [ "$bytes" -gt "$MAX_TOOL_OUTPUT" ]; then
        shortened="$TEMP_DIR/truncated"
        head -c "$MAX_TOOL_OUTPUT" "$path" > "$shortened"
        printf '\n[Output truncated after %s bytes.]\n' "$MAX_TOOL_OUTPUT" >> "$shortened"
        mv "$shortened" "$path"
    fi
}

valid_positive_integer() {
    case "$1" in
        ''|*[!0-9]*) return 1 ;;
        *) [ "$1" -gt 0 ] ;;
    esac
}

tool_read() {
    local arguments result path offset limit end
    arguments=$1
    result=$2
    json_get "$arguments" /path > "$TEMP_DIR/path" 2>/dev/null || {
        printf 'read: missing path\n' > "$result"
        return 1
    }
    path=$(cat "$TEMP_DIR/path")
    [ -n "$path" ] || {
        printf 'read: path must not be empty\n' > "$result"
        return 1
    }
    case "$path" in -*) path="./$path" ;; esac
    offset=$(json_get "$arguments" /offset 2>/dev/null || true)
    limit=$(json_get "$arguments" /limit 2>/dev/null || true)
    valid_positive_integer "$offset" || offset=1
    valid_positive_integer "$limit" || limit=400
    end=$((offset + limit - 1))

    if [ ! -f "$path" ]; then
        printf 'read: file not found: %s\n' "$path" > "$result"
        return 1
    fi
    awk -v first="$offset" -v last="$end" '
        NR >= first && NR <= last { printf "%d: %s\n", NR, $0 }
        NR > last { exit }
    ' "$path" > "$result" 2>&1
    [ -s "$result" ] || printf '(file is empty or offset is past the end)\n' > "$result"
    truncate_output "$result"
}

tool_write() {
    local arguments result path directory
    arguments=$1
    result=$2
    json_get "$arguments" /path > "$TEMP_DIR/path" 2>/dev/null || {
        printf 'write: missing path\n' > "$result"
        return 1
    }
    json_get "$arguments" /content > "$TEMP_DIR/write-content" 2>/dev/null || {
        printf 'write: missing content\n' > "$result"
        return 1
    }
    path=$(cat "$TEMP_DIR/path")
    [ -n "$path" ] || {
        printf 'write: path must not be empty\n' > "$result"
        return 1
    }
    case "$path" in -*) path="./$path" ;; esac
    directory=$(dirname "$path")
    mkdir -p "$directory" 2> "$result" || return 1
    cp "$TEMP_DIR/write-content" "$path" 2> "$result" || return 1
    printf 'Wrote %s bytes to %s\n' "$(wc -c < "$path" | tr -d ' ')" "$path" > "$result"
}

tool_edit() {
    local arguments result path replacement status
    arguments=$1
    result=$2
    json_get "$arguments" /path > "$TEMP_DIR/path" 2>/dev/null || {
        printf 'edit: missing path\n' > "$result"
        return 1
    }
    json_get "$arguments" /old_text > "$TEMP_DIR/old-text" 2>/dev/null || {
        printf 'edit: missing old_text\n' > "$result"
        return 1
    }
    json_get "$arguments" /new_text > "$TEMP_DIR/new-text" 2>/dev/null || {
        printf 'edit: missing new_text\n' > "$result"
        return 1
    }
    path=$(cat "$TEMP_DIR/path")
    case "$path" in -*) path="./$path" ;; esac
    [ -f "$path" ] || {
        printf 'edit: file not found: %s\n' "$path" > "$result"
        return 1
    }
    replacement="$TEMP_DIR/replacement"
    awk -f "$REPLACE_AWK" "$path" "$TEMP_DIR/old-text" "$TEMP_DIR/new-text" "$replacement" 2> "$result"
    status=$?
    [ "$status" -eq 0 ] || return "$status"
    cp "$replacement" "$path" 2> "$result" || return 1
    printf 'Edited %s\n' "$path" > "$result"
}

tool_bash() {
    local arguments result command_text status
    arguments=$1
    result=$2
    json_get "$arguments" /command > "$TEMP_DIR/command" 2>/dev/null || {
        printf 'bash: missing command\n' > "$result"
        return 1
    }
    command_text=$(cat "$TEMP_DIR/command")
    if command -v timeout >/dev/null 2>&1; then
        timeout "$TOOL_TIMEOUT" sh -c "$command_text" > "$result" 2>&1
        status=$?
    else
        sh -c "$command_text" > "$result" 2>&1
        status=$?
    fi
    printf '\n[exit status: %s]\n' "$status" >> "$result"
    truncate_output "$result"
    return 0
}

run_tool() {
    local name arguments result
    name=$1
    arguments=$2
    result=$3
    case "$name" in
        read) tool_read "$arguments" "$result" ;;
        write) tool_write "$arguments" "$result" ;;
        edit) tool_edit "$arguments" "$result" ;;
        bash) tool_bash "$arguments" "$result" ;;
        *)
            printf 'Unknown tool: %s\n' "$name" > "$result"
            return 1
            ;;
    esac
}

agent_turn() {
    local prompt_file round request response content calls count index finish error_message
    local prefix id name arguments_kind result
    prompt_file=$1
    append_user "$prompt_file"

    if ! load_api_key; then
        warn "no OpenRouter credential; run /login or set OPENROUTER_API_KEY"
        return 1
    fi

    round=0
    while [ "$round" -lt "$MAX_TOOL_ROUNDS" ]; do
        request="$TEMP_DIR/request.json"
        response="$TEMP_DIR/response.json"
        build_request "$request" || return 1
        api_request "$request" "$response" || return 1

        if ! awk -f "$JSON_AWK" -v op=kind -v path=/ "$response" >/dev/null 2>&1; then
            warn "OpenRouter returned invalid JSON"
            return 1
        fi

        error_message=$(json_get "$response" /error/message 2>/dev/null || true)
        if [ -n "$error_message" ]; then
            warn "$error_message"
            return 1
        fi

        content="$TEMP_DIR/assistant-content"
        : > "$content"
        json_get "$response" /choices/0/message/content > "$content" 2>/dev/null || true
        finish=$(json_get "$response" /choices/0/finish_reason 2>/dev/null || true)
        count=$(json_length "$response" /choices/0/message/tool_calls 2>/dev/null || printf '0')
        valid_positive_integer "$count" || count=0

        calls=""
        if [ "$count" -gt 0 ]; then
            calls="$TEMP_DIR/tool-calls.json"
            json_to_file "$response" /choices/0/message/tool_calls "$calls" || return 1
        fi
        append_assistant "$content" "$calls"

        if [ -s "$content" ]; then
            cat "$content"
            printf '\n'
        fi

        if [ "$count" -eq 0 ]; then
            [ -n "$finish" ] || warn "response did not include a finish reason"
            return 0
        fi

        index=0
        while [ "$index" -lt "$count" ]; do
            prefix="/choices/0/message/tool_calls/$index"
            id="$TEMP_DIR/tool-id"
            name="$TEMP_DIR/tool-name"
            json_get "$response" "$prefix/id" > "$id" 2>/dev/null || printf 'call_%s' "$index" > "$id"
            json_get "$response" "$prefix/function/name" > "$name" 2>/dev/null || printf 'unknown' > "$name"

            arguments_kind=$(json_kind "$response" "$prefix/function/arguments" 2>/dev/null || true)
            if [ "$arguments_kind" = "string" ]; then
                json_get "$response" "$prefix/function/arguments" > "$TEMP_DIR/tool-arguments.json" 2>/dev/null || printf '{}' > "$TEMP_DIR/tool-arguments.json"
            elif [ "$arguments_kind" = "object" ]; then
                json_to_file "$response" "$prefix/function/arguments" "$TEMP_DIR/tool-arguments.json" || printf '{}' > "$TEMP_DIR/tool-arguments.json"
            else
                printf '{}' > "$TEMP_DIR/tool-arguments.json"
            fi

            result="$TEMP_DIR/tool-result"
            printf '→ %s\n' "$(cat "$name")" >&2
            run_tool "$(cat "$name")" "$TEMP_DIR/tool-arguments.json" "$result" || true
            append_tool "$id" "$name" "$result"
            index=$((index + 1))
        done
        round=$((round + 1))
    done

    warn "stopped after $MAX_TOOL_ROUNDS tool rounds"
    return 1
}

save_auth_key() {
    local key auth_temp
    key=$1
    auth_temp="$TEMP_DIR/auth.json"
    printf '{"openrouter":{"type":"api_key","key":' > "$auth_temp"
    quote_text "$key" >> "$auth_temp"
    printf '}}\n' >> "$auth_temp"
    chmod 600 "$auth_temp"
    cp "$auth_temp" "$AUTH_FILE"
    chmod 600 "$AUTH_FILE"
}

read_secret() {
    local secret_value
    if [ -t 0 ]; then
        printf 'API key: ' >&2
        stty -echo 2>/dev/null || true
        IFS= read -r secret_value
        stty echo 2>/dev/null || true
        printf '\n' >&2
    else
        IFS= read -r secret_value
    fi
    printf '%s' "$secret_value"
}

random_verifier() {
    local value
    value=$(od -An -N32 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n')
    if [ ${#value} -lt 43 ]; then
        value=$(printf '%s-%s-%s' "$(date +%s)" "$$" "${RANDOM:-0}" | sha256sum | sed 's/[[:space:]].*//')
    fi
    printf '%s' "$value"
}

pkce_challenge() {
    local verifier digest escaped
    verifier=$1
    digest=$(printf '%s' "$verifier" | sha256sum | sed 's/[[:space:]].*//')
    escaped=$(printf '%s' "$digest" | sed 's/../\\x&/g')
    printf '%b' "$escaped" | base64 | tr -d '=\n' | tr '+/' '-_'
}

openrouter_oauth_login() {
    local verifier challenge encoded_callback auth_url supplied code exchange_request exchange_response http_status key
    verifier=$(random_verifier)
    challenge=$(pkce_challenge "$verifier")
    encoded_callback='http%3A%2F%2Flocalhost%3A17777%2Fcallback'
    auth_url="https://openrouter.ai/auth?callback_url=$encoded_callback&code_challenge=$challenge&code_challenge_method=S256"

    printf '\nOpen this URL in Safari:\n\n%s\n\n' "$auth_url"
    printf 'After authorization, paste the final redirect URL or authorization code:\n> '
    IFS= read -r supplied
    case "$supplied" in
        *code=*)
            code=${supplied#*code=}
            code=${code%%&*}
            ;;
        *) code=$supplied ;;
    esac
    [ -n "$code" ] || {
        warn "login cancelled"
        return 1
    }

    exchange_request="$TEMP_DIR/exchange-request.json"
    exchange_response="$TEMP_DIR/exchange-response.json"
    {
        printf '{"code":'
        quote_text "$code"
        printf ',"code_verifier":'
        quote_text "$verifier"
        printf ',"code_challenge_method":"S256"}'
    } > "$exchange_request"

    http_status=$(
        "$CURL_BIN" --silent --show-error --connect-timeout 30 --max-time "$API_TIMEOUT" \
            --request POST --header 'Content-Type: application/json' \
            --output "$exchange_response" --write-out '%{http_code}' \
            --data-binary "@$exchange_request" https://openrouter.ai/api/v1/auth/keys
    ) || return 1
    if [ "$http_status" != "200" ]; then
        warn "OpenRouter login returned HTTP $http_status"
        json_get "$exchange_response" /error/message >&2 2>/dev/null || true
        return 1
    fi
    key=$(json_get "$exchange_response" /key 2>/dev/null || true)
    [ -n "$key" ] || {
        warn "OpenRouter did not return an API key"
        return 1
    }
    save_auth_key "$key"
    API_KEY=""
    printf 'Logged in to OpenRouter.\n'
}

login_command() {
    local provider method key validation_status
    provider=${1:-}
    if [ -z "$provider" ]; then
        printf 'Select provider:\n  1) OpenRouter\n> '
        IFS= read -r provider
    fi
    case "$provider" in
        1|openrouter|OpenRouter) ;;
        *)
            warn "only OpenRouter is implemented in this release"
            return 1
            ;;
    esac

    printf 'OpenRouter login:\n  1) Sign in with OpenRouter\n  2) Use an API key\n> '
    IFS= read -r method
    case "$method" in
        1|'') openrouter_oauth_login ;;
        2)
            while :; do
                key=$(read_secret)
                [ -n "$key" ] || {
                    warn "login cancelled; saved credential was not changed"
                    return 1
                }
                if validate_api_key "$key"; then
                    save_auth_key "$key"
                    key=""
                    printf 'Verified and saved OpenRouter API key.\n'
                    return 0
                else
                    validation_status=$?
                fi
                key=""
                [ "$validation_status" -eq 1 ] || return 1
                printf 'Try another API key, or press Enter to cancel.\n' >&2
            done
            ;;
        *) warn "login cancelled"; return 1 ;;
    esac
}

logout_command() {
    if [ -f "$AUTH_FILE" ]; then
        rm -f "$AUTH_FILE"
        printf 'Logged out of OpenRouter.\n'
    else
        printf 'No saved login.\n'
    fi
    API_KEY=""
}

save_model() {
    printf '%s\n' "$1" > "$MODEL_FILE"
    chmod 600 "$MODEL_FILE"
}

model_command() {
    local requested
    requested=${1:-}
    if [ -z "$requested" ]; then
        printf 'Current model: %s\nNew OpenRouter model ID: ' "$MODEL"
        IFS= read -r requested
    fi
    [ -n "$requested" ] || return 0
    case "$requested" in
        *[!A-Za-z0-9._:/@+-]*)
            warn "invalid model ID"
            return 1
            ;;
    esac
    MODEL=$requested
    save_model "$MODEL"
    printf 'Model: %s\n' "$MODEL"
}

new_session() {
    local stamp
    stamp=$(date +%Y%m%d-%H%M%S)
    SESSION_FILE="$SESSIONS_DIR/$stamp-$$.cds"
    : > "$SESSION_FILE"
    chmod 600 "$SESSION_FILE"
}

continue_session() {
    local latest
    latest=$(find "$SESSIONS_DIR" -type f -name '*.cds' -print 2>/dev/null | sort | tail -n 1)
    if [ -n "$latest" ]; then
        SESSION_FILE=$latest
    else
        new_session
    fi
}

print_help() {
    cat <<'EOF'
cheesedanish - BusyBox coding agent

Usage:
  cheesedanish                     Start an interactive session
  cheesedanish update              Install the latest release
  cheesedanish -c                  Continue the newest session
  cheesedanish -p "prompt"         Run one prompt and exit
  cheesedanish --model MODEL       Select an OpenRouter model

Interactive commands:
  /login [openrouter]  Sign in or save an API key
  /logout              Remove the saved credential
  /model [model-id]    Show or change the model
  /new                 Start a new session
  /clear               Clear the current session
  /session             Show the session file
  /reload              Reload AGENTS.md and CLAUDE.md files
  /help                Show this help
  /exit                Exit
  !command             Run a local shell command

Environment:
  OPENROUTER_API_KEY          Use an OpenRouter key without saving it
  CHEESEDANISH_HOME           Config directory (default: ~/.cheesedanish)
  CHEESEDANISH_RELEASE_URL    Override the package release site
  CHEESEDANISH_INSTALL_PREFIX Override the prefix used by update
  CHEESEDANISH_TOOL_TIMEOUT   Shell-tool timeout in seconds
EOF
}

run_local_command() {
    local command_text status
    command_text=$1
    sh -c "$command_text"
    status=$?
    printf '[exit status: %s]\n' "$status"
}

interactive_loop() {
    local line command argument
    printf 'cheesedanish %s\n' "$CHEESEDANISH_VERSION"
    printf 'model: %s\n' "$MODEL"
    printf 'working directory: %s\n' "$WORKING_DIR"
    printf 'Type /help for commands.\n\n'

    while :; do
        printf 'cheese> '
        if ! IFS= read -r line; then
            printf '\n'
            break
        fi
        [ -n "$line" ] || continue
        case "$line" in
            /exit|/quit) break ;;
            /help) print_help ;;
            /login*)
                argument=${line#\/login}
                argument=$(printf '%s' "$argument" | sed 's/^[[:space:]]*//')
                login_command "$argument"
                ;;
            /logout) logout_command ;;
            /model*)
                argument=${line#\/model}
                argument=$(printf '%s' "$argument" | sed 's/^[[:space:]]*//')
                model_command "$argument"
                ;;
            /new)
                new_session
                printf 'New session: %s\n' "$SESSION_FILE"
                ;;
            /clear)
                : > "$SESSION_FILE"
                printf 'Session cleared.\n'
                ;;
            /session) printf '%s\n' "$SESSION_FILE" ;;
            /reload)
                build_system_prompt
                printf 'Context reloaded.\n'
                ;;
            /*) warn "unknown command: $line" ;;
            !*)
                command=${line#!}
                run_local_command "$command"
                ;;
            *)
                printf '%s' "$line" > "$TEMP_DIR/user-prompt"
                agent_turn "$TEMP_DIR/user-prompt" || true
                ;;
        esac
    done
}

PRINT_PROMPT=""
CONTINUE=0
NO_SESSION=0
MODEL_OVERRIDE=""
API_KEY_OVERRIDE=""
UPDATE=0

while [ "$#" -gt 0 ]; do
    case "$1" in
        -p|--print)
            [ "$#" -ge 2 ] || die "$1 requires a prompt"
            PRINT_PROMPT=$2
            shift 2
            ;;
        -c|--continue)
            CONTINUE=1
            shift
            ;;
        --model)
            [ "$#" -ge 2 ] || die "--model requires a model ID"
            MODEL_OVERRIDE=$2
            shift 2
            ;;
        --api-key)
            [ "$#" -ge 2 ] || die "--api-key requires a value"
            API_KEY_OVERRIDE=$2
            shift 2
            ;;
        --no-session)
            NO_SESSION=1
            shift
            ;;
        update)
            UPDATE=1
            shift
            ;;
        --help|-h)
            print_help
            exit 0
            ;;
        --version|-v)
            printf 'cheesedanish %s\n' "$CHEESEDANISH_VERSION"
            exit 0
            ;;
        --)
            shift
            break
            ;;
        *) die "unknown option: $1" ;;
    esac
done

if [ "$UPDATE" -eq 1 ]; then
    [ "$#" -eq 0 ] || die "update does not accept additional arguments"
    update_command
    exit 0
fi

if [ -n "$MODEL_OVERRIDE" ]; then
    MODEL=$MODEL_OVERRIDE
elif [ -s "$MODEL_FILE" ]; then
    IFS= read -r MODEL < "$MODEL_FILE"
else
    MODEL=$DEFAULT_MODEL
fi

if [ "$NO_SESSION" -eq 1 ]; then
    SESSION_FILE="$TEMP_DIR/session.cds"
    : > "$SESSION_FILE"
elif [ "$CONTINUE" -eq 1 ]; then
    continue_session
else
    new_session
fi

build_system_prompt

if [ -n "$PRINT_PROMPT" ]; then
    printf '%s' "$PRINT_PROMPT" > "$TEMP_DIR/user-prompt"
    agent_turn "$TEMP_DIR/user-prompt"
else
    interactive_loop
fi
