opencara 0.7.1 → 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 +582 -98
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,9 +14,10 @@ import * as os from "os";
14
14
  import { parse, stringify } from "yaml";
15
15
  var DEFAULT_PLATFORM_URL = "https://api.opencara.dev";
16
16
  var CONFIG_DIR = path.join(os.homedir(), ".opencara");
17
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.yml");
17
+ var CONFIG_FILE = process.env.OPENCARA_CONFIG && process.env.OPENCARA_CONFIG.trim() ? path.resolve(process.env.OPENCARA_CONFIG) : path.join(CONFIG_DIR, "config.yml");
18
18
  function ensureConfigDir() {
19
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
19
+ const dir = path.dirname(CONFIG_FILE);
20
+ fs.mkdirSync(dir, { recursive: true });
20
21
  }
21
22
  var DEFAULT_MAX_DIFF_SIZE_KB = 100;
22
23
  function parseLimits(data) {
@@ -96,6 +97,7 @@ function parseAnonymousAgents(data) {
96
97
  model: obj.model,
97
98
  tool: obj.tool
98
99
  };
100
+ if (typeof obj.name === "string") anon.name = obj.name;
99
101
  if (obj.repo_config && typeof obj.repo_config === "object") {
100
102
  const rc = obj.repo_config;
101
103
  if (typeof rc.mode === "string" && VALID_REPO_MODES.includes(rc.mode)) {
@@ -127,7 +129,9 @@ function parseAgents(data) {
127
129
  continue;
128
130
  }
129
131
  const agent = { model: obj.model, tool: obj.tool };
132
+ if (typeof obj.name === "string") agent.name = obj.name;
130
133
  if (typeof obj.command === "string") agent.command = obj.command;
134
+ if (obj.router === true) agent.router = true;
131
135
  const agentLimits = parseLimits(obj);
132
136
  if (agentLimits) agent.limits = agentLimits;
133
137
  const repoConfig = parseRepoConfig(obj, i);
@@ -192,6 +196,9 @@ function saveConfig(config) {
192
196
  model: a.model,
193
197
  tool: a.tool
194
198
  };
199
+ if (a.name) {
200
+ entry.name = a.name;
201
+ }
195
202
  if (a.repoConfig) {
196
203
  entry.repo_config = a.repoConfig;
197
204
  }
@@ -293,17 +300,17 @@ function calculateDelay(attempt, options = DEFAULT_RECONNECT_OPTIONS) {
293
300
  return base;
294
301
  }
295
302
  function sleep(ms) {
296
- return new Promise((resolve) => setTimeout(resolve, ms));
303
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
297
304
  }
298
305
 
299
306
  // src/commands/login.ts
300
307
  function promptYesNo(question) {
301
308
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
302
- return new Promise((resolve) => {
309
+ return new Promise((resolve2) => {
303
310
  rl.question(question, (answer) => {
304
311
  rl.close();
305
312
  const normalized = answer.trim().toLowerCase();
306
- resolve(normalized === "" || normalized === "y" || normalized === "yes");
313
+ resolve2(normalized === "" || normalized === "y" || normalized === "yes");
307
314
  });
308
315
  });
309
316
  }
@@ -402,50 +409,82 @@ var DEFAULT_REGISTRY = {
402
409
  name: "claude",
403
410
  displayName: "Claude",
404
411
  binary: "claude",
405
- commandTemplate: "claude --model ${MODEL} -p ${PROMPT} --output-format text",
412
+ commandTemplate: "claude --model ${MODEL} --allowedTools '*' --print",
406
413
  tokenParser: "claude"
407
414
  },
408
415
  {
409
416
  name: "codex",
410
417
  displayName: "Codex",
411
418
  binary: "codex",
412
- commandTemplate: "codex --model ${MODEL} -p ${PROMPT}",
419
+ commandTemplate: "codex --model ${MODEL} exec",
413
420
  tokenParser: "codex"
414
421
  },
415
422
  {
416
423
  name: "gemini",
417
424
  displayName: "Gemini",
418
425
  binary: "gemini",
419
- commandTemplate: "gemini --model ${MODEL} -p ${PROMPT}",
426
+ commandTemplate: "gemini -m ${MODEL}",
420
427
  tokenParser: "gemini"
421
428
  },
422
429
  {
423
430
  name: "qwen",
424
431
  displayName: "Qwen",
425
432
  binary: "qwen",
426
- commandTemplate: "qwen --model ${MODEL} -p ${PROMPT} -y",
433
+ commandTemplate: "qwen --model ${MODEL} -y",
427
434
  tokenParser: "qwen"
428
435
  }
429
436
  ],
430
437
  models: [
431
- { name: "claude-opus-4-6", displayName: "Claude Opus 4.6", tools: ["claude"] },
438
+ {
439
+ name: "claude-opus-4-6",
440
+ displayName: "Claude Opus 4.6",
441
+ tools: ["claude"],
442
+ defaultReputation: 0.8
443
+ },
432
444
  {
433
445
  name: "claude-opus-4-6[1m]",
434
446
  displayName: "Claude Opus 4.6 (1M context)",
435
- tools: ["claude"]
447
+ tools: ["claude"],
448
+ defaultReputation: 0.8
449
+ },
450
+ {
451
+ name: "claude-sonnet-4-6",
452
+ displayName: "Claude Sonnet 4.6",
453
+ tools: ["claude"],
454
+ defaultReputation: 0.7
436
455
  },
437
- { name: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6", tools: ["claude"] },
438
456
  {
439
457
  name: "claude-sonnet-4-6[1m]",
440
458
  displayName: "Claude Sonnet 4.6 (1M context)",
441
- tools: ["claude"]
459
+ tools: ["claude"],
460
+ defaultReputation: 0.7
461
+ },
462
+ {
463
+ name: "gpt-5-codex",
464
+ displayName: "GPT-5 Codex",
465
+ tools: ["codex"],
466
+ defaultReputation: 0.7
467
+ },
468
+ {
469
+ name: "gemini-2.5-pro",
470
+ displayName: "Gemini 2.5 Pro",
471
+ tools: ["gemini"],
472
+ defaultReputation: 0.7
442
473
  },
443
- { name: "gpt-5-codex", displayName: "GPT-5 Codex", tools: ["codex"] },
444
- { name: "gemini-2.5-pro", displayName: "Gemini 2.5 Pro", tools: ["gemini"] },
445
- { name: "qwen3.5-plus", displayName: "Qwen 3.5 Plus", tools: ["qwen"] },
446
- { name: "glm-5", displayName: "GLM-5", tools: ["qwen"] },
447
- { name: "kimi-k2.5", displayName: "Kimi K2.5", tools: ["qwen"] },
448
- { name: "minimax-m2.5", displayName: "Minimax M2.5", tools: ["qwen"] }
474
+ {
475
+ name: "qwen3.5-plus",
476
+ displayName: "Qwen 3.5 Plus",
477
+ tools: ["qwen"],
478
+ defaultReputation: 0.6
479
+ },
480
+ { name: "glm-5", displayName: "GLM-5", tools: ["qwen"], defaultReputation: 0.5 },
481
+ { name: "kimi-k2.5", displayName: "Kimi K2.5", tools: ["qwen"], defaultReputation: 0.5 },
482
+ {
483
+ name: "minimax-m2.5",
484
+ displayName: "Minimax M2.5",
485
+ tools: ["qwen"],
486
+ defaultReputation: 0.5
487
+ }
449
488
  ]
450
489
  };
451
490
 
@@ -554,7 +593,7 @@ function executeTool(commandTemplate, prompt, timeoutMs, signal, vars) {
554
593
  const promptViaArg = commandTemplate.includes("${PROMPT}");
555
594
  const allVars = { ...vars, PROMPT: prompt };
556
595
  const { command, args } = parseCommandTemplate(commandTemplate, allVars);
557
- return new Promise((resolve, reject) => {
596
+ return new Promise((resolve2, reject) => {
558
597
  if (signal?.aborted) {
559
598
  reject(new ToolTimeoutError("Tool execution aborted"));
560
599
  return;
@@ -626,7 +665,7 @@ function executeTool(commandTemplate, prompt, timeoutMs, signal, vars) {
626
665
  console.warn(`Tool stderr: ${stderr.slice(0, MAX_STDERR_LENGTH)}`);
627
666
  }
628
667
  const usage2 = parseTokenUsage(stdout, stderr);
629
- resolve({ stdout, stderr, tokensUsed: usage2.tokens, tokensParsed: usage2.parsed });
668
+ resolve2({ stdout, stderr, tokensUsed: usage2.tokens, tokensParsed: usage2.parsed });
630
669
  return;
631
670
  }
632
671
  const errMsg = stderr ? `Tool "${command}" failed (exit code ${code}): ${stderr.slice(0, MAX_STDERR_LENGTH)}` : `Tool "${command}" failed with exit code ${code}`;
@@ -634,7 +673,7 @@ function executeTool(commandTemplate, prompt, timeoutMs, signal, vars) {
634
673
  return;
635
674
  }
636
675
  const usage = parseTokenUsage(stdout, stderr);
637
- resolve({ stdout, stderr, tokensUsed: usage.tokens, tokensParsed: usage.parsed });
676
+ resolve2({ stdout, stderr, tokensUsed: usage.tokens, tokensParsed: usage.parsed });
638
677
  });
639
678
  });
640
679
  }
@@ -860,6 +899,141 @@ ${userMessage}`;
860
899
  }
861
900
  }
862
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
+
863
1037
  // src/consumption.ts
864
1038
  async function checkConsumptionLimits(_agentId, _limits) {
865
1039
  return { allowed: true };
@@ -884,6 +1058,7 @@ function formatTable(agents, trustLabels) {
884
1058
  }
885
1059
  const header = [
886
1060
  "ID".padEnd(38),
1061
+ "Name".padEnd(20),
887
1062
  "Model".padEnd(22),
888
1063
  "Tool".padEnd(16),
889
1064
  "Status".padEnd(10),
@@ -892,8 +1067,16 @@ function formatTable(agents, trustLabels) {
892
1067
  console.log(header);
893
1068
  for (const a of agents) {
894
1069
  const trust = trustLabels?.get(a.id) ?? "--";
1070
+ const name = a.displayName ?? "--";
895
1071
  console.log(
896
- [a.id.padEnd(38), a.model.padEnd(22), a.tool.padEnd(16), a.status.padEnd(10), trust].join("")
1072
+ [
1073
+ a.id.padEnd(38),
1074
+ name.padEnd(20),
1075
+ a.model.padEnd(22),
1076
+ a.tool.padEnd(16),
1077
+ a.status.padEnd(10),
1078
+ trust
1079
+ ].join("")
897
1080
  );
898
1081
  }
899
1082
  }
@@ -908,6 +1091,11 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
908
1091
  const verbose = options?.verbose ?? false;
909
1092
  const stabilityThreshold = options?.stabilityThresholdMs ?? CONNECTION_STABILITY_THRESHOLD_MS;
910
1093
  const repoConfig = options?.repoConfig;
1094
+ const displayName = options?.displayName;
1095
+ const routerRelay = options?.routerRelay;
1096
+ const prefix = options?.label ? `[${options.label}]` : "";
1097
+ const log = (...args) => console.log(...prefix ? [prefix, ...args] : args);
1098
+ const logError = (...args) => console.error(...prefix ? [prefix, ...args] : args);
911
1099
  let attempt = 0;
912
1100
  let intentionalClose = false;
913
1101
  let heartbeatTimer = null;
@@ -939,7 +1127,7 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
939
1127
  clearStabilityTimer();
940
1128
  clearWsPingTimer();
941
1129
  if (currentWs) currentWs.close();
942
- console.log("Disconnected.");
1130
+ log("Disconnected.");
943
1131
  process.exit(0);
944
1132
  }
945
1133
  process.once("SIGINT", shutdown);
@@ -951,13 +1139,13 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
951
1139
  function resetHeartbeatTimer() {
952
1140
  clearHeartbeatTimer();
953
1141
  heartbeatTimer = setTimeout(() => {
954
- console.log("No heartbeat received in 90s. Reconnecting...");
1142
+ log("No heartbeat received in 90s. Reconnecting...");
955
1143
  ws.terminate();
956
1144
  }, HEARTBEAT_TIMEOUT_MS);
957
1145
  }
958
1146
  ws.on("open", () => {
959
1147
  connectionOpenedAt = Date.now();
960
- console.log("Connected to platform.");
1148
+ log("Connected to platform.");
961
1149
  resetHeartbeatTimer();
962
1150
  clearWsPingTimer();
963
1151
  wsPingTimer = setInterval(() => {
@@ -969,12 +1157,12 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
969
1157
  }
970
1158
  }, WS_PING_INTERVAL_MS);
971
1159
  if (verbose) {
972
- console.log(`[verbose] Connection opened at ${new Date(connectionOpenedAt).toISOString()}`);
1160
+ log(`[verbose] Connection opened at ${new Date(connectionOpenedAt).toISOString()}`);
973
1161
  }
974
1162
  clearStabilityTimer();
975
1163
  stabilityTimer = setTimeout(() => {
976
1164
  if (verbose) {
977
- console.log(
1165
+ log(
978
1166
  `[verbose] Connection stable for ${stabilityThreshold / 1e3}s \u2014 resetting reconnect counter`
979
1167
  );
980
1168
  }
@@ -988,7 +1176,18 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
988
1176
  } catch {
989
1177
  return;
990
1178
  }
991
- handleMessage(ws, msg, resetHeartbeatTimer, reviewDeps, consumptionDeps, verbose, repoConfig);
1179
+ handleMessage(
1180
+ ws,
1181
+ msg,
1182
+ resetHeartbeatTimer,
1183
+ reviewDeps,
1184
+ consumptionDeps,
1185
+ verbose,
1186
+ repoConfig,
1187
+ displayName,
1188
+ prefix,
1189
+ routerRelay
1190
+ );
992
1191
  });
993
1192
  ws.on("close", (code, reason) => {
994
1193
  if (intentionalClose) return;
@@ -999,14 +1198,14 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
999
1198
  if (connectionOpenedAt) {
1000
1199
  const lifetimeMs = Date.now() - connectionOpenedAt;
1001
1200
  const lifetimeSec = (lifetimeMs / 1e3).toFixed(1);
1002
- console.log(
1201
+ log(
1003
1202
  `Disconnected (code=${code}, reason=${reason.toString()}). Connection was alive for ${lifetimeSec}s.`
1004
1203
  );
1005
1204
  } else {
1006
- console.log(`Disconnected (code=${code}, reason=${reason.toString()}).`);
1205
+ log(`Disconnected (code=${code}, reason=${reason.toString()}).`);
1007
1206
  }
1008
1207
  if (code === 4002) {
1009
- console.log("Connection replaced by server \u2014 not reconnecting.");
1208
+ log("Connection replaced by server \u2014 not reconnecting.");
1010
1209
  return;
1011
1210
  }
1012
1211
  connectionOpenedAt = null;
@@ -1014,18 +1213,18 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
1014
1213
  });
1015
1214
  ws.on("pong", () => {
1016
1215
  if (verbose) {
1017
- console.log(`[verbose] WS pong received at ${(/* @__PURE__ */ new Date()).toISOString()}`);
1216
+ log(`[verbose] WS pong received at ${(/* @__PURE__ */ new Date()).toISOString()}`);
1018
1217
  }
1019
1218
  });
1020
1219
  ws.on("error", (err) => {
1021
- console.error(`WebSocket error: ${err.message}`);
1220
+ logError(`WebSocket error: ${err.message}`);
1022
1221
  });
1023
1222
  }
1024
1223
  async function reconnect() {
1025
1224
  const delay = calculateDelay(attempt, DEFAULT_RECONNECT_OPTIONS);
1026
1225
  const delaySec = (delay / 1e3).toFixed(1);
1027
1226
  attempt++;
1028
- console.log(`Reconnecting in ${delaySec}s... (attempt ${attempt})`);
1227
+ log(`Reconnecting in ${delaySec}s... (attempt ${attempt})`);
1029
1228
  await sleep(delay);
1030
1229
  connect();
1031
1230
  }
@@ -1038,16 +1237,17 @@ function trySend(ws, data) {
1038
1237
  console.error("Failed to send message \u2014 WebSocket may be closed");
1039
1238
  }
1040
1239
  }
1041
- async function logPostReviewStats(type, verdict, tokensUsed, tokensEstimated, consumptionDeps) {
1240
+ async function logPostReviewStats(type, verdict, tokensUsed, tokensEstimated, consumptionDeps, logPrefix) {
1241
+ const pfx = logPrefix ? `${logPrefix} ` : "";
1042
1242
  const estimateTag = tokensEstimated ? " ~" : " ";
1043
1243
  if (!consumptionDeps) {
1044
1244
  if (verdict) {
1045
1245
  console.log(
1046
- `${type} complete: ${verdict} (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ", estimated" : ""})`
1246
+ `${pfx}${type} complete: ${verdict} (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ", estimated" : ""})`
1047
1247
  );
1048
1248
  } else {
1049
1249
  console.log(
1050
- `${type} complete (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ", estimated" : ""})`
1250
+ `${pfx}${type} complete (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ", estimated" : ""})`
1051
1251
  );
1052
1252
  }
1053
1253
  return;
@@ -1055,38 +1255,122 @@ async function logPostReviewStats(type, verdict, tokensUsed, tokensEstimated, co
1055
1255
  recordSessionUsage(consumptionDeps.session, tokensUsed);
1056
1256
  if (verdict) {
1057
1257
  console.log(
1058
- `${type} complete: ${verdict} (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ", estimated" : ""})`
1258
+ `${pfx}${type} complete: ${verdict} (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ", estimated" : ""})`
1059
1259
  );
1060
1260
  } else {
1061
1261
  console.log(
1062
- `${type} complete (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ", estimated" : ""})`
1262
+ `${pfx}${type} complete (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ", estimated" : ""})`
1063
1263
  );
1064
1264
  }
1065
- console.log(formatPostReviewStats(tokensUsed, consumptionDeps.session, consumptionDeps.limits));
1265
+ console.log(
1266
+ `${pfx}${formatPostReviewStats(tokensUsed, consumptionDeps.session, consumptionDeps.limits)}`
1267
+ );
1066
1268
  }
1067
- function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, verbose, repoConfig) {
1269
+ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, verbose, repoConfig, displayName, logPrefix, routerRelay) {
1270
+ const pfx = logPrefix ? `${logPrefix} ` : "";
1068
1271
  switch (msg.type) {
1069
1272
  case "connected":
1070
- console.log(`Authenticated. Protocol v${msg.version ?? "unknown"}`);
1273
+ console.log(`${pfx}Authenticated. Protocol v${msg.version ?? "unknown"}`);
1071
1274
  trySend(ws, {
1072
1275
  type: "agent_preferences",
1073
1276
  id: crypto2.randomUUID(),
1074
1277
  timestamp: Date.now(),
1278
+ ...displayName ? { displayName } : {},
1075
1279
  repoConfig: repoConfig ?? { mode: "all" }
1076
1280
  });
1281
+ if (routerRelay) {
1282
+ routerRelay.writeStatus("Waiting for review requests...");
1283
+ }
1077
1284
  break;
1078
1285
  case "heartbeat_ping":
1079
1286
  ws.send(JSON.stringify({ type: "heartbeat_pong", timestamp: Date.now() }));
1080
1287
  if (verbose) {
1081
- console.log(`[verbose] Heartbeat ping received, pong sent at ${(/* @__PURE__ */ new Date()).toISOString()}`);
1288
+ console.log(
1289
+ `${pfx}[verbose] Heartbeat ping received, pong sent at ${(/* @__PURE__ */ new Date()).toISOString()}`
1290
+ );
1082
1291
  }
1083
1292
  if (resetHeartbeat) resetHeartbeat();
1084
1293
  break;
1085
1294
  case "review_request": {
1086
1295
  const request = msg;
1087
1296
  console.log(
1088
- `Review request: task ${request.taskId} for ${request.project.owner}/${request.project.repo}#${request.pr.number}`
1297
+ `${pfx}Review request: task ${request.taskId} for ${request.project.owner}/${request.project.repo}#${request.pr.number}`
1089
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
+ }
1090
1374
  if (!reviewDeps) {
1091
1375
  ws.send(
1092
1376
  JSON.stringify({
@@ -1113,7 +1397,7 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
1113
1397
  taskId: request.taskId,
1114
1398
  reason: limitResult.reason ?? "consumption_limit_exceeded"
1115
1399
  });
1116
- console.log(`Review rejected: ${limitResult.reason}`);
1400
+ console.log(`${pfx}Review rejected: ${limitResult.reason}`);
1117
1401
  return;
1118
1402
  }
1119
1403
  }
