byob-cli 0.2.9

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/bin/byob.js ADDED
@@ -0,0 +1,752 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createInterface } from "node:readline";
4
+ import { spawn, spawnSync } from "node:child_process";
5
+ import { copyFileSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { homedir } from "node:os";
9
+
10
+ const PROD_AIR_URL = "https://air.api.byob.studio";
11
+ const PROD_PRIM_URL = "https://prim.api.byob.studio";
12
+ const DEFAULT_AIR_URL = process.env.BYOB_AIR_URL || PROD_AIR_URL;
13
+ const DEFAULT_PRIM_URL = process.env.BYOB_PRIM_URL || PROD_PRIM_URL;
14
+ const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
15
+
16
+ function usage() {
17
+ return `BYOB CLI
18
+
19
+ Usage:
20
+ byob mcp
21
+ byob mcp --multi-project
22
+ byob context [json_args]
23
+ byob resources
24
+ byob projects
25
+ byob tools
26
+ byob call <tool_name> [json_args]
27
+ byob status [json_args]
28
+ byob start [json_args]
29
+ byob deploy [json_args]
30
+ byob billing [json_args]
31
+ byob payment-url [json_args]
32
+ byob env-status [json_args]
33
+ byob grants [json_args]
34
+ byob dns [json_args]
35
+ byob device-code [--client-name name]
36
+ byob token <device_code>
37
+ byob refresh <refresh_token>
38
+ byob config
39
+ byob codex install
40
+ byob codex sync-skills
41
+
42
+ Options:
43
+ --air-url <url> Default: BYOB_AIR_URL or https://air.api.byob.studio
44
+ --prim-url <url> Default: BYOB_PRIM_URL or https://prim.api.byob.studio
45
+ --project-id <id> Default: BYOB_PROJECT_ID
46
+ --projects <ids> Comma-separated fallback project ids. Default: BYOB_PROJECT_IDS
47
+ --multi-project Route tools/call by arguments.project_id when present
48
+ --token <token> Default: BYOB_AGENT_TOKEN
49
+ --timeout <seconds> Default: 60
50
+ --client-name <name> Used by device-code
51
+ --codex-bin <path> Default: codex
52
+ --server-name <name> Default: byob
53
+ --no-skill Do not install the byob_cli Codex skill
54
+ --no-air-skills Do not download BYOB AIR coding skills into Codex skills
55
+ --no-open Do not try to open the browser approval URL
56
+ `;
57
+ }
58
+
59
+ function parseArgs(argv) {
60
+ const opts = {
61
+ airUrl: DEFAULT_AIR_URL,
62
+ primUrl: DEFAULT_PRIM_URL,
63
+ projectId: process.env.BYOB_PROJECT_ID || "",
64
+ projectIds: (process.env.BYOB_PROJECT_IDS || "")
65
+ .split(",")
66
+ .map((value) => value.trim())
67
+ .filter(Boolean),
68
+ multiProject: process.env.BYOB_CLI_MULTI_PROJECT === "1",
69
+ token: process.env.BYOB_AGENT_TOKEN || "",
70
+ timeout: 60,
71
+ clientName: "Local coding agent",
72
+ codexBin: "codex",
73
+ serverName: "byob",
74
+ installSkill: true,
75
+ installAirSkills: true,
76
+ openBrowser: true,
77
+ rest: [],
78
+ };
79
+
80
+ for (let i = 0; i < argv.length; i += 1) {
81
+ const arg = argv[i];
82
+ if (arg === "--air-url") opts.airUrl = argv[++i] || "";
83
+ else if (arg === "--prim-url") opts.primUrl = argv[++i] || "";
84
+ else if (arg === "--project-id") opts.projectId = argv[++i] || "";
85
+ else if (arg === "--projects") {
86
+ opts.projectIds = (argv[++i] || "")
87
+ .split(",")
88
+ .map((value) => value.trim())
89
+ .filter(Boolean);
90
+ } else if (arg === "--multi-project") opts.multiProject = true;
91
+ else if (arg === "--token") opts.token = argv[++i] || "";
92
+ else if (arg === "--timeout") opts.timeout = Number(argv[++i] || "60");
93
+ else if (arg === "--client-name") opts.clientName = argv[++i] || "";
94
+ else if (arg === "--codex-bin") opts.codexBin = argv[++i] || "codex";
95
+ else if (arg === "--server-name") opts.serverName = argv[++i] || "byob";
96
+ else if (arg === "--no-skill") opts.installSkill = false;
97
+ else if (arg === "--no-air-skills") opts.installAirSkills = false;
98
+ else if (arg === "--no-open") opts.openBrowser = false;
99
+ else if (arg === "--help" || arg === "-h") opts.rest.push("help");
100
+ else opts.rest.push(arg);
101
+ }
102
+ return opts;
103
+ }
104
+
105
+ function requireProject(opts) {
106
+ const projectId = opts.projectId || opts.projectIds[0] || "";
107
+ if (!projectId) {
108
+ throw new Error(
109
+ "Missing project id. Pass --project-id, set BYOB_PROJECT_ID, or set BYOB_PROJECT_IDS.",
110
+ );
111
+ }
112
+ return projectId;
113
+ }
114
+
115
+ function requireToken(opts) {
116
+ if (!opts.token) {
117
+ throw new Error("Missing agent token. Pass --token or set BYOB_AGENT_TOKEN.");
118
+ }
119
+ return opts.token;
120
+ }
121
+
122
+ function parseJsonArg(raw) {
123
+ if (!raw) return {};
124
+ const parsed = JSON.parse(raw);
125
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
126
+ throw new Error("Tool arguments must be a JSON object.");
127
+ }
128
+ return parsed;
129
+ }
130
+
131
+ async function jsonRequest(method, url, { body, token, timeoutSeconds = 60 } = {}) {
132
+ const controller = new AbortController();
133
+ const timer = setTimeout(() => controller.abort(), timeoutSeconds * 1000);
134
+ const headers = { Accept: "application/json" };
135
+ if (body !== undefined) headers["Content-Type"] = "application/json";
136
+ if (token) headers.Authorization = `Bearer ${token}`;
137
+
138
+ try {
139
+ const res = await fetch(url, {
140
+ method,
141
+ headers,
142
+ body: body === undefined ? undefined : JSON.stringify(body),
143
+ signal: controller.signal,
144
+ });
145
+ const text = await res.text();
146
+ const payload = text ? JSON.parse(text) : {};
147
+ if (!res.ok) {
148
+ const error = new Error(`HTTP ${res.status}`);
149
+ error.payload = payload;
150
+ error.status = res.status;
151
+ error.url = url;
152
+ throw error;
153
+ }
154
+ return payload;
155
+ } finally {
156
+ clearTimeout(timer);
157
+ }
158
+ }
159
+
160
+ function mcpUrl(opts, projectId = requireProject(opts)) {
161
+ return `${opts.airUrl.replace(/\/+$/, "")}/mcp/projects/${projectId}`;
162
+ }
163
+
164
+ function defaultProjectIds(opts) {
165
+ return [...new Set([opts.projectId, ...opts.projectIds].filter(Boolean))];
166
+ }
167
+
168
+ function projectFromRequest(opts, request) {
169
+ if (!opts.multiProject) return requireProject(opts);
170
+ const params = request?.params || {};
171
+ const args = params.arguments || {};
172
+ let explicitProjectId = args.project_id || params.project_id;
173
+ if (!explicitProjectId && params.uri) {
174
+ const match = String(params.uri).match(/^byob:\/\/project\/([^/]+)\/context$/);
175
+ if (match) explicitProjectId = match[1];
176
+ }
177
+ if (explicitProjectId) return explicitProjectId;
178
+ return requireProject(opts);
179
+ }
180
+
181
+ async function mcpRequest(opts, method, params = {}, requestId = Date.now(), projectId) {
182
+ return jsonRequest("POST", mcpUrl(opts, projectId), {
183
+ token: requireToken(opts),
184
+ timeoutSeconds: opts.timeout,
185
+ body: {
186
+ jsonrpc: "2.0",
187
+ id: requestId,
188
+ method,
189
+ params,
190
+ },
191
+ });
192
+ }
193
+
194
+ function printJson(payload) {
195
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
196
+ }
197
+
198
+ function printMcpResponse(payload) {
199
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
200
+ }
201
+
202
+ function byobBanner() {
203
+ let logo = "";
204
+ try {
205
+ logo = readFileSync(join(PACKAGE_ROOT, "byob-logo.ascii"), "utf8").replace(/\s+$/, "");
206
+ } catch {
207
+ logo = "BYOB";
208
+ }
209
+ return `${logo}\n\nBuild Your Own Buzz\nhttps://byob.studio\n`;
210
+ }
211
+
212
+ function printWizardStep(message) {
213
+ process.stdout.write(`\n> ${message}\n`);
214
+ }
215
+
216
+ function commandExists(command) {
217
+ const result = spawnSync(
218
+ process.platform === "win32" ? "where" : "command",
219
+ process.platform === "win32" ? [command] : ["-v", command],
220
+ {
221
+ shell: process.platform !== "win32",
222
+ stdio: "ignore",
223
+ },
224
+ );
225
+ return result.status === 0;
226
+ }
227
+
228
+ function tryOpenUrl(url) {
229
+ if (!url) return false;
230
+ let command;
231
+ let args;
232
+
233
+ if (process.platform === "darwin") {
234
+ command = "open";
235
+ args = [url];
236
+ } else if (process.platform === "win32") {
237
+ command = "cmd";
238
+ args = ["/c", "start", "", url];
239
+ } else {
240
+ command = "xdg-open";
241
+ args = [url];
242
+ }
243
+
244
+ if (process.platform !== "win32" && !commandExists(command)) return false;
245
+
246
+ const child = spawn(command, args, {
247
+ detached: true,
248
+ stdio: "ignore",
249
+ shell: false,
250
+ });
251
+ child.unref();
252
+ return true;
253
+ }
254
+
255
+ async function serve(opts) {
256
+ requireProject(opts);
257
+ requireToken(opts);
258
+
259
+ const rl = createInterface({
260
+ input: process.stdin,
261
+ crlfDelay: Infinity,
262
+ });
263
+
264
+ for await (const line of rl) {
265
+ const raw = line.trim();
266
+ if (!raw) continue;
267
+
268
+ let request;
269
+ try {
270
+ request = JSON.parse(raw);
271
+ } catch (error) {
272
+ printMcpResponse({
273
+ jsonrpc: "2.0",
274
+ id: null,
275
+ error: { code: -32700, message: `Parse error: ${error.message}` },
276
+ });
277
+ continue;
278
+ }
279
+
280
+ if (request.id === undefined) {
281
+ continue;
282
+ }
283
+
284
+ try {
285
+ const projectId = projectFromRequest(opts, request);
286
+ const response = await jsonRequest("POST", mcpUrl(opts, projectId), {
287
+ token: opts.token,
288
+ timeoutSeconds: opts.timeout,
289
+ body: request,
290
+ });
291
+ printMcpResponse(response);
292
+ } catch (error) {
293
+ printMcpResponse({
294
+ jsonrpc: "2.0",
295
+ id: request.id ?? null,
296
+ error: {
297
+ code: -32603,
298
+ message: error.payload
299
+ ? JSON.stringify(error.payload)
300
+ : error.message || "BYOB CLI request failed",
301
+ },
302
+ });
303
+ }
304
+ }
305
+ }
306
+
307
+ function config(opts) {
308
+ const command = process.platform === "win32" ? "npx.cmd" : "npx";
309
+ const projectIds = defaultProjectIds(opts);
310
+ const snippet = {
311
+ mcpServers: {
312
+ byob: {
313
+ command,
314
+ args: [
315
+ "-y",
316
+ "byob-cli",
317
+ "mcp",
318
+ ...(opts.multiProject ? ["--multi-project"] : []),
319
+ ],
320
+ env: {
321
+ ...(opts.airUrl !== PROD_AIR_URL ? { BYOB_AIR_URL: opts.airUrl } : {}),
322
+ ...(opts.multiProject
323
+ ? {
324
+ BYOB_PROJECT_IDS: projectIds.length
325
+ ? projectIds.join(",")
326
+ : "<project_id_1>,<project_id_2>",
327
+ BYOB_CLI_MULTI_PROJECT: "1",
328
+ }
329
+ : { BYOB_PROJECT_ID: opts.projectId || "<project_id>" }),
330
+ BYOB_AGENT_TOKEN: opts.token ? "<agent_access_token>" : "<agent_access_token>",
331
+ },
332
+ },
333
+ },
334
+ };
335
+ printJson(snippet);
336
+ }
337
+
338
+ async function deviceCode(opts) {
339
+ const payload = await jsonRequest(
340
+ "POST",
341
+ `${opts.primUrl.replace(/\/+$/, "")}/oauth/device/code`,
342
+ { body: { client_name: opts.clientName }, timeoutSeconds: opts.timeout },
343
+ );
344
+ printJson(payload);
345
+ }
346
+
347
+ async function token(opts, deviceCodeValue) {
348
+ if (!deviceCodeValue) throw new Error("Missing device_code.");
349
+ const payload = await jsonRequest(
350
+ "POST",
351
+ `${opts.primUrl.replace(/\/+$/, "")}/oauth/token`,
352
+ {
353
+ body: {
354
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
355
+ device_code: deviceCodeValue,
356
+ },
357
+ timeoutSeconds: opts.timeout,
358
+ },
359
+ );
360
+ printJson(payload);
361
+ }
362
+
363
+ async function refresh(opts, refreshToken) {
364
+ if (!refreshToken) throw new Error("Missing refresh_token.");
365
+ const payload = await jsonRequest(
366
+ "POST",
367
+ `${opts.primUrl.replace(/\/+$/, "")}/oauth/token`,
368
+ {
369
+ body: {
370
+ grant_type: "refresh_token",
371
+ refresh_token: refreshToken,
372
+ },
373
+ timeoutSeconds: opts.timeout,
374
+ },
375
+ );
376
+ printJson(payload);
377
+ }
378
+
379
+ function projectIdsFromTokenPayload(payload) {
380
+ const fromScope = String(payload.scope || "")
381
+ .split(/\s+/)
382
+ .map((scope) => (scope.startsWith("project:") ? scope.slice("project:".length) : ""))
383
+ .filter(Boolean);
384
+ const fromPayload = Array.isArray(payload.project_ids) ? payload.project_ids : [];
385
+ return [...new Set([...fromPayload, ...fromScope].filter(Boolean))];
386
+ }
387
+
388
+ async function exchangeDeviceCode(opts, deviceCodeValue) {
389
+ return jsonRequest("POST", `${opts.primUrl.replace(/\/+$/, "")}/oauth/token`, {
390
+ body: {
391
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
392
+ device_code: deviceCodeValue,
393
+ },
394
+ timeoutSeconds: opts.timeout,
395
+ });
396
+ }
397
+
398
+ function sleep(ms) {
399
+ return new Promise((resolve) => setTimeout(resolve, ms));
400
+ }
401
+
402
+ async function pollDeviceToken(opts, devicePayload) {
403
+ const startedAt = Date.now();
404
+ const expiresIn = Number(devicePayload.expires_in || 600);
405
+ const intervalMs = Math.max(1, Number(devicePayload.interval || 5)) * 1000;
406
+
407
+ while (Date.now() - startedAt < expiresIn * 1000) {
408
+ try {
409
+ return await exchangeDeviceCode(opts, devicePayload.device_code);
410
+ } catch (error) {
411
+ const code =
412
+ error.payload?.error ||
413
+ error.payload?.code ||
414
+ error.payload?.detail?.error ||
415
+ error.payload?.detail?.code;
416
+ if (code === "authorization_pending") {
417
+ await sleep(intervalMs);
418
+ continue;
419
+ }
420
+ throw error;
421
+ }
422
+ }
423
+
424
+ throw new Error("Device authorization timed out. Run the install command again.");
425
+ }
426
+
427
+ function runCodexMcpAdd(opts, tokenPayload, projectIds) {
428
+ const multiProject = opts.multiProject || projectIds.length > 1;
429
+ const args = [
430
+ "mcp",
431
+ "add",
432
+ opts.serverName,
433
+ ...(opts.airUrl !== PROD_AIR_URL ? ["--env", `BYOB_AIR_URL=${opts.airUrl}`] : []),
434
+ ...(opts.primUrl !== PROD_PRIM_URL ? ["--env", `BYOB_PRIM_URL=${opts.primUrl}`] : []),
435
+ ...(multiProject
436
+ ? [
437
+ "--env",
438
+ `BYOB_PROJECT_IDS=${projectIds.join(",")}`,
439
+ "--env",
440
+ "BYOB_CLI_MULTI_PROJECT=1",
441
+ ]
442
+ : ["--env", `BYOB_PROJECT_ID=${projectIds[0]}`]),
443
+ "--env",
444
+ `BYOB_AGENT_TOKEN=${tokenPayload.access_token}`,
445
+ "--",
446
+ process.platform === "win32" ? "npx.cmd" : "npx",
447
+ "-y",
448
+ "byob-cli",
449
+ "mcp",
450
+ ...(multiProject ? ["--multi-project"] : []),
451
+ ];
452
+
453
+ const result = spawnSync(opts.codexBin, args, {
454
+ stdio: "inherit",
455
+ env: process.env,
456
+ });
457
+ if (result.error) {
458
+ throw new Error(`Failed to run ${opts.codexBin}: ${result.error.message}`);
459
+ }
460
+ if (result.status !== 0) {
461
+ throw new Error(`${opts.codexBin} mcp add failed with exit code ${result.status}`);
462
+ }
463
+ }
464
+
465
+ function installCodexSkill() {
466
+ const source = join(PACKAGE_ROOT, "skills", "byob_cli", "SKILL.md");
467
+ const targetDir = join(homedir(), ".agents", "skills", "byob_cli");
468
+ mkdirSync(targetDir, { recursive: true });
469
+ copyFileSync(source, join(targetDir, "SKILL.md"));
470
+ for (const entry of readdirSync(join(PACKAGE_ROOT, "skills", "byob_cli"))) {
471
+ if (entry === "SKILL.md") continue;
472
+ const sourcePath = join(PACKAGE_ROOT, "skills", "byob_cli", entry);
473
+ if (statSync(sourcePath).isFile()) {
474
+ copyFileSync(sourcePath, join(targetDir, entry));
475
+ }
476
+ }
477
+ return targetDir;
478
+ }
479
+
480
+ function codexSkillSlug(name) {
481
+ return String(name || "")
482
+ .trim()
483
+ .toLowerCase()
484
+ .replace(/^@/, "")
485
+ .replace(/[^a-z0-9_-]+/g, "-")
486
+ .replace(/^-+|-+$/g, "")
487
+ .slice(0, 80);
488
+ }
489
+
490
+ function codexSkillNameFromUri(uri) {
491
+ const raw = String(uri || "").replace("byob://coding-skills/", "");
492
+ try {
493
+ return decodeURIComponent(raw);
494
+ } catch {
495
+ return raw;
496
+ }
497
+ }
498
+
499
+ function rewriteSkillFrontmatterName(markdown, skillName, folderName) {
500
+ const safeName = folderName;
501
+ const body = String(markdown || "");
502
+ if (!body.startsWith("---")) {
503
+ return `---\nname: ${safeName}\ndescription: BYOB coding skill synced from AIR for ${skillName}.\n---\n\n${body}`;
504
+ }
505
+ return body.replace(/^---\s*\n([\s\S]*?)\n---/, (match, fm) => {
506
+ const withoutName = fm
507
+ .split("\n")
508
+ .filter((line) => !line.trim().startsWith("name:"))
509
+ .join("\n")
510
+ .trim();
511
+ return `---\nname: ${safeName}\n${withoutName ? `${withoutName}\n` : ""}---`;
512
+ });
513
+ }
514
+
515
+ async function installAirCodingSkills(opts, projectIds) {
516
+ const projectId = projectIds[0] || requireProject(opts);
517
+ const listResponse = await mcpRequest(opts, "resources/list", {}, Date.now(), projectId);
518
+ const resources = listResponse?.result?.resources || [];
519
+ const skillResources = resources.filter((resource) =>
520
+ String(resource?.uri || "").startsWith("byob://coding-skills/"),
521
+ );
522
+ const installed = [];
523
+ for (const resource of skillResources) {
524
+ const skillName = resource?._meta?.byob_coding_skill?.name || codexSkillNameFromUri(resource.uri);
525
+ const slug = codexSkillSlug(skillName);
526
+ if (!slug) continue;
527
+ const folderName = `byob_${slug}`;
528
+ const targetDir = join(homedir(), ".agents", "skills", folderName);
529
+ const readResponse = await mcpRequest(
530
+ opts,
531
+ "resources/read",
532
+ { uri: resource.uri },
533
+ Date.now(),
534
+ projectId,
535
+ );
536
+ const content = readResponse?.result?.contents?.[0]?.text || "";
537
+ if (!content.trim()) continue;
538
+ mkdirSync(targetDir, { recursive: true });
539
+ writeFileSync(
540
+ join(targetDir, "SKILL.md"),
541
+ rewriteSkillFrontmatterName(content, skillName, folderName),
542
+ "utf8",
543
+ );
544
+ installed.push(folderName);
545
+ }
546
+ return installed;
547
+ }
548
+
549
+ async function codexInstall(opts) {
550
+ let tokenPayload;
551
+ let projectIds = defaultProjectIds(opts);
552
+
553
+ process.stdout.write(byobBanner());
554
+ printWizardStep("Preparing Codex connection");
555
+
556
+ if (opts.token) {
557
+ printWizardStep("Using provided agent token");
558
+ tokenPayload = { access_token: opts.token, scope: projectIds.map((id) => `project:${id}`).join(" ") };
559
+ } else {
560
+ printWizardStep("Starting BYOB browser approval");
561
+ const devicePayload = await jsonRequest(
562
+ "POST",
563
+ `${opts.primUrl.replace(/\/+$/, "")}/oauth/device/code`,
564
+ { body: { client_name: opts.clientName || "Codex" }, timeoutSeconds: opts.timeout },
565
+ );
566
+ const approvalUrl = devicePayload.verification_uri_complete || devicePayload.verification_uri;
567
+ const opened = opts.openBrowser ? tryOpenUrl(approvalUrl) : false;
568
+ process.stdout.write(
569
+ [
570
+ "Open this URL to approve BYOB for Codex:",
571
+ approvalUrl,
572
+ opened ? "Opened the approval URL in your browser." : "",
573
+ "",
574
+ `User code: ${devicePayload.user_code}`,
575
+ "Waiting for approval...",
576
+ "",
577
+ ].join("\n"),
578
+ );
579
+ tokenPayload = await pollDeviceToken(opts, devicePayload);
580
+ projectIds = projectIdsFromTokenPayload(tokenPayload);
581
+ }
582
+
583
+ if (!tokenPayload.access_token) {
584
+ throw new Error("OAuth response did not include an access token.");
585
+ }
586
+ opts.token = tokenPayload.access_token;
587
+ if (!projectIds.length) {
588
+ throw new Error("No approved project ids were found. Approve at least one project or pass --project-id.");
589
+ }
590
+
591
+ printWizardStep("Writing Codex MCP config");
592
+ runCodexMcpAdd(opts, tokenPayload, projectIds);
593
+ printWizardStep("Installing BYOB Codex skill");
594
+ const skillDir = opts.installSkill ? installCodexSkill() : "";
595
+ let airSkillNames = [];
596
+ if (opts.installSkill && opts.installAirSkills) {
597
+ printWizardStep("Downloading BYOB AIR coding skills");
598
+ airSkillNames = await installAirCodingSkills(opts, projectIds);
599
+ }
600
+ process.stdout.write(
601
+ [
602
+ "",
603
+ `BYOB CLI installed for Codex as MCP server "${opts.serverName}" with ${projectIds.length} project(s).`,
604
+ skillDir ? `BYOB Codex skill installed at ${skillDir}.` : "",
605
+ airSkillNames.length
606
+ ? `BYOB AIR coding skills installed: ${airSkillNames.length}.`
607
+ : opts.installAirSkills
608
+ ? "No BYOB AIR coding skills were installed."
609
+ : "",
610
+ "Restart Codex or start a new session if the skill list is already open.",
611
+ "",
612
+ ]
613
+ .filter(Boolean)
614
+ .join("\n"),
615
+ );
616
+ }
617
+
618
+ async function codexSyncSkills(opts) {
619
+ const projectIds = defaultProjectIds(opts);
620
+ requireToken(opts);
621
+ requireProject(opts);
622
+
623
+ printWizardStep("Downloading BYOB AIR coding skills");
624
+ const airSkillNames = await installAirCodingSkills(opts, projectIds);
625
+ process.stdout.write(
626
+ [
627
+ "",
628
+ airSkillNames.length
629
+ ? `BYOB AIR coding skills synced: ${airSkillNames.length}.`
630
+ : "No BYOB AIR coding skills were synced.",
631
+ airSkillNames.length ? airSkillNames.join(", ") : "",
632
+ "Restart Codex or start a new session if the skill list is already open.",
633
+ "",
634
+ ]
635
+ .filter(Boolean)
636
+ .join("\n"),
637
+ );
638
+ }
639
+
640
+ async function callTool(opts, toolName, rawArgs) {
641
+ if (!toolName) throw new Error("Missing tool_name.");
642
+ const toolArgs = parseJsonArg(rawArgs);
643
+ const projectId = toolArgs.project_id || requireProject(opts);
644
+ const response = await mcpRequest(opts, "tools/call", {
645
+ name: toolName,
646
+ arguments: toolArgs,
647
+ }, Date.now(), projectId);
648
+ printJson(response);
649
+ }
650
+
651
+ async function aliasCall(opts, alias, rawArgs) {
652
+ const toolNames = {
653
+ status: "byob_project_status",
654
+ start: "byob_start_project",
655
+ deploy: "byob_deploy_project",
656
+ billing: "byob_billing_status",
657
+ credits: "byob_billing_status",
658
+ "payment-url": "byob_payment_url",
659
+ "env-status": "byob_project_env_status",
660
+ grants: "byob_list_agent_grants",
661
+ dns: "byob_custom_domain_dns_instructions",
662
+ };
663
+ await callTool(opts, toolNames[alias], rawArgs);
664
+ }
665
+
666
+ async function projects(opts) {
667
+ await callTool(opts, "byob_list_projects", "{}");
668
+ }
669
+
670
+ async function resources(opts) {
671
+ const response = await mcpRequest(opts, "resources/list");
672
+ printJson(response);
673
+ }
674
+
675
+ async function context(opts, rawArgs) {
676
+ const args = parseJsonArg(rawArgs);
677
+ const projectId = args.project_id || requireProject(opts);
678
+ const response = await mcpRequest(opts, "tools/call", {
679
+ name: "byob_current_project_context",
680
+ arguments: { project_id: projectId },
681
+ }, Date.now(), projectId);
682
+ printJson(response);
683
+ }
684
+
685
+ async function contextResource(opts, rawArgs) {
686
+ const args = parseJsonArg(rawArgs);
687
+ const projectId = args.project_id || requireProject(opts);
688
+ const uri = args.uri || `byob://project/${projectId}/context`;
689
+ const response = await mcpRequest(
690
+ opts,
691
+ "resources/read",
692
+ { uri },
693
+ Date.now(),
694
+ projectId,
695
+ );
696
+ printJson(response);
697
+ }
698
+
699
+ async function main() {
700
+ const opts = parseArgs(process.argv.slice(2));
701
+ const [command, first, second] = opts.rest;
702
+
703
+ if (!command || command === "help") {
704
+ process.stdout.write(usage());
705
+ return;
706
+ }
707
+
708
+ if (command === "mcp") return serve(opts);
709
+ if (command === "codex" && first === "install") return codexInstall(opts);
710
+ if (command === "codex" && first === "sync-skills") return codexSyncSkills(opts);
711
+ if (command === "config") return config(opts);
712
+ if (command === "device-code") return deviceCode(opts);
713
+ if (command === "token") return token(opts, first);
714
+ if (command === "refresh") return refresh(opts, first);
715
+ if (command === "context") return context(opts, first);
716
+ if (command === "context-resource") return contextResource(opts, first);
717
+ if (command === "resources") return resources(opts);
718
+ if (command === "projects") return projects(opts);
719
+ if (command === "initialize") return printJson(await mcpRequest(opts, "initialize"));
720
+ if (command === "tools") return printJson(await mcpRequest(opts, "tools/list"));
721
+ if (command === "call") return callTool(opts, first, second);
722
+ if (["status", "start", "deploy", "billing", "credits", "payment-url", "env-status", "grants", "dns"].includes(command)) {
723
+ return aliasCall(opts, command, first);
724
+ }
725
+
726
+ throw new Error(`Unknown command: ${command}`);
727
+ }
728
+
729
+ main().catch((error) => {
730
+ if (
731
+ error.status === 404 &&
732
+ typeof error.url === "string" &&
733
+ error.url.includes("/oauth/device/code")
734
+ ) {
735
+ process.stderr.write(
736
+ [
737
+ `BYOB OAuth device endpoint was not found at ${error.url}.`,
738
+ "If you are testing locally, pass --prim-url http://localhost:8087 --air-url http://localhost:8086.",
739
+ "If you are using production, deploy PRIM with the Agent OAuth routes first.",
740
+ "",
741
+ ].join("\n"),
742
+ );
743
+ process.exitCode = 1;
744
+ return;
745
+ }
746
+ if (error.payload) {
747
+ printJson(error.payload);
748
+ } else {
749
+ process.stderr.write(`${error.message}\n`);
750
+ }
751
+ process.exitCode = 1;
752
+ });