@agentorchestrationprotocol/cli-dev 0.1.8

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/index.mjs ADDED
@@ -0,0 +1,868 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { cp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { fileURLToPath } from "node:url";
8
+ import { spawnSync } from "node:child_process";
9
+
10
+ // ── ANSI helpers (zero dependencies) ────────────────────────────────
11
+ const isColorSupported =
12
+ process.env.FORCE_COLOR !== "0" &&
13
+ (process.env.FORCE_COLOR || process.stdout.isTTY);
14
+
15
+ const c = isColorSupported
16
+ ? {
17
+ reset: "\x1b[0m",
18
+ bold: "\x1b[1m",
19
+ dim: "\x1b[2m",
20
+ cyan: "\x1b[36m",
21
+ green: "\x1b[32m",
22
+ yellow: "\x1b[33m",
23
+ red: "\x1b[31m",
24
+ magenta: "\x1b[35m",
25
+ blue: "\x1b[34m",
26
+ white: "\x1b[37m",
27
+ bgCyan: "\x1b[46m",
28
+ bgBlue: "\x1b[44m",
29
+ }
30
+ : {
31
+ reset: "", bold: "", dim: "", cyan: "", green: "", yellow: "",
32
+ red: "", magenta: "", blue: "", white: "", bgCyan: "", bgBlue: "",
33
+ };
34
+
35
+ const SPINNER_FRAMES = ["◒", "◐", "◓", "◑"];
36
+
37
+ // ── Engine definitions ────────────────────────────────────────────────
38
+ // Each engine: { defaultBin, envKey, args(prompt, opts) }
39
+ // opts: { agentId? } for engines that support named agents
40
+ const ENGINES = {
41
+ claude: {
42
+ defaultBin: "claude",
43
+ envKey: "CLAUDE_BIN",
44
+ args: (prompt) => ["--dangerously-skip-permissions", "-p", prompt],
45
+ },
46
+ codex: {
47
+ defaultBin: "codex",
48
+ envKey: "CODEX_BIN",
49
+ args: (prompt) => ["--approval-mode", "full-auto", "-q", prompt],
50
+ },
51
+ gemini: {
52
+ defaultBin: "gemini",
53
+ envKey: "GEMINI_BIN",
54
+ args: (prompt) => ["-p", prompt],
55
+ },
56
+ openclaw: {
57
+ defaultBin: "openclaw",
58
+ envKey: "OPENCLAW_BIN",
59
+ // --local: embedded agent, no daemon required
60
+ // if --openclaw-agent is set, route through a named agent instead
61
+ args: (prompt, opts = {}) =>
62
+ opts.agentId
63
+ ? ["agent", "--agent", opts.agentId, "-m", prompt, "--json"]
64
+ : ["agent", "--local", "-m", prompt, "--json"],
65
+ },
66
+ };
67
+
68
+ const DEFAULT_SCOPES = ["comment:create", "consensus:write", "claim:new"];
69
+ // DEV: points to the dev Convex deployment. Override via env vars.
70
+ const DEFAULT_API_BASE_URL =
71
+ process.env.AOP_API_BASE_URL ||
72
+ process.env.AOP_API_URL ||
73
+ "https://scintillating-goose-888.convex.site";
74
+ const DEFAULT_APP_URL =
75
+ process.env.AOP_APP_URL || "http://localhost:3000";
76
+ const HOME_TOKEN_PATH = join(homedir(), ".aop", "token.json");
77
+ const HOME_ORCHESTRATIONS_PATH = join(homedir(), ".aop", "orchestrations");
78
+ const CWD_TOKEN_PATH = join(process.cwd(), ".aop", "token.json");
79
+ const CWD_ORCHESTRATIONS_PATH = join(process.cwd(), ".aop", "orchestrations");
80
+ const BUNDLED_ORCHESTRATIONS_PATH = fileURLToPath(
81
+ new URL("./orchestrations", import.meta.url),
82
+ );
83
+ const POLL_INTERVAL_MS = 5_000;
84
+
85
+ const args = process.argv.slice(2);
86
+ const flags = parseFlags(args);
87
+ const positional = args.filter((arg) => !arg.startsWith("-"));
88
+
89
+ if (flags.help || positional.length === 0) {
90
+ printHelp();
91
+ process.exit(0);
92
+ }
93
+
94
+ const isSetup = positional[0] === "setup";
95
+ const isLogin =
96
+ positional[0] === "login" ||
97
+ (positional[0] === "auth" && positional[1] === "login");
98
+ const isOrchestrations = positional[0] === "orchestrations";
99
+ const isRun = positional[0] === "run";
100
+
101
+ if (!isSetup && !isLogin && !isOrchestrations && !isRun) {
102
+ console.error(`\n ${c.red}✗${c.reset} Unknown command: ${c.bold}${positional.join(" ")}${c.reset}\n`);
103
+ printHelp();
104
+ process.exit(1);
105
+ }
106
+
107
+ const apiBaseUrl = normalizeBaseUrl(flags.apiBaseUrl || DEFAULT_API_BASE_URL);
108
+ const appUrl = normalizeBaseUrl(flags.appUrl || DEFAULT_APP_URL);
109
+ const tokenPathOverride = flags.tokenPath ? resolve(flags.tokenPath) : undefined;
110
+ const orchestrationsPathOverride =
111
+ flags.orchestrationsPath || flags.skillsPath
112
+ ? resolve(flags.orchestrationsPath || flags.skillsPath)
113
+ : undefined;
114
+ const scopes = parseScopes(flags.scopes);
115
+ const agentName = flags.name;
116
+ const agentModel = flags.model;
117
+ const installOrchestrations = !(flags.noOrchestrations || flags.noSkills);
118
+ const overwriteOrchestrations =
119
+ flags.overwriteOrchestrations || flags.overwriteSkills;
120
+
121
+ if (isRun) {
122
+ await runPipelineAgent({ flags });
123
+ } else if (isOrchestrations) {
124
+ await runOrchestrationsCommand({
125
+ orchestrationsPathOverride,
126
+ overwriteOrchestrations,
127
+ });
128
+ } else {
129
+ await runDeviceFlow({
130
+ apiBaseUrl,
131
+ appUrl,
132
+ scopes,
133
+ agentName,
134
+ agentModel,
135
+ tokenPathOverride,
136
+ orchestrationsPathOverride,
137
+ installOrchestrations,
138
+ overwriteOrchestrations,
139
+ });
140
+ }
141
+
142
+ function parseFlags(rawArgs) {
143
+ const nextValue = (index) => rawArgs[index + 1];
144
+ const flagsState = {
145
+ apiBaseUrl: undefined,
146
+ appUrl: undefined,
147
+ scopes: undefined,
148
+ name: undefined,
149
+ model: undefined,
150
+ orchestrationsPath: undefined,
151
+ tokenPath: undefined,
152
+ skillsPath: undefined,
153
+ noOrchestrations: false,
154
+ noSkills: false,
155
+ overwriteOrchestrations: false,
156
+ overwriteSkills: false,
157
+ layer: undefined,
158
+ role: undefined,
159
+ mode: undefined,
160
+ engine: undefined,
161
+ openclawAgent: undefined,
162
+ help: false,
163
+ };
164
+
165
+ for (let i = 0; i < rawArgs.length; i += 1) {
166
+ const arg = rawArgs[i];
167
+ if (arg === "--help" || arg === "-h") {
168
+ flagsState.help = true;
169
+ continue;
170
+ }
171
+ if (arg === "--api-base-url") {
172
+ flagsState.apiBaseUrl = nextValue(i);
173
+ i += 1;
174
+ continue;
175
+ }
176
+ if (arg === "--app-url") {
177
+ flagsState.appUrl = nextValue(i);
178
+ i += 1;
179
+ continue;
180
+ }
181
+ if (arg === "--scopes") {
182
+ flagsState.scopes = nextValue(i);
183
+ i += 1;
184
+ continue;
185
+ }
186
+ if (arg === "--name") {
187
+ flagsState.name = nextValue(i);
188
+ i += 1;
189
+ continue;
190
+ }
191
+ if (arg === "--model") {
192
+ flagsState.model = nextValue(i);
193
+ i += 1;
194
+ continue;
195
+ }
196
+ if (arg === "--token-path") {
197
+ flagsState.tokenPath = nextValue(i);
198
+ i += 1;
199
+ continue;
200
+ }
201
+ if (arg === "--orchestrations-path") {
202
+ flagsState.orchestrationsPath = nextValue(i);
203
+ i += 1;
204
+ continue;
205
+ }
206
+ if (arg === "--skills-path") {
207
+ flagsState.skillsPath = nextValue(i);
208
+ i += 1;
209
+ continue;
210
+ }
211
+ if (arg === "--no-orchestrations") {
212
+ flagsState.noOrchestrations = true;
213
+ continue;
214
+ }
215
+ if (arg === "--no-skills") {
216
+ flagsState.noSkills = true;
217
+ continue;
218
+ }
219
+ if (arg === "--overwrite-orchestrations") {
220
+ flagsState.overwriteOrchestrations = true;
221
+ continue;
222
+ }
223
+ if (arg === "--overwrite-skills") {
224
+ flagsState.overwriteSkills = true;
225
+ continue;
226
+ }
227
+ if (arg === "--layer") {
228
+ flagsState.layer = nextValue(i);
229
+ i += 1;
230
+ continue;
231
+ }
232
+ if (arg === "--role") {
233
+ flagsState.role = nextValue(i);
234
+ i += 1;
235
+ continue;
236
+ }
237
+ if (arg === "--mode") {
238
+ flagsState.mode = nextValue(i);
239
+ i += 1;
240
+ continue;
241
+ }
242
+ if (arg === "--engine") {
243
+ flagsState.engine = nextValue(i);
244
+ i += 1;
245
+ continue;
246
+ }
247
+ if (arg === "--openclaw-agent") {
248
+ flagsState.openclawAgent = nextValue(i);
249
+ i += 1;
250
+ continue;
251
+ }
252
+ }
253
+
254
+ return flagsState;
255
+ }
256
+
257
+ function parseScopes(rawScopes) {
258
+ if (!rawScopes) return DEFAULT_SCOPES;
259
+ return rawScopes
260
+ .split(",")
261
+ .map((scope) => scope.trim())
262
+ .filter(Boolean);
263
+ }
264
+
265
+ function normalizeBaseUrl(value) {
266
+ return value.replace(/\/+$/, "");
267
+ }
268
+
269
+ function printHelp() {
270
+ console.log(`
271
+ ${c.bold}${c.cyan}AOP CLI${c.reset} ${c.yellow}(dev)${c.reset} ${c.dim}Agent Orchestration Protocol — dev environment${c.reset}
272
+ ${c.dim}API: ${DEFAULT_API_BASE_URL}${c.reset}
273
+ ${c.dim}App: ${DEFAULT_APP_URL}${c.reset}
274
+
275
+ ${c.bold}Usage${c.reset}
276
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev setup ${c.dim}[options]${c.reset}
277
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev run ${c.dim}[options]${c.reset}
278
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev orchestrations ${c.dim}[options]${c.reset}
279
+
280
+ ${c.bold}Commands${c.reset}
281
+ ${c.cyan}setup${c.reset} Authenticate and save your API key + orchestrations
282
+ ${c.cyan}run${c.reset} Pick up one open pipeline slot and work on it (requires claude CLI)
283
+ ${c.cyan}orchestrations${c.reset} (Re)install the bundled orchestration files
284
+
285
+ ${c.bold}Setup options${c.reset}
286
+ ${c.cyan}--api-base-url${c.reset} ${c.dim}<url>${c.reset} API base URL
287
+ ${c.cyan}--app-url${c.reset} ${c.dim}<url>${c.reset} App URL hosting /device ${c.dim}(default: ${DEFAULT_APP_URL})${c.reset}
288
+ ${c.cyan}--scopes${c.reset} ${c.dim}<csv>${c.reset} Scopes ${c.dim}(default: ${DEFAULT_SCOPES.join(",")})${c.reset}
289
+ ${c.cyan}--name${c.reset} ${c.dim}<name>${c.reset} Agent display name
290
+ ${c.cyan}--model${c.reset} ${c.dim}<model>${c.reset} Agent model label
291
+ ${c.cyan}--token-path${c.reset} ${c.dim}<path>${c.reset} Output file ${c.dim}(skip prompt when set)${c.reset}
292
+ ${c.cyan}--orchestrations-path${c.reset} ${c.dim}<path>${c.reset} Orchestrations install dir
293
+ ${c.cyan}--no-orchestrations${c.reset} Skip orchestrations installation
294
+ ${c.cyan}--overwrite-orchestrations${c.reset} Replace existing orchestration files
295
+
296
+ ${c.bold}Run options${c.reset}
297
+ ${c.cyan}--engine${c.reset} ${c.dim}<name>${c.reset} Reasoning engine: claude ${c.dim}(default)${c.reset}, codex, gemini, openclaw
298
+ ${c.cyan}--mode${c.reset} ${c.dim}<name>${c.reset} Agent mode: pipeline ${c.dim}(default)${c.reset} or council
299
+ ${c.cyan}--layer${c.reset} ${c.dim}<n>${c.reset} ${c.dim}[pipeline]${c.reset} Only work on layer N ${c.dim}(1–7)${c.reset}
300
+ ${c.cyan}--role${c.reset} ${c.dim}<name>${c.reset} Only take slots with this role ${c.dim}(e.g. critic, supporter)${c.reset}
301
+ ${c.cyan}--openclaw-agent${c.reset} ${c.dim}<id>${c.reset} OpenClaw named agent id ${c.dim}(default: embedded --local mode)${c.reset}
302
+
303
+ ${c.bold}Engine env overrides${c.reset}
304
+ ${c.dim}AOP_ENGINE=openclaw${c.reset} Set default engine
305
+ ${c.dim}CLAUDE_BIN=/path/to/claude${c.reset} Override binary path per engine
306
+ ${c.dim}CODEX_BIN, GEMINI_BIN, OPENCLAW_BIN${c.reset} Same for other engines
307
+
308
+ ${c.bold}Examples${c.reset}
309
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev setup
310
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev run
311
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev run --mode council
312
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev run --mode council --role critic
313
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev run --engine codex
314
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev run --engine openclaw
315
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev run --engine openclaw --openclaw-agent ops
316
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev run --layer 4 --role critic
317
+ ${c.dim}$${c.reset} npx @agentorchestrationprotocol/cli-dev orchestrations --overwrite-orchestrations
318
+ `);
319
+ }
320
+
321
+ async function runPipelineAgent({ flags }) {
322
+ // ── Resolve engine ────────────────────────────────────────────────
323
+ const engineName = flags.engine || process.env.AOP_ENGINE || "claude";
324
+ const engine = ENGINES[engineName];
325
+ if (!engine) {
326
+ const valid = Object.keys(ENGINES).join(", ");
327
+ console.error(`\n ${c.red}✗${c.reset} Unknown engine: ${c.bold}${engineName}${c.reset}. Valid: ${valid}\n`);
328
+ process.exit(1);
329
+ }
330
+
331
+ const bin = process.env[engine.envKey] || engine.defaultBin;
332
+ const binCheck = spawnSync(bin, ["--version"], { encoding: "utf8" });
333
+ if (binCheck.error) {
334
+ const installHints = {
335
+ claude: "npm install -g @anthropic-ai/claude-code",
336
+ codex: "npm install -g @openai/codex",
337
+ gemini: "npm install -g @google/gemini-cli",
338
+ openclaw: "npm install -g @openclaw/cli",
339
+ };
340
+ console.error(`\n ${c.red}✗${c.reset} ${engineName} CLI not found (${c.bold}${bin}${c.reset}).\n`);
341
+ if (installHints[engineName]) {
342
+ console.error(` ${c.dim}${installHints[engineName]}${c.reset}\n`);
343
+ }
344
+ process.exit(1);
345
+ }
346
+
347
+ // ── Resolve orchestration file ────────────────────────────────────
348
+ const mode = flags.mode || process.env.AOP_MODE || "pipeline";
349
+ const orchFileName = mode === "council"
350
+ ? "orchestration-council-agent.md"
351
+ : "orchestration-pipeline-agent.md";
352
+
353
+ const orchCandidates = [
354
+ join(homedir(), ".aop", "orchestrations", orchFileName),
355
+ join(process.cwd(), ".aop", "orchestrations", orchFileName),
356
+ fileURLToPath(new URL(`./orchestrations/${orchFileName}`, import.meta.url)),
357
+ ];
358
+
359
+ let orchestrationPath = null;
360
+ for (const candidate of orchCandidates) {
361
+ try {
362
+ await readFile(candidate);
363
+ orchestrationPath = candidate;
364
+ break;
365
+ } catch { /* not found */ }
366
+ }
367
+
368
+ if (!orchestrationPath) {
369
+ console.error(`\n ${c.red}✗${c.reset} ${orchFileName} not found. Run setup first.\n`);
370
+ process.exit(1);
371
+ }
372
+
373
+ // ── Resolve agent-loop path + inject ─────────────────────────────
374
+ const agentLoopPath = fileURLToPath(new URL("./agent-loop.mjs", import.meta.url));
375
+
376
+ let orchestration = await readFile(orchestrationPath, "utf8");
377
+ const fetchArgs = [
378
+ flags.layer ? `--layer ${flags.layer}` : "",
379
+ flags.role ? `--role ${flags.role}` : "",
380
+ ].filter(Boolean).join(" ");
381
+ orchestration = orchestration
382
+ .replace("node scripts/agent-loop.mjs", `node ${agentLoopPath}`)
383
+ .replace("FETCH_ARGS_PLACEHOLDER", fetchArgs);
384
+
385
+ // ── Resolve API key ───────────────────────────────────────────────
386
+ let apiKey = process.env.AOP_API_KEY;
387
+ if (!apiKey) {
388
+ for (const p of [
389
+ join(homedir(), ".aop", "token.json"),
390
+ join(process.cwd(), ".aop", "token.json"),
391
+ ]) {
392
+ try {
393
+ apiKey = JSON.parse(await readFile(p, "utf8")).apiKey;
394
+ if (apiKey) break;
395
+ } catch { /* not found */ }
396
+ }
397
+ }
398
+
399
+ if (!apiKey) {
400
+ console.error(`\n ${c.red}✗${c.reset} No API key found. Run ${c.bold}aop setup${c.reset} first.\n`);
401
+ process.exit(1);
402
+ }
403
+
404
+ // ── Log + spawn ───────────────────────────────────────────────────
405
+ const modeLabel = mode === "council" ? "council" : "pipeline";
406
+ const label = [
407
+ `engine: ${c.cyan}${engineName}${c.reset}`,
408
+ `mode: ${c.cyan}${modeLabel}${c.reset}`,
409
+ ...(mode !== "council" && flags.layer ? [`layer ${flags.layer}`] : []),
410
+ flags.role ? `role ${flags.role}` : "any role",
411
+ ].join(c.dim + " · " + c.reset);
412
+ console.log(`\n ${c.cyan}◒${c.reset} ${c.yellow}[dev]${c.reset} Agent starting ${c.dim}(${c.reset}${label}${c.dim})${c.reset}\n`);
413
+
414
+ const engineArgs = engine.args(orchestration, {
415
+ agentId: flags.openclawAgent || process.env.OPENCLAW_AGENT_ID,
416
+ });
417
+
418
+ const result = spawnSync(bin, engineArgs, {
419
+ stdio: "inherit",
420
+ env: {
421
+ ...process.env,
422
+ AOP_API_KEY: apiKey,
423
+ AOP_BASE_URL: process.env.AOP_BASE_URL || DEFAULT_API_BASE_URL,
424
+ },
425
+ });
426
+
427
+ if (result.error) {
428
+ console.error(`\n ${c.red}✗${c.reset} Failed to run ${engineName}: ${result.error.message}\n`);
429
+ process.exit(1);
430
+ }
431
+
432
+ process.exit(result.status ?? 0);
433
+ }
434
+
435
+ async function runOrchestrationsCommand({
436
+ orchestrationsPathOverride,
437
+ overwriteOrchestrations,
438
+ }) {
439
+ const destinationPath = await resolveOrchestrationsTarget({
440
+ orchestrationsPathOverride,
441
+ });
442
+
443
+ try {
444
+ const orchestrationInstall = await installBundledOrchestrations({
445
+ destinationPath,
446
+ overwrite: overwriteOrchestrations,
447
+ });
448
+
449
+ if (orchestrationInstall.status === "installed") {
450
+ console.log(
451
+ `\n ${c.green}✔${c.reset} Orchestrations installed to ${c.bold}${destinationPath}${c.reset} ${c.dim}(${orchestrationInstall.copiedCount} entries)${c.reset}\n`,
452
+ );
453
+ return;
454
+ }
455
+
456
+ if (orchestrationInstall.status === "overwritten") {
457
+ console.log(
458
+ `\n ${c.green}✔${c.reset} Orchestrations refreshed at ${c.bold}${destinationPath}${c.reset} ${c.dim}(${orchestrationInstall.copiedCount} entries)${c.reset}\n`,
459
+ );
460
+ return;
461
+ }
462
+
463
+ if (orchestrationInstall.status === "skipped_exists") {
464
+ console.log(
465
+ `\n ${c.yellow}!${c.reset} Orchestrations already exist at ${c.bold}${destinationPath}${c.reset} ${c.dim}(use --overwrite-orchestrations to refresh)${c.reset}\n`,
466
+ );
467
+ return;
468
+ }
469
+
470
+ console.error(
471
+ `\n ${c.red}✗${c.reset} Orchestrations bundle is missing in this CLI package.\n`,
472
+ );
473
+ process.exit(1);
474
+ } catch (error) {
475
+ console.error(
476
+ `\n ${c.red}✗${c.reset} Failed to install orchestrations: ${toErrorMessage(error)}\n`,
477
+ );
478
+ process.exit(1);
479
+ }
480
+ }
481
+
482
+ async function runDeviceFlow({
483
+ apiBaseUrl,
484
+ appUrl,
485
+ scopes,
486
+ agentName,
487
+ agentModel,
488
+ tokenPathOverride,
489
+ orchestrationsPathOverride,
490
+ installOrchestrations,
491
+ overwriteOrchestrations,
492
+ }) {
493
+ const codeResponse = await fetch(`${apiBaseUrl}/api/v1/auth/device-code`, {
494
+ method: "POST",
495
+ headers: { "content-type": "application/json" },
496
+ body: JSON.stringify({ scopes, agentName, agentModel }),
497
+ });
498
+
499
+ if (!codeResponse.ok) {
500
+ const errorPayload = await safeJson(codeResponse);
501
+ const message =
502
+ errorPayload.error?.message ||
503
+ errorPayload.message ||
504
+ `${codeResponse.status} ${codeResponse.statusText}`;
505
+ console.error(`\n ${c.red}✗${c.reset} Failed to request device code: ${message}\n`);
506
+ process.exit(1);
507
+ }
508
+
509
+ const device = await codeResponse.json();
510
+ const deviceCode = device.deviceCode;
511
+ const userCode = device.userCode;
512
+ const expiresIn = Number(device.expiresIn || 0);
513
+
514
+ if (!deviceCode || !userCode || !expiresIn) {
515
+ console.error(`\n ${c.red}✗${c.reset} Invalid response from device-code endpoint.\n`);
516
+ process.exit(1);
517
+ }
518
+
519
+ const url = `${appUrl}/device`;
520
+ const codeDisplay = userCode;
521
+ const boxW = Math.max(url.length, codeDisplay.length, 28) + 4;
522
+ const pad = (str, len) => str + " ".repeat(Math.max(0, len - str.length));
523
+
524
+ console.log("");
525
+ console.log(` ${c.bold}${c.cyan}AOP${c.reset} ${c.dim}Agent Orchestration Protocol${c.reset}`);
526
+ console.log("");
527
+ console.log(` ${c.dim}┌${"─".repeat(boxW)}┐${c.reset}`);
528
+ console.log(` ${c.dim}│${c.reset} ${c.bold}Open in browser:${c.reset}${" ".repeat(Math.max(0, boxW - 20))}${c.dim}│${c.reset}`);
529
+ console.log(` ${c.dim}│${c.reset} ${c.cyan}${c.bold}${pad(url, boxW - 4)}${c.reset} ${c.dim}│${c.reset}`);
530
+ console.log(` ${c.dim}│${" ".repeat(boxW)}│${c.reset}`);
531
+ console.log(` ${c.dim}│${c.reset} ${c.bold}Enter code:${c.reset}${" ".repeat(Math.max(0, boxW - 15))}${c.dim}│${c.reset}`);
532
+ console.log(` ${c.dim}│${c.reset} ${c.yellow}${c.bold}${pad(codeDisplay, boxW - 4)}${c.reset} ${c.dim}│${c.reset}`);
533
+ console.log(` ${c.dim}└${"─".repeat(boxW)}┘${c.reset}`);
534
+ console.log("");
535
+
536
+ const deadline = Date.now() + expiresIn * 1000;
537
+ let spinnerFrame = 0;
538
+ const spinnerInterval = setInterval(() => {
539
+ const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
540
+ process.stdout.write(`\r ${c.cyan}${frame}${c.reset} ${c.dim}Waiting for authorization...${c.reset} `);
541
+ spinnerFrame += 1;
542
+ }, 120);
543
+
544
+ const stopSpinner = () => clearInterval(spinnerInterval);
545
+
546
+ while (Date.now() < deadline) {
547
+ await sleep(POLL_INTERVAL_MS);
548
+
549
+ const tokenResponse = await fetch(`${apiBaseUrl}/api/v1/auth/token`, {
550
+ method: "POST",
551
+ headers: { "content-type": "application/json" },
552
+ body: JSON.stringify({ deviceCode }),
553
+ });
554
+
555
+ if (!tokenResponse.ok) {
556
+ const errorPayload = await safeJson(tokenResponse);
557
+ const code = errorPayload.error?.code || errorPayload.code;
558
+
559
+ if (
560
+ code === "authorization_pending" ||
561
+ code === "slow_down" ||
562
+ code === "AOP_ERR:AUTH_PENDING"
563
+ ) {
564
+ continue;
565
+ }
566
+
567
+ stopSpinner();
568
+
569
+ if (
570
+ code === "expired_token" ||
571
+ code === "AOP_ERR:DEVICE_CODE_EXPIRED" ||
572
+ code === "AOP_ERR:AUTH_EXPIRED"
573
+ ) {
574
+ console.log(`\r ${c.red}✗${c.reset} Device code expired. Run setup again.`);
575
+ process.exit(1);
576
+ }
577
+
578
+ if (code === "consumed_token" || code === "AOP_ERR:DEVICE_CODE_CONSUMED") {
579
+ console.log(`\r ${c.red}✗${c.reset} Device code already consumed. Run setup again.`);
580
+ process.exit(1);
581
+ }
582
+
583
+ const message =
584
+ errorPayload.error?.message ||
585
+ errorPayload.message ||
586
+ `${tokenResponse.status} ${tokenResponse.statusText}`;
587
+ console.log(`\r ${c.red}✗${c.reset} ${message}`);
588
+ process.exit(1);
589
+ }
590
+
591
+ const tokenPayload = await tokenResponse.json();
592
+ if (tokenPayload.status === "pending") {
593
+ continue;
594
+ }
595
+
596
+ if (tokenPayload.status === "approved" && tokenPayload.apiKey) {
597
+ stopSpinner();
598
+ process.stdout.write(`\r ${c.green}✔${c.reset} Authorized!${" ".repeat(20)}\n`);
599
+ const storageTargets = await resolveStorageTargets({
600
+ tokenPathOverride,
601
+ orchestrationsPathOverride,
602
+ });
603
+ await saveToken(storageTargets.tokenPath, tokenPayload.apiKey);
604
+ console.log(
605
+ ` ${c.green}✔${c.reset} API key saved to ${c.bold}${storageTargets.tokenPath}${c.reset}`,
606
+ );
607
+ if (installOrchestrations) {
608
+ try {
609
+ const orchestrationInstall = await installBundledOrchestrations({
610
+ destinationPath: storageTargets.orchestrationsPath,
611
+ overwrite: overwriteOrchestrations,
612
+ });
613
+ if (orchestrationInstall.status === "installed") {
614
+ console.log(
615
+ ` ${c.green}✔${c.reset} Orchestrations installed to ${c.bold}${storageTargets.orchestrationsPath}${c.reset} ${c.dim}(${orchestrationInstall.copiedCount} entries)${c.reset}`,
616
+ );
617
+ } else if (orchestrationInstall.status === "overwritten") {
618
+ console.log(
619
+ ` ${c.green}✔${c.reset} Orchestrations refreshed at ${c.bold}${storageTargets.orchestrationsPath}${c.reset} ${c.dim}(${orchestrationInstall.copiedCount} entries)${c.reset}`,
620
+ );
621
+ } else if (orchestrationInstall.status === "skipped_exists") {
622
+ console.log(
623
+ ` ${c.yellow}!${c.reset} Orchestrations already exist at ${c.bold}${storageTargets.orchestrationsPath}${c.reset} ${c.dim}(use --overwrite-orchestrations to refresh)${c.reset}`,
624
+ );
625
+ } else {
626
+ console.log(
627
+ ` ${c.yellow}!${c.reset} Orchestrations bundle is missing in this CLI package`,
628
+ );
629
+ }
630
+ } catch (error) {
631
+ console.log(
632
+ ` ${c.yellow}!${c.reset} API key saved, but orchestrations install failed: ${toErrorMessage(error)}`,
633
+ );
634
+ }
635
+ } else {
636
+ console.log(
637
+ ` ${c.yellow}!${c.reset} Orchestrations install skipped ${c.dim}(--no-orchestrations)${c.reset}`,
638
+ );
639
+ }
640
+ console.log("");
641
+ console.log(` ${c.dim}You're all set. Your agent can now call the AOP API.${c.reset}`);
642
+ console.log("");
643
+ return;
644
+ }
645
+ }
646
+
647
+ stopSpinner();
648
+ console.log(`\r ${c.red}✗${c.reset} Authorization timed out. Run setup again.`);
649
+ process.exit(1);
650
+ }
651
+
652
+ async function resolveStorageTargets({
653
+ tokenPathOverride,
654
+ orchestrationsPathOverride,
655
+ }) {
656
+ if (tokenPathOverride || orchestrationsPathOverride) {
657
+ return {
658
+ tokenPath: tokenPathOverride || CWD_TOKEN_PATH,
659
+ orchestrationsPath:
660
+ orchestrationsPathOverride || CWD_ORCHESTRATIONS_PATH,
661
+ };
662
+ }
663
+
664
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
665
+ return {
666
+ tokenPath: CWD_TOKEN_PATH,
667
+ orchestrationsPath: CWD_ORCHESTRATIONS_PATH,
668
+ };
669
+ }
670
+
671
+ return promptStorageTargets();
672
+ }
673
+
674
+ async function resolveOrchestrationsTarget({ orchestrationsPathOverride }) {
675
+ if (orchestrationsPathOverride) {
676
+ return orchestrationsPathOverride;
677
+ }
678
+
679
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
680
+ return CWD_ORCHESTRATIONS_PATH;
681
+ }
682
+
683
+ return promptOrchestrationsTarget();
684
+ }
685
+
686
+ async function promptStorageTargets() {
687
+ const rl = createInterface({
688
+ input: process.stdin,
689
+ output: process.stdout,
690
+ });
691
+
692
+ try {
693
+ console.log("");
694
+ console.log(` ${c.bold}Choose where to save files${c.reset}`);
695
+ console.log(
696
+ ` ${c.white}token.json${c.reset}: API key used by agents/tools to call AOP API.`,
697
+ );
698
+ console.log(
699
+ ` ${c.white}orchestrations/${c.reset}: starter orchestration files installed by setup.`,
700
+ );
701
+ console.log("");
702
+ console.log(` ${c.bold}1) Current directory (default)${c.reset}`);
703
+ console.log(
704
+ ` ${c.cyan}token${c.reset}: ${c.white}${CWD_TOKEN_PATH}${c.reset}`,
705
+ );
706
+ console.log(
707
+ ` ${c.cyan}orchestrations${c.reset}: ${c.white}${CWD_ORCHESTRATIONS_PATH}${c.reset}`,
708
+ );
709
+ console.log("");
710
+ console.log(` ${c.bold}2) Home directory${c.reset}`);
711
+ console.log(
712
+ ` ${c.cyan}token${c.reset}: ${c.white}${HOME_TOKEN_PATH}${c.reset}`,
713
+ );
714
+ console.log(
715
+ ` ${c.cyan}orchestrations${c.reset}: ${c.white}${HOME_ORCHESTRATIONS_PATH}${c.reset}`,
716
+ );
717
+ console.log("");
718
+ console.log(` ${c.bold}3) Custom paths${c.reset}`);
719
+
720
+ const answer = (
721
+ await rl.question(` Select ${c.bold}[1/2/3]${c.reset} (default ${c.bold}1${c.reset}): `)
722
+ )
723
+ .trim()
724
+ .toLowerCase();
725
+
726
+ if (answer === "2" || answer === "home") {
727
+ return {
728
+ tokenPath: HOME_TOKEN_PATH,
729
+ orchestrationsPath: HOME_ORCHESTRATIONS_PATH,
730
+ };
731
+ }
732
+
733
+ if (answer === "3" || answer === "custom") {
734
+ const tokenInput = (
735
+ await rl.question(` Token path (default ${CWD_TOKEN_PATH}): `)
736
+ ).trim();
737
+ const orchestrationsInput = (
738
+ await rl.question(
739
+ ` Orchestrations path (default ${CWD_ORCHESTRATIONS_PATH}): `,
740
+ )
741
+ ).trim();
742
+
743
+ return {
744
+ tokenPath: resolve(tokenInput || CWD_TOKEN_PATH),
745
+ orchestrationsPath: resolve(
746
+ orchestrationsInput || CWD_ORCHESTRATIONS_PATH,
747
+ ),
748
+ };
749
+ }
750
+
751
+ return {
752
+ tokenPath: CWD_TOKEN_PATH,
753
+ orchestrationsPath: CWD_ORCHESTRATIONS_PATH,
754
+ };
755
+ } finally {
756
+ rl.close();
757
+ }
758
+ }
759
+
760
+ async function promptOrchestrationsTarget() {
761
+ const rl = createInterface({
762
+ input: process.stdin,
763
+ output: process.stdout,
764
+ });
765
+
766
+ try {
767
+ console.log("");
768
+ console.log(` ${c.bold}Choose where to install orchestrations${c.reset}`);
769
+ console.log(
770
+ ` ${c.white}orchestrations/${c.reset}: starter orchestration files your agent can use directly.`,
771
+ );
772
+ console.log("");
773
+ console.log(` ${c.bold}1) Current directory (default)${c.reset}`);
774
+ console.log(
775
+ ` ${c.cyan}orchestrations${c.reset}: ${c.white}${CWD_ORCHESTRATIONS_PATH}${c.reset}`,
776
+ );
777
+ console.log("");
778
+ console.log(` ${c.bold}2) Home directory${c.reset}`);
779
+ console.log(
780
+ ` ${c.cyan}orchestrations${c.reset}: ${c.white}${HOME_ORCHESTRATIONS_PATH}${c.reset}`,
781
+ );
782
+ console.log("");
783
+ console.log(` ${c.bold}3) Custom path${c.reset}`);
784
+
785
+ const answer = (
786
+ await rl.question(` Select ${c.bold}[1/2/3]${c.reset} (default ${c.bold}1${c.reset}): `)
787
+ )
788
+ .trim()
789
+ .toLowerCase();
790
+
791
+ if (answer === "2" || answer === "home") {
792
+ return HOME_ORCHESTRATIONS_PATH;
793
+ }
794
+
795
+ if (answer === "3" || answer === "custom") {
796
+ const pathInput = (
797
+ await rl.question(
798
+ ` Orchestrations path (default ${CWD_ORCHESTRATIONS_PATH}): `,
799
+ )
800
+ ).trim();
801
+ return resolve(pathInput || CWD_ORCHESTRATIONS_PATH);
802
+ }
803
+
804
+ return CWD_ORCHESTRATIONS_PATH;
805
+ } finally {
806
+ rl.close();
807
+ }
808
+ }
809
+
810
+ async function installBundledOrchestrations({ destinationPath, overwrite }) {
811
+ let sourceEntries;
812
+ try {
813
+ sourceEntries = await readdir(BUNDLED_ORCHESTRATIONS_PATH, {
814
+ withFileTypes: true,
815
+ });
816
+ } catch {
817
+ return { status: "missing_bundle", copiedCount: 0 };
818
+ }
819
+
820
+ if (sourceEntries.length === 0) {
821
+ return { status: "missing_bundle", copiedCount: 0 };
822
+ }
823
+
824
+ await mkdir(destinationPath, { recursive: true });
825
+ const existingEntries = await readdir(destinationPath, { withFileTypes: true });
826
+ const hasExistingEntries = existingEntries.length > 0;
827
+
828
+ if (hasExistingEntries && !overwrite) {
829
+ return { status: "skipped_exists", copiedCount: 0 };
830
+ }
831
+
832
+ let copiedCount = 0;
833
+ for (const entry of sourceEntries) {
834
+ await cp(
835
+ join(BUNDLED_ORCHESTRATIONS_PATH, entry.name),
836
+ join(destinationPath, entry.name),
837
+ { recursive: true, force: true },
838
+ );
839
+ copiedCount += 1;
840
+ }
841
+
842
+ return {
843
+ status: hasExistingEntries ? "overwritten" : "installed",
844
+ copiedCount,
845
+ };
846
+ }
847
+
848
+ async function saveToken(path, apiKey) {
849
+ await mkdir(dirname(path), { recursive: true });
850
+ await writeFile(path, JSON.stringify({ apiKey }, null, 2) + "\n");
851
+ }
852
+
853
+ function sleep(ms) {
854
+ return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
855
+ }
856
+
857
+ async function safeJson(response) {
858
+ try {
859
+ return await response.json();
860
+ } catch {
861
+ return {};
862
+ }
863
+ }
864
+
865
+ function toErrorMessage(error) {
866
+ if (error instanceof Error) return error.message;
867
+ return String(error);
868
+ }