@bill10/agent-007 0.6.2

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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +222 -0
  3. package/VERSION +1 -0
  4. package/bin/adduser.js +69 -0
  5. package/bin/agent-007.js +88 -0
  6. package/lib/cron.js +189 -0
  7. package/lib/helpers.js +541 -0
  8. package/lib/jobs.js +965 -0
  9. package/package.json +63 -0
  10. package/public/app.js +650 -0
  11. package/public/assets/characters/LICENSE +21 -0
  12. package/public/assets/characters/char_0.png +0 -0
  13. package/public/assets/characters/char_1.png +0 -0
  14. package/public/assets/characters/char_2.png +0 -0
  15. package/public/assets/characters/char_3.png +0 -0
  16. package/public/assets/characters/char_4.png +0 -0
  17. package/public/assets/characters/char_5.png +0 -0
  18. package/public/assets/furniture/bookshelf.png +0 -0
  19. package/public/assets/furniture/cactus.png +0 -0
  20. package/public/assets/furniture/chair_back.png +0 -0
  21. package/public/assets/furniture/chair_front.png +0 -0
  22. package/public/assets/furniture/chair_side.png +0 -0
  23. package/public/assets/furniture/coffee.png +0 -0
  24. package/public/assets/furniture/coffee_table.png +0 -0
  25. package/public/assets/furniture/desk.png +0 -0
  26. package/public/assets/furniture/desk2.png +0 -0
  27. package/public/assets/furniture/plant_2.png +0 -0
  28. package/public/assets/furniture/sofa_front.png +0 -0
  29. package/public/assets/furniture/sofa_side.png +0 -0
  30. package/public/assets/furniture/table_front.png +0 -0
  31. package/public/index.html +245 -0
  32. package/public/modules/auth.js +83 -0
  33. package/public/modules/explorer.js +760 -0
  34. package/public/modules/jobs.js +971 -0
  35. package/public/modules/office.js +2154 -0
  36. package/public/modules/paths.js +20 -0
  37. package/public/modules/shortcuts.js +54 -0
  38. package/public/modules/state.js +75 -0
  39. package/public/modules/terminal.js +651 -0
  40. package/public/modules/voice.js +393 -0
  41. package/public/modules/ws.js +56 -0
  42. package/public/style.css +1843 -0
  43. package/server/agent-mcp-bridge.js +45 -0
  44. package/server/agent-mcp.js +184 -0
  45. package/server/agent-transcripts.js +195 -0
  46. package/server/approvals.js +155 -0
  47. package/server/auth.js +162 -0
  48. package/server/billion.js +176 -0
  49. package/server/claude-trust.js +66 -0
  50. package/server/command-path.js +102 -0
  51. package/server/config.js +184 -0
  52. package/server/direct-run.js +33 -0
  53. package/server/git.js +630 -0
  54. package/server/http.js +276 -0
  55. package/server/jobs.js +2044 -0
  56. package/server/mcp.js +596 -0
  57. package/server/messages.js +319 -0
  58. package/server/permission-hook.js +47 -0
  59. package/server/pty.js +360 -0
  60. package/server/state.js +104 -0
  61. package/server/ws.js +583 -0
  62. package/server.js +306 -0
  63. package/templates/billion/COMPANY.md +14 -0
  64. package/templates/billion/STATE.md +17 -0
  65. package/templates/billion/charter.md +232 -0
  66. package/templates/billion/owner.md +11 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Agent 007 Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,222 @@
