@oddessentials/agent-guild 0.5.0 → 0.7.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.
Files changed (86) hide show
  1. package/README.md +182 -135
  2. package/package.json +1 -1
  3. package/src/manager/changelog.mjs +195 -0
  4. package/src/manager/config.mjs +1 -0
  5. package/src/manager/github.mjs +842 -0
  6. package/src/manager/main.mjs +10 -3
  7. package/src/manager/news.mjs +5 -5
  8. package/src/manager/server.mjs +70 -8
  9. package/src/manager/session-manager.mjs +32 -3
  10. package/src/manager/session.mjs +4 -1
  11. package/web/app.js +1005 -11
  12. package/web/art/art.css +374 -159
  13. package/web/art/characters/anthropic/idle.avif +0 -0
  14. package/web/art/characters/anthropic/idle.webp +0 -0
  15. package/web/art/characters/anthropic/locked.avif +0 -0
  16. package/web/art/characters/anthropic/locked.webp +0 -0
  17. package/web/art/characters/anthropic/working.avif +0 -0
  18. package/web/art/characters/anthropic/working.webp +0 -0
  19. package/web/art/characters/google/idle.avif +0 -0
  20. package/web/art/characters/google/idle.webp +0 -0
  21. package/web/art/characters/google/locked.avif +0 -0
  22. package/web/art/characters/google/locked.webp +0 -0
  23. package/web/art/characters/google/working.avif +0 -0
  24. package/web/art/characters/google/working.webp +0 -0
  25. package/web/art/characters/openai/idle.avif +0 -0
  26. package/web/art/characters/openai/idle.webp +0 -0
  27. package/web/art/characters/openai/locked.avif +0 -0
  28. package/web/art/characters/openai/locked.webp +0 -0
  29. package/web/art/characters/openai/working.avif +0 -0
  30. package/web/art/characters/openai/working.webp +0 -0
  31. package/web/art/characters/shell/idle.avif +0 -0
  32. package/web/art/characters/shell/idle.webp +0 -0
  33. package/web/art/characters/shell/locked.avif +0 -0
  34. package/web/art/characters/shell/locked.webp +0 -0
  35. package/web/art/characters/shell/working.avif +0 -0
  36. package/web/art/characters/shell/working.webp +0 -0
  37. package/web/art/characters/xai/idle.avif +0 -0
  38. package/web/art/characters/xai/idle.webp +0 -0
  39. package/web/art/characters/xai/locked.avif +0 -0
  40. package/web/art/characters/xai/locked.webp +0 -0
  41. package/web/art/characters/xai/working.avif +0 -0
  42. package/web/art/characters/xai/working.webp +0 -0
  43. package/web/art/icons/anthropic.avif +0 -0
  44. package/web/art/icons/anthropic.webp +0 -0
  45. package/web/art/icons/google.avif +0 -0
  46. package/web/art/icons/google.webp +0 -0
  47. package/web/art/icons/openai.avif +0 -0
  48. package/web/art/icons/openai.webp +0 -0
  49. package/web/art/icons/shell.avif +0 -0
  50. package/web/art/icons/shell.webp +0 -0
  51. package/web/art/icons/xai.avif +0 -0
  52. package/web/art/icons/xai.webp +0 -0
  53. package/web/art/page-light.avif +0 -0
  54. package/web/art/page-light.webp +0 -0
  55. package/web/art/page.avif +0 -0
  56. package/web/art/page.webp +0 -0
  57. package/web/art/ui/empty-state.avif +0 -0
  58. package/web/art/ui/empty-state.webp +0 -0
  59. package/web/art/ui/favicon.png +0 -0
  60. package/web/art/ui/frame-ornate.png +0 -0
  61. package/web/art/ui/guild-crest.png +0 -0
  62. package/web/fonts/OFL.txt +93 -0
  63. package/web/fonts/cinzel.woff2 +0 -0
  64. package/web/index.html +101 -15
  65. package/web/styles.css +240 -71
  66. package/web/art/characters/anthropic/idle.png +0 -0
  67. package/web/art/characters/anthropic/locked.png +0 -0
  68. package/web/art/characters/anthropic/working.png +0 -0
  69. package/web/art/characters/google/idle.png +0 -0
  70. package/web/art/characters/google/locked.png +0 -0
  71. package/web/art/characters/google/working.png +0 -0
  72. package/web/art/characters/openai/idle.png +0 -0
  73. package/web/art/characters/openai/locked.png +0 -0
  74. package/web/art/characters/openai/working.png +0 -0
  75. package/web/art/characters/shell/idle.png +0 -0
  76. package/web/art/characters/shell/locked.png +0 -0
  77. package/web/art/characters/shell/working.png +0 -0
  78. package/web/art/characters/xai/idle.png +0 -0
  79. package/web/art/characters/xai/locked.png +0 -0
  80. package/web/art/characters/xai/working.png +0 -0
  81. package/web/art/icons/anthropic.png +0 -0
  82. package/web/art/icons/google.png +0 -0
  83. package/web/art/icons/openai.png +0 -0
  84. package/web/art/icons/shell.png +0 -0
  85. package/web/art/icons/xai.png +0 -0
  86. package/web/art/page.png +0 -0
