@bill10/agent-007 0.6.2 → 0.6.3000
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/VERSION +1 -1
- package/package.json +1 -1
- package/server/billion.js +10 -0
- package/server/command-path.js +34 -2
- package/server/pty.js +5 -1
- package/server.js +13 -6
package/README.md
CHANGED
|
@@ -156,6 +156,8 @@ Agent 007 runs on macOS, Linux, and Windows -- spawning agents, adding repos, an
|
|
|
156
156
|
- **Linux:** `build-essential` and `python3` (`sudo apt install build-essential python3`)
|
|
157
157
|
- **Windows:** [Visual Studio Build Tools](https://github.com/microsoft/node-pty#windows) with the C++ workload
|
|
158
158
|
|
|
159
|
+
**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.
|
|
160
|
+
|
|
159
161
|
## Architecture
|
|
160
162
|
|
|
161
163
|
```
|
|
@@ -166,7 +168,7 @@ server/
|
|
|
166
168
|
direct-run.js Entry-point detection (symlink/space-safe `npm start` guard)
|
|
167
169
|
git.js Git operations (worktree, file tree, diff)
|
|
168
170
|
jobs.js Job board dispatcher (scan, spawn, PR watch, schedule firing, attachment files)
|
|
169
|
-
command-path.js
|
|
171
|
+
command-path.js Checks a CLI is installed before a spawn; resolves commands to spawnable files on Windows (PATHEXT)
|
|
170
172
|
pty.js PTY lifecycle (spawn, handlers, state detection)
|
|
171
173
|
ws.js WebSocket (message routing, broadcast, origin check, shared terminal sizing)
|
|
172
174
|
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.3.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bill10/agent-007",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3000",
|
|
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": {
|
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;
|
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
|
}
|