@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 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
- apps for [Apple Vision Pro](https://apps.apple.com/us/app/agent-guild-remote/id6825451331),
62
- iPhone and iPad (beta) and
63
- [Android](https://github.com/oddessentials/agent-guild-android/releases/latest).
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). |
@@ -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) 开源。
@@ -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) 開源。
@@ -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. `accounts` lists the sign-ins the tool can run
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`); `title` is the session's name or first
273
- prompt, or null; `cwd` is the folder the session ran in, or null. Claude Code
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 (title, id
281
- or folder) **before** applying `limit`; `total` counts matching entries.
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
- Follow `nextCursor` until it is null, even when a page has no visible
301
- messages. `omitted` says records were left out; `incomplete` says the last one
302
- was partial, and a later read may complete it. 409 `history_changed` means the
303
- transcript changed: start again without a cursor.
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. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddessentials/agent-guild",
3
- "version": "1.18.0",
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
- let file = await fs.promises.realpath(dir);
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
+ }
@@ -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'); },