diff --git a/README-zh.md b/README-zh.md index 516b2f57..af76981f 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 e34ab534..5d5dcd91 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ One `.ai-devkit.json` configures all of them. Add a new agent to your team witho | [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/docs/ai/design/feature-antigravity-cli-adapter.md b/docs/ai/design/feature-antigravity-cli-adapter.md new file mode 100644 index 00000000..888f0906 --- /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 00000000..0f5469e9 --- /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 00000000..d2772a2a --- /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 00000000..77a67398 --- /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 00000000..6c991ffe --- /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 00000000..469f661d --- /dev/null +++ b/packages/agent-manager/src/__tests__/adapters/AntigravityCliAdapter.test.ts @@ -0,0 +1,292 @@ +/** + * 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 2444d644..c9b121c5 100644 --- a/packages/agent-manager/src/adapters/AgentAdapter.ts +++ b/packages/agent-manager/src/adapters/AgentAdapter.ts @@ -8,7 +8,7 @@ /** * Type of AI agent */ -export type AgentType = 'claude' | 'gemini_cli' | 'grok_cli' | 'codex' | 'opencode' | 'copilot' | 'pi' | 'other'; +export type AgentType = 'claude' | 'gemini_cli' | 'grok_cli' | 'antigravity_cli' | 'codex' | 'opencode' | 'copilot' | 'pi' | 'other'; /** * Current status of an agent diff --git a/packages/agent-manager/src/adapters/AntigravityCliAdapter.ts b/packages/agent-manager/src/adapters/AntigravityCliAdapter.ts new file mode 100644 index 00000000..c40f72ee --- /dev/null +++ b/packages/agent-manager/src/adapters/AntigravityCliAdapter.ts @@ -0,0 +1,350 @@ +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 c152c32c..8aa04355 100644 --- a/packages/agent-manager/src/adapters/index.ts +++ b/packages/agent-manager/src/adapters/index.ts @@ -3,6 +3,7 @@ export { CodexAdapter } from './CodexAdapter.js'; export { CopilotAdapter } from './CopilotAdapter.js'; export { GeminiCliAdapter } from './GeminiCliAdapter.js'; export { GrokCliAdapter } from './GrokCliAdapter.js'; +export { AntigravityCliAdapter } from './AntigravityCliAdapter.js'; export { OpenCodeAdapter } from './OpenCodeAdapter.js'; export { PiAdapter } from './PiAdapter.js'; export { AgentStatus } from './AgentAdapter.js'; diff --git a/packages/agent-manager/src/index.ts b/packages/agent-manager/src/index.ts index fc68ac30..39723364 100644 --- a/packages/agent-manager/src/index.ts +++ b/packages/agent-manager/src/index.ts @@ -5,6 +5,7 @@ export { CodexAdapter } from './adapters/CodexAdapter.js'; export { CopilotAdapter } from './adapters/CopilotAdapter.js'; export { GeminiCliAdapter } from './adapters/GeminiCliAdapter.js'; export { GrokCliAdapter } from './adapters/GrokCliAdapter.js'; +export { AntigravityCliAdapter } from './adapters/AntigravityCliAdapter.js'; export { OpenCodeAdapter } from './adapters/OpenCodeAdapter.js'; export { PiAdapter } from './adapters/PiAdapter.js'; export { AgentStatus } from './adapters/AgentAdapter.js'; diff --git a/packages/agent-manager/src/utils/agents.ts b/packages/agent-manager/src/utils/agents.ts index 718ed4f7..94cb1120 100644 --- a/packages/agent-manager/src/utils/agents.ts +++ b/packages/agent-manager/src/utils/agents.ts @@ -1,7 +1,7 @@ import path from 'path'; import type { AgentType } from '../adapters/AgentAdapter.js'; -export type StartableAgentType = Extract; +export type StartableAgentType = Extract; export interface AgentConfig { /** Shell command to launch the agent (sent to tmux via `send-keys`). */ @@ -21,6 +21,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']) }, }; diff --git a/packages/cli/src/__tests__/commands/agent.test.ts b/packages/cli/src/__tests__/commands/agent.test.ts index ad2264da..c176f544 100644 --- a/packages/cli/src/__tests__/commands/agent.test.ts +++ b/packages/cli/src/__tests__/commands/agent.test.ts @@ -84,6 +84,7 @@ vi.mock('@ai-devkit/agent-manager', () => ({ CopilotAdapter: vi.fn(), GeminiCliAdapter: vi.fn(), GrokCliAdapter: vi.fn(), + AntigravityCliAdapter: vi.fn(), OpenCodeAdapter: vi.fn(), PiAdapter: vi.fn(), TerminalFocusManager: vi.fn(function () { return mockFocusManager; }), @@ -111,6 +112,7 @@ vi.mock('@ai-devkit/agent-manager', () => ({ 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 }, }, @@ -270,7 +272,7 @@ describe('agent command', () => { await program.parseAsync(['node', 'test', 'agent', 'list', '--json']); expect(AgentManager).toHaveBeenCalled(); - expect(mockManager.registerAdapter).toHaveBeenCalledTimes(7); + expect(mockManager.registerAdapter).toHaveBeenCalledTimes(8); expect(logSpy).toHaveBeenCalledWith(JSON.stringify(agents, null, 2)); }); diff --git a/packages/cli/src/__tests__/commands/channel.test.ts b/packages/cli/src/__tests__/commands/channel.test.ts index bd8ede17..33c46a0f 100644 --- a/packages/cli/src/__tests__/commands/channel.test.ts +++ b/packages/cli/src/__tests__/commands/channel.test.ts @@ -75,6 +75,7 @@ vi.mock('@ai-devkit/agent-manager', () => ({ CopilotAdapter: vi.fn(), GeminiCliAdapter: vi.fn(), GrokCliAdapter: vi.fn(), + AntigravityCliAdapter: vi.fn(), PiAdapter: vi.fn(), TerminalFocusManager: vi.fn(function () { return mockTerminalFocusManager; }), TtyWriter: { @@ -601,7 +602,7 @@ describe('channel command', () => { agentPid: 4321, bridgePid: process.pid, })); - expect(mockAgentManager.registerAdapter).toHaveBeenCalledTimes(6); + expect(mockAgentManager.registerAdapter).toHaveBeenCalledTimes(7); 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 7b11f613..5a914160 100644 --- a/packages/cli/src/__tests__/tui/console/StartAgentPane.test.ts +++ b/packages/cli/src/__tests__/tui/console/StartAgentPane.test.ts @@ -9,7 +9,7 @@ import { describe('StartAgentPane helpers', () => { it('lists supported agent start types in pane order', () => { - expect(STARTABLE_AGENT_TYPES).toEqual(['claude', 'codex', 'copilot', 'gemini_cli', 'grok_cli', 'opencode', 'pi']); + expect(STARTABLE_AGENT_TYPES).toEqual(['claude', 'codex', 'copilot', 'gemini_cli', 'grok_cli', 'antigravity_cli', 'opencode', 'pi']); }); it('cycles to the next agent type', () => { diff --git a/packages/cli/src/__tests__/util/sessions.test.ts b/packages/cli/src/__tests__/util/sessions.test.ts index 97b63899..39d41e8c 100644 --- a/packages/cli/src/__tests__/util/sessions.test.ts +++ b/packages/cli/src/__tests__/util/sessions.test.ts @@ -45,7 +45,7 @@ describe('sessions util', () => { }); it('forwards a valid --type', () => { - for (const type of ['claude', 'codex', 'gemini_cli', 'grok_cli', 'opencode', 'copilot', 'pi'] as const) { + for (const type of ['claude', 'codex', 'gemini_cli', 'grok_cli', 'antigravity_cli', 'opencode', 'copilot', 'pi'] as const) { const result = resolveListSessionsOptions({ all: true, type }); expect(result.adapterOptions.type).toBe(type); } @@ -53,7 +53,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, opencode, copilot, pi.', + 'Invalid --type "wrong". Expected one of: claude, codex, gemini_cli, grok_cli, antigravity_cli, opencode, copilot, pi.', ); }); diff --git a/packages/cli/src/commands/agent.ts b/packages/cli/src/commands/agent.ts index 48a92eb5..33aad237 100644 --- a/packages/cli/src/commands/agent.ts +++ b/packages/cli/src/commands/agent.ts @@ -12,6 +12,7 @@ import { CopilotAdapter, GeminiCliAdapter, GrokCliAdapter, + AntigravityCliAdapter, OpenCodeAdapter, PiAdapter, AgentStatus, @@ -91,6 +92,7 @@ const TYPE_LABELS: Record = { copilot: 'Copilot', gemini_cli: 'Gemini CLI', grok_cli: 'Grok CLI', + antigravity_cli: 'Antigravity CLI', opencode: 'OpenCode', pi: 'Pi', other: 'Other', @@ -174,6 +176,7 @@ function createAgentManager(): AgentManager { manager.registerAdapter(new CopilotAdapter()); manager.registerAdapter(new GeminiCliAdapter()); manager.registerAdapter(new GrokCliAdapter()); + manager.registerAdapter(new AntigravityCliAdapter()); manager.registerAdapter(new OpenCodeAdapter()); manager.registerAdapter(new PiAdapter()); return manager; @@ -362,7 +365,7 @@ export function registerAgentCommand(program: Command): void { .description('List historical Claude/Codex/Gemini/Grok/OpenCode sessions for resume') .option('--all', 'Include sessions from every cwd (default: only current cwd)') .option('--cwd ', 'Override the cwd filter (implies non-default scope)') - .option('--type ', 'Filter to one of: claude, codex, gemini_cli, grok_cli, opencode, copilot, pi') + .option('--type ', 'Filter to one of: claude, codex, gemini_cli, grok_cli, antigravity_cli, opencode, copilot, pi') .option('--limit ', 'Max rows to print (default: 50; 0 = no limit)', '50') .option('-j, --json', 'Output as JSON') .action(withErrorHandler('list sessions', async (options) => { @@ -418,7 +421,7 @@ export function registerAgentCommand(program: Command): void { .description('Show detailed information about a historical session') .requiredOption('--id ', 'Session ID (as shown in agent sessions)') .option('-j, --json', 'Output as JSON') - .option('--type ', 'Filter to one of: claude, codex, gemini_cli, grok_cli, opencode, copilot, pi') + .option('--type ', 'Filter to one of: claude, codex, gemini_cli, grok_cli, antigravity_cli, opencode, copilot, pi') .option('--full', 'Show entire conversation history') .option('--tail ', 'Show last N messages (default: 20)', '20') .option('--verbose', 'Include tool call/result details') diff --git a/packages/cli/src/services/channel/channel-runner.ts b/packages/cli/src/services/channel/channel-runner.ts index 18b35c19..5e7fd086 100644 --- a/packages/cli/src/services/channel/channel-runner.ts +++ b/packages/cli/src/services/channel/channel-runner.ts @@ -6,6 +6,7 @@ import { CopilotAdapter, GeminiCliAdapter, GrokCliAdapter, + AntigravityCliAdapter, PiAdapter, TerminalFocusManager, TtyWriter, @@ -46,6 +47,7 @@ function createAgentManager(): AgentManager { manager.registerAdapter(new CopilotAdapter()); manager.registerAdapter(new GeminiCliAdapter()); manager.registerAdapter(new GrokCliAdapter()); + 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 a8b949c0..f1200b61 100644 --- a/packages/cli/src/util/sessions.ts +++ b/packages/cli/src/util/sessions.ts @@ -7,7 +7,7 @@ import { truncate } from './text.js'; const FIRST_MESSAGE_MAX_WIDTH = 80; const FIRST_MESSAGE_PLACEHOLDER = '(no message yet)'; -const VALID_AGENT_TYPES: AgentType[] = ['claude', 'codex', 'gemini_cli', 'grok_cli', 'opencode', 'copilot', 'pi']; +const VALID_AGENT_TYPES: AgentType[] = ['claude', 'codex', 'gemini_cli', 'grok_cli', 'antigravity_cli', 'opencode', 'copilot', 'pi']; export interface ResolvedListSessionsOptions { adapterOptions: ListSessionsOptions;