opencara 0.8.0 → 0.9.0

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 +451 -47
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -131,6 +131,7 @@ function parseAgents(data) {
131
131
  const agent = { model: obj.model, tool: obj.tool };
132
132
  if (typeof obj.name === "string") agent.name = obj.name;
133
133
  if (typeof obj.command === "string") agent.command = obj.command;
134
+ if (obj.router === true) agent.router = true;
134
135
  const agentLimits = parseLimits(obj);
135
136
  if (agentLimits) agent.limits = agentLimits;
136
137
  const repoConfig = parseRepoConfig(obj, i);
@@ -408,28 +409,28 @@ var DEFAULT_REGISTRY = {
408
409
  name: "claude",
409
410
  displayName: "Claude",
410
411
  binary: "claude",
411
- commandTemplate: "claude --model ${MODEL} -p ${PROMPT} --output-format text",
412
+ commandTemplate: "claude --model ${MODEL} --allowedTools '*' --print",
412
413
  tokenParser: "claude"
413
414
  },
414
415
  {
415
416
  name: "codex",
416
417
  displayName: "Codex",
417
418
  binary: "codex",
418
- commandTemplate: "codex --model ${MODEL} -p ${PROMPT}",
419
+ commandTemplate: "codex --model ${MODEL} exec",
419
420
  tokenParser: "codex"
420
421
  },
421
422
  {
422
423
  name: "gemini",
423
424
  displayName: "Gemini",
424
425
  binary: "gemini",
425
- commandTemplate: "gemini --model ${MODEL} -p ${PROMPT}",
426
+ commandTemplate: "gemini -m ${MODEL}",
426
427
  tokenParser: "gemini"
427
428
  },
428
429
  {
429
430
  name: "qwen",
430
431
  displayName: "Qwen",
431
432
  binary: "qwen",
432
- commandTemplate: "qwen --model ${MODEL} -p ${PROMPT} -y",
433
+ commandTemplate: "qwen --model ${MODEL} -y",
433
434
  tokenParser: "qwen"
434
435
  }
435
436
  ],
