@oddessentials/agent-guild 1.31.0 → 1.33.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/bin/agent-guild.mjs +52 -30
- package/bin/native/win32-arm64/agent-guild-report.exe +0 -0
- package/bin/native/win32-x64/agent-guild-report.exe +0 -0
- package/bin/windows/arm64/agent-guild-start.exe +0 -0
- package/bin/windows/x64/agent-guild-start.exe +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty.node +0 -0
- package/node_modules/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
- package/package.json +1 -1
- package/src/manager/command-resolver.mjs +4 -2
- package/src/manager/session-hooks.mjs +2 -1
- package/src/manager/shell-env.mjs +4 -2
- package/src/mcp/server.mjs +1 -1
- package/web/index.html +1 -1
- package/web/yard/art.json +1 -1
- package/web/yard/aura.js +165 -0
- package/web/yard/model.mjs +140 -0
- package/web/yard/renderer.js +119 -31
- package/web/yard/view.js +20 -1
- package/web/yard/yard.css +1 -0
package/bin/agent-guild.mjs
CHANGED
|
@@ -66,17 +66,25 @@ function portOf(url) {
|
|
|
66
66
|
return parsed.port ? Number(parsed.port) : parsed.protocol === 'https:' ? 443 : 80;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
/** The manager's health, `{ busy: pid }` when the manager its runtime file names for `url` runs but did not answer in time, or null. */
|
|
70
|
+
async function reach(url, timeoutMs = 1000) {
|
|
71
|
+
const signal = AbortSignal.timeout(timeoutMs);
|
|
70
72
|
try {
|
|
71
|
-
const res = await fetch(`${url}/api/v1/health`, { signal
|
|
73
|
+
const res = await fetch(`${url}/api/v1/health`, { signal });
|
|
72
74
|
if (!res.ok) return null;
|
|
73
75
|
const body = await res.json();
|
|
74
76
|
return body?.name === 'agent-guild' ? body : null;
|
|
75
77
|
} catch {
|
|
76
|
-
|
|
78
|
+
const owner = signal.aborted ? readRuntimeFile() : null;
|
|
79
|
+
return owner?.url === url && running(owner.pid) ? { busy: owner.pid } : null;
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
83
|
+
async function health(url, timeoutMs = 1000) {
|
|
84
|
+
const found = await reach(url, timeoutMs);
|
|
85
|
+
return found?.busy ? null : found;
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
function pageUrl(url, token) {
|
|
81
89
|
return `${url}/#token=${token}`;
|
|
82
90
|
}
|
|
@@ -114,7 +122,7 @@ async function ensureManager({ port = null } = {}) {
|
|
|
114
122
|
// A pinned port is the endpoint to serve, so it is also the one to check;
|
|
115
123
|
// a manager found anywhere else is not the one asked for.
|
|
116
124
|
const knownUrl = port === null ? baseUrl() : expectedUrl;
|
|
117
|
-
const answering = await
|
|
125
|
+
const answering = await reach(knownUrl);
|
|
118
126
|
if (answering) return { url: knownUrl, started: false, version: answering.version };
|
|
119
127
|
|
|
120
128
|
// Say why no manager can run here rather than start one that fails.
|
|
@@ -167,21 +175,23 @@ async function cmdOpen({ browser, signIn = false }) {
|
|
|
167
175
|
* a successor itself (a manager from before restarts only stops).
|
|
168
176
|
*/
|
|
169
177
|
async function requestShutdown(url, { restart = false } = {}) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
throw new Error(`${restart ? 'restart' : 'stop'} failed: ${error?.message ?? `HTTP ${res.status}`}`);
|
|
178
|
+
const signal = AbortSignal.timeout(5000);
|
|
179
|
+
try {
|
|
180
|
+
const res = await fetch(`${url}/api/v1/shutdown`, {
|
|
181
|
+
method: 'POST', signal,
|
|
182
|
+
headers: { Authorization: `Bearer ${loadOrCreateToken()}`, 'Content-Type': 'application/json' },
|
|
183
|
+
body: JSON.stringify(restart ? { force: true, restart: true } : { force: true }),
|
|
184
|
+
});
|
|
185
|
+
const body = await res.json().catch(() => ({}));
|
|
186
|
+
signal.throwIfAborted();
|
|
187
|
+
if (!res.ok) throw new Error(`${restart ? 'restart' : 'stop'} failed: ${body.error?.message ?? `HTTP ${res.status}`}`);
|
|
188
|
+
const running = body.running ?? 0;
|
|
189
|
+
if (running > 0) console.log(`Ending ${running} running session(s).`);
|
|
190
|
+
return { running, restart: body.restart === true };
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (signal.aborted) throw new Error(`the session manager did not answer the ${restart ? 'restart' : 'stop'} request within 5 seconds; its outcome could not be confirmed.`);
|
|
193
|
+
throw err;
|
|
180
194
|
}
|
|
181
|
-
const body = await res.json().catch(() => ({}));
|
|
182
|
-
const running = body.running ?? 0;
|
|
183
|
-
if (running > 0) console.log(`Ending ${running} running session(s).`);
|
|
184
|
-
return { running, restart: body.restart === true };
|
|
185
195
|
}
|
|
186
196
|
|
|
187
197
|
function running(pid) {
|
|
@@ -209,13 +219,13 @@ async function waitForExit(pid, timeoutMs = 15000) {
|
|
|
209
219
|
|
|
210
220
|
async function cmdStop() {
|
|
211
221
|
const url = baseUrl();
|
|
212
|
-
const h = await
|
|
222
|
+
const h = await reach(url);
|
|
213
223
|
if (!h) {
|
|
214
224
|
console.log('Session manager is not running.');
|
|
215
225
|
return;
|
|
216
226
|
}
|
|
217
227
|
await requestShutdown(url);
|
|
218
|
-
if (await waitForExit(h.pid)) {
|
|
228
|
+
if (await waitForExit(h.busy ?? h.pid)) {
|
|
219
229
|
console.log('Session manager stopped.');
|
|
220
230
|
return;
|
|
221
231
|
}
|
|
@@ -225,13 +235,14 @@ async function cmdStop() {
|
|
|
225
235
|
|
|
226
236
|
async function cmdRestart() {
|
|
227
237
|
const url = baseUrl();
|
|
228
|
-
const
|
|
229
|
-
if (!
|
|
238
|
+
const found = await reach(url);
|
|
239
|
+
if (!found) {
|
|
230
240
|
// Nothing to stop: a restart of a stopped manager is a start.
|
|
231
241
|
const { url: started, version } = await ensureManager();
|
|
232
242
|
console.log(`Session manager was not running; started Agent Guild ${version} at ${started}.`);
|
|
233
243
|
return;
|
|
234
244
|
}
|
|
245
|
+
const before = found.busy ? { pid: found.busy } : found;
|
|
235
246
|
const { restart } = await requestShutdown(url, { restart: true });
|
|
236
247
|
if (!restart) {
|
|
237
248
|
// A manager from before restarts stops without starting a successor,
|
|
@@ -239,7 +250,7 @@ async function cmdRestart() {
|
|
|
239
250
|
if (!(await waitForExit(before.pid))) throw new Error('the session manager did not stop, so it could not be restarted.');
|
|
240
251
|
// On the port the old one served, which an ephemeral port setting would otherwise lose.
|
|
241
252
|
const { url: started, version } = await ensureManager({ port: portOf(url) });
|
|
242
|
-
const changed = version !== before.version ? `, now Agent Guild ${version} (was ${before.version})` : '';
|
|
253
|
+
const changed = before.version && version !== before.version ? `, now Agent Guild ${version} (was ${before.version})` : '';
|
|
243
254
|
console.log(`Session manager restarted at ${started}${changed}.`);
|
|
244
255
|
return;
|
|
245
256
|
}
|
|
@@ -250,7 +261,7 @@ async function cmdRestart() {
|
|
|
250
261
|
await new Promise((r) => setTimeout(r, 250));
|
|
251
262
|
const now = await health(url, 500);
|
|
252
263
|
if (now && now.pid !== before.pid) {
|
|
253
|
-
const changed = now.version !== before.version ? `, now Agent Guild ${now.version} (was ${before.version})` : '';
|
|
264
|
+
const changed = before.version && now.version !== before.version ? `, now Agent Guild ${now.version} (was ${before.version})` : '';
|
|
254
265
|
console.log(`Session manager restarted at ${url}${changed}.`);
|
|
255
266
|
return;
|
|
256
267
|
}
|
|
@@ -261,7 +272,12 @@ async function cmdRestart() {
|
|
|
261
272
|
|
|
262
273
|
async function cmdStatus() {
|
|
263
274
|
const url = baseUrl();
|
|
264
|
-
const h = await
|
|
275
|
+
const h = await reach(url);
|
|
276
|
+
if (h?.busy) {
|
|
277
|
+
console.log(`Session manager running at ${url} (pid ${h.busy}) did not answer in time.`);
|
|
278
|
+
process.exitCode = 1;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
265
281
|
if (!h) {
|
|
266
282
|
console.log('Session manager is not running.');
|
|
267
283
|
// With no manager to ask, the boot service is read from systemd directly.
|
|
@@ -294,15 +310,21 @@ async function cmdStatus() {
|
|
|
294
310
|
|
|
295
311
|
async function cmdFireworks() {
|
|
296
312
|
const url = baseUrl();
|
|
297
|
-
if (!(await
|
|
313
|
+
if (!(await reach(url))) {
|
|
298
314
|
console.log('Session manager is not running.');
|
|
299
315
|
process.exitCode = 3;
|
|
300
316
|
return;
|
|
301
317
|
}
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
318
|
+
const signal = AbortSignal.timeout(5000);
|
|
319
|
+
try {
|
|
320
|
+
const res = await fetch(`${url}/api/v1/yard/fireworks`, { method: 'POST', signal, headers: { Authorization: `Bearer ${loadOrCreateToken()}` } });
|
|
321
|
+
if (!res.ok) throw new Error(`the session manager answered ${res.status}.`);
|
|
322
|
+
const { pages } = await res.json();
|
|
323
|
+
console.log(pages ? `Fireworks sent to ${pages} open page(s); every Yard on show lights up.` : 'No page is open to see the fireworks.');
|
|
324
|
+
} catch (err) {
|
|
325
|
+
if (signal.aborted) throw new Error('the session manager did not answer the fireworks request within 5 seconds; delivery could not be confirmed.');
|
|
326
|
+
throw err;
|
|
327
|
+
}
|
|
306
328
|
}
|
|
307
329
|
|
|
308
330
|
const MIN_NODE_MAJOR = 22;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oddessentials/agent-guild",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.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
|
"mcpName": "ai.oddessentials/agent-guild",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -184,17 +184,19 @@ export function npmSpawnSpec(npm, args, env = process.env, platform = process.pl
|
|
|
184
184
|
function runInOwnGroup(file, args, { env, cwd, timeoutMs, input, maxBuffer, signal }) {
|
|
185
185
|
return new Promise((resolve, reject) => {
|
|
186
186
|
let child;
|
|
187
|
+
// Registered before the spawn: a signal that lands once the command runs must find it.
|
|
188
|
+
const end = () => killGroup(child?.pid);
|
|
189
|
+
const release = endWithProcess(end);
|
|
187
190
|
try {
|
|
188
191
|
child = spawn(file, args, { env, cwd, detached: true });
|
|
189
192
|
} catch (err) {
|
|
193
|
+
release();
|
|
190
194
|
return reject(Object.assign(err, { stdout: '', stderr: '' }));
|
|
191
195
|
}
|
|
192
196
|
let stdout = '';
|
|
193
197
|
let stderr = '';
|
|
194
198
|
let timedOut = false;
|
|
195
199
|
let settled = false;
|
|
196
|
-
const end = () => killGroup(child.pid);
|
|
197
|
-
const release = endWithProcess(end);
|
|
198
200
|
const finish = (err) => {
|
|
199
201
|
if (settled) return;
|
|
200
202
|
settled = true;
|
|
@@ -214,6 +214,8 @@ export function codexHooksList(resolved, args, { env, platform = process.platfor
|
|
|
214
214
|
const stopped = () => finish(new Error('The manager is stopping'));
|
|
215
215
|
const timer = setTimeout(() => finish(new Error('Codex did not answer in time')), timeoutMs);
|
|
216
216
|
timer.unref?.();
|
|
217
|
+
// Registered before the spawn: a signal that lands once Codex runs must find it.
|
|
218
|
+
if (platform !== 'win32') release = endWithProcess(() => killGroup(child?.pid));
|
|
217
219
|
try {
|
|
218
220
|
const verbatim = typeof spec.args === 'string';
|
|
219
221
|
child = spawn(spec.file, verbatim ? [spec.args] : spec.args, {
|
|
@@ -222,7 +224,6 @@ export function codexHooksList(resolved, args, { env, platform = process.platfor
|
|
|
222
224
|
} catch (err) {
|
|
223
225
|
return finish(err);
|
|
224
226
|
}
|
|
225
|
-
if (platform !== 'win32') release = endWithProcess(() => killGroup(child.pid));
|
|
226
227
|
signal?.addEventListener('abort', stopped, { once: true });
|
|
227
228
|
let stderr = '';
|
|
228
229
|
let buffer = '';
|
|
@@ -78,6 +78,9 @@ export function probeLoginShell({ shell, env = process.env, timeoutMs = LOGIN_SH
|
|
|
78
78
|
if (process.platform === 'win32' || !shell) return Promise.resolve({ vars: null, failure: null });
|
|
79
79
|
return new Promise((resolve) => {
|
|
80
80
|
let child;
|
|
81
|
+
// Registered before the spawn: a signal that lands once the shell runs must find it.
|
|
82
|
+
const end = () => killGroup(child?.pid);
|
|
83
|
+
const release = endWithProcess(end);
|
|
81
84
|
try {
|
|
82
85
|
child = spawn(shell, LOGIN_SHELL_ARGS, {
|
|
83
86
|
detached: true,
|
|
@@ -85,12 +88,11 @@ export function probeLoginShell({ shell, env = process.env, timeoutMs = LOGIN_SH
|
|
|
85
88
|
env: { ...env, AGENT_GUILD_RESOLVING_ENV: '1' },
|
|
86
89
|
});
|
|
87
90
|
} catch (err) {
|
|
91
|
+
release();
|
|
88
92
|
return resolve({ vars: null, failure: `could not start (${err.code || err.message})` });
|
|
89
93
|
}
|
|
90
94
|
let stdout = '';
|
|
91
95
|
let settled = false;
|
|
92
|
-
const end = () => killGroup(child.pid);
|
|
93
|
-
const release = endWithProcess(end);
|
|
94
96
|
const finish = (result) => {
|
|
95
97
|
if (settled) return;
|
|
96
98
|
settled = true;
|
package/src/mcp/server.mjs
CHANGED
|
@@ -24,7 +24,7 @@ const HIDDEN = '[token hidden]';
|
|
|
24
24
|
// 64 bits of the known token; leave colors, short hashes and 12-digit session ids readable.
|
|
25
25
|
const MIN_TOKEN_FRAGMENT = 16;
|
|
26
26
|
|
|
27
|
-
const CUSTOMIZE = 'To customize a Yard
|
|
27
|
+
const CUSTOMIZE = 'To customize a Yard or the characters and companions of Agent Guild: World, or to install Grove Companion on the desktop, follow https://agentguild.oddessentials.ai/agents/.';
|
|
28
28
|
|
|
29
29
|
const INSTRUCTIONS = `Agent Guild runs coding agents (Claude Code, Codex CLI, Antigravity CLI, Grok Build, Docker Agent) and shells in real terminals on this computer, and opens them on every other screen.
|
|
30
30
|
Use guild_status first, list_sessions to see the sessions, read_reply for the answer that ended a session's last turn, and read_screen for what its terminal shows now.
|
package/web/index.html
CHANGED
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
<div class="yard-overlay">
|
|
152
152
|
<div id="yard-loading" class="yard-loading" role="status" data-mode="scene"><span id="yard-loading-text">Preparing your yard…</span><span id="yard-loading-size" class="yard-loading-size" aria-hidden="true"></span><span class="yard-loading-bar" aria-hidden="true"></span></div>
|
|
153
153
|
<div id="yard-swap-failure" class="yard-swap-failure" hidden role="status"><span id="yard-swap-failure-text"></span><button id="yard-swap-retry" class="btn" type="button">Retry</button></div>
|
|
154
|
-
<div class="yard-bar"><div id="yard-world" class="yard-world" role="group" aria-label="This Yard's world"><p id="yard-world-note" class="yard-world-note" role="status" hidden></p><div class="yard-world-actions"><button id="yard-customize-open" type="button" class="btn" aria-haspopup="dialog">Customize this Yard</button><a id="yard-world-get" class="btn" href="https://github.com/oddessentials/agent-guild-builders/releases/latest" target="_blank" rel="noopener noreferrer" hidden>Build a Yard</a></div></div><div class="yard-camera" role="group" aria-label="Yard controls"><button id="yard-zoom-in" type="button" class="btn" aria-label="Zoom in">+</button><button id="yard-zoom-out" type="button" class="btn" aria-label="Zoom out">−</button><button id="yard-fit" type="button" class="btn">Overview</button><button id="yard-focus" type="button" class="btn" disabled>Focus selected</button><button id="yard-photo" type="button" class="btn" aria-haspopup="dialog" aria-label="Save a picture of the Yard" title="Save a picture of the Yard" disabled hidden><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 8.5A1.5 1.5 0 0 1 5.5 7h2.3l1.4-2h5.6l1.4 2h2.3A1.5 1.5 0 0 1 20 8.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 17.5z"/><circle cx="12" cy="12.75" r="3.25"/></svg></button><button id="yard-fireworks" type="button" class="btn" aria-label="Fireworks in this Yard only" title="Fireworks in this Yard only" disabled><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M12 3v3m0 9v6M3 10h3m12 0h3M5.6 3.6l2.2 2.2m8.4 8.4 2.2 2.2M18.4 3.6l-2.2 2.2m-8.4 8.4-2.2 2.2"/><circle cx="12" cy="10" r="1"/></svg></button></div></div>
|
|
154
|
+
<div class="yard-bar"><div id="yard-world" class="yard-world" role="group" aria-label="This Yard's world"><p id="yard-world-note" class="yard-world-note" role="status" hidden></p><div class="yard-world-actions"><button id="yard-customize-open" type="button" class="btn" aria-haspopup="dialog">Customize this Yard</button><a id="yard-world-get" class="btn" href="https://github.com/oddessentials/agent-guild-builders/releases/latest" target="_blank" rel="noopener noreferrer" hidden>Build a Yard</a></div></div><div class="yard-camera" role="group" aria-label="Yard controls"><button id="yard-zoom-in" type="button" class="btn" aria-label="Zoom in">+</button><button id="yard-zoom-out" type="button" class="btn" aria-label="Zoom out">−</button><button id="yard-fit" type="button" class="btn">Overview</button><button id="yard-focus" type="button" class="btn" disabled>Focus selected</button><button id="yard-photo" type="button" class="btn" aria-haspopup="dialog" aria-label="Save a picture of the Yard" title="Save a picture of the Yard" disabled hidden><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4 8.5A1.5 1.5 0 0 1 5.5 7h2.3l1.4-2h5.6l1.4 2h2.3A1.5 1.5 0 0 1 20 8.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 4 17.5z"/><circle cx="12" cy="12.75" r="3.25"/></svg></button><button id="yard-fireworks" type="button" class="btn" aria-label="Fireworks in this Yard only" title="Fireworks in this Yard only" disabled><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M12 3v3m0 9v6M3 10h3m12 0h3M5.6 3.6l2.2 2.2m8.4 8.4 2.2 2.2M18.4 3.6l-2.2 2.2m-8.4 8.4-2.2 2.2"/><circle cx="12" cy="10" r="1"/></svg></button><button id="yard-vibe" type="button" class="btn" aria-label="Vibes Mode" aria-pressed="false"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="14.5" cy="4.5" r="1.8"/><path d="M8 21l3.5-5.5-2.5-3 2.5-4.5 4 1.5 2 3h3"/><path d="M11.5 8 7.5 9.5 6 13"/><path d="M11 15.5l4 2V21"/></svg></button></div></div>
|
|
155
155
|
<p id="yard-help"><span class="yard-help-pointer">Drag to explore · Scroll to zoom · Select to inspect · Double-click a session to open</span><span class="yard-help-touch">Drag to explore · Pinch to zoom · Tap to inspect · Open from the Inspector</span></p>
|
|
156
156
|
</div>
|
|
157
157
|
</div>
|