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>
This commit is contained in:
@@ -1,45 +1,68 @@
|
||||
---
|
||||
name: agy
|
||||
description: "Route a task to the Google Gemini CLI for an independent second model (sibling of grok): different-vendor second opinion, adversarial verification, Gemini code review of files/diffs, one-shot text answers. Triggers: ask gemini, gemini verify, gemini review, agy."
|
||||
description: "Route a task to the Google Antigravity CLI (agy) for an independent second model (sibling of grok): different-vendor second opinion, adversarial verification, code review of files/diffs, vision, live web search, one-shot text answers. Triggers: ask gemini, ask agy, agy verify, agy review, agy."
|
||||
---
|
||||
|
||||
# AGY — Gemini capability router
|
||||
# AGY — Antigravity CLI capability router
|
||||
|
||||
Claude shells out to the locally-installed **Google Gemini CLI** (`gemini`, npm
|
||||
global, v0.45.2) for a genuinely independent, different-vendor second model.
|
||||
AGY is the sibling of [`grok`](../grok/SKILL.md): both are second-opinion /
|
||||
review routers. Use whichever you want a second model from (or both, to triangulate).
|
||||
Verified working on this machine (2026-06-05; re-validated 2026-06-17 against the
|
||||
CLI's bundled help/README — JSON schema, all flags, pinned model, and live search
|
||||
all confirmed): text, verify, review (single file / file set / git diff),
|
||||
image-analyze (vision input), search (live Google web search). All KEYLESS — they
|
||||
work on Google OAuth, no API key.
|
||||
Claude shells out to the locally-installed **Google Antigravity CLI** (`agy`, a
|
||||
native Go binary at `~/AppData/Local/agy/bin/agy`, v1.0.16) for a genuinely
|
||||
independent, different-vendor second model. AGY is the sibling of
|
||||
[`grok`](../grok/SKILL.md): both are second-opinion / review routers. Use whichever
|
||||
you want a second model from (or both, to triangulate). Modes verified on this
|
||||
machine (2026-07-02): text, verify, review (single file / file set / git diff),
|
||||
image-analyze (vision), search (live web search + source URLs).
|
||||
|
||||
**Auth:** Gemini uses **Google login (OAuth)** — **no API key**. Creds live at
|
||||
`~/.gemini/oauth_creds.json`. If calls fail with an auth error, run `gemini`
|
||||
interactively once and choose **"Login with Google"**, then retry.
|
||||
> **REWIRED 2026-07-02 — `agy` replaces the old `gemini` npm CLI.** The former
|
||||
> Google `gemini` CLI (npm `@google/gemini-cli`) stopped working on this account
|
||||
> with a Cloud-project eligibility error (`throwIneligibleOrProjectIdError` — it
|
||||
> demanded a `GOOGLE_CLOUD_PROJECT` the personal account couldn't provide). The
|
||||
> Antigravity CLI (`agy`) uses Antigravity's OWN auth and works headless with no
|
||||
> project ID. The wrapper is `scripts/ask-agy.sh`; `scripts/ask-gemini.sh` is kept
|
||||
> as a thin **deprecated shim** that `exec`s ask-agy.sh so old references still work.
|
||||
|
||||
**Backend models (multi-vendor, `--model`):** `agy models` lists them — Gemini 3.5
|
||||
Flash (Low/Medium/High), Gemini 3.1 Pro (Low/High), Claude Sonnet 4.6 (Thinking),
|
||||
Claude Opus 4.6 (Thinking), GPT-OSS 120B. `text` uses the CLI default (Flash, fast);
|
||||
`verify`/`review*`/`image-analyze`/`search` pin the strong model
|
||||
`Gemini 3.1 Pro (High)`. Override any mode with `AGY_MODEL="<friendly name>"`.
|
||||
|
||||
**Auth:** Antigravity's own login (`~/.gemini/antigravity-cli/`) — **no API key, no
|
||||
`GOOGLE_CLOUD_PROJECT`**. If a call fails with an auth error, run `agy` interactively
|
||||
once to sign in, then retry.
|
||||
|
||||
## The wrapper
|
||||
|
||||
```
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/skills/agy/scripts/ask-gemini.sh" <mode> ...
|
||||
bash "$CLAUDETOOLS_ROOT/.claude/skills/agy/scripts/ask-agy.sh" <mode> ...
|
||||
```
|
||||
|
||||
Authoritative headless flags (from `agy --help`, Go flag package — the web docs at
|
||||
antigravity.google/docs/cli are JS-rendered SPAs and unreadable by fetch tools, so
|
||||
`--help` is the source of truth): `-p/--print/--prompt` runs one prompt and prints
|
||||
the response (**the prompt is the flag's VALUE and MUST be last** — a bare `-p --model`
|
||||
makes "--model" the prompt); `--model "<name>"`; `--add-dir <dir>` (repeatable, gives
|
||||
the file tools access to a review/vision target); `--dangerously-skip-permissions`
|
||||
(auto-approve tool calls — without it print mode HANGS on an approval prompt);
|
||||
`--print-timeout <dur>` (default 5m). There is **no JSON output flag** — stdout is
|
||||
plain text/markdown, so the wrapper does no JSON parsing.
|
||||
|
||||
| Mode | Usage | What it does |
|
||||
|------|-------|--------------|
|
||||
| `text` | `ask-gemini.sh text "<prompt>"` or `text --prompt-file <path>` | One-shot text answer from an independent model. `--prompt-file` for long content (review/summarize a doc). Default model routing. |
|
||||
| `verify` | `ask-gemini.sh verify "<claim/finding>"` or `verify --prompt-file <path>` | Adversarial second opinion — Gemini tries to REFUTE / find gaps, returns a verdict + reasons. Pinned to the strong model. |
|
||||
| `review` | `ask-gemini.sh review <file-path> ["<instructions>"]` | Gemini reads the file itself (its `read_file` tool, read-only `plan` mode) and reviews it. Path resolution: absolute, CWD-relative, or relative to `$CLAUDETOOLS_ROOT` — **see the path gotcha below**. Spaces OK. Works even on gitignored files. |
|
||||
| `review-files` | `ask-gemini.sh review-files [-i "<instr>"] <f1> [f2 …]` | Review a **set** of files together (cross-file consistency, multi-file change). Same path resolution as `review` (**see gotcha below**); spaces OK. No code passed as a shell arg. |
|
||||
| `review-diff` | `ask-gemini.sh review-diff [-C <repo-dir>] [-i "<instr>"] <gitref> [-- <pathspec>]` | Review a **git diff** (`git diff <gitref>` from `<repo-dir>`; default repo root, use `-C` for a submodule e.g. `-C projects/msp-tools/guru-rmm`). Diff goes via the prompt file; Gemini can `read_file` changed files for full context. |
|
||||
| `image-analyze` | `ask-gemini.sh image-analyze <image-path> ["<question>"]` | **Vision** — Gemini `read_file`s the image and describes/answers about it. Pins the **pro vision model** (the default flash-lite router hallucinates image content). Path absolute or repo-relative; spaces OK. KEYLESS (works on OAuth). |
|
||||
| `search` | `ask-gemini.sh search "<query>"` (or `search --prompt-file <path>`) | **Live Google web search** (sibling of `grok xsearch`) — Gemini uses its `google_web_search` tool and returns the answer **with source URLs**. KEYLESS (works on OAuth). |
|
||||
| `raw` | `ask-gemini.sh raw <gemini args...>` | Escape hatch — passes args straight to `gemini`. |
|
||||
| `text` | `ask-agy.sh text "<prompt>"` or `text --prompt-file <path>` | One-shot text answer from an independent model. `--prompt-file` for long content (review/summarize a doc). Default model routing. |
|
||||
| `verify` | `ask-agy.sh verify "<claim/finding>"` or `verify --prompt-file <path>` | Adversarial second opinion — Gemini tries to REFUTE / find gaps, returns a verdict + reasons. Pinned to the strong model. |
|
||||
| `review` | `ask-agy.sh review <file-path> ["<instructions>"]` | agy reads the file itself (copied into an `--add-dir` workspace) and reviews it. Path resolution: absolute, CWD-relative, or relative to `$CLAUDETOOLS_ROOT` — **see the path gotcha below**. Spaces OK. Works even on gitignored files. |
|
||||
| `review-files` | `ask-agy.sh review-files [-i "<instr>"] <f1> [f2 …]` | Review a **set** of files together (cross-file consistency, multi-file change). Same path resolution as `review` (**see gotcha below**); spaces OK. No code passed as a shell arg. |
|
||||
| `review-diff` | `ask-agy.sh review-diff [-C <repo-dir>] [-i "<instr>"] <gitref> [-- <pathspec>]` | Review a **git diff** (`git diff <gitref>` from `<repo-dir>`; default repo root, use `-C` for a submodule e.g. `-C projects/msp-tools/guru-rmm`). Diff goes via the prompt file; Gemini can `read_file` changed files for full context. |
|
||||
| `image-analyze` | `ask-agy.sh image-analyze <image-path> ["<question>"]` | **Vision** — agy reads the image (via `--add-dir`) and describes/answers about it. Pinned to the strong model. Path absolute or repo-relative; spaces OK. |
|
||||
| `search` | `ask-agy.sh search "<query>"` (or `search --prompt-file <path>`) | **Live web search** (sibling of `grok xsearch`) — agy searches the web and returns the answer **with source URLs**. |
|
||||
| `raw` | `ask-agy.sh raw <agy args...>` | Escape hatch — passes args straight to `agy`. |
|
||||
|
||||
The script runs Gemini headless with `-o json`, extracts the answer from
|
||||
`.response` (parsing from the first `{` so the CLI's cosmetic warning lines are
|
||||
ignored), and keeps stderr separate from the JSON so 429-backoff / warning noise
|
||||
never corrupts the parse.
|
||||
The script runs `agy` headless (`--dangerously-skip-permissions ... -p "<prompt>"`),
|
||||
captures plain-text stdout (no JSON — agy has no structured-output mode), keeps stderr
|
||||
separate for diagnostics, and retries a couple times with backoff on an empty turn.
|
||||
For review/vision it copies the target into a temp dir and `--add-dir`s it so agy's
|
||||
file tools can read it regardless of location/spaces.
|
||||
|
||||
> [!WARNING]
|
||||
> **Path gotcha for `review` / `review-files` (this has bitten us repeatedly).**
|
||||
@@ -55,96 +78,84 @@ never corrupts the parse.
|
||||
|
||||
### Model
|
||||
|
||||
- `text` uses Gemini's **default routing** (currently a flash-tier model) — fast, cheap.
|
||||
- `verify` / `review*` pin a **strong** model — `gemini-3.1-pro-preview` (verified
|
||||
available 2026-06-05, still valid 2026-06-17; the GA-looking `gemini-3.1-pro` and
|
||||
`gemini-3-pro` both `ModelNotFoundError`, so keep the `-preview` suffix).
|
||||
- Override either with `GEMINI_MODEL=<id>` (e.g. `GEMINI_MODEL=gemini-2.5-pro`).
|
||||
- `image-analyze` and `search` also pin the strong model (`GEMINI_MODEL` still honored).
|
||||
- `text` uses the CLI **default** (Gemini 3.5 Flash) — fast, cheap.
|
||||
- `verify` / `review*` / `image-analyze` / `search` pin the strong model
|
||||
**`Gemini 3.1 Pro (High)`** via `--model`.
|
||||
- Override any mode with `AGY_MODEL="<friendly name>"` (exact strings from
|
||||
`agy models`, e.g. `AGY_MODEL="Claude Opus 4.6 (Thinking)"`).
|
||||
|
||||
### Multimodal: image INPUT works, image GENERATION does not
|
||||
### Vision
|
||||
|
||||
- **Image INPUT (vision) works on OAuth** — `image-analyze` reads an image with the
|
||||
pinned **pro vision model** and describes it correctly. The default flash-lite
|
||||
router HALLUCINATES image content, which is why the pro model is pinned.
|
||||
- **Image GENERATION (nano-banana) does NOT work on OAuth** — it needs a Google AI
|
||||
Studio `NANOBANANA_API_KEY` plus the `nanobanana` extension. **Deferred** for now.
|
||||
Image/video **generation** stays [GROK](../grok/SKILL.md)'s lane (`grok image` /
|
||||
`grok video`); AGY's multimodal support is read/analyze only.
|
||||
`image-analyze` copies the image into an `--add-dir` workspace and asks agy to read
|
||||
and describe it (pinned to the strong model). Report-only — describes an image you
|
||||
give it. Image/video **generation** stays [GROK](../grok/SKILL.md)'s lane
|
||||
(`grok image` / `grok video`).
|
||||
|
||||
## Machine availability (fleet)
|
||||
|
||||
AGY is **per-machine** — the skill syncs fleet-wide but the `gemini` binary does
|
||||
not. Availability is gated by `identity.json` (per-machine, gitignored):
|
||||
AGY is **per-machine** — the skill syncs fleet-wide but the `agy` binary does not.
|
||||
Availability is gated by `identity.json` (per-machine, gitignored):
|
||||
|
||||
```json
|
||||
"gemini": { "installed": true,
|
||||
"binary": "C:/Users/guru/AppData/Roaming/npm/gemini",
|
||||
"auth": "oauth", "is_fleet_host": true,
|
||||
"capabilities": ["text","verify","review","image-analyze","search"] }
|
||||
"agy": { "installed": true,
|
||||
"binary": "C:/Users/guru/AppData/Local/agy/bin/agy",
|
||||
"auth": "antigravity", "is_fleet_host": true,
|
||||
"capabilities": ["text","verify","review","review-files","review-diff","image-analyze","search"] }
|
||||
```
|
||||
|
||||
- If `gemini.installed` is `false` (or the block is absent), `ask-gemini.sh` exits
|
||||
**3** with routing guidance instead of failing obscurely. Claude on such a
|
||||
machine should NOT attempt local Gemini.
|
||||
- **Fleet Gemini hosts: `GURU-5070`, `GURU-BEAST-ROG`** — machines with the Gemini
|
||||
CLI installed and Google-OAuth'd. When others get it, install
|
||||
`@google/gemini-cli`, run `gemini` once to log in with Google, then set their
|
||||
`identity.json` `gemini` block (and update this line).
|
||||
- If `agy.installed` is `false` (or the block is absent), `ask-agy.sh` exits **3**
|
||||
with routing guidance instead of failing obscurely. Claude on such a machine
|
||||
should NOT attempt local agy. (The wrapper falls back to the legacy `.gemini`
|
||||
block if `.agy` is absent, for machines mid-migration.)
|
||||
- **Fleet host: `GURU-5070`** (agy installed + Antigravity-authed). When others get
|
||||
it, install the Antigravity CLI, run `agy` once to sign in, then set their
|
||||
`identity.json` `agy` block (and update this line).
|
||||
|
||||
**Remote routing (NOT yet wired):** a non-host machine cannot run Gemini locally.
|
||||
To fulfill an AGY request from elsewhere, route it to the host (`GURU-5070`) —
|
||||
same pending channels as Grok (GuruRMM agent exec, a relay, or a coord-API job
|
||||
queue). Until that's built, AGY requests originate on the host machine.
|
||||
**Remote routing (NOT yet wired):** a non-host machine cannot run agy locally. Route
|
||||
the request to the host (`GURU-5070`) — same pending channels as Grok (GuruRMM agent
|
||||
exec, a relay, or a coord-API job queue). Until built, AGY requests originate on the host.
|
||||
|
||||
## When to route to Gemini (AGY)
|
||||
## When to route to AGY
|
||||
|
||||
- **Independent verification** — a genuinely different vendor/model to red-team a
|
||||
Claude finding or design before acting on it. (`verify`)
|
||||
- **Second-model code review** — have Gemini read and critique a file, a set of
|
||||
files, or a diff independently of Claude. (`review`, `review-files`, `review-diff`)
|
||||
- **Diverse drafts / second opinion** — alternative phrasing or approach to
|
||||
compare. (`text`)
|
||||
- **Google-ecosystem reach** — when a Google-side model/behavior is specifically
|
||||
wanted as the comparison point.
|
||||
- **Independent verification** — a different vendor/model to red-team a Claude
|
||||
finding or design before acting on it. (`verify`)
|
||||
- **Second-model code review** — agy reads and critiques a file, a set, or a diff
|
||||
independently of Claude. (`review`, `review-files`, `review-diff`)
|
||||
- **Diverse drafts / second opinion** — alternative phrasing or approach. (`text`)
|
||||
- **Live web facts** — current info past Claude's cutoff, with source URLs. (`search`)
|
||||
|
||||
AGY and [GROK](../grok/SKILL.md) are sibling second-opinion routers. Pick one, or
|
||||
run both and compare — disagreement between them is a strong signal to slow down.
|
||||
AGY and [GROK](../grok/SKILL.md) are sibling second-opinion routers. Pick one, or run
|
||||
both and compare — disagreement between them is a strong signal to slow down.
|
||||
|
||||
## When NOT to
|
||||
|
||||
- Pure classify / extract / summarize → cheaper via Tier-0 Ollama (`.claude/OLLAMA.md`).
|
||||
- Editing this repo's code → Claude's own agents own the codebase work. Gemini's
|
||||
`review*` modes are read-only (`--approval-mode plan`) by design; do not give
|
||||
Gemini write access to this repo.
|
||||
- Image / video **generation** → that's GROK's lane (`grok image` / `grok video`),
|
||||
not Gemini here (nano-banana needs an API key — deferred). Gemini CAN analyze an
|
||||
image you give it (`image-analyze`, vision input on OAuth).
|
||||
- **Never** delegate unsupervised destructive / production actions to Gemini.
|
||||
Always review Gemini output before acting on it — like Grok, it can over-claim.
|
||||
- Editing this repo's code → Claude's own agents own the codebase work. The wrapper
|
||||
runs agy read-only in intent (review prompts say "do not modify anything"); do not
|
||||
point it at write tasks in this repo.
|
||||
- Image / video **generation** → GROK's lane. AGY vision is read/analyze only.
|
||||
- **Never** delegate unsupervised destructive / production actions to agy. Always
|
||||
review its output before acting — like Grok, it can over-claim.
|
||||
|
||||
## Safety / operational notes
|
||||
|
||||
- `--skip-trust` is REQUIRED for headless runs (the CWD isn't a Gemini "trusted
|
||||
folder"). Equivalent env: `GEMINI_CLI_TRUST_WORKSPACE=true`. The wrapper passes it.
|
||||
- `review*` runs under `--approval-mode plan` (read-only): Gemini can read files
|
||||
but cannot modify anything. Do not change this to `auto_edit`/`yolo`.
|
||||
- Gemini's `read_file` honors `.gitignore` **and** a workspace sandbox (only files
|
||||
inside the workspace are readable). The wrapper sidesteps both by copying each
|
||||
review target into a temp dir added via `--include-directories` — so review
|
||||
works for tracked, gitignored, and spaced-path files alike.
|
||||
- Prompts are passed via `-p "$(cat <prompt-file>)"` built from a temp file, not
|
||||
inline shell args (avoids quote hell with long/structured content).
|
||||
- stdin is always closed (`</dev/null`) so `-p` never hangs waiting on stdin.
|
||||
- stdout carries two cosmetic warning lines ("True color (24-bit) support not
|
||||
detected", "Ripgrep is not available...") before output; JSON extraction from
|
||||
the first `{` ignores them. A transient `429 No capacity` backoff may appear on
|
||||
**stderr** and self-recovers — it does not affect the parsed answer.
|
||||
- `--dangerously-skip-permissions` is passed so print mode never HANGS on a tool
|
||||
approval prompt (headless). It only lets agy run its own read/search tools inside
|
||||
its sandbox to answer — the wrapper never asks it to write to this repo.
|
||||
- The prompt is passed as the VALUE of `-p` and MUST be last on the command line;
|
||||
all other flags (`--model`, `--add-dir`, `--print-timeout`) come before it.
|
||||
- Prompts are built in a temp file and read with `-p "$(cat <file>)"` (avoids
|
||||
quote hell); stdin is closed (`</dev/null`) so `-p` never waits on stdin.
|
||||
- Output is plain text/markdown. Tool-using turns (review/search/vision) may emit a
|
||||
line or two of tool narration before the answer; the prompts ask agy to suppress
|
||||
it, and residual narration is harmless.
|
||||
|
||||
## Reference
|
||||
- Binary: npm global `gemini` (`C:/Users/guru/AppData/Roaming/npm/gemini` on the
|
||||
host; the npm global dir is on PATH). The wrapper auto-locates it or honors `GEMINI=`.
|
||||
- Version 0.45.2. Auth: Google OAuth (`~/.gemini/oauth_creds.json`), no API key.
|
||||
- Headless contract: `gemini -p "<prompt>" -o json --skip-trust </dev/null` →
|
||||
`{session_id, response, stats}`; answer is `.response`.
|
||||
- Binary: native Go `agy` (`C:/Users/guru/AppData/Local/agy/bin/agy`; on PATH). The
|
||||
wrapper auto-locates it, honors `AGY=`, or reads `identity.json` `agy.binary`.
|
||||
- Version 1.0.16. Auth: Antigravity login (`~/.gemini/antigravity-cli/`), no API key,
|
||||
no `GOOGLE_CLOUD_PROJECT`.
|
||||
- Headless contract: `agy [--model "<name>"] [--add-dir <dir>] --dangerously-skip-permissions -p "<prompt>"` → plain-text answer on stdout (no JSON).
|
||||
- Migration: replaced the `gemini` npm CLI 2026-07-02 (that CLI hit
|
||||
`throwIneligibleOrProjectIdError`). `ask-gemini.sh` is a deprecated shim → `ask-agy.sh`.
|
||||
- Sibling router: [`grok`](../grok/SKILL.md) (image/video/live-data + second opinion).
|
||||
|
||||
280
.claude/skills/agy/scripts/ask-agy.sh
Normal file
280
.claude/skills/agy/scripts/ask-agy.sh
Normal file
@@ -0,0 +1,280 @@
|
||||
#!/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
|
||||
@@ -1,388 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# ask-gemini.sh — Claude -> Google Gemini CLI router (independent second model).
|
||||
#
|
||||
# Sibling of ask-grok.sh. Routes a task to the official Google Gemini CLI
|
||||
# (`gemini`, npm global) for an independent, different-vendor second opinion,
|
||||
# verification, or a Gemini code review. Headless, safe-by-default, JSON-parsed.
|
||||
#
|
||||
# Auth is Google login (OAuth) — NO API key. Creds: ~/.gemini/oauth_creds.json.
|
||||
# If a call fails with an auth error, run `gemini` interactively once and pick
|
||||
# "Login with Google".
|
||||
#
|
||||
# Output contract (VERIFIED on GURU-5070, gemini 0.45.2):
|
||||
# - Prefer JSON: `gemini -p ... -o json` -> {session_id, response, stats}.
|
||||
# The answer text is `.response`. stdout may carry two cosmetic warning lines
|
||||
# ("True color..." / "Ripgrep is not available...") before the JSON; we extract
|
||||
# the object starting at the FIRST '{' to ignore them. stderr (429 backoff,
|
||||
# warnings) is captured SEPARATELY and never fed to the JSON parser.
|
||||
# - `--skip-trust` is REQUIRED headless (the CWD isn't a trusted folder).
|
||||
# - stdin is always closed (</dev/null) so `-p` never hangs waiting on stdin.
|
||||
#
|
||||
# File reads (review*): Gemini's read_file honors .gitignore AND a workspace
|
||||
# sandbox (only files under the workspace/included dirs are readable). To make
|
||||
# review robust for ANY file (tracked, gitignored, with spaces), we copy each
|
||||
# target into a temp dir and add it to the workspace via --include-directories.
|
||||
# review-diff runs with the repo dir included so changed files read in place.
|
||||
#
|
||||
# Usage:
|
||||
# ask-gemini.sh text "<prompt>" # one-shot answer
|
||||
# ask-gemini.sh text --prompt-file <path> # long content
|
||||
# ask-gemini.sh verify "<claim or finding to refute>" # adversarial check
|
||||
# ask-gemini.sh verify --prompt-file <path>
|
||||
# ask-gemini.sh review <file> [instructions] # gemini reads + reviews one file
|
||||
# ask-gemini.sh review-files [-i "instr"] <f1> [f2 ...] # review a SET of files together
|
||||
# ask-gemini.sh review-diff [-C <repo-dir>] [-i "instr"] <gitref> [-- <pathspec>]
|
||||
# ask-gemini.sh image-analyze <image-path> ["question"] # vision: read_file image + describe (PRO model)
|
||||
# ask-gemini.sh search "<query>" # Google-grounded live web search + sources
|
||||
# ask-gemini.sh raw <gemini args...> # escape hatch
|
||||
#
|
||||
# Exit: 0 ok, 1 no result, 2 usage, 3 not installed here, 127 gemini/python not found.
|
||||
set -uo pipefail
|
||||
SELF="ask-gemini"
|
||||
|
||||
PY="$(command -v py 2>/dev/null || command -v python 2>/dev/null || command -v python3 2>/dev/null || true)"
|
||||
[ -z "$PY" ] && { echo "[$SELF] python (py/python/python3) required for JSON parsing" >&2; exit 127; }
|
||||
|
||||
# --- path conversion: native-Windows path for the gemini args (no-op off Windows) ---
|
||||
# gemini is a native Windows binary (npm shim -> node.exe); Git Bash hands it POSIX
|
||||
# paths (/tmp, /c/.., /d/..) it cannot resolve. cygpath -w converts to C:\... on
|
||||
# MSYS/Cygwin; on Linux/macOS it passes through unchanged. Explicit conversion
|
||||
# removes reliance on MSYS auto-conversion (which breaks on spaces/edge cases).
|
||||
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) declares whether gemini is installed here ---
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)"
|
||||
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"
|
||||
idgem() { # read field $1 from identity.json .gemini (empty if absent)
|
||||
[ -f "$IDFILE" ] || { echo ""; return; }
|
||||
"$PY" -c "import json,sys
|
||||
try:
|
||||
g=(json.load(sys.stdin).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 identity explicitly says gemini is NOT installed here, fail fast with guidance.
|
||||
if [ "$(idgem installed)" = "false" ]; then
|
||||
echo "[$SELF] gemini is not installed on this machine (identity.json gemini.installed=false)." >&2
|
||||
echo "[$SELF] Gemini runs only on the fleet host. Route this request there, or install the gemini CLI (npm i -g @google/gemini-cli) + set identity.json gemini.installed=true." >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# --- locate the gemini binary: GEMINI env > identity.json gemini.binary > auto-locate ---
|
||||
# An explicit GEMINI= override that isn't runnable is a user error -> fail clearly up front
|
||||
# (covers absolute paths AND a bare name resolvable on PATH, e.g. GEMINI=gemini).
|
||||
GEMINI="${GEMINI:-}"
|
||||
if [ -n "$GEMINI" ] && [ ! -x "$GEMINI" ] && ! command -v "$GEMINI" >/dev/null 2>&1; then
|
||||
echo "[$SELF] GEMINI='$GEMINI' is not an executable gemini binary." >&2; exit 127
|
||||
fi
|
||||
cand="$(idgem binary)"
|
||||
[ -z "$GEMINI" ] && [ -n "$cand" ] && [ -x "$cand" ] && GEMINI="$cand"
|
||||
if [ -z "$GEMINI" ]; then
|
||||
if command -v gemini >/dev/null 2>&1; then GEMINI="$(command -v gemini)"; else
|
||||
for c in "${APPDATA:-}/npm/gemini" "/c/Users/${USERNAME:-${USER:-x}}/AppData/Roaming/npm/gemini" \
|
||||
"$HOME/AppData/Roaming/npm/gemini" "/usr/local/bin/gemini" "$HOME/.npm-global/bin/gemini"; do
|
||||
[ -n "$c" ] && [ -x "$c" ] && { GEMINI="$c"; break; }
|
||||
done
|
||||
fi
|
||||
fi
|
||||
[ -z "$GEMINI" ] && { echo "[$SELF] gemini CLI not found (set identity.json gemini.binary, GEMINI=, or install: npm i -g @google/gemini-cli)" >&2; exit 127; }
|
||||
|
||||
# Model: default routing for text; a strong pinned model for verify/review.
|
||||
# gemini-3.1-pro-preview verified available on this account (2026-06-05); overridable.
|
||||
STRONG_MODEL="${GEMINI_MODEL:-gemini-3.1-pro-preview}"
|
||||
|
||||
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)}"
|
||||
# Functional-error logger (skill name "agy"); soft-fails, never breaks the caller.
|
||||
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "agy" "$@" >/dev/null 2>&1 || true; }
|
||||
|
||||
# gtimeout on macOS (brew coreutils), timeout elsewhere.
|
||||
TIMEOUT_CMD="timeout"
|
||||
if [[ "${OSTYPE:-}" == "darwin"* ]]; then
|
||||
TIMEOUT_CMD="$(command -v gtimeout 2>/dev/null || echo timeout)"
|
||||
fi
|
||||
|
||||
# run gemini headless reading the prompt file. $1=timeout secs; rest=extra flags.
|
||||
# stdout -> $OUT, stderr -> $ERR (kept separate so warning/429 noise never reaches
|
||||
# the JSON parser). Never fail the script on gemini's exit code; we judge by output.
|
||||
# Records the invocation so emit_or_fail can replay it once on a transient empty turn.
|
||||
LAST_RUN=()
|
||||
run_gemini() {
|
||||
local to="$1"; shift
|
||||
LAST_RUN=("$to" "$@")
|
||||
"$TIMEOUT_CMD" "$to" "$GEMINI" -p "$(cat "$PF")" -o json --skip-trust "$@" \
|
||||
>"$OUT" 2>"$ERR" </dev/null || true
|
||||
}
|
||||
|
||||
# extract .response from the JSON object starting at the first '{' in $OUT.
|
||||
# Parsed via stdin so Windows python never resolves a git-bash (/c/...) path.
|
||||
#
|
||||
# Some pinned-pro tool-using turns (notably image-analyze) leak the model's
|
||||
# internal reasoning stream into .response: a stray token + a 'thought' marker
|
||||
# followed by 'CRITICAL INSTRUCTION N:' lines, then the real answer. We strip
|
||||
# that preamble ONLY when the signature is clearly present, so clean responses
|
||||
# (text/verify/review/search) pass through byte-for-byte unchanged.
|
||||
gresponse() { "$PY" -c "import json,sys,re,os
|
||||
raw=sys.stdin.read()
|
||||
i=raw.find('{')
|
||||
if i < 0:
|
||||
print(''); sys.exit(0)
|
||||
try:
|
||||
r=json.loads(raw[i:]).get('response','') or ''
|
||||
except Exception:
|
||||
print(''); sys.exit(0)
|
||||
head=r[:40].lower()
|
||||
leak=('thought' in head) or ('critical instruction' in r.lower()[:600])
|
||||
if leak:
|
||||
lines=r.split('\n')
|
||||
keep=[]; dropping=True
|
||||
for ln in lines:
|
||||
s=ln.strip()
|
||||
low=s.lower()
|
||||
if dropping and (
|
||||
low.endswith('thought') or low.startswith('critical instruction')
|
||||
or low.startswith('thought:') or low=='' ):
|
||||
continue
|
||||
dropping=False
|
||||
keep.append(ln)
|
||||
cleaned='\n'.join(keep).strip()
|
||||
r=cleaned if cleaned else r.strip()
|
||||
# AGY_CLEAN: aggressive prefix scrub for tool-using turns (image-analyze), which
|
||||
# can fuse a stray stream/tool token onto the front of the answer (e.g. '.',
|
||||
# '.94>', 'uem_image_0_0_png}'). Off by default so text/verify/review/search are
|
||||
# byte-exact. We only remove a junk run that ends in a stream delimiter (} > :)
|
||||
# or a lone leading punctuation char, immediately before the first real sentence.
|
||||
if os.environ.get('AGY_CLEAN') == '1' and r:
|
||||
# The pro-preview tool loop sometimes prepends a numbered/markdown reasoning
|
||||
# block before the actual answer. If a clear answer pivot follows such a
|
||||
# preamble, keep from the pivot onward (the user-facing answer).
|
||||
if re.search(r'(?im)^\s*\d+[.)]\s', r) or 'thought' in r[:60].lower():
|
||||
pivs=list(re.finditer(r'(?i)(Based on the image\b|\*\*Answer:?\*\*|The image (?:contains|shows|displays)\b)', r))
|
||||
if pivs:
|
||||
r=r[pivs[-1].start():]
|
||||
m=re.match(r'^[^\n]{0,40}?(?:\.png\)|\.jpe?g\)|[}>:)])\s*([\"A-Z].*)$', r, re.S)
|
||||
if m and m.group(1):
|
||||
r=m.group(1)
|
||||
else:
|
||||
# a short leading junk run (ASCII punctuation/digits or non-Latin stream
|
||||
# tokens) before a capitalized/quoted sentence start. Bounded length so we
|
||||
# never eat a real lowercase sentence or real prose.
|
||||
m=re.match(r'^(?:[^A-Za-z\"]|[^\x00-\x7f]){1,8}([A-Z\"].*)$', r, re.S)
|
||||
if m and m.group(1):
|
||||
r=m.group(1)
|
||||
r=r.strip()
|
||||
print(r)" < "$OUT"; }
|
||||
|
||||
# detect a GENUINE auth failure in stderr (precise remediation hint). Tightened 2026-06-17 -
|
||||
# the old broad regex (bare login|credential|authenticat|oauth|401) matched benign mid-run
|
||||
# token-refresh lines and false-flagged working sessions as auth failures.
|
||||
auth_failed() { grep -qiE 'invalid_grant|unauthorized|not authenticated|authentication failed|re-?authenticat|please (log|sign).?in|login with google|token (has )?expired|no (valid )?credentials' "$ERR" 2>/dev/null; }
|
||||
# detect a quota / rate-capacity exhaustion (the pinned strong model can be capped mid-session)
|
||||
quota_exhausted() { grep -qiE 'exhausted your capacity|quota|resource[_ ]?exhausted|rate limit|too many requests|429' "$ERR" 2>/dev/null; }
|
||||
|
||||
emit_or_fail() { # print .response; gemini intermittently returns an empty turn, so retry a few
|
||||
# times with backoff before giving up (single retry was insufficient - 2 empties
|
||||
# in a row caused spurious failures during live research, 2026-06-17).
|
||||
# Do ALL retries first; only classify the failure (auth vs generic) AFTER exhausting them.
|
||||
# (Checking auth_failed INSIDE the loop caused false aborts: a benign mid-run credential-refresh
|
||||
# line in stderr matched the auth regex and killed the retries even though auth was fine. 2026-06-17.)
|
||||
local txt tries=0 max="${AGY_MAX_TRIES:-3}"
|
||||
txt="$(gresponse)"
|
||||
while [ -z "$txt" ] && [ "$tries" -lt $((max-1)) ] && [ ${#LAST_RUN[@]} -gt 0 ]; do
|
||||
tries=$((tries+1))
|
||||
echo "[$SELF] empty response - retry $tries/$((max-1)) (backoff ${tries}x3s)..." >&2
|
||||
sleep $((tries*3)) # 3s, 6s backoff (covers transient empties / 429s / token refresh)
|
||||
run_gemini "${LAST_RUN[@]}"
|
||||
txt="$(gresponse)"
|
||||
done
|
||||
if [ -n "$txt" ]; then printf '%s\n' "$txt"; return 0; fi
|
||||
# Quota fallback: if the pinned strong model is capacity/quota-capped, retry ONCE on the default
|
||||
# (lighter) model by stripping -m from the last invocation - the default model has a separate quota.
|
||||
if quota_exhausted && [ ${#LAST_RUN[@]} -gt 0 ]; then
|
||||
echo "[$SELF] '$STRONG_MODEL' quota exhausted - retrying once on the default (lighter) model..." >&2
|
||||
local nr=() a skip=0
|
||||
for a in "${LAST_RUN[@]}"; do
|
||||
if [ "$skip" = 1 ]; then skip=0; continue; fi
|
||||
if [ "$a" = "-m" ]; then skip=1; continue; fi
|
||||
nr+=("$a")
|
||||
done
|
||||
run_gemini "${nr[@]}"
|
||||
txt="$(gresponse)"
|
||||
if [ -n "$txt" ]; then printf '%s\n' "$txt"; return 0; fi
|
||||
fi
|
||||
if auth_failed; then
|
||||
echo "[$SELF] Gemini auth error - run 'gemini' interactively and choose 'Login with Google', then retry." >&2
|
||||
_logerr "gemini auth/login failure" --context "mode=$MODE"
|
||||
else
|
||||
echo "[$SELF] no response from gemini after $max attempts. stderr tail:" >&2
|
||||
tail -3 "$ERR" >&2 2>/dev/null || true
|
||||
_logerr "gemini 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 target files into an included temp workspace dir so gemini's read_file can
|
||||
# reach them regardless of .gitignore / workspace sandbox. Echoes the included dir.
|
||||
INCLUDE_DIR="$TMP/inbox"
|
||||
prep_includes() { mkdir -p "$INCLUDE_DIR"; }
|
||||
|
||||
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 (e.g. correct / partly correct / incorrect) plus specific justification. Answer in text only; do not use any tools.\n\nContent:\n%s' "$SRC" > "$PF"
|
||||
run_gemini 180 -m "$STRONG_MODEL"
|
||||
else
|
||||
printf 'Answer the following directly in text. Do not use any tools.\n\n%s' "$SRC" > "$PF"
|
||||
run_gemini 180
|
||||
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.}"
|
||||
# GOTCHA: a relative path resolves against ONLY CWD or $REPO_ROOT ($CLAUDETOOLS_ROOT) --
|
||||
# NOT a submodule/subdir. "server/src/x.rs" relative to a submodule fails ("file not found")
|
||||
# unless CWD is that submodule. Pass ABSOLUTE paths for submodule/subtree files.
|
||||
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 'Use your read_file tool to read the file at this absolute path, then perform the task and stop. Do not modify anything.\nPath: %s\n\nTask: %s' "$tgt_win" "$instr" > "$PF"
|
||||
run_gemini 240 -m "$STRONG_MODEL" --approval-mode plan --include-directories "$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=()
|
||||
# GOTCHA: each relative path resolves against ONLY CWD or $REPO_ROOT ($CLAUDETOOLS_ROOT) --
|
||||
# NOT a submodule/subdir. Paths relative to a submodule fail unless CWD is that submodule.
|
||||
# Pass ABSOLUTE paths for submodule/subtree files (e.g. build the list with `find "$(pwd)/..."`).
|
||||
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")"
|
||||
# de-collide identical basenames from different dirs
|
||||
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 'Use your read_file tool to read EACH of these files (absolute paths), then perform the task across ALL of them and stop. Do not modify anything.\n\nFiles:\n%s\nTask: %s' "$list" "$instr" > "$PF"
|
||||
run_gemini 300 -m "$STRONG_MODEL" --approval-mode plan --include-directories "$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\nYou may use your read_file tool on any changed file for full context (paths in the diff are relative to %s; strip the a/ b/ prefixes). Do not modify anything.\n\n=== BEGIN DIFF ===\n' "$instr" "$gdir_win"; cat "$TMP/diff.txt"; printf '\n=== END DIFF ===\n'; } > "$PF"
|
||||
run_gemini 300 -m "$STRONG_MODEL" --approval-mode plan --include-directories "$gdir_win"
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
image-analyze|image|vision)
|
||||
# Independent second-model VISION. The default flash-lite router hallucinates
|
||||
# image content, so we PIN the pro vision model (STRONG_MODEL) and run with
|
||||
# yolo approval so read_file can execute. The image is copied into an included
|
||||
# temp dir (like the review modes) and handed to Gemini by absolute winpath.
|
||||
[ -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")"
|
||||
# Image path goes in via %s (never as a printf format string).
|
||||
printf 'Use your read_file tool to read the image at this absolute path, then describe exactly what you see. Report only what is actually present in the image; do not guess or invent content. Then stop. Do not modify anything.\nImage path: %s\n\nQuestion: %s' "$img_win" "$question" > "$PF"
|
||||
run_gemini 240 -m "$STRONG_MODEL" --approval-mode yolo --include-directories "$inc_win"
|
||||
AGY_CLEAN=1 emit_or_fail
|
||||
;;
|
||||
|
||||
search|websearch)
|
||||
# Google-grounded LIVE web search (mirrors grok xsearch). Gemini's
|
||||
# google_web_search tool works on OAuth; run with yolo so the tool can fire.
|
||||
# Query goes via the prompt file so long queries don't hit shell-quote limits.
|
||||
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 'Use your google_web_search tool to find 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.\n\nQuery: %s' "$SRC" > "$PF"
|
||||
run_gemini 180 -m "$STRONG_MODEL" --approval-mode yolo
|
||||
emit_or_fail
|
||||
;;
|
||||
|
||||
raw)
|
||||
"$GEMINI" "$@"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "[$SELF] unknown mode '$MODE' (use text|verify|review|review-files|review-diff|image-analyze|search|raw)" >&2; exit 2 ;;
|
||||
esac
|
||||
# ask-gemini.sh — DEPRECATED shim. The AGY skill now routes to the Antigravity CLI
|
||||
# (`agy`), not the old Google `gemini` npm CLI (which broke on this account with a
|
||||
# Cloud-project eligibility error). This shim forwards to ask-agy.sh so any existing
|
||||
# references keep working. New callers should use ask-agy.sh directly.
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)"
|
||||
exec bash "$DIR/ask-agy.sh" "$@"
|
||||
|
||||
@@ -238,12 +238,12 @@ for ln in sys.stdin:
|
||||
if e.get("type")=="text": t.append(e.get("data",""))
|
||||
print("".join(t).strip())' < "$OUT")"
|
||||
if [ "$GRC" -eq 0 ] && [ -n "$ans" ]; then printf '%s\n' "$ans"; exit 0; fi
|
||||
echo "[$SELF] grok xsearch did not finish (rc=$GRC) -> falling back to gemini search" >&2
|
||||
_logerr "grok xsearch incomplete (rc=$GRC); auto-fell back to gemini" --context "mode=xsearch"
|
||||
GEM="$REPO_ROOT/.claude/skills/agy/scripts/ask-gemini.sh"
|
||||
if [ -f "$GEM" ]; then echo "[grok xsearch timed out -> answered via gemini search]"; exec bash "$GEM" search "$Q"; fi
|
||||
echo "[$SELF] grok xsearch did not finish (rc=$GRC) -> falling back to agy search" >&2
|
||||
_logerr "grok xsearch incomplete (rc=$GRC); auto-fell back to agy" --context "mode=xsearch"
|
||||
GEM="$REPO_ROOT/.claude/skills/agy/scripts/ask-agy.sh"
|
||||
if [ -f "$GEM" ]; then echo "[grok xsearch timed out -> answered via agy search]"; exec bash "$GEM" search "$Q"; fi
|
||||
[ -n "$ans" ] && { printf '%s\n' "$ans"; exit 0; } # last resort: whatever partial streamed
|
||||
echo "[$SELF] no result (grok timed out; gemini fallback unavailable)" >&2; exit 1
|
||||
echo "[$SELF] no result (grok timed out; agy fallback unavailable)" >&2; exit 1
|
||||
;;
|
||||
review|file)
|
||||
[ -z "${1:-}" ] && { echo "usage: $SELF review <file-path> [instructions]" >&2; exit 2; }
|
||||
|
||||
Reference in New Issue
Block a user