@@ -898,6 +899,141 @@ ${userMessage}`;
898
899
  }
899
900
  }
900
901
 
902
+ // src/router.ts
903
+ import * as readline2 from "readline";
904
+ var END_OF_RESPONSE = "<<<OPENCARA_END_RESPONSE>>>";
905
+ var RouterRelay = class {
906
+ pending = null;
907
+ responseLines = [];
908
+ rl = null;
909
+ stdout;
910
+ stderr;
911
+ stdin;
912
+ stopped = false;
913
+ constructor(deps) {
914
+ this.stdin = deps?.stdin ?? process.stdin;
915
+ this.stdout = deps?.stdout ?? process.stdout;
916
+ this.stderr = deps?.stderr ?? process.stderr;
917
+ }
918
+ /** Start listening for stdin input */
919
+ start() {
920
+ this.stopped = false;
921
+ this.rl = readline2.createInterface({
922
+ input: this.stdin,
923
+ terminal: false
924
+ });
925
+ this.rl.on("line", (line) => {
926
+ this.handleLine(line);
927
+ });
928
+ this.rl.on("close", () => {
929
+ if (this.stopped) return;
930
+ if (this.pending) {
931
+ const response = this.responseLines.join("\n");
932
+ this.responseLines = [];
933
+ clearTimeout(this.pending.timer);
934
+ const task = this.pending;
935
+ this.pending = null;
936
+ if (response.trim()) {
937
+ task.resolve(response);
938
+ } else {
939
+ task.reject(new Error("stdin closed with no response"));
940
+ }
941
+ }
942
+ });
943
+ }
944
+ /** Stop listening and clean up */
945
+ stop() {
946
+ this.stopped = true;
947
+ if (this.rl) {
948
+ this.rl.close();
949
+ this.rl = null;
950
+ }
951
+ if (this.pending) {
952
+ clearTimeout(this.pending.timer);
953
+ this.pending.reject(new Error("Router relay stopped"));
954
+ this.pending = null;
955
+ this.responseLines = [];
956
+ }
957
+ }
958
+ /** Write the prompt as plain text to stdout */
959
+ writePrompt(prompt) {
960
+ this.stdout.write(prompt + "\n");
961
+ }
962
+ /** Write a status message to stderr (doesn't interfere with prompt/response on stdout/stdin) */
963
+ writeStatus(message) {
964
+ this.stderr.write(message + "\n");
965
+ }
966
+ /** Build the full prompt for a review request */
967
+ buildReviewPrompt(req) {
968
+ const systemPrompt = buildSystemPrompt(
969
+ req.owner,
970
+ req.repo,
971
+ req.reviewMode
972
+ );
973
+ const userMessage = buildUserMessage(req.prompt, req.diffContent);
974
+ return `${systemPrompt}
975
+
976
+ ${userMessage}`;
977
+ }
978
+ /** Build the full prompt for a summary request */
979
+ buildSummaryPrompt(req) {
980
+ const systemPrompt = buildSummarySystemPrompt(req.owner, req.repo, req.reviews.length);
981
+ const userMessage = buildSummaryUserMessage(req.prompt, req.reviews, req.diffContent);
982
+ return `${systemPrompt}
983
+
984
+ ${userMessage}`;
985
+ }
986
+ /**
987
+ * Send a prompt to the external agent via stdout (plain text)
988
+ * and wait for the response via stdin (plain text, terminated by END_OF_RESPONSE or EOF).
989
+ */
990
+ sendPrompt(_type, _taskId, prompt, timeoutSec) {
991
+ return new Promise((resolve2, reject) => {
992
+ if (this.pending) {
993
+ reject(new Error("Another prompt is already pending"));
994
+ return;
995
+ }
996
+ const timeoutMs = timeoutSec * 1e3;
997
+ this.responseLines = [];
998
+ const timer = setTimeout(() => {
999
+ this.pending = null;
1000
+ this.responseLines = [];
1001
+ reject(new RouterTimeoutError(`Response timeout (${timeoutSec}s)`));
1002
+ }, timeoutMs);
1003
+ this.pending = { resolve: resolve2, reject, timer };
1004
+ this.writePrompt(prompt);
1005
+ });
1006
+ }
1007
+ /** Parse a review response: extract verdict and review text */
1008
+ parseReviewResponse(response) {
1009
+ return extractVerdict(response);
1010
+ }
1011
+ /** Get whether a task is pending (for testing) */
1012
+ get pendingCount() {
1013
+ return this.pending ? 1 : 0;
1014
+ }
1015
+ /** Handle a single line from stdin */
1016
+ handleLine(line) {
1017
+ if (!this.pending) return;
1018
+ if (line.trim() === END_OF_RESPONSE) {
1019
+ const response = this.responseLines.join("\n");
1020
+ this.responseLines = [];
1021
+ clearTimeout(this.pending.timer);
1022
+ const task = this.pending;
1023
+ this.pending = null;
1024
+ task.resolve(response);
1025
+ return;
1026
+ }
1027
+ this.responseLines.push(line);
1028
+ }
1029
+ };
1030
+ var RouterTimeoutError = class extends Error {
1031
+ constructor(message) {
1032
+ super(message);
1033
+ this.name = "RouterTimeoutError";
1034
+ }
1035
+ };
1036
+
901
1037
  // src/consumption.ts
902
1038
  async function checkConsumptionLimits(_agentId, _limits) {
903
1039
  return { allowed: true };
@@ -956,6 +1092,7 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
956
1092
  const stabilityThreshold = options?.stabilityThresholdMs ?? CONNECTION_STABILITY_THRESHOLD_MS;
957
1093
  const repoConfig = options?.repoConfig;
958
1094
  const displayName = options?.displayName;
1095
+ const routerRelay = options?.routerRelay;
959
1096
  const prefix = options?.label ? `[${options.label}]` : "";
960
1097
  const log = (...args) => console.log(...prefix ? [prefix, ...args] : args);
961
1098
  const logError = (...args) => console.error(...prefix ? [prefix, ...args] : args);
@@ -1048,7 +1185,8 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
1048
1185
  verbose,
1049
1186
  repoConfig,
1050
1187
  displayName,
1051
- prefix
1188
+ prefix,
1189
+ routerRelay
1052
1190
  );
1053
1191
  });
1054
1192
  ws.on("close", (code, reason) => {
@@ -1128,7 +1266,7 @@ async function logPostReviewStats(type, verdict, tokensUsed, tokensEstimated, co
1128
1266
  `${pfx}${formatPostReviewStats(tokensUsed, consumptionDeps.session, consumptionDeps.limits)}`
1129
1267
  );
1130
1268
  }
1131
- function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, verbose, repoConfig, displayName, logPrefix) {
1269
+ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, verbose, repoConfig, displayName, logPrefix, routerRelay) {
1132
1270
  const pfx = logPrefix ? `${logPrefix} ` : "";
1133
1271
  switch (msg.type) {
1134
1272
  case "connected":
@@ -1140,6 +1278,9 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
1140
1278
  ...displayName ? { displayName } : {},
1141
1279
  repoConfig: repoConfig ?? { mode: "all" }
1142
1280
  });
1281
+ if (routerRelay) {
1282
+ routerRelay.writeStatus("Waiting for review requests...");
1283
+ }
1143
1284
  break;
1144
1285
  case "heartbeat_ping":
1145
1286
  ws.send(JSON.stringify({ type: "heartbeat_pong", timestamp: Date.now() }));
@@ -1155,6 +1296,81 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
1155
1296
  console.log(
1156
1297
  `${pfx}Review request: task ${request.taskId} for ${request.project.owner}/${request.project.repo}#${request.pr.number}`
