#!/usr/bin/env bash # ask-agy.sh — Claude -> Google Antigravity CLI (`agy`) router (independent second model). # # Sibling of ask-grok.sh. Routes a task to the Antigravity CLI (`agy`, a native Go # binary at ~/AppData/Local/agy/bin/agy) for an independent, different-vendor second # opinion, verification, code review, vision, or live web search. Multi-model backend # (Gemini 3.5 Flash / Gemini 3.1 Pro / Claude / GPT-OSS) selectable via --model. # # REPLACES the old `gemini` npm CLI (ask-gemini.sh), which broke on this account with # a Cloud-project eligibility error (throwIneligibleOrProjectIdError). `agy` uses # Antigravity's own auth (~/.gemini/antigravity-cli/), no GOOGLE_CLOUD_PROJECT needed. # # Authoritative flag reference (from `agy --help`, Go flag package — 2026-07-02, v1.0.16): # -p | --print | --prompt Run ONE prompt non-interactively and print the response. # STRING flag: the prompt is its VALUE. MUST be LAST on the # line (a bare `-p --model` makes "--model" the prompt). # --model "" Model for the session. Friendly names from `agy models`, # e.g. "Gemini 3.1 Pro (High)", "Gemini 3.5 Flash (Medium)", # "Claude Opus 4.6 (Thinking)". Omit -> CLI default (Flash). # --add-dir Add a directory to the workspace (repeatable). Needed so # the agent's file tools can read a review/vision target. # --dangerously-skip-permissions Auto-approve tool calls (else print mode can HANG # on an approval prompt until --print-timeout). Headless-safe. # --print-timeout Print-mode wait timeout (default 5m0s). We also wrap in the # shell `timeout` as a hard belt-and-suspenders bound. # (There is NO JSON/structured-output flag — stdout is plain text/markdown.) # # Output contract: plain text on stdout. Tool-using turns (review/search/vision) may # emit a line or two of tool narration ("I will view the content of ...") before the # answer; we ask the model to suppress it but tolerate residual. No JSON parsing. # # Usage (unchanged from the old skill so callers/SKILL.md stay valid): # ask-agy.sh text "" # one-shot answer # ask-agy.sh text --prompt-file # long content # ask-agy.sh verify "" # adversarial check # ask-agy.sh verify --prompt-file # ask-agy.sh review [instructions] # agy reads + reviews one file # ask-agy.sh review-files [-i "instr"] [f2 ...] # review a SET together # ask-agy.sh review-diff [-C ] [-i "instr"] [-- ] # ask-agy.sh image-analyze ["question"] # vision: read image + describe # ask-agy.sh search "" # live web search + sources # ask-agy.sh raw # escape hatch # # Exit: 0 ok, 1 no result, 2 usage, 3 not installed here, 127 agy not found. set -uo pipefail SELF="ask-agy" # --- path conversion: native-Windows path for agy args (no-op off Windows) --- # agy is a native Windows binary; Git Bash hands it POSIX paths it cannot resolve. if command -v cygpath >/dev/null 2>&1; then winpath() { cygpath -w -- "$1" 2>/dev/null || printf '%s' "$1"; } else winpath() { printf '%s' "$1"; } fi # --- identity.json (per-machine, gitignored): is agy installed here? --- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)" PYBIN="$(command -v py 2>/dev/null || command -v python 2>/dev/null || command -v python3 2>/dev/null || true)" IDFILE="" [ -n "${CLAUDETOOLS_ROOT:-}" ] && [ -f "$CLAUDETOOLS_ROOT/.claude/identity.json" ] && IDFILE="$CLAUDETOOLS_ROOT/.claude/identity.json" [ -z "$IDFILE" ] && IDFILE="$(cd "$SCRIPT_DIR/../../.." 2>/dev/null && pwd)/identity.json" idagy() { # read field $1 from identity.json .agy (fallback .gemini), empty if absent [ -f "$IDFILE" ] || { echo ""; return; } [ -n "$PYBIN" ] || { echo ""; return; } "$PYBIN" -c "import json,sys try: d=json.load(sys.stdin); g=(d.get('agy') or d.get('gemini') or {}); v=g.get('$1','') print('' if v is None else (str(v).lower() if isinstance(v,bool) else v)) except Exception: print('')" < "$IDFILE" } if [ "$(idagy installed)" = "false" ]; then echo "[$SELF] agy is not installed on this machine (identity.json agy.installed=false)." >&2 echo "[$SELF] Antigravity CLI runs only on the fleet host. Route this request there, or install agy + set identity.json agy.installed=true." >&2 exit 3 fi # --- locate the agy binary: AGY env > identity.json agy.binary > PATH > known paths --- AGY="${AGY:-}" if [ -n "$AGY" ] && [ ! -x "$AGY" ] && ! command -v "$AGY" >/dev/null 2>&1; then echo "[$SELF] AGY='$AGY' is not an executable agy binary." >&2; exit 127 fi cand="$(idagy binary)" [ -z "$AGY" ] && [ -n "$cand" ] && [ -x "$cand" ] && AGY="$cand" if [ -z "$AGY" ]; then if command -v agy >/dev/null 2>&1; then AGY="$(command -v agy)"; else for c in "/c/Users/${USERNAME:-${USER:-x}}/AppData/Local/agy/bin/agy" \ "$HOME/AppData/Local/agy/bin/agy" "$LOCALAPPDATA/agy/bin/agy" \ "/usr/local/bin/agy" "$HOME/.local/bin/agy"; do [ -n "$c" ] && [ -x "$c" ] && { AGY="$c"; break; } done fi fi [ -z "$AGY" ] && { echo "[$SELF] agy CLI not found (set identity.json agy.binary, AGY=, or install the Antigravity CLI)" >&2; exit 127; } # Strong model for verify/review*/vision/search; text uses the CLI default (Flash). STRONG_MODEL="${AGY_MODEL:-Gemini 3.1 Pro (High)}" MODE="${1:-}"; shift 2>/dev/null || true [ -z "$MODE" ] && { echo "usage: $SELF {text|verify|review|review-files|review-diff|image-analyze|search|raw} ..." >&2; exit 2; } TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT PF="$TMP/prompt.txt"; OUT="$TMP/out.txt"; ERR="$TMP/err.txt" REPO_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd)}" _logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "agy" "$@" >/dev/null 2>&1 || true; } TIMEOUT_CMD="timeout" if [[ "${OSTYPE:-}" == "darwin"* ]]; then TIMEOUT_CMD="$(command -v gtimeout 2>/dev/null || echo timeout)" fi # run agy headless. $1 = hard timeout secs; remaining args = flags placed BEFORE -p. # The prompt (from $PF) is always the LAST token, as the value of -p. agy has no JSON # mode, so stdout is the answer (plain text); stderr kept separate for diagnostics. LAST_FLAGS=() run_agy() { local to="$1"; shift LAST_FLAGS=("$@") "$TIMEOUT_CMD" "$to" "$AGY" "$@" --print-timeout "${to}s" -p "$(cat "$PF")" \ >"$OUT" 2>"$ERR" &2 sleep $((tries*3)) run_agy 240 "${LAST_FLAGS[@]}" txt="$(cat "$OUT")" done if [ -n "${txt//[[:space:]]/}" ]; then cat "$OUT"; return 0; fi if grep -qiE 'not authenticated|please (log|sign).?in|auth(entication)? (failed|error|required)|token (has )?expired|unauthorized' "$ERR" 2>/dev/null; then echo "[$SELF] agy auth error - run 'agy' interactively once to sign in, then retry." >&2 _logerr "agy auth/login failure" --context "mode=$MODE" else echo "[$SELF] no response from agy after $max attempts. stderr tail:" >&2 tail -3 "$ERR" >&2 2>/dev/null || true _logerr "agy returned no response (empty after $max attempts)" --context "mode=$MODE err=$(tail -1 "$ERR" 2>/dev/null | tr -d '\n' | cut -c1-80)" fi exit 1 } # Copy review/vision targets into an included workspace dir so agy's file tools can # reach them regardless of location/spaces; --add-dir this dir. INCLUDE_DIR="$TMP/inbox" prep_includes() { mkdir -p "$INCLUDE_DIR"; } NO_TOOLS='Do not use any tools; answer directly in text.' NO_NARRATE='Do not narrate your tool use or steps; output only the final answer.' case "$MODE" in text|verify) SRC="" if [ "${1:-}" = "--prompt-file" ]; then [ -f "${2:-}" ] || { echo "[$SELF] prompt file not found: ${2:-}" >&2; exit 2; } SRC="$(cat "$2")" else SRC="${1:-}" fi [ -z "$SRC" ] && { echo "usage: $SELF $MODE \"\" | $SELF $MODE --prompt-file " >&2; exit 2; } if [ "$MODE" = "verify" ]; then printf 'You are an adversarial reviewer giving an independent second opinion. Evaluate the following claim/finding/document: try hard to find any way it is WRONG, incomplete, unsupported, or overstated. Then give a clear VERDICT (correct / partly correct / incorrect) plus specific justification. %s\n\nContent:\n%s' "$NO_TOOLS" "$SRC" > "$PF" run_agy 240 --dangerously-skip-permissions --model "$STRONG_MODEL" else printf '%s\n\n%s' "$NO_TOOLS" "$SRC" > "$PF" run_agy 240 --dangerously-skip-permissions fi emit_or_fail ;; review|file) [ -z "${1:-}" ] && { echo "usage: $SELF review [instructions]" >&2; exit 2; } target="$1" instr="${2:-Give an independent, critical review of this file: accuracy, gaps/omissions, bugs, and concrete improvements. Be specific.}" if [ -f "$target" ]; then resolved="$target" elif [ -f "$REPO_ROOT/$target" ]; then resolved="$REPO_ROOT/$target" else echo "[$SELF] file not found: $target" >&2; exit 2; fi prep_includes base="$(basename "$resolved")"; cp -f "$resolved" "$INCLUDE_DIR/$base" tgt_win="$(winpath "$INCLUDE_DIR/$base")"; inc_win="$(winpath "$INCLUDE_DIR")" printf 'Read the file at this absolute path, then perform the task and stop. Do not modify anything. %s\nPath: %s\n\nTask: %s' "$NO_NARRATE" "$tgt_win" "$instr" > "$PF" run_agy 300 --dangerously-skip-permissions --model "$STRONG_MODEL" --add-dir "$inc_win" emit_or_fail ;; review-files) instr='Independently review these files together as a unit: correctness/bugs, gaps, cross-file consistency, and concrete improvements. Be specific and cite file:line.' files=() while [ $# -gt 0 ]; do case "$1" in -i|--instr) instr="${2:-}"; shift 2 2>/dev/null || shift ;; *) files+=("$1"); shift ;; esac done [ ${#files[@]} -eq 0 ] && { echo "usage: $SELF review-files [-i \"instructions\"] [file ...]" >&2; exit 2; } prep_includes list=""; declare -A seen=() for f in "${files[@]}"; do if [ -f "$f" ]; then r="$f" elif [ -f "$REPO_ROOT/$f" ]; then r="$REPO_ROOT/$f" else echo "[$SELF] file not found: $f" >&2; exit 2; fi base="$(basename "$r")" if [ -n "${seen[$base]:-}" ]; then n=1; while [ -e "$INCLUDE_DIR/${n}_${base}" ]; do n=$((n+1)); done; base="${n}_${base}" fi seen[$base]=1; cp -f "$r" "$INCLUDE_DIR/$base" list+="- $(winpath "$INCLUDE_DIR/$base") " done inc_win="$(winpath "$INCLUDE_DIR")" printf 'Read EACH of these files (absolute paths), then perform the task across ALL of them and stop. Do not modify anything. %s\n\nFiles:\n%s\nTask: %s' "$NO_NARRATE" "$list" "$instr" > "$PF" run_agy 360 --dangerously-skip-permissions --model "$STRONG_MODEL" --add-dir "$inc_win" emit_or_fail ;; review-diff) gdir="$REPO_ROOT" instr='Review this git diff: correctness/bugs introduced, regressions, missing edge cases, and concrete fixes. Focus on the CHANGES. Be specific and cite file:line.' ref=""; pathspec=() while [ $# -gt 0 ]; do case "$1" in -C|--dir) gdir="${2:-}"; shift 2 2>/dev/null || shift ;; -i|--instr) instr="${2:-}"; shift 2 2>/dev/null || shift ;; --) shift; while [ $# -gt 0 ]; do pathspec+=("$1"); shift; done ;; *) if [ -z "$ref" ]; then ref="$1"; else pathspec+=("$1"); fi; shift ;; esac done [ -z "$ref" ] && { echo "usage: $SELF review-diff [-C ] [-i \"instr\"] [-- ]" >&2; exit 2; } [ -d "$gdir" ] || { [ -d "$REPO_ROOT/$gdir" ] && gdir="$REPO_ROOT/$gdir"; } git -C "$gdir" rev-parse --git-dir >/dev/null 2>&1 || { echo "[$SELF] not a git repo: $gdir" >&2; exit 2; } if [ ${#pathspec[@]} -gt 0 ]; then git -C "$gdir" diff "$ref" -- "${pathspec[@]}" > "$TMP/diff.txt" 2>"$TMP/differr.txt" else git -C "$gdir" diff "$ref" > "$TMP/diff.txt" 2>"$TMP/differr.txt" fi [ -s "$TMP/diff.txt" ] || { echo "[$SELF] empty/failed diff for '$ref' in $gdir: $(head -1 "$TMP/differr.txt" 2>/dev/null)" >&2; exit 1; } gdir_win="$(winpath "$gdir")" { printf 'Review the following unified git diff. %s\n%s\nYou may read any changed file for full context (paths are relative to %s; strip a/ b/ prefixes). Do not modify anything.\n\n=== BEGIN DIFF ===\n' "$instr" "$NO_NARRATE" "$gdir_win"; cat "$TMP/diff.txt"; printf '\n=== END DIFF ===\n'; } > "$PF" run_agy 360 --dangerously-skip-permissions --model "$STRONG_MODEL" --add-dir "$gdir_win" emit_or_fail ;; image-analyze|image|vision) [ -z "${1:-}" ] && { echo "usage: $SELF image-analyze [\"question\"]" >&2; exit 2; } target="$1"; question="${2:-Describe exactly what is in this image.}" if [ -f "$target" ]; then resolved="$target" elif [ -f "$REPO_ROOT/$target" ]; then resolved="$REPO_ROOT/$target" else echo "[$SELF] image not found: $target" >&2; exit 2; fi prep_includes base="$(basename "$resolved")"; cp -f "$resolved" "$INCLUDE_DIR/$base" img_win="$(winpath "$INCLUDE_DIR/$base")"; inc_win="$(winpath "$INCLUDE_DIR")" printf 'Read the image at this absolute path and describe exactly what you see. Report only what is actually present; do not guess or invent content. Then stop. %s\nImage path: %s\n\nQuestion: %s' "$NO_NARRATE" "$img_win" "$question" > "$PF" run_agy 300 --dangerously-skip-permissions --model "$STRONG_MODEL" --add-dir "$inc_win" emit_or_fail ;; search|websearch) SRC="" if [ "${1:-}" = "--prompt-file" ]; then [ -f "${2:-}" ] || { echo "[$SELF] prompt file not found: ${2:-}" >&2; exit 2; } SRC="$(cat "$2")" else SRC="${1:-}" fi [ -z "$SRC" ] && { echo "usage: $SELF search \"\" | $SELF search --prompt-file " >&2; exit 2; } printf 'Search the web for current, live information answering the following, then stop. Answer concisely and ALWAYS include the source URLs you used (a Sources list of full URLs). Do not fabricate URLs. %s\n\nQuery: %s' "$NO_NARRATE" "$SRC" > "$PF" run_agy 240 --dangerously-skip-permissions --model "$STRONG_MODEL" emit_or_fail ;; raw) "$AGY" "$@" ;; *) echo "[$SELF] unknown mode '$MODE' (use text|verify|review|review-files|review-diff|image-analyze|search|raw)" >&2; exit 2 ;; esac