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.
- package/dist/index.js +451 -47
- 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}
|
|
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}
|
|
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
|
|
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} -
|
|
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}
|
|
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
|
|
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
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
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
|
-
|
|
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
|
|
1662
|
-
|
|
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 = [
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
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
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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.
|
|
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();
|