@@ -1145,7 +1429,8 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
1145
1429
  result.verdict,
1146
1430
  result.tokensUsed,
1147
1431
  result.tokensEstimated,
1148
- consumptionDeps
1432
+ consumptionDeps,
1433
+ logPrefix
1149
1434
  );
1150
1435
  } catch (err) {
1151
1436
  if (err instanceof DiffTooLargeError) {
@@ -1165,7 +1450,7 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
1165
1450
  error: err instanceof Error ? err.message : "Unknown error"
1166
1451
  });
1167
1452
  }
1168
- console.error("Review failed:", err);
1453
+ console.error(`${pfx}Review failed:`, err);
1169
1454
  }
1170
1455
  })();
1171
1456
  break;
@@ -1173,8 +1458,81 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
1173
1458
  case "summary_request": {
1174
1459
  const summaryRequest = msg;
1175
1460
  console.log(
1176
- `Summary request: task ${summaryRequest.taskId} for ${summaryRequest.project.owner}/${summaryRequest.project.repo}#${summaryRequest.pr.number} (${summaryRequest.reviews.length} reviews)`
1461
+ `${pfx}Summary request: task ${summaryRequest.taskId} for ${summaryRequest.project.owner}/${summaryRequest.project.repo}#${summaryRequest.pr.number} (${summaryRequest.reviews.length} reviews)`
1177
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
+ }
1178
1536
  if (!reviewDeps) {
1179
1537
  trySend(ws, {
1180
1538
  type: "review_rejected",
@@ -1199,7 +1557,7 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
1199
1557
  taskId: summaryRequest.taskId,
1200
1558
  reason: limitResult.reason ?? "consumption_limit_exceeded"
1201
1559
  });
1202
- console.log(`Summary rejected: ${limitResult.reason}`);
1560
+ console.log(`${pfx}Summary rejected: ${limitResult.reason}`);
1203
1561
  return;
1204
1562
  }
1205
1563
  }
