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.
- package/dist/index.js +582 -98
- 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
|
-
|
|
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((
|
|
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((
|
|
309
|
+
return new Promise((resolve2) => {
|
|
303
310
|
rl.question(question, (answer) => {
|
|
304
311
|
rl.close();
|
|
305
312
|
const normalized = answer.trim().toLowerCase();
|
|
306
|
-
|
|
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}
|
|
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}
|
|
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
|
|
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} -
|
|
433
|
+
commandTemplate: "qwen --model ${MODEL} -y",
|
|
427
434
|
tokenParser: "qwen"
|
|
428
435
|
}
|
|
429
436
|
],
|
|
430
437
|
models: [
|
|
431
|
-
{
|
|
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
|
-
{
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1160
|
+
log(`[verbose] Connection opened at ${new Date(connectionOpenedAt).toISOString()}`);
|
|
973
1161
|
}
|
|
974
1162
|
clearStabilityTimer();
|
|
975
1163
|
stabilityTimer = setTimeout(() => {
|
|
976
1164
|
if (verbose) {
|
|
977
|
-
|
|
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(
|
|
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
|
-
|
|
1201
|
+
log(
|
|
1003
1202
|
`Disconnected (code=${code}, reason=${reason.toString()}). Connection was alive for ${lifetimeSec}s.`
|
|
1004
1203
|
);
|
|
1005
1204
|
} else {
|
|
1006
|
-
|
|
1205
|
+
log(`Disconnected (code=${code}, reason=${reason.toString()}).`);
|
|
1007
1206
|
}
|
|
1008
1207
|
if (code === 4002) {
|
|
1009
|
-
|
|
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
|
-
|
|
1216
|
+
log(`[verbose] WS pong received at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
1018
1217
|
}
|
|
1019
1218
|
});
|
|
1020
1219
|
ws.on("error", (err) => {
|
|
1021
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
1612
|
+
console.error(`${pfx}Summary failed:`, err);
|
|
1254
1613
|
}
|
|
1255
1614
|
})();
|
|
1256
1615
|
break;
|
|
1257
1616
|
}
|
|
1258
1617
|
case "error":
|
|
1259
|
-
console.error(
|
|
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}
|
|
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
|
|
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
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1588
|
-
|
|
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 = [
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
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.
|
|
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();
|