@oddessentials/agent-guild 1.18.0 → 1.20.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 +11 -4
- package/README.zh-CN.md +67 -0
- package/README.zh-TW.md +67 -0
- package/bin/agent-guild.mjs +16 -0
- package/docs/api.md +33 -13
- 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/antigravity-transcript.mjs +4 -70
- package/src/manager/card-names.mjs +110 -0
- package/src/manager/config.mjs +1 -0
- package/src/manager/history-transcripts.mjs +598 -0
- package/src/manager/install-channels.mjs +8 -2
- package/src/manager/launch-flags.mjs +2 -3
- package/src/manager/launchers.mjs +66 -0
- package/src/manager/main.mjs +4 -2
- package/src/manager/providers.mjs +8 -10
- package/src/manager/pty.mjs +26 -0
- package/src/manager/server.mjs +4 -0
- package/src/manager/session-history.mjs +135 -66
- package/src/manager/session-hooks.mjs +2 -2
- package/src/manager/session-manager.mjs +53 -9
- package/src/manager/session.mjs +21 -18
- package/src/mcp/server.mjs +11 -0
- package/web/app.js +38 -45
- package/web/history-preview.js +7 -7
- package/web/index.html +1 -1
- package/web/yard/art.json +1 -1
- package/web/yard/model.mjs +1 -1
- package/web/yard/renderer.js +1 -1
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
<img src="https://img.shields.io/node/v/@oddessentials/agent-guild?color=4cc38a" alt="Node.js version">
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
|
+
<p align="center"><strong>English</strong> · <a href="README.zh-CN.md">简体中文</a> · <a href="README.zh-TW.md">繁體中文</a></p>
|
|
11
|
+
|
|
10
12
|
**Run Claude Code, Codex CLI, Antigravity CLI, Grok Build, Docker Agent and
|
|
11
13
|
your own shells side by side in real terminals. Pick the same live sessions up
|
|
12
14
|
from your phone, VS Code or any browser. Free, on Windows, macOS and Linux.**
|
|
@@ -58,11 +60,15 @@ then connects over [MCP](docs/mcp.md) to see and steer your sessions.
|
|
|
58
60
|
Codex CLI.
|
|
59
61
|
* **The same session on every screen.** Any browser, a phone over
|
|
60
62
|
[Tailscale](#remote-access-with-tailscale) with touch keys, VS Code, and
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
the Agent Guild app: iPhone, iPad and Apple Vision Pro on the
|
|
64
|
+
[App Store](https://apps.apple.com/us/app/agent-guild-remote/id6825451331);
|
|
65
|
+
Android, Android TV, Google TV and Android XR on
|
|
66
|
+
[Google Play](https://play.google.com/store/apps/details?id=com.oddessentials.agentguild)
|
|
67
|
+
or [GitHub](https://github.com/oddessentials/agent-guild-android/releases/latest);
|
|
68
|
+
Fire tablets and Fire TV on the [Amazon Appstore](https://www.amazon.com/dp/B0HPLBDYPR);
|
|
69
|
+
and Meta Quest on the [Meta Horizon Store](https://www.meta.com/experiences/1325496170657270/).
|
|
64
70
|
Apple Watch and Meta Ray-Ban Display list your sessions and read a
|
|
65
|
-
session's last answer aloud.
|
|
71
|
+
session's last answer aloud. See [what each screen needs](https://agentguild.oddessentials.ai/requirements/).
|
|
66
72
|
* **Native on Windows.** No WSL. Also macOS, Linux, and headless Linux
|
|
67
73
|
servers over SSH.
|
|
68
74
|
* **Nothing in between.** No account and no relay server: the manager
|
|
@@ -257,6 +263,7 @@ lets you try every skin.
|
|
|
257
263
|
| `agent-guild stop` | Stop the manager and end every session. |
|
|
258
264
|
| `agent-guild restart` | Restart the manager on the installed version and end every session. |
|
|
259
265
|
| `agent-guild url` | Print the page link with the access token. |
|
|
266
|
+
| `agent-guild fireworks` | Launch fireworks over every open Yard. A coding agent in any terminal can run it too. |
|
|
260
267
|
| `agent-guild start` | Run the manager in the foreground. |
|
|
261
268
|
| `agent-guild doctor` | Diagnose the health of the installation and related tooling. |
|
|
262
269
|
| `agent-guild mcp` | Serve MCP on stdio, so a coding agent can see and steer the sessions. See [Agent Guild for agents](docs/mcp.md). |
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<h1 align="center">Agent Guild</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center"><a href="README.md">English</a> · <strong>简体中文</strong> · <a href="README.zh-TW.md">繁體中文</a></p>
|
|
4
|
+
|
|
5
|
+
**在你自己的电脑上,用真正的终端并排运行 Claude Code、Codex CLI、Antigravity CLI、
|
|
6
|
+
Grok Build、Docker Agent 和你自己的 shell。在手机、VS Code 或任何浏览器上接着用同一批
|
|
7
|
+
正在运行的会话。免费,支持 Windows、macOS 和 Linux。**
|
|
8
|
+
|
|
9
|
+
Agent Guild 是一个运行在你电脑上的小管理器,提供一个本地网页。网页随时可以关掉,会话照常运行。
|
|
10
|
+
|
|
11
|
+
<a href="https://oddessentials.github.io/agent-guild/?view=yard&skin=guild&theme=dark"><picture>
|
|
12
|
+
<source media="(prefers-reduced-motion: reduce)" srcset="docs/images/yard-still.webp">
|
|
13
|
+
<img src="docs/images/yard-clip.webp" alt="七个编程智能体会话化身为 Guild Yard 里的角色:它们离开广场前往各自工具的大厅开始工作;双击其中一个,就会打开 Claude Code 的真实终端" width="100%">
|
|
14
|
+
</picture></a>
|
|
15
|
+
|
|
16
|
+
**[试玩演示](https://oddessentials.github.io/agent-guild/?view=yard&skin=guild&theme=dark)**:
|
|
17
|
+
在浏览器里逛一逛 Yard。模拟数据,免费,无需账号,什么都不用安装。
|
|
18
|
+
|
|
19
|
+
## 快速开始
|
|
20
|
+
|
|
21
|
+
需要 Node.js 22 或更新版本:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install -g @oddessentials/agent-guild
|
|
25
|
+
agent-guild
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
网页会自动在浏览器中打开。在某个工具的卡片上按 **New** 即可启动会话。
|
|
29
|
+
每个工具在第一个会话里照常登录,和在任何终端里一样。
|
|
30
|
+
|
|
31
|
+
**或者交给你的智能体。** 让它读取
|
|
32
|
+
[安装技能](https://github.com/oddessentials/agent-guild/blob/main/plugins/agent-guild/skills/setup-agent-guild/SKILL.md),
|
|
33
|
+
它会安装并检查 Agent Guild,然后通过 [MCP](docs/mcp.md) 连接,查看并操控你的会话。
|
|
34
|
+
|
|
35
|
+
## 为什么选 Agent Guild
|
|
36
|
+
|
|
37
|
+
* **原版 CLI,原封不动。** 每个会话都是工具本身,跑在真实终端里,而不是聊天界面的复制品。
|
|
38
|
+
工具支持的登录方式都能用:订阅、API key、Amazon Bedrock 或你自己的网关。
|
|
39
|
+
* **所有智能体,一处管理。** Claude Code、Codex CLI、Antigravity CLI、Grok Build、
|
|
40
|
+
Docker Agent、任意 shell,以及 tmux 或 herdr 会话。Claude Code、Codex CLI 和
|
|
41
|
+
Grok Build 可以同时登录多个账号;Claude Code 和 Codex 账号的用量上限实时可见。
|
|
42
|
+
* **同一个会话,每块屏幕。** 任何浏览器;通过 Tailscale 在手机上使用,带触屏按键;
|
|
43
|
+
VS Code;以及 Agent Guild 应用:iPhone、iPad 和 Apple Vision Pro([App Store](https://apps.apple.com/us/app/agent-guild-remote/id6825451331)),
|
|
44
|
+
Android、Android TV、Google TV 和 Android XR([Google Play](https://play.google.com/store/apps/details?id=com.oddessentials.agentguild)),Fire 平板和 Fire TV
|
|
45
|
+
([Amazon Appstore](https://www.amazon.com/dp/B0HPLBDYPR)),[Meta Horizon Store](https://www.meta.com/experiences/1325496170657270/)。Apple Watch 和 Meta Ray-Ban Display
|
|
46
|
+
会列出你的会话,并朗读某个会话的最后一条回答。
|
|
47
|
+
* **Yard:看着它们干活。** 每个会话都是 3D 世界里的一个角色。等你回复的会被标出来;
|
|
48
|
+
CI 通过时 Yard 会放烟花。双击任何一个角色,就能打开它的真实终端直接输入。
|
|
49
|
+
* **原生支持 Windows。** 无需 WSL。也支持 macOS、Linux,以及通过 SSH 使用的无头 Linux 服务器。
|
|
50
|
+
* **中间没有任何东西。** 不需要账号,没有中转服务器。其他设备通过你自己的 Tailscale
|
|
51
|
+
网络或反向代理访问。开启远程访问后,拿到二维码或链接的人都能使用你的终端。
|
|
52
|
+
|
|
53
|
+
尚未支持:推送通知。
|
|
54
|
+
|
|
55
|
+
完整的功能、设置和命令说明见 [英文 README](README.md)。
|
|
56
|
+
|
|
57
|
+
## 链接
|
|
58
|
+
|
|
59
|
+
* 网站:https://agentguild.oddessentials.ai
|
|
60
|
+
* 各设备的要求:https://agentguild.oddessentials.ai/requirements/
|
|
61
|
+
* App:[App Store](https://apps.apple.com/us/app/agent-guild-remote/id6825451331) · [Google Play](https://play.google.com/store/apps/details?id=com.oddessentials.agentguild) · [Amazon Appstore](https://www.amazon.com/dp/B0HPLBDYPR) · [Meta Horizon Store](https://www.meta.com/experiences/1325496170657270/) · [GitHub 上的 Android 版](https://github.com/oddessentials/agent-guild-android/releases/latest)
|
|
62
|
+
* VS Code:[Marketplace](https://marketplace.visualstudio.com/items?itemName=OddEssentials.agent-guild) · [Open VSX](https://open-vsx.org/extension/OddEssentials/agent-guild)(Cursor、Windsurf 等编辑器)
|
|
63
|
+
* [Agent Guild: World](https://store.steampowered.com/app/5462970/Agent_Guild_World/):Steam 上的 3D 世界
|
|
64
|
+
* YouTube:https://www.youtube.com/@Agent-Guild
|
|
65
|
+
* X:https://x.com/agentguild_3d
|
|
66
|
+
|
|
67
|
+
以 [GNU AGPL 3.0](LICENSE) 开源。
|
package/README.zh-TW.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<h1 align="center">Agent Guild</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center"><a href="README.md">English</a> · <a href="README.zh-CN.md">简体中文</a> · <strong>繁體中文</strong></p>
|
|
4
|
+
|
|
5
|
+
**在你自己的電腦上,用真正的終端機並排執行 Claude Code、Codex CLI、Antigravity CLI、
|
|
6
|
+
Grok Build、Docker Agent 和你自己的 shell。在手機、VS Code 或任何瀏覽器上接著使用同一批
|
|
7
|
+
正在執行的工作階段。免費,支援 Windows、macOS 和 Linux。**
|
|
8
|
+
|
|
9
|
+
Agent Guild 是一個在你電腦上執行的小型管理程式,提供一個本機網頁。網頁隨時可以關掉,工作階段照常執行。
|
|
10
|
+
|
|
11
|
+
<a href="https://oddessentials.github.io/agent-guild/?view=yard&skin=guild&theme=dark"><picture>
|
|
12
|
+
<source media="(prefers-reduced-motion: reduce)" srcset="docs/images/yard-still.webp">
|
|
13
|
+
<img src="docs/images/yard-clip.webp" alt="七個程式設計代理的工作階段化身為 Guild Yard 裡的角色:它們離開廣場前往各自工具的大廳開始工作;雙擊其中一個,就會開啟 Claude Code 的真實終端機" width="100%">
|
|
14
|
+
</picture></a>
|
|
15
|
+
|
|
16
|
+
**[試玩展示](https://oddessentials.github.io/agent-guild/?view=yard&skin=guild&theme=dark)**:
|
|
17
|
+
在瀏覽器裡逛逛 Yard。模擬資料,免費,不需要帳號,什麼都不用安裝。
|
|
18
|
+
|
|
19
|
+
## 快速開始
|
|
20
|
+
|
|
21
|
+
需要 Node.js 22 或更新版本:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install -g @oddessentials/agent-guild
|
|
25
|
+
agent-guild
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
網頁會自動在瀏覽器中開啟。在某個工具的卡片上按 **New** 就能啟動工作階段。
|
|
29
|
+
每個工具在第一個工作階段裡照常登入,和在任何終端機裡一樣。
|
|
30
|
+
|
|
31
|
+
**或者交給你的代理。** 讓它讀取
|
|
32
|
+
[安裝技能](https://github.com/oddessentials/agent-guild/blob/main/plugins/agent-guild/skills/setup-agent-guild/SKILL.md),
|
|
33
|
+
它會安裝並檢查 Agent Guild,然後透過 [MCP](docs/mcp.md) 連線,查看並操控你的工作階段。
|
|
34
|
+
|
|
35
|
+
## 為什麼選 Agent Guild
|
|
36
|
+
|
|
37
|
+
* **原版 CLI,原封不動。** 每個工作階段都是工具本身,跑在真實的終端機裡,而不是聊天介面的複製品。
|
|
38
|
+
工具支援的登入方式都能用:訂閱、API 金鑰、Amazon Bedrock 或你自己的閘道。
|
|
39
|
+
* **所有代理,一處管理。** Claude Code、Codex CLI、Antigravity CLI、Grok Build、
|
|
40
|
+
Docker Agent、任何 shell,以及 tmux 或 herdr 工作階段。Claude Code、Codex CLI 和
|
|
41
|
+
Grok Build 可以同時登入多個帳號;Claude Code 和 Codex 帳號的用量上限即時可見。
|
|
42
|
+
* **同一個工作階段,每個螢幕。** 任何瀏覽器;透過 Tailscale 在手機上使用,附觸控按鍵;
|
|
43
|
+
VS Code;以及 Agent Guild App:iPhone、iPad 和 Apple Vision Pro([App Store](https://apps.apple.com/us/app/agent-guild-remote/id6825451331)),
|
|
44
|
+
Android、Android TV、Google TV 和 Android XR([Google Play](https://play.google.com/store/apps/details?id=com.oddessentials.agentguild)),Fire 平板和 Fire TV
|
|
45
|
+
([Amazon Appstore](https://www.amazon.com/dp/B0HPLBDYPR)),[Meta Horizon Store](https://www.meta.com/experiences/1325496170657270/)。Apple Watch 和 Meta Ray-Ban Display
|
|
46
|
+
會列出你的工作階段,並朗讀某個工作階段的最後一則回答。
|
|
47
|
+
* **Yard:看著它們工作。** 每個工作階段都是 3D 世界裡的一個角色。等你回覆的會被標示出來;
|
|
48
|
+
CI 通過時 Yard 會放煙火。雙擊任何一個角色,就能開啟它的真實終端機直接輸入。
|
|
49
|
+
* **原生支援 Windows。** 不需要 WSL。也支援 macOS、Linux,以及透過 SSH 使用的無頭 Linux 伺服器。
|
|
50
|
+
* **中間沒有任何東西。** 不需要帳號,沒有中繼伺服器。其他裝置透過你自己的 Tailscale
|
|
51
|
+
網路或反向代理連線。開啟遠端存取後,拿到 QR code 或連結的人都能使用你的終端機。
|
|
52
|
+
|
|
53
|
+
尚未支援:推播通知。
|
|
54
|
+
|
|
55
|
+
完整的功能、設定和指令說明請見 [英文 README](README.md)。
|
|
56
|
+
|
|
57
|
+
## 連結
|
|
58
|
+
|
|
59
|
+
* 網站:https://agentguild.oddessentials.ai
|
|
60
|
+
* 各裝置的需求:https://agentguild.oddessentials.ai/requirements/
|
|
61
|
+
* App:[App Store](https://apps.apple.com/us/app/agent-guild-remote/id6825451331) · [Google Play](https://play.google.com/store/apps/details?id=com.oddessentials.agentguild) · [Amazon Appstore](https://www.amazon.com/dp/B0HPLBDYPR) · [Meta Horizon Store](https://www.meta.com/experiences/1325496170657270/) · [GitHub 上的 Android 版](https://github.com/oddessentials/agent-guild-android/releases/latest)
|
|
62
|
+
* VS Code:[Marketplace](https://marketplace.visualstudio.com/items?itemName=OddEssentials.agent-guild) · [Open VSX](https://open-vsx.org/extension/OddEssentials/agent-guild)(Cursor、Windsurf 等編輯器)
|
|
63
|
+
* [Agent Guild: World](https://store.steampowered.com/app/5462970/Agent_Guild_World/):Steam 上的 3D 世界
|
|
64
|
+
* YouTube:https://www.youtube.com/@Agent-Guild
|
|
65
|
+
* X:https://x.com/agentguild_3d
|
|
66
|
+
|
|
67
|
+
以 [GNU AGPL 3.0](LICENSE) 開源。
|
package/bin/agent-guild.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// agent-guild restart stop the manager and start it again (ends all sessions)
|
|
8
8
|
// agent-guild status show whether the manager is running
|
|
9
9
|
// agent-guild url print the page URL (includes the access token)
|
|
10
|
+
// agent-guild fireworks launch fireworks over every open Yard
|
|
10
11
|
// agent-guild mcp serve the Model Context Protocol on stdio, for coding agents
|
|
11
12
|
|
|
12
13
|
import fs from 'node:fs';
|
|
@@ -35,6 +36,7 @@ Commands:
|
|
|
35
36
|
restart Stop the session manager and start it again; ends every session
|
|
36
37
|
status Show whether the session manager is running
|
|
37
38
|
url Print the web page URL, including the access token
|
|
39
|
+
fireworks Launch fireworks over every open Yard
|
|
38
40
|
doctor Diagnose the health of the installation and related tooling
|
|
39
41
|
mcp Serve the Model Context Protocol on stdio, so a coding agent can
|
|
40
42
|
see and steer the sessions (--allow-input lets it type into them)
|
|
@@ -287,6 +289,19 @@ async function cmdStatus() {
|
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
async function cmdFireworks() {
|
|
293
|
+
const url = baseUrl();
|
|
294
|
+
if (!(await health(url))) {
|
|
295
|
+
console.log('Session manager is not running.');
|
|
296
|
+
process.exitCode = 3;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const res = await fetch(`${url}/api/v1/yard/fireworks`, { method: 'POST', headers: { Authorization: `Bearer ${loadOrCreateToken()}` } });
|
|
300
|
+
if (!res.ok) throw new Error(`the session manager answered ${res.status}.`);
|
|
301
|
+
const { pages } = await res.json();
|
|
302
|
+
console.log(pages ? `Fireworks sent to ${pages} open page(s); every Yard on show lights up.` : 'No page is open to see the fireworks.');
|
|
303
|
+
}
|
|
304
|
+
|
|
290
305
|
const MIN_NODE_MAJOR = 22;
|
|
291
306
|
|
|
292
307
|
async function main() {
|
|
@@ -335,6 +350,7 @@ async function main() {
|
|
|
335
350
|
process.exitCode = ok ? 0 : 1;
|
|
336
351
|
return undefined;
|
|
337
352
|
}
|
|
353
|
+
case 'fireworks': return cmdFireworks();
|
|
338
354
|
case 'url': {
|
|
339
355
|
console.log(pageUrl(baseUrl(), loadOrCreateToken()));
|
|
340
356
|
return undefined;
|
package/docs/api.md
CHANGED
|
@@ -132,7 +132,8 @@ listed them once.
|
|
|
132
132
|
null when `GET /usage`, `GET /providers/:id/history`,
|
|
133
133
|
`GET /providers/:id/memory` or `GET /providers/:id/instructions` cannot report
|
|
134
134
|
the tool. `historyDetails` is true when `GET /providers/:id/history/detail`
|
|
135
|
-
can read saved messages
|
|
135
|
+
can read saved messages: for every built-in history source, not for a
|
|
136
|
+
`command`. `accounts` lists the sign-ins the tool can run
|
|
136
137
|
under: `default` is the tool's own, and each further one has its own home
|
|
137
138
|
folder, so it keeps its own sign-in and usage. `POST /sessions` takes an
|
|
138
139
|
account id. `accountsEditable` is true when accounts can be added, renamed
|
|
@@ -260,7 +261,7 @@ account, true when one was read, and null when that is unknown.
|
|
|
260
261
|
"providerId": "anthropic",
|
|
261
262
|
"accountId": "default",
|
|
262
263
|
"sessions": [
|
|
263
|
-
{ "id": "581893e5-a93d-5e49-968b-1c1c277d3255", "title": "Fix the login bug", "cwd": "/Users/me/src/app", "startedAt": "2026-10-01T13:26:53.713Z", "updatedAt": "2026-10-01T13:49:28.000Z" }
|
|
264
|
+
{ "id": "581893e5-a93d-5e49-968b-1c1c277d3255", "name": "Login bug", "title": "Fix the login bug", "cwd": "/Users/me/src/app", "startedAt": "2026-10-01T13:26:53.713Z", "updatedAt": "2026-10-01T13:49:28.000Z" }
|
|
264
265
|
],
|
|
265
266
|
"total": 42,
|
|
266
267
|
"fetchedAt": "2026-10-01T14:00:00.000Z",
|
|
@@ -269,16 +270,19 @@ account, true when one was read, and null when that is unknown.
|
|
|
269
270
|
```
|
|
270
271
|
|
|
271
272
|
The tool's own earlier sessions, newest first. `id` is what the tool resumes
|
|
272
|
-
by (`POST /sessions` with `resume`); `
|
|
273
|
-
|
|
273
|
+
by (`POST /sessions` with `resume`); `name` is the name a card holding
|
|
274
|
+
that session was given, or null; `title` is the name the tool gave the
|
|
275
|
+
session, else its first prompt, or null; `cwd` is the folder the session ran
|
|
276
|
+
in, or null. Claude Code
|
|
274
277
|
finds a session only from its own folder, so resume with that `cwd`.
|
|
275
278
|
`sessions` holds at most `limit` entries of the `total` found. `error` says
|
|
276
279
|
why nothing could be listed; a tool that has never run lists no sessions and
|
|
277
280
|
no error. Antigravity CLI entries add `workspaces`, and its history may add a
|
|
278
281
|
`note`.
|
|
279
282
|
|
|
280
|
-
The optional `cwd` and `q` parameters filter by folder and by text (
|
|
281
|
-
or folder) **before** applying `limit`; `total` counts matching
|
|
283
|
+
The optional `cwd` and `q` parameters filter by folder and by text (name,
|
|
284
|
+
title, id or folder) **before** applying `limit`; `total` counts matching
|
|
285
|
+
entries. Filtering uses the listing only and never reads a conversation.
|
|
282
286
|
|
|
283
287
|
#### Saved messages
|
|
284
288
|
|
|
@@ -291,16 +295,26 @@ or folder) **before** applying `limit`; `total` counts matching entries.
|
|
|
291
295
|
"messages": [{ "role": "user", "text": "Fix the login bug" }],
|
|
292
296
|
"nextCursor": null,
|
|
293
297
|
"incomplete": false,
|
|
294
|
-
"omitted": false
|
|
298
|
+
"omitted": false,
|
|
299
|
+
"truncated": false
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
302
|
```
|
|
298
303
|
|
|
299
|
-
Only the user's requests and the visible answers are included, as plain text
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
+
Only the user's requests and the visible answers are included, as plain text:
|
|
305
|
+
no thinking, tool calls or results, text the tool inserted, or turns it
|
|
306
|
+
rewound or rolled back. The first page holds the newest messages; each
|
|
307
|
+
`nextCursor` reads the page before it, and messages within a page are oldest
|
|
308
|
+
first. Every message is whole. Follow `nextCursor` until it is null, even when
|
|
309
|
+
a page has no visible messages. `omitted` says records were left out, such as
|
|
310
|
+
an unreadable one the tool itself skips; `truncated` says the history ends
|
|
311
|
+
early, at a record too large to read or one that cannot be read safely, and
|
|
312
|
+
nothing before it is shown; `incomplete` says the newest record was still being
|
|
313
|
+
written, and a later read may complete it. Messages added after the first page
|
|
314
|
+
appear when it is read again without a cursor. 409 `history_changed` means
|
|
315
|
+
the part a cursor depends on was rewritten, truncated or replaced: start again
|
|
316
|
+
without a cursor. 413 `history_too_large` means a compressed Codex rollout
|
|
317
|
+
decodes to more than 64 MiB.
|
|
304
318
|
|
|
305
319
|
### Memory
|
|
306
320
|
|
|
@@ -618,6 +632,7 @@ All paths are under `/api/v1`.
|
|
|
618
632
|
| GET | `/yard-art` | | `{ yardArt }`: the Yard's art. `state` is `ready`, `missing`, `downloading` (with `received` bytes), `installing`, `failed` or `unavailable` (each with `error`), or `cancelled`; `size` is the pack's size in bytes. A checkout serves `web/yard/assets` and is always `ready`. `/yard/assets/` answers 404 until the art is `ready`. |
|
|
619
633
|
| POST | `/yard-art/download` | | `202 { yardArt }`: starts the one download of the art pack named by the package, unless it is running or installed. Progress and the outcome arrive as `yard-art.updated`. |
|
|
620
634
|
| POST | `/yard-art/cancel` | | `{ yardArt }`: stops the download for every client; the state becomes `cancelled`. |
|
|
635
|
+
| POST | `/yard/fireworks` | | `{ ok, pages }`: fireworks over the Yard of every open page, sent as `yard.fireworks`. `pages` is how many pages were listening. |
|
|
621
636
|
| GET | `/changelog` | | `{ refreshing, okAt, error, releases }`: Agent Guild's own releases, newest first, each `{ version, url, publishedAt, sections: [{ title, changes }] }`, where `title` may be null and a change is a list of text runs. A `changelog.updated` event follows a refresh. |
|
|
622
637
|
| GET | `/providers/:id/history?account=&limit=&cwd=&q=` | | `{ history }`: a History object for one account (default `default`). |
|
|
623
638
|
| GET | `/providers/:id/history/detail?account=&id=&cursor=` | | `{ detail }`; see [Saved messages](#saved-messages). |
|
|
@@ -664,7 +679,11 @@ All paths are under `/api/v1`.
|
|
|
664
679
|
`cwd` defaults to the user's home folder, and a leading `~` is expanded.
|
|
665
680
|
`args` are appended to the provider's configured arguments. `resume` is an
|
|
666
681
|
id or name of one of the tool's own sessions; one that starts with `-` is
|
|
667
|
-
refused with 400 `bad_resume`. `account` defaults to `default`.
|
|
682
|
+
refused with 400 `bad_resume`. `account` defaults to `default`. A session
|
|
683
|
+
resumed without a `name` gets back the name a card holding it was given.
|
|
684
|
+
A name given to a session, by `name` or a rename, is kept for every tool
|
|
685
|
+
session the card holds; renaming the card to its default name drops it for
|
|
686
|
+
the tool session it holds then.
|
|
668
687
|
|
|
669
688
|
The `POST /sessions/:id/…` report routes (`agents`, `model`, `tool-session`,
|
|
670
689
|
`context`, `reply`, `reporting`, `docker` and `shells`) also accept the per-session
|
|
@@ -695,6 +714,7 @@ but `{ type: "ping" }`, which the manager answers with `{ type: "pong" }`.
|
|
|
695
714
|
| `{ type: "environment.updated", environment }` | An environment check started or finished. |
|
|
696
715
|
| `{ type: "news.updated" }` | Fetch `/news` again. |
|
|
697
716
|
| `{ type: "yard-art.updated", yardArt }` | The Yard's art changed state or made download progress, as in `/yard-art`. |
|
|
717
|
+
| `{ type: "yard.fireworks" }` | Fireworks over the Yard, when it is on show. |
|
|
698
718
|
| `{ type: "changelog.updated" }` | Fetch `/changelog` again. |
|
|
699
719
|
| `{ type: "github.updated" }` | Fetch `/github` again. |
|
|
700
720
|
| `{ type: "remote-access.updated" }` | Fetch `/remote-access` again. |
|
|
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.20.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",
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
// or the binary conversation/implicit stores as conversation text.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { createHash } from 'node:crypto';
|
|
6
5
|
import { antigravityUserConversation } from '../report/hooks.mjs';
|
|
7
6
|
|
|
8
7
|
export const ANTIGRAVITY_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
-
export const TRANSCRIPT_PAGE_BYTES = 256 * 1024;
|
|
10
8
|
|
|
11
9
|
export function historyError(status, code, message) {
|
|
12
10
|
return Object.assign(new Error(message), { status, code });
|
|
@@ -15,14 +13,13 @@ export function historyError(status, code, message) {
|
|
|
15
13
|
/** Reject links below the provider home, including a linked conversation or logs folder. */
|
|
16
14
|
export async function transcriptFile(dir, id) {
|
|
17
15
|
if (!ANTIGRAVITY_ID.test(id)) throw historyError(400, 'bad_history_id', 'Invalid conversation id.');
|
|
18
|
-
|
|
16
|
+
const unavailable = () => historyError(404, 'history_unavailable', 'This conversation transcript is unavailable.');
|
|
17
|
+
let file = await fs.promises.realpath(dir).catch(() => { throw unavailable(); });
|
|
19
18
|
const parts = ['brain', id, '.system_generated', 'logs', 'transcript.jsonl'];
|
|
20
19
|
for (const [i, part] of parts.entries()) {
|
|
21
20
|
file = path.join(file, part);
|
|
22
|
-
const stat = await fs.promises.lstat(file);
|
|
23
|
-
if (stat.isSymbolicLink() || (i === parts.length - 1 ? !stat.isFile() : !stat.isDirectory()))
|
|
24
|
-
throw historyError(404, 'history_unavailable', 'This conversation transcript is unavailable.');
|
|
25
|
-
}
|
|
21
|
+
const stat = await fs.promises.lstat(file).catch(() => { throw unavailable(); });
|
|
22
|
+
if (stat.isSymbolicLink() || (i === parts.length - 1 ? !stat.isFile() : !stat.isDirectory())) throw unavailable();
|
|
26
23
|
}
|
|
27
24
|
return file;
|
|
28
25
|
}
|
|
@@ -39,66 +36,3 @@ export function transcriptMessage(record) {
|
|
|
39
36
|
}
|
|
40
37
|
return text?.trim() ? { role, text: text.trim() } : null;
|
|
41
38
|
}
|
|
42
|
-
|
|
43
|
-
/** One bounded byte page. A cursor pins the file revision; partial final lines wait for Refresh. */
|
|
44
|
-
export async function readAntigravityTranscript(dir, id, cursor = null) {
|
|
45
|
-
let handle;
|
|
46
|
-
try {
|
|
47
|
-
handle = await fs.promises.open(await transcriptFile(dir, id), 'r');
|
|
48
|
-
const stat = await handle.stat();
|
|
49
|
-
const revision = createHash('sha256').update(`${id}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`).digest('hex').slice(0, 24);
|
|
50
|
-
let offset = 0, skipping = false;
|
|
51
|
-
if (cursor) {
|
|
52
|
-
if (typeof cursor !== 'string' || cursor.length > 256) {
|
|
53
|
-
throw historyError(400, 'bad_history_cursor', 'Invalid conversation page.');
|
|
54
|
-
}
|
|
55
|
-
let value;
|
|
56
|
-
try { value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')); } catch { /* invalid below */ }
|
|
57
|
-
if (!value || !Number.isSafeInteger(value.offset)
|
|
58
|
-
|| value.offset < 0 || typeof value.skipping !== 'boolean' || typeof value.revision !== 'string') {
|
|
59
|
-
throw historyError(400, 'bad_history_cursor', 'Invalid conversation page.');
|
|
60
|
-
}
|
|
61
|
-
if (value.revision !== revision) throw historyError(409, 'history_changed', 'The conversation changed. Refresh to read it again.');
|
|
62
|
-
if (value.offset > stat.size) throw historyError(400, 'bad_history_cursor', 'Invalid conversation page.');
|
|
63
|
-
({ offset, skipping } = value);
|
|
64
|
-
}
|
|
65
|
-
// Check the first record on every request, including requests using a supplied cursor.
|
|
66
|
-
const head = Buffer.alloc(Math.min(stat.size, TRANSCRIPT_PAGE_BYTES));
|
|
67
|
-
const firstRead = await handle.read(head, 0, head.length, 0);
|
|
68
|
-
const headEnd = head.subarray(0, firstRead.bytesRead).indexOf(10);
|
|
69
|
-
let first;
|
|
70
|
-
try { first = JSON.parse(head.subarray(0, headEnd < 0 ? firstRead.bytesRead : headEnd).toString('utf8')); } catch { /* unavailable */ }
|
|
71
|
-
if (!antigravityUserConversation(first)) throw historyError(404, 'history_unavailable', 'This conversation transcript is unavailable.');
|
|
72
|
-
const buffer = offset === 0 ? head : Buffer.alloc(Math.min(stat.size - offset, TRANSCRIPT_PAGE_BYTES));
|
|
73
|
-
const bytesRead = offset === 0 ? firstRead.bytesRead : (await handle.read(buffer, 0, buffer.length, offset)).bytesRead;
|
|
74
|
-
const data = buffer.subarray(0, bytesRead);
|
|
75
|
-
const messages = [];
|
|
76
|
-
let start = 0, omitted = false;
|
|
77
|
-
for (let end; (end = data.indexOf(10, start)) >= 0;) {
|
|
78
|
-
if (skipping) { skipping = false; omitted = true; }
|
|
79
|
-
else if (end > start) {
|
|
80
|
-
try {
|
|
81
|
-
const message = transcriptMessage(JSON.parse(data.subarray(start, end).toString('utf8')));
|
|
82
|
-
if (message) messages.push(message);
|
|
83
|
-
} catch { omitted = true; }
|
|
84
|
-
}
|
|
85
|
-
start = end + 1;
|
|
86
|
-
}
|
|
87
|
-
const atEnd = offset + bytesRead >= stat.size;
|
|
88
|
-
const incomplete = atEnd && start < bytesRead;
|
|
89
|
-
// Leave a split record for the next page. An oversized record is skipped in bounded pieces.
|
|
90
|
-
if (!atEnd && start === 0) { start = bytesRead; skipping = true; omitted = true; }
|
|
91
|
-
const nextCursor = !atEnd && bytesRead > 0
|
|
92
|
-
? Buffer.from(JSON.stringify({ offset: offset + start, skipping, revision })).toString('base64url') : null;
|
|
93
|
-
const after = await handle.stat();
|
|
94
|
-
if (after.size !== stat.size || after.mtimeMs !== stat.mtimeMs || after.ctimeMs !== stat.ctimeMs) {
|
|
95
|
-
throw historyError(409, 'history_changed', 'The conversation changed. Refresh to read it again.');
|
|
96
|
-
}
|
|
97
|
-
return { id, messages, nextCursor, incomplete, omitted };
|
|
98
|
-
} catch (err) {
|
|
99
|
-
if (err.status) throw err;
|
|
100
|
-
throw historyError(404, 'history_unavailable', 'This conversation transcript is unavailable.');
|
|
101
|
-
} finally {
|
|
102
|
-
await handle?.close();
|
|
103
|
-
}
|
|
104
|
-
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Names given to cards, by provider, account and the tool's own session id.
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export const CARD_NAMES_LIMIT = 5000;
|
|
8
|
+
const FILE_LIMIT = 4 * 1024 * 1024;
|
|
9
|
+
|
|
10
|
+
const key = (providerId, accountId, toolSessionId) => `${providerId}\0${accountId}\0${toolSessionId}`;
|
|
11
|
+
|
|
12
|
+
function readNames(file) {
|
|
13
|
+
let stat;
|
|
14
|
+
try { stat = fs.statSync(file); } catch (error) {
|
|
15
|
+
if (error.code === 'ENOENT') return [];
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
if (!stat.isFile() || stat.size > FILE_LIMIT) throw new Error('not a names file');
|
|
19
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
20
|
+
if (value?.version !== 1 || !Array.isArray(value.names)) throw new Error('not a names file');
|
|
21
|
+
return value.names.filter((n) => n && typeof n.provider === 'string' && typeof n.account === 'string' && typeof n.id === 'string'
|
|
22
|
+
&& typeof n.name === 'string' && n.name && typeof n.at === 'string');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function writeNames(file, names) {
|
|
26
|
+
const contents = `${JSON.stringify({ version: 1, names }, null, 2)}\n`;
|
|
27
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
28
|
+
const next = `${file}.${randomUUID()}.tmp`;
|
|
29
|
+
let descriptor;
|
|
30
|
+
try {
|
|
31
|
+
descriptor = fs.openSync(next, 'wx', 0o600);
|
|
32
|
+
fs.writeFileSync(descriptor, contents);
|
|
33
|
+
fs.fsyncSync(descriptor);
|
|
34
|
+
fs.closeSync(descriptor);
|
|
35
|
+
descriptor = undefined;
|
|
36
|
+
fs.renameSync(next, file);
|
|
37
|
+
} finally {
|
|
38
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
39
|
+
try { fs.unlinkSync(next); } catch { /* renamed, or never created */ }
|
|
40
|
+
}
|
|
41
|
+
try { fs.chmodSync(file, 0o600); } catch { /* not supported on Windows */ }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} [file] omit for an in-memory store (tests). The real manager passes its names file.
|
|
46
|
+
* @param {{ limit?: number, warn?: (message: string) => void, now?: () => string }} [options]
|
|
47
|
+
*/
|
|
48
|
+
export function createCardNames(file, { limit = CARD_NAMES_LIMIT, warn = (message) => console.warn(message), now = () => new Date().toISOString() } = {}) {
|
|
49
|
+
let names = null;
|
|
50
|
+
let warned = false;
|
|
51
|
+
|
|
52
|
+
/** An unreadable file is read again next time and never overwritten, so fixing or removing it needs no restart. */
|
|
53
|
+
function load() {
|
|
54
|
+
if (names) return names;
|
|
55
|
+
if (!file) return (names = new Map());
|
|
56
|
+
try {
|
|
57
|
+
names = new Map(readNames(file).map((n) => [key(n.provider, n.account, n.id), n]));
|
|
58
|
+
warned = false;
|
|
59
|
+
} catch {
|
|
60
|
+
if (!warned) warn(`[names] ${file} cannot be read, so card names are not kept until it is fixed or removed.`);
|
|
61
|
+
warned = true;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return names;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function save(current) {
|
|
68
|
+
if (!file) return true;
|
|
69
|
+
try {
|
|
70
|
+
writeNames(file, [...current.values()]);
|
|
71
|
+
return true;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
warn(`[names] could not save card names: ${err.message}`);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
get(providerId, accountId, toolSessionId) {
|
|
80
|
+
return load()?.get(key(providerId, accountId, toolSessionId))?.name ?? null;
|
|
81
|
+
},
|
|
82
|
+
/** True once the name is stored. The cache changes only with the file, so a failed save is retried by the next call. */
|
|
83
|
+
set(providerId, accountId, toolSessionId, name) {
|
|
84
|
+
const current = load();
|
|
85
|
+
if (!current) return false;
|
|
86
|
+
const k = key(providerId, accountId, toolSessionId);
|
|
87
|
+
if (current.get(k)?.name === name) return true;
|
|
88
|
+
const next = new Map(current);
|
|
89
|
+
// Newest last, so the oldest are dropped first.
|
|
90
|
+
next.delete(k);
|
|
91
|
+
next.set(k, { provider: providerId, account: accountId, id: toolSessionId, name, at: now() });
|
|
92
|
+
while (next.size > limit) next.delete(next.keys().next().value);
|
|
93
|
+
if (!save(next)) return false;
|
|
94
|
+
names = next;
|
|
95
|
+
return true;
|
|
96
|
+
},
|
|
97
|
+
/** True once no name is stored. */
|
|
98
|
+
delete(providerId, accountId, toolSessionId) {
|
|
99
|
+
const current = load();
|
|
100
|
+
if (!current) return false;
|
|
101
|
+
const k = key(providerId, accountId, toolSessionId);
|
|
102
|
+
if (!current.has(k)) return true;
|
|
103
|
+
const next = new Map(current);
|
|
104
|
+
next.delete(k);
|
|
105
|
+
if (!save(next)) return false;
|
|
106
|
+
names = next;
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
package/src/manager/config.mjs
CHANGED
|
@@ -47,6 +47,7 @@ export const paths = {
|
|
|
47
47
|
get remoteAccess() { return path.join(dataDir(), 'remote-access.json'); },
|
|
48
48
|
/** The shared notepad. A missing file means it was never stored. */
|
|
49
49
|
get notes() { return path.join(dataDir(), 'notes.json'); },
|
|
50
|
+
get cardNames() { return path.join(dataDir(), 'card-names.json'); },
|
|
50
51
|
get log() { return path.join(dataDir(), 'manager.log'); },
|
|
51
52
|
/** Launchers for agent-guild-report, put first on every session's PATH. */
|
|
52
53
|
get shims() { return path.join(dataDir(), 'bin'); },
|