1
+ # Agent 007
2
+
3
+ [![Tests (Ubuntu)](https://github.com/bill10/agent-007/actions/workflows/test-ubuntu.yml/badge.svg)](https://github.com/bill10/agent-007/actions/workflows/test-ubuntu.yml)
4
+ [![Tests (Windows)](https://github.com/bill10/agent-007/actions/workflows/test-windows.yml/badge.svg)](https://github.com/bill10/agent-007/actions/workflows/test-windows.yml)
5
+
6
+ **Queue coding jobs, walk away, review the PRs.**
7
+
8
+ Agents run in parallel, each in its own git worktree; one boss agent runs the board for you; and a pixel office shows who's working and who's waiting on you.
9
+
10
+ ![An agent walks to its desk and starts work, a job card is posted and dispatched to a second desk, and an agent turns orange when it stops to ask a question](docs/demo.gif)
11
+
12
+ *Recorded from the running app. If the capture does not load, there is a [still screenshot](docs/screenshot.png).*
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ npx @bill10/agent-007
18
+ ```
19
+
20
+ Or install it globally with `npm i -g @bill10/agent-007`, then run `agent-007`.
21
+
22
+ Or run it from a clone:
23
+
24
+ ```bash
25
+ git clone https://github.com/bill10/agent-007.git && cd agent-007
26
+ npm install
27
+ npm start
28
+ ```
29
+
30
+ Open [http://localhost:7007](http://localhost:7007). Click **+ Job** to queue work on the board, or **+ Agent** to start one by hand -- preset buttons (Claude Code, Codex, Gemini, Bash; PowerShell on a Windows server) fill in the command, or type your own under Advanced. Needs Node.js 20.12+ and Git ([full requirements](#requirements)).
31
+
32
+ ## Highlights
33
+
34
+ - **A job board, not a babysitting job** -- Each card gets a fresh agent on its own worktree and branch. It moves To do -> In progress -> Review on its own and lands as a pull request (or a summary, for work that isn't code). Cards can also run on a cron schedule.
35
+ - **Billion, the one agent you talk to** -- Give it a mission and it plans, posts cards, reviews what comes back, merges PRs and answers its workers' permission requests. It comes to you only for money, access, anything irreversible and real forks in direction.
36
+ - **Your agents, your subscriptions** -- Claude Code, Codex, any terminal agent is supported -- use your existing subscriptions, no extra charge.
37
+ - **See everything at a glance** -- Every agent gets a desk in the pixel office: facing the screen while it works, turning to face you when it needs you. One window for every repo, with live terminals, a file explorer and inline diffs.
38
+ - **Agents that talk to each other** -- Tell one to "add that to the job board" or "ask Viper what it changed", and it does it over MCP.
39
+
40
+ Everything else -- scheduled jobs, phone layout, voice input, themes, and the details and caveats of each feature -- is in [docs/FEATURES.md](docs/FEATURES.md).
41
+
42
+ ## Keyboard Shortcuts
43
+
44
+ | Key | Action |
45
+ |-----|--------|
46
+ | `Cmd+N` | Spawn a new agent |
47
+ | `Cmd+1..9` | Switch to agent by tab position |
48
+ | `Cmd+E` | Toggle the file explorer panel |
49
+ | `Cmd+D` | Toggle voice input (dictation) |
50
+
51
+ ## How It Works
52
+
53
+ Each agent runs in its own [git worktree](https://git-scm.com/docs/git-worktree), so multiple agents can work on the same repo without stepping on each other. The server manages PTY processes via [node-pty](https://github.com/microsoft/node-pty) and communicates with the browser over WebSocket. The pixel office is rendered on an HTML canvas with a day/night cycle that follows your local time.
54
+
55
+ Every agent's branch starts from your repository's base branch as it exists on
56
+ the remote, fetched just before the worktree is created, so an agent never picks
57
+ up a stale local base or whatever unrelated branch you happen to have checked
58
+ out. Override it per agent with **Advanced -> Start from** when you want to
59
+ branch off work in progress.
60
+
61
+ The job board reuses that same machinery: a dispatched job is an ordinary agent, with a real terminal you can type into and take over at any point. Each job gets its own worktree and branch, so a job maps one-to-one onto a branch and a pull request. When the agent finishes it calls the board's `finish_job` tool (a card that requires a pull request hands over the PR it opened; one that does not hands over a summary), and the card moves to Review with the agent still running, so you can click straight into it to ask about the work. When the card reaches Done the board closes the agent and releases its worktree and local branch; the PR itself is untouched, and work that was never pushed is kept as an orphan rather than deleted. **Re-spawn** on an orphan picks that conversation back up with the CLI it ran, `codex resume <session-id>` (the newest Codex session recorded in that exact worktree) or `claude --continue`, under the permission mode its job card was dispatched with (a board agent whose card is already finished or deleted follows the board's current setting), or, for an agent you spawned by hand, under the permission flags you started it with. When the PR merges the job is filed away as finished -- the record is kept, the card is not.
62
+
63
+ ```
64
+ ┌─────────────┬──────────────┬────────────────────┐
65
+ │ Explorer │ Pixel │ Jobs + Terminals │
66
+ │ (repos, │ Office │ (job board tab, │
67
+ │ files, │ (canvas, │ xterm.js, one │
68
+ │ diffs) │ agents) │ tab per agent) │
69
+ └─────────────┴──────────────┴────────────────────┘
70
+ ```
71
+
72
+ ## Configuration
73
+
74
+ Configure via environment variables, either inline or in a `.env` file. On
75
+ startup `npm start` auto-loads `.env` if present (via Node's built-in
76
+ `--env-file-if-exists`), and `npx @bill10/agent-007` loads a `.env` in the
77
+ directory you run it from; variables already set in your environment win over
78
+ the file. `npx @bill10/agent-007 --port 8080` overrides `PORT`, and
79
+ `npx @bill10/agent-007 --help` lists the options. Everything the app saves
80
+ lives in `~/.agent-007`. Copy the template to get going:
81
+
82
+ ```bash
83
+ cp .env.example .env # then edit; .env is gitignored
84
+ npm start
85
+ ```
86
+
87
+ Or set them inline:
88
+
89
+ ```bash
90
+ PORT=8080 npm start # Custom port (default: 7007)
91
+ HOST=0.0.0.0 npm start # Bind all interfaces (default: 127.0.0.1)
92
+ ALLOWED_ORIGINS=mac-mini.tailXXXX.ts.net npm start # Allow a remote browser origin
93
+ ```
94
+
95
+ | Variable | Default | Purpose |
96
+ |----------|---------|---------|
97
+ | `PORT` | `7007` | Listen port |
98
+ | `HOST` | `127.0.0.1` | Bind interface. Use `0.0.0.0` only behind Tailscale/a trusted network |
99
+ | `ALLOWED_ORIGINS` | *(none)* | Comma-separated extra origins for the cross-origin check (`localhost` is always allowed) |
100
+ | `CLAUDE_PERMISSION_MODE` | *(the CLI's own)* | Permission mode every Claude Code agent the app starts runs in (`auto`, `acceptEdits`, `bypassPermissions`, `manual`, `dontAsk`, `plan`). Flags in the command, a card's own mode or a mode picked in the board's dropdown win over it |
101
+ | `CODEX_PERMISSION_MODE` | *(the CLI's own)* | The same for Codex agents, mapped onto Codex's sandbox and approval flags |
102
+ | `AGENT_MESSAGING` | *(guarded)* | `open` lets any of your agents message any other. By default an agent that asks before acting cannot message one that never asks |
103
+ | `BILLION` | *(on)* | `0` (or `false`/`off`/`no`) turns Billion off. It is also off whenever user accounts exist, since it would belong to everyone |
104
+ | `BILLION_DIR` | `~/.agent-007/billion` | Billion's own folder and git repo. Point it at a new or empty folder |
105
+ | `TRUST_BOARD_WORKTREES` | *(on)* | Board-dispatched Claude Code workers skip the workspace-trust dialog, so queued jobs start unattended. That also lets the repo's own `.claude/settings.json` hooks and permission allow rules apply without asking. `0` (or `false`/`off`/`no`) keeps the dialog. Hand-started agents always keep it |
106
+
107
+ > **Running remotely?** The server spawns real shells, so never expose it to the
108
+ > open internet. See [docs/REMOTE.md](docs/REMOTE.md) for the recommended
109
+ > Tailscale setup.
110
+
111
+ ### Multiplayer & login
112
+
113
+ By default there are no user accounts and no login — the app runs open on
114
+ localhost, exactly as before. To turn on per-user login (for shared/remote use),
115
+ create a user:
116
+
117
+ ```bash
118
+ npm run adduser -- "Alice" # prints a one-time login token
119
+ npx @bill10/agent-007 adduser "Alice" # the same, without a clone
120
+ ```
121
+
122
+ The moment the first user exists, the server **requires a token** for every
123
+ `/api` call and WebSocket connection. Log in by opening the app and pasting the
124
+ token, or visit `http://<host>:7007/?token=<token>` once (the token is stored in
125
+ your browser and stripped from the URL). Add a user per person; each gets a
126
+ distinct color and shows up in the presence indicator.
127
+
128
+ > Login establishes **identity**, not isolation — every logged-in user can still
129
+ > spawn their own shells on the host. Only issue tokens to people you'd give an
130
+ > SSH login, and keep the server behind Tailscale/a trusted network.
131
+ >
132
+ > Each agent is owned by the user who spawned it. You have full control of your
133
+ > own agents and are **read-only** on everyone else's — you see their live
134
+ > terminal but can't type into, resize, kill, rename, or upload to it (enforced
135
+ > server-side). The dimmed-tile / read-only-terminal UI polish is still to come
136
+ > (see [docs/designs/multiplayer.md](docs/designs/multiplayer.md)).
137
+ >
138
+ > Caveat: agents you spawned **before** creating the first user are unowned, so
139
+ > once auth is on anyone can still control them. Spawn agents after enabling auth
140
+ > (or restart them) if you want them owned.
141
+
142
+ ## Requirements
143
+
144
+ - Node.js 20.12+
145
+ - Git
146
+ - A modern browser (three panels at 900px+, explorer hidden below that, one panel at a time on a phone)
147
+ - A CLI to run as the agent (defaults to `claude`, but works with any command)
148
+
149
+ Agent 007 runs on macOS, Linux, and Windows -- spawning agents, adding repos, and browsing paths all handle Windows natively, and CI runs the test suite on both Ubuntu and Windows. One Windows caveat: the per-agent MCP config file protects its token with POSIX file permissions, which Windows doesn't have, so on a shared Windows machine other local users can read it.
150
+
151
+ ## Troubleshooting
152
+
153
+ **`npm install` fails building `node-pty`.** `node-pty` ships prebuilt binaries for macOS, Linux and Windows on x64 and arm64, so normally nothing compiles. Only if the install tries to build it from source and fails, install a C++ toolchain and run `npm install` again:
154
+
155
+ - **macOS:** Xcode Command Line Tools (`xcode-select --install`)
156
+ - **Linux:** `build-essential` and `python3` (`sudo apt install build-essential python3`)
157
+ - **Windows:** [Visual Studio Build Tools](https://github.com/microsoft/node-pty#windows) with the C++ workload
158
+
159
+ ## Architecture
160
+
161
+ ```
162
+ server.js Entry point + orchestrators (createSession, killSession)
163
+ server/
164
+ state.js Shared mutable state (sessions, orphans, pools, config)
165
+ config.js Config persistence (load, save, crash recovery)
166
+ direct-run.js Entry-point detection (symlink/space-safe `npm start` guard)
167
+ git.js Git operations (worktree, file tree, diff)
168
+ jobs.js Job board dispatcher (scan, spawn, PR watch, schedule firing, attachment files)
169
+ command-path.js Resolves commands to spawnable files on Windows (PATHEXT)
170
+ pty.js PTY lifecycle (spawn, handlers, state detection)
171
+ ws.js WebSocket (message routing, broadcast, origin check, shared terminal sizing)
172
+ http.js HTTP routes (/api/browse, /api/jobs, job attachment downloads, /mcp, origin + auth gates)
173
+ mcp.js The board's MCP server (post_job, list_jobs, read_job, edit_job, finish_job, list_agents, send_message; Billion also gets billion_ready, add_repo, close_job, answer_permission)
174
+ messages.js Agent-to-agent messages and board notices (who can reach whom, rate limit, queued until the recipient rests at its prompt)
175
+ billion.js Billion's folder (git repo, templates, charter refresh) and whether it runs
176
+ approvals.js Hands a worker's permission request to Billion and waits for its answer
177
+ permission-hook.js Claude Code PermissionRequest hook a worker on Billion's cards runs
178
+ agent-mcp.js Per-session MCP config + the flags that connect Claude Code and Codex to it
179
+ agent-mcp-bridge.js Codex stdio bridge to the board's HTTP endpoint
180
+ agent-transcripts.js Which CLI last ran in a worktree, read off its transcripts (re-spawn fallback)
181
+ auth.js Login tokens, user accounts, agent session tokens
182
+ bin/
183
+ agent-007.js The `agent-007` command (`npx @bill10/agent-007`): flags, .env, start
184
+ adduser.js Create a login user (`npm run adduser`)
185
+ public/
186
+ index.html Three-panel layout, plus the phone's bottom nav
187
+ style.css Dark/light themes via CSS custom properties
188
+ app.js Main entry point
189
+ assets/ Pixel art sprites (characters/ MIT with vendored LICENSE; furniture/ mixes Antea CC-BY 4.0 and pixel-agents MIT, see Acknowledgements)
190
+ modules/
191
+ office.js Canvas pixel art (workstations, characters, job boards, day/night)
192
+ terminal.js xterm.js terminals, clipboard paste, tab management
193
+ explorer.js File tree, diff viewer, repo management
194
+ jobs.js Job board UI (columns, cards, the job form)
195
+ ws.js WebSocket client with auto-reload on reconnect
196
+ state.js Shared client state (agents, repos, viewer identity, server platform, the panel a phone shows)
197
+ shortcuts.js Keyboard shortcuts
198
+ voice.js Voice input (Web Speech API dictation)
199
+ auth.js Login tokens, presence, HTML escaping
200
+ lib/
201
+ helpers.js State detection (dialog patterns per CLI, the synchronized-output frames Codex paints in), git parsing, codename/cocktail pools, the file-name sanitiser
202
+ jobs.js Pure job-board logic (states, prompts, dispatch selection)
203
+ cron.js Five-field cron parser (schedules for scheduled jobs)
204
+ templates/
205
+ billion/ Billion's starting files (charter, owner rules, STATE.md, COMPANY.md)
206
+ ```
207
+
208
+ ## Acknowledgements
209
+
210
+ - Inspired by [pixel-agents](https://github.com/pablodelucca/pixel-agents), the VS Code extension that put AI agents in a pixel office first.
211
+ - Character sprites and the ambient decor sprites (`furniture/cactus.png`, `plant_2.png`, `sofa_side.png`, `sofa_front.png`, `coffee_table.png`, `coffee.png`, `table_front.png`, `chair_side.png`, `chair_back.png` -- the two chairs recolored, and `chair_front.png` drawn for this project in the same style) from [pixel-agents](https://github.com/pablodelucca/pixel-agents) (MIT, © Pablo De Lucca — license vendored at `public/assets/characters/LICENSE`), character bases by JIK-A-4's ["Metro City" free top-down character pack](https://jik-a-4.itch.io/metrocity-free-topdown-character-pack) (CC0)
212
+ - Desk and `bookshelf.png` sprites from the Free Furniture Office Equipment Set by Antea (CC-BY 4.0)
213
+
214
+ ## Contributing
215
+
216
+ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and guidelines.
217
+
218
+ ## License
219
+
220
+ [MIT](LICENSE)
221
+
222
+ If this is useful, a ⭐ helps others find it.
package/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.6.2.1
package/bin/adduser.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ // adduser — create an Agent 007 user and print a one-time login token.
3
+ //
4
+ // Usage: npm run adduser -- "Display Name"
5
+ //
6
+ // Writes ~/.agent-007/users.json (override with AGENT007_USERS_PATH). The token
7
+ // is shown ONCE and only its hash is stored — if it's lost, re-run to mint a new
8
+ // one. Creating the first user switches the server into authenticated mode.
9
+
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'fs';
11
+ import { dirname } from 'path';
12
+ import {
13
+ USERS_PATH, USER_COLORS, hashToken, generateToken, newUserId, normalizeUsers,
14
+ } from '../server/auth.js';
15
+
16
+ const displayName = process.argv.slice(2).join(' ').trim();
17
+ if (!displayName) {
18
+ console.error('Usage: npm run adduser -- "Display Name"');
19
+ process.exit(1);
20
+ }
21
+
22
+ let users = [];
23
+ if (existsSync(USERS_PATH)) {
24
+ try {
25
+ users = normalizeUsers(JSON.parse(readFileSync(USERS_PATH, 'utf8')));
26
+ } catch (err) {
27
+ console.error(`Cannot read ${USERS_PATH}: ${err.message}`);
28
+ process.exit(1);
29
+ }
30
+ }
31
+
32
+ if (users.some(u => u.displayName === displayName)) {
33
+ console.error(`A user named "${displayName}" already exists. Pick another name or remove it first.`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const token = generateToken();
38
+ const user = {
39
+ id: newUserId(),
40
+ displayName,
41
+ color: USER_COLORS[users.length % USER_COLORS.length],
42
+ tokenHash: hashToken(token),
43
+ createdAt: new Date().toISOString(),
44
+ };
45
+ users.push(user);
46
+
47
+ try {
48
+ mkdirSync(dirname(USERS_PATH), { recursive: true });
49
+ // Atomic write: a concurrent server read sees either the old or new complete
50
+ // file, never a torn one (which would otherwise trip the fail-closed path).
51
+ const tmp = `${USERS_PATH}.tmp-${process.pid}`;
52
+ writeFileSync(tmp, JSON.stringify(users, null, 2));
53
+ renameSync(tmp, USERS_PATH);
54
+ } catch (err) {
55
+ console.error(`Cannot write ${USERS_PATH}: ${err.message}`);
56
+ process.exit(1);
57
+ }
58
+
59
+ const first = users.length === 1;
60
+ console.log(`\n Created user "${displayName}" (${user.id}, ${user.color})`);
61
+ console.log(`\n Login token (shown once — copy it now):\n`);
62
+ console.log(` ${token}\n`);
63
+ console.log(' Log in by opening the app and pasting the token, or visit:');
64
+ console.log(` http://localhost:${process.env.PORT || 7007}/?token=${token}\n`);
65
+ if (first) {
66
+ console.log(' This is the first user — the server now REQUIRES login.');
67
+ console.log(' Restart is not required (users are re-read live), but open sessions');
68
+ console.log(' already connected stay connected until they reconnect.\n');
69
+ }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ // agent-007 — the command `npx @bill10/agent-007` (or a global install) runs.
3
+ //
4
+ // Loads ./.env from the current directory when there is one (the same file
5
+ // `npm start` reads in a clone; variables already in the environment win),
6
+ // applies --port over both, then starts server.js. Everything the server reads
7
+ // or writes resolves from the package or from ~/.agent-007, so it runs the
8
+ // same from node_modules as from a clone.
9
+
10
+ import { existsSync, readFileSync } from 'fs';
11
+ import { resolve } from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import { parseArgs } from 'util';
14
+
15
+ const HELP = `Usage: agent-007 [--port <n>]
16
+ agent-007 adduser "Display Name"
17
+
18
+ Starts Agent 007 at http://localhost:7007 (or --port).
19
+
20
+ Options:
21
+ -p, --port <n> Port to listen on (overrides PORT)
22
+ -h, --help Show this help
23
+ -v, --version Print the version
24
+
25
+ Settings come from environment variables, or a .env file in the current
26
+ directory: PORT, HOST, ALLOWED_ORIGINS, CLAUDE_PERMISSION_MODE,
27
+ CODEX_PERMISSION_MODE, AGENT_MESSAGING, BILLION, BILLION_DIR.
28
+ See https://github.com/bill10/agent-007#configuration
29
+
30
+ State lives in ~/.agent-007 (AGENT007_CONFIG_DIR to move it).
31
+ \`adduser\` creates a login user and turns on login for the server.
32
+ `;
33
+
34
+ let parsed;
35
+ try {
36
+ parsed = parseArgs({
37
+ allowPositionals: true,
38
+ options: {
39
+ port: { type: 'string', short: 'p' },
40
+ help: { type: 'boolean', short: 'h' },
41
+ version: { type: 'boolean', short: 'v' },
42
+ },
43
+ });
44
+ } catch (err) {
45
+ console.error(`${err.message}\n\n${HELP}`);
46
+ process.exit(2);
47
+ }
48
+ const { values, positionals } = parsed;
49
+
50
+ if (values.help) {
51
+ process.stdout.write(HELP);
52
+ process.exit(0);
53
+ }
54
+ if (values.version) {
55
+ console.log(readFileSync(new URL('../VERSION', import.meta.url), 'utf8').trim());
56
+ process.exit(0);
57
+ }
58
+
59
+ // Said out loud: run from inside another project, its .env (a HOST=0.0.0.0,
60
+ // say) would otherwise change this server without a word.
61
+ if (existsSync('.env')) {
62
+ process.loadEnvFile('.env');
63
+ console.error(` Loaded settings from ${resolve('.env')}`);
64
+ }
65
+
66
+ if (positionals[0] === 'adduser') {
67
+ // adduser.js reads the display name from argv[2..].
68
+ process.argv = [process.argv[0], fileURLToPath(new URL('./adduser.js', import.meta.url)), ...positionals.slice(1)];
69
+ await import('./adduser.js');
70
+ } else {
71
+ if (positionals.length) {
72
+ console.error(`Unknown command: ${positionals[0]}\n\n${HELP}`);
73
+ process.exit(2);
74
+ }
75
+ if (values.port !== undefined) {
76
+ const port = Number(values.port);
77
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
78
+ console.error(`--port must be a whole number from 1 to 65535, not "${values.port}"`);
79
+ process.exit(2);
80
+ }
81
+ process.env.PORT = String(port);
82
+ }
83
+ // Imported only now: server/state.js reads PORT when it loads.
84
+ const { startup, gracefulShutdown } = await import('../server.js');
85
+ startup();
86
+ process.on('SIGINT', gracefulShutdown);
87
+ process.on('SIGTERM', gracefulShutdown);
88
+ }
package/lib/cron.js ADDED
@@ -0,0 +1,189 @@
1
+ // A small five-field cron parser, for the board's scheduled jobs.
2
+ //
3
+ // Written here rather than pulled in as a dependency: the app ships with four
4
+ // runtime dependencies and this is the whole of what the board needs — parse an
5
+ // expression, say whether it is valid, and answer "when does this next fire?".
6
+ // Pure and side-effect free, like the rest of lib/, so it is testable on its own.
7
+ //
8
+ // Times are the SERVER's local time. A schedule is written by the person
9
+ // sitting in front of the machine the agents run on, so local is the reading
10
+ // they mean; there is no per-user timezone anywhere else in the app either.
11
+
12
+ const FIELDS = [
13
+ { key: 'minute', label: 'minute', min: 0, max: 59 },
14
+ { key: 'hour', label: 'hour', min: 0, max: 23 },
15
+ { key: 'dom', label: 'day of month', min: 1, max: 31 },
16
+ { key: 'month', label: 'month', min: 1, max: 12 },
17
+ // 7 is Sunday as well as 0, which is what every crontab accepts.
18
+ { key: 'dow', label: 'day of week', min: 0, max: 7 },
19
+ ];
20
+
21
+ // The shorthands people actually type. Expanded before parsing so everything
22
+ // downstream only ever deals with the five-field form.
23
+ export const CRON_MACROS = {
24
+ '@yearly': '0 0 1 1 *',
25
+ '@annually': '0 0 1 1 *',
26
+ '@monthly': '0 0 1 * *',
27
+ '@weekly': '0 0 * * 0',
28
+ '@daily': '0 0 * * *',
29
+ '@midnight': '0 0 * * *',
30
+ '@hourly': '0 * * * *',
31
+ };
32
+
33
+ // Bounds the stored text, for the same reason MAX_TITLE_LEN exists: this string
34
+ // is persisted in config.json and rendered on a card. Mirrored by the
35
+ // maxlength on #job-schedule in public/index.html — keep the two in step.
36
+ export const MAX_SCHEDULE_LEN = 120;
37
+
38
+ function parseField(text, spec) {
39
+ const values = new Set();
40
+ for (const part of String(text).split(',')) {
41
+ const piece = part.trim();
42
+ if (!piece) return { error: `Empty ${spec.label} in the schedule` };
43
+ // `*/15` and `1-30/5` share a step suffix; `*` and `a-b` are the same rule
44
+ // with different bounds, so normalise both into a from/to pair.
45
+ const [rangeText, stepText, ...extra] = piece.split('/');
46
+ if (extra.length > 0) return { error: `"${piece}" is not a valid ${spec.label}` };
47
+ let step = 1;
48
+ if (stepText !== undefined) {
49
+ if (!/^\d+$/.test(stepText.trim())) return { error: `"${piece}" has an invalid step for the ${spec.label}` };
50
+ step = parseInt(stepText.trim(), 10);
51
+ if (step < 1) return { error: `The step in "${piece}" must be 1 or more` };
52
+ }
53
+ let from, to;
54
+ const range = rangeText.trim();
55
+ if (range === '*') {
56
+ from = spec.min;
57
+ to = spec.max;
58
+ } else if (/^\d+$/.test(range)) {
59
+ from = parseInt(range, 10);
60
+ // A bare number with a step means "from here to the end of the field",
61
+ // which is how crontab reads `5/10`.
62
+ to = stepText === undefined ? from : spec.max;
63
+ } else {
64
+ const m = /^(\d+)-(\d+)$/.exec(range);
65
+ if (!m) return { error: `"${piece}" is not a valid ${spec.label}` };
66
+ from = parseInt(m[1], 10);
67
+ to = parseInt(m[2], 10);
68
+ if (from > to) return { error: `The range "${range}" runs backwards for the ${spec.label}` };
69
+ }
70
+ if (from < spec.min || to > spec.max) {
71
+ return { error: `The ${spec.label} must be between ${spec.min} and ${spec.max} (got "${piece}")` };
72
+ }
73
+ for (let v = from; v <= to; v += step) values.add(v === 7 && spec.key === 'dow' ? 0 : v);
74
+ }
75
+ return { values };
76
+ }
77
+
78
+ /**
79
+ * Parse a cron expression.
80
+ *
81
+ * @returns { minute, hour, dom, month, dow, domStar, dowStar, expr } — sets of
82
+ * the matching values — or { error } with a message written for the
83
+ * person who typed it.
84
+ */
85
+ export function parseCron(expr) {
86
+ const raw = String(expr || '').trim();
87
+ if (!raw) return { error: 'A scheduled job needs a cron schedule (for example "0 9 * * 1-5")' };
88
+ if (raw.length > MAX_SCHEDULE_LEN) return { error: 'That schedule is too long to be a cron expression' };
89
+ const expanded = raw.startsWith('@') ? CRON_MACROS[raw.toLowerCase()] : raw;
90
+ if (!expanded) return { error: `Unknown schedule shorthand "${raw}" — try ${Object.keys(CRON_MACROS).join(', ')}` };
91
+ const parts = expanded.split(/\s+/);
92
+ if (parts.length !== 5) {
93
+ return { error: `A cron schedule has five fields (minute hour day-of-month month day-of-week) — got ${parts.length}` };
94
+ }
95
+ const parsed = { expr: raw };
96
+ for (let i = 0; i < FIELDS.length; i++) {
97
+ const spec = FIELDS[i];
98
+ const result = parseField(parts[i], spec);
99
+ if (result.error) return { error: result.error };
100
+ parsed[spec.key] = result.values;
101
+ }
102
+ // Which of the two day fields BEGIN with `*`, because that decides how they
103
+ // combine — see matchesDay. Vixie cron counts `*/2` as a star here, not just
104
+ // the bare `*`, and diverging from that makes pasted crontab lines fire on
105
+ // days they never named.
106
+ parsed.domStar = parts[2].trim().startsWith('*');
107
+ parsed.dowStar = parts[4].trim().startsWith('*');
108
+ return parsed;
109
+ }
110
+
111
+ export function isValidCron(expr) {
112
+ return !parseCron(expr).error;
113
+ }
114
+
115
+ // The one genuinely surprising cron rule: when BOTH day fields are restricted,
116
+ // a day matches if EITHER does (so `0 0 13 * 5` is the 13th *or* any Friday).
117
+ // A field beginning with `*` (bare, or a step like `*/2`) is a "star" field,
118
+ // and a star on either side means both conditions must hold instead — which is
119
+ // exactly Vixie's rule, and for a bare `*` degenerates to the other field
120
+ // alone since its set contains every day.
121
+ function matchesDay(parsed, date) {
122
+ const domHit = parsed.dom.has(date.getDate());
123
+ const dowHit = parsed.dow.has(date.getDay());
124
+ if (parsed.domStar || parsed.dowStar) return domHit && dowHit;
125
+ return domHit || dowHit;
126
+ }
127
+
128
+ // Four years covers the one expression that legitimately takes years to come
129
+ // round (Feb 29), and bounds the walk for one that never fires at all — "0 0 30
130
+ // 2 *" is syntactically fine and matches no date that will ever exist.
131
+ const SEARCH_LIMIT_MS = 4 * 366 * 24 * 60 * 60 * 1000;
132
+
133
+ /**
134
+ * The next time the expression fires, strictly after `from`.
135
+ *
136
+ * @returns epoch milliseconds, or null when the expression is invalid or can
137
+ * never match. Walks whole months/days/hours at a time rather than
138
+ * minute by minute, so a yearly schedule costs a few hundred steps.
139
+ */
140
+ export function nextCronTime(expr, from = Date.now()) {
141
+ const parsed = parseCron(expr);
142
+ if (parsed.error) return null;
143
+ const limit = from + SEARCH_LIMIT_MS;
144
+ // Strictly after: a job that just ran at 09:00 must not immediately match
145
+ // 09:00 again and loop.
146
+ const at = new Date(from);
147
+ at.setSeconds(0, 0);
148
+ at.setMinutes(at.getMinutes() + 1);
149
+ let previous = -1;
150
+ while (at.getTime() <= limit) {
151
+ // Every branch below moves `at` forward, and the limit bounds the walk —
152
+ // but all of them move it by setting LOCAL fields, and a daylight-saving
153
+ // transition is exactly where local arithmetic can fail to advance. This
154
+ // loop runs inside the dispatcher's scan, so it gets a belt as well as
155
+ // braces: if a step ever fails to move the clock on, give up rather than
156
+ // spin. (A DST skip is normal cron behaviour: an hour that does not exist
157
+ // that day simply does not fire.)
158
+ if (at.getTime() <= previous) return null;
159
+ previous = at.getTime();
160
+ if (!parsed.month.has(at.getMonth() + 1)) {
161
+ // setMonth(m + 1, 1) rolls the year over on its own.
162
+ at.setMonth(at.getMonth() + 1, 1);
163
+ at.setHours(0, 0, 0, 0);
164
+ continue;
165
+ }
166
+ if (!matchesDay(parsed, at)) {
167
+ at.setDate(at.getDate() + 1);
168
+ at.setHours(0, 0, 0, 0);
169
+ continue;
170
+ }
171
+ if (!parsed.hour.has(at.getHours())) {
172
+ at.setHours(at.getHours() + 1, 0, 0, 0);
173
+ continue;
174
+ }
175
+ if (!parsed.minute.has(at.getMinutes())) {
176
+ at.setMinutes(at.getMinutes() + 1, 0, 0);
177
+ continue;
178
+ }
179
+ return at.getTime();
180
+ }
181
+ return null;
182
+ }
183
+
184
+ // ISO form, for storing on a job. Null when the expression will never fire
185
+ // again, so a card can say so instead of showing a date that is a lie.
186
+ export function nextCronIso(expr, from = Date.now()) {
187
+ const at = nextCronTime(expr, from);
188
+ return at === null ? null : new Date(at).toISOString();
189
+ }