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>
281 lines
15 KiB
Bash
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
|