package/README.md CHANGED
@@ -1,183 +1,230 @@
1
- # Agent Guild
2
-
3
- Agent Guild runs AI coding assistants side by side from one local web page.
4
- Pick a provider, get a real interactive terminal running that provider's
5
- coding tool, and see at a glance which sessions and agents are working.
6
-
7
- ![Agent Guild session cards](docs/screenshot.png)
8
-
9
- * **Start sessions from provider cards.** Anthropic (Claude Code), OpenAI
10
- (Codex CLI), Google (Gemini CLI), xAI (Grok Build), and a plain shell.
11
- **New** starts a fresh session; **Existing** lists the tool's own earlier
12
- sessions, read from where the tool keeps them, with their ids, and
13
- resumes one in its own folder, or any session by id. Providers whose tool is not
14
- installed are shown greyed out with an **Install** button that runs
15
- `npm install -g` in a session you can watch, or with install instructions
16
- when the tool is not an npm package. Installed tools show their version
17
- and an **Update** button; updating while that tool's sessions are running
18
- asks first, because it can break them.
19
- * **See what is left of your limits.** Claude Code, Codex CLI and Gemini
20
- CLI cards show a meter per rate-limit window (5-hour, 7-day, or per
21
- model, including Claude's weekly Fable window and the model and feature
22
- limits Codex meters separately) with the time until it resets, read from
23
- the tool's own sign-in.
24
- Claude's extra-usage spend and Codex's prepaid credit balance appear
25
- when the account has them. Other providers can supply a command that
26
- prints usage.
27
- * **Work in real terminals.** Each session is a card. Open it for a full
28
- interactive terminal: type instructions, answer prompts, watch output. Run
29
- as many sessions at once as you like.
30
- * **See agents and models at work.** When a coding tool reports helper
31
- agents, they appear as small icons on that session's card. The card also
32
- names the main model in use, reported by the tool or, failing that,
33
- spotted on its screen.
34
- * **Close the page any time.** A separate local session manager owns the
35
- terminals. Reopen the page and it reconnects to the same sessions with
36
- their screens intact, as long as the manager is still running. Sessions do
37
- not survive a computer restart. **Restart manager** in the top bar ends
38
- every session and starts a fresh manager, and the page reconnects to it by
39
- itself; **Stop manager** ends every session and leaves the manager
40
- stopped. Both ask first while any session is still running. The top bar
41
- also shows the version you are running.
42
- * **Stay current.** When a newer Agent Guild is on npm, an **Upgrade**
43
- button appears in the top bar and runs `npm install -g` in a session you
44
- can watch. Sessions keep running; once they are done, **Restart to use
45
- vX.Y.Z** in the top bar switches to the new version.
46
- * **Light or dark.** The page follows your system theme and the top-bar
47
- toggle switches it. The guild artwork is the dark theme.
48
-
49
- ## Requirements
50
-
51
- * Windows 10 1809 or later, or macOS 11 or later. Linux works for development.
52
- * Node.js 22 or newer.
53
- * Each coding tool you want to use, signed in on its own. Agent Guild can
54
- install the npm-packaged tools for you; it does not authenticate them.
55
-
56
- node-pty ships prebuilt binaries for Windows, macOS and Linux on x64 and
57
- arm64, so no compiler is needed. WSL is not required.
58
-
59
- ## Install and run
1
+ <p align="center">
2
+ <img src="docs/images/banner.webp" alt="Agent Guild: Work hard, play hard. An agentic UI that enhances instead of hinders." width="100%">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@oddessentials/agent-guild"><img src="https://img.shields.io/npm/v/@oddessentials/agent-guild?color=7c5cff&label=npm" alt="npm version"></a>
7
+ <a href="https://github.com/oddessentials/agent-guild/actions/workflows/release.yml"><img src="https://img.shields.io/github/actions/workflow/status/oddessentials/agent-guild/release.yml?branch=main&label=release" alt="Release status"></a>
8
+ <img src="https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux-e8c47c" alt="Windows, macOS and Linux">
9
+ <img src="https://img.shields.io/node/v/@oddessentials/agent-guild?color=4cc38a" alt="Node.js version">
10
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/oddessentials/agent-guild?color=a67cf6" alt="MIT license"></a>
11
+ </p>
12
+
13
+ **Agent Guild** runs Claude Code, Codex CLI, Gemini CLI, Grok Build and your
14
+ own shell side by side, in real terminals, from one local web page. Each
15
+ session is a card that shows what the tool is doing, which model it runs and
16
+ which helper agents it has summoned. Close the page whenever you like; the
17
+ sessions keep working.
18
+
19
+ <picture>
20
+ <source media="(prefers-color-scheme: light)" srcset="docs/images/overview-light.webp">
21
+ <img src="docs/images/overview-dark.webp" alt="Agent Guild with one card per coding tool and six sessions at work">
22
+ </picture>
23
+
24
+ ## Quick start
60
25
 
