@bill10/agent-007 0.12.1001 → 0.12.2001
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/VERSION +1 -1
- package/package.json +1 -1
- package/server/agent-mcp.js +17 -4
- package/server/models.js +47 -23
- package/server/pty.js +6 -2
- package/server/ws.js +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.12.
|
|
1
|
+
0.12.2.1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bill10/agent-007",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2001",
|
|
4
4
|
"description": "From web terminals for your coding agents to a self-running agent company: Claude Code and Codex in parallel git worktrees, a job board they pick work from, and one agent that runs the board from a goal.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/server/agent-mcp.js
CHANGED
|
@@ -91,6 +91,10 @@ export function writeMcpConfig(sessionId, agentToken) {
|
|
|
91
91
|
// server/permission-hook.js with this session's MCP config. Claude Code only;
|
|
92
92
|
// Codex takes its hook differently and is not wired yet (docs/BILLION.md).
|
|
93
93
|
const PERMISSION_HOOK = fileURLToPath(new URL('./permission-hook.js', import.meta.url));
|
|
94
|
+
// The two board tools the job prompt tells a worker on Billion's card to use:
|
|
95
|
+
// finishing its card, and asking Billion. Asking permission for those would
|
|
96
|
+
// only send Billion (or the owner) a request to approve its own instructions.
|
|
97
|
+
export const WORKER_BOARD_TOOLS = ['finish_job', 'send_message'];
|
|
94
98
|
const shellQuote = (s) => `"${String(s).replace(/(["\\$`])/g, '\\$1')}"`;
|
|
95
99
|
// Forward slashes on Windows, which node takes as well: a backslash means
|
|
96
100
|
// something different to each shell a hook might run under (doubled by the
|
|
@@ -99,10 +103,7 @@ export const hookPath = (p, platform = process.platform) => (platform === 'win32
|
|
|
99
103
|
export function withApprovalHook(file, args, configPath) {
|
|
100
104
|
if (!configPath || agentName(file) !== 'claude' || args.includes('--settings')) return args;
|
|
101
105
|
const settings = {
|
|
102
|
-
|
|
103
|
-
// its card, and asking Billion. Asking permission for those would only
|
|
104
|
-
// send Billion a request to approve its own instructions.
|
|
105
|
-
permissions: { allow: ['finish_job', 'send_message'].map(tool => `mcp__${MCP_SERVER_NAME}__${tool}`) },
|
|
106
|
+
permissions: { allow: WORKER_BOARD_TOOLS.map(tool => `mcp__${MCP_SERVER_NAME}__${tool}`) },
|
|
106
107
|
hooks: {
|
|
107
108
|
PermissionRequest: [{
|
|
108
109
|
matcher: '*',
|
|
@@ -113,6 +114,18 @@ export function withApprovalHook(file, args, configPath) {
|
|
|
113
114
|
return ['--settings', JSON.stringify(settings), ...args];
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
// The Codex side of the same pre-allow: per-run -c overrides, one separate argv
|
|
118
|
+
// element each, so ~/.codex/config.toml is never touched. Only these tools;
|
|
119
|
+
// everything else on the server keeps Codex's default (ask). Must come after
|
|
120
|
+
// withMcpConfig's server table on the command line, which replaces the table.
|
|
121
|
+
export function withCodexWorkerTools(file, args, configPath) {
|
|
122
|
+
if (!configPath || agentName(file) !== 'codex') return args;
|
|
123
|
+
return [
|
|
124
|
+
...WORKER_BOARD_TOOLS.flatMap(tool => ['-c', `mcp_servers.${MCP_SERVER_NAME}.tools.${tool}.approval_mode="approve"`]),
|
|
125
|
+
...args,
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
export function removeMcpConfig(sessionId) {
|
|
117
130
|
try {
|
|
118
131
|
rmSync(mcpConfigPath(sessionId), { force: true });
|
package/server/models.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// Which models a card may name, per CLI, discovered on this machine so nobody
|
|
2
|
-
// keeps a list.
|
|
3
|
-
//
|
|
4
|
-
// already caches the account's model list on disk.
|
|
2
|
+
// keeps a list. Claude Code's aliases are fixed words its --model resolves to
|
|
3
|
+
// the newest model itself; Codex is asked for its catalog.
|
|
5
4
|
//
|
|
6
5
|
// A CLI that is not on the PATH the board spawns with offers nothing — a card
|
|
7
6
|
// could not run there anyway — and so does anything that fails to read. An
|
|
8
7
|
// empty list leaves only the CLI's own default on offer.
|
|
9
8
|
|
|
9
|
+
import { execFile } from 'child_process';
|
|
10
10
|
import { readFileSync } from 'fs';
|
|
11
11
|
import { homedir } from 'os';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
-
import { commandExists } from './command-path.js';
|
|
13
|
+
import { commandExists, resolveExecutable } from './command-path.js';
|
|
14
14
|
import { isSafeModelName } from '../lib/jobs.js';
|
|
15
15
|
|
|
16
16
|
// `claude --model` takes these as "the latest model of that family"
|
|
@@ -21,30 +21,51 @@ export const CLAUDE_ALIASES = ['fable', 'opus', 'sonnet', 'haiku'];
|
|
|
21
21
|
|
|
22
22
|
const codexHome = (env) => env.CODEX_HOME || join(homedir(), '.codex');
|
|
23
23
|
|
|
24
|
-
// Codex
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
export function parseCodexModels(text) {
|
|
24
|
+
// Codex's catalog, from `codex debug models` (0.157: "Render the raw model
|
|
25
|
+
// catalog as JSON") or, failing that, the cache its picker is built from,
|
|
26
|
+
// $CODEX_HOME/models_cache.json. Both are { models: [{ slug, visibility,
|
|
27
|
+
// priority, ... }] }. Only `visibility: "list"` entries are kept: that is
|
|
28
|
+
// Codex's own filter for its /model picker, and it is what hides the internal
|
|
29
|
+
// ones (codex-auto-review, the reviewer's model; gpt-reserve) that are not for
|
|
30
|
+
// a coding session. Picker order is `priority`, lowest first. A slug must also
|
|
31
|
+
// pass isSafeModelName, since it ends up on a command line. Null when the text
|
|
32
|
+
// is not a catalog. Read-only; nothing here ever writes under CODEX_HOME.
|
|
33
|
+
function listedCodexModels(text) {
|
|
35
34
|
let models;
|
|
36
|
-
try { models = JSON.parse(text)?.models; } catch { return
|
|
37
|
-
if (!Array.isArray(models)) return
|
|
35
|
+
try { models = JSON.parse(text)?.models; } catch { return null; }
|
|
36
|
+
if (!Array.isArray(models)) return null;
|
|
38
37
|
return models
|
|
39
38
|
.filter(m => m && m.visibility === 'list' && isSafeModelName(m.slug))
|
|
40
39
|
.sort((a, b) => (Number(a.priority) || 0) - (Number(b.priority) || 0))
|
|
41
40
|
.map(m => m.slug);
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
export
|
|
43
|
+
export const parseCodexModels = (text) => listedCodexModels(text) ?? [];
|
|
44
|
+
|
|
45
|
+
// The same codex, found the same way, the board launches workers with. No
|
|
46
|
+
// shell. On Windows the npm install is codex.cmd, which execFile will not run
|
|
47
|
+
// without one, so there this fails and the cache is read instead.
|
|
48
|
+
export function runCodexModels(env) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
execFile(resolveExecutable('codex', env) ?? 'codex', ['debug', 'models'],
|
|
51
|
+
{ env, timeout: 10_000, maxBuffer: 32 * 1024 * 1024, windowsHide: true },
|
|
52
|
+
(err, stdout) => (err ? reject(err) : resolve(stdout)));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function discoverModels({
|
|
57
|
+
env = process.env, exists = (f) => commandExists(f, env), read = (p) => readFileSync(p, 'utf8'),
|
|
58
|
+
run = runCodexModels, log = console.log,
|
|
59
|
+
} = {}) {
|
|
45
60
|
let codex = [];
|
|
46
61
|
if (exists('codex')) {
|
|
47
|
-
|
|
62
|
+
let source = '`codex debug models`';
|
|
63
|
+
codex = await run(env).then(listedCodexModels, () => null);
|
|
64
|
+
if (!codex) {
|
|
65
|
+
source = 'models_cache.json';
|
|
66
|
+
try { codex = parseCodexModels(read(join(codexHome(env), 'models_cache.json'))); } catch { codex = []; source = 'nowhere (no models_cache.json either)'; }
|
|
67
|
+
}
|
|
68
|
+
log(` Models: Codex's from ${source}`);
|
|
48
69
|
}
|
|
49
70
|
return { claude: exists('claude') ? [...CLAUDE_ALIASES] : [], codex };
|
|
50
71
|
}
|
|
@@ -55,19 +76,22 @@ const STALE_MS = 10 * 60 * 1000;
|
|
|
55
76
|
let cached = { claude: [], codex: [] };
|
|
56
77
|
let cachedAt = 0;
|
|
57
78
|
|
|
58
|
-
|
|
59
|
-
|
|
79
|
+
// cachedAt is stamped before the await so a second ask while Codex is still
|
|
80
|
+
// answering does not start another run. It never rejects: callers fire and
|
|
81
|
+
// forget, and a failed look keeps the last answer.
|
|
82
|
+
export async function refreshModels(opts) {
|
|
60
83
|
cachedAt = Date.now();
|
|
84
|
+
try { cached = await discoverModels(opts); } catch (err) { console.error(' Models: discovery failed:', err); }
|
|
61
85
|
return cached;
|
|
62
86
|
}
|
|
63
87
|
|
|
64
88
|
export function availableModels() { return cached; }
|
|
65
89
|
|
|
66
90
|
// Whether a refresh changed anything, so the caller only repaints boards when it did.
|
|
67
|
-
export function refreshIfStale(now = Date.now()) {
|
|
91
|
+
export async function refreshIfStale(now = Date.now()) {
|
|
68
92
|
if (now - cachedAt < STALE_MS) return false;
|
|
69
93
|
const before = JSON.stringify(cached);
|
|
70
|
-
return JSON.stringify(refreshModels()) !== before;
|
|
94
|
+
return JSON.stringify(await refreshModels()) !== before;
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
export function startModelRefresh() {
|
package/server/pty.js
CHANGED
|
@@ -10,7 +10,7 @@ export { trackSyncFrames } from '../lib/helpers.js';
|
|
|
10
10
|
import { resolveExecutable, isUsableCwd, commandExists, missingCommandMessage } from './command-path.js';
|
|
11
11
|
import { RING_BUFFER_MAX } from './state.js';
|
|
12
12
|
import { mintAgentToken, authEnabled } from './auth.js';
|
|
13
|
-
import { writeMcpConfig, removeMcpConfig, withMcpConfig, takesMcpConfig, withApprovalHook } from './agent-mcp.js';
|
|
13
|
+
import { writeMcpConfig, removeMcpConfig, withMcpConfig, takesMcpConfig, withApprovalHook, withCodexWorkerTools } from './agent-mcp.js';
|
|
14
14
|
import { broadcastJobs, requestDispatch } from './jobs.js';
|
|
15
15
|
import { flushMessages, dropMessages } from './messages.js';
|
|
16
16
|
import { sessionAgentFromCommand, permissionFlagsFromCommand } from '../lib/jobs.js';
|
|
@@ -244,7 +244,11 @@ export function createSessionFromConfig({ sessionId, name, color, command, repoP
|
|
|
244
244
|
// use it — a plain `bash` tab does not need one.
|
|
245
245
|
const mcpConfigPath = takesMcpConfig(file) ? writeMcpConfig(sessionId, agentToken) : null;
|
|
246
246
|
const codexTrust = autoTrust && sessionAgentFromCommand(command) === 'codex';
|
|
247
|
-
const
|
|
247
|
+
const ownArgs = codexTrust ? [...codexTrustArgs(worktreePath), ...args] : args;
|
|
248
|
+
// Codex has no hook yet, but its worker on Billion's card still gets the
|
|
249
|
+
// same board tools pre-allowed. Inside withMcpConfig, whose server table
|
|
250
|
+
// override would otherwise replace them.
|
|
251
|
+
const mcpArgs = withMcpConfig(file, approvalsToBillion ? withCodexWorkerTools(file, ownArgs, mcpConfigPath) : ownArgs, mcpConfigPath);
|
|
248
252
|
// A worker on one of Billion's cards asks Billion before it asks a person
|
|
249
253
|
// (server/approvals.js) — where the CLI can be hooked, which today is
|
|
250
254
|
// Claude Code only. Recorded as whether the hook actually went in.
|
package/server/ws.js
CHANGED
|
@@ -635,7 +635,7 @@ export function setupWebSocket(wss, { createSession, killSession, startBillion }
|
|
|
635
635
|
// The card form opening: look again if the list is over 10 minutes
|
|
636
636
|
// old, so a CLI installed since shows up without a restart.
|
|
637
637
|
case 'models-refresh': {
|
|
638
|
-
if (
|
|
638
|
+
refreshIfStale().then(changed => { if (changed) broadcastJobs(broadcast); }).catch(err => console.error('models-refresh:', err));
|
|
639
639
|
break;
|
|
640
640
|
}
|
|
641
641
|
case 'job-pause': {
|