opencara 0.15.3 → 0.15.6

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.
Files changed (2) hide show
  1. package/dist/index.js +253 -65
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command2 } from "commander";
4
+ import { Command as Command3 } from "commander";
5
5
 
6
6
  // src/commands/agent.ts
7
7
  import { Command } from "commander";
@@ -204,10 +204,10 @@ function parseAgents(data) {
204
204
  `\u26A0 Config warning: agents[${i}].tool "${resolvedTool}" is deprecated, using "${alias}" instead`
205
205
  );
206
206
  resolvedTool = alias;
207
- } else {
207
+ } else if (typeof obj.command !== "string") {
208
208
  const toolNames = [...KNOWN_TOOL_NAMES].join(", ");
209
209
  console.warn(
210
- `\u26A0 Config warning: agents[${i}].tool "${resolvedTool}" not in registry (known: ${toolNames}), skipping agent`
210
+ `\u26A0 Config warning: agents[${i}].tool "${resolvedTool}" not in registry (known: ${toolNames}) and no custom command provided, skipping agent`
211
211
  );
212
212
  continue;
213
213
  }
@@ -469,19 +469,36 @@ var HttpError = class extends Error {
469
469
  this.name = "HttpError";
470
470
  }
471
471
  };
472
+ var UpgradeRequiredError = class extends Error {
473
+ constructor(currentVersion, minimumVersion) {
474
+ const minPart = minimumVersion ? ` Minimum required: ${minimumVersion}` : "";
475
+ super(
476
+ `Your CLI version (${currentVersion}) is outdated.${minPart} Please upgrade: npm update -g opencara`
477
+ );
478
+ this.currentVersion = currentVersion;
479
+ this.minimumVersion = minimumVersion;
480
+ this.name = "UpgradeRequiredError";
481
+ }
482
+ };
472
483
  var ApiClient = class {
473
484
  constructor(baseUrl, debugOrOptions) {
474
485
  this.baseUrl = baseUrl;
475
486
  if (typeof debugOrOptions === "object" && debugOrOptions !== null) {
476
487
  this.debug = debugOrOptions.debug ?? process.env.OPENCARA_DEBUG === "1";
477
488
  this.apiKey = debugOrOptions.apiKey ?? null;
489
+ this.cliVersion = debugOrOptions.cliVersion ?? null;
490
+ this.versionOverride = debugOrOptions.versionOverride ?? null;
478
491
  } else {
479
492
  this.debug = debugOrOptions ?? process.env.OPENCARA_DEBUG === "1";
480
493
  this.apiKey = null;
494
+ this.cliVersion = null;
495
+ this.versionOverride = null;
481
496
  }
482
497
  }
483
498
  debug;
484
499
  apiKey;
500
+ cliVersion;
501
+ versionOverride;
485
502
  log(msg) {
486
503
  if (this.debug) console.debug(`[ApiClient] ${msg}`);
487
504
  }
@@ -492,6 +509,12 @@ var ApiClient = class {
492
509
  if (this.apiKey) {
493
510
  h["Authorization"] = `Bearer ${this.apiKey}`;
494
511
  }
512
+ if (this.cliVersion) {
513
+ h["X-OpenCara-CLI-Version"] = this.cliVersion;
514
+ }
515
+ if (this.versionOverride) {
516
+ h["Cloudflare-Workers-Version-Overrides"] = this.versionOverride;
517
+ }
495
518
  return h;
496
519
  }
497
520
  async get(path6) {
@@ -515,15 +538,22 @@ var ApiClient = class {
515
538
  if (!res.ok) {
516
539
  let message = `HTTP ${res.status}`;
517
540
  let errorCode;
541
+ let minimumVersion;
518
542
  try {
519
543
  const body = await res.json();
520
544
  if (body.error && typeof body.error === "object" && "code" in body.error) {
521
545
  errorCode = body.error.code;
522
546
  message = body.error.message;
523
547
  }
548
+ if (body.minimum_version) {
549
+ minimumVersion = body.minimum_version;
550
+ }
524
551
  } catch {
525
552
  }
526
553
  this.log(`${res.status} ${message} (${path6})`);
554
+ if (res.status === 426) {
555
+ throw new UpgradeRequiredError(this.cliVersion ?? "unknown", minimumVersion);
556
+ }
527
557
  throw new HttpError(res.status, message, errorCode);
528
558
  }
529
559
  this.log(`${res.status} OK (${path6})`);
@@ -1601,6 +1631,7 @@ var icons = {
1601
1631
  success: pc.green("\u2713"),
1602
1632
  running: pc.blue("\u25B6"),
1603
1633
  stop: pc.red("\u25A0"),
1634
+ info: pc.blue("\u2139"),
1604
1635
  warn: pc.yellow("\u26A0"),
1605
1636
  error: pc.red("\u2717")
1606
1637
  };
@@ -1798,6 +1829,11 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1798
1829
  }
1799
1830
  } catch (err) {
1800
1831
  if (signal?.aborted) break;
1832
+ if (err instanceof UpgradeRequiredError) {
1833
+ logWarn(`${icons.warn} ${err.message}`);
1834
+ process.exitCode = 1;
1835
+ break;
1836
+ }
1801
1837
  agentSession.errorsEncountered++;
1802
1838
  if (err instanceof HttpError && (err.status === 401 || err.status === 403)) {
1803
1839
  consecutiveAuthErrors++;
@@ -2212,8 +2248,8 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
2212
2248
  }
2213
2249
  const summaryReviews = reviews.map((r) => ({
2214
2250
  agentId: r.agent_id,
2215
- model: "unknown",
2216
- tool: "unknown",
2251
+ model: r.model ?? "unknown",
2252
+ tool: r.tool ?? "unknown",
2217
2253
  review: r.review_text,
2218
2254
  verdict: r.verdict
2219
2255
  }));
@@ -2319,7 +2355,11 @@ function sleep2(ms, signal) {
2319
2355
  });
2320
2356
  }
