@bill10/agent-007 0.6.2 → 0.6.4000
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/.env.example +54 -0
- package/README.md +36 -15
- package/VERSION +1 -1
- package/bin/agent-007.js +71 -31
- package/package.json +5 -4
- package/server/billion.js +10 -0
- package/server/command-path.js +34 -2
- package/server/pty.js +5 -1
- package/server/settings.js +42 -0
- package/server/state.js +2 -2
- package/server.js +13 -6
package/.env.example
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Agent 007 settings. Everything is optional and commented out: the app runs on
|
|
2
|
+
# its defaults (the values shown). Uncomment a line to change it, then restart.
|
|
3
|
+
#
|
|
4
|
+
# Read from ~/.agent-007/.env (`npx @bill10/agent-007 init` writes this file
|
|
5
|
+
# there) and from a .env in the directory you start the app from. Precedence,
|
|
6
|
+
# highest first: command-line flags, the real environment, ./.env,
|
|
7
|
+
# ~/.agent-007/.env. AGENT007_CONFIG_DIR moves ~/.agent-007.
|
|
8
|
+
|
|
9
|
+
# Port to listen on.
|
|
10
|
+
# PORT=7007
|
|
11
|
+
|
|
12
|
+
# Bind interface. 127.0.0.1 = localhost-only (safe default).
|
|
13
|
+
# Set to 0.0.0.0 to accept remote connections — only behind Tailscale or another
|
|
14
|
+
# trusted network, since the server spawns real shells. See docs/REMOTE.md.
|
|
15
|
+
# HOST=127.0.0.1
|
|
16
|
+
|
|
17
|
+
# Comma-separated extra browser origins allowed by the cross-origin check.
|
|
18
|
+
# localhost and 127.0.0.1 are always allowed. Add your tailnet hostname to reach
|
|
19
|
+
# the app from another machine. Bare hostnames or full origins both work; only
|
|
20
|
+
# the hostname is used. A single `*` disables the check (not recommended).
|
|
21
|
+
# ALLOWED_ORIGINS=mac-mini.tailXXXX.ts.net
|
|
22
|
+
|
|
23
|
+
# The permission mode every Claude Code / Codex agent the app starts runs in,
|
|
24
|
+
# unless something more specific says otherwise: flags already in the command
|
|
25
|
+
# you type, a job card's own mode, or a mode picked in the job board's
|
|
26
|
+
# "permissions" dropdown. One of: auto, acceptEdits, bypassPermissions, manual,
|
|
27
|
+
# dontAsk, plan. Unset: each CLI's own default. Billion keeps its own mode.
|
|
28
|
+
# CLAUDE_PERMISSION_MODE=bypassPermissions
|
|
29
|
+
# CODEX_PERMISSION_MODE=bypassPermissions
|
|
30
|
+
|
|
31
|
+
# Who may message whom. By default an agent that asks before acting cannot
|
|
32
|
+
# message one that never asks, so it cannot borrow the other's permissions.
|
|
33
|
+
# `open` lifts that rule for all your agents (messages are still labelled as
|
|
34
|
+
# coming from another agent). It changes nothing when every agent runs in the
|
|
35
|
+
# same kind of mode.
|
|
36
|
+
# AGENT_MESSAGING=open
|
|
37
|
+
|
|
38
|
+
# Billion, the always-on agent you talk to (docs/BILLION.md). On by default;
|
|
39
|
+
# 0/false/off/no turns it off. It is also off whenever user accounts exist.
|
|
40
|
+
# BILLION=0
|
|
41
|
+
|
|
42
|
+
# Billion's own folder and git repo. Must be new or empty the first time.
|
|
43
|
+
# BILLION_DIR=~/.agent-007/billion
|
|
44
|
+
|
|
45
|
+
# Claude Code stops at a "do you trust this folder?" dialog the first time it
|
|
46
|
+
# runs anywhere, and every job-board worker gets a brand-new worktree. By default
|
|
47
|
+
# the app accepts it for board workers (it adds the worktree to ~/.claude.json
|
|
48
|
+
# before the spawn, and answers "Yes" if the dialog still appears), so queued
|
|
49
|
+
# jobs start on their own. A trusted worktree loads that repo's own
|
|
50
|
+
# .claude/settings.json: its hooks run, and its permission allow rules can
|
|
51
|
+
# pre-approve what a strict board mode would ask about. 0/false/off/no keeps
|
|
52
|
+
# the dialog.
|
|
53
|
+
# Hand-started agents always keep it.
|
|
54
|
+
# TRUST_BOARD_WORKTREES=0
|
package/README.md
CHANGED
|
@@ -29,6 +29,29 @@ npm start
|
|
|
29
29
|
|
|
30
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
31
|
|
|
32
|
+
### Settings
|
|
33
|
+
|
|
34
|
+
It works with no configuration. To change something, create a settings file:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx @bill10/agent-007 init # writes ~/.agent-007/.env, every line commented out
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Uncomment what you want, then restart. The ones people change:
|
|
41
|
+
|
|
42
|
+
| Setting | What it does |
|
|
43
|
+
|---------|--------------|
|
|
44
|
+
| `BILLION=0` | Turns off Billion, the always-on agent |
|
|
45
|
+
| `HOST=0.0.0.0` + `ALLOWED_ORIGINS=<tailnet name>` | Reach it from your phone or another machine (behind Tailscale only, see [docs/REMOTE.md](docs/REMOTE.md)) |
|
|
46
|
+
| `CLAUDE_PERMISSION_MODE` | Mode Claude Code agents start in, e.g. `bypassPermissions` |
|
|
47
|
+
| `CODEX_PERMISSION_MODE` | The same for Codex |
|
|
48
|
+
| `TRUST_BOARD_WORKTREES=0` | Keeps Claude Code's folder-trust prompt for job board workers |
|
|
49
|
+
| `PORT` | Port to listen on (default `7007`) |
|
|
50
|
+
|
|
51
|
+
Highest wins: command-line flags (`--port`), then environment variables, then
|
|
52
|
+
a `.env` in the directory you start it from, then `~/.agent-007/.env`. The full
|
|
53
|
+
list is in [Configuration](#configuration) and `--help`.
|
|
54
|
+
|
|
32
55
|
## Highlights
|
|
33
56
|
|
|
34
57
|
- **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.
|
|
@@ -71,20 +94,15 @@ The job board reuses that same machinery: a dispatched job is an ordinary agent,
|
|
|
71
94
|
|
|
72
95
|
## Configuration
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
the
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
cp .env.example .env # then edit; .env is gitignored
|
|
84
|
-
npm start
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
Or set them inline:
|
|
97
|
+
Settings are environment variables. Set them inline, in `~/.agent-007/.env`
|
|
98
|
+
(`npx @bill10/agent-007 init` creates it from [`.env.example`](.env.example),
|
|
99
|
+
every setting explained and commented out), or in a `.env` in the directory you
|
|
100
|
+
start from. `npx @bill10/agent-007`, a global `agent-007` and `npm start` in a
|
|
101
|
+
clone all read both files, and the startup log names the ones it loaded. Highest
|
|
102
|
+
wins: flags (`--port 8080` overrides `PORT`), then the environment, then
|
|
103
|
+
`./.env`, then `~/.agent-007/.env`. `--help` lists them all. Everything the app
|
|
104
|
+
saves lives in `~/.agent-007` (`AGENT007_CONFIG_DIR` moves it, and the settings
|
|
105
|
+
file with it).
|
|
88
106
|
|
|
89
107
|
```bash
|
|
90
108
|
PORT=8080 npm start # Custom port (default: 7007)
|
|
@@ -156,17 +174,20 @@ Agent 007 runs on macOS, Linux, and Windows -- spawning agents, adding repos, an
|
|
|
156
174
|
- **Linux:** `build-essential` and `python3` (`sudo apt install build-essential python3`)
|
|
157
175
|
- **Windows:** [Visual Studio Build Tools](https://github.com/microsoft/node-pty#windows) with the C++ workload
|
|
158
176
|
|
|
177
|
+
**An agent will not start: `"claude" is not installed, or not on the PATH Agent 007 was started with.`** The CLI the agent runs (`claude`, `codex` or `gemini`) was not found, and the message says how to install it. If it is installed, Agent 007 was started from somewhere with a shorter `PATH` than your shell (a launcher or a service manager, say); start it from a terminal where `which claude` finds it. Billion runs on Claude Code too, so without it Billion's tab says what to do instead: install Claude Code and press **Start** next to Billion, or restart with `BILLION=0` to turn Billion off.
|
|
178
|
+
|
|
159
179
|
## Architecture
|
|
160
180
|
|
|
161
181
|
```
|
|
162
182
|
server.js Entry point + orchestrators (createSession, killSession)
|
|
163
183
|
server/
|
|
164
184
|
state.js Shared mutable state (sessions, orphans, pools, config)
|
|
185
|
+
settings.js Config dir and the settings files (./.env, ~/.agent-007/.env)
|
|
165
186
|
config.js Config persistence (load, save, crash recovery)
|
|
166
187
|
direct-run.js Entry-point detection (symlink/space-safe `npm start` guard)
|
|
167
188
|
git.js Git operations (worktree, file tree, diff)
|
|
168
189
|
jobs.js Job board dispatcher (scan, spawn, PR watch, schedule firing, attachment files)
|
|
169
|
-
command-path.js
|
|
190
|
+
command-path.js Checks a CLI is installed before a spawn; resolves commands to spawnable files on Windows (PATHEXT)
|
|
170
191
|
pty.js PTY lifecycle (spawn, handlers, state detection)
|
|
171
192
|
ws.js WebSocket (message routing, broadcast, origin check, shared terminal sizing)
|
|
172
193
|
http.js HTTP routes (/api/browse, /api/jobs, job attachment downloads, /mcp, origin + auth gates)
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.4.0
|
package/bin/agent-007.js
CHANGED
|
@@ -1,34 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// agent-007 — the command `npx @bill10/agent-007` (or a global install
|
|
2
|
+
// agent-007 — the command `npx @bill10/agent-007` (or a global install, or
|
|
3
|
+
// `npm start` in a clone) runs.
|
|
3
4
|
//
|
|
4
|
-
// Loads ./.env
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// same from node_modules as from a clone.
|
|
5
|
+
// Loads settings (server/settings.js: ./.env, then ~/.agent-007/.env; the real
|
|
6
|
+
// environment and --port win over both), then starts server.js. Everything the
|
|
7
|
+
// server reads or writes resolves from the package or from ~/.agent-007, so it
|
|
8
|
+
// runs the same from node_modules as from a clone.
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
11
|
+
import { basename, join } from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { parseArgs } from 'util';
|
|
14
|
+
import { configDir, loadSettings, settingsLine } from '../server/settings.js';
|
|
14
15
|
|
|
16
|
+
// Each .env.example setting has a line here; test/cli.test.js checks that.
|
|
15
17
|
const HELP = `Usage: agent-007 [--port <n>]
|
|
18
|
+
agent-007 init
|
|
16
19
|
agent-007 adduser "Display Name"
|
|
17
20
|
|
|
18
21
|
Starts Agent 007 at http://localhost:7007 (or --port).
|
|
19
22
|
|
|
23
|
+
Commands:
|
|
24
|
+
init Create ~/.agent-007/.env, a commented settings template
|
|
25
|
+
adduser Create a login user and turn on login for the server
|
|
26
|
+
|
|
20
27
|
Options:
|
|
21
28
|
-p, --port <n> Port to listen on (overrides PORT)
|
|
22
29
|
-h, --help Show this help
|
|
23
30
|
-v, --version Print the version
|
|
24
31
|
|
|
25
|
-
Settings
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
Settings (default in brackets):
|
|
33
|
+
PORT Port to listen on [7007]
|
|
34
|
+
HOST Interface to bind; 0.0.0.0 to reach it from a phone
|
|
35
|
+
or over Tailscale [127.0.0.1]
|
|
36
|
+
ALLOWED_ORIGINS Extra hostnames the browser may use, comma-separated,
|
|
37
|
+
e.g. your tailnet name [none]
|
|
38
|
+
CLAUDE_PERMISSION_MODE Mode Claude Code agents start in: auto, acceptEdits,
|
|
39
|
+
bypassPermissions, manual, dontAsk, plan [Claude's own]
|
|
40
|
+
CODEX_PERMISSION_MODE The same for Codex agents [Codex's own]
|
|
41
|
+
AGENT_MESSAGING open = any agent may message any other [guarded]
|
|
42
|
+
BILLION 0 turns off Billion, the always-on agent [on]
|
|
43
|
+
BILLION_DIR Billion's folder and repo [~/.agent-007/billion]
|
|
44
|
+
TRUST_BOARD_WORKTREES 0 keeps Claude Code's folder-trust prompt for job
|
|
45
|
+
board workers [on]
|
|
46
|
+
|
|
47
|
+
Set them in the environment, in ~/.agent-007/.env (\`agent-007 init\` writes it,
|
|
48
|
+
every setting explained and commented out), or in a .env in the current
|
|
49
|
+
directory. Highest first: flags, environment, ./.env, ~/.agent-007/.env.
|
|
29
50
|
|
|
30
51
|
State lives in ~/.agent-007 (AGENT007_CONFIG_DIR to move it).
|
|
31
|
-
|
|
52
|
+
See https://github.com/bill10/agent-007#settings
|
|
32
53
|
`;
|
|
33
54
|
|
|
34
55
|
let parsed;
|
|
@@ -56,30 +77,49 @@ if (values.version) {
|
|
|
56
77
|
process.exit(0);
|
|
57
78
|
}
|
|
58
79
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
80
|
+
if (values.port !== undefined) {
|
|
81
|
+
const port = Number(values.port);
|
|
82
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
83
|
+
console.error(`--port must be a whole number from 1 to 65535, not "${values.port}"`);
|
|
84
|
+
process.exit(2);
|
|
85
|
+
}
|
|
86
|
+
// Before the files load, which never overwrite a variable already set.
|
|
87
|
+
process.env.PORT = String(port);
|
|
64
88
|
}
|
|
89
|
+
if (positionals.length && !['init', 'adduser'].includes(positionals[0])) {
|
|
90
|
+
console.error(`Unknown command: ${positionals[0]}\n\n${HELP}`);
|
|
91
|
+
process.exit(2);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const settingsFiles = loadSettings();
|
|
65
95
|
|
|
66
|
-
|
|
96
|
+
// Said the way it was launched, so it can be pasted back.
|
|
97
|
+
function initCommand() {
|
|
98
|
+
if (process.env.npm_command === 'exec') return 'npx @bill10/agent-007 init';
|
|
99
|
+
if (process.env.npm_lifecycle_event) return 'npm start -- init';
|
|
100
|
+
if (basename(process.argv[1] || '') === 'agent-007') return 'agent-007 init';
|
|
101
|
+
return 'npx @bill10/agent-007 init';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (positionals[0] === 'init') {
|
|
105
|
+
const file = join(configDir(), '.env');
|
|
106
|
+
mkdirSync(configDir(), { recursive: true });
|
|
107
|
+
try {
|
|
108
|
+
// wx: never overwrite someone's settings.
|
|
109
|
+
writeFileSync(file, readFileSync(new URL('../.env.example', import.meta.url)), { flag: 'wx' });
|
|
110
|
+
console.log(`Created ${file}\nEvery setting in it is commented out, so nothing changes yet. Edit it, then restart agent-007.`);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (err.code !== 'EEXIST') throw err;
|
|
113
|
+
console.log(`${file} already exists, left as it is. Edit it, then restart agent-007.`);
|
|
114
|
+
}
|
|
115
|
+
} else if (positionals[0] === 'adduser') {
|
|
67
116
|
// adduser.js reads the display name from argv[2..].
|
|
68
117
|
process.argv = [process.argv[0], fileURLToPath(new URL('./adduser.js', import.meta.url)), ...positionals.slice(1)];
|
|
69
118
|
await import('./adduser.js');
|
|
70
119
|
} else {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
}
|
|
120
|
+
// Said out loud: run from inside another project, its .env (a HOST=0.0.0.0,
|
|
121
|
+
// say) would otherwise change this server without a word.
|
|
122
|
+
console.log(` ${settingsLine(settingsFiles, initCommand())}`);
|
|
83
123
|
// Imported only now: server/state.js reads PORT when it loads.
|
|
84
124
|
const { startup, gracefulShutdown } = await import('../server.js');
|
|
85
125
|
startup();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bill10/agent-007",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4000",
|
|
4
4
|
"description": "Self-hosted office where a manager agent posts jobs to Claude Code and Codex workers in parallel git worktrees, then reviews and merges their PRs. Cron jobs, agent-to-agent messaging, pixel-art status view.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"agent-007": "bin/agent-007.js"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
+
".env.example",
|
|
17
18
|
"bin/",
|
|
18
19
|
"lib/",
|
|
19
20
|
"public/",
|
|
@@ -26,9 +27,9 @@
|
|
|
26
27
|
"node": ">=20.12"
|
|
27
28
|
},
|
|
28
29
|
"scripts": {
|
|
29
|
-
"start": "node
|
|
30
|
-
"dev": "node --
|
|
31
|
-
"adduser": "node
|
|
30
|
+
"start": "node bin/agent-007.js",
|
|
31
|
+
"dev": "node --watch bin/agent-007.js",
|
|
32
|
+
"adduser": "node bin/agent-007.js adduser",
|
|
32
33
|
"test": "vitest run",
|
|
33
34
|
"test:watch": "vitest"
|
|
34
35
|
},
|
package/server/billion.js
CHANGED
|
@@ -150,6 +150,16 @@ export function billionCommand({ created, hasConversation, dir, projectsHint })
|
|
|
150
150
|
return `claude --dangerously-skip-permissions${!created && hasConversation ? ' --continue' : ''} ${quote(prompt)}`;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// Billion is Claude Code. Without it, its tab runs this instead: one line
|
|
154
|
+
// saying what to do, then an exit. Nothing restarts it, so there is no loop;
|
|
155
|
+
// the Start button checks for claude again. It stays up a moment after
|
|
156
|
+
// printing, since a console host can drop the output of a process that exits
|
|
157
|
+
// at once.
|
|
158
|
+
export const NO_CLAUDE_NOTICE = 'Billion runs on Claude Code, which is not installed (no "claude" on the PATH Agent 007 was started with). Install it from https://docs.anthropic.com/en/docs/claude-code/setup and press Start next to Billion, or restart Agent 007 with BILLION=0 to turn Billion off.';
|
|
159
|
+
export function noClaudeCommand(node = process.execPath) {
|
|
160
|
+
return `${quote(node)} -e ${quote(`console.log(${JSON.stringify(NO_CLAUDE_NOTICE)}); setTimeout(() => {}, 1000)`)}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
153
163
|
// Claude Code asks whether to trust a folder the first time it runs there, and
|
|
154
164
|
// highlights "No, exit". Billion's folder is Agent 007's own, holding only
|
|
155
165
|
// what the server put there, so the server answers for it. Board workers get
|
package/server/command-path.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
// something the caller can try/catch. Resolving the command to a full path
|
|
13
13
|
// with a launchable extension up front is what keeps it from happening.
|
|
14
14
|
|
|
15
|
-
import { statSync } from 'fs';
|
|
16
|
-
import { join, resolve } from 'path';
|
|
15
|
+
import { statSync, accessSync, constants } from 'fs';
|
|
16
|
+
import { join, resolve, delimiter } from 'path';
|
|
17
17
|
|
|
18
18
|
const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
|
|
19
19
|
|
|
@@ -100,3 +100,35 @@ export function isUsableCwd(dir) {
|
|
|
100
100
|
if (!dir) return false;
|
|
101
101
|
try { return statSync(dir).isDirectory(); } catch { return false; }
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
function isExecutable(p) {
|
|
105
|
+
try { accessSync(p, constants.X_OK); return statSync(p).isFile(); } catch { return false; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether `file` would launch: a path to an executable, or a name on PATH.
|
|
110
|
+
*
|
|
111
|
+
* Checked before a spawn because on macOS and Linux node-pty "starts" a
|
|
112
|
+
* command that does not exist: the fork succeeds, the exec fails, and the tab
|
|
113
|
+
* shows an empty terminal that is already dead. Windows goes through
|
|
114
|
+
* resolveExecutable, the same lookup the spawn itself uses.
|
|
115
|
+
*/
|
|
116
|
+
export function commandExists(file, env = process.env, platform = process.platform, cwd = process.cwd()) {
|
|
117
|
+
if (!file) return false;
|
|
118
|
+
if (platform === 'win32') return !!resolveExecutable(file, env, platform, cwd);
|
|
119
|
+
if (file.includes('/')) return isExecutable(resolve(cwd, file));
|
|
120
|
+
return (env.PATH || '').split(delimiter).filter(Boolean).some(dir => isExecutable(join(dir, file)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Where to get the CLIs the + Agent presets and the board start.
|
|
124
|
+
const INSTALL_HINTS = {
|
|
125
|
+
claude: 'Install Claude Code: https://docs.anthropic.com/en/docs/claude-code/setup',
|
|
126
|
+
codex: 'Install Codex: npm install -g @openai/codex',
|
|
127
|
+
gemini: 'Install Gemini CLI: npm install -g @google/gemini-cli',
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// The one line a person sees when `file` is not there.
|
|
131
|
+
export function missingCommandMessage(file) {
|
|
132
|
+
const hint = INSTALL_HINTS[file];
|
|
133
|
+
return `"${file}" is not installed, or not on the PATH Agent 007 was started with.${hint ? ` ${hint}` : ''}`;
|
|
134
|
+
}
|
package/server/pty.js
CHANGED
|
@@ -6,7 +6,7 @@ import { basename } from 'path';
|
|
|
6
6
|
import { stripAnsiComplete, detectState, createRingBuffer, parseCommand, isRealOutput, trackSyncFrames, ptyEnv } from '../lib/helpers.js';
|
|
7
7
|
// Re-exported so the handler's tests reach the parser through the module they drive.
|
|
8
8
|
export { trackSyncFrames } from '../lib/helpers.js';
|
|
9
|
-
import { resolveExecutable, isUsableCwd } from './command-path.js';
|
|
9
|
+
import { resolveExecutable, isUsableCwd, commandExists, missingCommandMessage } from './command-path.js';
|
|
10
10
|
import { RING_BUFFER_MAX } from './state.js';
|
|
11
11
|
import { mintAgentToken, authEnabled } from './auth.js';
|
|
12
12
|
import { writeMcpConfig, removeMcpConfig, withMcpConfig, takesMcpConfig, withApprovalHook } from './agent-mcp.js';
|
|
@@ -218,6 +218,10 @@ export function createSessionFromConfig({ sessionId, name, color, command, repoP
|
|
|
218
218
|
if (!isUsableCwd(cwd)) {
|
|
219
219
|
return { error: `Working directory no longer exists: ${cwd}` };
|
|
220
220
|
}
|
|
221
|
+
// A missing CLI "starts" on macOS and Linux and leaves a dead, empty tab.
|
|
222
|
+
if (!commandExists(file, process.env, process.platform, cwd)) {
|
|
223
|
+
return { error: missingCommandMessage(file) };
|
|
224
|
+
}
|
|
221
225
|
// Falls back to the bare name when nothing matched, leaving node-pty's own
|
|
222
226
|
// lookup (and its catchable "File not found") in charge.
|
|
223
227
|
const resolvedFile = resolveExecutable(file, process.env, process.platform, cwd) || file;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Where settings come from, for bin/agent-007.js (which `npm start` runs too).
|
|
2
|
+
// Imports nothing from state.js: that module reads PORT and HOST when it loads,
|
|
3
|
+
// so it may only be imported once the files below are in process.env.
|
|
4
|
+
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join, resolve } from 'path';
|
|
8
|
+
|
|
9
|
+
// Everything the app saves, and the settings file `agent-007 init` writes.
|
|
10
|
+
export function configDir(env = process.env) {
|
|
11
|
+
return env.AGENT007_CONFIG_DIR || join(homedir(), '.agent-007');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ./.env first, then <config dir>/.env. process.loadEnvFile never overwrites a
|
|
15
|
+
// variable that is already set, so the real environment beats ./.env, and
|
|
16
|
+
// ./.env beats the config-dir file. Returns the files it loaded.
|
|
17
|
+
export function loadSettings() {
|
|
18
|
+
const files = [];
|
|
19
|
+
if (existsSync('.env')) {
|
|
20
|
+
process.loadEnvFile('.env');
|
|
21
|
+
files.push(resolve('.env'));
|
|
22
|
+
}
|
|
23
|
+
// Resolved after ./.env, which may set AGENT007_CONFIG_DIR.
|
|
24
|
+
const shared = join(configDir(), '.env');
|
|
25
|
+
if (existsSync(shared) && !files.includes(resolve(shared))) {
|
|
26
|
+
process.loadEnvFile(shared);
|
|
27
|
+
files.push(resolve(shared));
|
|
28
|
+
}
|
|
29
|
+
return files;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tilde = (p) => {
|
|
33
|
+
const home = homedir();
|
|
34
|
+
return p.startsWith(home + '/') || p.startsWith(home + '\\') ? '~' + p.slice(home.length) : p;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// One startup line saying where settings came from, or how to make the file.
|
|
38
|
+
export function settingsLine(files, initCommand) {
|
|
39
|
+
return files.length
|
|
40
|
+
? `Settings: ${files.map(tilde).join(', ')}`
|
|
41
|
+
: `Settings: defaults (run \`${initCommand}\` to create ${tilde(join(configDir(), '.env'))})`;
|
|
42
|
+
}
|
package/server/state.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Shared mutable state — single owner for all Maps, Sets, and singletons.
|
|
2
2
|
// All other modules import from here. No circular deps.
|
|
3
3
|
|
|
4
|
-
import { homedir } from 'os';
|
|
5
4
|
import { join } from 'path';
|
|
5
|
+
import { configDir } from './settings.js';
|
|
6
6
|
import {
|
|
7
7
|
createCodenamePool, createCocktailPool, createColorCycler,
|
|
8
8
|
} from '../lib/helpers.js';
|
|
@@ -69,7 +69,7 @@ export const GIT_USER_TIMEOUT = 30000;
|
|
|
69
69
|
// (mirrors AGENT007_WORKTREE_DIR and AGENT007_USERS_PATH, both set by test/setup.js).
|
|
70
70
|
// saveConfig() rewrites this file wholesale from in-memory state, so a test that
|
|
71
71
|
// touches config would otherwise silently destroy the real repo/orphan list.
|
|
72
|
-
export const CONFIG_DIR =
|
|
72
|
+
export const CONFIG_DIR = configDir();
|
|
73
73
|
// Overridable so tests never create worktrees in the developer's live
|
|
74
74
|
// ~/.agent-007/worktrees (mirrors AGENT007_USERS_PATH, which test/setup.js uses).
|
|
75
75
|
export const WORKTREE_DIR = process.env.AGENT007_WORKTREE_DIR || join(CONFIG_DIR, 'worktrees');
|
package/server.js
CHANGED
|
@@ -33,7 +33,9 @@ import { startDispatcher, stopDispatcher, boardSettings } from './server/jobs.js
|
|
|
33
33
|
import { orphans, config } from './server/state.js';
|
|
34
34
|
import { sweepMcpConfigs } from './server/agent-mcp.js';
|
|
35
35
|
import { withDefaultPermission, envPermissionMode, PERMISSION_MODES, ENV_PERMISSION_MODE } from './lib/jobs.js';
|
|
36
|
-
import { BILLION_NAME, billionEnabled, billionRuns, billionDir, ensureBillionRepo, refreshCharter, suggestProjectsDir, billionCommand } from './server/billion.js';
|
|
36
|
+
import { BILLION_NAME, billionEnabled, billionRuns, billionDir, ensureBillionRepo, refreshCharter, suggestProjectsDir, billionCommand, noClaudeCommand } from './server/billion.js';
|
|
37
|
+
import { commandExists, missingCommandMessage } from './server/command-path.js';
|
|
38
|
+
import { parseCommand } from './lib/helpers.js';
|
|
37
39
|
import { hasClaudeTranscript } from './server/agent-transcripts.js';
|
|
38
40
|
import { autoTrusts, trustClaudeFolder } from './server/claude-trust.js';
|
|
39
41
|
|
|
@@ -57,6 +59,10 @@ async function createSession(command, name, repoPath, customBranch, ownerId, met
|
|
|
57
59
|
// worktree directory's codename: two holders of one name means the first
|
|
58
60
|
// kill frees it while the other still names a directory on disk.
|
|
59
61
|
if (name && codenamePool.has(name)) return { error: `An agent named ${name} already exists` };
|
|
62
|
+
// A missing CLI, before any worktree is made for it: the board retries a
|
|
63
|
+
// failed card every tick. A path is left to the spawn, which knows the cwd.
|
|
64
|
+
const { file } = parseCommand(command);
|
|
65
|
+
if (!/[\\/]/.test(file) && !commandExists(file)) return { error: missingCommandMessage(file) };
|
|
60
66
|
// An agent someone starts gets the .env default mode for its CLI, unless its
|
|
61
67
|
// command already says how it asks. The board settles its own workers' mode
|
|
62
68
|
// (boardModeFor in server/jobs.js), so their commands are left as built.
|
|
@@ -190,12 +196,13 @@ function startBillion() {
|
|
|
190
196
|
console.error(`Billion: could not commit the updated charter in ${dir}:`, err.message);
|
|
191
197
|
}
|
|
192
198
|
}
|
|
193
|
-
const
|
|
199
|
+
const hasClaude = commandExists('claude', process.env, process.platform, dir);
|
|
200
|
+
const command = hasClaude ? billionCommand({
|
|
194
201
|
created,
|
|
195
202
|
hasConversation: !created && hasClaudeTranscript(dir),
|
|
196
203
|
dir,
|
|
197
204
|
projectsHint: suggestProjectsDir(config.repos.map(r => r.path)),
|
|
198
|
-
});
|
|
205
|
+
}) : noClaudeCommand();
|
|
199
206
|
const result = createSessionFromConfig({
|
|
200
207
|
sessionId: nextSessionId(), name: BILLION_NAME, color: colorCycler.next(), command,
|
|
201
208
|
repoPath: null, worktreePath: null, cwd: dir, isBillion: true, ownerId: null,
|
|
@@ -205,7 +212,7 @@ function startBillion() {
|
|
|
205
212
|
// introduction, and at the start of every cycle after a restart.
|
|
206
213
|
result.session.messagesHeld = true;
|
|
207
214
|
sessions.set(result.session.id, result.session);
|
|
208
|
-
return { session: result.session };
|
|
215
|
+
return { session: result.session, ...(hasClaude ? {} : { notice: 'Claude Code (claude) is not installed; its tab says how to fix that' }) };
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
// --- WebSocket ---
|
|
@@ -245,8 +252,8 @@ async function startup() {
|
|
|
245
252
|
else if (raw) console.log(` ${agent} agents start in ${raw} unless told otherwise`);
|
|
246
253
|
}
|
|
247
254
|
if (billionRuns()) {
|
|
248
|
-
const { error } = startBillion();
|
|
249
|
-
console.log(error ? ` Billion: not started (${error})` : ` Billion: running in ${billionDir()}`);
|
|
255
|
+
const { error, notice } = startBillion();
|
|
256
|
+
console.log(error ? ` Billion: not started (${error})` : notice ? ` Billion: ${notice}` : ` Billion: running in ${billionDir()}`);
|
|
250
257
|
} else if (billionEnabled()) {
|
|
251
258
|
console.log(' Billion: off while user accounts are enabled');
|
|
252
259
|
}
|