@oddessentials/agent-guild 0.10.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
@@ -140,20 +141,19 @@ The full reference, including every field and environment variable, is in
140
141
 
141
142
  ## Show agents and models
142
143
 
143
- Agents are reported by the coding tool, not guessed from its output. To see
144
- them, add the hooks from the matching file in [examples/](examples/) to
145
- Claude Code, Codex CLI, Gemini CLI or Grok Build. Each helper agent then
146
- appears on the card while it runs, and the card shows the model in use.
147
- Without hooks, the card still names the model when it is given with
148
- `--model` or shown on the tool's screen.
144
+ Agents are reported by the coding tool's hooks, not guessed from its output.
145
+ Each helper agent appears on the card while it runs. Agent Guild gives every
146
+ Claude Code and Codex CLI session its reporting hooks for that session only,
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
151
+ from [examples/grok-hooks.json](examples/grok-hooks.json) to see its agents.
152
+ When a tool's hooks do not run, because hooks are turned off, restricted by
153
+ an administrator or not trusted for the folder, the card says so.
149
154
 
150
- The hooks call `agent-guild-report`, which the manager puts on the PATH of
151
- every session, so nothing else needs installing. Two tools need a one-time
152
- approval:
153
-
154
- * Codex CLI runs no hook until you trust it: choose "Trust all and continue"
155
- when it starts, or run `/hooks`.
156
- * Claude Code runs none until you accept its workspace-trust prompt.
155
+ The card also names the model, from the hooks, from `--model` or from the
156
+ tool's screen.
157
157
 
158
158
  Any other tool or script can report agents and its model too. See
159
159
  [docs/agent-reporting.md](docs/agent-reporting.md).
@@ -174,8 +174,7 @@ Any other tool or script can report agents and its model too. See
174
174
  own `~/.ssh` and Git configuration are not read or changed.
175
175
  * Usage meters are fetched by the manager with each tool's own sign-in. The
176
176
  page only receives percentages. On macOS the first lookup may ask for
177
- keychain access to the "Claude Code-credentials" and "gemini-cli-oauth"
178
- items; choose Always Allow.
177
+ keychain access to the "Claude Code-credentials" item; choose Always Allow.
179
178
 
180
179
  The manager makes these outbound requests, and none of them carry your code
181
180
  or prompts:
@@ -183,7 +182,7 @@ or prompts:
183
182
  | To | For | How often |
184
183
  | --- | --- | --- |
185
184
  | npm registry | Tool and Agent Guild version checks | About hourly; `AGENT_GUILD_NO_UPDATE_CHECK=1` turns them off |
186
- | 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 |
187
186
  | OpenRouter's public model list | Benchmarks | Every 6 hours |
188
187
  | Public news feeds, Hacker News, arXiv and GitHub | The newsfeed | Every 30 minutes while the page is open |
189
188
  | GitHub's releases API | What's new | Hourly |
@@ -226,12 +225,9 @@ npm test
226
225
  ## Current limits
227
226
 
228
227
  * Sessions end when the manager stops or the computer restarts.
229
- * Grok Build has no usage meter.
230
- * Gemini CLI usage meters read its sign-in where Gemini CLI keeps it:
231
- `oauth_creds.json`, or with `GEMINI_FORCE_ENCRYPTED_FILE_STORAGE=true` the
232
- OS keychain (macOS, or Linux with `secret-tool`) or its encrypted
233
- credentials file. A sign-in kept in the Windows Credential Manager cannot
234
- 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.
235
231
 
236
232
  ## License
237
233
 
@@ -10,26 +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
- import { hookToReports, claudeStatuslineToReport, formatStatusLine } from '../src/report/hooks.mjs';
20
+ import fs from 'node:fs';
21
+ import { createRequire } from 'node:module';
22
+ import { hookToReports, claudeStatuslineToReport, formatStatusLine, antigravityUserConversation } from '../src/report/hooks.mjs';
20
23
 
24
+ // Importing node:http as an ES module costs a hook about 50 ms more than requiring it.
25
+ const http = createRequire(import.meta.url)('node:http');
21
26
  const env = process.env;
