From 172a3d2e532a4cdcc9dbfb823beb4c0247172951 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 31 May 2026 15:42:05 +0200 Subject: [PATCH 1/9] feat: add agy (Antigravity CLI) as a supported --cli provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds agy alongside claude, gemini, codex, agent, openclaw, opencode, and copilot. Uses a plain-text path (--print + stdin) since agy does not support --output-format json. Default model is null — agy uses its own session default (Gemini 3.5 Flash) when no model is specified. Binary resolved via AGY_PATH env var or agy on PATH. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 12 +- apps/chrome-extension/src/lib/settings.ts | 3 +- src/config/parse-helpers.ts | 3 +- src/config/sections.ts | 3 + src/config/types.ts | 4 +- src/daemon/agent-model.ts | 4 +- src/daemon/chat.ts | 6 +- src/daemon/env-snapshot.ts | 2 + src/daemon/models.ts | 6 + src/llm/cli-provider-output.ts | 5 +- src/llm/cli.ts | 25 ++++ src/llm/provider-profile.ts | 11 +- src/model-auto-cli.ts | 4 +- src/model-spec.ts | 7 +- src/run/cli-fallback-state.ts | 3 +- src/run/env.ts | 7 +- src/run/help.ts | 3 +- src/run/run-models.ts | 1 + src/run/run-settings-parse.ts | 1 + src/run/summary-engine.ts | 6 + src/run/types.ts | 3 +- tests/config.test.ts | 15 +++ tests/daemon.agent.test.ts | 36 ++++++ tests/daemon.chat.test.ts | 36 ++++++ tests/daemon.config.test.ts | 4 + tests/llm.cli.agy.test.ts | 136 ++++++++++++++++++++++ 26 files changed, 324 insertions(+), 22 deletions(-) create mode 100644 tests/llm.cli.agy.test.ts diff --git a/README.md b/README.md index 8c93b7709..17e51571b 100644 --- a/README.md +++ b/README.md @@ -331,7 +331,7 @@ Use `summarize --help` or `summarize help` for the full help text. - `--length short|medium|long|xl|xxl|s|m|l|` - `--language, --lang `: output language (`auto` = match source) - `--max-output-tokens `: hard cap for LLM output tokens -- `--cli [provider]`: use a CLI provider (`--model cli/`). Supports `claude`, `gemini`, `codex`, `agent`, `openclaw`, `opencode`. If omitted, uses auto selection with CLI enabled. +- `--cli [provider]`: use a CLI provider (`--model cli/`). Supports `claude`, `gemini`, `codex`, `agent`, `openclaw`, `opencode`, `agy`. If omitted, uses auto selection with CLI enabled. - `--stream auto|on|off`: stream LLM output (`auto` = TTY only; disabled in `--json` mode) - `--plain`: keep raw output (no ANSI/OSC Markdown rendering) - `--no-color`: disable ANSI colors @@ -353,7 +353,7 @@ Use `summarize --help` or `summarize help` for the full help text. - `--verbose`: debug/diagnostics on stderr - `--metrics off|on|detailed`: metrics output (default `on`) -### Coding CLIs (Codex, Claude, Gemini, Agent, OpenClaw, OpenCode) +### Coding CLIs (Codex, Claude, Gemini, Agent, OpenClaw, OpenCode, Antigravity) Summarize can use common coding CLIs as local model backends: @@ -363,6 +363,7 @@ Summarize can use common coding CLIs as local model backends: - `agent` (Cursor Agent CLI) -> `--cli agent` / `--model cli/agent/` - `openclaw` -> `--cli openclaw` / `--model cli/openclaw/` or `--model openclaw/` - `opencode` -> `--cli opencode` / `--model cli/opencode/` (`--model cli/opencode` uses the OpenCode runtime default) +- `agy` (Antigravity CLI) -> `--cli agy` / `--model cli/agy/` (uses agy's active session model when no model is specified) Built-in preset: @@ -370,8 +371,8 @@ Built-in preset: Requirements: -- Binary installed and on `PATH` (or set `CODEX_PATH`, `CLAUDE_PATH`, `GEMINI_PATH`, `AGENT_PATH`, `OPENCLAW_PATH`, `OPENCODE_PATH`) -- Provider authenticated (`codex login`, `claude auth`, `gemini` login flow, `agent login` or `CURSOR_API_KEY`, `opencode auth login`) +- Binary installed and on `PATH` (or set `CODEX_PATH`, `CLAUDE_PATH`, `GEMINI_PATH`, `AGENT_PATH`, `OPENCLAW_PATH`, `OPENCODE_PATH`, `AGY_PATH`) +- Provider authenticated (`codex login`, `claude auth`, `gemini` login flow, `agent login` or `CURSOR_API_KEY`, `opencode auth login`, `agy` login flow or `ANTIGRAVITY_API_KEY`) Quick smoke test: @@ -384,13 +385,14 @@ summarize --cli gemini --plain --timeout 2m /tmp/summarize-cli-smoke.txt summarize --cli agent --plain --timeout 2m /tmp/summarize-cli-smoke.txt summarize --cli openclaw --plain --timeout 2m /tmp/summarize-cli-smoke.txt summarize --cli opencode --plain --timeout 2m /tmp/summarize-cli-smoke.txt +summarize --cli agy --plain --timeout 2m /tmp/summarize-cli-smoke.txt ``` Set explicit CLI allowlist/order: ```json { - "cli": { "enabled": ["codex", "claude", "gemini", "agent", "openclaw", "opencode"] } + "cli": { "enabled": ["codex", "claude", "gemini", "agent", "openclaw", "opencode", "agy"] } } ``` diff --git a/apps/chrome-extension/src/lib/settings.ts b/apps/chrome-extension/src/lib/settings.ts index 88f0b50e1..d463f7477 100644 --- a/apps/chrome-extension/src/lib/settings.ts +++ b/apps/chrome-extension/src/lib/settings.ts @@ -151,7 +151,8 @@ function normalizeAutoCliOrder(value: unknown): string { item !== "agent" && item !== "openclaw" && item !== "opencode" && - item !== "copilot" + item !== "copilot" && + item !== "agy" ) { continue; } diff --git a/src/config/parse-helpers.ts b/src/config/parse-helpers.ts index 21c565634..00caa7452 100644 --- a/src/config/parse-helpers.ts +++ b/src/config/parse-helpers.ts @@ -17,7 +17,8 @@ export function parseCliProvider(value: unknown, path: string): CliProvider { trimmed === "agent" || trimmed === "openclaw" || trimmed === "opencode" || - trimmed === "copilot" + trimmed === "copilot" || + trimmed === "agy" ) { return trimmed as CliProvider; } diff --git a/src/config/sections.ts b/src/config/sections.ts index 37f846488..e681e3721 100644 --- a/src/config/sections.ts +++ b/src/config/sections.ts @@ -325,6 +325,7 @@ export function parseCliConfig(root: Record, path: string): Cli const copilot = value.copilot ? parseCliProviderConfig(value.copilot, path, "copilot") : undefined; + const agy = value.agy ? parseCliProviderConfig(value.agy, path, "agy") : undefined; if (typeof value.autoFallback !== "undefined" && typeof value.magicAuto !== "undefined") { throw new Error( `Invalid config file ${path}: use only one of "cli.autoFallback" or legacy "cli.magicAuto".`, @@ -359,6 +360,7 @@ export function parseCliConfig(root: Record, path: string): Cli openclaw || opencode || copilot || + agy || autoFallback || promptOverride || typeof allowTools === "boolean" || @@ -373,6 +375,7 @@ export function parseCliConfig(root: Record, path: string): Cli ...(openclaw ? { openclaw } : {}), ...(opencode ? { opencode } : {}), ...(copilot ? { copilot } : {}), + ...(agy ? { agy } : {}), ...(autoFallback ? { autoFallback } : {}), ...(promptOverride ? { promptOverride } : {}), ...(typeof allowTools === "boolean" ? { allowTools } : {}), diff --git a/src/config/types.ts b/src/config/types.ts index 036adf2e4..bc563eed5 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -7,7 +7,8 @@ export type CliProvider = | "agent" | "openclaw" | "opencode" - | "copilot"; + | "copilot" + | "agy"; export type OpenAiReasoningEffort = "none" | "low" | "medium" | "high" | "xhigh"; export type OpenAiTextVerbosity = "low" | "medium" | "high"; export type ModelRequestOptions = { @@ -36,6 +37,7 @@ export type CliConfig = { openclaw?: CliProviderConfig; opencode?: CliProviderConfig; copilot?: CliProviderConfig; + agy?: CliProviderConfig; autoFallback?: CliAutoFallbackConfig; magicAuto?: CliAutoFallbackConfig; promptOverride?: string; diff --git a/src/daemon/agent-model.ts b/src/daemon/agent-model.ts index 163b7d891..037d451c3 100644 --- a/src/daemon/agent-model.ts +++ b/src/daemon/agent-model.ts @@ -206,8 +206,10 @@ function buildNoAgentModelAvailableError({ if (attempt.requiredEnv === "CLI_GEMINI") return "gemini"; if (attempt.requiredEnv === "CLI_AGENT") return "agent"; if (attempt.requiredEnv === "CLI_OPENCLAW") return "openclaw"; + if (attempt.requiredEnv === "CLI_OPENCODE") return "opencode"; if (attempt.requiredEnv === "CLI_COPILOT") return "copilot"; - return "opencode"; + if (attempt.requiredEnv === "CLI_AGY") return "agy"; + return "unknown"; }) .filter((provider) => !cliAvailability[provider as keyof typeof cliAvailability]), ), diff --git a/src/daemon/chat.ts b/src/daemon/chat.ts index 4d901511e..0eb2aa619 100644 --- a/src/daemon/chat.ts +++ b/src/daemon/chat.ts @@ -42,7 +42,11 @@ function resolveConfiguredCliModel( ? cli?.agent?.model : provider === "openclaw" ? cli?.openclaw?.model - : cli?.opencode?.model; + : provider === "opencode" + ? cli?.opencode?.model + : provider === "agy" + ? cli?.agy?.model + : cli?.copilot?.model; return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : null; } diff --git a/src/daemon/env-snapshot.ts b/src/daemon/env-snapshot.ts index dd2bb81e0..57d7922cd 100644 --- a/src/daemon/env-snapshot.ts +++ b/src/daemon/env-snapshot.ts @@ -41,6 +41,8 @@ const ENV_KEYS = [ "OPENCLAW_PATH", "OPENCODE_PATH", "COPILOT_PATH", + "AGY_PATH", + "ANTIGRAVITY_API_KEY", "UVX_PATH", ] as const; diff --git a/src/daemon/models.ts b/src/daemon/models.ts index 8ffa943d7..9e441b764 100644 --- a/src/daemon/models.ts +++ b/src/daemon/models.ts @@ -142,6 +142,7 @@ export async function buildModelPickerOptions({ cliOpenclaw: boolean; cliOpencode: boolean; cliCopilot: boolean; + cliAgy: boolean; }; openaiBaseUrl: string | null; localModelsSource: { kind: "openai-compatible"; baseUrlHost: string } | null; @@ -164,6 +165,7 @@ export async function buildModelPickerOptions({ cliOpenclaw: false, cliOpencode: false, cliCopilot: false, + cliAgy: false, }; const cliAvailability = resolveCliAvailability({ env: envForRun, config: configForCli }); providers.cliClaude = Boolean(cliAvailability.claude); @@ -173,6 +175,7 @@ export async function buildModelPickerOptions({ providers.cliOpenclaw = Boolean(cliAvailability.openclaw); providers.cliOpencode = Boolean(cliAvailability.opencode); providers.cliCopilot = Boolean(cliAvailability.copilot); + providers.cliAgy = Boolean(cliAvailability.agy); const options: ModelPickerOption[] = [ { id: "auto", label: "Auto" }, @@ -201,6 +204,9 @@ export async function buildModelPickerOptions({ if (providers.cliCopilot) { options.push({ id: "cli/copilot", label: "CLI: GitHub Copilot" }); } + if (providers.cliAgy) { + options.push({ id: "cli/agy", label: "CLI: Antigravity (agy)" }); + } if (providers.openrouter) { options.push({ id: "free", label: "Free (OpenRouter)" }); diff --git a/src/llm/cli-provider-output.ts b/src/llm/cli-provider-output.ts index 369aa703c..e26eb17a7 100644 --- a/src/llm/cli-provider-output.ts +++ b/src/llm/cli-provider-output.ts @@ -1,7 +1,7 @@ import type { CliProvider } from "../config.js"; import type { LlmTokenUsage } from "./generate-text.js"; -export type JsonCliProvider = Exclude; +export type JsonCliProvider = Exclude; const JSON_RESULT_FIELDS = ["result", "response", "output", "message", "text"] as const; @@ -10,7 +10,8 @@ export function isJsonCliProvider(provider: CliProvider): provider is JsonCliPro provider !== "codex" && provider !== "openclaw" && provider !== "opencode" && - provider !== "copilot" + provider !== "copilot" && + provider !== "agy" ); } diff --git a/src/llm/cli.ts b/src/llm/cli.ts index 000ab78a5..63658960a 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -23,6 +23,7 @@ const DEFAULT_BINARIES: Record = { openclaw: "openclaw", opencode: "opencode", copilot: "copilot", + agy: "agy", }; const OPENCLAW_MAX_MESSAGE_ARG_BYTES = 120 * 1024; @@ -37,6 +38,7 @@ const PROVIDER_PATH_ENV: Record = { openclaw: "OPENCLAW_PATH", opencode: "OPENCODE_PATH", copilot: "COPILOT_PATH", + agy: "AGY_PATH", }; type RunCliModelOptions = { @@ -72,6 +74,7 @@ function getCliProviderConfig( if (provider === "agent") return config.agent; if (provider === "openclaw") return config.openclaw; if (provider === "opencode") return config.opencode; + if (provider === "agy") return config.agy; return config.copilot; } @@ -371,6 +374,28 @@ export async function runCliModel({ return { text, usage: null, costUsd: null }; } + if (provider === "agy") { + const agyArgs: string[] = [...providerExtraArgs, "--print"]; + if (requestedModel) { + agyArgs.push("--model", requestedModel); + } + if (allowTools) { + agyArgs.push("--dangerously-skip-permissions"); + } + const { stdout } = await execCliWithInput({ + execFileImpl: execFileFn, + cmd: binary, + args: agyArgs, + input: prompt, + timeoutMs, + env: effectiveEnv, + cwd, + }); + const text = stdout.trim(); + if (!text) throw new Error("CLI returned empty output"); + return { text, usage: null, costUsd: null }; + } + if (!isJsonCliProvider(provider)) { throw new Error(`Unsupported CLI provider "${provider}".`); } diff --git a/src/llm/provider-profile.ts b/src/llm/provider-profile.ts index 78abaa95f..f8254b1cf 100644 --- a/src/llm/provider-profile.ts +++ b/src/llm/provider-profile.ts @@ -35,7 +35,8 @@ export type RequiredModelEnv = | "CLI_AGENT" | "CLI_OPENCLAW" | "CLI_OPENCODE" - | "CLI_COPILOT"; + | "CLI_COPILOT" + | "CLI_AGY"; type GatewayProviderProfile = { requiredEnv: RequiredModelEnv; @@ -105,6 +106,7 @@ export const DEFAULT_CLI_MODELS: Record = { openclaw: "main", opencode: null, copilot: null, + agy: null, }; export const DEFAULT_AUTO_CLI_ORDER: CliProvider[] = [ @@ -115,6 +117,8 @@ export const DEFAULT_AUTO_CLI_ORDER: CliProvider[] = [ "openclaw", "opencode", "copilot", + // agy is intentionally excluded from the default auto-fallback order. + // Use --cli agy or --model cli/agy/ to opt in explicitly. ]; export function parseCliProviderName(raw: string): CliProvider | null { @@ -126,6 +130,7 @@ export function parseCliProviderName(raw: string): CliProvider | null { if (normalized === "openclaw") return "openclaw"; if (normalized === "opencode") return "opencode"; if (normalized === "copilot") return "copilot"; + if (normalized === "agy") return "agy"; return null; } @@ -142,7 +147,9 @@ export function requiredEnvForCliProvider(provider: CliProvider): RequiredModelE ? "CLI_OPENCODE" : provider === "copilot" ? "CLI_COPILOT" - : "CLI_CLAUDE"; + : provider === "agy" + ? "CLI_AGY" + : "CLI_CLAUDE"; } export function getGatewayProviderProfile(provider: GatewayProvider): GatewayProviderProfile { diff --git a/src/model-auto-cli.ts b/src/model-auto-cli.ts index 8e252b1c6..2e3048952 100644 --- a/src/model-auto-cli.ts +++ b/src/model-auto-cli.ts @@ -114,7 +114,9 @@ export function prependCliCandidates({ ? cli?.opencode?.model : provider === "copilot" ? cli?.copilot?.model - : cli?.claude?.model; + : provider === "agy" + ? cli?.agy?.model + : cli?.claude?.model; add(provider, modelOverride); } diff --git a/src/model-spec.ts b/src/model-spec.ts index 8a70314c4..55559cd91 100644 --- a/src/model-spec.ts +++ b/src/model-spec.ts @@ -54,7 +54,8 @@ export type FixedModelSpec = | "CLI_AGENT" | "CLI_OPENCLAW" | "CLI_OPENCODE" - | "CLI_COPILOT"; + | "CLI_COPILOT" + | "CLI_AGY"; cliProvider: CliProvider; cliModel: string | null; }; @@ -191,7 +192,8 @@ export function parseRequestedModelId(raw: string): RequestedModel { providerRaw !== "agent" && providerRaw !== "openclaw" && providerRaw !== "opencode" && - providerRaw !== "copilot" + providerRaw !== "copilot" && + providerRaw !== "agy" ) { throw new Error(`Invalid CLI model id "${trimmed}". Expected cli//.`); } @@ -207,6 +209,7 @@ export function parseRequestedModelId(raw: string): RequestedModel { | "CLI_OPENCLAW" | "CLI_OPENCODE" | "CLI_COPILOT" + | "CLI_AGY" >; const userModelId = cliModel ? `cli/${cliProvider}/${cliModel}` : `cli/${cliProvider}`; return { diff --git a/src/run/cli-fallback-state.ts b/src/run/cli-fallback-state.ts index fcc5f3fc0..ac4dc7220 100644 --- a/src/run/cli-fallback-state.ts +++ b/src/run/cli-fallback-state.ts @@ -18,7 +18,8 @@ function parseCliProvider(value: unknown): CliProvider | null { value === "agent" || value === "openclaw" || value === "opencode" || - value === "copilot" + value === "copilot" || + value === "agy" ) { return value; } diff --git a/src/run/env.ts b/src/run/env.ts index f1a796675..394e826c7 100644 --- a/src/run/env.ts +++ b/src/run/env.ts @@ -84,6 +84,7 @@ export function resolveCliAvailability({ "openclaw", "opencode", "copilot", + "agy", ]; const availability: Partial> = {}; for (const provider of providers) { @@ -113,7 +114,8 @@ export function parseCliUserModelId(modelId: string): { provider !== "agent" && provider !== "openclaw" && provider !== "opencode" && - provider !== "copilot" + provider !== "copilot" && + provider !== "agy" ) { throw new Error(`Invalid CLI model id "${modelId}". Expected cli//.`); } @@ -130,7 +132,8 @@ export function parseCliProviderArg(raw: string): CliProvider { normalized === "agent" || normalized === "openclaw" || normalized === "opencode" || - normalized === "copilot" + normalized === "copilot" || + normalized === "agy" ) { return normalized as CliProvider; } diff --git a/src/run/help.ts b/src/run/help.ts index 6bccd8c9a..395d6c26d 100644 --- a/src/run/help.ts +++ b/src/run/help.ts @@ -141,7 +141,7 @@ export function buildProgram() { .addOption( new Option( "--cli [provider]", - "Use a CLI provider: claude, gemini, codex, agent, openclaw, opencode, copilot (equivalent to --model cli/). If omitted, use auto selection with CLI enabled.", + "Use a CLI provider: claude, gemini, codex, agent, openclaw, opencode, copilot, agy (equivalent to --model cli/). If omitted, use auto selection with CLI enabled.", ), ) .option("--extract", "Print extracted content and exit (no LLM summary)", false) @@ -295,6 +295,7 @@ ${heading("Env Vars")} OPENCLAW_PATH optional (path to OpenClaw CLI binary) OPENCODE_PATH optional (path to OpenCode CLI binary) COPILOT_PATH optional (path to GitHub Copilot CLI binary) + AGY_PATH optional (path to Antigravity CLI binary) SUMMARIZE_MODEL optional (overrides default model selection) SUMMARIZE_THEME optional (${CLI_THEME_NAMES.join(", ")}) SUMMARIZE_TRUECOLOR optional (force 24-bit color) diff --git a/src/run/run-models.ts b/src/run/run-models.ts index 39551427b..ec03b1109 100644 --- a/src/run/run-models.ts +++ b/src/run/run-models.ts @@ -16,6 +16,7 @@ function resolveConfiguredCliModel( if (provider === "agent") return cli?.agent?.model; if (provider === "openclaw") return cli?.openclaw?.model; if (provider === "opencode") return cli?.opencode?.model; + if (provider === "agy") return cli?.agy?.model; return cli?.copilot?.model; })(); return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : null; diff --git a/src/run/run-settings-parse.ts b/src/run/run-settings-parse.ts index 21b6fc60d..519ee9714 100644 --- a/src/run/run-settings-parse.ts +++ b/src/run/run-settings-parse.ts @@ -44,6 +44,7 @@ export const parseCliProvider = (raw: string): CliProvider | null => { if (normalized === "openclaw") return "openclaw"; if (normalized === "opencode") return "opencode"; if (normalized === "copilot") return "copilot"; + if (normalized === "agy") return "agy"; return null; }; diff --git a/src/run/summary-engine.ts b/src/run/summary-engine.ts index 55501ec62..fb7b565e2 100644 --- a/src/run/summary-engine.ts +++ b/src/run/summary-engine.ts @@ -163,6 +163,9 @@ export function createSummaryEngine(deps: SummaryEngineDeps) { if (requiredEnv === "CLI_COPILOT") { return Boolean(deps.cliAvailability.copilot); } + if (requiredEnv === "CLI_AGY") { + return Boolean(deps.cliAvailability.agy); + } if (requiredEnv === "GEMINI_API_KEY") { return deps.keyFlags.googleConfigured; } @@ -212,6 +215,9 @@ export function createSummaryEngine(deps: SummaryEngineDeps) { if (attempt.requiredEnv === "CLI_COPILOT") { return `GitHub Copilot CLI not found for model ${attempt.userModelId}. Install Copilot CLI or set COPILOT_PATH.`; } + if (attempt.requiredEnv === "CLI_AGY") { + return `Antigravity CLI not found for model ${attempt.userModelId}. Install agy or set AGY_PATH.`; + } return `Missing ${attempt.requiredEnv} for model ${attempt.userModelId}. Set the env var or choose a different --model.`; }; diff --git a/src/run/types.ts b/src/run/types.ts index 3a660ca61..7ceca12ef 100644 --- a/src/run/types.ts +++ b/src/run/types.ts @@ -18,7 +18,8 @@ export type ModelAttemptRequiredEnv = | "CLI_AGENT" | "CLI_OPENCLAW" | "CLI_OPENCODE" - | "CLI_COPILOT"; + | "CLI_COPILOT" + | "CLI_AGY"; export type ModelAttempt = { transport: "native" | "openrouter" | "cli"; diff --git a/tests/config.test.ts b/tests/config.test.ts index 4a3bd2f0e..36888a121 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -499,6 +499,21 @@ describe("config loading", () => { expect(() => loadSummarizeConfig({ env: { HOME: root } })).toThrow(/unknown CLI provider/); }); + it("parses agy cli config", () => { + const { root } = writeJsonConfig({ + cli: { + enabled: ["agy"], + agy: { binary: "/usr/local/bin/agy", model: "Gemini 3.5 Flash (Medium)" }, + }, + }); + expect(loadSummarizeConfig({ env: { HOME: root } }).config).toEqual({ + cli: { + enabled: ["agy"], + agy: { binary: "/usr/local/bin/agy", model: "Gemini 3.5 Flash (Medium)" }, + }, + }); + }); + it("parses openclaw cli config", () => { const { root } = writeJsonConfig({ cli: { diff --git a/tests/daemon.agent.test.ts b/tests/daemon.agent.test.ts index 2247ac23e..e9cd65ae6 100644 --- a/tests/daemon.agent.test.ts +++ b/tests/daemon.agent.test.ts @@ -432,4 +432,40 @@ describe("daemon/agent", () => { autoSpy.mockRestore(); } }); + + it("labels CLI_AGY as agy in unavailable-provider diagnostics", async () => { + const home = makeTempHome(); + const autoSpy = vi.spyOn(modelAuto, "buildAutoModelAttempts").mockReturnValue([ + { + transport: "cli", + userModelId: "cli/agy", + llmModelId: null, + openrouterProviders: null, + forceOpenRouter: false, + requiredEnv: "CLI_AGY", + cliProvider: "agy", + cliModel: null, + debug: "agy fallback", + }, + ]); + + try { + await expect( + completeAgentResponse({ + env: { HOME: home, PATH: "" }, + pageUrl: "https://example.com", + pageTitle: null, + pageContent: "Hello world", + messages: [{ role: "user", content: "Hi" }], + modelOverride: null, + tools: [], + automationEnabled: false, + }), + ).rejects.toThrow( + /CLI unavailable: agy/i, + ); + } finally { + autoSpy.mockRestore(); + } + }); }); diff --git a/tests/daemon.chat.test.ts b/tests/daemon.chat.test.ts index e830100ee..6f7ff2d23 100644 --- a/tests/daemon.chat.test.ts +++ b/tests/daemon.chat.test.ts @@ -206,6 +206,42 @@ describe("daemon/chat", () => { expect(meta[0]?.model).toBe("cli/opencode/openai/gpt-5.4"); }); + it("resolves configured agy models before emitting chat metadata", async () => { + const home = mkdtempSync(join(tmpdir(), "summarize-daemon-chat-agy-fixed-")); + const meta: Array<{ model?: string | null }> = []; + + await streamChatResponse({ + env: { HOME: home }, + fetchImpl: fetch, + configForCli: { + cli: { + agy: { + model: "Gemini 3.5 Flash (Medium)", + }, + }, + }, + session: { + id: "s-agy-fixed", + lastMeta: { model: null, modelLabel: null, inputSummary: null, summaryFromCache: null }, + }, + pageUrl: "https://example.com", + pageTitle: "Example", + pageContent: "Hello world", + messages: [{ role: "user", content: "Hi" }], + modelOverride: "cli/agy", + pushToSession: () => {}, + emitMeta: (patch) => meta.push(patch), + }); + + expect(runCliModel).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "agy", + model: "Gemini 3.5 Flash (Medium)", + }), + ); + expect(meta[0]?.model).toBe("cli/agy/Gemini 3.5 Flash (Medium)"); + }); + it("routes openrouter overrides through openrouter transport", async () => { const home = mkdtempSync(join(tmpdir(), "summarize-daemon-chat-openrouter-")); const meta: Array<{ model?: string | null }> = []; diff --git a/tests/daemon.config.test.ts b/tests/daemon.config.test.ts index de699309a..d22e2855d 100644 --- a/tests/daemon.config.test.ts +++ b/tests/daemon.config.test.ts @@ -148,6 +148,8 @@ describe("daemon config", () => { OPENAI_API_KEY: " k ", OPENAI_WHISPER_BASE_URL: " http://127.0.0.1:8080/v1 ", COPILOT_PATH: " /opt/copilot ", + AGY_PATH: " /opt/agy ", + ANTIGRAVITY_API_KEY: " test-key-123 ", PATH: "", SUMMARIZE_TRANSCRIBER: " parakeet ", SUMMARIZE_ONNX_PARAKEET_CMD: " run-parakeet {input} ", @@ -169,6 +171,8 @@ describe("daemon config", () => { OPENAI_API_KEY: "k", OPENAI_WHISPER_BASE_URL: "http://127.0.0.1:8080/v1", COPILOT_PATH: "/opt/copilot", + AGY_PATH: "/opt/agy", + ANTIGRAVITY_API_KEY: "test-key-123", SUMMARIZE_TRANSCRIBER: "parakeet", SUMMARIZE_ONNX_PARAKEET_CMD: "run-parakeet {input}", SUMMARIZE_ONNX_CANARY_CMD: "run-canary {input}", diff --git a/tests/llm.cli.agy.test.ts b/tests/llm.cli.agy.test.ts new file mode 100644 index 000000000..5cda0ade4 --- /dev/null +++ b/tests/llm.cli.agy.test.ts @@ -0,0 +1,136 @@ +import { describe, expect, it } from "vitest"; +import { resolveCliBinary, runCliModel } from "../src/llm/cli.js"; +import type { ExecFileFn } from "../src/markitdown.js"; + +const makeStub = (handler: (args: string[], input?: string) => { stdout?: string; stderr?: string }) => { + const execFileStub: ExecFileFn = ((_cmd, args, _options, cb) => { + const result = handler(args); + if (cb) cb(null, result.stdout ?? "", result.stderr ?? ""); + return { + stdin: { write: (_chunk: unknown) => {}, end: () => {} }, + } as unknown as ReturnType; + }) as ExecFileFn; + return execFileStub; +}; + +describe("runCliModel - agy provider", () => { + it("invokes agy with --print, passes prompt via stdin, returns plain text", async () => { + let seenCmd = ""; + let seenInput = ""; + const seen: string[][] = []; + const execFileImpl: ExecFileFn = ((cmd, args, _options, cb) => { + seenCmd = String(cmd); + seen.push(args); + cb?.(null, " Hello from agy. \n", ""); + return { + stdin: { + write: (chunk: unknown) => { seenInput += String(chunk); }, + end: () => {}, + }, + } as unknown as ReturnType; + }) as ExecFileFn; + + const result = await runCliModel({ + provider: "agy", + prompt: "Summarize this.", + model: null, + allowTools: false, + timeoutMs: 1000, + env: {}, + execFileImpl, + config: null, + }); + + expect(result.text).toBe("Hello from agy."); + expect(result.usage).toBeNull(); + expect(result.costUsd).toBeNull(); + expect(seenCmd).toBe("agy"); + expect(seen[0]).toContain("--print"); + expect(seen[0]).not.toContain("--output-format"); + expect(seenInput).toContain("Summarize this."); + }); + + it("passes --model when model is specified", async () => { + const seen: string[][] = []; + const execFileImpl = makeStub((args) => { + seen.push(args); + return { stdout: "answer text" }; + }); + + const result = await runCliModel({ + provider: "agy", + prompt: "Q?", + model: "Gemini 3.5 Flash (Medium)", + allowTools: false, + timeoutMs: 1000, + env: {}, + execFileImpl, + config: null, + }); + + expect(result.text).toBe("answer text"); + expect(seen[0]).toContain("--print"); + expect(seen[0]).toContain("--model"); + expect(seen[0]).toContain("Gemini 3.5 Flash (Medium)"); + }); + + it("passes --dangerously-skip-permissions when allowTools is true", async () => { + const seen: string[][] = []; + const execFileImpl = makeStub((args) => { + seen.push(args); + return { stdout: "ok" }; + }); + + await runCliModel({ + provider: "agy", + prompt: "Q", + model: null, + allowTools: true, + timeoutMs: 1000, + env: {}, + execFileImpl, + config: null, + }); + + expect(seen[0]).toContain("--dangerously-skip-permissions"); + }); + + it("throws when agy returns empty output", async () => { + const execFileImpl = makeStub(() => ({ stdout: " \n" })); + await expect( + runCliModel({ + provider: "agy", + prompt: "Q", + model: null, + allowTools: false, + timeoutMs: 1000, + env: {}, + execFileImpl, + config: null, + }), + ).rejects.toThrow(/empty output/); + }); + + it("respects AGY_PATH and config-provided binary/extraArgs", async () => { + expect(resolveCliBinary("agy", null, { AGY_PATH: "/custom/agy" })).toBe("/custom/agy"); + expect(resolveCliBinary("agy", { agy: { binary: "/cfg/agy" } }, {})).toBe("/cfg/agy"); + expect(resolveCliBinary("agy", null, {})).toBe("agy"); + + const seen: string[][] = []; + const execFileImpl = makeStub((args) => { + seen.push(args); + return { stdout: "ok" }; + }); + await runCliModel({ + provider: "agy", + prompt: "Q", + model: null, + allowTools: false, + timeoutMs: 1000, + env: {}, + execFileImpl, + config: { agy: { extraArgs: ["--no-color"] } }, + }); + expect(seen[0]?.[0]).toBe("--no-color"); + }); +}); From 08382b9394a1cebc400e87ba9a14a28dd192a648 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 1 Jun 2026 03:27:09 +0100 Subject: [PATCH 2/9] fix: align agy provider with print mode --- CHANGELOG.md | 4 ++++ README.md | 2 +- docs/README.md | 2 +- docs/cli.md | 12 +++++++++--- docs/config.md | 6 ++++-- docs/llm.md | 5 +++-- src/daemon/chat.ts | 2 +- src/llm/cli-provider-output.ts | 5 ++++- src/llm/cli.ts | 3 --- src/llm/provider-profile.ts | 2 +- src/model-auto-cli.ts | 2 +- src/model-spec.ts | 5 +++++ src/run/run-models.ts | 2 +- tests/config.test.ts | 4 ++-- tests/daemon.agent.test.ts | 4 +--- tests/daemon.chat.test.ts | 6 +++--- tests/llm.cli.agy.test.ts | 14 +++++++++----- tests/model-spec.test.ts | 13 +++++++++++++ 18 files changed, 63 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b98a50ea..b7e66a994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add Antigravity CLI (`agy`) as a supported CLI provider. (#231, thanks @yetmike) + ### Fixes - Chrome extension: abort stale side-panel summary streams on tab changes so delayed output from a closed or replaced tab cannot render under the new page title. diff --git a/README.md b/README.md index 17e51571b..f666b1fdb 100644 --- a/README.md +++ b/README.md @@ -363,7 +363,7 @@ Summarize can use common coding CLIs as local model backends: - `agent` (Cursor Agent CLI) -> `--cli agent` / `--model cli/agent/` - `openclaw` -> `--cli openclaw` / `--model cli/openclaw/` or `--model openclaw/` - `opencode` -> `--cli opencode` / `--model cli/opencode/` (`--model cli/opencode` uses the OpenCode runtime default) -- `agy` (Antigravity CLI) -> `--cli agy` / `--model cli/agy/` (uses agy's active session model when no model is specified) +- `agy` (Antigravity CLI) -> `--cli agy` / `--model cli/agy` (uses agy's active session model; per-call model selection is not supported by agy print mode) Built-in preset: diff --git a/docs/README.md b/docs/README.md index 1c0a5d738..c30987739 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ summary: "Docs index for summarize behaviors and modes." - `docs/chrome-extension.md` — Chrome side panel extension + daemon setup/troubleshooting - `docs/cache.md` — cache design + config (SQLite) -- `docs/cli.md` — CLI models (Claude/Codex/Gemini/Agent/OpenClaw/OpenCode/Copilot) +- `docs/cli.md` — CLI models (Claude/Codex/Gemini/Agent/OpenClaw/OpenCode/Copilot/Antigravity) - `docs/config.md` — config file location, precedence, and schema - `docs/extract-only.md` — extract mode (no summary LLM call) - `docs/firecrawl.md` — Firecrawl mode + API key diff --git a/docs/cli.md b/docs/cli.md index 4b80997c6..ee18ddcca 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,14 +1,14 @@ --- title: "CLI providers" kicker: "models" -summary: "CLI model providers and config for Claude, Codex, Gemini, Cursor Agent, OpenClaw, OpenCode, and GitHub Copilot." +summary: "CLI model providers and config for Claude, Codex, Gemini, Cursor Agent, OpenClaw, OpenCode, GitHub Copilot, and Antigravity." read_when: - "When changing CLI model integration." --- # CLI models -Summarize can use installed CLIs (Claude, Codex, Gemini, Cursor Agent, OpenClaw, OpenCode, GitHub Copilot) as local model backends. +Summarize can use installed CLIs (Claude, Codex, Gemini, Cursor Agent, OpenClaw, OpenCode, GitHub Copilot, Antigravity) as local model backends. ## Model ids @@ -22,8 +22,10 @@ Summarize can use installed CLIs (Claude, Codex, Gemini, Cursor Agent, OpenClaw, - `cli/opencode` (use the OpenCode runtime default model) - `cli/copilot/` (e.g. `cli/copilot/gpt-5.2`) - `cli/copilot` (use the Copilot CLI runtime default model) +- `cli/agy` (use the Antigravity CLI active session model) Use `--cli [provider]` (case-insensitive) for the provider default, or `--model cli//` to pin a model. +Antigravity does not support per-call model selection in print mode, so use `cli/agy` without a model suffix. If `--cli` is provided without a provider, auto selection is used with CLI enabled. Codex GPT Fast: @@ -106,7 +108,7 @@ path-based prompt and enables the required tool flags: ```json { "cli": { - "enabled": ["claude", "gemini", "codex", "agent", "openclaw", "opencode", "copilot"], + "enabled": ["claude", "gemini", "codex", "agent", "openclaw", "opencode", "copilot", "agy"], "autoFallback": { "enabled": true, "onlyWhenNoApiKeys": true, @@ -132,6 +134,9 @@ path-based prompt and enables the required tool flags: }, "copilot": { "binary": "/usr/local/bin/copilot" + }, + "agy": { + "binary": "/usr/local/bin/agy" } } } @@ -142,6 +147,7 @@ Notes: - CLI output is treated as text only (no token accounting). - If a CLI call fails, auto mode falls back to the next candidate. - Cursor Agent CLI uses the `agent` binary and relies on Cursor CLI auth (login or `CURSOR_API_KEY`). +- Antigravity CLI uses the active agy session model; `cli.agy.model` is ignored by runtime selection. - Codex CLI normal text summaries run isolated by default: `codex exec --ephemeral --ignore-user-config --ignore-rules -C ...` with a sanitized temporary `CODEX_HOME` that carries auth only. Set `cli.codex.isolated` to `false` only when you intentionally need Codex to inherit local config/rules. - Gemini CLI is invoked in headless mode with `--prompt` for compatibility with current Gemini CLI releases. - OpenClaw uses `openclaw agent --agent --message --json` because current OpenClaw requires `-m/--message`; very large extracted inputs are rejected before launch to avoid argv limits. diff --git a/docs/config.md b/docs/config.md index 92b66bb42..ed938b195 100644 --- a/docs/config.md +++ b/docs/config.md @@ -361,7 +361,7 @@ Examples: ```json { "cli": { - "enabled": ["gemini", "agent", "openclaw", "opencode", "copilot"], + "enabled": ["gemini", "agent", "openclaw", "opencode", "copilot", "agy"], "autoFallback": { "enabled": true, "onlyWhenNoApiKeys": true, @@ -372,7 +372,8 @@ Examples: "agent": { "binary": "/usr/local/bin/agent", "model": "gpt-5.2" }, "openclaw": { "binary": "/usr/local/bin/openclaw", "model": "main" }, "opencode": { "binary": "/usr/local/bin/opencode", "model": "openai/gpt-5.4" }, - "copilot": { "binary": "/usr/local/bin/copilot", "model": "gpt-5.2" } + "copilot": { "binary": "/usr/local/bin/copilot", "model": "gpt-5.2" }, + "agy": { "binary": "/usr/local/bin/agy" } } } ``` @@ -385,6 +386,7 @@ Notes: - Auto fallback stores the last successful provider in `~/.summarize/cli-state.json` and prioritizes it on the next run. - `cli..binary` overrides CLI binary discovery. - `cli..extraArgs` appends extra CLI args. +- Antigravity CLI uses the active agy session model; `cli.agy.model` is ignored by runtime selection. - `cli.codex.isolated` defaults to `true` for normal summaries, adding Codex ephemeral/no-user-config/no-rules flags, a temporary cwd, and a sanitized temporary `CODEX_HOME` that carries auth only. Set it to `false` only when local Codex config/rules are intentional. ## OpenAI config diff --git a/docs/llm.md b/docs/llm.md index 176f1af1b..4d1cb8765 100644 --- a/docs/llm.md +++ b/docs/llm.md @@ -38,7 +38,7 @@ installed, auto mode can use local CLI models via `cli.enabled` or implicit auto - `ANTHROPIC_API_KEY` (required for `anthropic/...` models) - `ANTHROPIC_BASE_URL` (optional; override Anthropic API endpoint) - `SUMMARIZE_MODEL` (optional; overrides default model selection) -- `CLAUDE_PATH` / `CODEX_PATH` / `GEMINI_PATH` / `AGENT_PATH` / `OPENCLAW_PATH` / `OPENCODE_PATH` / `COPILOT_PATH` (optional; override CLI binary paths) +- `CLAUDE_PATH` / `CODEX_PATH` / `GEMINI_PATH` / `AGENT_PATH` / `OPENCLAW_PATH` / `OPENCODE_PATH` / `COPILOT_PATH` / `AGY_PATH` (optional; override CLI binary paths) ## Flags @@ -53,6 +53,7 @@ installed, auto mode can use local CLI models via `cli.enabled` or implicit auto - `cli/openclaw/main` - `cli/opencode/openai/gpt-5.4` - `cli/copilot/gpt-5.2` + - `cli/agy` - `openai/gpt-5.4` - `openai/gpt-5.4-mini` - `openai/gpt-5.4-nano` @@ -69,7 +70,7 @@ installed, auto mode can use local CLI models via `cli.enabled` or implicit auto - `anthropic/claude-sonnet-4-5` - `openrouter/meta-llama/llama-3.3-70b-instruct:free` (force OpenRouter) - `--cli [provider]` - - Examples: `--cli claude`, `--cli Gemini`, `--cli codex`, `--cli agent`, `--cli openclaw`, `--cli opencode`, `--cli copilot` (equivalent to `--model cli/`); `--cli` alone uses auto selection with CLI enabled. + - Examples: `--cli claude`, `--cli Gemini`, `--cli codex`, `--cli agent`, `--cli openclaw`, `--cli opencode`, `--cli copilot`, `--cli agy` (equivalent to `--model cli/`); `--cli` alone uses auto selection with CLI enabled. - `--model auto` - See `docs/model-auto.md` - `--model ` diff --git a/src/daemon/chat.ts b/src/daemon/chat.ts index 0eb2aa619..601d04c7c 100644 --- a/src/daemon/chat.ts +++ b/src/daemon/chat.ts @@ -45,7 +45,7 @@ function resolveConfiguredCliModel( : provider === "opencode" ? cli?.opencode?.model : provider === "agy" - ? cli?.agy?.model + ? null : cli?.copilot?.model; return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : null; } diff --git a/src/llm/cli-provider-output.ts b/src/llm/cli-provider-output.ts index e26eb17a7..72c5c1a44 100644 --- a/src/llm/cli-provider-output.ts +++ b/src/llm/cli-provider-output.ts @@ -1,7 +1,10 @@ import type { CliProvider } from "../config.js"; import type { LlmTokenUsage } from "./generate-text.js"; -export type JsonCliProvider = Exclude; +export type JsonCliProvider = Exclude< + CliProvider, + "codex" | "openclaw" | "opencode" | "copilot" | "agy" +>; const JSON_RESULT_FIELDS = ["result", "response", "output", "message", "text"] as const; diff --git a/src/llm/cli.ts b/src/llm/cli.ts index 63658960a..93533d835 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -376,9 +376,6 @@ export async function runCliModel({ if (provider === "agy") { const agyArgs: string[] = [...providerExtraArgs, "--print"]; - if (requestedModel) { - agyArgs.push("--model", requestedModel); - } if (allowTools) { agyArgs.push("--dangerously-skip-permissions"); } diff --git a/src/llm/provider-profile.ts b/src/llm/provider-profile.ts index f8254b1cf..d727b817c 100644 --- a/src/llm/provider-profile.ts +++ b/src/llm/provider-profile.ts @@ -118,7 +118,7 @@ export const DEFAULT_AUTO_CLI_ORDER: CliProvider[] = [ "opencode", "copilot", // agy is intentionally excluded from the default auto-fallback order. - // Use --cli agy or --model cli/agy/ to opt in explicitly. + // Use --cli agy or --model cli/agy to opt in explicitly. ]; export function parseCliProviderName(raw: string): CliProvider | null { diff --git a/src/model-auto-cli.ts b/src/model-auto-cli.ts index 2e3048952..d7a761b2e 100644 --- a/src/model-auto-cli.ts +++ b/src/model-auto-cli.ts @@ -115,7 +115,7 @@ export function prependCliCandidates({ : provider === "copilot" ? cli?.copilot?.model : provider === "agy" - ? cli?.agy?.model + ? undefined : cli?.claude?.model; add(provider, modelOverride); } diff --git a/src/model-spec.ts b/src/model-spec.ts index 55559cd91..a951ee168 100644 --- a/src/model-spec.ts +++ b/src/model-spec.ts @@ -199,6 +199,11 @@ export function parseRequestedModelId(raw: string): RequestedModel { } const cliProvider = providerRaw as CliProvider; const requestedModel = parts.slice(2).join("/").trim(); + if (cliProvider === "agy" && requestedModel.length > 0) { + throw new Error( + `Invalid CLI model id "${trimmed}". Antigravity CLI uses cli/agy without a model suffix.`, + ); + } const cliModel = requestedModel.length > 0 ? requestedModel : DEFAULT_CLI_MODELS[cliProvider]; const requiredEnv = requiredEnvForCliProvider(cliProvider) as Extract< RequiredModelEnv, diff --git a/src/run/run-models.ts b/src/run/run-models.ts index ec03b1109..5b6c6cac0 100644 --- a/src/run/run-models.ts +++ b/src/run/run-models.ts @@ -16,7 +16,7 @@ function resolveConfiguredCliModel( if (provider === "agent") return cli?.agent?.model; if (provider === "openclaw") return cli?.openclaw?.model; if (provider === "opencode") return cli?.opencode?.model; - if (provider === "agy") return cli?.agy?.model; + if (provider === "agy") return null; return cli?.copilot?.model; })(); return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : null; diff --git a/tests/config.test.ts b/tests/config.test.ts index 36888a121..b60d3e034 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -503,13 +503,13 @@ describe("config loading", () => { const { root } = writeJsonConfig({ cli: { enabled: ["agy"], - agy: { binary: "/usr/local/bin/agy", model: "Gemini 3.5 Flash (Medium)" }, + agy: { binary: "/usr/local/bin/agy", extraArgs: ["--sandbox"] }, }, }); expect(loadSummarizeConfig({ env: { HOME: root } }).config).toEqual({ cli: { enabled: ["agy"], - agy: { binary: "/usr/local/bin/agy", model: "Gemini 3.5 Flash (Medium)" }, + agy: { binary: "/usr/local/bin/agy", extraArgs: ["--sandbox"] }, }, }); }); diff --git a/tests/daemon.agent.test.ts b/tests/daemon.agent.test.ts index e9cd65ae6..b96c1ddf2 100644 --- a/tests/daemon.agent.test.ts +++ b/tests/daemon.agent.test.ts @@ -461,9 +461,7 @@ describe("daemon/agent", () => { tools: [], automationEnabled: false, }), - ).rejects.toThrow( - /CLI unavailable: agy/i, - ); + ).rejects.toThrow(/CLI unavailable: agy/i); } finally { autoSpy.mockRestore(); } diff --git a/tests/daemon.chat.test.ts b/tests/daemon.chat.test.ts index 6f7ff2d23..05d4b3ebd 100644 --- a/tests/daemon.chat.test.ts +++ b/tests/daemon.chat.test.ts @@ -206,7 +206,7 @@ describe("daemon/chat", () => { expect(meta[0]?.model).toBe("cli/opencode/openai/gpt-5.4"); }); - it("resolves configured agy models before emitting chat metadata", async () => { + it("uses agy's active session model for chat metadata", async () => { const home = mkdtempSync(join(tmpdir(), "summarize-daemon-chat-agy-fixed-")); const meta: Array<{ model?: string | null }> = []; @@ -236,10 +236,10 @@ describe("daemon/chat", () => { expect(runCliModel).toHaveBeenCalledWith( expect.objectContaining({ provider: "agy", - model: "Gemini 3.5 Flash (Medium)", + model: null, }), ); - expect(meta[0]?.model).toBe("cli/agy/Gemini 3.5 Flash (Medium)"); + expect(meta[0]?.model).toBe("cli/agy"); }); it("routes openrouter overrides through openrouter transport", async () => { diff --git a/tests/llm.cli.agy.test.ts b/tests/llm.cli.agy.test.ts index 5cda0ade4..42797876e 100644 --- a/tests/llm.cli.agy.test.ts +++ b/tests/llm.cli.agy.test.ts @@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest"; import { resolveCliBinary, runCliModel } from "../src/llm/cli.js"; import type { ExecFileFn } from "../src/markitdown.js"; -const makeStub = (handler: (args: string[], input?: string) => { stdout?: string; stderr?: string }) => { +const makeStub = ( + handler: (args: string[], input?: string) => { stdout?: string; stderr?: string }, +) => { const execFileStub: ExecFileFn = ((_cmd, args, _options, cb) => { const result = handler(args); if (cb) cb(null, result.stdout ?? "", result.stderr ?? ""); @@ -24,7 +26,9 @@ describe("runCliModel - agy provider", () => { cb?.(null, " Hello from agy. \n", ""); return { stdin: { - write: (chunk: unknown) => { seenInput += String(chunk); }, + write: (chunk: unknown) => { + seenInput += String(chunk); + }, end: () => {}, }, } as unknown as ReturnType; @@ -50,7 +54,7 @@ describe("runCliModel - agy provider", () => { expect(seenInput).toContain("Summarize this."); }); - it("passes --model when model is specified", async () => { + it("uses the active agy session model instead of passing --model", async () => { const seen: string[][] = []; const execFileImpl = makeStub((args) => { seen.push(args); @@ -70,8 +74,8 @@ describe("runCliModel - agy provider", () => { expect(result.text).toBe("answer text"); expect(seen[0]).toContain("--print"); - expect(seen[0]).toContain("--model"); - expect(seen[0]).toContain("Gemini 3.5 Flash (Medium)"); + expect(seen[0]).not.toContain("--model"); + expect(seen[0]).not.toContain("Gemini 3.5 Flash (Medium)"); }); it("passes --dangerously-skip-permissions when allowTools is true", async () => { diff --git a/tests/model-spec.test.ts b/tests/model-spec.test.ts index 9dba98b3c..006a61d6a 100644 --- a/tests/model-spec.test.ts +++ b/tests/model-spec.test.ts @@ -74,6 +74,19 @@ describe("model spec parsing", () => { expect(parsed.requiredEnv).toBe("CLI_OPENCODE"); }); + it("uses agy's active session model and rejects model suffixes", () => { + const parsed = parseRequestedModelId("cli/agy"); + expect(parsed.kind).toBe("fixed"); + expect(parsed.transport).toBe("cli"); + expect(parsed.userModelId).toBe("cli/agy"); + expect(parsed.cliProvider).toBe("agy"); + expect(parsed.cliModel).toBeNull(); + expect(parsed.requiredEnv).toBe("CLI_AGY"); + expect(() => parseRequestedModelId("cli/agy/Gemini 3.5 Flash (Medium)")).toThrow( + /without a model suffix/, + ); + }); + it("rejects invalid cli providers", () => { expect(() => parseRequestedModelId("cli/unknown/model")).toThrow(/Invalid CLI model id/); }); From d03de50d6cc9add8e155540a7bb956bc77cdb7e4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 1 Jun 2026 03:38:19 +0100 Subject: [PATCH 3/9] chore: document agy stdin transport --- .gitignore | 1 + src/llm/cli.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index e63d3cc6d..0d4c133e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules .clawpatch/ +.antigravitycli/ dist dist-bun *.bun-build diff --git a/src/llm/cli.ts b/src/llm/cli.ts index 93533d835..53bb6c189 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -379,6 +379,7 @@ export async function runCliModel({ if (allowTools) { agyArgs.push("--dangerously-skip-permissions"); } + // agy print mode accepts stdin; keep prompt content out of argv and process listings. const { stdout } = await execCliWithInput({ execFileImpl: execFileFn, cmd: binary, From 37ec127139bb991bf533eb5b0c078076eb7c3b2a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 1 Jun 2026 03:48:04 +0100 Subject: [PATCH 4/9] fix: propagate agy print timeout --- src/llm/cli.ts | 15 ++++++++++++ tests/llm.cli.agy.test.ts | 49 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/llm/cli.ts b/src/llm/cli.ts index 53bb6c189..ed1a03ead 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -126,6 +126,14 @@ function resolveCodexModelAndArgs( return { model: CODEX_GPT_FAST_MODEL, extraArgs }; } +function hasAnyFlag(args: string[], flags: string[]): boolean { + return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`))); +} + +function goDurationFromMs(timeoutMs: number): string { + return `${Math.max(1, Math.ceil(timeoutMs / 1000))}s`; +} + async function copyCodexAuthFiles(sourceDir: string | undefined, targetDir: string): Promise { const codexHome = sourceDir?.trim() || path.join(homedir(), ".codex"); const authPath = path.join(codexHome, "auth.json"); @@ -376,6 +384,13 @@ export async function runCliModel({ if (provider === "agy") { const agyArgs: string[] = [...providerExtraArgs, "--print"]; + if ( + Number.isFinite(timeoutMs) && + timeoutMs > 0 && + !hasAnyFlag(providerExtraArgs, ["--print-timeout", "-print-timeout"]) + ) { + agyArgs.push("--print-timeout", goDurationFromMs(timeoutMs)); + } if (allowTools) { agyArgs.push("--dangerously-skip-permissions"); } diff --git a/tests/llm.cli.agy.test.ts b/tests/llm.cli.agy.test.ts index 42797876e..25dbd791d 100644 --- a/tests/llm.cli.agy.test.ts +++ b/tests/llm.cli.agy.test.ts @@ -50,6 +50,8 @@ describe("runCliModel - agy provider", () => { expect(result.costUsd).toBeNull(); expect(seenCmd).toBe("agy"); expect(seen[0]).toContain("--print"); + expect(seen[0]).toContain("--print-timeout"); + expect(seen[0]).toContain("1s"); expect(seen[0]).not.toContain("--output-format"); expect(seenInput).toContain("Summarize this."); }); @@ -99,6 +101,53 @@ describe("runCliModel - agy provider", () => { expect(seen[0]).toContain("--dangerously-skip-permissions"); }); + it("passes summarize timeout to agy unless extra args override it", async () => { + const seen: string[][] = []; + const execFileImpl = makeStub((args) => { + seen.push(args); + return { stdout: "ok" }; + }); + + await runCliModel({ + provider: "agy", + prompt: "Q", + model: null, + allowTools: false, + timeoutMs: 125_000, + env: {}, + execFileImpl, + config: null, + }); + expect(seen[0]).toContain("--print-timeout"); + expect(seen[0]).toContain("125s"); + + await runCliModel({ + provider: "agy", + prompt: "Q", + model: null, + allowTools: false, + timeoutMs: 125_000, + env: {}, + execFileImpl, + config: { agy: { extraArgs: ["--print-timeout=10m"] } }, + }); + expect(seen[1]?.filter((arg) => arg.startsWith("--print-timeout"))).toEqual([ + "--print-timeout=10m", + ]); + + await runCliModel({ + provider: "agy", + prompt: "Q", + model: null, + allowTools: false, + timeoutMs: 125_000, + env: {}, + execFileImpl, + config: { agy: { extraArgs: ["-print-timeout=10m"] } }, + }); + expect(seen[2]?.filter((arg) => arg.includes("print-timeout"))).toEqual(["-print-timeout=10m"]); + }); + it("throws when agy returns empty output", async () => { const execFileImpl = makeStub(() => ({ stdout: " \n" })); await expect( From 8013fd7e049b7b05bd6c914980c1be3c0e2839ff Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 1 Jun 2026 03:55:41 +0100 Subject: [PATCH 5/9] fix: avoid auto-approving agy tools --- docs/cli.md | 1 + src/llm/cli.ts | 3 --- tests/llm.cli.agy.test.ts | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index ee18ddcca..008804816 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -102,6 +102,7 @@ path-based prompt and enables the required tool flags: - Agent: uses built-in file tools in `agent --print` mode (no extra flags) - OpenCode: `opencode run --format json ... --file ` when a file/image path is required - Copilot: `copilot -p `; passes `--model ` when one is configured +- Antigravity: `agy --print`; does not auto-approve tools for attachment prompts ## Config diff --git a/src/llm/cli.ts b/src/llm/cli.ts index ed1a03ead..103cd37e8 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -391,9 +391,6 @@ export async function runCliModel({ ) { agyArgs.push("--print-timeout", goDurationFromMs(timeoutMs)); } - if (allowTools) { - agyArgs.push("--dangerously-skip-permissions"); - } // agy print mode accepts stdin; keep prompt content out of argv and process listings. const { stdout } = await execCliWithInput({ execFileImpl: execFileFn, diff --git a/tests/llm.cli.agy.test.ts b/tests/llm.cli.agy.test.ts index 25dbd791d..47777d7a7 100644 --- a/tests/llm.cli.agy.test.ts +++ b/tests/llm.cli.agy.test.ts @@ -80,7 +80,7 @@ describe("runCliModel - agy provider", () => { expect(seen[0]).not.toContain("Gemini 3.5 Flash (Medium)"); }); - it("passes --dangerously-skip-permissions when allowTools is true", async () => { + it("does not auto-approve agy tools when allowTools is true", async () => { const seen: string[][] = []; const execFileImpl = makeStub((args) => { seen.push(args); @@ -98,7 +98,7 @@ describe("runCliModel - agy provider", () => { config: null, }); - expect(seen[0]).toContain("--dangerously-skip-permissions"); + expect(seen[0]).not.toContain("--dangerously-skip-permissions"); }); it("passes summarize timeout to agy unless extra args override it", async () => { From 2867a6b73eb86294591aaa06ff8a3fc2ba15a95f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 1 Jun 2026 04:05:59 +0100 Subject: [PATCH 6/9] fix: sandbox agy text summaries --- docs/cli.md | 1 + src/llm/cli.ts | 52 ++++++++++++++++++++++++--------------- tests/llm.cli.agy.test.ts | 18 ++++++++++++-- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 008804816..c617060ec 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -149,6 +149,7 @@ Notes: - If a CLI call fails, auto mode falls back to the next candidate. - Cursor Agent CLI uses the `agent` binary and relies on Cursor CLI auth (login or `CURSOR_API_KEY`). - Antigravity CLI uses the active agy session model; `cli.agy.model` is ignored by runtime selection. +- Antigravity normal text summaries run in a temporary cwd with `--sandbox`; attachment prompts keep the caller cwd so agy can inspect the requested path but do not auto-approve tools. - Codex CLI normal text summaries run isolated by default: `codex exec --ephemeral --ignore-user-config --ignore-rules -C ...` with a sanitized temporary `CODEX_HOME` that carries auth only. Set `cli.codex.isolated` to `false` only when you intentionally need Codex to inherit local config/rules. - Gemini CLI is invoked in headless mode with `--prompt` for compatibility with current Gemini CLI releases. - OpenClaw uses `openclaw agent --agent --message --json` because current OpenClaw requires `-m/--message`; very large extracted inputs are rejected before launch to avoid argv limits. diff --git a/src/llm/cli.ts b/src/llm/cli.ts index 103cd37e8..992bb4058 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -383,27 +383,39 @@ export async function runCliModel({ } if (provider === "agy") { - const agyArgs: string[] = [...providerExtraArgs, "--print"]; - if ( - Number.isFinite(timeoutMs) && - timeoutMs > 0 && - !hasAnyFlag(providerExtraArgs, ["--print-timeout", "-print-timeout"]) - ) { - agyArgs.push("--print-timeout", goDurationFromMs(timeoutMs)); + const isolatedCwd = !allowTools + ? await fs.mkdtemp(path.join(tmpdir(), "summarize-agy-")) + : null; + try { + const agyArgs: string[] = [...providerExtraArgs, "--print"]; + if (!allowTools && !hasAnyFlag(providerExtraArgs, ["--sandbox"])) { + agyArgs.push("--sandbox"); + } + if ( + Number.isFinite(timeoutMs) && + timeoutMs > 0 && + !hasAnyFlag(providerExtraArgs, ["--print-timeout", "-print-timeout"]) + ) { + agyArgs.push("--print-timeout", goDurationFromMs(timeoutMs)); + } + // agy print mode accepts stdin; keep prompt content out of argv and process listings. + const { stdout } = await execCliWithInput({ + execFileImpl: execFileFn, + cmd: binary, + args: agyArgs, + input: prompt, + timeoutMs, + env: effectiveEnv, + cwd: isolatedCwd ?? cwd, + }); + const text = stdout.trim(); + if (!text) throw new Error("CLI returned empty output"); + return { text, usage: null, costUsd: null }; + } finally { + if (isolatedCwd) { + await fs.rm(isolatedCwd, { recursive: true, force: true }).catch(() => {}); + } } - // agy print mode accepts stdin; keep prompt content out of argv and process listings. - const { stdout } = await execCliWithInput({ - execFileImpl: execFileFn, - cmd: binary, - args: agyArgs, - input: prompt, - timeoutMs, - env: effectiveEnv, - cwd, - }); - const text = stdout.trim(); - if (!text) throw new Error("CLI returned empty output"); - return { text, usage: null, costUsd: null }; } if (!isJsonCliProvider(provider)) { diff --git a/tests/llm.cli.agy.test.ts b/tests/llm.cli.agy.test.ts index 47777d7a7..decee5ae6 100644 --- a/tests/llm.cli.agy.test.ts +++ b/tests/llm.cli.agy.test.ts @@ -18,11 +18,13 @@ const makeStub = ( describe("runCliModel - agy provider", () => { it("invokes agy with --print, passes prompt via stdin, returns plain text", async () => { let seenCmd = ""; + let seenCwd = ""; let seenInput = ""; const seen: string[][] = []; - const execFileImpl: ExecFileFn = ((cmd, args, _options, cb) => { + const execFileImpl: ExecFileFn = ((cmd, args, options, cb) => { seenCmd = String(cmd); seen.push(args); + seenCwd = typeof options?.cwd === "string" ? options.cwd : ""; cb?.(null, " Hello from agy. \n", ""); return { stdin: { @@ -43,6 +45,7 @@ describe("runCliModel - agy provider", () => { env: {}, execFileImpl, config: null, + cwd: "/tmp/agy-original-cwd", }); expect(result.text).toBe("Hello from agy."); @@ -50,9 +53,12 @@ describe("runCliModel - agy provider", () => { expect(result.costUsd).toBeNull(); expect(seenCmd).toBe("agy"); expect(seen[0]).toContain("--print"); + expect(seen[0]).toContain("--sandbox"); expect(seen[0]).toContain("--print-timeout"); expect(seen[0]).toContain("1s"); expect(seen[0]).not.toContain("--output-format"); + expect(seenCwd).toContain("summarize-agy-"); + expect(seenCwd).not.toBe("/tmp/agy-original-cwd"); expect(seenInput).toContain("Summarize this."); }); @@ -82,10 +88,15 @@ describe("runCliModel - agy provider", () => { it("does not auto-approve agy tools when allowTools is true", async () => { const seen: string[][] = []; + let seenCwd = ""; const execFileImpl = makeStub((args) => { seen.push(args); return { stdout: "ok" }; }); + const wrappedExecFileImpl: ExecFileFn = ((cmd, args, options, cb) => { + seenCwd = typeof options?.cwd === "string" ? options.cwd : ""; + return execFileImpl(cmd, args, options, cb); + }) as ExecFileFn; await runCliModel({ provider: "agy", @@ -94,11 +105,14 @@ describe("runCliModel - agy provider", () => { allowTools: true, timeoutMs: 1000, env: {}, - execFileImpl, + execFileImpl: wrappedExecFileImpl, config: null, + cwd: "/tmp/agy-tools-cwd", }); expect(seen[0]).not.toContain("--dangerously-skip-permissions"); + expect(seen[0]).not.toContain("--sandbox"); + expect(seenCwd).toBe("/tmp/agy-tools-cwd"); }); it("passes summarize timeout to agy unless extra args override it", async () => { From 4f7a56a9f8251a6f2951b88052b0fa547eafa806 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 1 Jun 2026 04:26:30 +0100 Subject: [PATCH 7/9] fix: keep agy prompt on stdin --- docs/cli.md | 2 +- src/llm/cli.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index c617060ec..5972f1e23 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -149,7 +149,7 @@ Notes: - If a CLI call fails, auto mode falls back to the next candidate. - Cursor Agent CLI uses the `agent` binary and relies on Cursor CLI auth (login or `CURSOR_API_KEY`). - Antigravity CLI uses the active agy session model; `cli.agy.model` is ignored by runtime selection. -- Antigravity normal text summaries run in a temporary cwd with `--sandbox`; attachment prompts keep the caller cwd so agy can inspect the requested path but do not auto-approve tools. +- Antigravity normal text summaries run `agy --print` in a temporary cwd with `--sandbox`, streaming the prompt over stdin so extracted content is not exposed in argv. Attachment prompts keep the caller cwd so agy can inspect the requested path but do not auto-approve tools. - Codex CLI normal text summaries run isolated by default: `codex exec --ephemeral --ignore-user-config --ignore-rules -C ...` with a sanitized temporary `CODEX_HOME` that carries auth only. Set `cli.codex.isolated` to `false` only when you intentionally need Codex to inherit local config/rules. - Gemini CLI is invoked in headless mode with `--prompt` for compatibility with current Gemini CLI releases. - OpenClaw uses `openclaw agent --agent --message --json` because current OpenClaw requires `-m/--message`; very large extracted inputs are rejected before launch to avoid argv limits. diff --git a/src/llm/cli.ts b/src/llm/cli.ts index 992bb4058..7ece1bf69 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -26,7 +26,7 @@ const DEFAULT_BINARIES: Record = { agy: "agy", }; -const OPENCLAW_MAX_MESSAGE_ARG_BYTES = 120 * 1024; +const CLI_MAX_MESSAGE_ARG_BYTES = 120 * 1024; const CODEX_GPT_FAST_MODEL = "gpt-5.5"; const CODEX_GPT_FAST_ALIASES = new Set(["gpt-fast", "gpt-5.5-fast"]); @@ -218,7 +218,7 @@ export async function runCliModel({ } if (provider === "openclaw") { const promptBytes = Buffer.byteLength(prompt, "utf8"); - if (promptBytes > OPENCLAW_MAX_MESSAGE_ARG_BYTES) { + if (promptBytes > CLI_MAX_MESSAGE_ARG_BYTES) { throw new Error( `OpenClaw CLI requires --message and cannot safely receive large prompts over argv (${promptBytes} bytes). ` + "Use a different CLI provider for this input, reduce extracted content, or update OpenClaw to support stdin/file input.", @@ -387,7 +387,7 @@ export async function runCliModel({ ? await fs.mkdtemp(path.join(tmpdir(), "summarize-agy-")) : null; try { - const agyArgs: string[] = [...providerExtraArgs, "--print"]; + const agyArgs: string[] = [...providerExtraArgs]; if (!allowTools && !hasAnyFlag(providerExtraArgs, ["--sandbox"])) { agyArgs.push("--sandbox"); } @@ -398,7 +398,7 @@ export async function runCliModel({ ) { agyArgs.push("--print-timeout", goDurationFromMs(timeoutMs)); } - // agy print mode accepts stdin; keep prompt content out of argv and process listings. + agyArgs.push("--print"); const { stdout } = await execCliWithInput({ execFileImpl: execFileFn, cmd: binary, From bec9c6e2ec0ff67c8567afc879a36f029f59b798 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 1 Jun 2026 04:37:56 +0100 Subject: [PATCH 8/9] fix: pass agy stdin prompt safely --- docs/cli.md | 2 +- src/llm/cli.ts | 3 ++- tests/llm.cli.agy.test.ts | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 5972f1e23..556ae66a3 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -149,7 +149,7 @@ Notes: - If a CLI call fails, auto mode falls back to the next candidate. - Cursor Agent CLI uses the `agent` binary and relies on Cursor CLI auth (login or `CURSOR_API_KEY`). - Antigravity CLI uses the active agy session model; `cli.agy.model` is ignored by runtime selection. -- Antigravity normal text summaries run `agy --print` in a temporary cwd with `--sandbox`, streaming the prompt over stdin so extracted content is not exposed in argv. Attachment prompts keep the caller cwd so agy can inspect the requested path but do not auto-approve tools. +- Antigravity normal text summaries run `agy --print ""` in a temporary cwd with `--sandbox`, streaming the real prompt over stdin so extracted content is not exposed in argv. Attachment prompts keep the caller cwd so agy can inspect the requested path but do not auto-approve tools. - Codex CLI normal text summaries run isolated by default: `codex exec --ephemeral --ignore-user-config --ignore-rules -C ...` with a sanitized temporary `CODEX_HOME` that carries auth only. Set `cli.codex.isolated` to `false` only when you intentionally need Codex to inherit local config/rules. - Gemini CLI is invoked in headless mode with `--prompt` for compatibility with current Gemini CLI releases. - OpenClaw uses `openclaw agent --agent --message --json` because current OpenClaw requires `-m/--message`; very large extracted inputs are rejected before launch to avoid argv limits. diff --git a/src/llm/cli.ts b/src/llm/cli.ts index 7ece1bf69..969b66020 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -29,6 +29,7 @@ const DEFAULT_BINARIES: Record = { const CLI_MAX_MESSAGE_ARG_BYTES = 120 * 1024; const CODEX_GPT_FAST_MODEL = "gpt-5.5"; const CODEX_GPT_FAST_ALIASES = new Set(["gpt-fast", "gpt-5.5-fast"]); +const AGY_STDIN_PROMPT_ARG = "Read the prompt from standard input and answer it."; const PROVIDER_PATH_ENV: Record = { claude: "CLAUDE_PATH", @@ -391,6 +392,7 @@ export async function runCliModel({ if (!allowTools && !hasAnyFlag(providerExtraArgs, ["--sandbox"])) { agyArgs.push("--sandbox"); } + agyArgs.push("--print", AGY_STDIN_PROMPT_ARG); if ( Number.isFinite(timeoutMs) && timeoutMs > 0 && @@ -398,7 +400,6 @@ export async function runCliModel({ ) { agyArgs.push("--print-timeout", goDurationFromMs(timeoutMs)); } - agyArgs.push("--print"); const { stdout } = await execCliWithInput({ execFileImpl: execFileFn, cmd: binary, diff --git a/tests/llm.cli.agy.test.ts b/tests/llm.cli.agy.test.ts index decee5ae6..4756f2764 100644 --- a/tests/llm.cli.agy.test.ts +++ b/tests/llm.cli.agy.test.ts @@ -53,6 +53,7 @@ describe("runCliModel - agy provider", () => { expect(result.costUsd).toBeNull(); expect(seenCmd).toBe("agy"); expect(seen[0]).toContain("--print"); + expect(seen[0]).toContain("Read the prompt from standard input and answer it."); expect(seen[0]).toContain("--sandbox"); expect(seen[0]).toContain("--print-timeout"); expect(seen[0]).toContain("1s"); @@ -82,6 +83,7 @@ describe("runCliModel - agy provider", () => { expect(result.text).toBe("answer text"); expect(seen[0]).toContain("--print"); + expect(seen[0]).toContain("Read the prompt from standard input and answer it."); expect(seen[0]).not.toContain("--model"); expect(seen[0]).not.toContain("Gemini 3.5 Flash (Medium)"); }); From bb9c485c1300a80c1e58b403563dfc7c40f0010a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 1 Jun 2026 04:46:34 +0100 Subject: [PATCH 9/9] fix: use agy stdin print mode --- docs/cli.md | 2 +- src/llm/cli.ts | 4 ++-- tests/llm.cli.agy.test.ts | 2 -- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 556ae66a3..cbc2e2875 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -149,7 +149,7 @@ Notes: - If a CLI call fails, auto mode falls back to the next candidate. - Cursor Agent CLI uses the `agent` binary and relies on Cursor CLI auth (login or `CURSOR_API_KEY`). - Antigravity CLI uses the active agy session model; `cli.agy.model` is ignored by runtime selection. -- Antigravity normal text summaries run `agy --print ""` in a temporary cwd with `--sandbox`, streaming the real prompt over stdin so extracted content is not exposed in argv. Attachment prompts keep the caller cwd so agy can inspect the requested path but do not auto-approve tools. +- Antigravity normal text summaries run `agy --print` with no prompt argument in a temporary cwd with `--sandbox`, streaming the prompt over stdin so extracted content is not exposed in argv. Attachment prompts keep the caller cwd so agy can inspect the requested path but do not auto-approve tools. - Codex CLI normal text summaries run isolated by default: `codex exec --ephemeral --ignore-user-config --ignore-rules -C ...` with a sanitized temporary `CODEX_HOME` that carries auth only. Set `cli.codex.isolated` to `false` only when you intentionally need Codex to inherit local config/rules. - Gemini CLI is invoked in headless mode with `--prompt` for compatibility with current Gemini CLI releases. - OpenClaw uses `openclaw agent --agent --message --json` because current OpenClaw requires `-m/--message`; very large extracted inputs are rejected before launch to avoid argv limits. diff --git a/src/llm/cli.ts b/src/llm/cli.ts index 969b66020..deee9647f 100644 --- a/src/llm/cli.ts +++ b/src/llm/cli.ts @@ -29,7 +29,6 @@ const DEFAULT_BINARIES: Record = { const CLI_MAX_MESSAGE_ARG_BYTES = 120 * 1024; const CODEX_GPT_FAST_MODEL = "gpt-5.5"; const CODEX_GPT_FAST_ALIASES = new Set(["gpt-fast", "gpt-5.5-fast"]); -const AGY_STDIN_PROMPT_ARG = "Read the prompt from standard input and answer it."; const PROVIDER_PATH_ENV: Record = { claude: "CLAUDE_PATH", @@ -392,7 +391,8 @@ export async function runCliModel({ if (!allowTools && !hasAnyFlag(providerExtraArgs, ["--sandbox"])) { agyArgs.push("--sandbox"); } - agyArgs.push("--print", AGY_STDIN_PROMPT_ARG); + // With no prompt argument, agy print mode reads the prompt from stdin. + agyArgs.push("--print"); if ( Number.isFinite(timeoutMs) && timeoutMs > 0 && diff --git a/tests/llm.cli.agy.test.ts b/tests/llm.cli.agy.test.ts index 4756f2764..decee5ae6 100644 --- a/tests/llm.cli.agy.test.ts +++ b/tests/llm.cli.agy.test.ts @@ -53,7 +53,6 @@ describe("runCliModel - agy provider", () => { expect(result.costUsd).toBeNull(); expect(seenCmd).toBe("agy"); expect(seen[0]).toContain("--print"); - expect(seen[0]).toContain("Read the prompt from standard input and answer it."); expect(seen[0]).toContain("--sandbox"); expect(seen[0]).toContain("--print-timeout"); expect(seen[0]).toContain("1s"); @@ -83,7 +82,6 @@ describe("runCliModel - agy provider", () => { expect(result.text).toBe("answer text"); expect(seen[0]).toContain("--print"); - expect(seen[0]).toContain("Read the prompt from standard input and answer it."); expect(seen[0]).not.toContain("--model"); expect(seen[0]).not.toContain("Gemini 3.5 Flash (Medium)"); });