@oddessentials/agent-guild 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
  <a href="https://www.youtube.com/watch?v=ziT62WtXQ1M"><img src="https://img.shields.io/badge/trailer-watch%20on%20YouTube-e5534b?logo=youtube&logoColor=white" alt="Watch the Agent Guild trailer on YouTube"></a>
12
12
  </p>
13
13
 
14
- **Agent Guild** runs Claude Code, Codex CLI, Gemini CLI, Grok Build and your
14
+ **Agent Guild** runs Claude Code, Codex CLI, Antigravity CLI, Grok Build and your
15
15
  own shell side by side, in real terminals, from one local web page. Each
16
16
  session is a card that shows what the tool is doing, which model it runs and
17
17
  which helper agents it has summoned. Close the page whenever you like; the
@@ -41,9 +41,10 @@ compiler and no WSL are needed.
41
41
 
42
42
  ## Features
43
43
 
44
- **Every major coding CLI, one place.** Claude Code, Codex CLI, Gemini CLI,
44
+ **Every major coding CLI, one place.** Claude Code, Codex CLI, Antigravity CLI,
45
45
  Grok Build and a plain shell each get a card. A tool that is missing shows
46
- **Install**, which runs the install in a session you can watch. An installed
46
+ **Install**, which runs the install in a session you can watch, or the
47
+ vendor's install command for a tool not published on npm. An installed
47
48
  tool shows its version and offers **Update** when a newer one is out, using
48
49
  the same installer that put it there: npm, Homebrew, WinGet or the vendor's
49
50
  own.
@@ -64,7 +65,7 @@ with every hour it runs.
64
65
  one and switch with a chip on the card. Each account keeps its own sign-in,