@@ -1230,7 +1588,8 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
1230
1588
  void 0,
1231
1589
  result.tokensUsed,
1232
1590
  result.tokensEstimated,
1233
- consumptionDeps
1591
+ consumptionDeps,
1592
+ logPrefix
1234
1593
  );
1235
1594
  } catch (err) {
1236
1595
  if (err instanceof InputTooLargeError) {
@@ -1250,13 +1609,13 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
1250
1609
  error: err instanceof Error ? err.message : "Summary failed"
1251
1610
  });
1252
1611
  }
1253
- console.error("Summary failed:", err);
1612
+ console.error(`${pfx}Summary failed:`, err);
1254
1613
  }
1255
1614
  })();
1256
1615
  break;
1257
1616
  }
1258
1617
  case "error":
1259
- console.error(`Platform error: ${msg.code ?? "unknown"}`);
1618
+ console.error(`${pfx}Platform error: ${msg.code ?? "unknown"}`);
1260
1619
  if (msg.code === "auth_revoked") process.exit(1);
1261
1620
  break;
1262
1621
  default:
@@ -1271,6 +1630,9 @@ async function syncAgentToServer(client, serverAgents, localAgent) {
1271
1630
  return { agentId: existing.id, created: false };
1272
1631
  }
1273
1632
  const body = { model: localAgent.model, tool: localAgent.tool };
