diff --git a/DESIGN.md b/DESIGN.md
index 39d1ea37..55177554 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -1,7 +1,7 @@
# Design System — Spool
## Product Context
-- **What this is:** A local AI session library — an Electron macOS app that collects, organizes, and lets you revisit every Claude Code, Codex, and Gemini session you've ever had.
+- **What this is:** A local AI session library — an Electron macOS app that collects, organizes, and lets you revisit every Claude Code, Codex, Gemini, Antigravity, and OpenCode session you've ever had.
- **Who it's for:** Developers who think with AI daily and have accumulated hundreds of sessions across multiple tools. The persona is overwhelmed by the archive itself, not only by re-explaining context.
- **Space/industry:** Developer productivity / local-first tooling. Peers: Raycast, Spotlight, Obsidian, DevonThink — but none of them treat AI sessions as first-class library items.
- **Project type:** macOS Electron app — sidebar + main pane shell, the shape of a library client.
@@ -89,6 +89,7 @@ Only currently-supported agent sources are listed. New sources arrive via the da
| Claude Code | `#C26A4E` | `#E89A7C` |
| Codex CLI | `#4A9670` | `#7CC9A2` |
| Gemini | `#5887D0` | `#8AB0E5` |
+| Antigravity | `#6B5CA5` | `#9B8FD0` |
| OpenCode | `#8A6F3D` | `#C9A761` |
### Semantic States
@@ -282,5 +283,5 @@ In list contexts (Library Home, Project View, search results), trust the surroun
| 2026-05-08 | Warm-tuned status colors replace Tailwind defaults | `green-400 / amber-400 / red-400` were the only un-tuned colors in the system. Replaced with warm green `#6BAF6B`, warm amber `#E4A640`, terracotta `#C95A4F` so status reads as part of the same palette. |
| 2026-05-08 | Page title type added (20px) | Library Home and Settings need a real h1 — previous scale topped at sidebar wordmark 16px, leaving the home pane labelless. |
| 2026-05-08 | First-person rule softened from "all metadata" to "where it adds signal" | Literal "You discussed this · Mar 15" prefix on every row was repetitive once a user had hundreds of sessions. Library context already conveys ownership; reserve first-person for empty states, detail headers, and confirmations. |
-| 2026-05-08 | Source badge list trimmed to active agents only | Twitter / GitHub / YouTube / ChatGPT were carryover from the connector era. Spool ships only Claude Code / Codex CLI / Gemini today; new sources arrive via the daemon and get a row when shipped, not preemptively. |
+| 2026-05-08 | Source badge list trimmed to active agents only | Twitter / GitHub / YouTube / ChatGPT were carryover from the connector era. Spool ships only Claude Code / Codex CLI / Gemini / Antigravity today; new sources arrive via the daemon and get a row when shipped, not preemptively. |
| 2026-05-26 | Retire the "icons only 12/14/16/20, stroke 1.5" rule — describe reality instead | The 2026-05-08 lock never held: the renderer actually uses 11/12/13/14 (13 & 14 dominant) at strokes 1.6–1.8 (1.6 most common, 1.5 a minority). The aspirational whitelist was actively misleading new work into picking sizes that clashed with adjacent icons. Replaced with a role-based working set + a "match adjacent icons" rule. Sizes/strokes are now chosen for local consistency, not global uniformity. |
diff --git a/README.md b/README.md
index 56c0a37f..ae7b7305 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ Your local AI session library.
-Spool collects every Claude Code, Codex CLI, Gemini CLI, and OpenCode session you've ever had into a sidebar of projects you can browse, pin, and revisit. Press ⌘K to search across the whole archive.
+Spool collects every Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, and OpenCode session you've ever had into a sidebar of projects you can browse, pin, and revisit. Press ⌘K to search across the whole archive.
> **Early stage.** Spool is under active development — expect rough edges. Feedback, bug reports, and ideas are very welcome via [Issues](https://github.com/paperboytm/spool/issues) or [Discord](https://discord.gg/aqeDxQUs5E).
@@ -29,7 +29,7 @@ pnpm build
Spool turns the pile of AI sessions sitting on your disk into a browsable library.
- **Library shell** — sidebar of projects (derived from working-dir paths across agents) and a main pane that shows recent + pinned sessions for whatever you've selected
-- **Session indexing** — watches Claude/Codex/Gemini session dirs and OpenCode's SQLite database in real time, including profile-based paths like `~/.claude-profiles/*/projects`, `~/.codex-profiles/*/sessions`, Gemini's project temp dirs under `~/.gemini/tmp/*/chats`, and `~/.local/share/opencode/opencode.db`
+- **Session indexing** — watches Claude/Codex/Gemini/Antigravity session dirs and OpenCode's SQLite database in real time, including profile-based paths like `~/.claude-profiles/*/projects`, `~/.codex-profiles/*/sessions`, Gemini/Antigravity project temp dirs under `~/.gemini/tmp/*/chats` and `~/.gemini/antigravity-cli/brain/`, and `~/.local/share/opencode/opencode.db`
- **Pin** — keep important sessions on top of their project and on the global Library Home
- **⌘K search** — fast full-text search scoped to All or the current project; AI mode synthesizes answers across fragments
- **Agent search** — a `/spool` skill inside Claude Code (and any ACP agent) feeds matching fragments back into your conversation
diff --git a/docs/spool-positioning.md b/docs/spool-positioning.md
index 151eab09..e9619a82 100644
--- a/docs/spool-positioning.md
+++ b/docs/spool-positioning.md
@@ -2,7 +2,7 @@
> **The missing search engine for your own AI sessions.**
-Search your `[Claude Code sessions · Codex history · Gemini chats]` — locally.
+Search your `[Claude Code sessions · Codex history · Gemini chats · Antigravity transcripts]` — locally.
---
@@ -10,11 +10,11 @@ Search your `[Claude Code sessions · Codex history · Gemini chats]` — locall
### Your coding agent is already the best search engine you have.
-Spool lets Claude Code, Codex, Gemini CLI, and any coding agent search your past sessions from a single search box.
+Spool lets Claude Code, Codex, Gemini CLI, Antigravity CLI, and any coding agent search your past sessions from a single search box.
### Every agent session, indexed automatically.
-Spool watches `~/.claude/`, `~/.codex/`, and Gemini CLI's `~/.gemini/tmp/*/chats` in real time. Every conversation you have with Claude Code, Codex, or Gemini CLI — searchable the moment it's written.
+Spool watches `~/.claude/`, `~/.codex/`, Gemini CLI's `~/.gemini/tmp/*/chats`, and Antigravity CLI's `~/.gemini/antigravity-cli/brain/` in real time. Every conversation you have with Claude Code, Codex, Gemini CLI, or Antigravity CLI — searchable the moment it's written.
### Context that flows back in.
diff --git a/packages/app/src/main/acp.ts b/packages/app/src/main/acp.ts
index a93459f7..12335117 100644
--- a/packages/app/src/main/acp.ts
+++ b/packages/app/src/main/acp.ts
@@ -201,7 +201,7 @@ function ensureAgentSearchCwd(): string {
* the Spool-authored session write.
*/
function agentIdToSource(agentId: string): SessionSource | null {
- if (agentId === 'claude' || agentId === 'codex' || agentId === 'gemini' || agentId === 'opencode') return agentId
+ if (agentId === 'claude' || agentId === 'codex' || agentId === 'gemini' || agentId === 'antigravity' || agentId === 'opencode') return agentId
return null
}
@@ -873,12 +873,12 @@ export class AcpManager {
// after stripping the prelude only the bare query remains as the first
// user message — clean derived title, clean FTS, clean session detail.
const systemBody = [
- 'You have access to a local knowledge base called Spool that indexes the user\'s AI coding sessions (Claude Code, Codex CLI, Gemini CLI, OpenCode).',
+ 'You have access to a local knowledge base called Spool that indexes the user\'s AI coding sessions (Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, OpenCode).',
'',
'The database is at ~/.spool/spool.db (SQLite with FTS5). You can query it directly with the `sqlite3` CLI.',
'',
'── Schema ──',
- ' sources(id, name TEXT, base_path TEXT) -- "claude", "codex", "gemini", or "opencode"',
+ ' sources(id, name TEXT, base_path TEXT) -- "claude", "codex", "gemini", "antigravity", or "opencode"',
' projects(id, source_id, slug, display_path, display_name, last_synced)',
' sessions(id, project_id, source_id, session_uuid TEXT, title TEXT, started_at TEXT, ended_at TEXT, message_count INT, has_tool_use INT)',
' messages(id, session_id, source_id, role TEXT, content_text TEXT, timestamp TEXT, tool_names TEXT)',
@@ -893,7 +893,7 @@ export class AcpManager {
'',
'Important:',
'- Interpret the user\'s intent and decide what to search. Don\'t just match their exact words.',
- '- If the user names a specific source (claude/codex/gemini/opencode), only return results from that source unless they explicitly ask for cross-source search.',
+ '- If the user names a specific source (claude/codex/gemini/antigravity/opencode), only return results from that source unless they explicitly ask for cross-source search.',
'- For cross-source questions, first identify the relevant sources, then query each source separately, confirm hits or no-hits per source, and only then merge them into one answer.',
'- For temporal queries ("what did I do recently"), use explicit date filters and be conservative when comparing times across different sources.',
'- You may run multiple queries to find relevant information.',
diff --git a/packages/app/src/main/index.ts b/packages/app/src/main/index.ts
index ff3ec1b4..9682e20c 100644
--- a/packages/app/src/main/index.ts
+++ b/packages/app/src/main/index.ts
@@ -802,7 +802,7 @@ ipcMain.handle('spool:search', (_e, { query, limit = 10, source, onlyPinned, ide
if (cached) return cached
}
- const sessionSource = source === 'claude' || source === 'codex' || source === 'gemini' || source === 'opencode'
+ const sessionSource = source === 'claude' || source === 'codex' || source === 'gemini' || source === 'antigravity' || source === 'opencode'
? source
: undefined
const results = searchFragments(db, query, {
@@ -824,7 +824,7 @@ ipcMain.handle('spool:search-preview', (_e, { query, limit = 5, source }: { quer
const cached = searchCache.get(cacheKey)
if (cached) return cached
- const sessionSource = source === 'claude' || source === 'codex' || source === 'gemini' || source === 'opencode'
+ const sessionSource = source === 'claude' || source === 'codex' || source === 'gemini' || source === 'antigravity' || source === 'opencode'
? source
: undefined
const fragments = searchSessionPreview(db, query, {
@@ -983,11 +983,11 @@ ipcMain.handle('spool:force-resync-session', (_e, { sessionUuid }: { sessionUuid
ipcMain.handle('spool:resume-cli', (_e, { sessionUuid, source, cwd }: { sessionUuid: string; source: string; cwd?: string }) => {
try {
+ const session = getSessionWithMessages(db, sessionUuid)?.session
const command = getSessionResumeCommand(source, sessionUuid)
if (!command) {
return { ok: false, error: `Session source "${source}" cannot be resumed from the CLI.` }
}
- const session = getSessionWithMessages(db, sessionUuid)?.session
const resumeCwd = session
? resolveResumeWorkingDirectory(session)
: resolveResumeWorkingDirectory({
diff --git a/packages/app/src/renderer/App.tsx b/packages/app/src/renderer/App.tsx
index 126475d4..fb488d07 100644
--- a/packages/app/src/renderer/App.tsx
+++ b/packages/app/src/renderer/App.tsx
@@ -907,6 +907,7 @@ export default function App() {
claudeCount={status?.claudeSessions ?? null}
codexCount={status?.codexSessions ?? null}
geminiCount={status?.geminiSessions ?? null}
+ antigravityCount={status?.antigravitySessions ?? null}
opencodeCount={status?.opencodeSessions ?? null}
themeEditor={themeEditor}
onThemeEditorChange={setThemeEditor}
@@ -1103,6 +1104,7 @@ export default function App() {
claudeCount={status?.claudeSessions ?? null}
codexCount={status?.codexSessions ?? null}
geminiCount={status?.geminiSessions ?? null}
+ antigravityCount={status?.antigravitySessions ?? null}
opencodeCount={status?.opencodeSessions ?? null}
themeEditor={themeEditor}
onThemeEditorChange={setThemeEditor}
diff --git a/packages/app/src/renderer/components/AiAnswerCard.tsx b/packages/app/src/renderer/components/AiAnswerCard.tsx
index 99014f33..81216339 100644
--- a/packages/app/src/renderer/components/AiAnswerCard.tsx
+++ b/packages/app/src/renderer/components/AiAnswerCard.tsx
@@ -109,7 +109,7 @@ export default function AiAnswerCard({ answer, streaming, agentName, sources, er
)}
{/* CTA — only shown when a resume target is wired (i.e. agent has a
- source we persisted a session row for: claude/codex/gemini/opencode). */}
+ source we persisted a session row for: claude/codex/gemini/antigravity/opencode). */}
{!streaming && answer && onResume && (
void
@@ -154,6 +155,7 @@ export default function SettingsPanel({
claudeCount,
codexCount,
geminiCount,
+ antigravityCount,
opencodeCount,
themeEditor,
onThemeEditorChange,
@@ -252,7 +254,7 @@ export default function SettingsPanel({
)}
{activeTab === 'shortcuts' && }
- {activeTab === 'sources' && }
+ {activeTab === 'sources' && }
{activeTab === 'agent' && }
{activeTab === 'account' && }
{activeTab === 'labs' && }
@@ -439,11 +441,13 @@ function SourcesTab({
claudeCount,
codexCount,
geminiCount,
+ antigravityCount,
opencodeCount,
}: {
claudeCount: number | null
codexCount: number | null
geminiCount: number | null
+ antigravityCount: number | null
opencodeCount: number | null
}) {
const { t } = useTranslation()
@@ -453,6 +457,7 @@ function SourcesTab({
+
diff --git a/packages/app/src/renderer/i18n/locales/de.json b/packages/app/src/renderer/i18n/locales/de.json
index b9af12da..eaf8d92c 100644
--- a/packages/app/src/renderer/i18n/locales/de.json
+++ b/packages/app/src/renderer/i18n/locales/de.json
@@ -140,7 +140,7 @@
"pinnedSessions": "Angeheftet",
"viewAll": "Alle anzeigen",
"empty_title": "Noch keine Sitzungen",
- "empty_body": "Öffnen Sie Claude Code, Codex, Gemini oder OpenCode, um eine Sitzung zu starten – Spool indexiert sie automatisch.",
+ "empty_body": "Öffnen Sie Claude Code, Codex, Gemini, Antigravity oder OpenCode, um eine Sitzung zu starten – Spool indexiert sie automatisch.",
"section_pinned": "Angeheftet · {{count}} Sitzung",
"section_pinned_other": "Angeheftet · {{count}} Sitzungen",
"section_recent": "ZULETZT",
diff --git a/packages/app/src/renderer/i18n/locales/en.json b/packages/app/src/renderer/i18n/locales/en.json
index df1a6ea6..6168cd7e 100644
--- a/packages/app/src/renderer/i18n/locales/en.json
+++ b/packages/app/src/renderer/i18n/locales/en.json
@@ -140,7 +140,7 @@
"pinnedSessions": "Pinned",
"viewAll": "View all",
"empty_title": "No sessions yet",
- "empty_body": "Open Claude Code, Codex, Gemini, or OpenCode to start a session — Spool indexes it automatically.",
+ "empty_body": "Open Claude Code, Codex, Gemini, Antigravity, or OpenCode to start a session — Spool indexes it automatically.",
"section_pinned": "Pinned · {{count}} session",
"section_pinned_other": "Pinned · {{count}} sessions",
"section_recent": "RECENT",
diff --git a/packages/app/src/renderer/i18n/locales/fr.json b/packages/app/src/renderer/i18n/locales/fr.json
index 952f6c94..867fd95f 100644
--- a/packages/app/src/renderer/i18n/locales/fr.json
+++ b/packages/app/src/renderer/i18n/locales/fr.json
@@ -140,7 +140,7 @@
"pinnedSessions": "Épinglés",
"viewAll": "Tout afficher",
"empty_title": "Aucune session pour l'instant",
- "empty_body": "Ouvrez Claude Code, Codex, Gemini ou OpenCode pour démarrer une session — Spool l'indexe automatiquement.",
+ "empty_body": "Ouvrez Claude Code, Codex, Gemini, Antigravity ou OpenCode pour démarrer une session — Spool l'indexe automatiquement.",
"section_pinned": "Épinglé · {{count}} session",
"section_pinned_other": "Épinglés · {{count}} sessions",
"section_recent": "RÉCENT",
diff --git a/packages/app/src/renderer/i18n/locales/ko.json b/packages/app/src/renderer/i18n/locales/ko.json
index a92076e3..b8873a02 100644
--- a/packages/app/src/renderer/i18n/locales/ko.json
+++ b/packages/app/src/renderer/i18n/locales/ko.json
@@ -136,7 +136,7 @@
"pinnedSessions": "고정됨",
"viewAll": "전체 보기",
"empty_title": "아직 세션이 없습니다",
- "empty_body": "Claude Code, Codex, Gemini, OpenCode를 열어 세션을 시작하세요 — Spool이 자동으로 인덱싱합니다.",
+ "empty_body": "Claude Code, Codex, Gemini, Antigravity, OpenCode를 열어 세션을 시작하세요 — Spool이 자동으로 인덱싱합니다.",
"section_pinned_other": "고정됨 · {{count}}개",
"section_recent": "최근",
"bucket_today": "오늘",
diff --git a/packages/app/src/renderer/lib/compose-from-session.ts b/packages/app/src/renderer/lib/compose-from-session.ts
index a23b32e1..6ffeed8d 100644
--- a/packages/app/src/renderer/lib/compose-from-session.ts
+++ b/packages/app/src/renderer/lib/compose-from-session.ts
@@ -16,6 +16,7 @@ const SOURCE_LABEL: Record = {
claude: 'Claude',
codex: 'Codex',
gemini: 'Gemini',
+ antigravity: 'Antigravity',
opencode: 'OpenCode',
}
diff --git a/packages/app/src/shared/resumeCommand.test.ts b/packages/app/src/shared/resumeCommand.test.ts
index ba722187..2b06e285 100644
--- a/packages/app/src/shared/resumeCommand.test.ts
+++ b/packages/app/src/shared/resumeCommand.test.ts
@@ -6,6 +6,7 @@ describe('getSessionResumeCommandPrefix', () => {
expect(getSessionResumeCommandPrefix('claude')).toBe('claude --resume')
expect(getSessionResumeCommandPrefix('codex')).toBe('codex resume')
expect(getSessionResumeCommandPrefix('gemini')).toBe('gemini --resume')
+ expect(getSessionResumeCommandPrefix('antigravity')).toBe('agy --conversation')
})
it('returns null for unsupported sources', () => {
@@ -18,6 +19,7 @@ describe('getSessionResumeCommand', () => {
expect(getSessionResumeCommand('claude', 'test-session-uuid')).toBe("claude --resume 'test-session-uuid'")
expect(getSessionResumeCommand('codex', '11111111-2222-4333-8444-555555555555')).toBe("codex resume '11111111-2222-4333-8444-555555555555'")
expect(getSessionResumeCommand('gemini', '99999999-2222-4333-8444-555555555555')).toBe("gemini --resume '99999999-2222-4333-8444-555555555555'")
+ expect(getSessionResumeCommand('antigravity', 'conv-uuid-123')).toBe("agy --conversation 'conv-uuid-123'")
})
it('escapes embedded single quotes safely', () => {
diff --git a/packages/app/src/shared/resumeCommand.ts b/packages/app/src/shared/resumeCommand.ts
index f48e9974..29f4eacb 100644
--- a/packages/app/src/shared/resumeCommand.ts
+++ b/packages/app/src/shared/resumeCommand.ts
@@ -2,6 +2,7 @@ const RESUME_COMMAND_PREFIXES: Record = {
claude: 'claude --resume',
codex: 'codex resume',
gemini: 'gemini --resume',
+ antigravity: 'agy --conversation',
opencode: 'opencode --session',
}
diff --git a/packages/app/src/shared/sessionSources.ts b/packages/app/src/shared/sessionSources.ts
index 4675598b..2d12a1e5 100644
--- a/packages/app/src/shared/sessionSources.ts
+++ b/packages/app/src/shared/sessionSources.ts
@@ -17,6 +17,12 @@ const SESSION_SOURCE_META = {
color: '#5887D0',
colorDark: '#8AB0E5',
},
+ antigravity: {
+ label: 'Antigravity',
+ shortLabel: 'agy',
+ color: '#6B5CA5',
+ colorDark: '#9B8FD0',
+ },
opencode: {
label: 'OpenCode',
shortLabel: 'opencode',
diff --git a/packages/cli/src/commands/list.ts b/packages/cli/src/commands/list.ts
index c7d96041..361754af 100644
--- a/packages/cli/src/commands/list.ts
+++ b/packages/cli/src/commands/list.ts
@@ -2,12 +2,12 @@ import { Command } from 'commander'
import { getDB, listRecentSessionsPage } from '@spool-lab/core'
import { printSession } from '../format.js'
-const SESSION_SOURCES = new Set(['claude', 'codex', 'gemini', 'opencode'])
+const SESSION_SOURCES = new Set(['claude', 'codex', 'gemini', 'antigravity', 'opencode'])
export const listCommand = new Command('list')
.description('List recent AI sessions')
.option('-n, --limit ', 'Max results', '20')
- .option('-s, --source ', 'Filter by source: claude|codex|gemini|opencode')
+ .option('-s, --source ', 'Filter by source: claude|codex|gemini|antigravity|opencode')
.option('-p, --project ', 'Filter by project path substring')
.option('--json', 'Output as JSON')
.action((opts: { limit: string; source?: string; project?: string; json?: boolean }) => {
diff --git a/packages/cli/src/commands/search.ts b/packages/cli/src/commands/search.ts
index e50bd856..cf2ede16 100644
--- a/packages/cli/src/commands/search.ts
+++ b/packages/cli/src/commands/search.ts
@@ -2,13 +2,13 @@ import { Command } from 'commander'
import { getDB, searchFragments } from '@spool-lab/core'
import type { FragmentResult, SessionSource } from '@spool-lab/core'
-const SESSION_SOURCES = new Set(['claude', 'codex', 'gemini', 'opencode'])
+const SESSION_SOURCES = new Set(['claude', 'codex', 'gemini', 'antigravity', 'opencode'])
export const searchCommand = new Command('search')
.description('Search your AI session history')
.argument('', 'Search query')
.option('-n, --limit ', 'Max results', '10')
- .option('-s, --source ', 'Filter by source: claude|codex|gemini|opencode')
+ .option('-s, --source ', 'Filter by source: claude|codex|gemini|antigravity|opencode')
.option('--since ', 'Only search sessions after this date (ISO or relative like "7d")')
.option('--json', 'Output as JSON')
.action(async (query: string, opts: { limit: string; source?: string; since?: string; json?: boolean }) => {
diff --git a/packages/core/README.md b/packages/core/README.md
index b416e00a..6bbda3d9 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -17,14 +17,14 @@ const results = searchFragments(db, 'authentication middleware', { limit: 10 })
// List recent sessions
const sessions = listRecentSessions(db, 20)
-// Sync new sessions from Claude, Codex, Gemini, OpenCode
+// Sync new sessions from Claude, Codex, Gemini, Antigravity, OpenCode
const syncer = new Syncer(db)
syncer.syncAll()
```
## What's inside
-- **Session parsers** — reads Claude Code, Codex, Gemini CLI, and OpenCode sessions
+- **Session parsers** — reads Claude Code, Codex, Gemini CLI, Antigravity CLI, and OpenCode sessions
- **Full-text search** — FTS5 with unicode + trigram indexes for CJK support
- **Watcher** — incremental indexing as new session files arrive
- **Stars** — pin sessions for quick recall
diff --git a/packages/core/src/db/db.ts b/packages/core/src/db/db.ts
index 118b4c56..5e1f452b 100644
--- a/packages/core/src/db/db.ts
+++ b/packages/core/src/db/db.ts
@@ -79,6 +79,7 @@ export function runMigrations(db: Database.Database): void {
('claude', '~/.claude/projects'),
('codex', '~/.codex/sessions'),
('gemini', '~/.gemini/tmp'),
+ ('antigravity', '~/.gemini/antigravity-cli/brain'),
('opencode', '~/.local/share/opencode/opencode.db');
CREATE TABLE IF NOT EXISTS projects (
diff --git a/packages/core/src/db/migration-v1-v3.test.ts b/packages/core/src/db/migration-v1-v3.test.ts
index b47f951a..aac02b9c 100644
--- a/packages/core/src/db/migration-v1-v3.test.ts
+++ b/packages/core/src/db/migration-v1-v3.test.ts
@@ -151,7 +151,7 @@ describe('migration v1-v3 (historical connector path)', () => {
// 'connector' source row dropped by v5
const sources = (db.prepare('SELECT name FROM sources').all() as { name: string }[]).map(s => s.name)
- expect(sources.sort()).toEqual(['claude', 'codex', 'gemini', 'opencode'])
+ expect(sources.sort()).toEqual(['antigravity', 'claude', 'codex', 'gemini', 'opencode'])
// Session preserved
const sess = db.prepare("SELECT session_uuid FROM sessions WHERE session_uuid='sess-uuid'").get() as { session_uuid: string }
diff --git a/packages/core/src/db/migration-v5.test.ts b/packages/core/src/db/migration-v5.test.ts
index 91b3bcc4..46a154fb 100644
--- a/packages/core/src/db/migration-v5.test.ts
+++ b/packages/core/src/db/migration-v5.test.ts
@@ -162,7 +162,7 @@ describe('migration v5 (connector subsystem removal)', () => {
// 'connector' source row also dropped
const sources = db.prepare('SELECT name FROM sources').all() as Array<{ name: string }>
- expect(sources.map(s => s.name).sort()).toEqual(['claude', 'codex', 'gemini', 'opencode'])
+ expect(sources.map(s => s.name).sort()).toEqual(['antigravity', 'claude', 'codex', 'gemini', 'opencode'])
// After v7: stars dropped, session star preserved as pin, capture star gone
expect(tableNames.has('stars')).toBe(false)
diff --git a/packages/core/src/db/queries.ts b/packages/core/src/db/queries.ts
index 9782aee6..b431c3f9 100644
--- a/packages/core/src/db/queries.ts
+++ b/packages/core/src/db/queries.ts
@@ -1077,6 +1077,7 @@ export function getStatus(db: Database.Database): StatusInfo {
const claudeRow = counts.find(r => r.name === 'claude')
const codexRow = counts.find(r => r.name === 'codex')
const geminiRow = counts.find(r => r.name === 'gemini')
+ const antigravityRow = counts.find(r => r.name === 'antigravity')
const opencodeRow = counts.find(r => r.name === 'opencode')
return {
@@ -1085,6 +1086,7 @@ export function getStatus(db: Database.Database): StatusInfo {
claudeSessions: claudeRow?.cnt ?? 0,
codexSessions: codexRow?.cnt ?? 0,
geminiSessions: geminiRow?.cnt ?? 0,
+ antigravitySessions: antigravityRow?.cnt ?? 0,
opencodeSessions: opencodeRow?.cnt ?? 0,
lastSyncedAt: lastSync?.last ?? null,
dbSizeBytes: getDBSize(),
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index b534085d..20c2e359 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -6,6 +6,7 @@ export * from './db/published-shares-cache.js'
export * from './parsers/claude.js'
export * from './parsers/codex.js'
export * from './parsers/gemini.js'
+export * from './parsers/antigravity.js'
export * from './parsers/opencode.js'
export * from './parsers/spool-prelude.js'
export * from './sync/syncer.js'
diff --git a/packages/core/src/parsers/antigravity.test.ts b/packages/core/src/parsers/antigravity.test.ts
new file mode 100644
index 00000000..ab8cbe43
--- /dev/null
+++ b/packages/core/src/parsers/antigravity.test.ts
@@ -0,0 +1,205 @@
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
+import { join } from 'node:path'
+import { tmpdir } from 'node:os'
+import { parseAntigravitySession } from './antigravity.js'
+
+const tempDirs: string[] = []
+
+afterEach(() => {
+ vi.unstubAllEnvs()
+ while (tempDirs.length > 0) {
+ const dir = tempDirs.pop()
+ if (dir) rmSync(dir, { recursive: true, force: true })
+ }
+})
+
+function makeAntigravityHome(): string {
+ const dir = mkdtempSync(join(tmpdir(), 'spool-antigravity-parser-'))
+ tempDirs.push(dir)
+ return dir
+}
+
+function makeTranscript(steps: object[]): string {
+ return steps.map(s => JSON.stringify(s)).join('\n')
+}
+
+function writeHistoryJsonl(cliRoot: string, convId: string, cwd: string | null = '/tmp/project'): void {
+ const historyPath = join(cliRoot, 'history.jsonl')
+ const entry: Record = { conversationId: convId }
+ if (cwd) entry.workspace = cwd
+ writeFileSync(historyPath, JSON.stringify(entry) + '\n')
+}
+
+describe('parseAntigravitySession', () => {
+ it('extracts user prompt from tags', () => {
+ const cliRoot = makeAntigravityHome()
+ const convId = 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'
+ writeHistoryJsonl(cliRoot, convId)
+ const logsDir = join(cliRoot, 'brain', convId, '.system_generated', 'logs')
+ mkdirSync(logsDir, { recursive: true })
+
+ vi.stubEnv('ANTIGRAVITY_CLI_HOME', cliRoot)
+
+ const filePath = join(logsDir, 'transcript.jsonl')
+ writeFileSync(filePath, makeTranscript([
+ {
+ step_index: 0,
+ source: 'USER_EXPLICIT',
+ type: 'USER_INPUT',
+ status: 'DONE',
+ created_at: '2026-06-01T00:00:00Z',
+ content: '\u0013command(just build) command(tail) \u0010command(git log)\n\nHelp me fix the parser\n ',
+ },
+ {
+ step_index: 1,
+ source: 'MODEL',
+ type: 'PLANNER_RESPONSE',
+ status: 'DONE',
+ created_at: '2026-06-01T00:00:30Z',
+ content: 'I will inspect the parser implementation.',
+ tool_calls: [{ name: 'view_file', args: { AbsolutePath: '/tmp/parser.ts' } }],
+ },
+ ]))
+
+ const parsed = parseAntigravitySession(filePath)
+ expect(parsed).not.toBeNull()
+ expect(parsed?.source).toBe('antigravity')
+ expect(parsed?.sessionUuid).toBe(convId)
+ expect(parsed?.title).toBe('Help me fix the parser')
+ expect(parsed?.messages).toHaveLength(2)
+ expect(parsed?.messages[0]?.role).toBe('user')
+ expect(parsed?.messages[0]?.contentText).toBe('Help me fix the parser')
+ expect(parsed?.messages[1]?.role).toBe('assistant')
+ expect(parsed?.messages[1]?.toolNames).toEqual(['view_file'])
+ })
+
+ it('preserves newlines in user content', () => {
+ const cliRoot = makeAntigravityHome()
+ const convId = 'bbbbbbbb-cccc-4ddd-8eee-ffffffffffff'
+ writeHistoryJsonl(cliRoot, convId)
+ const logsDir = join(cliRoot, 'brain', convId, '.system_generated', 'logs')
+ mkdirSync(logsDir, { recursive: true })
+ vi.stubEnv('ANTIGRAVITY_CLI_HOME', cliRoot)
+
+ const filePath = join(logsDir, 'transcript.jsonl')
+ writeFileSync(filePath, makeTranscript([
+ {
+ step_index: 0,
+ source: 'USER_EXPLICIT',
+ type: 'USER_INPUT',
+ status: 'DONE',
+ created_at: '2026-06-01T00:00:00Z',
+ content: '\nLine one\nLine two\nLine three\n ',
+ },
+ ]))
+
+ const parsed = parseAntigravitySession(filePath)
+ expect(parsed?.messages[0]?.contentText).toBe('Line one\nLine two\nLine three')
+ })
+
+ it('skips CONVERSATION_HISTORY steps', () => {
+ const cliRoot = makeAntigravityHome()
+ const convId = 'cccccccc-dddd-4eee-8fff-aaaaaaaaaaaa'
+ writeHistoryJsonl(cliRoot, convId)
+ const logsDir = join(cliRoot, 'brain', convId, '.system_generated', 'logs')
+ mkdirSync(logsDir, { recursive: true })
+ vi.stubEnv('ANTIGRAVITY_CLI_HOME', cliRoot)
+
+ const filePath = join(logsDir, 'transcript.jsonl')
+ writeFileSync(filePath, makeTranscript([
+ {
+ step_index: 0,
+ source: 'SYSTEM',
+ type: 'CONVERSATION_HISTORY',
+ status: 'DONE',
+ created_at: '2026-06-01T00:00:00Z',
+ content: 'Previous conversation context...',
+ },
+ {
+ step_index: 1,
+ source: 'USER_EXPLICIT',
+ type: 'USER_INPUT',
+ status: 'DONE',
+ created_at: '2026-06-01T00:00:10Z',
+ content: '\nActual user prompt\n ',
+ },
+ ]))
+
+ const parsed = parseAntigravitySession(filePath)
+ expect(parsed?.messages).toHaveLength(1)
+ expect(parsed?.messages[0]?.contentText).toBe('Actual user prompt')
+ })
+
+ it('falls back to full content when USER_REQUEST tags are missing', () => {
+ const cliRoot = makeAntigravityHome()
+ const convId = 'dddddddd-eeee-4fff-8aaa-bbbbbbbbbbbb'
+ writeHistoryJsonl(cliRoot, convId)
+ const logsDir = join(cliRoot, 'brain', convId, '.system_generated', 'logs')
+ mkdirSync(logsDir, { recursive: true })
+ vi.stubEnv('ANTIGRAVITY_CLI_HOME', cliRoot)
+
+ const filePath = join(logsDir, 'transcript.jsonl')
+ writeFileSync(filePath, makeTranscript([
+ {
+ step_index: 0,
+ source: 'USER_EXPLICIT',
+ type: 'USER_INPUT',
+ status: 'DONE',
+ created_at: '2026-06-01T00:00:00Z',
+ content: 'Just a plain text prompt without tags',
+ },
+ ]))
+
+ const parsed = parseAntigravitySession(filePath)
+ expect(parsed?.messages[0]?.contentText).toBe('Just a plain text prompt without tags')
+ })
+
+ it('falls back to empty CWD (Loose) when missing in history.jsonl', () => {
+ const cliRoot = makeAntigravityHome()
+ const convId = 'eeeeeeee-ffff-4aaa-8bbb-cccccccccccc'
+ // Write history entry without CWD/workspace to force database fallback
+ writeHistoryJsonl(cliRoot, convId, null)
+ const logsDir = join(cliRoot, 'brain', convId, '.system_generated', 'logs')
+ mkdirSync(logsDir, { recursive: true })
+ vi.stubEnv('ANTIGRAVITY_CLI_HOME', cliRoot)
+
+ const filePath = join(logsDir, 'transcript.jsonl')
+ writeFileSync(filePath, makeTranscript([
+ {
+ step_index: 0,
+ source: 'USER_EXPLICIT',
+ type: 'USER_INPUT',
+ status: 'DONE',
+ created_at: '2026-06-01T00:00:00Z',
+ content: 'Testing empty CWD fallback',
+ },
+ ]))
+
+ const parsed = parseAntigravitySession(filePath)
+ expect(parsed?.cwd).toBe('')
+ })
+
+ it('skips sessions that are not present in history.jsonl', () => {
+ const cliRoot = makeAntigravityHome()
+ const convId = 'ffffffff-0000-4111-8222-333333333333'
+ const logsDir = join(cliRoot, 'brain', convId, '.system_generated', 'logs')
+ mkdirSync(logsDir, { recursive: true })
+ vi.stubEnv('ANTIGRAVITY_CLI_HOME', cliRoot)
+
+ const filePath = join(logsDir, 'transcript.jsonl')
+ writeFileSync(filePath, makeTranscript([
+ {
+ step_index: 0,
+ source: 'USER_EXPLICIT',
+ type: 'USER_INPUT',
+ status: 'DONE',
+ created_at: '2026-06-01T00:00:00Z',
+ content: 'Should be skipped since not in history.jsonl',
+ },
+ ]))
+
+ const result = parseAntigravitySession(filePath)
+ expect(result).toBeNull() // parseAntigravitySession returns null for skipped results
+ })
+})
diff --git a/packages/core/src/parsers/antigravity.ts b/packages/core/src/parsers/antigravity.ts
new file mode 100644
index 00000000..df1fd50a
--- /dev/null
+++ b/packages/core/src/parsers/antigravity.ts
@@ -0,0 +1,193 @@
+import { readFileSync, existsSync } from 'node:fs'
+import { basename, join } from 'node:path'
+import { homedir } from 'node:os'
+import type { ParseSessionResult, ParsedMessage, ParsedSession } from '../types.js'
+
+export const ANTIGRAVITY_INDEX_VERSION = 'antigravity-v1-jsonl-transcript'
+
+interface AntigravityStep {
+ step_index: number
+ source: string
+ type: string
+ status: string
+ created_at: string
+ content?: string | null
+ thinking?: string | null
+ tool_calls?: Array<{ name: string; args?: Record }> | null
+}
+
+function cleanUserContent(content: string): string {
+ const startTag = ''
+ const endTag = ' '
+ const startIdx = content.indexOf(startTag)
+ if (startIdx >= 0) {
+ const endIdx = content.indexOf(endTag)
+ if (endIdx > startIdx) {
+ const start = startIdx + startTag.length
+ return content.slice(start, endIdx).trim()
+ }
+ }
+ return content.trim()
+}
+
+// Check for explicit GEMINI_CLI_HOME or ANTIGRAVITY_CLI_HOME.
+// This matches standard path expansion.
+function getAntigravityCliRoot(): string {
+ const explicit = process.env['ANTIGRAVITY_CLI_HOME']?.trim()
+ if (explicit) return expandHome(explicit)
+
+ const configuredHome = process.env['GEMINI_CLI_HOME']?.trim()
+ if (configuredHome) {
+ const resolved = expandHome(configuredHome)
+ if (basename(resolved) === 'antigravity-cli') return resolved
+ return join(resolved, '.gemini', 'antigravity-cli')
+ }
+
+ return join(homedir(), '.gemini', 'antigravity-cli')
+}
+
+function expandHome(filePath: string): string {
+ if (filePath === '~') return homedir()
+ if (filePath.startsWith('~/')) return join(homedir(), filePath.slice(2))
+ return filePath
+}
+
+function getHistoryConversationCwds(): Map {
+ const cliRoot = getAntigravityCliRoot()
+ const mappings = new Map()
+
+ // Load ONLY from history.jsonl
+ const historyPath = join(cliRoot, 'history.jsonl')
+ try {
+ if (existsSync(historyPath)) {
+ const raw = readFileSync(historyPath, 'utf8')
+ const lines = raw.split('\n')
+ for (const line of lines) {
+ if (!line.trim()) continue
+ try {
+ const data = JSON.parse(line)
+ const cid = data.conversationId
+ const ws = data.workspace
+ if (cid && typeof cid === 'string') {
+ mappings.set(cid, typeof ws === 'string' ? ws : '')
+ }
+ } catch {
+ // Ignore parse errors on individual lines
+ }
+ }
+ }
+ } catch {
+ // Ignore history load failure
+ }
+
+ return mappings
+}
+
+function extractConversationId(filePath: string): string {
+ // .../brain//.system_generated/logs/transcript.jsonl
+ const parts = filePath.split('/')
+ const logsIdx = parts.lastIndexOf('logs')
+ if (logsIdx >= 3) {
+ return parts[logsIdx - 2]! // conversation-id is 2 levels above logs/
+ }
+ return ''
+}
+
+export function loadAntigravitySession(filePath: string): ParseSessionResult {
+ const raw = readFileSync(filePath, 'utf8')
+ const lines = raw.split('\n').filter(l => l.trim().length > 0)
+ const messages: ParsedMessage[] = []
+ let model = ''
+
+ const conversationId = extractConversationId(filePath)
+
+ const historyCwds = getHistoryConversationCwds()
+ if (!historyCwds.has(conversationId)) {
+ return { kind: 'skipped' }
+ }
+
+ for (const line of lines) {
+ let step: AntigravityStep
+ try {
+ step = JSON.parse(line) as AntigravityStep
+ } catch {
+ continue
+ }
+
+ const { type, content, created_at: timestamp } = step
+ if (!type || !timestamp) continue
+
+ if (type === 'CONVERSATION_HISTORY') continue
+
+ if (type === 'USER_INPUT') {
+ const text = cleanUserContent(content ?? '')
+ if (text) {
+ messages.push({
+ uuid: `agy-${conversationId}-${step.step_index}`,
+ parentUuid: null,
+ role: 'user',
+ contentText: text,
+ timestamp,
+ isSidechain: false,
+ toolNames: [],
+ seq: messages.length,
+ })
+ }
+ continue
+ }
+
+ if (type === 'PLANNER_RESPONSE') {
+ const text = (content ?? '').trim()
+ const toolNames = (step.tool_calls ?? [])
+ .map(tc => tc.name)
+ .filter((name): name is string => typeof name === 'string' && name.length > 0)
+ if (text || toolNames.length > 0) {
+ messages.push({
+ uuid: `agy-${conversationId}-${step.step_index}`,
+ parentUuid: null,
+ role: 'assistant',
+ contentText: text,
+ timestamp,
+ isSidechain: false,
+ toolNames,
+ seq: messages.length,
+ })
+ }
+ continue
+ }
+ }
+
+ if (messages.length === 0) return { kind: 'skipped' }
+
+ const cliCwds = getHistoryConversationCwds()
+ const cwd = cliCwds.get(conversationId) || ''
+
+ const firstUserMsg = messages.find(m => m.role === 'user' && m.contentText.trim().length > 0)
+ const title = firstUserMsg?.contentText.slice(0, 120) ?? `Antigravity ${conversationId.slice(0, 8)}`
+
+ const timestamps = messages.map(m => m.timestamp).filter(Boolean).sort()
+
+ return {
+ kind: 'parsed',
+ session: {
+ source: 'antigravity',
+ sessionUuid: conversationId || filePath,
+ filePath,
+ title,
+ cwd,
+ model: model || '',
+ startedAt: timestamps[0] ?? new Date().toISOString(),
+ endedAt: timestamps[timestamps.length - 1] ?? new Date().toISOString(),
+ messages,
+ },
+ }
+}
+
+export function parseAntigravitySession(filePath: string): ParsedSession | null {
+ try {
+ const result = loadAntigravitySession(filePath)
+ return result.kind === 'parsed' ? result.session : null
+ } catch {
+ return null
+ }
+}
diff --git a/packages/core/src/sync/source-paths.ts b/packages/core/src/sync/source-paths.ts
index 7465352a..67d92c80 100644
--- a/packages/core/src/sync/source-paths.ts
+++ b/packages/core/src/sync/source-paths.ts
@@ -4,7 +4,7 @@ import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep
import type { SessionSource } from '../types.js'
import { OPENCODE_DB_NAME, isOpenCodeDatabaseFile } from '../parsers/opencode.js'
-const SOURCE_DIR_NAMES: Record, string> = {
+const SOURCE_DIR_NAMES: Record, string> = {
claude: 'projects',
codex: 'sessions',
}
@@ -13,15 +13,16 @@ const SOURCE_ENV_VARS: Record = {
claude: 'SPOOL_CLAUDE_DIR',
codex: 'SPOOL_CODEX_DIR',
gemini: 'SPOOL_GEMINI_DIR',
+ antigravity: 'SPOOL_ANTIGRAVITY_DIR',
opencode: 'SPOOL_OPENCODE_DIR',
}
-const SOURCE_DEFAULT_BASES: Record, string> = {
+const SOURCE_DEFAULT_BASES: Record, string> = {
claude: '.claude',
codex: '.codex',
}
-const SOURCE_PROFILE_BASES: Record, string> = {
+const SOURCE_PROFILE_BASES: Record, string> = {
claude: '.claude-profiles',
codex: '.codex-profiles',
}
@@ -36,6 +37,10 @@ export function getSessionRoots(source: SessionSource): string[] {
return dedupePaths([normalizeSourceRoot('gemini', join(getGeminiBaseDir(), 'tmp'))])
}
+ if (source === 'antigravity') {
+ return dedupePaths([normalizeSourceRoot('antigravity', join(getAntigravityCliBaseDir(), 'brain'))])
+ }
+
if (source === 'opencode') {
return dedupePaths([normalizeSourceRoot('opencode', getOpenCodeBaseDir())])
}
@@ -66,10 +71,11 @@ export function detectSessionSource(
claude: getSessionRoots('claude'),
codex: getSessionRoots('codex'),
gemini: getSessionRoots('gemini'),
+ antigravity: getSessionRoots('antigravity'),
opencode: getSessionRoots('opencode'),
},
): SessionSource | undefined {
- for (const source of ['claude', 'codex', 'gemini', 'opencode'] as const) {
+ for (const source of ['claude', 'codex', 'gemini', 'antigravity', 'opencode'] as const) {
if (sourceRoots[source]?.some(root => isSessionFileForSource(source, filePath, root))) {
return source
}
@@ -81,6 +87,9 @@ export function getSessionWatchPatterns(
source: SessionSource,
roots = getSessionRoots(source),
): string[] {
+ if (source === 'antigravity') {
+ return roots.map(root => join(root, '**', 'transcript.jsonl'))
+ }
const pattern = source === 'gemini'
? 'session-*.json'
: source === 'opencode'
@@ -109,6 +118,10 @@ function normalizeSourceRoot(source: SessionSource, filePath: string): string {
return resolvedPath
}
+ if (source === 'antigravity') {
+ return resolvedPath
+ }
+
if (source === 'opencode') {
if (basename(resolvedPath) === OPENCODE_DB_NAME) return dirname(resolvedPath)
if (existsSync(join(resolvedPath, OPENCODE_DB_NAME))) return resolvedPath
@@ -142,6 +155,24 @@ function getGeminiBaseDir(): string {
: join(homedir(), '.gemini')
}
+function getGeminiCliBaseDir(): string {
+ const explicit = process.env['ANTIGRAVITY_CLI_HOME']?.trim()
+ if (explicit) return resolve(expandHome(explicit))
+
+ const configuredHome = process.env['GEMINI_CLI_HOME']?.trim()
+ if (configuredHome) {
+ const resolved = resolve(expandHome(configuredHome))
+ if (basename(resolved) === 'antigravity-cli') return resolved
+ return join(resolved, '.gemini', 'antigravity-cli')
+ }
+
+ return join(homedir(), '.gemini', 'antigravity-cli')
+}
+
+function getAntigravityCliBaseDir(): string {
+ return getGeminiCliBaseDir()
+}
+
function getOpenCodeBaseDir(): string {
const configuredHome = process.env['OPENCODE_DATA_DIR']?.trim()
if (configuredHome) return resolve(expandHome(configuredHome))
@@ -159,6 +190,9 @@ export function isSessionFileForSource(source: SessionSource, filePath: string,
&& basename(filePath).startsWith('session-')
&& /(?:^|\/)chats\//.test(filePath)
}
+ if (source === 'antigravity') {
+ return filePath.endsWith('transcript.jsonl')
+ }
if (source === 'opencode') {
return isOpenCodeDatabaseFile(filePath)
}
diff --git a/packages/core/src/sync/syncer.test.ts b/packages/core/src/sync/syncer.test.ts
index faf3af35..b4df658b 100644
--- a/packages/core/src/sync/syncer.test.ts
+++ b/packages/core/src/sync/syncer.test.ts
@@ -280,6 +280,7 @@ describe('Syncer', () => {
vi.stubEnv('SPOOL_CLAUDE_DIR', join(baseDir, 'missing-claude'))
vi.stubEnv('SPOOL_CODEX_DIR', join(baseDir, 'missing-codex'))
vi.stubEnv('SPOOL_GEMINI_DIR', join(baseDir, 'missing-gemini'))
+ vi.stubEnv('SPOOL_ANTIGRAVITY_DIR', join(baseDir, 'missing-antigravity'))
vi.stubEnv('SPOOL_OPENCODE_DIR', opencodeDir)
const dbPath = join(opencodeDir, 'opencode.db')
@@ -335,6 +336,7 @@ describe('Syncer', () => {
vi.stubEnv('SPOOL_CLAUDE_DIR', join(baseDir, 'missing-claude'))
vi.stubEnv('SPOOL_CODEX_DIR', join(baseDir, 'missing-codex'))
vi.stubEnv('SPOOL_GEMINI_DIR', join(baseDir, 'missing-gemini'))
+ vi.stubEnv('SPOOL_ANTIGRAVITY_DIR', join(baseDir, 'missing-antigravity'))
vi.stubEnv('SPOOL_OPENCODE_DIR', opencodeDir)
const dbPath = join(opencodeDir, 'opencode.db')
diff --git a/packages/core/src/sync/syncer.ts b/packages/core/src/sync/syncer.ts
index de655224..2e8deae4 100644
--- a/packages/core/src/sync/syncer.ts
+++ b/packages/core/src/sync/syncer.ts
@@ -5,6 +5,7 @@ import type Database from 'better-sqlite3'
import { loadClaudeSession, decodeProjectSlug } from '../parsers/claude.js'
import { loadCodexSession, CODEX_INDEX_VERSION } from '../parsers/codex.js'
import { loadGeminiSession } from '../parsers/gemini.js'
+import { loadAntigravitySession, ANTIGRAVITY_INDEX_VERSION } from '../parsers/antigravity.js'
import {
getOpenCodeSessionIndexedMtime,
isOpenCodeDatabaseFile,
@@ -80,7 +81,7 @@ export class Syncer {
const seenPaths = new Set()
const files: Array<{ path: string; source: SessionSource }> = []
- for (const source of ['claude', 'codex', 'gemini', 'opencode'] as const) {
+ for (const source of ['claude', 'codex', 'gemini', 'antigravity', 'opencode'] as const) {
for (const dir of getSessionRoots(source)) {
try { addUniqueFiles(files, seenPaths, collectSessionFiles(dir, source)) } catch { /* dir may not exist */ }
}
@@ -275,7 +276,9 @@ export class Syncer {
? loadCodexSession(filePath)
: source === 'gemini'
? loadGeminiSession(filePath)
- : loadOpenCodeSession(filePath)
+ : source === 'antigravity'
+ ? loadAntigravitySession(filePath)
+ : loadOpenCodeSession(filePath)
if (parseResult.kind !== 'parsed') {
// The "filtered" path normally removes a session whose source
@@ -523,6 +526,7 @@ function getIndexedMtime(filePath: string, source: SessionSource): string {
function getIndexVersion(source: SessionSource): string {
if (source === 'codex') return CODEX_INDEX_VERSION
if (source === 'gemini') return 'gemini-v1-session-search-fts'
+ if (source === 'antigravity') return ANTIGRAVITY_INDEX_VERSION
if (source === 'opencode') return OPENCODE_INDEX_VERSION
return 'claude-v3-session-search-fts'
}
@@ -635,6 +639,12 @@ function resolveProject(
const displayName = parts[parts.length - 1] ?? 'codex'
const slug = displayPath.replace(/^\//, '').replace(/\//g, '-') || 'default'
return { slug, displayPath, displayName }
+ } else if (source === 'antigravity') {
+ const displayPath = cwd || home
+ const parts = displayPath.split('/').filter(Boolean)
+ const displayName = parts[parts.length - 1] ?? 'antigravity'
+ const slug = displayPath.replace(/^\//, '').replace(/\//g, '-') || 'default'
+ return { slug, displayPath, displayName }
} else if (source === 'opencode') {
const displayPath = cwd || home
const parts = displayPath.split('/').filter(Boolean)
diff --git a/packages/core/src/sync/watcher.test.ts b/packages/core/src/sync/watcher.test.ts
index a3c8a32e..ed3bb1fb 100644
--- a/packages/core/src/sync/watcher.test.ts
+++ b/packages/core/src/sync/watcher.test.ts
@@ -28,15 +28,18 @@ function makeTempRoots() {
const codexRoot = join(baseDir, 'codex', 'sessions')
const geminiRoot = join(baseDir, 'gemini', 'tmp')
const opencodeRoot = join(baseDir, 'opencode')
+ const antigravityRoot = join(baseDir, 'antigravity')
mkdirSync(join(claudeRoot, 'project-a'), { recursive: true })
mkdirSync(join(codexRoot, '2026', '04', '20'), { recursive: true })
mkdirSync(join(geminiRoot, 'workspace', 'chats'), { recursive: true })
mkdirSync(opencodeRoot, { recursive: true })
+ mkdirSync(antigravityRoot, { recursive: true })
vi.stubEnv('SPOOL_CLAUDE_DIR', claudeRoot)
vi.stubEnv('SPOOL_CODEX_DIR', codexRoot)
vi.stubEnv('SPOOL_GEMINI_DIR', join(baseDir, 'gemini'))
+ vi.stubEnv('SPOOL_ANTIGRAVITY_DIR', antigravityRoot)
vi.stubEnv('SPOOL_OPENCODE_DIR', opencodeRoot)
- return { baseDir, claudeRoot, codexRoot, geminiRoot, opencodeRoot }
+ return { baseDir, claudeRoot, codexRoot, geminiRoot, antigravityRoot, opencodeRoot }
}
interface SyncCall { path: string; source: SessionSource }
diff --git a/packages/core/src/sync/watcher.ts b/packages/core/src/sync/watcher.ts
index eb503e5e..4c241373 100644
--- a/packages/core/src/sync/watcher.ts
+++ b/packages/core/src/sync/watcher.ts
@@ -46,7 +46,7 @@ export class SpoolWatcher {
private pending: Map = new Map()
private pendingNew = 0
private flushTimer: ReturnType | null = null
- private sourceRoots: Record = { claude: [], codex: [], gemini: [], opencode: [] }
+ private sourceRoots: Record = { claude: [], codex: [], gemini: [], antigravity: [], opencode: [] }
private stopped = false
private readonly stabilityMs: number
private readonly pollMs: number
@@ -66,12 +66,14 @@ export class SpoolWatcher {
claude: getSessionRoots('claude'),
codex: getSessionRoots('codex'),
gemini: getSessionRoots('gemini'),
+ antigravity: getSessionRoots('antigravity'),
opencode: getSessionRoots('opencode'),
}
const roots = [
...this.sourceRoots.claude,
...this.sourceRoots.codex,
...this.sourceRoots.gemini,
+ ...this.sourceRoots.antigravity,
...this.sourceRoots.opencode,
]
for (const root of roots) this.watchRoot(root)
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index c84997ee..6d9291a1 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -1,4 +1,4 @@
-export type SessionSource = 'claude' | 'codex' | 'gemini' | 'opencode'
+export type SessionSource = 'claude' | 'codex' | 'gemini' | 'antigravity' | 'opencode'
export type Source = SessionSource
export type SearchMatchType = 'fts' | 'phrase' | 'all_terms'
@@ -127,6 +127,7 @@ export interface StatusInfo {
claudeSessions: number
codexSessions: number
geminiSessions: number
+ antigravitySessions: number
opencodeSessions: number
lastSyncedAt: string | null
dbSizeBytes: number
diff --git a/packages/landing/pages/blog/hello-spool.md b/packages/landing/pages/blog/hello-spool.md
index c3d8aa4d..780361f0 100644
--- a/packages/landing/pages/blog/hello-spool.md
+++ b/packages/landing/pages/blog/hello-spool.md
@@ -6,7 +6,7 @@ author: Yifeng
tags: [announcement, product]
---
-If you use Claude Code, Codex, Gemini CLI, or any AI coding agent daily, you've accumulated hundreds of sessions. Each one contains decisions, debugging breakthroughs, architectural discussions — your best thinking, scattered across session files on your machine.
+If you use Claude Code, Codex, Gemini CLI, Antigravity CLI, or any AI coding agent daily, you've accumulated hundreds of sessions. Each one contains decisions, debugging breakthroughs, architectural discussions — your best thinking, scattered across session files on your machine.
Spool makes all of that searchable.
diff --git a/packages/landing/pages/docs/guides/data-sources.md b/packages/landing/pages/docs/guides/data-sources.md
index 0d46ef06..e2036d8c 100644
--- a/packages/landing/pages/docs/guides/data-sources.md
+++ b/packages/landing/pages/docs/guides/data-sources.md
@@ -14,6 +14,7 @@ Spool indexes your AI agent sessions automatically. Each source is watched in re
| Codex CLI | `~/.codex/sessions/` |
| Codex CLI (profiles) | `~/.codex-profiles/*/sessions/` |
| Gemini CLI | `~/.gemini/tmp/*/chats/` |
+| Antigravity CLI | `~/.gemini/antigravity-cli/brain/` |
| OpenCode | `~/.local/share/opencode/opencode.db` |
## Platform data (Twitter, GitHub, Reddit, etc.)
diff --git a/packages/landing/pages/docs/installation.md b/packages/landing/pages/docs/installation.md
index c4d18948..053ebce8 100644
--- a/packages/landing/pages/docs/installation.md
+++ b/packages/landing/pages/docs/installation.md
@@ -20,7 +20,7 @@ This downloads the latest `.dmg` from GitHub Releases, mounts it, and copies `Sp
## Verify installation
-After installation, launch Spool from `/Applications` or Spotlight. The app will start indexing your Claude Code, Codex, Gemini CLI, and OpenCode sessions automatically.
+After installation, launch Spool from `/Applications` or Spotlight. The app will start indexing your Claude Code, Codex, Gemini CLI, Antigravity CLI, and OpenCode sessions automatically.
## Optional: install the CLI
diff --git a/packages/landing/pages/docs/quick-start.md b/packages/landing/pages/docs/quick-start.md
index 98e4c65a..2e758f1f 100644
--- a/packages/landing/pages/docs/quick-start.md
+++ b/packages/landing/pages/docs/quick-start.md
@@ -7,11 +7,11 @@ After [installing Spool](/docs/installation), you're a few clicks away from a br
## 1. Launch Spool
-Open Spool from your Applications folder. It starts indexing your Claude Code, Codex CLI, Gemini CLI, and OpenCode sessions automatically — new sessions become visible the moment they're written.
+Open Spool from your Applications folder. It starts indexing your Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, and OpenCode sessions automatically — new sessions become visible the moment they're written.
## 2. Browse your library
-The left sidebar lists **projects** — derived from the working directories your agents ran in, so a project called `api-core` collects every Claude / Codex / Gemini session you ever opened from that repo. Click a project to see its sessions in the main pane.
+The left sidebar lists **projects** — derived from the working directories your agents ran in, so a project called `api-core` collects every Claude / Codex / Gemini / Antigravity session you ever opened from that repo. Click a project to see its sessions in the main pane.
The Library Home (the default main-pane view) shows your most recent sessions across **all** projects, bucketed by date.
diff --git a/packages/landing/pages/docs/reference/configuration.md b/packages/landing/pages/docs/reference/configuration.md
index 83763955..f0aa5b41 100644
--- a/packages/landing/pages/docs/reference/configuration.md
+++ b/packages/landing/pages/docs/reference/configuration.md
@@ -24,6 +24,7 @@ Spool watches the following directories for real-time session indexing. These pa
| Codex CLI | `~/.codex/sessions/` |
| Codex CLI (profiles) | `~/.codex-profiles/*/sessions/` |
| Gemini CLI | `~/.gemini/tmp/*/chats/` |
+| Antigravity CLI | `~/.gemini/antigravity-cli/brain/` |
| OpenCode | `~/.local/share/opencode/opencode.db` |
New sessions become searchable the moment they're written.
diff --git a/packages/landing/pages/index.server.ts b/packages/landing/pages/index.server.ts
index 85b8d985..611cc725 100644
--- a/packages/landing/pages/index.server.ts
+++ b/packages/landing/pages/index.server.ts
@@ -38,7 +38,7 @@ export const head = defineHead(() => ({
"@type": "SoftwareApplication",
name: "Spool",
description:
- "Your local AI session library. Browse, pin, and search every Claude Code, Codex, Gemini, and OpenCode session you've ever had — entirely on your machine. Your AI agents can query it too via the /spool skill.",
+ "Your local AI session library. Browse, pin, and search every Claude Code, Codex, Gemini, Antigravity, and OpenCode session you've ever had — entirely on your machine. Your AI agents can query it too via the /spool skill.",
url: "https://spool.pro",
applicationCategory: "DeveloperApplication",
operatingSystem: "macOS",
diff --git a/packages/redact/src/detectors.ts b/packages/redact/src/detectors.ts
index 5c9e3c7c..97e79a4b 100644
--- a/packages/redact/src/detectors.ts
+++ b/packages/redact/src/detectors.ts
@@ -1,7 +1,7 @@
// Regex detection pipeline for @spool-lab/redact.
//
// Primary scenario: Spool sessions captured from local coding agents
-// (Claude Code, codex, gemini) — terminal output, tool results, files
+// (Claude Code, codex, gemini, antigravity) — terminal output, tool results, files
// the agent read, error logs. The patterns favour structured leaks
// that *appear in this kind of content*: a stray `cat ~/.aws/creds`
// line, `gh auth status` dumping a token, a `kubectl config view`
diff --git a/packages/share-kit/src/lib/types.ts b/packages/share-kit/src/lib/types.ts
index 2f15ee43..432a0183 100644
--- a/packages/share-kit/src/lib/types.ts
+++ b/packages/share-kit/src/lib/types.ts
@@ -16,7 +16,7 @@ export type Platform = 'ChatGPT' | 'Claude' | 'Gemini'
* claude.ai / gemini.google.com). The shared URL is the user's
* own published artifact — public-by-design.
* - `agent-session`: user opened a local `.spool` session captured
- * from a coding agent (Claude Code, codex, gemini-cli). The
+ * from a coding agent (Claude Code, codex, gemini-cli, antigravity-cli). The
* transcript was never public — agent transcripts can contain
* secrets the agent read off disk and that the user never
* intended to publish, which is why the Privacy panel exists.
diff --git a/skills/spool/SKILL.md b/skills/spool/SKILL.md
index 1137df8e..10ba1ab3 100644
--- a/skills/spool/SKILL.md
+++ b/skills/spool/SKILL.md
@@ -1,10 +1,10 @@
---
name: spool
-description: Search your local Claude Code, Codex CLI, Gemini CLI, and OpenCode session history
+description: Search your local Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, and OpenCode session history
allowed-tools: Bash
---
-Search your thinking with spool — a local search engine over your Claude Code, Codex CLI, Gemini CLI, and OpenCode sessions.
+Search your thinking with spool — a local search engine over your Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, and OpenCode sessions.
## Steps
@@ -30,7 +30,7 @@ where `$ARGS` is everything the user passed to `/spool`.
For each result in the JSON array, show:
- **Session title** and date (`startedAt`)
-- **Source** (claude, codex, gemini, or opencode) and **project** path
+- **Source** (claude, codex, gemini, antigravity, or opencode) and **project** path
- The **snippet** with highlighted terms (strip `` / ` ` tags for plain display)
- A note of the session UUID