65
66
  usage meters and sessions. See [Accounts](docs/configuration.md#accounts).
66
67
 
67
- **See what is left of your limits.** Claude Code, Codex CLI and Gemini CLI
68
+ **See what is left of your limits.** Claude Code and Codex CLI
68
69
  cards show a meter for each rate-limit window, such as 5-hour and 7-day, with
69
70
  the time until it resets. They also show your plan, Claude's extra-usage
70
71
  spend and Codex's credit balance when the account has them. Any other tool
@@ -143,9 +144,10 @@ The full reference, including every field and environment variable, is in
143
144
  Agents are reported by the coding tool's hooks, not guessed from its output.
144
145
  Each helper agent appears on the card while it runs. Agent Guild gives every
145
146
  Claude Code and Codex CLI session its reporting hooks for that session only,
146
- without changing the tool's own settings. Gemini CLI has no such option: turn
147
- on **Agent reporting** on its card once, which links an Agent Guild extension
148
- into Gemini. Grok Build cannot take hooks for one session yet; add the hooks
147
+ without changing the tool's own settings. Antigravity CLI has no such option:
148
+ turn on **Agent reporting** on its card once, which installs an Agent Guild
149
+ plugin into Antigravity. It reports the model and the conversation, but no
150
+ helper agents or shell commands. Grok Build cannot take hooks for one session yet; add the hooks
149
151
  from [examples/grok-hooks.json](examples/grok-hooks.json) to see its agents.
150
152
  When a tool's hooks do not run, because hooks are turned off, restricted by
151
153
  an administrator or not trusted for the folder, the card says so.
@@ -172,8 +174,7 @@ Any other tool or script can report agents and its model too. See
172
174
  own `~/.ssh` and Git configuration are not read or changed.
173
175
  * Usage meters are fetched by the manager with each tool's own sign-in. The
174
176
  page only receives percentages. On macOS the first lookup may ask for
175
- keychain access to the "Claude Code-credentials" and "gemini-cli-oauth"
176
- items; choose Always Allow.
177
+ keychain access to the "Claude Code-credentials" item; choose Always Allow.
177
178
 
178
179
  The manager makes these outbound requests, and none of them carry your code
179
180
  or prompts:
@@ -181,7 +182,7 @@ or prompts:
181
182
  | To | For | How often |
182
183
  | --- | --- | --- |
183
184
  | npm registry | Tool and Agent Guild version checks | About hourly; `AGENT_GUILD_NO_UPDATE_CHECK=1` turns them off |
184
- | Anthropic, OpenAI and Google usage endpoints | Usage meters, with the tool's own sign-in | Every minute while the page is open |
185
+ | Anthropic and OpenAI usage endpoints | Usage meters, with the tool's own sign-in | Every minute while the page is open |
185
186
  | OpenRouter's public model list | Benchmarks | Every 6 hours |
186
187
  | Public news feeds, Hacker News, arXiv and GitHub | The newsfeed | Every 30 minutes while the page is open |
187
188
  | GitHub's releases API | What's new | Hourly |
@@ -224,12 +225,9 @@ npm test
224
225
  ## Current limits
225
226
 
226
227
  * Sessions end when the manager stops or the computer restarts.
227
- * Grok Build has no usage meter.
228
- * Gemini CLI usage meters read its sign-in where Gemini CLI keeps it:
229
- `oauth_creds.json`, or with `GEMINI_FORCE_ENCRYPTED_FILE_STORAGE=true` the
230
- OS keychain (macOS, or Linux with `secret-tool`) or its encrypted
231
- credentials file. A sign-in kept in the Windows Credential Manager cannot
232
- be read.
228
+ * Antigravity CLI and Grok Build have no usage meter.
229
+ * Antigravity CLI has one account per computer user: it keeps its sign-in in
230
+ the system keychain and has no setting for another home folder.
233
231
 
234
232
  ## License
235
233
 
@@ -10,40 +10,31 @@
10
10
  // [--detail TEXT] [--kind KIND] [--remove]
11
11
  // agent-guild-report --model NAME [--display-name TEXT]
12
12
  // agent-guild-report --session ID (the tool's own session id, for resuming it later)
13
- // agent-guild-report --hook (reads a hook event as JSON on stdin:
14
- // Claude Code, Codex CLI, Gemini CLI, Grok Build)
13
+ // agent-guild-report --hook [--event NAME] (reads a hook event as JSON on stdin:
14
+ // Claude Code, Codex CLI, Antigravity CLI, Grok Build;
15
+ // --event names one that does not name itself)
15
16
  // agent-guild-report --claude-statusline [--passthrough]
16
17
  // (reads Claude Code status line JSON on stdin; prints a
17
18
  // status line, or the JSON itself with --passthrough)
18
19
 
19
20
  import fs from 'node:fs';
20
21
  import { createRequire } from 'node:module';
21
- import { hookToReports, claudeStatuslineToReport, formatStatusLine } from '../src/report/hooks.mjs';
22
+ import { hookToReports, claudeStatuslineToReport, formatStatusLine, antigravityUserConversation } from '../src/report/hooks.mjs';
22
23
 
23
24
  // Importing node:http as an ES module costs a hook about 50 ms more than requiring it.
24
25
  const http = createRequire(import.meta.url)('node:http');
25
26
  const env = process.env;
26
27
 
27
- function reportToken() {
28
- if (env.AGENT_GUILD_REPORT_TOKEN) return env.AGENT_GUILD_REPORT_TOKEN;
29
- if (!env.AGENT_GUILD_REPORT_FILE) return null;
30
- try {
31
- return fs.readFileSync(env.AGENT_GUILD_REPORT_FILE, 'utf8').trim() || null;
32
- } catch {
33
- return null;
34
- }
35
- }
36
-
37
- const token = env.AGENT_GUILD_URL && env.AGENT_GUILD_SESSION_ID ? reportToken() : null;
28
+ const token = env.AGENT_GUILD_URL && env.AGENT_GUILD_SESSION_ID ? env.AGENT_GUILD_REPORT_TOKEN || null : null;
38
29
  const inSession = Boolean(token);
39
30
  if (!inSession && env.AGENT_GUILD_SESSION_ID) {
40
- console.error('agent-guild-report: the session report token is missing; the tool may have removed AGENT_GUILD_REPORT_TOKEN and AGENT_GUILD_REPORT_FILE from the environment, or the session has ended');
31
+ console.error('agent-guild-report: the session report token is missing; the tool may have removed AGENT_GUILD_REPORT_TOKEN from the environment');
41
32
  }
42
33
 
43
34
  const USAGE = `Usage: agent-guild-report <agent-id> [--name N] [--status working|waiting|idle|done] [--detail TEXT] [--kind KIND] [--remove]
44
35
  agent-guild-report --model NAME [--display-name TEXT]
45
36
  agent-guild-report --session ID (the tool's own session id)
46
- agent-guild-report --hook (reads a coding tool's hook event JSON from stdin)
37
+ agent-guild-report --hook [--event NAME] (reads a coding tool's hook event JSON from stdin)
47
38
  agent-guild-report --claude-statusline [--passthrough] (reads Claude Code status line JSON from stdin)`;
48
39
 
49
40
  function parseArgs(argv) {
@@ -105,6 +96,32 @@ function send(report) {
105
96
  /** A hook must never break the coding tool, so report failures quietly. */
106
97
  const sendQuietly = (report) => send(report).catch((err) => console.error(`agent-guild-report: ${err.message}`));
107
98
 
99
+ const MAX_FIRST_LINE = 1024 * 1024;
100
+
101
+ /** The first JSON record of a JSONL file, or null. */
102
+ function firstRecord(file) {
103
+ if (typeof file !== 'string') return null;
104
+ let fd;
105
+ try {
106
+ fd = fs.openSync(file, 'r');
107
+ const chunks = [];
108
+ const buffer = Buffer.alloc(64 * 1024);
109
+ for (let total = 0; total < MAX_FIRST_LINE;) {
110
+ const n = fs.readSync(fd, buffer, 0, buffer.length, null);
111
+ if (n === 0) break;
112
+ const end = buffer.subarray(0, n).indexOf(10);
113
+ chunks.push(Buffer.from(buffer.subarray(0, end === -1 ? n : end)));
114
+ if (end !== -1) break;
115
+ total += n;
116
+ }
117
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
118
+ } catch {
119
+ return null;
120
+ } finally {
121
+ if (fd !== undefined) fs.closeSync(fd);
122
+ }
123
+ }
124
+
108
125
  function parseJson(raw) {
109
126
  try { return JSON.parse(raw); } catch { return null; }
110
127
  }
@@ -117,7 +134,10 @@ async function main() {
117
134
  }
118
135
  if (args.hook) {
119
136
  const input = parseJson(await readStdin());
120
- if (!inSession || !input) return;
137
+ if (!inSession || !input || typeof input !== 'object') return;
138
+ if (args.event && input.hook_event_name === undefined) input.hook_event_name = args.event;
139
+ // Antigravity CLI runs the hook in its sub-agents too, each a conversation of its own.
140
+ if (typeof input.conversationId === 'string' && !antigravityUserConversation(firstRecord(input.transcriptPath))) return;
121
141
  for (const report of hookToReports(input)) await sendQuietly(report);
122
142
  return;
123
143
  }
@@ -76,28 +76,28 @@
76
76
  {
77
77
  "id": "google",
78
78
  "vendor": "Google",
79
- "tool": "Gemini CLI",
80
- "command": "gemini",
81
- "package": "@google/gemini-cli",
79
+ "tool": "Antigravity CLI",
80
+ "command": "agy",
82
81
  "versionArgs": ["--version"],
83
- "usage": "gemini",
84
- "history": "gemini",
85
- "reporting": "gemini",
86
- "homeVar": "GEMINI_CLI_HOME",
87
- "accountEnv": { "GEMINI_FORCE_FILE_STORAGE": "true" },
88
- "hooks": { "path": ".gemini/settings.json", "example": "gemini-settings.json" },
82
+ "history": "antigravity",
83
+ "reporting": "antigravity",
89
84
  "modelPattern": "\\bgemini-\\d[a-z0-9.-]*",
90
85
  "args": [],
91
- "resumeArgs": ["--resume", "{id}"],
86
+ "resumeArgs": ["--conversation", "{id}"],
92
87
  "color": "#4285F4",
93
88
  "monogram": "G",
94
- "install": "npm install -g @google/gemini-cli",
89
+ "install": "curl -fsSL https://antigravity.google/cli/install.sh | bash",
95
90
  "channels": {
96
- "brew": { "names": ["gemini-cli"] }
91
+ "native": { "paths": ["~/.local/bin/agy"], "update": ["update"], "uninstall": "rm -f ~/.local/bin/agy" }
97
92
  },
98
- "docs": "https://github.com/google-gemini/gemini-cli",
99
- "usageUrl": "https://aistudio.google.com/usage",
100
- "billingUrl": "https://one.google.com/settings"
93
+ "docs": "https://antigravity.google/docs/cli/install",
94
+ "billingUrl": "https://one.google.com/settings",
95
+ "win32": {
96
+ "install": "irm https://antigravity.google/cli/install.ps1 | iex",
97
+ "channels": {
98
+ "native": { "paths": ["~/AppData/Local/agy/bin"], "update": ["update"], "uninstall": "Remove-Item -Path \"$env:LOCALAPPDATA\\agy\" -Recurse -Force" }
99
+ }
100
+ }
101
101
  },
102
102
  {
103
103
  "id": "xai",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddessentials/agent-guild",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Launch and watch AI coding-assistant terminal sessions from one local web page. A bundled session manager owns the terminals so the page can close and reconnect.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,7 +48,6 @@ export const paths = {
48
48
  /** Launchers for agent-guild-report, put first on every session's PATH. */
49
49
  get shims() { return path.join(dataDir(), 'bin'); },
50
50
  get reporting() { return path.join(dataDir(), 'reporting'); },
51
- get reportTokens() { return path.join(dataDir(), 'report-tokens'); },
52
51
  };
53
52
 
54
53
  function writePrivate(file, contents) {
@@ -122,6 +122,13 @@ function wingetOwner({ realPath, id, platform }) {
122
122
  return folder.startsWith(`${id.toLowerCase()}_`) ? { id } : null;
123
123
  }
124
124
 
125
+ /** Whether `<tool> <update args> --help` shows the update command. Go programs exit 2 after their help; a failure that reports an error does not count. */
126
+ export function updateHelpAccepted({ stdout = '', stderr = '' }, args, { failed = false } = {}) {
127
+ const text = `${stdout}\n${stderr}`;
128
+ if (failed && /\b(?:error|unknown|unrecogni[sz]ed|invalid)\b/i.test(text)) return false;
129
+ return helpDescribes(text, args);
130
+ }
131
+
125
132
  export function helpDescribes(text, args) {
126
133
  const words = args.map((a) => a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
127
134
  const named = new RegExp(`(?<![\\w-])${words.join('(?![\\w-]).*(?<![\\w-])')}(?![\\w-])`, 'i');
@@ -63,11 +63,11 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
63
63
  let api;
64
64
  const selfUpdate = new SelfUpdate({ pkg: PACKAGE_NAME, version, packageFile, registry });
65
65
  const github = new GitHub({ dir: paths.github, registry, ...githubOptions });
66
- const sessionHooks = new SessionHooks({ registry, dir: paths.reporting, version, shimDir });
67
- registry.reportingEnabled = (provider, account) => sessionHooks.enabled(provider, account);
66
+ const sessionHooks = new SessionHooks({ registry, dir: paths.reporting, version });
67
+ registry.reportingEnabled = (provider) => sessionHooks.enabled(provider);
68
68
  sessionHooks.warm();
69
69
  const manager = new SessionManager({
70
- registry, baseEnv, getApiUrl: () => api.url, sessionDefaults, shimDir, selfUpdate, github, sessionHooks, reportTokenDir: paths.reportTokens,
70
+ registry, baseEnv, getApiUrl: () => api.url, sessionDefaults, shimDir, selfUpdate, github, sessionHooks,
71
71
  });
72
72
  const usage = new UsageMonitor({ registry, env: baseEnv });
73
73
  const history = new SessionHistory({ registry, env: baseEnv });
@@ -147,7 +147,6 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
147
147
  throw err;
148
148
  }
149
149
 
150
- manager.sweepReportTokens();
151
150
  writeRuntimeFile({
152
151
  pid: process.pid,
153
152
  host,
@@ -30,7 +30,7 @@ export const FEEDS = [
30
30
  { id: 'ollama-blog', name: 'Ollama', category: 'news', url: 'https://ollama.com/blog/rss.xml' },
31
31
  latestRelease('claude-code', 'Claude Code', 'anthropics/claude-code', 'anthropic'),
32
32
  latestRelease('codex-cli', 'Codex CLI', 'openai/codex', 'openai'),
33
- latestRelease('gemini-cli', 'Gemini CLI', 'google-gemini/gemini-cli', 'google'),
33
+ latestRelease('antigravity-cli', 'Antigravity CLI', 'google-antigravity/antigravity-cli', 'google'),
34
34
  { id: 'ollama', name: 'Ollama', category: 'releases', url: 'https://github.com/ollama/ollama/releases.atom' },
35
35
  { id: 'llama-cpp', name: 'llama.cpp', category: 'releases', url: 'https://github.com/ggml-org/llama.cpp/releases.atom' },
36
36
  { id: 'transformers', name: 'Transformers', category: 'releases', url: 'https://github.com/huggingface/transformers/releases.atom' },
@@ -11,7 +11,7 @@ import { EventEmitter } from 'node:events';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { resolveCommand, resolveAllCommands, pathKey, buildSpawnSpec, runSpec } from './command-resolver.mjs';
13
13
  import { compareVersions, probeVersion, fetchManifest, latestVersion, DEFAULT_NPM_REGISTRY } from './versions.mjs';
14
- import { CHANNEL_LABELS, classifyInstall, expandHome, formatCommand, helpDescribes, knownLaunchers, listInstallations, platformDependency } from './install-channels.mjs';
14
+ import { CHANNEL_LABELS, classifyInstall, expandHome, formatCommand, knownLaunchers, listInstallations, platformDependency, updateHelpAccepted } from './install-channels.mjs';
15
15
  import { weavePaths } from './shell-env.mjs';
16
16
  import { paths } from './config.mjs';
17
17
 
@@ -19,7 +19,7 @@ const here = path.dirname(fileURLToPath(import.meta.url));
19
19
  export const DEFAULTS_FILE = path.resolve(here, '../../config/providers.default.json');
20
20
  const ID_RE = /^[a-z0-9][a-z0-9_-]{0,31}$/;
21
21
  const PLATFORM_KEYS = ['win32', 'darwin', 'linux'];
22
- const REPORTING_MODES = new Set(['claude', 'codex', 'gemini', 'grok']);
22
+ const REPORTING_MODES = new Set(['claude', 'codex', 'antigravity', 'grok']);
23
23
  const VERSION_TTL_MS = 60 * 60 * 1000;
24
24
  const FAILED_PROBE_TTL_MS = 5 * 60 * 1000;
25
25
  const PATH_REFRESH_MS = 60 * 1000;
@@ -52,18 +52,18 @@ function normalizeEnv(env) {
52
52
  return out;
53
53
  }
54
54
 
55
- /** "claude", "codex", "gemini", a { command, args } that prints usage JSON, or null. */
55
+ /** "claude", "codex", a { command, args } that prints usage JSON, or null. */
56
56
  function normalizeUsage(usage) {
57
- if (usage === 'claude' || usage === 'codex' || usage === 'gemini') return usage;
57
+ if (usage === 'claude' || usage === 'codex') return usage;
58
58
  if (usage && typeof usage === 'object' && typeof usage.command === 'string' && usage.command) {
59
59
  return { command: usage.command, args: Array.isArray(usage.args) ? usage.args.map(String) : [] };
60
60
  }
61
61
  return null;
62
62
  }
63
63
 
64
- /** "claude", "codex", "gemini", "grok", a { command, args } that prints past sessions as JSON, or null. */
64
+ /** "claude", "codex", "antigravity", "grok", a { command, args } that prints past sessions as JSON, or null. */
65
65
  function normalizeHistory(history) {
66
- if (['claude', 'codex', 'gemini', 'grok'].includes(history)) return history;
66
+ if (['claude', 'codex', 'antigravity', 'grok'].includes(history)) return history;
67
67
  if (history && typeof history === 'object' && typeof history.command === 'string' && history.command) {
68
68
  return { command: history.command, args: Array.isArray(history.args) ? history.args.map(String) : [] };
69
69
  }
@@ -349,7 +349,7 @@ export class ProviderRegistry extends EventEmitter {
349
349
  if (force || entry.probePath !== found || entry.probeMtime !== mtime || now - entry.probeAt > ttl) {
350
350
  const spec = buildSpawnSpec(channel.update.file, [...channel.update.args, '--help'], this.env, this.platform);
351
351
  const ok = await runSpec(spec, { env: { ...this.env, ...provider.env } })
352
- .then(({ stdout, stderr }) => helpDescribes(`${stdout}\n${stderr}`, channel.update.args), () => false);
352
+ .then((out) => updateHelpAccepted(out, channel.update.args), (err) => updateHelpAccepted(err, channel.update.args, { failed: true }));
353
353
  changed ||= ok !== entry.probeOk;
354
354
  Object.assign(entry, { probePath: found, probeMtime: mtime, probeAt: now, probeOk: ok });
355
355
  }
@@ -627,10 +627,8 @@ export class ProviderRegistry extends EventEmitter {
627
627
  usageSource: provider.usage === null ? null : typeof provider.usage === 'string' ? provider.usage : 'command',
628
628
  historySource: provider.history === null ? null : typeof provider.history === 'string' ? provider.history : 'command',
629
629
  reporting: provider.reporting,
630
- accounts: provider.accounts.map((account) => {
631
- const reportingEnabled = this.reportingEnabled?.(provider, this.accountFor(provider, account)) ?? null;
632
- return { id: account.id, label: account.label, ...(reportingEnabled === null ? {} : { reportingEnabled }) };
633
- }),
630
+ reportingEnabled: this.reportingEnabled?.(provider) ?? null,
631
+ accounts: provider.accounts.map((account) => ({ id: account.id, label: account.label })),
634
632
  modelPattern: provider.modelPattern,
635
633
  color: provider.color,
636
634
  monogram: provider.monogram,
@@ -2,8 +2,8 @@
2
2
  // session, without `npm link` or a global install.
3
3
  //
4
4
  // The coding tools run their hook commands through a shell (sh or Git Bash
5
- // for Claude Code, a login shell for Codex CLI, bash or PowerShell for
6
- // Gemini CLI, sh or PowerShell for Grok Build) and look the command up on
5
+ // for Claude Code, a login shell for Codex CLI, sh or cmd.exe for
6
+ // Antigravity CLI, sh or PowerShell for Grok Build) and look the command up on
7
7
  // the PATH they inherit from the terminal. The manager writes these shims
8
8
  // into its data folder at every start and puts that folder first on each
9
9
  // session's PATH. They run the reporter with the manager's own Node.js, and
@@ -12,8 +12,8 @@
12
12
  //
13
13
  // On Windows there is deliberately no .ps1 shim: PowerShell prefers a .ps1
14
14
  // over the PATHEXT extensions, and the default Restricted execution policy
15
- // refuses to run it. Gemini CLI and Grok Build run hooks in PowerShell without
16
- // bypassing that policy, so they would fail; with only a .cmd present,
15
+ // refuses to run it. Grok Build runs hooks in PowerShell without
16
+ // bypassing that policy, so it would fail; with only a .cmd present,
17
17
  // PowerShell falls through to it and cmd.exe runs it with no policy involved.
18
18
  // cmd.exe reads a batch file in the OEM code page, so no path may be written
19
19
  // into the .cmd: the script's path lives in a loader module next to it, as
@@ -280,8 +280,7 @@ export function createManagerServer({
280
280
  if (!provider) throw new HttpError(404, `unknown provider "${reportingMatch[1]}"`, 'unknown_provider');
281
281
  const body = await readJsonBody(req);
282
282
  if (typeof body.enabled !== 'boolean') throw new HttpError(400, 'enabled must be true or false', 'bad_request');
283
- const account = registry.account(provider, body.account ?? null);
284
- await manager.sessionHooks.setEnabled(provider, account, body.enabled);
283
+ await manager.sessionHooks.setEnabled(provider, body.enabled);
285
284
  registry.emit('updated');
286
285
  return sendJson(res, 200, { provider: registry.describe(provider) });
287
286
  }
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  import zlib from 'node:zlib';
9
9
  import { resolveCommand, buildSpawnSpec, runSpec } from './command-resolver.mjs';
10
10
  import { toIso } from './usage.mjs';
11
+ import { antigravityUserConversation } from '../report/hooks.mjs';
11
12
 
12
13
  export const HISTORY_TTL_MS = 5 * 1000;
13
14
  export const DEFAULT_LIMIT = 100;
@@ -255,88 +256,45 @@ export async function listCodexSessions(dir, memo = new FileMemo()) {
255
256
  return dedupe(entries);
256
257
  }
257
258
 
258
- // ---- Gemini CLI -----------------------------------------------------------
259
+ // ---- Antigravity CLI ------------------------------------------------------
259
260
 
260
- export function geminiDir(env = process.env) {
261
- return path.join(env.GEMINI_CLI_HOME || os.homedir(), '.gemini');
261
+ export function antigravityDir(env = process.env, platform = process.platform) {
262
+ return path.join((platform === 'win32' ? env.USERPROFILE : env.HOME) || os.homedir(), '.gemini', 'antigravity-cli');
262
263
  }
263
264
 
264
- function geminiPrompt(record) {
265
- if (!record || typeof record !== 'object') return null;
266
- if (record.type === 'user' && record.id !== undefined) {
267
- const text = contentText(record.content);
268
- return /^[/?]/.test(text.trim()) ? null : promptTitle(text);
269
- }
270
- const checkpoint = record.$set?.messages;
271
- if (Array.isArray(checkpoint)) {
272
- for (const message of checkpoint) {
273
- const title = geminiPrompt(message);
274
- if (title) return title;
275
- }
276
- }
277
- return null;
278
- }
265
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
279
266
 
280
- function geminiEntry(text, stat, cwd, legacy) {
281
- let meta;
282
- let records;
283
- if (legacy) {
284
- try { meta = JSON.parse(text); } catch { return null; }
285
- records = Array.isArray(meta?.messages) ? meta.messages : [];
286
- } else {
287
- records = parseLines(text);
288
- meta = records.shift();
289
- }
290
- if (!meta || typeof meta !== 'object' || typeof meta.sessionId !== 'string') return null;
291
- if (meta.kind === 'subagent') return null;
292
- let title = typeof meta.summary === 'string' && meta.summary.trim() ? meta.summary : null;
293
- for (const record of records) {
294
- if (title) break;
295
- title = geminiPrompt(record);
296
- }
297
- return cleanEntry({ id: meta.sessionId, title, cwd, startedAt: meta.startTime, updatedAt: stat.mtime });
267
+ function antigravityEntry(id, text, stat) {
268
+ const [first] = parseLines(text ?? '');
269
+ if (!antigravityUserConversation(first) || typeof first.content !== 'string') return null;
270
+ const request = first.content.match(/<USER_REQUEST>([\s\S]*?)<\/USER_REQUEST>/)?.[1];
271
+ return cleanEntry({ id, title: promptTitle(request), startedAt: first.created_at, updatedAt: stat.mtime });
298
272
  }
299
273
 
300
- /** The project folder a Gemini CLI temp folder stands for: its `.project_root` marker, else the registry in `projects.json`. */
301
- async function geminiProjectRoots(root, folders) {
302
- const roots = new Map();
303
- let registry = null;
304
- for (const folder of folders) {
305
- const marker = (await readText(path.join(root, folder, '.project_root')))?.trim();
306
- if (marker) {
307
- roots.set(folder, marker);
308
- continue;
309
- }
310
- if (registry === null) {
311
- let projects = {};
312
- try { projects = JSON.parse(await readText(path.join(path.dirname(root), 'projects.json')))?.projects ?? {}; } catch { /* no registry */ }
313
- registry = new Map(Object.entries(projects).map(([dir, slug]) => [slug, dir]));
314
- }
315
- roots.set(folder, registry.get(folder) ?? null);
316
- }
317
- return roots;
274
+ /** The folder of each folder's latest conversation; the CLI keeps no folder for the others. */
275
+ async function antigravityFolders(dir) {
276
+ const folders = new Map();
277
+ let latest = null;
278
+ try { latest = JSON.parse(await readText(path.join(dir, 'cache', 'last_conversations.json'))); } catch { /* none yet */ }
279
+ if (!latest || typeof latest !== 'object') return folders;
280
+ for (const [folder, id] of Object.entries(latest)) if (typeof id === 'string') folders.set(id, folder);
281
+ return folders;
318
282
  }
319
283
 
320
- /** Chats under `<dir>/tmp/<project>/chats/session-*.jsonl` (older versions: `.json`). Sub-agent chats sit in sub-folders. */
321
- export async function listGeminiSessions(dir, memo = new FileMemo()) {
322
- const root = path.join(dir, 'tmp');
284
+ /** Conversations under `<dir>/brain/<id>/.system_generated/logs/transcript.jsonl`. */
285
+ export async function listAntigravitySessions(dir, memo = new FileMemo()) {
286
+ const root = path.join(dir, 'brain');
287
+ const folders = await antigravityFolders(dir);
323
288
  const entries = [];
324
289
  const seen = new Set();
325
- const folders = (await readDir(root)).filter((item) => item.isDirectory()).map((item) => item.name);
326
- const roots = await geminiProjectRoots(root, folders);
327
- for (const folder of folders) {
328
- const chats = path.join(root, folder, 'chats');
329
- for (const item of await readDir(chats)) {
330
- const legacy = item.name.endsWith('.json');
331
- if (!item.isFile() || !item.name.startsWith('session-') || !(legacy || item.name.endsWith('.jsonl'))) continue;
332
- const file = path.join(chats, item.name);
333
- const stat = await statFile(file);
334
- if (!stat) continue;
335
- seen.add(file);
336
- const cwd = roots.get(folder);
337
- const entry = await memo.entry(file, stat, async () => geminiEntry(legacy ? await readText(file) : await readHead(file), stat, cwd, legacy));
338
- if (entry) entries.push(entry);
339
- }
290
+ for (const item of await readDir(root)) {
291
+ if (!item.isDirectory() || !UUID_RE.test(item.name)) continue;
292
+ const file = path.join(root, item.name, '.system_generated', 'logs', 'transcript.jsonl');
293
+ const stat = await statFile(file);
294
+ if (!stat) continue;
295
+ seen.add(file);
296
+ const entry = await memo.entry(file, stat, async () => antigravityEntry(item.name, await readHead(file), stat));
297
+ if (entry) entries.push({ ...entry, cwd: folders.get(entry.id) ?? null });
340
298
  }
341
299
  memo.prune(root, seen);
342
300
  return dedupe(entries);
@@ -447,7 +405,7 @@ export class SessionHistory {
447
405
  this.env = env;
448
406
  this.platform = platform;
449
407
  this.ttlMs = ttlMs;
450
- this.readers = { claude: listClaudeSessions, codex: listCodexSessions, gemini: listGeminiSessions, grok: listGrokSessions, ...readers };
408
+ this.readers = { claude: listClaudeSessions, codex: listCodexSessions, antigravity: listAntigravitySessions, grok: listGrokSessions, ...readers };
451
409
  this.memo = new FileMemo();
452
410
  this.cache = new Map();
453
411
  }
@@ -455,7 +413,7 @@ export class SessionHistory {
455
413
  static sourceDir(source, env) {
456
414
  if (source === 'claude') return claudeConfigDir(env);
457
415
  if (source === 'codex') return codexHome(env);
458
- if (source === 'gemini') return geminiDir(env);
416
+ if (source === 'antigravity') return antigravityDir(env);
459
417
  if (source === 'grok') return grokHome(env);
460
418
  return null;
461
419
  }