@botbuddy/cli 1.1.0 → 1.2.1
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/package.json +1 -1
- package/src/codex-bridge.mjs +75 -36
- package/src/commands.mjs +39 -55
- package/src/version.mjs +6 -0
package/package.json
CHANGED
package/src/codex-bridge.mjs
CHANGED
|
@@ -9,16 +9,19 @@
|
|
|
9
9
|
* - Bridge ↔ Codex app-server: localhost-only WS with nonce handshake
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
|
-
* botbuddy
|
|
12
|
+
* botbuddy start [--port 4500] [--repo /path/to/repo] [--model gpt-5.4]
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { createHash, randomBytes } from "crypto";
|
|
16
16
|
import { getConfig, SERVER_URL } from "./config.mjs";
|
|
17
17
|
import { green, red, cyan, dim, bold, yellow, die } from "./utils.mjs";
|
|
18
|
+
import { VERSION } from "./version.mjs";
|
|
18
19
|
|
|
19
20
|
const RELAY_URL = SERVER_URL.replace("/mcp-server", "/codex-relay");
|
|
20
21
|
const POLL_INTERVAL_MS = 2000;
|
|
21
22
|
const PING_INTERVAL_MS = 15000;
|
|
23
|
+
const STARTUP_TIMEOUT_MS = 5000;
|
|
24
|
+
const READY_POLL_INTERVAL_MS = 250;
|
|
22
25
|
|
|
23
26
|
// ─── HMAC signing ───────────────────────────────────────────────
|
|
24
27
|
let sessionSecret = null;
|
|
@@ -37,10 +40,33 @@ async function signRequest(sessionId) {
|
|
|
37
40
|
};
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
function sleep(ms) {
|
|
44
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function waitForCodexReady(readyUrl, timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
48
|
+
const deadline = Date.now() + timeoutMs;
|
|
49
|
+
|
|
50
|
+
while (Date.now() < deadline) {
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(readyUrl);
|
|
53
|
+
if (res.ok) return true;
|
|
54
|
+
} catch {}
|
|
55
|
+
|
|
56
|
+
await sleep(READY_POLL_INTERVAL_MS);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isAddressInUseError(text) {
|
|
63
|
+
return /address already in use|eaddrinuse|os error 48/i.test(text);
|
|
64
|
+
}
|
|
65
|
+
|
|
40
66
|
export async function runBridge(args) {
|
|
41
67
|
const cfg = getConfig();
|
|
42
68
|
if (!cfg.api_key && !cfg.access_token) {
|
|
43
|
-
die(`Not authenticated. Run: ${cyan("botbuddy
|
|
69
|
+
die(`Not authenticated. Run: ${cyan("botbuddy start")} or ${cyan("botbuddy login")}`);
|
|
44
70
|
}
|
|
45
71
|
|
|
46
72
|
// Parse args
|
|
@@ -57,10 +83,11 @@ export async function runBridge(args) {
|
|
|
57
83
|
}
|
|
58
84
|
|
|
59
85
|
const wsUrl = `ws://127.0.0.1:${wsPort}`;
|
|
86
|
+
const readyUrl = `http://127.0.0.1:${wsPort}/readyz`;
|
|
60
87
|
const { hostname: getHostname } = await import("os");
|
|
61
88
|
const host = getHostname();
|
|
62
89
|
|
|
63
|
-
console.log(`${bold("BotBuddy Codex Bridge")}\n`);
|
|
90
|
+
console.log(`${bold("BotBuddy Codex Bridge")} ${dim(`v${VERSION}`)}\n`);
|
|
64
91
|
console.log(` ${dim("WebSocket:")} ${cyan(wsUrl)}`);
|
|
65
92
|
console.log(` ${dim("Repo:")} ${cyan(repoPath)}`);
|
|
66
93
|
console.log(` ${dim("Model:")} ${cyan(model)}`);
|
|
@@ -70,45 +97,57 @@ export async function runBridge(args) {
|
|
|
70
97
|
// ── Step 0: Auto-start codex app-server ──
|
|
71
98
|
let serverProc = null;
|
|
72
99
|
if (!noServer) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
100
|
+
const existingServerReady = await waitForCodexReady(readyUrl, 750);
|
|
101
|
+
|
|
102
|
+
if (existingServerReady) {
|
|
103
|
+
console.log(dim("→ Reusing existing codex app-server..."));
|
|
104
|
+
console.log(` ${green("✓")} codex app-server already running on port ${wsPort}`);
|
|
105
|
+
} else {
|
|
106
|
+
console.log(dim("→ Starting codex app-server..."));
|
|
107
|
+
const { spawn } = await import("child_process");
|
|
108
|
+
serverProc = spawn("codex", ["app-server", "--listen", wsUrl], {
|
|
109
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
110
|
+
env: { ...process.env },
|
|
111
|
+
});
|
|
79
112
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
113
|
+
serverProc.on("error", (err) => {
|
|
114
|
+
if (err.code === "ENOENT") {
|
|
115
|
+
console.log(` ${red("✗")} 'codex' not found in PATH. Install it or use ${cyan("--no-server")} to skip.`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
console.error(` ${red("✗")} Failed to start codex app-server: ${err.message}`);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
serverProc.on("exit", () => {
|
|
122
|
+
serverProc = null;
|
|
123
|
+
});
|
|
87
124
|
|
|
88
|
-
// Wait for the server to be ready
|
|
89
|
-
await new Promise((resolve) => {
|
|
90
|
-
let resolved = false;
|
|
91
125
|
serverProc.stdout.on("data", (chunk) => {
|
|
92
|
-
const text = chunk.toString();
|
|
93
|
-
if (!
|
|
94
|
-
|
|
95
|
-
console.log(` ${
|
|
96
|
-
resolve();
|
|
126
|
+
const text = chunk.toString().trim();
|
|
127
|
+
if (!text) return;
|
|
128
|
+
for (const line of text.split(/\r?\n/)) {
|
|
129
|
+
console.log(` ${dim(`[codex] ${line}`)}`);
|
|
97
130
|
}
|
|
98
131
|
});
|
|
132
|
+
|
|
99
133
|
serverProc.stderr.on("data", (chunk) => {
|
|
100
134
|
const text = chunk.toString().trim();
|
|
101
|
-
if (text)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
setTimeout(() => {
|
|
105
|
-
if (!resolved) {
|
|
106
|
-
resolved = true;
|
|
107
|
-
console.log(` ${yellow("⚠")} Proceeding without readiness confirmation`);
|
|
108
|
-
resolve();
|
|
135
|
+
if (!text) return;
|
|
136
|
+
for (const line of text.split(/\r?\n/)) {
|
|
137
|
+
console.log(` ${dim(`[codex] ${line}`)}`);
|
|
109
138
|
}
|
|
110
|
-
|
|
111
|
-
|
|
139
|
+
if (isAddressInUseError(text)) {
|
|
140
|
+
console.log(` ${yellow("⚠")} Port ${wsPort} is already in use — attempting to reuse the existing codex app-server.`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const ready = await waitForCodexReady(readyUrl);
|
|
145
|
+
if (ready) {
|
|
146
|
+
console.log(` ${green("✓")} codex app-server ready on port ${wsPort}`);
|
|
147
|
+
} else {
|
|
148
|
+
console.log(` ${yellow("⚠")} Proceeding without readiness confirmation`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
112
151
|
}
|
|
113
152
|
|
|
114
153
|
// ── Step 1: Register bridge session with BotBuddy ──
|
|
@@ -117,7 +156,7 @@ export async function runBridge(args) {
|
|
|
117
156
|
bridge_name: cfg.agent_name || `bridge-${host}`,
|
|
118
157
|
machine_host: host,
|
|
119
158
|
repo_path: repoPath,
|
|
120
|
-
config: { ws_port: wsPort, model, auto_start:
|
|
159
|
+
config: { ws_port: wsPort, model, auto_start: !noServer },
|
|
121
160
|
});
|
|
122
161
|
|
|
123
162
|
if (!session?.session?.id) die("Failed to register bridge session");
|
|
@@ -175,7 +214,7 @@ export async function runBridge(args) {
|
|
|
175
214
|
clientInfo: {
|
|
176
215
|
name: "botbuddy_bridge",
|
|
177
216
|
title: "BotBuddy Codex Bridge",
|
|
178
|
-
version:
|
|
217
|
+
version: VERSION,
|
|
179
218
|
bridgeNonce,
|
|
180
219
|
},
|
|
181
220
|
capabilities: { experimentalApi: true },
|
package/src/commands.mjs
CHANGED
|
@@ -3,17 +3,18 @@ import { doLogin } from "./auth.mjs";
|
|
|
3
3
|
import { runBridge } from "./codex-bridge.mjs";
|
|
4
4
|
import { loadConfig, getConfig, clearConfig, saveConfig, getConfigPath } from "./config.mjs";
|
|
5
5
|
import { green, red, cyan, dim, bold, die } from "./utils.mjs";
|
|
6
|
+
import { VERSION } from "./version.mjs";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export function run(argv) {
|
|
8
|
+
export async function run(argv) {
|
|
10
9
|
loadConfig();
|
|
11
10
|
const [command, ...args] = argv;
|
|
12
11
|
|
|
13
12
|
switch (command) {
|
|
13
|
+
case "start": return cmdStart(args);
|
|
14
14
|
case "login": return doLogin();
|
|
15
15
|
case "logout": return cmdLogout();
|
|
16
16
|
case "status": return cmdStatus();
|
|
17
|
+
// Agent-only commands (used by MCP agents, not humans)
|
|
17
18
|
case "register": return cmdRegister(args);
|
|
18
19
|
case "heartbeat": return cmdHeartbeat(args);
|
|
19
20
|
case "lock": return cmdLock(args);
|
|
@@ -39,52 +40,40 @@ function cmdHelp() {
|
|
|
39
40
|
console.log(`${bold("botbuddy")} ${dim(`v${VERSION}`)} — Swarm coordination CLI
|
|
40
41
|
|
|
41
42
|
${bold("USAGE")}
|
|
42
|
-
botbuddy
|
|
43
|
+
botbuddy start Start BotBuddy (login + codex server + bridge)
|
|
44
|
+
|
|
45
|
+
${bold("OPTIONS")}
|
|
46
|
+
start [options]
|
|
47
|
+
-p, --port <port> WebSocket port (default: 4500)
|
|
48
|
+
-r, --repo <path> Repository path (default: cwd)
|
|
49
|
+
-m, --model <model> Default model (default: gpt-5.4)
|
|
50
|
+
--no-server Don't auto-start codex app-server
|
|
43
51
|
|
|
44
52
|
${bold("AUTH")}
|
|
45
|
-
login
|
|
46
|
-
logout
|
|
47
|
-
status
|
|
48
|
-
|
|
49
|
-
${bold("AGENTS")}
|
|
50
|
-
register <name> [type] Register a new agent (type: codex|claude|gpt|custom)
|
|
51
|
-
agents List all agents
|
|
52
|
-
heartbeat [task] Send a heartbeat
|
|
53
|
-
|
|
54
|
-
${bold("RESOURCES")}
|
|
55
|
-
lock <name> <type> Acquire a single lock (type: port|mcp_server|file|...)
|
|
56
|
-
locks [options] Batch-acquire resources (auto-assigns free ones)
|
|
57
|
-
-p, --port [type] Request a port (frontend|backend, default: frontend)
|
|
58
|
-
-m, --mcp Request an MCP server slot
|
|
59
|
-
-t, --ticket <id> Ticket ID (shared across all locks)
|
|
60
|
-
--pr <id> PR ID (shared across all locks)
|
|
61
|
-
unlock <name> Release a lock
|
|
62
|
-
resources List all resources and locks
|
|
63
|
-
|
|
64
|
-
${bold("TASKS")}
|
|
65
|
-
task create <title> [-d description] [-p priority]
|
|
66
|
-
task claim <id> Claim a pending task
|
|
67
|
-
task done <id> Mark a task as done
|
|
68
|
-
tasks List all tasks
|
|
69
|
-
|
|
70
|
-
${bold("SWARM")}
|
|
71
|
-
hours Get current working hours
|
|
72
|
-
hours derive Derive working hours from activity
|
|
73
|
-
hours set <json> Set working hours manually
|
|
74
|
-
|
|
75
|
-
${bold("BROWSE")}
|
|
76
|
-
browse agents|tasks|resources|activity|settings
|
|
77
|
-
|
|
78
|
-
${bold("CODEX")}
|
|
79
|
-
codex bridge [options] Start Codex app-server + bridge (single command)
|
|
80
|
-
-p, --port <port> WebSocket port (default: 4500)
|
|
81
|
-
-r, --repo <path> Repository path (default: cwd)
|
|
82
|
-
-m, --model <model> Default model (default: gpt-5.4)
|
|
83
|
-
--no-server Don't auto-start app-server (if already running)
|
|
53
|
+
login Re-authenticate via OAuth
|
|
54
|
+
logout Remove saved credentials
|
|
55
|
+
status Show current auth status
|
|
84
56
|
|
|
85
57
|
${bold("OTHER")}
|
|
86
|
-
help
|
|
87
|
-
version
|
|
58
|
+
help Show this help
|
|
59
|
+
version Show version`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function cmdStart(args) {
|
|
63
|
+
console.log(`${bold("botbuddy")} ${dim(`v${VERSION}`)}\n`);
|
|
64
|
+
const cfg = getConfig();
|
|
65
|
+
|
|
66
|
+
// Auto-login if not authenticated
|
|
67
|
+
if (!cfg.access_token && !cfg.api_key) {
|
|
68
|
+
console.log(dim("→ No credentials found. Starting login...\n"));
|
|
69
|
+
await doLogin();
|
|
70
|
+
} else if (cfg.token_expires_at && cfg.token_expires_at <= Date.now()) {
|
|
71
|
+
console.log(dim("→ Token expired. Re-authenticating...\n"));
|
|
72
|
+
await doLogin();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Start the bridge (which auto-starts codex app-server)
|
|
76
|
+
return runBridge(args);
|
|
88
77
|
}
|
|
89
78
|
|
|
90
79
|
function cmdStatus() {
|
|
@@ -96,7 +85,7 @@ function cmdStatus() {
|
|
|
96
85
|
if (cfg.token_expires_at) {
|
|
97
86
|
const remaining = cfg.token_expires_at - Date.now();
|
|
98
87
|
if (remaining <= 0) {
|
|
99
|
-
console.log(` Token: ${red("EXPIRED")} — run ${cyan("botbuddy
|
|
88
|
+
console.log(` Token: ${red("EXPIRED")} — run ${cyan("botbuddy start")}`);
|
|
100
89
|
} else {
|
|
101
90
|
const mins = Math.round(remaining / 60000);
|
|
102
91
|
const label = mins > 60 ? `${Math.round(mins / 60)}h ${mins % 60}m` : `${mins}m`;
|
|
@@ -109,7 +98,7 @@ function cmdStatus() {
|
|
|
109
98
|
if (cfg.agent_name) console.log(` Agent: ${cyan(cfg.agent_name)}`);
|
|
110
99
|
} else {
|
|
111
100
|
console.log(`${red("✗")} Not authenticated`);
|
|
112
|
-
console.log(` Run: ${cyan("botbuddy
|
|
101
|
+
console.log(` Run: ${cyan("botbuddy start")}`);
|
|
113
102
|
}
|
|
114
103
|
}
|
|
115
104
|
|
|
@@ -214,12 +203,7 @@ function cmdBrowse(args) {
|
|
|
214
203
|
}
|
|
215
204
|
|
|
216
205
|
function cmdCodex(args) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
case "bridge":
|
|
221
|
-
return runBridge(args.slice(1));
|
|
222
|
-
default:
|
|
223
|
-
die(`Unknown codex subcommand: ${sub}. Try: botbuddy codex bridge`);
|
|
224
|
-
}
|
|
206
|
+
// Legacy alias — redirect to start
|
|
207
|
+
console.log(dim("Note: 'botbuddy codex bridge' is now 'botbuddy start'\n"));
|
|
208
|
+
return runBridge(args.slice(1));
|
|
225
209
|
}
|