@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,16 +9,19 @@
9
9
  * - Bridge ↔ Codex app-server: localhost-only WS with nonce handshake
10
10
  *
11
11
  * Usage:
12
- * botbuddy codex bridge [--port 4500] [--repo /path/to/repo] [--model gpt-5.4]
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 login")} or ${cyan("botbuddy register <name>")}`);
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
- console.log(dim("→ Starting codex app-server..."));
74
- const { spawn } = await import("child_process");
75
- serverProc = spawn("codex", ["app-server", "--listen", wsUrl], {
76
- stdio: ["ignore", "pipe", "pipe"],
77
- env: { ...process.env },
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
- serverProc.on("error", (err) => {
81
- if (err.code === "ENOENT") {
82
- console.log(` ${red("✗")} 'codex' not found in PATH. Install it or use ${cyan("--no-server")} to skip.`);
83
- process.exit(1);
84
- }
85
- console.error(` ${red("✗")} Failed to start codex app-server: ${err.message}`);
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 (!resolved && text.includes("listening on")) {
94
- resolved = true;
95
- console.log(` ${green("✓")} codex app-server started on port ${wsPort}`);
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) console.log(` ${dim(`[codex] ${text}`)}`);
102
- });
103
- // Timeout fallback — proceed after 5s even if we didn't see the message
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
- }, 5000);
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: autoStart },
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: "1.0.0",
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
- const VERSION = "1.0.0";
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 <command> [options]
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 Authenticate via OAuth (PKCE)
46
- logout Remove saved credentials
47
- status Show current auth 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 Show this help
87
- version Show 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 login")}`);
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 login")}`);
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
- const sub = args[0];
218
- if (!sub) die("Usage: botbuddy codex <bridge> [options]");
219
- switch (sub) {
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
  }
@@ -0,0 +1,6 @@
1
+ import { readFileSync } from "fs";
2
+
3
+ const packageJsonUrl = new URL("../package.json", import.meta.url);
4
+ const packageJson = JSON.parse(readFileSync(packageJsonUrl, "utf8"));
5
+
6
+ export const VERSION = packageJson.version;