1157
1298
  );
1299
+ if (routerRelay) {
1300
+ void (async () => {
1301
+ if (consumptionDeps) {
1302
+ const limitResult = await checkConsumptionLimits(
1303
+ consumptionDeps.agentId,
1304
+ consumptionDeps.limits
1305
+ );
1306
+ if (!limitResult.allowed) {
1307
+ trySend(ws, {
1308
+ type: "review_rejected",
1309
+ id: crypto2.randomUUID(),
1310
+ timestamp: Date.now(),
1311
+ taskId: request.taskId,
1312
+ reason: limitResult.reason ?? "consumption_limit_exceeded"
1313
+ });
1314
+ console.log(`${pfx}Review rejected: ${limitResult.reason}`);
1315
+ return;
1316
+ }
1317
+ }
1318
+ try {
1319
+ const prompt = routerRelay.buildReviewPrompt({
1320
+ owner: request.project.owner,
1321
+ repo: request.project.repo,
1322
+ reviewMode: request.reviewMode ?? "full",
1323
+ prompt: request.project.prompt,
1324
+ diffContent: request.diffContent
1325
+ });
1326
+ const response = await routerRelay.sendPrompt(
1327
+ "review_request",
1328
+ request.taskId,
1329
+ prompt,
1330
+ request.timeout
1331
+ );
1332
+ const { verdict, review } = routerRelay.parseReviewResponse(response);
1333
+ const tokensUsed = estimateTokens(prompt) + estimateTokens(response);
1334
+ trySend(ws, {
1335
+ type: "review_complete",
1336
+ id: crypto2.randomUUID(),
1337
+ timestamp: Date.now(),
1338
+ taskId: request.taskId,
1339
+ review,
1340
+ verdict,
1341
+ tokensUsed
1342
+ });
1343
+ await logPostReviewStats(
1344
+ "Review",
1345
+ verdict,
1346
+ tokensUsed,
1347
+ true,
1348
+ consumptionDeps,
1349
+ logPrefix
1350
+ );
1351
+ } catch (err) {
1352
+ if (err instanceof RouterTimeoutError) {
1353
+ trySend(ws, {
1354
+ type: "review_error",
1355
+ id: crypto2.randomUUID(),
1356
+ timestamp: Date.now(),
1357
+ taskId: request.taskId,
1358
+ error: err.message
1359
+ });
1360
+ } else {
1361
+ trySend(ws, {
1362
+ type: "review_error",
1363
+ id: crypto2.randomUUID(),
1364
+ timestamp: Date.now(),
1365
+ taskId: request.taskId,
1366
+ error: err instanceof Error ? err.message : "Unknown error"
1367
+ });
1368
+ }
1369
+ console.error(`${pfx}Review failed:`, err);
1370
+ }
1371
+ })();
1372
+ break;
1373
+ }
1158
1374
  if (!reviewDeps) {
1159
1375
  ws.send(
1160
1376
  JSON.stringify({
@@ -1244,6 +1460,79 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
1244
1460
  console.log(
1245
1461
  `${pfx}Summary request: task ${summaryRequest.taskId} for ${summaryRequest.project.owner}/${summaryRequest.project.repo}#${summaryRequest.pr.number} (${summaryRequest.reviews.length} reviews)`
1246
1462
  );
1463
+ if (routerRelay) {
1464
+ void (async () => {
1465
+ if (consumptionDeps) {
1466
+ const limitResult = await checkConsumptionLimits(
1467
+ consumptionDeps.agentId,
1468
+ consumptionDeps.limits
1469
+ );
1470
+ if (!limitResult.allowed) {
1471
+ trySend(ws, {
1472
+ type: "review_rejected",
1473
+ id: crypto2.randomUUID(),
1474
+ timestamp: Date.now(),
1475
+ taskId: summaryRequest.taskId,
1476
+ reason: limitResult.reason ?? "consumption_limit_exceeded"
1477
+ });
1478
+ console.log(`${pfx}Summary rejected: ${limitResult.reason}`);
1479
+ return;
1480
+ }
1481
+ }
1482
+ try {
1483
+ const prompt = routerRelay.buildSummaryPrompt({
1484
+ owner: summaryRequest.project.owner,
1485
+ repo: summaryRequest.project.repo,
1486
+ prompt: summaryRequest.project.prompt,
1487
+ reviews: summaryRequest.reviews,
1488
+ diffContent: summaryRequest.diffContent ?? ""
1489
+ });
1490
+ const response = await routerRelay.sendPrompt(
1491
+ "summary_request",
1492
+ summaryRequest.taskId,
1493
+ prompt,
1494
+ summaryRequest.timeout
1495
+ );
1496
+ const tokensUsed = estimateTokens(prompt) + estimateTokens(response);
1497
+ trySend(ws, {
1498
+ type: "summary_complete",
1499
+ id: crypto2.randomUUID(),
1500
+ timestamp: Date.now(),
1501
+ taskId: summaryRequest.taskId,
1502
+ summary: response,
1503
+ tokensUsed
1504
+ });
1505
+ await logPostReviewStats(
1506
+ "Summary",
1507
+ void 0,
1508
+ tokensUsed,
1509
+ true,
1510
+ consumptionDeps,
1511
+ logPrefix
1512
+ );
1513
+ } catch (err) {
1514
+ if (err instanceof RouterTimeoutError) {
1515
+ trySend(ws, {
1516
+ type: "review_error",
1517
+ id: crypto2.randomUUID(),
1518
+ timestamp: Date.now(),
1519
+ taskId: summaryRequest.taskId,
1520
+ error: err.message
1521
+ });
1522
+ } else {
1523
+ trySend(ws, {
1524
+ type: "review_error",
1525
+ id: crypto2.randomUUID(),
1526
+ timestamp: Date.now(),
1527
+ taskId: summaryRequest.taskId,
1528
+ error: err instanceof Error ? err.message : "Summary failed"
1529
+ });
1530
+ }
1531
+ console.error(`${pfx}Summary failed:`, err);
1532
+ }
1533
+ })();
1534
+ break;
1535
+ }
1247
1536
  if (!reviewDeps) {
1248
1537
  trySend(ws, {
1249
1538
  type: "review_rejected",
@@ -1354,6 +1643,12 @@ function resolveLocalAgentCommand(localAgent, globalAgentCommand) {
1354
1643
  const effectiveCommand = localAgent.command ?? globalAgentCommand;
1355
1644
  return resolveCommandTemplate(effectiveCommand);
1356
1645
  }
1646
+ function startAgentRouter() {
1647
+ void agentCommand.parseAsync(
1648
+ ["start", "--router", "--anonymous", "--model", "router", "--tool", "opencara"],
1649
+ { from: "user" }
1650
+ );
1651
+ }
1357
1652
  var agentCommand = new Command2("agent").description("Manage review agents");
1358
1653
  agentCommand.command("create").description("Add an agent to local config (interactive or via flags)").option("--model <model>", "AI model name (e.g., claude-opus-4-6)").option("--tool <tool>", "Review tool name (e.g., claude-code)").option("--command <cmd>", "Custom command template (bypasses registry lookup)").action(async (opts) => {
1359
1654
  const config = loadConfig();
@@ -1433,7 +1728,7 @@ agentCommand.command("create").description("Add an agent to local config (intera
1433
1728
  break;
1434
1729
  }
1435
1730
  const toolEntry = registry.tools.find((t) => t.name === tool);
1436
- const defaultCommand = toolEntry ? toolEntry.commandTemplate.replaceAll("${MODEL}", model) : `${tool} --model ${model} -p \${PROMPT}`;
1731
+ const defaultCommand = toolEntry ? toolEntry.commandTemplate.replaceAll("${MODEL}", model) : `${tool} --model ${model}`;
1437
1732
  command = await input({
1438
1733
  message: "Command:",
1439
1734
  default: defaultCommand,
@@ -1573,7 +1868,7 @@ async function resolveAnonymousAgent(config, model, tool) {
1573
1868
  );
1574
1869
  return { entry, command };
1575
1870
  }
1576
- agentCommand.command("start [agentIdOrModel]").description("Connect agent to platform via WebSocket").option("--all", "Start all agents from local config concurrently").option("-a, --anonymous", "Start an anonymous agent (no login required)").option("--model <model>", "AI model name (used with --anonymous)").option("--tool <tool>", "Review tool name (used with --anonymous)").option("--verbose", "Enable detailed WebSocket diagnostic logging").option(
1871
+ agentCommand.command("start [agentIdOrModel]").description("Connect agent to platform via WebSocket").option("--all", "Start all agents from local config concurrently").option("-a, --anonymous", "Start an anonymous agent (no login required)").option("--model <model>", "AI model name (used with --anonymous)").option("--tool <tool>", "Review tool name (used with --anonymous)").option("--verbose", "Enable detailed WebSocket diagnostic logging").option("--router", "Router mode: relay prompts to stdout, read responses from stdin").option(
1577
1872
  "--stability-threshold <ms>",
1578
1873
  `Connection stability threshold in ms (${STABILITY_THRESHOLD_MIN_MS}\u2013${STABILITY_THRESHOLD_MAX_MS}, default: ${CONNECTION_STABILITY_THRESHOLD_MS})`
1579
1874
  ).action(
@@ -1595,24 +1890,57 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1595
1890
  console.error("Both --model and --tool are required with --anonymous.");
1596
1891
  process.exit(1);
1597
1892
  }
1598
- let resolved;
1599
- try {
1600
- resolved = await resolveAnonymousAgent(config, opts.model, opts.tool);
1601
- } catch (err) {
1602
- console.error(
1603
- "Failed to register anonymous agent:",
1604
- err instanceof Error ? err.message : err
1605
- );
1606
- process.exit(1);
1607
- }
1608
- const { entry, command } = resolved;
1893
+ let entry;
1609
1894
  let reviewDeps2;
1610
- if (validateCommandBinary(command)) {
1611
- reviewDeps2 = { commandTemplate: command, maxDiffSizeKb: config.maxDiffSizeKb };
1612
- } else {
1613
- console.warn(
1614
- `Warning: binary "${command.split(" ")[0]}" not found. Reviews will be rejected.`
1895
+ let relay2;
1896
+ if (opts.router) {
1897
+ const existing = config.anonymousAgents.find(
1898
+ (a) => a.model === opts.model && a.tool === opts.tool
1615
1899
  );
1900
+ if (existing) {
1901
+ console.log(
1902
+ `Reusing stored anonymous agent ${existing.agentId} (${opts.model} / ${opts.tool})`
1903
+ );
1904
+ entry = existing;
1905
+ } else {
1906
+ console.log("Registering anonymous agent...");
1907
+ const client2 = new ApiClient(config.platformUrl);
1908
+ const res = await client2.post("/api/agents/anonymous", {
1909
+ model: opts.model,
1910
+ tool: opts.tool
1911
+ });
1912
+ entry = {
1913
+ agentId: res.agentId,
1914
+ apiKey: res.apiKey,
1915
+ model: opts.model,
1916
+ tool: opts.tool
1917
+ };
1918
+ config.anonymousAgents.push(entry);
1919
+ saveConfig(config);
1920
+ console.log(`Agent registered: ${res.agentId} (${opts.model} / ${opts.tool})`);
1921
+ }
1922
+ relay2 = new RouterRelay();
1923
+ relay2.start();
1924
+ } else {
1925
+ let resolved;
1926
+ try {
1927
+ resolved = await resolveAnonymousAgent(config, opts.model, opts.tool);
1928
+ } catch (err) {
1929
+ console.error(
1930
+ "Failed to register anonymous agent:",
1931
+ err instanceof Error ? err.message : err
1932
+ );
1933
+ process.exit(1);
1934
+ }
1935
+ entry = resolved.entry;
1936
+ const command = resolved.command;
1937
+ if (validateCommandBinary(command)) {
1938
+ reviewDeps2 = { commandTemplate: command, maxDiffSizeKb: config.maxDiffSizeKb };
1939
+ } else {
1940
+ console.warn(
1941
+ `Warning: binary "${command.split(" ")[0]}" not found. Reviews will be rejected.`
1942
+ );
1943
+ }
1616
1944
  }
1617
1945
  const consumptionDeps2 = {
1618
1946
  agentId: entry.agentId,
@@ -1624,13 +1952,19 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1624
1952
  verbose: opts.verbose,
1625
1953
  stabilityThresholdMs,
1626
1954
  displayName: entry.name,
1627
- repoConfig: entry.repoConfig
1955
+ repoConfig: entry.repoConfig,
1956
+ routerRelay: relay2
1628
1957
  });
1629
1958
  return;
1630
1959
  }
1631
1960
  if (config.agents !== null) {
1961
+ const routerAgents = [];
1632
1962
  const validAgents = [];
1633
1963
  for (const local of config.agents) {
1964
+ if (opts.router || local.router) {
1965
+ routerAgents.push(local);
1966
+ continue;
1967
+ }
1634
1968
  let cmd;
1635
1969
  try {
1636
1970
  cmd = resolveLocalAgentCommand(local, config.agentCommand);
@@ -1648,30 +1982,45 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1648
1982
  }
1649
1983
  validAgents.push({ local, command: cmd });
1650
1984
  }
1651
- if (validAgents.length === 0 && config.anonymousAgents.length === 0) {
1985
+ const totalValid = validAgents.length + routerAgents.length;
1986
+ if (totalValid === 0 && config.anonymousAgents.length === 0) {
1652
1987
  console.error("No valid agents in config. Check that tool binaries are installed.");
1653
1988
  process.exit(1);
1654
1989
  }
1655
1990
  let agentsToStart;
1991
+ let routerAgentsToStart;
1656
1992
  const anonAgentsToStart = [];
1657
1993
  if (opts.all) {
1658
1994
  agentsToStart = validAgents;
1995
+ routerAgentsToStart = routerAgents;
1659
1996
  anonAgentsToStart.push(...config.anonymousAgents);
1660
1997
  } else if (agentIdOrModel) {
1661
- const match = validAgents.find((a) => a.local.model === agentIdOrModel);
1662
- if (!match) {
1998
+ const cmdMatch = validAgents.find((a) => a.local.model === agentIdOrModel);
1999
+ const routerMatch = routerAgents.find((a) => a.model === agentIdOrModel);
2000
+ if (!cmdMatch && !routerMatch) {
1663
2001
  console.error(`No agent with model "${agentIdOrModel}" found in local config.`);
1664
2002
  console.error("Available agents:");
1665
2003
  for (const a of validAgents) {
1666
2004
  console.error(` ${a.local.model} (${a.local.tool})`);
1667
2005
  }
2006
+ for (const a of routerAgents) {
2007
+ console.error(` ${a.model} (${a.tool}) [router]`);
2008
+ }
1668
2009
  process.exit(1);
1669
2010
  }
1670
- agentsToStart = [match];
1671
- } else if (validAgents.length === 1) {
1672
- agentsToStart = [validAgents[0]];
1673
- console.log(`Using agent ${validAgents[0].local.model} (${validAgents[0].local.tool})`);
1674
- } else if (validAgents.length === 0) {
2011
+ agentsToStart = cmdMatch ? [cmdMatch] : [];
2012
+ routerAgentsToStart = routerMatch ? [routerMatch] : [];
2013
+ } else if (totalValid === 1) {
2014
+ if (validAgents.length === 1) {
2015
+ agentsToStart = [validAgents[0]];
2016
+ routerAgentsToStart = [];
2017
+ console.log(`Using agent ${validAgents[0].local.model} (${validAgents[0].local.tool})`);
2018
+ } else {
2019
+ agentsToStart = [];
2020
+ routerAgentsToStart = [routerAgents[0]];
2021
+ console.log(`Using router agent ${routerAgents[0].model} (${routerAgents[0].tool})`);
2022
+ }
2023
+ } else if (totalValid === 0) {
1675
2024
  console.error("No valid authenticated agents in config. Use --anonymous or --all.");
1676
2025
  process.exit(1);
1677
2026
  } else {
@@ -1679,9 +2028,12 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1679
2028
  for (const a of validAgents) {
1680
2029
  console.error(` ${a.local.model} (${a.local.tool})`);
1681
2030
  }
2031
+ for (const a of routerAgents) {
2032
+ console.error(` ${a.model} (${a.tool}) [router]`);
2033
+ }
1682
2034
  process.exit(1);
1683
2035
  }
1684
- const totalAgents = agentsToStart.length + anonAgentsToStart.length;
2036
+ const totalAgents = agentsToStart.length + routerAgentsToStart.length + anonAgentsToStart.length;
1685
2037
  if (totalAgents > 1) {
1686
2038
  process.setMaxListeners(process.getMaxListeners() + totalAgents * 2);
1687
2039
  }
@@ -1689,7 +2041,7 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1689
2041
  let apiKey2;
1690
2042
  let client2;
1691
2043
  let serverAgents;
1692
- if (agentsToStart.length > 0) {
2044
+ if (agentsToStart.length > 0 || routerAgentsToStart.length > 0) {
1693
2045
  apiKey2 = requireApiKey(config);
1694
2046
  client2 = new ApiClient(config.platformUrl, apiKey2);
1695
2047
  try {
@@ -1743,6 +2095,48 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1743
2095
  });
1744
2096
  startedCount++;
1745
2097
  }
2098
+ for (const local of routerAgentsToStart) {
2099
+ let agentId2;
2100
+ try {
2101
+ const sync = await syncAgentToServer(client2, serverAgents, local);
2102
+ agentId2 = sync.agentId;
2103
+ if (sync.created) {
2104
+ console.log(`Registered new agent ${agentId2} on platform`);
2105
+ serverAgents.push({
2106
+ id: agentId2,
2107
+ model: local.model,
2108
+ tool: local.tool,
2109
+ isAnonymous: false,
2110
+ status: "offline",
2111
+ repoConfig: null,
2112
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2113
+ });
2114
+ }
2115
+ } catch (err) {
2116
+ console.error(
2117
+ `Failed to sync router agent ${local.model} to server:`,
2118
+ err instanceof Error ? err.message : err
2119
+ );
2120
+ continue;
2121
+ }
2122
+ const relay2 = new RouterRelay();
2123
+ relay2.start();
2124
+ const consumptionDeps2 = {
2125
+ agentId: agentId2,
2126
+ limits: resolveAgentLimits(local.limits, config.limits),
2127
+ session: createSessionTracker()
2128
+ };
2129
+ const label = local.name || local.model || "unnamed";
2130
+ console.log(`Starting router agent ${label} (${agentId2})...`);
2131
+ startAgent(agentId2, config.platformUrl, apiKey2, void 0, consumptionDeps2, {
2132
+ verbose: opts.verbose,
2133
+ stabilityThresholdMs,
2134
+ repoConfig: local.repos,
2135
+ label,
2136
+ routerRelay: relay2
2137
+ });
2138
+ startedCount++;
2139
+ }
1746
2140
  for (const anon of anonAgentsToStart) {
1747
2141
  let command;
1748
2142
  try {
@@ -1815,16 +2209,22 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1815
2209
  }
1816
2210
  }
1817
2211
  let reviewDeps;
1818
- try {
1819
- const commandTemplate = resolveCommandTemplate(config.agentCommand);
1820
- reviewDeps = {
1821
- commandTemplate,
1822
- maxDiffSizeKb: config.maxDiffSizeKb
1823
- };
1824
- } catch (err) {
1825
- console.warn(
1826
- `Warning: ${err instanceof Error ? err.message : "Could not determine agent command."} Reviews will be rejected.`
1827
- );
2212
+ let relay;
2213
+ if (opts.router) {
2214
+ relay = new RouterRelay();
2215
+ relay.start();
2216
+ } else {
2217
+ try {
2218
+ const commandTemplate = resolveCommandTemplate(config.agentCommand);
2219
+ reviewDeps = {
2220
+ commandTemplate,
2221
+ maxDiffSizeKb: config.maxDiffSizeKb
2222
+ };
2223
+ } catch (err) {
2224
+ console.warn(
2225
+ `Warning: ${err instanceof Error ? err.message : "Could not determine agent command."} Reviews will be rejected.`
2226
+ );
2227
+ }
1828
2228
  }
1829
2229
  const consumptionDeps = {
1830
2230
  agentId,
@@ -1835,7 +2235,8 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1835
2235
  startAgent(agentId, config.platformUrl, apiKey, reviewDeps, consumptionDeps, {
1836
2236
  verbose: opts.verbose,
1837
2237
  stabilityThresholdMs,
1838
- label: agentId
2238
+ label: agentId,
2239
+ routerRelay: relay
1839
2240
  });
1840
2241
  }
1841
2242
  );
@@ -2016,8 +2417,11 @@ var statsCommand = new Command3("stats").description("Display agent dashboard: t
2016
2417
  });
2017
2418
 
2018
2419
  // src/index.ts
2019
- var program = new Command4().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.8.0");
2420
+ var program = new Command4().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.9.0");
2020
2421
  program.addCommand(loginCommand);
2021
2422
  program.addCommand(agentCommand);
2022
2423
  program.addCommand(statsCommand);
2424
+ program.action(() => {
2425
+ startAgentRouter();
2426
+ });
2023
2427
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",