22
- const inSession = env.AGENT_GUILD_URL && env.AGENT_GUILD_SESSION_ID && env.AGENT_GUILD_REPORT_TOKEN;
23
- // Gemini CLI's optional environment redaction removes every variable whose
24
- // name contains TOKEN before it runs a hook. Say so instead of staying silent.
25
- if (!inSession && env.AGENT_GUILD_SESSION_ID && !env.AGENT_GUILD_REPORT_TOKEN) {
26
- console.error('agent-guild-report: AGENT_GUILD_REPORT_TOKEN is missing from the environment; the tool may be redacting variables named *TOKEN*');
27
+
28
+ const token = env.AGENT_GUILD_URL && env.AGENT_GUILD_SESSION_ID ? env.AGENT_GUILD_REPORT_TOKEN || null : null;
29
+ const inSession = Boolean(token);
30
+ if (!inSession && env.AGENT_GUILD_SESSION_ID) {
31
+ console.error('agent-guild-report: the session report token is missing; the tool may have removed AGENT_GUILD_REPORT_TOKEN from the environment');
27
32
  }
28
33
 
29
34
  const USAGE = `Usage: agent-guild-report <agent-id> [--name N] [--status working|waiting|idle|done] [--detail TEXT] [--kind KIND] [--remove]
30
35
  agent-guild-report --model NAME [--display-name TEXT]
31
36
  agent-guild-report --session ID (the tool's own session id)
32
- 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)
33
38
  agent-guild-report --claude-statusline [--passthrough] (reads Claude Code status line JSON from stdin)`;
34
39
 
35
40
  function parseArgs(argv) {
@@ -61,28 +66,62 @@ function readStdin() {
61
66
  });
62
67
  }
63
68
 
64
- async function send(report) {
65
- const kind = report.agentId !== undefined || report.finishForeground === true ? 'agents'
69
+ function send(report) {
70
+ const kind = report.shell !== undefined ? 'shells'
71
+ : report.agentId !== undefined || report.finishForeground === true ? 'agents'
72
+ : report.hello === true ? 'reporting'
66
73
  : report.toolSessionId !== undefined ? 'tool-session' : 'model';
67
- const url = `${env.AGENT_GUILD_URL}/api/v1/sessions/${env.AGENT_GUILD_SESSION_ID}/${kind}`;
68
- const res = await fetch(url, {
69
- method: 'POST',
70
- headers: {
71
- 'Content-Type': 'application/json',
72
- 'X-Agent-Guild-Report-Token': env.AGENT_GUILD_REPORT_TOKEN,
73
- },
74
- body: JSON.stringify(report),
75
- signal: AbortSignal.timeout(3000),
74
+ const body = JSON.stringify(report);
75
+ return new Promise((resolve, reject) => {
76
+ const req = http.request(`${env.AGENT_GUILD_URL}/api/v1/sessions/${env.AGENT_GUILD_SESSION_ID}/${kind}`, {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Content-Type': 'application/json',
80
+ 'Content-Length': Buffer.byteLength(body),
81
+ 'X-Agent-Guild-Report-Token': token,
82
+ },
83
+ timeout: 3000,
84
+ }, (res) => {
85
+ let text = '';
86
+ res.setEncoding('utf8');
87
+ res.on('data', (chunk) => { text += chunk; });
88
+ res.on('end', () => (res.statusCode >= 200 && res.statusCode < 300 ? resolve() : reject(new Error(`HTTP ${res.statusCode}: ${text}`))));
89
+ });
90
+ req.on('timeout', () => req.destroy(new Error('the manager did not answer in time')));
91
+ req.on('error', reject);
92
+ req.end(body);
76
93
  });
77
- if (!res.ok) {
78
- const body = await res.text();
79
- throw new Error(`HTTP ${res.status}: ${body}`);
80
- }
81
94
  }
82
95
 
83
96
  /** A hook must never break the coding tool, so report failures quietly. */
84
97
  const sendQuietly = (report) => send(report).catch((err) => console.error(`agent-guild-report: ${err.message}`));
85
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
+
86
125
  function parseJson(raw) {
87
126
  try { return JSON.parse(raw); } catch { return null; }
88
127
  }
@@ -95,7 +134,10 @@ async function main() {
95
134
  }
96
135
  if (args.hook) {
97
136
  const input = parseJson(await readStdin());
98
- 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;
99
141
  for (const report of hookToReports(input)) await sendQuietly(report);
100
142
  return;
101
143
  }
@@ -9,6 +9,7 @@
9
9
  "versionArgs": ["--version"],
10
10
  "usage": "claude",
11
11
  "history": "claude",
12
+ "reporting": "claude",
12
13
  "homeVar": "CLAUDE_CONFIG_DIR",
13
14
  "accountEnv": { "CLAUDE_SECURESTORAGE_CONFIG_DIR": "{dir}" },
14
15
  "hooks": { "path": "settings.json", "example": "claude-code-settings.json" },
@@ -46,6 +47,7 @@
46
47
  "versionArgs": ["--version"],
47
48
  "usage": "codex",
48
49
  "history": "codex",
50
+ "reporting": "codex",
49
51
  "homeVar": "CODEX_HOME",
50
52
  "hooks": { "path": "hooks.json", "example": "codex-hooks.json" },
51
53
  "modelPattern": "\\bgpt-\\d[a-z0-9.-]*",
@@ -74,27 +76,28 @@
74
76
  {
75
77
  "id": "google",
76
78
  "vendor": "Google",
77
- "tool": "Gemini CLI",
78
- "command": "gemini",
79
- "package": "@google/gemini-cli",
79
+ "tool": "Antigravity CLI",
80
+ "command": "agy",
80
81
  "versionArgs": ["--version"],
81
- "usage": "gemini",
82
- "history": "gemini",
83
- "homeVar": "GEMINI_CLI_HOME",
84
- "accountEnv": { "GEMINI_FORCE_FILE_STORAGE": "true" },
85
- "hooks": { "path": ".gemini/settings.json", "example": "gemini-settings.json" },
82
+ "history": "antigravity",
83
+ "reporting": "antigravity",
86
84
  "modelPattern": "\\bgemini-\\d[a-z0-9.-]*",
87
85
  "args": [],
88
- "resumeArgs": ["--resume", "{id}"],
86
+ "resumeArgs": ["--conversation", "{id}"],
89
87
  "color": "#4285F4",
90
88
  "monogram": "G",
91
- "install": "npm install -g @google/gemini-cli",
89
+ "install": "curl -fsSL https://antigravity.google/cli/install.sh | bash",
92
90
  "channels": {
93
- "brew": { "names": ["gemini-cli"] }
91
+ "native": { "paths": ["~/.local/bin/agy"], "update": ["update"], "uninstall": "rm -f ~/.local/bin/agy" }
94
92
  },
95
- "docs": "https://github.com/google-gemini/gemini-cli",
96
- "usageUrl": "https://aistudio.google.com/usage",
97
- "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
+ }
98
101
  },
99
102
  {
100
103
  "id": "xai",
@@ -104,6 +107,7 @@
104
107
  "package": "@xai-official/grok",
105
108
  "versionArgs": ["--version"],
106
109
  "history": "grok",
110
+ "reporting": "grok",
107
111
  "homeVar": "GROK_HOME",
108
112
  "hooks": { "path": "hooks/agent-guild.json", "example": "grok-hooks.json" },
109
113
  "modelPattern": "\\bgrok-(?:build|\\d)[a-z0-9.-]*",
@@ -50,6 +50,49 @@
50
50
  }
51
51
  ]