1633
+ if (localAgent.name) {
1634
+ body.displayName = localAgent.name;
1635
+ }
1274
1636
  if (localAgent.repos) {
1275
1637
  body.repoConfig = localAgent.repos;
1276
1638
  }
@@ -1281,6 +1643,12 @@ function resolveLocalAgentCommand(localAgent, globalAgentCommand) {
1281
1643
  const effectiveCommand = localAgent.command ?? globalAgentCommand;
1282
1644
  return resolveCommandTemplate(effectiveCommand);
1283
1645
  }
1646
+ function startAgentRouter() {
1647
+ void agentCommand.parseAsync(
1648
+ ["start", "--router", "--anonymous", "--model", "router", "--tool", "opencara"],
1649
+ { from: "user" }
1650
+ );
1651
+ }
1284
1652
  var agentCommand = new Command2("agent").description("Manage review agents");
1285
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) => {
1286
1654
  const config = loadConfig();
@@ -1360,7 +1728,7 @@ agentCommand.command("create").description("Add an agent to local config (intera
1360
1728
  break;
1361
1729
  }
1362
1730
  const toolEntry = registry.tools.find((t) => t.name === tool);
1363
- 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}`;
1364
1732
  command = await input({
1365
1733
  message: "Command:",
1366
1734
  default: defaultCommand,
@@ -1500,7 +1868,7 @@ async function resolveAnonymousAgent(config, model, tool) {
1500
1868
  );
1501
1869
  return { entry, command };
1502
1870
  }
1503
- 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(
1504
1872
  "--stability-threshold <ms>",
1505
1873
  `Connection stability threshold in ms (${STABILITY_THRESHOLD_MIN_MS}\u2013${STABILITY_THRESHOLD_MAX_MS}, default: ${CONNECTION_STABILITY_THRESHOLD_MS})`
1506
1874
  ).action(
@@ -1522,24 +1890,57 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1522
1890
  console.error("Both --model and --tool are required with --anonymous.");
1523
1891
  process.exit(1);
1524
1892
  }
1525
- let resolved;
1526
- try {
1527
- resolved = await resolveAnonymousAgent(config, opts.model, opts.tool);
1528
- } catch (err) {
1529
- console.error(
1530
- "Failed to register anonymous agent:",
1531
- err instanceof Error ? err.message : err
1532
- );
1533
- process.exit(1);
1534
- }
1535
- const { entry, command } = resolved;
1893
+ let entry;
1536
1894
  let reviewDeps2;
1537
- if (validateCommandBinary(command)) {
1538
- reviewDeps2 = { commandTemplate: command, maxDiffSizeKb: config.maxDiffSizeKb };
1539
- } else {
1540
- console.warn(
1541
- `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
1542
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
+ }
1543
1944
  }