61
26
  ```sh
62
27
  npm install -g @oddessentials/agent-guild
63
28
  agent-guild
64
29
  ```
65
30
 
66
- `agent-guild open` starts the session manager in the background if needed and
67
- opens the page. The page URL carries an access token in its `#` fragment. The
68
- page stores it and then removes it from the address bar.
31
+ The page opens in your browser. Pick a provider, press **New**, and you are
32
+ in a live terminal.
33
+
34
+ **You need** Node.js 22 or newer on Windows 10 1809+, macOS 11+ or Linux
35
+ (x64 or arm64), and each coding tool you want to use, signed in on its own.
36
+ Agent Guild installs the tools for you but never handles your sign-in. No
37
+ compiler and no WSL are needed.
38
+
39
+ ## Features
40
+
41
+ **Every major coding CLI, one place.** Claude Code, Codex CLI, Gemini CLI,
42
+ Grok Build and a plain shell each get a card. A tool that is missing shows
43
+ **Install**, which runs the install in a session you can watch. An installed
44
+ tool shows its version and offers **Update** when a newer one is out, using
45
+ the same installer that put it there: npm, Homebrew, WinGet or the vendor's
46
+ own.
47
+
48
+ **Real terminals that outlive the page.** Every session is a full interactive
49
+ terminal: type instructions, answer prompts, watch output. Run as many as you
50
+ like. A local session manager owns them, so you can close or reload the page
51
+ and come back to the same screens.
52
+
53
+ <img src="docs/images/terminal.webp" alt="An open Claude Code session with its helper agents shown in the header">
54
+
55
+ **Agents and models at work.** Helper agents that a tool starts appear on its
56
+ card as familiars while they run, and the card names the model in use. The
57
+ character comes alive while the session works, and the session's level rises
58
+ with every hour it runs.
59
+
60
+ **Several subscriptions per tool.** Add a work account next to your personal
61
+ one and switch with a chip on the card. Each account keeps its own sign-in,
62
+ usage meters and sessions. See [Accounts](docs/configuration.md#accounts).
63
+
64
+ **See what is left of your limits.** Claude Code, Codex CLI and Gemini CLI
65
+ cards show a meter for each rate-limit window, such as 5-hour and 7-day, with
66
+ the time until it resets. They also show your plan, Claude's extra-usage
67
+ spend and Codex's credit balance when the account has them. Any other tool
68
+ can supply a command that prints its usage.
69
+
70
+ **Pick the right model.** Each card grades the tool's newest fully
71
+ benchmarked model on coding, intelligence and agentic work, from S to D. Open it to compare every
72
+ model the tool offers across 13 benchmarks: Artificial Analysis indexes and
73
+ Design Arena results for websites, UI, game dev, data visualization, 3D, SVG,
74
+ web apps, full stack and mobile. It also shows context size and price.
75
+
76
+ <img src="docs/images/models.webp" alt="The Claude Code models dialog with benchmark tiers for each model">
77
+
78
+ **Pick up where you left off.** **Existing…** lists the tool's own earlier
79
+ sessions, newest first, read from where the tool keeps them. Filter by title,
80
+ folder or id and resume one in its own folder, or resume any session by id.
81
+
82
+ <img src="docs/images/history.webp" alt="The Claude Code session history with a filter and Resume buttons">
83
+
84
+ **Agentic development news.** A newsfeed gathers about 20 sources on AI
85
+ coding, agents and local models, including vendor blogs, Hacker News, arXiv,
86
+ and releases of the tools you have installed. The five latest headlines
87
+ appear on the page; **All news** opens the full feed with News, Releases and Research
88
+ filters.
89
+
90
+ <img src="docs/images/news.webp" alt="The news panel with today's items from news sources and tool releases">
91
+
92
+ **Always current.** When a new Agent Guild is published, **Upgrade to
93
+ vX.Y.Z** installs it while your sessions keep running, and **Restart to use
94
+ vX.Y.Z** switches over when you are ready. The version badge opens
95
+ **What's new** with the notes of every release.
96
+
97
+ <img src="docs/images/whats-new.webp" alt="The What's new panel listing the release notes of each version">
98
+
99
+ **Light or dark.** The page follows your system theme, and the top-bar
100
+ toggle switches it.
101
+
102
+ ## Commands
69
103
 
70
104
  | Command | What it does |
71
105
  | --- | --- |
72
106
  | `agent-guild` or `agent-guild open` | Start the manager if needed and open the page. `--no-browser` prints the URL instead. |
73
107
  | `agent-guild status` | Show whether the manager is running and list its sessions. |
74
- | `agent-guild stop` | Stop the manager. This ends every session, without asking. The page's **Stop manager** button does the same and asks first while sessions are running. |
75
- | `agent-guild restart` | Stop the manager and start it again, on the version installed on disk. This ends every session, without asking. The page's **Restart manager** button does the same and asks first while sessions are running. |
108
+ | `agent-guild stop` | Stop the manager, ending every session. |
109
+ | `agent-guild restart` | Stop the manager and start it again on the version installed on disk, ending every session. |
110
+ | `agent-guild url` | Print the page URL with its access token. |
76
111
  | `agent-guild start` | Run the manager in the foreground, for debugging. |
77
- | `agent-guild url` | Print the page URL with its token. |
78
112
 
79
- **Changelog:** each version's changes are listed on the
80
- [Releases page](https://github.com/oddessentials/agent-guild/releases).
113
+ The page's **Restart manager** and **Stop manager** buttons do the same as
114
+ `restart` and `stop`, but ask first while sessions are running. The page also
115
+ asks before you close it with sessions running. Sessions end when the manager
116
+ stops or the computer restarts.
81
117
 
82
- ## Configure providers
118
+ ## Configuration
83
119
 
84
- Create `providers.json` in the data folder to change or add providers:
120
+ Nothing needs configuring. To add a provider, change a command, sign in with
121
+ more than one account or point usage meters at your own command, create a
122
+ `providers.json` in the data folder:
85
123
 
86
124
  | Platform | Data folder |
87
125
  | --- | --- |
88
126
  | Windows | `%APPDATA%\AgentGuild` |
89
127
  | macOS | `~/Library/Application Support/AgentGuild` |
128
+ | Linux | `~/.config/agent-guild` |
90
129
 
91
- Entries are merged with the built-in ones by `id`. A new `id` adds a
92
- provider, and `"enabled": false` hides one. Any field can be overridden for
93
- one platform under a `win32` or `darwin` key. See
94
- [examples/providers.json](examples/providers.json).
95
-
96
- | Field | Meaning |
97
- | --- | --- |
98
- | `id` | Lowercase identifier. |
99
- | `vendor`, `tool` | Names shown on the icon. |
100
- | `command`, `args` | What to run. `command` is looked up on PATH. `@shell` means the user's default shell. |
101
- | `package` | The tool's npm package, e.g. `@openai/codex`. Enables the **Install** button and the version check. |
102
- | `channels` | How an installed copy is recognised, so **Update** runs that installation's own updater. A copy installed by npm needs no entry. `brew.names` lists the tool's own Homebrew formula or cask names, e.g. `{ "brew": { "names": ["gemini-cli"] } }`, and `winget.id` is its WinGet package id. A provider you add must set these for its Homebrew or WinGet copy to get an **Update** button or a removal command; without them that copy shows as an unknown install with guidance only. `native.paths` are the launcher and folders the vendor's own installer uses, and `native.update` the arguments that make the tool update itself, e.g. `["update"]`. |
103
- | `versionArgs` | Arguments that make the command print its version, used instead of `args`, e.g. `["--version"]`. |
104
- | `usage` | Where the usage meters come from: `"claude"`, `"codex"`, `"gemini"`, `{ "command", "args" }` for a program that prints `{ "windows": [{ "label", "usedPercent", "resetsAt" }] }`, or `null` for none. |
105
- | `modelPattern` | Regular expression that finds the model name on the tool's screen when the tool does not report it. |
106
- | `resumeArgs` | Arguments that resume the tool's own session, with `{id}` standing for the id, e.g. `["--resume", "{id}"]`. Without it the card has no **Existing** button. |
107
- | `history` | Where the list of earlier sessions comes from: `"claude"`, `"codex"`, `"gemini"`, `"grok"` (the tool's own session files under its home folder), `{ "command", "args" }` for a program that prints `{ "sessions": [{ "id", "title", "cwd", "startedAt", "updatedAt" }] }`, or `null` for none, in which case **Existing** asks for an id. |
108
- | `env` | Extra environment variables for the tool. |
109
- | `accounts` | Further sign-ins of the tool, each in its own home folder, e.g. `[{ "id": "work", "label": "Work", "dir": "~/.claude-work" }]`. Without `dir`, the folder is `accounts/<provider>/<account>` in the data folder. The card shows one chip per account with its own meters, and a session starts under the chip picked; the tool signs in from inside the first session, and its reporting hooks are copied into the folder on first use. An entry with id `default` renames the tool's own sign-in. Needs `homeVar`. |
110
- | `homeVar` | The environment variable that moves the tool's home folder, e.g. `CLAUDE_CONFIG_DIR`. Set for Claude Code, Codex CLI, Gemini CLI and Grok Build by default. |
111
- | `accountEnv` | Further variables set for every account other than the default, with `{dir}` standing for the account's folder. By default Claude Code's secure-storage folder follows the account, and Gemini CLI keeps the account's sign-in in a file rather than the shared OS keychain. |
112
- | `hooks` | `{ "path", "example" }`: the hooks file inside the home folder and the file in `examples/` copied there for a new account. |
113
- | `color`, `monogram`, `icon` | Icon appearance. `icon` is a URL path; you can also drop `<id>.svg` into `web/icons/`. |
114
- | `install`, `docs` | Help shown when the tool is not installed. |
115
- | `usageUrl`, `billingUrl` | `https://` links to the vendor's usage and billing pages, shown on the card. The defaults point at the subscription pages; set your API console instead, or `null` to hide a link. Google's usage link opens AI Studio, which counts API-key usage only, not the Gemini CLI sign-in quota the card's meters show. |
116
-
117
- The page's "Working folder" field sets where new sessions start. It defaults
118
- to your home folder.
119
-
120
- Version checks, for the tools and for Agent Guild itself, ask the registry
121
- from npm's global configuration (the one `npm install -g` uses) about once
122
- an hour, and installs use the same registry. Set `AGENT_GUILD_NPM_REGISTRY`
123
- to override it for both, or `AGENT_GUILD_NO_UPDATE_CHECK=1` to skip the
124
- checks.
130
+ The full reference, including every field and environment variable, is in
131
+ [docs/configuration.md](docs/configuration.md).
125
132
 
126
133
  ## Show agents and models
127
134
 
128
- Agents are reported by the coding tool, not guessed from its output. Add
129
- the hooks from the matching file in [examples/](examples/) to Claude Code,
130
- Codex CLI, Gemini CLI or Grok Build: each sub-agent appears on the card
131
- while it runs, and the card shows the model in use. The hooks call
132
- `agent-guild-report`, which the manager puts on the PATH of every session it
133
- starts, so no global install is needed. Codex CLI runs no hook until you
134
- trust it (choose "Trust all and continue" when it starts, or run `/hooks`),
135
- and Claude Code runs none until you accept its workspace-trust prompt. Any
136
- tool or script can also report agents and the model with the
137
- `agent-guild-report` command or an escape sequence. See
135
+ Agents are reported by the coding tool, not guessed from its output. To see
136
+ them, add the hooks from the matching file in [examples/](examples/) to
137
+ Claude Code, Codex CLI, Gemini CLI or Grok Build. Each helper agent then
138
+ appears on the card while it runs, and the card shows the model in use.
139
+ Without hooks, the card still names the model when it is given with
140
+ `--model` or shown on the tool's screen.
141
+
142
+ The hooks call `agent-guild-report`, which the manager puts on the PATH of
143
+ every session, so nothing else needs installing. Two tools need a one-time
144
+ approval:
145
+
146
+ * Codex CLI runs no hook until you trust it: choose "Trust all and continue"
147
+ when it starts, or run `/hooks`.
148
+ * Claude Code runs none until you accept its workspace-trust prompt.
149
+
150
+ Any other tool or script can report agents and its model too. See
138
151
  [docs/agent-reporting.md](docs/agent-reporting.md).
139
152
 
140
- ## Security
153
+ ## Security and privacy
141
154
 
142
155
  * The manager listens on `127.0.0.1` only.
143
156
  * Every API call needs a random per-user token, stored in the data folder
144
- with owner-only permissions.
145
- * Requests with a foreign `Host` or `Origin` header are refused. This stops
146
- other websites from reaching the terminals through your browser.
157
+ with owner-only permissions. Anyone who can run programs as your user can
158
+ read it, as with any local developer tool.
159
+ * Requests with a foreign `Host` or `Origin` header are refused, so other
160
+ websites cannot reach your terminals through your browser.
147
161
  * Tools inside a session get a separate token that can only report agents
148
162
  for that session.
149
- * Usage meters are fetched by the manager with the coding tool's own
150
- sign-in (Claude Code's credentials, Codex CLI's `auth.json`, Gemini CLI's
151
- sign-in). The page only ever receives percentages. On macOS the first
152
- lookup may ask for keychain access to the "Claude Code-credentials" and
153
- "gemini-cli-oauth" items; choose Always Allow.
154
-
155
- Anyone who can run programs as your user can already read the token, as with
156
- any local developer tool.
163
+ * GitHub sign-ins, the SSH key Agent Guild makes for each GitHub account and
164
+ GitHub's SSH host keys are kept in the `github` folder of the data folder,
165
+ readable only by you. A clone uses only that key and those host keys; your
166
+ own `~/.ssh` and Git configuration are not read or changed.
167
+ * Usage meters are fetched by the manager with each tool's own sign-in. The
168
+ page only receives percentages. On macOS the first lookup may ask for
169
+ keychain access to the "Claude Code-credentials" and "gemini-cli-oauth"
170
+ items; choose Always Allow.
171
+
172
+ The manager makes these outbound requests, and none of them carry your code
173
+ or prompts:
174
+
175
+ | To | For | How often |
176
+ | --- | --- | --- |
177
+ | npm registry | Tool and Agent Guild version checks | About hourly; `AGENT_GUILD_NO_UPDATE_CHECK=1` turns them off |
178
+ | Anthropic, OpenAI and Google usage endpoints | Usage meters, with the tool's own sign-in | Every minute while the page is open |
179
+ | OpenRouter's public model list | Benchmarks | Every 6 hours |
180
+ | Public news feeds, Hacker News, arXiv and GitHub | The newsfeed | Every 30 minutes while the page is open |
181
+ | GitHub's releases API | What's new | Hourly |
182
+ | GitHub (sign-in, API, avatars and SSH) | Signing in to GitHub, listing your repositories, adding your SSH key and cloning | When you use **Clone from GitHub…** |
157
183
 
158
184
  ## Other front ends
159
185
 
160
- The page is only one client. The manager's API is documented in
161
- [docs/api.md](docs/api.md) so that another interface, such as a planned
162
- Unreal Engine version where provider characters and their workers stand in
163
- for the icons, can drive the same sessions. See
186
+ The page is one client of the manager's local API, documented in
187
+ [docs/api.md](docs/api.md). Another interface, such as a planned Unreal Engine
188
+ guild hall, can drive the same sessions at the same time. See
164
189
  [docs/architecture.md](docs/architecture.md).
165
190
 
166
191
  ## Development
167
192
 
168
193
  ```sh
194
+ git clone https://github.com/oddessentials/agent-guild.git
195
+ cd agent-guild
169
196
  npm install
197
+ npm start # open the page, starting a manager from this checkout if none runs
170
198
  npm test
171
199
  ```
172
200
 
173
- The tests start real managers and real pseudo-terminals, using a small fake
174
- coding tool in `tests/fixtures`. CI runs them on Windows, macOS and Linux.
201
+ * The tests start real managers and real pseudo-terminals, using a small fake
202
+ coding tool in `tests/fixtures`. CI runs them on Windows, macOS and Linux
203
+ with Node.js 22, 24 and 26, and installs the packed package on x64 and
204
+ arm64.
205
+ * In a checkout, `launchers/AgentGuild.cmd` (Windows) and
206
+ `launchers/AgentGuild.command` (macOS) start Agent Guild with a
207
+ double-click.
208
+ * `node docs/capture/capture.mjs --root <folder>` refreshes the screenshots
209
+ in `docs/images` from the real page, with demo sessions in place of real
210
+ tools. The cards show the folder's path, so pick a neutral one such as
211
+ `D:\code` or `/work`. It needs Chrome or Edge and leaves any running
212
+ manager alone. See the comment at the top of the script for options.
213
+ * Pull request titles follow
214
+ [Conventional Commits](https://www.conventionalcommits.org/). Merging to
215
+ `main` publishes a release to npm and GitHub when it includes a `feat`,
216
+ `fix`, `perf` or `revert`.
175
217
 
176
218
  ## Current limits
177
219
 
178
220
  * Sessions end when the manager stops or the computer restarts.
221
+ * Grok Build has no usage meter.
179
222
  * Gemini CLI usage meters read its sign-in where Gemini CLI keeps it:
180
- `oauth_creds.json`, or with `GEMINI_FORCE_ENCRYPTED_FILE_STORAGE=true`
181
- the OS keychain (macOS, or Linux with `secret-tool`) or its encrypted
182
- credentials file. A sign-in kept in the Windows Credential Manager
183
- cannot be read.
223
+ `oauth_creds.json`, or with `GEMINI_FORCE_ENCRYPTED_FILE_STORAGE=true` the
224
+ OS keychain (macOS, or Linux with `secret-tool`) or its encrypted
225
+ credentials file. A sign-in kept in the Windows Credential Manager cannot
226
+ be read.
227
+
228
+ ## License
229
+
230
+ [MIT](LICENSE) © Odd Essentials
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oddessentials/agent-guild",
3
- "version": "0.5.0",
3
+ "version": "0.7.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": {
@@ -0,0 +1,195 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { compareVersions } from './versions.mjs';
3
+ import { FeedError, USER_AGENT, decodeEntities, failure, readBody, refusal, webUrl } from './news.mjs';
4
+
5
+ const REPOSITORY = 'oddessentials/agent-guild';
6
+ const RELEASES_URL = `https://github.com/${REPOSITORY}/releases`;
7
+ const API_URL = `https://api.github.com/repos/${REPOSITORY}/releases?per_page=30`;
8
+ const TTL_MS = 60 * 60 * 1000;
9
+ const RETRY_MS = 10 * 60 * 1000;
10
+ const MISSING_MS = 2 * 60 * 1000;
11
+ const FETCH_TIMEOUT_MS = 20000;
12
+ const MAX_BYTES = 5 * 1024 * 1024;
13
+ const MAX_CHANGES = 100;
14
+ const MAX_NOTES = 20000;
15
+ const MAX_LINE = 1000;
16
+ const RELEASE_TAG = /^v?(\d+\.\d+\.\d+)$/;
17
+ const VERSION_HEADING = /^v?\d+\.\d+\.\d+(?![\d.])/;
18
+ const INSTALL_STEPS = /^install or update$/i;
19
+ const COMMIT_LINK = /\s*\(\[[0-9a-f]{7,40}\]\([^)\s]*\)\)/gi;
20
+ const INLINE = /\*\*(.+?)\*\*|`([^`]+)`|!\[[^\]]*\]\([^)]*\)|<(https?:\/\/[^>\s]+)>|\[([^\]]+)\]\(([^)\s]+)\)|<\/?[a-z][^>]*>/gi;
21
+
22
+ const iso = (time) => (time ? new Date(time).toISOString() : null);
23
+
24
+ function textRun(text, strong) {
25
+ return strong ? { text: decodeEntities(text), strong: true } : { text: decodeEntities(text) };
26
+ }
27
+
28
+ function inline(source, strong = false) {
29
+ const runs = [];
30
+ let at = 0;
31
+ for (const match of source.matchAll(INLINE)) {
32
+ if (match.index > at) runs.push(textRun(source.slice(at, match.index), strong));
33
+ const [, bold, code, autolink, label, href] = match;
34
+ if (bold !== undefined) {
35
+ runs.push(...inline(bold, true));
36
+ } else if (code !== undefined) {
37
+ runs.push({ ...(strong && { strong: true }), text: code, code: true });
38
+ } else if (autolink !== undefined || label !== undefined) {
39
+ const url = webUrl(autolink ?? href);
40
+ runs.push({ ...textRun(label ?? autolink, strong), ...(url && { url }) });
41
+ }
42
+ at = match.index + match[0].length;
43
+ }
44
+ if (at < source.length) runs.push(textRun(source.slice(at), strong));
45
+ return runs;
46
+ }
47
+
48
+ const sameStyle = (a, b) => !a.url && !b.url && Boolean(a.code) === Boolean(b.code) && Boolean(a.strong) === Boolean(b.strong);
49
+
50
+ function tidy(runs) {
51
+ const tidied = [];
52
+ for (const run of runs) {
53
+ const text = run.text.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ');
54
+ if (!text) continue;
55
+ const last = tidied.at(-1);
56
+ if (last && sameStyle(last, run)) last.text += text;
57
+ else tidied.push({ ...run, text });
58
+ }
59
+ if (tidied.length) {
60
+ tidied[0].text = tidied[0].text.trimStart();
61
+ tidied.at(-1).text = tidied.at(-1).text.trimEnd();
62
+ }
63
+ return tidied.filter((run) => run.text);
64
+ }
65
+
66
+ function plain(source) {
67
+ return tidy(inline(source)).map((run) => run.text).join('');
68
+ }
69
+
70
+ export function parseNotes(markdown) {
71
+ const sections = [];
72
+ let section = null;
73
+ let change = null;
74
+ let skipping = false;
75
+ let fenced = false;
76
+ let count = 0;
77
+ for (const whole of String(markdown ?? '').slice(0, MAX_NOTES).replace(/<!--[\s\S]*?(?:-->|$)/g, '').split(/\r?\n/)) {
78
+ const line = whole.slice(0, MAX_LINE);
79
+ if (/^\s*(?:```|~~~)/.test(line)) {
80
+ fenced = !fenced;
81
+ change = null;
82
+ continue;
83
+ }
84
+ if (fenced) continue;
85
+ const heading = /^\s{0,3}#{1,6}\s+(.*?)(?:\s+#+)?\s*$/.exec(line);
86
+ if (heading) {
87
+ const title = plain(heading[1]);
88
+ change = null;
89
+ section = null;
90
+ skipping = INSTALL_STEPS.test(title);
91
+ if (title && !skipping && !VERSION_HEADING.test(title)) sections.push(section = { title, changes: [] });
92
+ continue;
93
+ }
94
+ if (skipping) continue;
95
+ if (/^\s*([-*_])(?:\s*\1){2,}\s*$/.test(line)) {
96
+ change = null;
97
+ continue;
98
+ }
99
+ const bullet = /^\s*(?:[-*+]|\d{1,9}[.)])\s+(.*)$/.exec(line);
100
+ const body = (bullet ? bullet[1] : line.replace(/^\s*(?:>\s?)+/, '')).trim();
101
+ if (!body) {
102
+ change = null;
103
+ continue;
104
+ }
105
+ const runs = tidy(inline(body.replace(COMMIT_LINK, '')));
106
+ if (runs.length === 0) continue;
107
+ if (!bullet && change) {
108
+ change.splice(0, change.length, ...tidy([...change, { text: ' ' }, ...runs]));
109
+ continue;
110
+ }
111
+ if (count === MAX_CHANGES) break;
112
+ if (!section) sections.push(section = { title: null, changes: [] });
113
+ section.changes.push(change = runs);
114
+ count++;
115
+ }
116
+ return sections.filter((s) => s.changes.length > 0);
117
+ }
118
+
119
+ export function parseReleases(json) {
120
+ const list = JSON.parse(json);
121
+ if (!Array.isArray(list)) throw new FeedError('sent no releases');
122
+ const releases = new Map();
123
+ for (const release of list) {
124
+ if (!release || typeof release !== 'object' || release.draft || release.prerelease) continue;
125
+ const tag = String(release.tag_name ?? '');
126
+ const version = RELEASE_TAG.exec(tag)?.[1];
127
+ if (!version || releases.has(version)) continue;
128
+ const published = Date.parse(release.published_at);
129
+ releases.set(version, {
130
+ version,
131
+ url: webUrl(String(release.html_url ?? '')) ?? `${RELEASES_URL}/tag/${encodeURIComponent(tag)}`,
132
+ publishedAt: Number.isFinite(published) ? new Date(published).toISOString() : null,
133
+ sections: parseNotes(typeof release.body === 'string' ? release.body : ''),
134
+ });
135
+ }
136
+ return [...releases.values()].sort((a, b) => compareVersions(b.version, a.version));
137
+ }
138
+
139
+ export class Changelog extends EventEmitter {
140
+ constructor({
141
+ latest = () => null, fetchImpl = fetch, url = API_URL,
142
+ ttlMs = TTL_MS, retryMs = RETRY_MS, missingMs = MISSING_MS, timeoutMs = FETCH_TIMEOUT_MS, maxBytes = MAX_BYTES,
143
+ } = {}) {
144
+ super();
145
+ this.latest = latest;
146
+ this.fetchImpl = fetchImpl;
147
+ this.url = url;
148
+ this.ttlMs = ttlMs;
149
+ this.retryMs = retryMs;
150
+ this.missingMs = missingMs;
151
+ this.timeoutMs = timeoutMs;
152
+ this.maxBytes = maxBytes;
153
+ this.releases = [];
154
+ this.etag = null;
155
+ this.checkedAt = 0;
156
+ this.okAt = 0;
157
+ this.error = null;
158
+ this.refreshing = null;
159
+ }
160
+
161
+ snapshot() {
162
+ this._refreshDue();
163
+ return { refreshing: Boolean(this.refreshing), okAt: iso(this.okAt), error: this.error, releases: this.releases };
164
+ }
165
+
166
+ _refreshDue() {
167
+ if (this.refreshing) return;
168
+ const latest = this.latest();
169
+ const missing = Boolean(latest) && !this.releases.some((release) => release.version === latest);
170
+ const wait = this.error ? this.retryMs : missing ? this.missingMs : this.ttlMs;
171
+ if (Date.now() - this.checkedAt < wait) return;
172
+ this.refreshing = this._refresh().finally(() => {
173
+ this.refreshing = null;
174
+ this.emit('updated');
175
+ });
176
+ }
177
+
178
+ async _refresh() {
179
+ this.checkedAt = Date.now();
180
+ try {
181
+ const headers = { 'User-Agent': USER_AGENT, Accept: 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28' };
182
+ if (this.etag) headers['If-None-Match'] = this.etag;
183
+ const res = await this.fetchImpl(this.url, { headers, signal: AbortSignal.timeout(this.timeoutMs) });
184
+ if (res.status !== 304) {
185
+ if (!res.ok) throw new FeedError(refusal(res));
186
+ this.releases = parseReleases(await readBody(res, this.maxBytes));
187
+ this.etag = res.headers.get('etag');
188
+ }
189
+ this.error = null;
190
+ this.okAt = Date.now();
191
+ } catch (err) {
192
+ this.error = failure(err, this.timeoutMs);
193
+ }
194
+ }
195
+ }
@@ -43,6 +43,7 @@ export const paths = {
43
43
  get runtime() { return path.join(dataDir(), 'manager.json'); },
44
44
  get providers() { return path.join(dataDir(), 'providers.json'); },
45
45
  get accounts() { return path.join(dataDir(), 'accounts'); },
46
+ get github() { return path.join(dataDir(), 'github'); },
46
47
  get log() { return path.join(dataDir(), 'manager.log'); },
47
48
  /** Launchers for agent-guild-report, put first on every session's PATH. */
48
49
  get shims() { return path.join(dataDir(), 'bin'); },