2321
2357
  async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
2322
- const client = new ApiClient(platformUrl, { apiKey: options?.apiKey });
2358
+ const client = new ApiClient(platformUrl, {
2359
+ apiKey: options?.apiKey,
2360
+ cliVersion: "0.15.6",
2361
+ versionOverride: options?.versionOverride
2362
+ });
2323
2363
  const session = consumptionDeps?.session ?? createSessionTracker();
2324
2364
  const usageTracker = consumptionDeps?.usageTracker ?? new UsageTracker();
2325
2365
  const usageLimits = options?.usageLimits ?? {
@@ -2337,6 +2377,9 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
2337
2377
  const agentSession = createAgentSession();
2338
2378
  log(`${icons.start} Agent started (polling ${platformUrl})`);
2339
2379
  log(`Model: ${agentInfo.model} | Tool: ${agentInfo.tool}`);
2380
+ if (options?.versionOverride) {
2381
+ log(`${icons.info} Version override active: ${options.versionOverride}`);
2382
+ }
2340
2383
  if (!reviewDeps) {
2341
2384
  logError(`${icons.error} No review command configured. Set command in config.yml`);
2342
2385
  return;
@@ -2407,6 +2450,7 @@ async function startAgentRouter() {
2407
2450
  const tool = agentConfig?.tool ?? "unknown";
2408
2451
  const label = agentConfig?.name ?? "agent[0]";
2409
2452
  const roles = agentConfig ? computeRoles(agentConfig) : void 0;
2453
+ const versionOverride = process.env.OPENCARA_VERSION_OVERRIDE || null;
2410
2454
  await startAgent(
2411
2455
  agentId,
2412
2456
  config.platformUrl,
@@ -2428,12 +2472,13 @@ async function startAgentRouter() {
2428
2472
  githubUsername,
2429
2473
  label,
2430
2474
  apiKey: config.apiKey,
2431
- usageLimits: config.usageLimits
2475
+ usageLimits: config.usageLimits,
2476
+ versionOverride
2432
2477
  }
2433
2478
  );
2434
2479
  router.stop();
2435
2480
  }
2436
- function startAgentByIndex(config, agentIndex, pollIntervalMs, auth, githubUsername) {
2481
+ function startAgentByIndex(config, agentIndex, pollIntervalMs, auth, githubUsername, versionOverride) {
2437
2482
  const agentId = crypto.randomUUID();
2438
2483
  let commandTemplate;
2439
2484
  let agentConfig;
@@ -2496,7 +2541,8 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, auth, githubUsern
2496
2541
  githubUsername,
2497
2542
  label,
2498
2543
  apiKey: config.apiKey,
2499
- usageLimits: config.usageLimits
2544
+ usageLimits: config.usageLimits,
2545
+ versionOverride
2500
2546
  }
2501
2547
  ).finally(() => {
2502
2548
  routerRelay?.stop();
@@ -2504,75 +2550,217 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, auth, githubUsern
2504
2550
  return agentPromise;
2505
2551
  }
2506
2552
  var agentCommand = new Command("agent").description("Manage review agents");
2507
- agentCommand.command("start").description("Start agents in polling mode").option("--poll-interval <seconds>", "Poll interval in seconds", "10").option("--agent <index>", "Agent index from config.yml (0-based)", "0").option("--all", "Start all configured agents concurrently").action(async (opts) => {
2508
- const config = loadConfig();
2509
- const pollIntervalMs = parseInt(opts.pollInterval, 10) * 1e3;
2510
- const configToken = resolveGithubToken(void 0, config.githubToken);
2511
- const auth = resolveGithubToken2(configToken);
2512
- logAuthMethod(auth.method, console.log.bind(console));
2513
- const githubUsername = config.githubUsername ?? await resolveGithubUsername(auth.token) ?? void 0;
2514
- if (githubUsername) {
2515
- console.log(`GitHub identity: ${githubUsername}`);
2553
+ agentCommand.command("start").description("Start agents in polling mode").option("--poll-interval <seconds>", "Poll interval in seconds", "10").option("--agent <index>", "Agent index from config.yml (0-based)", "0").option("--all", "Start all configured agents concurrently").option(
2554
+ "--version-override <value>",
2555
+ "Cloudflare Workers version override (e.g. opencara-server=abc123)"
2556
+ ).action(
2557
+ async (opts) => {
2558
+ const config = loadConfig();
2559
+ const pollIntervalMs = parseInt(opts.pollInterval, 10) * 1e3;
2560
+ const versionOverride = opts.versionOverride || process.env.OPENCARA_VERSION_OVERRIDE || null;
2561
+ const configToken = resolveGithubToken(void 0, config.githubToken);
2562
+ const auth = resolveGithubToken2(configToken);
2563
+ logAuthMethod(auth.method, console.log.bind(console));
2564
+ const githubUsername = config.githubUsername ?? await resolveGithubUsername(auth.token) ?? void 0;
2565
+ if (githubUsername) {
2566
+ console.log(`GitHub identity: ${githubUsername}`);
2567
+ }
2568
+ if (opts.all) {
2569
+ if (!config.agents || config.agents.length === 0) {
2570
+ console.error("No agents configured in ~/.opencara/config.yml");
2571
+ process.exit(1);
2572
+ return;
2573
+ }
2574
+ console.log(`Starting ${config.agents.length} agent(s)...`);
2575
+ const promises = [];
2576
+ let startFailed = false;
2577
+ for (let i = 0; i < config.agents.length; i++) {
2578
+ const p = startAgentByIndex(
2579
+ config,
2580
+ i,
2581
+ pollIntervalMs,
2582
+ auth,
2583
+ githubUsername,
2584
+ versionOverride
2585
+ );
2586
+ if (p) {
2587
+ promises.push(p);
2588
+ } else {
2589
+ startFailed = true;
2590
+ }
2591
+ }
2592
+ if (promises.length === 0) {
2593
+ console.error("No agents could be started. Check your config.");
2594
+ process.exit(1);
2595
+ return;
2596
+ }
2597
+ if (startFailed) {
2598
+ console.error(
2599
+ "One or more agents could not start (see warnings above). Continuing with the rest."
2600
+ );
2601
+ }
2602
+ console.log(`${promises.length} agent(s) running. Press Ctrl+C to stop all.
2603
+ `);
2604
+ const results = await Promise.allSettled(promises);
2605
+ const failures = results.filter((r) => r.status === "rejected");
2606
+ if (failures.length > 0) {
2607
+ for (const f of failures) {
2608
+ console.error(`Agent exited with error: ${f.reason}`);
2609
+ }
2610
+ process.exit(1);
2611
+ }
2612
+ } else {
2613
+ const maxIndex = (config.agents?.length ?? 0) - 1;
2614
+ const agentIndex = Number(opts.agent);
2615
+ if (!Number.isInteger(agentIndex) || agentIndex < 0 || agentIndex > maxIndex) {
2616
+ console.error(
2617
+ maxIndex >= 0 ? `--agent must be an integer between 0 and ${maxIndex}.` : "No agents configured in ~/.opencara/config.yml"
2618
+ );
2619
+ process.exit(1);
2620
+ return;
2621
+ }
2622
+ const p = startAgentByIndex(
2623
+ config,
2624
+ agentIndex,
2625
+ pollIntervalMs,
2626
+ auth,
2627
+ githubUsername,
2628
+ versionOverride
2629
+ );
2630
+ if (!p) {
2631
+ process.exit(1);
2632
+ return;
2633
+ }
2634
+ await p;
2635
+ }
2516
2636
  }
2517
- if (opts.all) {
2518
- if (!config.agents || config.agents.length === 0) {
2519
- console.error("No agents configured in ~/.opencara/config.yml");
2520
- process.exit(1);
2521
- return;
2637
+ );
2638
+
2639
+ // src/commands/status.ts
2640
+ import { Command as Command2 } from "commander";
2641
+ import pc2 from "picocolors";
2642
+ var REQUEST_TIMEOUT_MS = 1e4;
2643
+ function isValidMetrics(data) {
2644
+ if (!data || typeof data !== "object") return false;
2645
+ const obj = data;
2646
+ if (!obj.tasks || typeof obj.tasks !== "object") return false;
2647
+ const tasks = obj.tasks;
2648
+ return typeof tasks.pending === "number" && typeof tasks.reviewing === "number" && typeof tasks.failed === "number";
2649
+ }
2650
+ function agentRoleLabel(agent) {
2651
+ if (agent.review_only) return "reviewer only";
2652
+ if (agent.synthesizer_only) return "synthesizer only";
2653
+ return "reviewer+synthesizer";
2654
+ }
2655
+ function resolveToolBinary(toolName) {
2656
+ const entry = DEFAULT_REGISTRY.tools.find((t) => t.name === toolName);
2657
+ return entry?.binary ?? toolName;
2658
+ }
2659
+ function resolveCommand(agent) {
2660
+ if (agent.command) return agent.command;
2661
+ const entry = DEFAULT_REGISTRY.tools.find((t) => t.name === agent.tool);
2662
+ return entry?.commandTemplate ?? null;
2663
+ }
2664
+ async function checkConnectivity(platformUrl, fetchFn = fetch) {
2665
+ const start = Date.now();
2666
+ try {
2667
+ const res = await fetchFn(`${platformUrl}/health`, {
2668
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
2669
+ });
2670
+ const ms = Date.now() - start;
2671
+ if (!res.ok) {
2672
+ return { ok: false, ms, error: `HTTP ${res.status}` };
2522
2673
  }
2523
- console.log(`Starting ${config.agents.length} agent(s)...`);
2524
- const promises = [];
2525
- let startFailed = false;
2526
- for (let i = 0; i < config.agents.length; i++) {
2527
- const p = startAgentByIndex(config, i, pollIntervalMs, auth, githubUsername);
2528
- if (p) {
2529
- promises.push(p);
2674
+ return { ok: true, ms };
2675
+ } catch (err) {
2676
+ const ms = Date.now() - start;
2677
+ const message = err instanceof Error ? err.message : String(err);
2678
+ return { ok: false, ms, error: message };
2679
+ }
2680
+ }
2681
+ async function fetchMetrics(platformUrl, fetchFn = fetch) {
2682
+ try {
2683
+ const res = await fetchFn(`${platformUrl}/metrics`, {
2684
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
2685
+ });
2686
+ if (!res.ok) return null;
2687
+ const data = await res.json();
2688
+ if (!isValidMetrics(data)) return null;
2689
+ return data;
2690
+ } catch {
2691
+ return null;
2692
+ }
2693
+ }
2694
+ async function runStatus(deps) {
2695
+ const {
2696
+ loadConfigFn = loadConfig,
2697
+ fetchFn = fetch,
2698
+ validateBinaryFn = validateCommandBinary,
2699
+ log = console.log
2700
+ } = deps;
2701
+ const config = loadConfigFn();
2702
+ log(`${pc2.bold("OpenCara Agent Status")}`);
2703
+ log(pc2.dim("\u2500".repeat(30)));
2704
+ log(`Config: ${pc2.cyan(CONFIG_FILE)}`);
2705
+ log(`Platform: ${pc2.cyan(config.platformUrl)}`);
2706
+ log(
2707
+ `GitHub: ${config.githubToken ? `${icons.success} token present` : `${icons.error} no token`}`
2708
+ );
2709
+ log("");
2710
+ const conn = await checkConnectivity(config.platformUrl, fetchFn);
2711
+ if (conn.ok) {
2712
+ log(`Connectivity: ${icons.success} OK (${conn.ms}ms)`);
2713
+ } else {
2714
+ log(`Connectivity: ${icons.error} Connection failed: ${conn.error}`);
2715
+ }
2716
+ log("");
2717
+ const agents = config.agents;
2718
+ if (!agents || agents.length === 0) {
2719
+ log(`Agents: ${pc2.dim("No agents configured")}`);
2720
+ } else {
2721
+ log(`Agents (${agents.length} configured):`);
2722
+ for (let i = 0; i < agents.length; i++) {
2723
+ const agent = agents[i];
2724
+ const label = agent.name ?? `${agent.model}/${agent.tool}`;
2725
+ const role = agentRoleLabel(agent);
2726
+ log(` ${i + 1}. ${pc2.bold(label)} \u2014 ${role}`);
2727
+ const commandTemplate = resolveCommand(agent);
2728
+ if (commandTemplate) {
2729
+ const binaryOk = validateBinaryFn(commandTemplate);
2730
+ const binary = resolveToolBinary(agent.tool);
2731
+ if (binaryOk) {
2732
+ log(` Binary: ${icons.success} ${binary} executable`);
2733
+ } else {
2734
+ log(` Binary: ${icons.error} ${binary} not found`);
2735
+ }
2530
2736
  } else {
2531
- startFailed = true;
2737
+ log(` Binary: ${icons.warn} unknown tool "${agent.tool}"`);
2532
2738
  }
2533
2739
  }
2534
- if (promises.length === 0) {
2535
- console.error("No agents could be started. Check your config.");
2536
- process.exit(1);
2537
- return;
2538
- }
2539
- if (startFailed) {
2540
- console.error(
2541
- "One or more agents could not start (see warnings above). Continuing with the rest."
2740
+ }
2741
+ log("");
2742
+ if (conn.ok) {
2743
+ const metrics = await fetchMetrics(config.platformUrl, fetchFn);
2744
+ if (metrics) {
2745
+ log("Platform Status:");
2746
+ log(
2747
+ ` Tasks: ${metrics.tasks.pending} pending, ${metrics.tasks.reviewing} reviewing, ${metrics.tasks.failed} failed`
2542
2748
  );
2543
- }
2544
- console.log(`${promises.length} agent(s) running. Press Ctrl+C to stop all.
2545
- `);
2546
- const results = await Promise.allSettled(promises);
2547
- const failures = results.filter((r) => r.status === "rejected");
2548
- if (failures.length > 0) {
2549
- for (const f of failures) {
2550
- console.error(`Agent exited with error: ${f.reason}`);
2551
- }
2552
- process.exit(1);
2749
+ } else {
2750
+ log(`Platform Status: ${icons.error} Could not fetch metrics`);
2553
2751
  }
2554
2752
  } else {
2555
- const maxIndex = (config.agents?.length ?? 0) - 1;
2556
- const agentIndex = Number(opts.agent);
2557
- if (!Number.isInteger(agentIndex) || agentIndex < 0 || agentIndex > maxIndex) {
2558
- console.error(
2559
- maxIndex >= 0 ? `--agent must be an integer between 0 and ${maxIndex}.` : "No agents configured in ~/.opencara/config.yml"
2560
- );
2561
- process.exit(1);
2562
- return;
2563
- }
2564
- const p = startAgentByIndex(config, agentIndex, pollIntervalMs, auth, githubUsername);
2565
- if (!p) {
2566
- process.exit(1);
2567
- return;
2568
- }
2569
- await p;
2753
+ log(`Platform Status: ${pc2.dim("skipped (no connectivity)")}`);
2570
2754
  }
2755
+ }
2756
+ var statusCommand = new Command2("status").description("Show agent config, connectivity, and platform status").action(async () => {
2757
+ await runStatus({});
2571
2758
  });
2572
2759
 
2573
2760
  // src/index.ts
2574
- var program = new Command2().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.15.3");
2761
+ var program = new Command3().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.15.6");
2575
2762
  program.addCommand(agentCommand);
2763
+ program.addCommand(statusCommand);
2576
2764
  program.action(() => {
2577
2765
  startAgentRouter();
2578
2766
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.15.3",
3
+ "version": "0.15.6",
4
4
  "description": "Distributed AI code review agent — poll, review, and submit PR reviews using your own AI tools",
5
5
  "type": "module",
6
6
  "license": "MIT",