1544
1945
  const consumptionDeps2 = {
1545
1946
  agentId: entry.agentId,
@@ -1550,13 +1951,20 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1550
1951
  startAgent(entry.agentId, config.platformUrl, entry.apiKey, reviewDeps2, consumptionDeps2, {
1551
1952
  verbose: opts.verbose,
1552
1953
  stabilityThresholdMs,
1553
- repoConfig: entry.repoConfig
1954
+ displayName: entry.name,
1955
+ repoConfig: entry.repoConfig,
1956
+ routerRelay: relay2
1554
1957
  });
1555
1958
  return;
1556
1959
  }
1557
1960
  if (config.agents !== null) {
1961
+ const routerAgents = [];
1558
1962
  const validAgents = [];
1559
1963
  for (const local of config.agents) {
1964
+ if (opts.router || local.router) {
1965
+ routerAgents.push(local);
1966
+ continue;
1967
+ }
1560
1968
  let cmd;
1561
1969
  try {
1562
1970
  cmd = resolveLocalAgentCommand(local, config.agentCommand);
@@ -1574,30 +1982,45 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1574
1982
  }
1575
1983
  validAgents.push({ local, command: cmd });
1576
1984
  }
1577
- if (validAgents.length === 0 && config.anonymousAgents.length === 0) {
1985
+ const totalValid = validAgents.length + routerAgents.length;
1986
+ if (totalValid === 0 && config.anonymousAgents.length === 0) {
1578
1987
  console.error("No valid agents in config. Check that tool binaries are installed.");
1579
1988
  process.exit(1);
1580
1989
  }
1581
1990
  let agentsToStart;
1991
+ let routerAgentsToStart;
1582
1992
  const anonAgentsToStart = [];
1583
1993
  if (opts.all) {
1584
1994
  agentsToStart = validAgents;
1995
+ routerAgentsToStart = routerAgents;
1585
1996
  anonAgentsToStart.push(...config.anonymousAgents);
1586
1997
  } else if (agentIdOrModel) {
1587
- const match = validAgents.find((a) => a.local.model === agentIdOrModel);
1588
- 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) {
1589
2001
  console.error(`No agent with model "${agentIdOrModel}" found in local config.`);
1590
2002
  console.error("Available agents:");
1591
2003
  for (const a of validAgents) {
1592
2004
  console.error(` ${a.local.model} (${a.local.tool})`);
1593
2005
  }
2006
+ for (const a of routerAgents) {
2007
+ console.error(` ${a.model} (${a.tool}) [router]`);
2008
+ }
1594
2009
  process.exit(1);
1595
2010
  }
1596
- agentsToStart = [match];
1597
- } else if (validAgents.length === 1) {
1598
- agentsToStart = [validAgents[0]];
1599
- console.log(`Using agent ${validAgents[0].local.model} (${validAgents[0].local.tool})`);
1600
- } 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) {
1601
2024
  console.error("No valid authenticated agents in config. Use --anonymous or --all.");
1602
2025
  process.exit(1);
1603
2026
  } else {
@@ -1605,9 +2028,12 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1605
2028
  for (const a of validAgents) {
1606
2029
  console.error(` ${a.local.model} (${a.local.tool})`);
1607
2030
  }
2031
+ for (const a of routerAgents) {
2032
+ console.error(` ${a.model} (${a.tool}) [router]`);
2033
+ }
1608
2034
  process.exit(1);
1609
2035
  }
