Files
claudetools/.claude/skills/agy/scripts/ask-agy.sh
Mike Swanson 41c12a934f agy: rewire skill from dead gemini npm CLI to Antigravity 'agy' binary
The old Google gemini CLI stopped working on this account (throwIneligibleOrProjectIdError -
needs a GOOGLE_CLOUD_PROJECT the personal account can't supply). Replace it with the
Antigravity CLI (agy, native Go binary, own auth, no project ID).

- New scripts/ask-agy.sh: plain-text output (no JSON), -p prompt-last, --model friendly
  names (Gemini 3.1 Pro (High) default for verify/review*/vision/search), --add-dir for
  file/vision reads, --dangerously-skip-permissions to avoid print-mode approval hangs.
  All modes smoke-tested (text/verify/review/search).
- scripts/ask-gemini.sh: deprecated shim -> exec ask-agy.sh (back-compat).
- SKILL.md: rewired header/flags/models/auth/availability/reference to agy.
- ask-grok.sh: xsearch fallback now execs ask-agy.sh (was the dead gemini).
- identity.json (local, gitignored): agy block added, gemini marked retired.

Authoritative flags sourced from 'agy --help' (web docs are JS SPAs, unreadable by
fetch tools; grok fetch/search timed out).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 13:19:21 -07:00

281 lines
15 KiB
Bash

#!/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 "<name>" 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 <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 <dur> 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 "<prompt>" # one-shot answer
# ask-agy.sh text --prompt-file <path> # long content
# ask-agy.sh verify "<claim or finding to refute>" # adversarial check
# ask-agy.sh verify --prompt-file <path>
# ask-agy.sh review <file> [instructions] # agy reads + reviews one file
# ask-agy.sh review-files [-i "instr"] <f1> [f2 ...] # review a SET together
# ask-agy.sh review-diff [-C <repo-dir>] [-i "instr"] <gitref> [-- <pathspec>]
# ask-agy.sh image-analyze <image-path> ["question"] # vision: read image + describe
# ask-agy.sh search "<query>" # live web search + sources
# ask-agy.sh raw <agy args...> # 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" </dev/null || true
}
# agy occasionally returns an empty turn; retry a couple times with backoff, then fail.
emit_or_fail() {
local txt tries=0 max="${AGY_MAX_TRIES:-3}"
txt="$(sed -e 's/[[:space:]]*$//' "$OUT" | sed -e :a -e '/^\s*$/{$d;N;ba}')"
while [ -z "${txt//[[:space:]]/}" ] && [ "$tries" -lt $((max-1)) ] && [ ${#LAST_FLAGS[@]} -ge 0 ]; do
tries=$((tries+1))
echo "[$SELF] empty response - retry $tries/$((max-1)) (backoff ${tries}x3s)..." >&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 \"<prompt>\" | $SELF $MODE --prompt-file <path>" >&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 <file-path> [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> [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 <repo-dir>] [-i \"instr\"] <gitref> [-- <pathspec>]" >&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 <image-path> [\"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 \"<query>\" | $SELF search --prompt-file <path>" >&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