@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 +22 -26
- package/bin/agent-guild-report.mjs +67 -25
- package/config/providers.default.json +18 -14
- package/examples/grok-hooks.json +43 -0
- package/package.json +1 -1
- package/src/manager/command-resolver.mjs +6 -2
- package/src/manager/config.mjs +1 -0
- package/src/manager/install-channels.mjs +7 -0
- package/src/manager/main.mjs +8 -2
- package/src/manager/news.mjs +1 -1
- package/src/manager/providers.mjs +14 -9
- package/src/manager/report-shims.mjs +6 -4
- package/src/manager/server.mjs +18 -3
- package/src/manager/session-history.mjs +32 -74
- package/src/manager/session-hooks.mjs +351 -0
- package/src/manager/session-manager.mjs +27 -19
- package/src/manager/session.mjs +183 -25
- package/src/manager/usage.mjs +3 -258
- package/src/report/hooks.mjs +87 -38
- package/web/app.js +89 -13
- package/web/index.html +5 -0
- package/web/styles.css +10 -2
- package/examples/gemini-settings.json +0 -61
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,
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
144
|
-
|
|
145
|
-
Claude Code
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
|
151
|
-
|
|
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"
|
|
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
|
|
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
|
|
230
|
-
*
|
|
231
|
-
|
|
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
|
|
14
|
-
// Claude Code, Codex CLI,
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (!inSession && env.AGENT_GUILD_SESSION_ID
|
|
26
|
-
console.error('agent-guild-report:
|
|
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
|
|
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
|
-
|
|
65
|
-
const kind = report.
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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": "
|
|
78
|
-
"command": "
|
|
79
|
-
"package": "@google/gemini-cli",
|
|
79
|
+
"tool": "Antigravity CLI",
|
|
80
|
+
"command": "agy",
|
|
80
81
|
"versionArgs": ["--version"],
|
|
81
|
-
"
|
|
82
|
-
"
|
|
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": ["--
|
|
86
|
+
"resumeArgs": ["--conversation", "{id}"],
|
|
89
87
|
"color": "#4285F4",
|
|
90
88
|
"monogram": "G",
|
|
91
|
-
"install": "
|
|
89
|
+
"install": "curl -fsSL https://antigravity.google/cli/install.sh | bash",
|
|
92
90
|
"channels": {
|
|
93
|
-
"
|
|
91
|
+
"native": { "paths": ["~/.local/bin/agy"], "update": ["update"], "uninstall": "rm -f ~/.local/bin/agy" }
|
|
94
92
|
},
|
|
95
|
-
"docs": "https://
|
|
96
|
-
"
|
|
97
|
-
"
|
|
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.-]*",
|
package/examples/grok-hooks.json
CHANGED
|
@@ -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.
|
|
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;
|
package/src/manager/config.mjs
CHANGED
|
@@ -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');
|
package/src/manager/main.mjs
CHANGED
|
@@ -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
|
|
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().
|
|
80
|
+
registry.refreshVersions().then(() => sessionHooks.warm(), () => {});
|
|
75
81
|
selfUpdate.refresh().catch(() => {});
|
|
76
82
|
};
|
|
77
83
|
const versionTimer = setInterval(refreshVersions, VERSION_REFRESH_MS);
|
package/src/manager/news.mjs
CHANGED
|
@@ -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('
|
|
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,
|
|
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",
|
|
55
|
+
/** "claude", "codex", a { command, args } that prints usage JSON, or null. */
|
|
55
56
|
function normalizeUsage(usage) {
|
|
56
|
-
if (usage === 'claude' || usage === 'codex'
|
|
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", "
|
|
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', '
|
|
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((
|
|
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
|
-
|
|
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,
|
|
6
|
-
//
|
|
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.
|
|
16
|
-
// bypassing that policy, so
|
|
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%',
|
package/src/manager/server.mjs
CHANGED
|
@@ -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.
|
|
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
|
}
|