1610
- const totalAgents = agentsToStart.length + anonAgentsToStart.length;
2036
+ const totalAgents = agentsToStart.length + routerAgentsToStart.length + anonAgentsToStart.length;
1611
2037
  if (totalAgents > 1) {
1612
2038
  process.setMaxListeners(process.getMaxListeners() + totalAgents * 2);
1613
2039
  }
@@ -1615,7 +2041,7 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1615
2041
  let apiKey2;
1616
2042
  let client2;
1617
2043
  let serverAgents;
1618
- if (agentsToStart.length > 0) {
2044
+ if (agentsToStart.length > 0 || routerAgentsToStart.length > 0) {
1619
2045
  apiKey2 = requireApiKey(config);
1620
2046
  client2 = new ApiClient(config.platformUrl, apiKey2);
1621
2047
  try {
@@ -1659,11 +2085,55 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1659
2085
  limits: resolveAgentLimits(selected.local.limits, config.limits),
1660
2086
  session: createSessionTracker()
1661
2087
  };
1662
- console.log(`Starting agent ${selected.local.model} (${agentId2})...`);
2088
+ const label = selected.local.name || selected.local.model || "unnamed";
2089
+ console.log(`Starting agent ${label} (${agentId2})...`);
1663
2090
  startAgent(agentId2, config.platformUrl, apiKey2, reviewDeps2, consumptionDeps2, {
1664
2091
  verbose: opts.verbose,
1665
2092
  stabilityThresholdMs,
1666
- repoConfig: selected.local.repos
2093
+ repoConfig: selected.local.repos,
2094
+ label
2095
+ });
2096
+ startedCount++;
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
1667
2137
  });