52
52
  }
53
+ ],
54
+ "PreToolUse": [
55
+ {
56
+ "matcher": "run_terminal_command",
57
+ "hooks": [
58
+ {
59
+ "type": "command",
60
+ "command": "agent-guild-report --hook"
61
+ }
62
+ ]
63
+ }
64
+ ],
65
+ "PostToolUse": [
66
+ {
67
+ "matcher": "run_terminal_command",
68
+ "hooks": [
69
+ {
70
+ "type": "command",
71
+ "command": "agent-guild-report --hook"
72
+ }
73
+ ]
74
+ }
75
+ ],
76
+ "PostToolUseFailure": [
77
+ {
78
+ "matcher": "run_terminal_command",
79
+ "hooks": [
80
+ {
81
+ "type": "command",
82
+ "command": "agent-guild-report --hook"
83
+ }
84
+ ]
85
+ }
86
+ ],
87
+ "Stop": [
88
+ {
89
+ "hooks": [
90
+ {
91
+ "type": "command",
92
+ "command": "agent-guild-report --hook"
93
+ }
94
+ ]
95
+ }
53
96
  ]
54
97
  }
55
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddessentials/agent-guild",
3
- "version": "0.10.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": {
@@ -83,6 +83,10 @@ export function resolveAllCommands(command, env = process.env, platform = proces
83
83
  return hits;
84
84
  }
85
85
 
86
+ export function killWindowsTree(pid, done = () => {}) {
87
+ execFile('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, timeout: 5000 }, (err) => done(err));
88
+ }
89
+
86
90
  /** Quote one argument for a cmd.exe command line. */
87
91
  export function quoteForCmd(arg) {
88
92
  const s = String(arg);
@@ -120,9 +124,9 @@ export function buildSpawnSpec(resolvedPath, args = [], env = process.env, platf
120
124
  * Run a spawn spec to completion without a terminal. Resolves with its
121
125
  * output; rejects with the error carrying stdout and stderr.
122
126
  */
123
- export function runSpec(spec, { env, timeoutMs = 15000 } = {}) {
127
+ export function runSpec(spec, { env, timeoutMs = 15000, cwd } = {}) {
124
128
  return new Promise((resolve, reject) => {
125
- const opts = { env, timeout: timeoutMs, windowsHide: true, maxBuffer: 1024 * 1024 };
129
+ const opts = { env, cwd, timeout: timeoutMs, windowsHide: true, maxBuffer: 1024 * 1024 };
126
130
  let args = spec.args;
127
131
  if (typeof args === 'string') {
128
132
  opts.windowsVerbatimArguments = true;
@@ -47,6 +47,7 @@ export const paths = {
47
47
  get log() { return path.join(dataDir(), 'manager.log'); },
48
48
  /** Launchers for agent-guild-report, put first on every session's PATH. */
49
49
  get shims() { return path.join(dataDir(), 'bin'); },
50
+ get reporting() { return path.join(dataDir(), 'reporting'); },
50
51
  };
51
52
 
52
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');
@@ -16,6 +16,7 @@ import { createManagerServer } from './server.mjs';
16
16
  import { SelfUpdate } from './self-update.mjs';
17
17
  import { resolveBaseEnv, pathReader } from './shell-env.mjs';
18
18
  import { writeReportShims } from './report-shims.mjs';
19
+ import { SessionHooks } from './session-hooks.mjs';
19
20
  import { launcherPath, spawnManager } from './launch.mjs';
20
21
  import {
21
22
  DEFAULT_HOST,
@@ -62,7 +63,12 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
62
63
  let api;
63
64
  const selfUpdate = new SelfUpdate({ pkg: PACKAGE_NAME, version, packageFile, registry });
64
65
  const github = new GitHub({ dir: paths.github, registry, ...githubOptions });
65
- const manager = new SessionManager({ registry, baseEnv, getApiUrl: () => api.url, sessionDefaults, shimDir, selfUpdate, github });
66
+ const sessionHooks = new SessionHooks({ registry, dir: paths.reporting, version });
67
+ registry.reportingEnabled = (provider) => sessionHooks.enabled(provider);
68
+ sessionHooks.warm();
69
+ const manager = new SessionManager({
70
+ registry, baseEnv, getApiUrl: () => api.url, sessionDefaults, shimDir, selfUpdate, github, sessionHooks,
71
+ });
66
72
  const usage = new UsageMonitor({ registry, env: baseEnv });
67
73
  const history = new SessionHistory({ registry, env: baseEnv });
68
74
  const modelStats = new ModelStats({ registry });
@@ -71,7 +77,7 @@ export async function startManager({ port = resolvePort(), host = DEFAULT_HOST,
71
77
  let closing = null;
72
78
 
73
79
  const refreshVersions = () => {
74
- registry.refreshVersions().catch(() => {});
80
+ registry.refreshVersions().then(() => sessionHooks.warm(), () => {});
75
81
  selfUpdate.refresh().catch(() => {});
76
82
  };
77
83
  const versionTimer = setInterval(refreshVersions, VERSION_REFRESH_MS);
@@ -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,6 +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', 'antigravity', 'grok']);
22
23
  const VERSION_TTL_MS = 60 * 60 * 1000;
23
24
  const FAILED_PROBE_TTL_MS = 5 * 60 * 1000;
24
25
  const PATH_REFRESH_MS = 60 * 1000;
@@ -51,18 +52,18 @@ function normalizeEnv(env) {
51
52
  return out;
52
53
  }
53
54
 
54
- /** "claude", "codex", "gemini", a { command, args } that prints usage JSON, or null. */
55
+ /** "claude", "codex", a { command, args } that prints usage JSON, or null. */
55
56
  function normalizeUsage(usage) {
56
- if (usage === 'claude' || usage === 'codex' || usage === 'gemini') return usage;
57
+ if (usage === 'claude' || usage === 'codex') return usage;
57
58
  if (usage && typeof usage === 'object' && typeof usage.command === 'string' && usage.command) {
58
59
  return { command: usage.command, args: Array.isArray(usage.args) ? usage.args.map(String) : [] };
59
60
  }
60
61
  return null;
61
62
  }
62
63
 
63
- /** "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. */
64
65
  function normalizeHistory(history) {
65
- if (['claude', 'codex', 'gemini', 'grok'].includes(history)) return history;
66
+ if (['claude', 'codex', 'antigravity', 'grok'].includes(history)) return history;
66
67
  if (history && typeof history === 'object' && typeof history.command === 'string' && history.command) {
67
68
  return { command: history.command, args: Array.isArray(history.args) ? history.args.map(String) : [] };
68
69
  }
@@ -163,6 +164,7 @@ function normalize(raw, platform, warnings) {
163
164
  versionArgs: Array.isArray(merged.versionArgs) && merged.versionArgs.length ? merged.versionArgs.map(String) : null,
164
165
  usage: normalizeUsage(merged.usage),
165
166
  history: normalizeHistory(merged.history),
167
+ reporting: REPORTING_MODES.has(merged.reporting) ? merged.reporting : null,
166
168
  modelPattern: merged.modelPattern ? String(merged.modelPattern) : null,
167
169
  args: Array.isArray(merged.args) ? merged.args.map(String) : [],
168
170
  resumeArgs: Array.isArray(merged.resumeArgs) ? merged.resumeArgs.map(String) : [],
@@ -251,6 +253,7 @@ export class ProviderRegistry extends EventEmitter {
251
253
  this.versions = new Map();
252
254
  this._refreshing = null;
253
255
  this._npmRegistry = null;
256
+ this.reportingEnabled = null;
254
257
  this.reload();
255
258
  }
256
259
 
@@ -346,7 +349,7 @@ export class ProviderRegistry extends EventEmitter {
346
349
  if (force || entry.probePath !== found || entry.probeMtime !== mtime || now - entry.probeAt > ttl) {
347
350
  const spec = buildSpawnSpec(channel.update.file, [...channel.update.args, '--help'], this.env, this.platform);
348
351
  const ok = await runSpec(spec, { env: { ...this.env, ...provider.env } })
349
- .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 }));
350
353
  changed ||= ok !== entry.probeOk;
351
354
  Object.assign(entry, { probePath: found, probeMtime: mtime, probeAt: now, probeOk: ok });
352
355
  }
@@ -623,7 +626,9 @@ export class ProviderRegistry extends EventEmitter {
623
626
  npmNote: provider.npmNote,
624
627
  usageSource: provider.usage === null ? null : typeof provider.usage === 'string' ? provider.usage : 'command',
625
628
  historySource: provider.history === null ? null : typeof provider.history === 'string' ? provider.history : 'command',
626
- accounts: provider.accounts.map(({ id, label }) => ({ id, label })),
629
+ reporting: provider.reporting,
630
+ reportingEnabled: this.reportingEnabled?.(provider) ?? null,
631
+ accounts: provider.accounts.map((account) => ({ id: account.id, label: account.label })),
627
632
  modelPattern: provider.modelPattern,
628
633
  color: provider.color,
629
634
  monogram: provider.monogram,
@@ -642,7 +647,7 @@ export class ProviderRegistry extends EventEmitter {
642
647
  }
643
648
 
644
649
  /** Spawn spec for node-pty, or throws with a user-facing message. */
645
- spawnSpec(provider, extraArgs = [], resume = null) {
650
+ spawnSpec(provider, extraArgs = [], resume = null, leadArgs = []) {
646
651
  const resolved = this.resolve(provider);
647
652
  if (!resolved) {
648
653
  const hint = provider.install ? ` ${provider.install}` : '';
@@ -661,7 +666,7 @@ export class ProviderRegistry extends EventEmitter {
661
666
  }
662
667
  resumeArgs = provider.resumeArgs.map((arg) => arg.replaceAll('{id}', resume));
663
668
  }
664
- return buildSpawnSpec(resolved, [...provider.args, ...resumeArgs, ...extraArgs], this.env, this.platform);
669
+ return buildSpawnSpec(resolved, [...leadArgs, ...provider.args, ...resumeArgs, ...extraArgs], this.env, this.platform);
665
670
  }
666
671
 
667
672
  async updateSpec(provider) {
@@ -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
@@ -48,6 +48,7 @@ export function shimContents({ execPath, script, platform = process.platform })
48
48
  const files = {
49
49
  [SHIM_NAME]: [
50
50
  '#!/bin/sh',
51
+ '[ "$1" = --hook ] && [ -z "$AGENT_GUILD_SESSION_ID" ] && exec cat >/dev/null',
51
52
  `n=${shQuote(forSh(execPath))}`,
52
53
  '[ -x "$n" ] || n=node',
53
54
  `exec "$n" ${shQuote(forSh(script))} "$@"`,
@@ -58,6 +59,7 @@ export function shimContents({ execPath, script, platform = process.platform })
58
59
  files[LOADER_NAME] = `import ${JSON.stringify(fileUrl(script, platform))};\n`;
59
60
  files[`${SHIM_NAME}.cmd`] = [
60
61
  '@ECHO OFF',
62
+ 'IF "%~1"=="--hook" IF NOT DEFINED AGENT_GUILD_SESSION_ID EXIT /B 0',
61
63
  'IF EXIST "%AGENT_GUILD_NODE%" GOTO manager',
62
64
  `node "%~dp0${LOADER_NAME}" %*`,
63
65
  'EXIT /B %ERRORLEVEL%',
@@ -207,7 +207,7 @@ export function createManagerServer({
207
207
  // Agent, model and tool-session reports may authenticate with the
208
208
  // per-session report token that the manager injects into each tool's
209
209
  // environment.
210
- const reportMatch = route.match(/^\/sessions\/([a-f0-9]+)\/(agents|model|tool-session)$/);
210
+ const reportMatch = route.match(/^\/sessions\/([a-f0-9]+)\/(agents|model|tool-session|reporting|shells)$/);
211
211
  if (reportMatch && method === 'POST') {
212
212
  const [, id, kind] = reportMatch;
213
213
  const body = await readJsonBody(req);
@@ -217,6 +217,11 @@ export function createManagerServer({
217
217
  };
218
218
  if (kind === 'agents') return sendJson(res, 200, { agent: manager.reportAgent(id, body, auth) });
219
219
  if (kind === 'model') return sendJson(res, 200, { model: manager.reportModel(id, body, auth) });
220
+ if (kind === 'reporting') return sendJson(res, 200, { reporting: manager.reportHello(id, auth) });
221
+ if (kind === 'shells') {
222
+ manager.reportShell(id, body, auth);
223
+ return sendJson(res, 200, { ok: true });
224
+ }
220
225
  return sendJson(res, 200, { toolSessionId: manager.reportToolSession(id, body, auth) });
221
226
  }
222
227
 
@@ -269,6 +274,16 @@ export function createManagerServer({
269
274
  const account = registry.account(provider, url.searchParams.get('account'));
270
275
  return sendJson(res, 200, { history: await history.list(provider, account, { limit: url.searchParams.get('limit') }) });
271
276
  }
277
+ const reportingMatch = route.match(/^\/providers\/([a-z0-9][a-z0-9_-]{0,31})\/reporting$/);
278
+ if (reportingMatch && method === 'POST' && manager.sessionHooks) {
279
+ const provider = registry.get(reportingMatch[1]);
280
+ if (!provider) throw new HttpError(404, `unknown provider "${reportingMatch[1]}"`, 'unknown_provider');
281
+ const body = await readJsonBody(req);
282
+ if (typeof body.enabled !== 'boolean') throw new HttpError(400, 'enabled must be true or false', 'bad_request');
283
+ await manager.sessionHooks.setEnabled(provider, body.enabled);
284
+ registry.emit('updated');
285
+ return sendJson(res, 200, { provider: registry.describe(provider) });
286
+ }
272
287
  const installMatch = route.match(/^\/providers\/([a-z0-9][a-z0-9_-]{0,31})\/install$/);
273
288
  if (installMatch && method === 'POST') {
274
289
  const body = await readJsonBody(req);
@@ -280,7 +295,7 @@ export function createManagerServer({
280
295
  }
281
296
  if (route === '/sessions' && method === 'POST') {
282
297
  const body = await readJsonBody(req);
283
- const session = manager.create(body);
298
+ const session = await manager.create(body);
284
299
  return sendJson(res, 201, { session: session.toJSON() });
285
300
  }
286
301
  if (route === '/shutdown' && method === 'POST') {
@@ -452,7 +467,7 @@ export function createManagerServer({
452
467
  if (isBinary) return;
453
468
  let msg;
454
469
  try { msg = JSON.parse(raw.toString('utf8')); } catch { return; }
455
- if (msg.type === 'input' && typeof msg.data === 'string') session.write(msg.data);
470
+ if (msg.type === 'input' && typeof msg.data === 'string') session.input(msg.data);
456
471
  else if (msg.type === 'resize') session.resize(msg.cols, msg.rows);
457
472
  });
458
473
  }