diff --git a/README-zh.md b/README-zh.md index 516b2f571..af76981f2 100644 --- a/README-zh.md +++ b/README-zh.md @@ -201,7 +201,7 @@ ai-devkit init --template ./senior-engineer.yaml | [Pi](https://pi.dev) | yes | yes | | [Cursor](https://cursor.sh/) | yes | — | | [GitHub Copilot](https://code.visualstudio.com/) | yes | — | -| [Antigravity](https://antigravity.google/) | yes | — | +| [Antigravity](https://antigravity.google/) | yes | yes | | [Amp](https://ampcode.com/) | yes | — | | [Kilo Code](https://github.com/Kilo-Org/kilocode) | yes | — | | [Roo Code](https://roocode.com/) | testing | — | diff --git a/README.md b/README.md index 417efaf46..13ae4b46a 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ One `.ai-devkit.json` configures all of them. Add a new agent to your team witho | [Kiro CLI](https://kiro.dev/cli/) | yes | yes | | [Cursor](https://cursor.sh/) | yes | — | | [GitHub Copilot](https://code.visualstudio.com/) | yes | — | -| [Antigravity](https://antigravity.google/) | yes | — | +| [Antigravity](https://antigravity.google/) | yes | yes | | [Amp](https://ampcode.com/) | yes | — | | [Kilo Code](https://github.com/Kilo-Org/kilocode) | yes | — | | [Roo Code](https://roocode.com/) | testing | — | diff --git a/docs/ai/design/feature-antigravity-cli-adapter.md b/docs/ai/design/feature-antigravity-cli-adapter.md new file mode 100644 index 000000000..888f0906a --- /dev/null +++ b/docs/ai/design/feature-antigravity-cli-adapter.md @@ -0,0 +1,37 @@ +--- +phase: design +title: "Antigravity CLI Adapter in @ai-devkit/agent-manager - Design" +feature: antigravity-cli-adapter +description: Architecture for the Antigravity (`agy`) CLI detection adapter, launch map entry, and CLI wiring +--- + +# Design: Antigravity CLI Adapter for @ai-devkit/agent-manager + +## Responsibilities +- `AntigravityCliAdapter`: discover running `agy` processes, resolve each to its conversation via `cache/last_conversations.json`, read its transcript, emit `AgentInfo`, and enumerate conversations — parsing kept inline like the other recent adapters. +- `AGENTS.antigravity_cli`: launch command (`agy`) + `ps` matcher. +- CLI/channel: register the adapter, label the type, validate `--type`. + +## Data model +Reuse `AgentAdapter`, `AgentInfo`, `AgentStatus`, `AgentType`, `SessionSummary`. `AgentType` gains `'antigravity_cli'`. + +- `cache/last_conversations.json` (at the CLI home root): `{ : }`. The live process cwd is the join key; there is no pid in this file, so `proc.cwd` is the lookup key. +- `brain//.system_generated/logs/transcript.jsonl`: `{ source, type, created_at, content }` per line. `content` is a string or an array of `{ type: "text", text }` blocks. User prompt = text inside `...` of a `type: "USER_INPUT"` record; assistant reply = `type: "PLANNER_RESPONSE"`. Other MODEL records (tool calls like RUN_COMMAND) and `SYSTEM` records are execution detail, skipped unless verbose. + +Normalized into `AgentInfo`: `name` ← `generateAgentName(projectPath, pid)`; `projectPath` ← cwd from the registry (else process cwd); `sessionId` ← the conversation id; `summary` ← last ``; `status` ← time-threshold + last-transcript-role heuristic; `sessionFilePath` ← the conversation's `transcript.jsonl`. + +## Component breakdown +1. `packages/agent-manager/src/adapters/AntigravityCliAdapter.ts` (new, self-contained) + - `canHandle`: argv[0] basename `agy`/`agy.exe`. + - `detectAgents`: `enrichProcesses(listAgentProcesses('agy'))`; per process resolve cwd → conversationId via `readRegistry()`, read the transcript; live processes with no conversation → process-only RUNNING agents. + - inline parsing: `readRegistry`, `readSession`, `parseTranscript` (`` prompts + `PLANNER_RESPONSE` replies), `determineStatus`, `getConversation`. + - `listSessions`: iterate the `cwd → id` registry, parse each transcript, strict `cwd` filter. +2. `AntigravityCliAdapter.test.ts` (new): fixtures from the captured real format. +3. Exports, launch map, CLI registration, labels, validation. + +## Design decisions +- Resolve a live process's conversation from `cache/last_conversations.json` (cwd → id), not from cwd-encoded session dirs; there is one current conversation per workspace. +- Parse `transcript.jsonl` for conversation and summary; take the prompt inside `...`. +- `sessionFilePath` points at `transcript.jsonl` so the console's `fs.stat().mtime` cache invalidation tracks conversation growth. +- Keep parsing resilient — a missing/malformed transcript skips the session; adapter-level failures return empty so other adapters still render. +- Independent of the `antigravity` IDE environment (same split as `gemini_cli` vs `gemini`). diff --git a/docs/ai/implementation/feature-antigravity-cli-adapter.md b/docs/ai/implementation/feature-antigravity-cli-adapter.md new file mode 100644 index 000000000..0f5469e97 --- /dev/null +++ b/docs/ai/implementation/feature-antigravity-cli-adapter.md @@ -0,0 +1,27 @@ +--- +phase: implementation +title: "Antigravity CLI Adapter in @ai-devkit/agent-manager - Implementation" +feature: antigravity-cli-adapter +description: Implementation notes for the Antigravity (`agy`) CLI adapter +--- + +# Implementation: Antigravity CLI Adapter + +## What shipped +- `packages/agent-manager/src/adapters/AntigravityCliAdapter.ts` — self-contained adapter. +- `AgentType`/`StartableAgentType` gain `'antigravity_cli'`; `AGENTS.antigravity_cli = { command: 'agy', matches: matchArgv0('agy') }`; exported from `adapters/index.ts` and `index.ts`. +- CLI: registered in `commands/agent.ts` and `services/channel/channel-runner.ts`; `TYPE_LABELS.antigravity_cli = 'Antigravity CLI'`; `--type` help + `VALID_AGENT_TYPES` updated. + +## Notes +- Home: `~/.gemini/antigravity-cli/` (override via `ANTIGRAVITY_CLI_HOME`). A live process is resolved to its conversation via `cache/last_conversations.json` (`{cwd:id}`); the process cwd is the join key. +- Parsing: `transcript.jsonl` → user prompts are the text inside `...` of `type:'USER_INPUT'` records; the assistant reply is a `type:'PLANNER_RESPONSE'` record; other MODEL records (tool calls like RUN_COMMAND) and `SYSTEM` records are skipped unless verbose. `lastActive` is the newest record `created_at` (else file mtime). +- `AgentInfo.sessionFilePath` / `SessionSummary.sessionFilePath` point at `transcript.jsonl`; `getConversation` accepts the file path, a session dir, or a bare conversation id. + +## Patterns & best practices +- Mirror `GrokCliAdapter`'s shape (registry JSON + transcript JSONL), parsing kept **inline** like the other recent adapters. +- Fail soft: a missing/malformed `transcript.jsonl` skips the session; adapter-level failures return empty so other adapters still render. + +## Error handling +- Missing `cache/last_conversations.json` → empty registry, no throw. +- Mapped conversation with no transcript → process-only RUNNING agent. +- A live `agy` process whose cwd is not in the registry → process-only RUNNING agent. diff --git a/docs/ai/planning/feature-antigravity-cli-adapter.md b/docs/ai/planning/feature-antigravity-cli-adapter.md new file mode 100644 index 000000000..d2772a2aa --- /dev/null +++ b/docs/ai/planning/feature-antigravity-cli-adapter.md @@ -0,0 +1,24 @@ +--- +phase: planning +title: "Antigravity CLI Adapter in @ai-devkit/agent-manager - Planning" +feature: antigravity-cli-adapter +description: Task plan for the Antigravity (`agy`) CLI adapter +--- + +# Planning: Antigravity CLI Adapter + +## Key facts +- Conversation source: `brain//.system_generated/logs/transcript.jsonl`. Live cwd→id from `cache/last_conversations.json`. +- Binary `agy`; home `~/.gemini/antigravity-cli/` (override `ANTIGRAVITY_CLI_HOME`). + +## Tasks +- [x] Capture real layout: `agy -p` produces `cache/last_conversations.json` (`{cwd:id}`) + `brain//.system_generated/logs/transcript.jsonl`. +- [x] Confirm schemas (`{cwd:id}` registry; transcript `{source,type,created_at,content}` with `` prompts and `MODEL` responses). +- [x] Implement self-contained `AntigravityCliAdapter` (`canHandle`, `detectAgents` via registry, inline `transcript.jsonl` parsing, `getConversation`, `listSessions`) + unit tests. +- [x] Add `'antigravity_cli'` to `AgentType`/`StartableAgentType`; `AGENTS.antigravity_cli` launch map; exports. +- [x] Wire CLI: register in `agent` command + channel runner; `TYPE_LABELS`; `--type` help; `VALID_AGENT_TYPES`. +- [x] Update cli test fixtures (adapter mock, registerAdapter counts, `STARTABLE_AGENT_TYPES`, `--type` list). + +## Risks +- Risk: Antigravity CLI schema evolves. Mitigation: defensive parsing, fixtures for partial/malformed inputs. +- Risk: `proc.cwd` unavailable for some processes → falls back to process-only agent (still listed). diff --git a/docs/ai/requirements/feature-antigravity-cli-adapter.md b/docs/ai/requirements/feature-antigravity-cli-adapter.md new file mode 100644 index 000000000..77a67398f --- /dev/null +++ b/docs/ai/requirements/feature-antigravity-cli-adapter.md @@ -0,0 +1,31 @@ +--- +phase: requirements +title: "Antigravity CLI Adapter in @ai-devkit/agent-manager - Requirements" +feature: antigravity-cli-adapter +description: Detect, list, and manage Google's Antigravity (`agy`) CLI agents in the ai-devkit control plane +--- + +# Requirements: Antigravity CLI Adapter for @ai-devkit/agent-manager + +## Problem +ai-devkit already configures the Antigravity **IDE** via the `antigravity` environment, but it cannot see or control the Antigravity **CLI** (`agy`). Running `agy` sessions do not show in `agent list`, `agent sessions`, the console, or `agent send`. This mirrors how `gemini` (env) and `gemini_cli` (adapter) are separate; Antigravity needs its CLI adapter. + +## Goals +- Detect running `agy` processes and surface them in `agent list` / console. +- List historical Antigravity CLI conversations in `agent sessions --type antigravity_cli`. +- Read a conversation transcript for `agent detail` / `agent send` targeting. +- Keep the existing `antigravity` IDE environment untouched. + +## Non-goals +- Launching is data-driven from `AGENTS`; no bespoke start flow. +- No changes to the Antigravity IDE environment or its skill paths. + +## On-disk facts (verified against `agy`, Google Gemini-family CLI) +- Home: `~/.gemini/antigravity-cli/` (`ANTIGRAVITY_CLI_HOME` overrides it). +- `cache/last_conversations.json`: a `{ "": "" }` map of the current conversation per workspace. This is the join key from a live process's cwd to its conversation. +- Transcript: `brain//.system_generated/logs/transcript.jsonl`, newline-delimited `{ source, type, created_at, content }`. User turns are `type: "USER_INPUT"` with the prompt inside `...`; the model reply is a `type: "PLANNER_RESPONSE"` record. Other MODEL records (tool calls such as RUN_COMMAND) and `SYSTEM` records (conversation history, checkpoints) are non-conversational. +- The `agy` binary's argv[0] basename is `agy`. + +## Acceptance +- Unit tests cover: `canHandle`, no-process, registry cwd→id resolution, process-only fallback, missing transcript, transcript conversation extraction, status mapping, `listSessions` + cwd filter. +- Verified end-to-end against a real `agy 0.x` session on disk (`agent-manager:test` and `cli:test` pass). diff --git a/docs/ai/testing/feature-antigravity-cli-adapter.md b/docs/ai/testing/feature-antigravity-cli-adapter.md new file mode 100644 index 000000000..6c991ffef --- /dev/null +++ b/docs/ai/testing/feature-antigravity-cli-adapter.md @@ -0,0 +1,30 @@ +--- +phase: testing +title: "Antigravity CLI Adapter in @ai-devkit/agent-manager - Testing" +feature: antigravity-cli-adapter +description: Test strategy and coverage for the Antigravity (`agy`) CLI adapter +--- + +# Testing Strategy: Antigravity CLI Adapter + +## Unit Tests (`AntigravityCliAdapter.test.ts`) +- [x] Exposes `antigravity_cli` type +- [x] `canHandle` true for `agy` (plain + full path + args); false for non-agy / arg-only matches +- [x] `detectAgents` returns `[]` with no processes +- [x] Resolves the conversation via `last_conversations.json` (cwd → id) +- [x] Process-only RUNNING fallback when the cwd is not in the registry +- [x] Process-only when the mapped conversation has no transcript +- [x] `getConversation` maps `USER_INPUT` (``) + `PLANNER_RESPONSE` records to roles; excludes MODEL tool calls (RUN_COMMAND) except in verbose; skips malformed lines +- [x] `getConversation` excludes `SYSTEM` records unless verbose +- [x] Status: WAITING on trailing MODEL turn; RUNNING on trailing USER_INPUT; IDLE past threshold +- [x] Agent summary is the last user request +- [x] `listSessions` returns `[]` without a registry; lists from the registry with cwd; applies the cwd filter + +## Integration / launch map +- [x] `AGENTS.antigravity_cli.command === 'agy'`; registered in `agent` command + channel runner; `STARTABLE_AGENT_TYPES` includes `antigravity_cli`; `--type antigravity_cli` accepted. + +## End-to-End (verified against a real `agy` install) +- [x] `listSessions()` against the real `~/.gemini/antigravity-cli` lists the on-disk conversation with `cwd`, `firstUserMessage`, and the `transcript.jsonl` `sessionFilePath`. +- [x] `getConversation()` against the real transcript returns the `[{user}, {assistant}]` turns (`` extracted, `MODEL` response, `SYSTEM` skipped). +- [x] `detectAgents()` with a live `agy` process + real registry resolves the cwd, picks the conversation, and surfaces `{type:"antigravity_cli", projectPath:, summary:}`. +- [x] Regression: full `agent-manager` and `cli` suites pass. diff --git a/packages/agent-manager/src/__tests__/adapters/AntigravityCliAdapter.test.ts b/packages/agent-manager/src/__tests__/adapters/AntigravityCliAdapter.test.ts new file mode 100644 index 000000000..9a9e0892b --- /dev/null +++ b/packages/agent-manager/src/__tests__/adapters/AntigravityCliAdapter.test.ts @@ -0,0 +1,324 @@ +/** + * Tests for AntigravityCliAdapter + * + * The adapter resolves a live `agy` process to its conversation via + * ~/.gemini/antigravity-cli/cache/last_conversations.json (cwd -> conversationId) + * and reads session details from brain//.system_generated/logs/transcript.jsonl. + */ + +import type { MockedFunction } from "vitest"; +import * as fs from "fs"; +import * as os from "os"; +import * as path from "path"; + +import { AntigravityCliAdapter } from "../../adapters/AntigravityCliAdapter.js"; +import type { ProcessInfo } from "../../adapters/AgentAdapter.js"; +import { AgentStatus } from "../../adapters/AgentAdapter.js"; +import { listAgentProcesses, enrichProcesses } from "../../utils/process.js"; +import { generateAgentName } from "../../utils/matching.js"; + +vi.mock("../../utils/process.js", async (importOriginal) => { + const actual = (await importOriginal()) as typeof import("../../utils/process.js"); + return { + ...actual, + listAgentProcesses: vi.fn(), + enrichProcesses: vi.fn(), + }; +}); + +vi.mock("../../utils/matching.js", async (importOriginal) => { + const actual = (await importOriginal()) as typeof import("../../utils/matching.js"); + return { + ...actual, + generateAgentName: vi.fn(), + }; +}); + +const mockedListAgentProcesses = listAgentProcesses as MockedFunction; +const mockedEnrichProcesses = enrichProcesses as MockedFunction; +const mockedGenerateAgentName = generateAgentName as MockedFunction; + +const CONVERSATION_ID = "10485e13-2742-4e9e-b286-ac0606f0cb1e"; + +const iso = (d: Date) => d.toISOString().replace(/\.\d+Z$/, "Z"); +/** A user transcript record: real prompt wrapped in like agy writes it. */ +const userRecord = (text: string, at?: Date) => ({ + source: "USER_EXPLICIT", + type: "USER_INPUT", + created_at: iso(at ?? new Date()), + content: `\n${text}\n\nignored`, +}); +const modelRecord = (text: string, at?: Date) => ({ + source: "MODEL", + type: "PLANNER_RESPONSE", + created_at: iso(at ?? new Date()), + content: text, +}); +/** A MODEL tool-call record (e.g. RUN_COMMAND) — execution detail, not a reply. */ +const toolRecord = (text: string, at?: Date) => ({ + source: "MODEL", + type: "RUN_COMMAND", + created_at: iso(at ?? new Date()), + content: text, +}); +const systemRecord = (text: string, at?: Date) => ({ + source: "SYSTEM", + type: "CHECKPOINT", + created_at: iso(at ?? new Date()), + content: text, +}); + +describe("AntigravityCliAdapter", () => { + let adapter: AntigravityCliAdapter; + let base: string; + let cwd: string; + + beforeEach(() => { + base = fs.mkdtempSync(path.join(os.tmpdir(), "agy-adapter-test-")); + process.env.ANTIGRAVITY_CLI_HOME = base; + cwd = "/Users/dev/my-project"; + + adapter = new AntigravityCliAdapter(); + + mockedListAgentProcesses.mockReset(); + mockedEnrichProcesses.mockReset(); + mockedGenerateAgentName.mockReset(); + + mockedEnrichProcesses.mockImplementation((procs) => procs); + mockedGenerateAgentName.mockImplementation( + (c: string, pid: number) => `${path.basename(c) || "unknown"}-${pid}`, + ); + }); + + afterEach(() => { + delete process.env.ANTIGRAVITY_CLI_HOME; + fs.rmSync(base, { recursive: true, force: true }); + }); + + /** Write brain//.system_generated/logs/transcript.jsonl. */ + function writeTranscript(opts: { + id?: string; + records?: object[]; + transcript?: boolean; + }): string { + const id = opts.id ?? CONVERSATION_ID; + const dir = path.join(base, "brain", id, ".system_generated", "logs"); + fs.mkdirSync(dir, { recursive: true }); + if (opts.transcript !== false) { + const records = opts.records ?? [userRecord("fix the bug")]; + fs.writeFileSync( + path.join(dir, "transcript.jsonl"), + records.map((r) => JSON.stringify(r)).join("\n"), + ); + } + return path.join(dir, "transcript.jsonl"); + } + + /** Write cache/last_conversations.json (cwd -> conversationId). */ + function writeRegistry(map: Record): void { + const dir = path.join(base, "cache"); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, "last_conversations.json"), JSON.stringify(map)); + } + + function proc(overrides: Partial = {}): ProcessInfo { + return { + pid: 4242, + ppid: 1, + command: "agy", + cwd, + tty: "ttys010", + startTime: new Date(), + ...overrides, + }; + } + + describe("initialization", () => { + it("exposes the antigravity_cli type", () => { + expect(adapter.type).toBe("antigravity_cli"); + }); + }); + + describe("canHandle", () => { + it("returns true for a plain agy command", () => { + expect(adapter.canHandle(proc({ command: "agy" }))).toBe(true); + }); + + it("returns true for agy with a full path and args", () => { + expect( + adapter.canHandle( + proc({ command: "/Users/dev/.local/bin/agy --dangerously-skip-permissions" }), + ), + ).toBe(true); + }); + + it("returns false for non-agy processes", () => { + expect(adapter.canHandle(proc({ command: "node app.js" }))).toBe(false); + }); + + it('returns false when "agy" appears only in an argument path', () => { + expect(adapter.canHandle(proc({ command: "node /path/to/agy-thing.js" }))).toBe(false); + }); + }); + + describe("detectAgents", () => { + it("returns [] when there are no agy processes", async () => { + mockedListAgentProcesses.mockReturnValue([]); + expect(await adapter.detectAgents()).toEqual([]); + }); + + it("resolves the conversation via last_conversations.json (cwd -> id)", async () => { + writeTranscript({}); + writeRegistry({ [cwd]: CONVERSATION_ID }); + mockedListAgentProcesses.mockReturnValue([proc()]); + + const agents = await adapter.detectAgents(); + + expect(agents).toHaveLength(1); + expect(agents[0]).toMatchObject({ + type: "antigravity_cli", + pid: 4242, + projectPath: cwd, + sessionId: CONVERSATION_ID, + summary: "fix the bug", + }); + expect(agents[0].sessionFilePath).toBe( + path.join(base, "brain", CONVERSATION_ID, ".system_generated", "logs", "transcript.jsonl"), + ); + }); + + it("falls back to a process-only RUNNING agent when the cwd is not in the registry", async () => { + writeTranscript({}); + writeRegistry({ "/some/other/cwd": CONVERSATION_ID }); + mockedListAgentProcesses.mockReturnValue([proc()]); + + const agents = await adapter.detectAgents(); + + expect(agents).toHaveLength(1); + expect(agents[0].status).toBe(AgentStatus.RUNNING); + expect(agents[0].sessionId).toBe("pid-4242"); + expect(agents[0].sessionFilePath).toBeUndefined(); + }); + + it("is process-only when the mapped conversation has no transcript", async () => { + writeTranscript({ transcript: false }); + writeRegistry({ [cwd]: CONVERSATION_ID }); + mockedListAgentProcesses.mockReturnValue([proc()]); + + const agents = await adapter.detectAgents(); + + expect(agents[0].sessionId).toBe("pid-4242"); + }); + }); + + describe("getConversation", () => { + it("maps USER_INPUT () and MODEL records to roles", () => { + const file = writeTranscript({ records: [userRecord("hi"), modelRecord("hello")] }); + expect(adapter.getConversation(file)).toEqual([ + { role: "user", content: "hi" }, + { role: "assistant", content: "hello" }, + ]); + }); + + it("accepts a bare conversation id and skips malformed lines", () => { + const file = writeTranscript({ records: [userRecord("hi")] }); + fs.appendFileSync(file, "\n{bad json"); + expect(adapter.getConversation(CONVERSATION_ID)).toEqual([{ role: "user", content: "hi" }]); + }); + + it("excludes SYSTEM records unless verbose", () => { + const file = writeTranscript({ records: [systemRecord("checkpoint"), userRecord("go")] }); + expect(adapter.getConversation(file)).toEqual([{ role: "user", content: "go" }]); + expect(adapter.getConversation(file, { verbose: true }).map((m) => m.role)).toEqual([ + "system", + "user", + ]); + }); + + it("excludes MODEL tool-call (RUN_COMMAND) records except in verbose", () => { + const file = writeTranscript({ + records: [userRecord("go"), toolRecord("ran: echo hi"), modelRecord("done")], + }); + // A tool call is not an assistant reply; only USER_INPUT + PLANNER_RESPONSE show. + expect(adapter.getConversation(file)).toEqual([ + { role: "user", content: "go" }, + { role: "assistant", content: "done" }, + ]); + expect(adapter.getConversation(file, { verbose: true }).map((m) => m.role)).toEqual([ + "user", + "system", + "assistant", + ]); + }); + }); + + describe("detectAgents status + summary mapping", () => { + const detectFirst = async () => (await adapter.detectAgents())[0]; + + it("marks WAITING when the last reply is a PLANNER_RESPONSE (after a tool call)", async () => { + writeTranscript({ + records: [userRecord("go"), toolRecord("ran: echo hi"), modelRecord("done")], + }); + writeRegistry({ [cwd]: CONVERSATION_ID }); + mockedListAgentProcesses.mockReturnValue([proc()]); + expect((await detectFirst()).status).toBe(AgentStatus.WAITING); + }); + + it("marks RUNNING when the last transcript turn is a USER_INPUT message", async () => { + writeTranscript({ records: [userRecord("still there?")] }); + writeRegistry({ [cwd]: CONVERSATION_ID }); + mockedListAgentProcesses.mockReturnValue([proc()]); + expect((await detectFirst()).status).toBe(AgentStatus.RUNNING); + }); + + it("marks IDLE when the last created_at is older than the threshold", async () => { + const old = new Date(Date.now() - 10 * 60 * 1000); + writeTranscript({ records: [userRecord("go", old)] }); + writeRegistry({ [cwd]: CONVERSATION_ID }); + mockedListAgentProcesses.mockReturnValue([proc()]); + expect((await detectFirst()).status).toBe(AgentStatus.IDLE); + }); + + it("uses the last user request as the agent summary", async () => { + writeTranscript({ records: [userRecord("refactor the parser"), modelRecord("on it")] }); + writeRegistry({ [cwd]: CONVERSATION_ID }); + mockedListAgentProcesses.mockReturnValue([proc()]); + expect((await detectFirst()).summary).toBe("refactor the parser"); + }); + }); + + describe("listSessions", () => { + it("returns [] when there is no registry", async () => { + expect(await adapter.listSessions()).toEqual([]); + }); + + it("lists sessions from the registry with cwd + first user message", async () => { + writeTranscript({}); + writeRegistry({ [cwd]: CONVERSATION_ID }); + const summaries = await adapter.listSessions(); + expect(summaries).toHaveLength(1); + expect(summaries[0]).toMatchObject({ + type: "antigravity_cli", + sessionId: CONVERSATION_ID, + cwd, + firstUserMessage: "fix the bug", + }); + }); + + it("applies the cwd filter", async () => { + writeTranscript({ id: "aaa0000a-0000-4000-8000-00000000000a", records: [userRecord("a")] }); + writeTranscript({ id: "bbb0000b-0000-4000-8000-00000000000b", records: [userRecord("b")] }); + writeRegistry({ + "/Users/dev/project-a": "aaa0000a-0000-4000-8000-00000000000a", + "/Users/dev/project-b": "bbb0000b-0000-4000-8000-00000000000b", + }); + + const all = await adapter.listSessions(); + expect(all).toHaveLength(2); + + const filtered = await adapter.listSessions({ cwd: "/Users/dev/project-a" }); + expect(filtered).toHaveLength(1); + expect(filtered[0].cwd).toBe("/Users/dev/project-a"); + }); + }); +}); diff --git a/packages/agent-manager/src/adapters/AgentAdapter.ts b/packages/agent-manager/src/adapters/AgentAdapter.ts index 2f2c1a1d3..679cc092f 100644 --- a/packages/agent-manager/src/adapters/AgentAdapter.ts +++ b/packages/agent-manager/src/adapters/AgentAdapter.ts @@ -13,6 +13,7 @@ export type AgentType = | "gemini_cli" | "grok_cli" | "kiro" + | "antigravity_cli" | "codex" | "opencode" | "copilot" diff --git a/packages/agent-manager/src/adapters/AntigravityCliAdapter.ts b/packages/agent-manager/src/adapters/AntigravityCliAdapter.ts new file mode 100644 index 000000000..4af7f75d1 --- /dev/null +++ b/packages/agent-manager/src/adapters/AntigravityCliAdapter.ts @@ -0,0 +1,356 @@ +import * as path from "path"; +import type { + AgentAdapter, + AgentInfo, + ProcessInfo, + ConversationMessage, + SessionSummary, + ListSessionsOptions, +} from "./AgentAdapter.js"; +import { AgentStatus } from "./AgentAdapter.js"; +import { listAgentProcesses, enrichProcesses } from "../utils/process.js"; +import { safeReadFile, safeStat } from "../utils/session.js"; +import { generateAgentName } from "../utils/matching.js"; + +/** + * Antigravity CLI Adapter + * + * Detects running Antigravity CLI agents (Google's Gemini-family `agy` CLI) by: + * 1. Finding running `agy` processes via shared listAgentProcesses() — Antigravity + * ships a native binary (argv[0] basename `agy`). + * 2. Resolving each live process to its conversation via + * ~/.gemini/antigravity-cli/cache/last_conversations.json, which the CLI + * maintains as a `{ : }` map of the current conversation + * per workspace. The process cwd is the join key. + * 3. Reading the transcript from + * brain//.system_generated/logs/transcript.jsonl. User turns are + * USER_INPUT records (prompt inside ...) and the + * model's reply is a PLANNER_RESPONSE record; tool calls (RUN_COMMAND, ...) are + * skipped. The last user turn is the summary; each record's created_at gives the + * last-activity time. + * + * This is the runtime/agent side of Antigravity; it is independent of the + * `antigravity` environment (which configures the Antigravity IDE), the same way + * GeminiCliAdapter is independent of the `gemini` environment. + */ + +const REGISTRY_FILE = path.join("cache", "last_conversations.json"); +const BRAIN_DIR = "brain"; +const TRANSCRIPT_REL = path.join(".system_generated", "logs", "transcript.jsonl"); +const IDLE_THRESHOLD_MINUTES = 5; + +/** One line of transcript.jsonl. */ +interface TranscriptRecord { + source?: string; + type?: string; + created_at?: string; + content?: unknown; +} + +interface TranscriptScan { + messages: ConversationMessage[]; + firstUserMessage?: string; + lastUserMessage?: string; + lastRole?: ConversationMessage["role"]; + lastActive?: Date; +} + +/** Parsed state for a single conversation. */ +interface AntigravitySession { + sessionId: string; + projectPath: string; + summary: string; + sessionStart: Date; + lastActive: Date; + firstUserMessage?: string; + lastUserMessage?: string; + lastRole?: ConversationMessage["role"]; +} + +export class AntigravityCliAdapter implements AgentAdapter { + readonly type = "antigravity_cli" as const; + + private base: string; + + constructor() { + // ANTIGRAVITY_CLI_HOME overrides the ~/.gemini/antigravity-cli base dir. + const homeDir = process.env.HOME || process.env.USERPROFILE || ""; + this.base = + process.env.ANTIGRAVITY_CLI_HOME || path.join(homeDir, ".gemini", "antigravity-cli"); + } + + canHandle(processInfo: ProcessInfo): boolean { + return this.isAgyExecutable(processInfo.command); + } + + private isAgyExecutable(command: string): boolean { + const executable = command.trim().split(/\s+/)[0] || ""; + const base = path.basename(executable).toLowerCase(); + return base === "agy" || base === "agy.exe"; + } + + async detectAgents(): Promise { + const processes = enrichProcesses(listAgentProcesses("agy")); + if (processes.length === 0) { + return []; + } + + // last_conversations.json maps a workspace cwd to its current + // conversation id; the live process cwd is the join key. + const cwdToConversation = this.readRegistry(); + + const agents: AgentInfo[] = []; + for (const proc of processes) { + const cwd = proc.cwd || ""; + const conversationId = cwd ? cwdToConversation.get(cwd) : undefined; + const session = conversationId ? this.readSession(conversationId, cwd) : null; + + if (session) { + agents.push(this.mapSessionToAgent(session, proc)); + } else { + agents.push(this.mapProcessOnlyAgent(proc, cwd)); + } + } + + return agents; + } + + /** + * Read cache/last_conversations.json into a cwd -> conversationId map. The + * CLI rewrites this as the current conversation per workspace changes. + */ + private readRegistry(): Map { + const map = new Map(); + const content = safeReadFile(path.join(this.base, REGISTRY_FILE)); + if (content === undefined) return map; + + let parsed: unknown; + try { + parsed = JSON.parse(content); + } catch { + return map; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return map; + + for (const [cwd, conversationId] of Object.entries(parsed as Record)) { + if (cwd && typeof conversationId === "string" && conversationId) { + map.set(cwd, conversationId); + } + } + return map; + } + + private transcriptPath(conversationId: string): string { + return path.join(this.base, BRAIN_DIR, conversationId, TRANSCRIPT_REL); + } + + private mapSessionToAgent(session: AntigravitySession, processInfo: ProcessInfo): AgentInfo { + const projectPath = session.projectPath || processInfo.cwd || ""; + return { + name: generateAgentName(projectPath, processInfo.pid), + type: this.type, + status: this.determineStatus(session), + summary: session.summary || "Antigravity CLI session active", + pid: processInfo.pid, + projectPath, + sessionId: session.sessionId, + lastActive: session.lastActive, + sessionFilePath: this.transcriptPath(session.sessionId), + }; + } + + private mapProcessOnlyAgent(processInfo: ProcessInfo, cwd: string): AgentInfo { + const projectPath = cwd || processInfo.cwd || ""; + return { + name: generateAgentName(projectPath, processInfo.pid), + type: this.type, + status: AgentStatus.RUNNING, + summary: "Antigravity CLI process running", + pid: processInfo.pid, + projectPath, + sessionId: `pid-${processInfo.pid}`, + lastActive: new Date(), + }; + } + + getConversation(sessionFilePath: string, options?: { verbose?: boolean }): ConversationMessage[] { + return this.parseTranscript( + this.resolveTranscriptPath(sessionFilePath), + options?.verbose ?? false, + ).messages; + } + + async listSessions(opts?: ListSessionsOptions): Promise { + const filterCwd = opts?.cwd; + const summaries: SessionSummary[] = []; + + for (const [cwd, conversationId] of this.readRegistry()) { + if (filterCwd !== undefined && cwd !== filterCwd) continue; + + const session = this.readSession(conversationId, cwd); + if (!session) continue; + + summaries.push({ + type: this.type, + sessionId: session.sessionId, + cwd: session.projectPath || cwd, + firstUserMessage: session.firstUserMessage || "", + lastActive: session.lastActive, + startedAt: session.sessionStart, + sessionFilePath: this.transcriptPath(conversationId), + }); + } + + return summaries; + } + + // --- Session parsing (transcript.jsonl) --- + + /** + * Parse a conversation into an {@link AntigravitySession} from its + * transcript.jsonl. Returns null when the transcript is missing — i.e. there + * is no real session to surface. + */ + private readSession(conversationId: string, defaultCwd: string): AntigravitySession | null { + const transcriptPath = this.transcriptPath(conversationId); + const stat = safeStat(transcriptPath); + if (!stat) return null; + + const scan = this.parseTranscript(transcriptPath, false); + const lastActive = scan.lastActive || stat.mtime; + + return { + sessionId: conversationId, + projectPath: defaultCwd || "", + summary: scan.lastUserMessage || "Antigravity CLI session active", + sessionStart: stat.birthtime || lastActive, + lastActive, + firstUserMessage: scan.firstUserMessage, + lastUserMessage: scan.lastUserMessage, + lastRole: scan.lastRole, + }; + } + + /** + * Determine agent status from parsed session state. + * + * - past the idle threshold → IDLE + * - last transcript turn is an assistant message → WAITING (awaiting user) + * - otherwise (last turn was a user message, or unknown) → RUNNING + */ + private determineStatus(session: AntigravitySession): AgentStatus { + const diffMinutes = (Date.now() - session.lastActive.getTime()) / 60000; + if (diffMinutes > IDLE_THRESHOLD_MINUTES) { + return AgentStatus.IDLE; + } + if (session.lastRole === "assistant") { + return AgentStatus.WAITING; + } + return AgentStatus.RUNNING; + } + + /** + * Single pass over transcript.jsonl. Each line is a + * { source, type, created_at, content } record. User turns are + * `type: 'USER_INPUT'` with the real prompt wrapped in + * ...; the model's reply is `type: + * 'PLANNER_RESPONSE'`. Other MODEL records (tool calls like RUN_COMMAND) and + * SYSTEM records (conversation history, checkpoints) are execution detail and + * are skipped unless verbose. + */ + private parseTranscript(transcriptPath: string, verbose: boolean): TranscriptScan { + const empty: TranscriptScan = { messages: [] }; + const content = safeReadFile(transcriptPath); + if (content === undefined) return empty; + + const messages: ConversationMessage[] = []; + let lastRole: ConversationMessage["role"] | undefined; + let lastActive: Date | undefined; + + for (const line of content.trim().split("\n")) { + if (!line.trim()) continue; + + let record: TranscriptRecord; + try { + record = JSON.parse(line); + } catch { + continue; + } + + const at = this.parseTimestamp(record.created_at); + if (at && (!lastActive || at.getTime() > lastActive.getTime())) lastActive = at; + + const text = this.extractText(record.content); + if (record.type === "USER_INPUT") { + const request = this.extractUserRequest(text); + if (request === null) continue; // not a real prompt + messages.push({ role: "user", content: request }); + lastRole = "user"; + } else if (record.type === "PLANNER_RESPONSE") { + // The model's user-facing reply. Other MODEL records (tool calls + // such as RUN_COMMAND, empty planning steps) are execution detail, + // not conversation, so they are excluded from the normal view. + if (!text) continue; + messages.push({ role: "assistant", content: text }); + lastRole = "assistant"; + } else if (verbose) { + // Tool calls (MODEL/RUN_COMMAND, ...) and SYSTEM records surface + // only in verbose mode. + if (!text) continue; + messages.push({ role: "system", content: text }); + } + } + + const userTurns = messages.filter((m) => m.role === "user"); + return { + messages, + firstUserMessage: userTurns[0]?.content, + lastUserMessage: userTurns[userTurns.length - 1]?.content, + lastRole, + lastActive, + }; + } + + /** Flatten a transcript record's content (string or text-block array) to text. */ + private extractText(content: unknown): string { + if (typeof content === "string") return content; + if (Array.isArray(content)) { + return content + .map((block) => + block && + typeof block === "object" && + typeof (block as { text?: unknown }).text === "string" + ? (block as { text: string }).text + : "", + ) + .join(""); + } + return ""; + } + + /** + * Extract the prompt inside .... Returns null when + * the record has no such tag (context injection rather than a prompt). Falls + * back to the trimmed text when a USER_INPUT record has no wrapper. + */ + private extractUserRequest(text: string): string | null { + const match = text.match(/\s*([\s\S]*?)\s*<\/USER_REQUEST>/); + if (match) return match[1].trim(); + const trimmed = text.trim(); + return trimmed ? trimmed : null; + } + + /** Resolve a conversation dir, transcript path, or bare id to the transcript file. */ + private resolveTranscriptPath(sessionPath: string): string { + if (sessionPath.endsWith(".jsonl")) return sessionPath; + // A bare conversation id (no separator) resolves under the brain dir. + if (!sessionPath.includes(path.sep)) return this.transcriptPath(sessionPath); + return path.join(sessionPath, TRANSCRIPT_REL); + } + + private parseTimestamp(value?: string): Date | null { + if (!value) return null; + const ts = new Date(value); + return Number.isNaN(ts.getTime()) ? null : ts; + } +} diff --git a/packages/agent-manager/src/adapters/index.ts b/packages/agent-manager/src/adapters/index.ts index 6ac9291c6..d0003c79b 100644 --- a/packages/agent-manager/src/adapters/index.ts +++ b/packages/agent-manager/src/adapters/index.ts @@ -3,6 +3,7 @@ export { CodexAdapter } from "../providers/codex/CodexAdapter.js"; export { CopilotAdapter } from "../providers/copilot/CopilotAdapter.js"; export { GeminiCliAdapter } from "../providers/gemini/GeminiCliAdapter.js"; export { GrokCliAdapter } from "../providers/grok/GrokCliAdapter.js"; +export { AntigravityCliAdapter } from "./AntigravityCliAdapter.js"; export { KiroAdapter } from "./KiroAdapter.js"; export { OpenCodeAdapter } from "../providers/opencode/OpenCodeAdapter.js"; export { PiAdapter } from "../providers/pi/PiAdapter.js"; diff --git a/packages/agent-manager/src/index.ts b/packages/agent-manager/src/index.ts index 487cb7bab..570677e88 100644 --- a/packages/agent-manager/src/index.ts +++ b/packages/agent-manager/src/index.ts @@ -37,6 +37,7 @@ export { CodexAdapter } from "./providers/codex/CodexAdapter.js"; export { CopilotAdapter } from "./providers/copilot/CopilotAdapter.js"; export { GeminiCliAdapter } from "./providers/gemini/GeminiCliAdapter.js"; export { GrokCliAdapter } from "./providers/grok/GrokCliAdapter.js"; +export { AntigravityCliAdapter } from "./adapters/AntigravityCliAdapter.js"; export { KiroAdapter } from "./adapters/KiroAdapter.js"; export { OpenCodeAdapter } from "./providers/opencode/OpenCodeAdapter.js"; export { PiAdapter } from "./providers/pi/PiAdapter.js"; diff --git a/packages/agent-manager/src/readiness/AgentReadiness.ts b/packages/agent-manager/src/readiness/AgentReadiness.ts index 184f9d18b..0f931198e 100644 --- a/packages/agent-manager/src/readiness/AgentReadiness.ts +++ b/packages/agent-manager/src/readiness/AgentReadiness.ts @@ -12,7 +12,10 @@ const ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/] export type ReadinessStatus = "pass" | "warn" | "fail"; export type ReadinessAuthState = "authenticated" | "unauthenticated" | "unknown"; -export type ReadinessAgentType = Exclude; +export type ReadinessAgentType = Exclude< + StartableAgentType, + "gemini_cli" | "kiro" | "antigravity_cli" +>; type ReadinessInfoStatus = ReadinessStatus | "info"; type CommandResult = { stdout: string; stderr: string }; diff --git a/packages/agent-manager/src/runtime/ManagedAgentRuntime.ts b/packages/agent-manager/src/runtime/ManagedAgentRuntime.ts index 961084a91..674c07766 100644 --- a/packages/agent-manager/src/runtime/ManagedAgentRuntime.ts +++ b/packages/agent-manager/src/runtime/ManagedAgentRuntime.ts @@ -260,6 +260,7 @@ function agentRuntimeKind(type: StartableAgentType, provider: AgentRuntimeProvid copilot: "copilot", gemini_cli: "gemini", grok_cli: "grok", + antigravity_cli: "agy", opencode: "opencode", pi: "pi", kiro: "kiro", diff --git a/packages/agent-manager/src/runtime/herdr/HerdrAgentDiscovery.ts b/packages/agent-manager/src/runtime/herdr/HerdrAgentDiscovery.ts index 7ca30a172..c01bdd63b 100644 --- a/packages/agent-manager/src/runtime/herdr/HerdrAgentDiscovery.ts +++ b/packages/agent-manager/src/runtime/herdr/HerdrAgentDiscovery.ts @@ -93,6 +93,7 @@ function herdrAgentMatchesType(herdrAgent: string, agentType: AgentType): boolea copilot: ["copilot"], gemini_cli: ["gemini", "gemini_cli"], grok_cli: ["grok", "grok_cli"], + antigravity_cli: ["agy", "antigravity", "antigravity_cli"], kiro: ["kiro", "kiro_cli", "kiro-cli"], opencode: ["opencode"], pi: ["pi"], @@ -120,7 +121,9 @@ async function enrichHerdrAgentPanes( ); } -function extractPaneProcessIds(value: unknown): Pick { +function extractPaneProcessIds( + value: unknown, +): Pick { const processInfo = getObject(value, ["result", "process_info"]); if (!processInfo) return {}; diff --git a/packages/agent-manager/src/utils/agents.ts b/packages/agent-manager/src/utils/agents.ts index 80f030993..d93de4c5d 100644 --- a/packages/agent-manager/src/utils/agents.ts +++ b/packages/agent-manager/src/utils/agents.ts @@ -3,7 +3,15 @@ import type { AgentType } from "../adapters/AgentAdapter.js"; export type StartableAgentType = Extract< AgentType, - "claude" | "codex" | "copilot" | "gemini_cli" | "grok_cli" | "opencode" | "pi" | "kiro" + | "claude" + | "codex" + | "copilot" + | "gemini_cli" + | "grok_cli" + | "antigravity_cli" + | "opencode" + | "pi" + | "kiro" >; export interface AgentConfig { @@ -24,6 +32,7 @@ export const AGENTS: Record = { copilot: { command: "copilot", matches: matchArgv0Name("copilot-cli") }, gemini_cli: { command: "gemini", matches: matchAnyToken("gemini") }, grok_cli: { command: "grok", matches: matchArgv0("grok") }, + antigravity_cli: { command: "agy", matches: matchArgv0("agy") }, opencode: { command: "opencode", matches: matchArgv0("opencode") }, pi: { command: "pi", matches: matchAnyBasename(["pi"]) }, kiro: { command: "kiro-cli", matches: matchAnyBasename(["kiro-cli", "kiro"]) }, diff --git a/packages/cli/src/__tests__/commands/agent.test.ts b/packages/cli/src/__tests__/commands/agent.test.ts index ed2a2ed82..c5772ea3d 100644 --- a/packages/cli/src/__tests__/commands/agent.test.ts +++ b/packages/cli/src/__tests__/commands/agent.test.ts @@ -228,6 +228,7 @@ vi.mock( CopilotAdapter: vi.fn(), GeminiCliAdapter: vi.fn(), GrokCliAdapter: vi.fn(), + AntigravityCliAdapter: vi.fn(), KiroAdapter: vi.fn(), OpenCodeAdapter: vi.fn(), PiAdapter: vi.fn(), @@ -283,6 +284,7 @@ vi.mock( copilot: { command: "copilot", matches: () => true }, gemini_cli: { command: "gemini", matches: () => true }, grok_cli: { command: "grok", matches: () => true }, + antigravity_cli: { command: "agy", matches: () => true }, opencode: { command: "opencode", matches: () => true }, pi: { command: "pi", matches: () => true }, kiro: { command: "kiro-cli", matches: () => true }, @@ -605,7 +607,7 @@ describe("agent command", () => { onRuntimeDiscoveryError: expect.any(Function), }), ); - expect(mockManager.registerAdapter).toHaveBeenCalledTimes(8); + expect(mockManager.registerAdapter).toHaveBeenCalledTimes(9); expect(logSpy).toHaveBeenCalledWith( JSON.stringify([{ ...agents[0], mode: "interactive" }], null, 2), ); diff --git a/packages/cli/src/__tests__/commands/channel.test.ts b/packages/cli/src/__tests__/commands/channel.test.ts index 1064c6d8c..43ecc37c5 100644 --- a/packages/cli/src/__tests__/commands/channel.test.ts +++ b/packages/cli/src/__tests__/commands/channel.test.ts @@ -105,6 +105,7 @@ vi.mock( CopilotAdapter: vi.fn(), GeminiCliAdapter: vi.fn(), GrokCliAdapter: vi.fn(), + AntigravityCliAdapter: vi.fn(), KiroAdapter: vi.fn(), PiAdapter: vi.fn(), TerminalFocusManager: vi.fn(function () { @@ -926,7 +927,7 @@ describe("channel command", () => { bridgePid: process.pid, }), ); - expect(mockAgentManager.registerAdapter).toHaveBeenCalledTimes(7); + expect(mockAgentManager.registerAdapter).toHaveBeenCalledTimes(8); expect( mockChannelService.registerBridge.mock.invocationCallOrder[0], ).toBeLessThan(mockChannelManager.startAll.mock.invocationCallOrder[0]); diff --git a/packages/cli/src/__tests__/tui/console/StartAgentPane.test.ts b/packages/cli/src/__tests__/tui/console/StartAgentPane.test.ts index fbd722604..1776851e0 100644 --- a/packages/cli/src/__tests__/tui/console/StartAgentPane.test.ts +++ b/packages/cli/src/__tests__/tui/console/StartAgentPane.test.ts @@ -15,6 +15,7 @@ describe("StartAgentPane helpers", () => { "copilot", "gemini_cli", "grok_cli", + "antigravity_cli", "opencode", "pi", "kiro", diff --git a/packages/cli/src/__tests__/util/sessions.test.ts b/packages/cli/src/__tests__/util/sessions.test.ts index 6507cc5bf..19ab0857e 100644 --- a/packages/cli/src/__tests__/util/sessions.test.ts +++ b/packages/cli/src/__tests__/util/sessions.test.ts @@ -50,6 +50,7 @@ describe("sessions util", () => { "gemini_cli", "grok_cli", "kiro", + "antigravity_cli", "opencode", "copilot", "pi", @@ -61,7 +62,7 @@ describe("sessions util", () => { it("throws on an invalid --type", () => { expect(() => resolveListSessionsOptions({ all: true, type: "wrong" })).toThrow( - 'Invalid --type "wrong". Expected one of: claude, codex, gemini_cli, grok_cli, kiro, opencode, copilot, pi.', + 'Invalid --type "wrong". Expected one of: claude, codex, gemini_cli, grok_cli, kiro, antigravity_cli, opencode, copilot, pi.', ); }); diff --git a/packages/cli/src/commands/agent.ts b/packages/cli/src/commands/agent.ts index d7499bed1..5fa98108d 100644 --- a/packages/cli/src/commands/agent.ts +++ b/packages/cli/src/commands/agent.ts @@ -13,6 +13,7 @@ import { GeminiCliAdapter, GrokCliAdapter, KiroAdapter, + AntigravityCliAdapter, OpenCodeAdapter, PiAdapter, ClaudePrintAgentService, @@ -150,6 +151,7 @@ const TYPE_LABELS: Record = { gemini_cli: "Gemini CLI", grok_cli: "Grok CLI", kiro: "Kiro", + antigravity_cli: "Antigravity CLI", opencode: "OpenCode", pi: "Pi", other: "Other", @@ -281,6 +283,7 @@ function createAgentManager(): AgentManager { manager.registerAdapter(new GeminiCliAdapter()); manager.registerAdapter(new GrokCliAdapter()); manager.registerAdapter(new KiroAdapter()); + manager.registerAdapter(new AntigravityCliAdapter()); manager.registerAdapter(new OpenCodeAdapter()); manager.registerAdapter(new PiAdapter()); return manager; diff --git a/packages/cli/src/services/channel/channel-runner.ts b/packages/cli/src/services/channel/channel-runner.ts index 78913dc07..684fac54d 100644 --- a/packages/cli/src/services/channel/channel-runner.ts +++ b/packages/cli/src/services/channel/channel-runner.ts @@ -7,6 +7,7 @@ import { GeminiCliAdapter, GrokCliAdapter, KiroAdapter, + AntigravityCliAdapter, PiAdapter, TerminalFocusManager, TtyWriter, @@ -51,6 +52,7 @@ function createAgentManager(): AgentManager { manager.registerAdapter(new GeminiCliAdapter()); manager.registerAdapter(new GrokCliAdapter()); manager.registerAdapter(new KiroAdapter()); + manager.registerAdapter(new AntigravityCliAdapter()); manager.registerAdapter(new PiAdapter()); return manager; } diff --git a/packages/cli/src/util/sessions.ts b/packages/cli/src/util/sessions.ts index de758be13..ed52fba16 100644 --- a/packages/cli/src/util/sessions.ts +++ b/packages/cli/src/util/sessions.ts @@ -9,6 +9,7 @@ const VALID_AGENT_TYPES: AgentType[] = [ "gemini_cli", "grok_cli", "kiro", + "antigravity_cli", "opencode", "copilot", "pi",