1668
2138
  startedCount++;
1669
2139
  }
@@ -1692,11 +2162,14 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1692
2162
  limits: config.limits,
1693
2163
  session: createSessionTracker()
1694
2164
  };
1695
- console.log(`Starting anonymous agent ${anon.model} (${anon.agentId})...`);
2165
+ const anonLabel = anon.name || anon.model || "anonymous";
2166
+ console.log(`Starting anonymous agent ${anonLabel} (${anon.agentId})...`);
1696
2167
  startAgent(anon.agentId, config.platformUrl, anon.apiKey, reviewDeps2, consumptionDeps2, {
1697
2168
  verbose: opts.verbose,
1698
2169
  stabilityThresholdMs,
1699
- repoConfig: anon.repoConfig
2170
+ displayName: anon.name,
2171
+ repoConfig: anon.repoConfig,
2172
+ label: anonLabel
1700
2173
  });
1701
2174
  startedCount++;
1702
2175
  }
@@ -1736,16 +2209,22 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1736
2209
  }
1737
2210
  }
1738
2211
  let reviewDeps;
1739
- try {
1740
- const commandTemplate = resolveCommandTemplate(config.agentCommand);
1741
- reviewDeps = {
1742
- commandTemplate,
1743
- maxDiffSizeKb: config.maxDiffSizeKb
1744
- };
1745
- } catch (err) {
1746
- console.warn(
1747
- `Warning: ${err instanceof Error ? err.message : "Could not determine agent command."} Reviews will be rejected.`
1748
- );
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
+ }
1749
2228
  }
1750
2229
  const consumptionDeps = {
1751
2230
  agentId,
@@ -1755,7 +2234,9 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
1755
2234
  console.log(`Starting agent ${agentId}...`);
1756
2235
  startAgent(agentId, config.platformUrl, apiKey, reviewDeps, consumptionDeps, {
1757
2236
  verbose: opts.verbose,
1758
- stabilityThresholdMs
2237
+ stabilityThresholdMs,
2238
+ label: agentId,
2239
+ routerRelay: relay
1759
2240
  });
1760
2241
  }
1761
2242
  );
@@ -1936,8 +2417,11 @@ var statsCommand = new Command3("stats").description("Display agent dashboard: t
1936
2417
  });
1937
2418
 
1938
2419
  // src/index.ts
1939
- var program = new Command4().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.7.0");
2420
+ var program = new Command4().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.9.0");
1940
2421
  program.addCommand(loginCommand);
1941
2422
  program.addCommand(agentCommand);
1942
2423
  program.addCommand(statsCommand);
2424
+ program.action(() => {
2425
+ startAgentRouter();
2426
+ });
1943
2427
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",