opencara 0.6.2 → 0.8.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 +62 -45
- 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) {
|
|
@@ -298,17 +299,17 @@ function calculateDelay(attempt, options = DEFAULT_RECONNECT_OPTIONS) {
|
|
|
298
299
|
return base;
|
|
299
300
|
}
|
|
300
301
|
function sleep(ms) {
|
|
301
|
-
return new Promise((
|
|
302
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
302
303
|
}
|
|
303
304
|
|
|
304
305
|
// src/commands/login.ts
|
|
305
306
|
function promptYesNo(question) {
|
|
306
307
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
307
|
-
return new Promise((
|
|
308
|
+
return new Promise((resolve2) => {
|
|
308
309
|
rl.question(question, (answer) => {
|
|
309
310
|
rl.close();
|
|
310
311
|
const normalized = answer.trim().toLowerCase();
|
|
311
|
-
|
|
312
|
+
resolve2(normalized === "" || normalized === "y" || normalized === "yes");
|
|
312
313
|
});
|
|
313
314
|
});
|
|
314
315
|
}
|
|
@@ -591,7 +592,7 @@ function executeTool(commandTemplate, prompt, timeoutMs, signal, vars) {
|
|
|
591
592
|
const promptViaArg = commandTemplate.includes("${PROMPT}");
|
|
592
593
|
const allVars = { ...vars, PROMPT: prompt };
|
|
593
594
|
const { command, args } = parseCommandTemplate(commandTemplate, allVars);
|
|
594
|
-
return new Promise((
|
|
595
|
+
return new Promise((resolve2, reject) => {
|
|
595
596
|
if (signal?.aborted) {
|
|
596
597
|
reject(new ToolTimeoutError("Tool execution aborted"));
|
|
597
598
|
return;
|
|
@@ -663,7 +664,7 @@ function executeTool(commandTemplate, prompt, timeoutMs, signal, vars) {
|
|
|
663
664
|
console.warn(`Tool stderr: ${stderr.slice(0, MAX_STDERR_LENGTH)}`);
|
|
664
665
|
}
|
|
665
666
|
const usage2 = parseTokenUsage(stdout, stderr);
|
|
666
|
-
|
|
667
|
+
resolve2({ stdout, stderr, tokensUsed: usage2.tokens, tokensParsed: usage2.parsed });
|
|
667
668
|
return;
|
|
668
669
|
}
|
|
669
670
|
const errMsg = stderr ? `Tool "${command}" failed (exit code ${code}): ${stderr.slice(0, MAX_STDERR_LENGTH)}` : `Tool "${command}" failed with exit code ${code}`;
|
|
@@ -671,7 +672,7 @@ function executeTool(commandTemplate, prompt, timeoutMs, signal, vars) {
|
|
|
671
672
|
return;
|
|
672
673
|
}
|
|
673
674
|
const usage = parseTokenUsage(stdout, stderr);
|
|
674
|
-
|
|
675
|
+
resolve2({ stdout, stderr, tokensUsed: usage.tokens, tokensParsed: usage.parsed });
|
|
675
676
|
});
|
|
676
677
|
});
|
|
677
678
|
}
|
|
@@ -955,6 +956,9 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
|
|
|
955
956
|
const stabilityThreshold = options?.stabilityThresholdMs ?? CONNECTION_STABILITY_THRESHOLD_MS;
|
|
956
957
|
const repoConfig = options?.repoConfig;
|
|
957
958
|
const displayName = options?.displayName;
|
|
959
|
+
const prefix = options?.label ? `[${options.label}]` : "";
|
|
960
|
+
const log = (...args) => console.log(...prefix ? [prefix, ...args] : args);
|
|
961
|
+
const logError = (...args) => console.error(...prefix ? [prefix, ...args] : args);
|
|
958
962
|
let attempt = 0;
|
|
959
963
|
let intentionalClose = false;
|
|
960
964
|
let heartbeatTimer = null;
|
|
@@ -986,7 +990,7 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
|
|
|
986
990
|
clearStabilityTimer();
|
|
987
991
|
clearWsPingTimer();
|
|
988
992
|
if (currentWs) currentWs.close();
|
|
989
|
-
|
|
993
|
+
log("Disconnected.");
|
|
990
994
|
process.exit(0);
|
|
991
995
|
}
|
|
992
996
|
process.once("SIGINT", shutdown);
|
|
@@ -998,13 +1002,13 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
|
|
|
998
1002
|
function resetHeartbeatTimer() {
|
|
999
1003
|
clearHeartbeatTimer();
|
|
1000
1004
|
heartbeatTimer = setTimeout(() => {
|
|
1001
|
-
|
|
1005
|
+
log("No heartbeat received in 90s. Reconnecting...");
|
|
1002
1006
|
ws.terminate();
|
|
1003
1007
|
}, HEARTBEAT_TIMEOUT_MS);
|
|
1004
1008
|
}
|
|
1005
1009
|
ws.on("open", () => {
|
|
1006
1010
|
connectionOpenedAt = Date.now();
|
|
1007
|
-
|
|
1011
|
+
log("Connected to platform.");
|
|
1008
1012
|
resetHeartbeatTimer();
|
|
1009
1013
|
clearWsPingTimer();
|
|
1010
1014
|
wsPingTimer = setInterval(() => {
|
|
@@ -1016,12 +1020,12 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
|
|
|
1016
1020
|
}
|
|
1017
1021
|
}, WS_PING_INTERVAL_MS);
|
|
1018
1022
|
if (verbose) {
|
|
1019
|
-
|
|
1023
|
+
log(`[verbose] Connection opened at ${new Date(connectionOpenedAt).toISOString()}`);
|
|
1020
1024
|
}
|
|
1021
1025
|
clearStabilityTimer();
|
|
1022
1026
|
stabilityTimer = setTimeout(() => {
|
|
1023
1027
|
if (verbose) {
|
|
1024
|
-
|
|
1028
|
+
log(
|
|
1025
1029
|
`[verbose] Connection stable for ${stabilityThreshold / 1e3}s \u2014 resetting reconnect counter`
|
|
1026
1030
|
);
|
|
1027
1031
|
}
|
|
@@ -1043,7 +1047,8 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
|
|
|
1043
1047
|
consumptionDeps,
|
|
1044
1048
|
verbose,
|
|
1045
1049
|
repoConfig,
|
|
1046
|
-
displayName
|
|
1050
|
+
displayName,
|
|
1051
|
+
prefix
|
|
1047
1052
|
);
|
|
1048
1053
|
});
|
|
1049
1054
|
ws.on("close", (code, reason) => {
|
|
@@ -1055,14 +1060,14 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
|
|
|
1055
1060
|
if (connectionOpenedAt) {
|
|
1056
1061
|
const lifetimeMs = Date.now() - connectionOpenedAt;
|
|
1057
1062
|
const lifetimeSec = (lifetimeMs / 1e3).toFixed(1);
|
|
1058
|
-
|
|
1063
|
+
log(
|
|
1059
1064
|
`Disconnected (code=${code}, reason=${reason.toString()}). Connection was alive for ${lifetimeSec}s.`
|
|
1060
1065
|
);
|
|
1061
1066
|
} else {
|
|
1062
|
-
|
|
1067
|
+
log(`Disconnected (code=${code}, reason=${reason.toString()}).`);
|
|
1063
1068
|
}
|
|
1064
1069
|
if (code === 4002) {
|
|
1065
|
-
|
|
1070
|
+
log("Connection replaced by server \u2014 not reconnecting.");
|
|
1066
1071
|
return;
|
|
1067
1072
|
}
|
|
1068
1073
|
connectionOpenedAt = null;
|
|
@@ -1070,18 +1075,18 @@ function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, o
|
|
|
1070
1075
|
});
|
|
1071
1076
|
ws.on("pong", () => {
|
|
1072
1077
|
if (verbose) {
|
|
1073
|
-
|
|
1078
|
+
log(`[verbose] WS pong received at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
1074
1079
|
}
|
|
1075
1080
|
});
|
|
1076
1081
|
ws.on("error", (err) => {
|
|
1077
|
-
|
|
1082
|
+
logError(`WebSocket error: ${err.message}`);
|
|
1078
1083
|
});
|
|
1079
1084
|
}
|
|
1080
1085
|
async function reconnect() {
|
|
1081
1086
|
const delay = calculateDelay(attempt, DEFAULT_RECONNECT_OPTIONS);
|
|
1082
1087
|
const delaySec = (delay / 1e3).toFixed(1);
|
|
1083
1088
|
attempt++;
|
|
1084
|
-
|
|
1089
|
+
log(`Reconnecting in ${delaySec}s... (attempt ${attempt})`);
|
|
1085
1090
|
await sleep(delay);
|
|
1086
1091
|
connect();
|
|
1087
1092
|
}
|
|
@@ -1094,16 +1099,17 @@ function trySend(ws, data) {
|
|
|
1094
1099
|
console.error("Failed to send message \u2014 WebSocket may be closed");
|
|
1095
1100
|
}
|
|
1096
1101
|
}
|
|
1097
|
-
async function logPostReviewStats(type, verdict, tokensUsed, tokensEstimated, consumptionDeps) {
|
|
1102
|
+
async function logPostReviewStats(type, verdict, tokensUsed, tokensEstimated, consumptionDeps, logPrefix) {
|
|
1103
|
+
const pfx = logPrefix ? `${logPrefix} ` : "";
|
|
1098
1104
|
const estimateTag = tokensEstimated ? " ~" : " ";
|
|
1099
1105
|
if (!consumptionDeps) {
|
|
1100
1106
|
if (verdict) {
|
|
1101
1107
|
console.log(
|
|
1102
|
-
`${type} complete: ${verdict} (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1108
|
+
`${pfx}${type} complete: ${verdict} (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1103
1109
|
);
|
|
1104
1110
|
} else {
|
|
1105
1111
|
console.log(
|
|
1106
|
-
`${type} complete (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1112
|
+
`${pfx}${type} complete (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1107
1113
|
);
|
|
1108
1114
|
}
|
|
1109
1115
|
return;
|
|
@@ -1111,19 +1117,22 @@ async function logPostReviewStats(type, verdict, tokensUsed, tokensEstimated, co
|
|
|
1111
1117
|
recordSessionUsage(consumptionDeps.session, tokensUsed);
|
|
1112
1118
|
if (verdict) {
|
|
1113
1119
|
console.log(
|
|
1114
|
-
`${type} complete: ${verdict} (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1120
|
+
`${pfx}${type} complete: ${verdict} (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1115
1121
|
);
|
|
1116
1122
|
} else {
|
|
1117
1123
|
console.log(
|
|
1118
|
-
`${type} complete (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1124
|
+
`${pfx}${type} complete (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1119
1125
|
);
|
|
1120
1126
|
}
|
|
1121
|
-
console.log(
|
|
1127
|
+
console.log(
|
|
1128
|
+
`${pfx}${formatPostReviewStats(tokensUsed, consumptionDeps.session, consumptionDeps.limits)}`
|
|
1129
|
+
);
|
|
1122
1130
|
}
|
|
1123
|
-
function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, verbose, repoConfig, displayName) {
|
|
1131
|
+
function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, verbose, repoConfig, displayName, logPrefix) {
|
|
1132
|
+
const pfx = logPrefix ? `${logPrefix} ` : "";
|
|
1124
1133
|
switch (msg.type) {
|
|
1125
1134
|
case "connected":
|
|
1126
|
-
console.log(
|
|
1135
|
+
console.log(`${pfx}Authenticated. Protocol v${msg.version ?? "unknown"}`);
|
|
1127
1136
|
trySend(ws, {
|
|
1128
1137
|
type: "agent_preferences",
|
|
1129
1138
|
id: crypto2.randomUUID(),
|
|
@@ -1135,14 +1144,16 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
|
|
|
1135
1144
|
case "heartbeat_ping":
|
|
1136
1145
|
ws.send(JSON.stringify({ type: "heartbeat_pong", timestamp: Date.now() }));
|
|
1137
1146
|
if (verbose) {
|
|
1138
|
-
console.log(
|
|
1147
|
+
console.log(
|
|
1148
|
+
`${pfx}[verbose] Heartbeat ping received, pong sent at ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
1149
|
+
);
|
|
1139
1150
|
}
|
|
1140
1151
|
if (resetHeartbeat) resetHeartbeat();
|
|
1141
1152
|
break;
|
|
1142
1153
|
case "review_request": {
|
|
1143
1154
|
const request = msg;
|
|
1144
1155
|
console.log(
|
|
1145
|
-
|
|
1156
|
+
`${pfx}Review request: task ${request.taskId} for ${request.project.owner}/${request.project.repo}#${request.pr.number}`
|
|
1146
1157
|
);
|
|
1147
1158
|
if (!reviewDeps) {
|
|
1148
1159
|
ws.send(
|
|
@@ -1170,7 +1181,7 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
|
|
|
1170
1181
|
taskId: request.taskId,
|
|
1171
1182
|
reason: limitResult.reason ?? "consumption_limit_exceeded"
|
|
1172
1183
|
});
|
|
1173
|
-
console.log(
|
|
1184
|
+
console.log(`${pfx}Review rejected: ${limitResult.reason}`);
|
|
1174
1185
|
return;
|
|
1175
1186
|
}
|
|
1176
1187
|
}
|
|
@@ -1202,7 +1213,8 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
|
|
|
1202
1213
|
result.verdict,
|
|
1203
1214
|
result.tokensUsed,
|
|
1204
1215
|
result.tokensEstimated,
|
|
1205
|
-
consumptionDeps
|
|
1216
|
+
consumptionDeps,
|
|
1217
|
+
logPrefix
|
|
1206
1218
|
);
|
|
1207
1219
|
} catch (err) {
|
|
1208
1220
|
if (err instanceof DiffTooLargeError) {
|
|
@@ -1222,7 +1234,7 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
|
|
|
1222
1234
|
error: err instanceof Error ? err.message : "Unknown error"
|
|
1223
1235
|
});
|
|
1224
1236
|
}
|
|
1225
|
-
console.error(
|
|
1237
|
+
console.error(`${pfx}Review failed:`, err);
|
|
1226
1238
|
}
|
|
1227
1239
|
})();
|
|
1228
1240
|
break;
|
|
@@ -1230,7 +1242,7 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
|
|
|
1230
1242
|
case "summary_request": {
|
|
1231
1243
|
const summaryRequest = msg;
|
|
1232
1244
|
console.log(
|
|
1233
|
-
|
|
1245
|
+
`${pfx}Summary request: task ${summaryRequest.taskId} for ${summaryRequest.project.owner}/${summaryRequest.project.repo}#${summaryRequest.pr.number} (${summaryRequest.reviews.length} reviews)`
|
|
1234
1246
|
);
|
|
1235
1247
|
if (!reviewDeps) {
|
|
1236
1248
|
trySend(ws, {
|
|
@@ -1256,7 +1268,7 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
|
|
|
1256
1268
|
taskId: summaryRequest.taskId,
|
|
1257
1269
|
reason: limitResult.reason ?? "consumption_limit_exceeded"
|
|
1258
1270
|
});
|
|
1259
|
-
console.log(
|
|
1271
|
+
console.log(`${pfx}Summary rejected: ${limitResult.reason}`);
|
|
1260
1272
|
return;
|
|
1261
1273
|
}
|
|
1262
1274
|
}
|
|
@@ -1287,7 +1299,8 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
|
|
|
1287
1299
|
void 0,
|
|
1288
1300
|
result.tokensUsed,
|
|
1289
1301
|
result.tokensEstimated,
|
|
1290
|
-
consumptionDeps
|
|
1302
|
+
consumptionDeps,
|
|
1303
|
+
logPrefix
|
|
1291
1304
|
);
|
|
1292
1305
|
} catch (err) {
|
|
1293
1306
|
if (err instanceof InputTooLargeError) {
|
|
@@ -1307,13 +1320,13 @@ function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, ver
|
|
|
1307
1320
|
error: err instanceof Error ? err.message : "Summary failed"
|
|
1308
1321
|
});
|
|
1309
1322
|
}
|
|
1310
|
-
console.error(
|
|
1323
|
+
console.error(`${pfx}Summary failed:`, err);
|
|
1311
1324
|
}
|
|
1312
1325
|
})();
|
|
1313
1326
|
break;
|
|
1314
1327
|
}
|
|
1315
1328
|
case "error":
|
|
1316
|
-
console.error(
|
|
1329
|
+
console.error(`${pfx}Platform error: ${msg.code ?? "unknown"}`);
|
|
1317
1330
|
if (msg.code === "auth_revoked") process.exit(1);
|
|
1318
1331
|
break;
|
|
1319
1332
|
default:
|
|
@@ -1720,12 +1733,13 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
|
|
|
1720
1733
|
limits: resolveAgentLimits(selected.local.limits, config.limits),
|
|
1721
1734
|
session: createSessionTracker()
|
|
1722
1735
|
};
|
|
1723
|
-
|
|
1736
|
+
const label = selected.local.name || selected.local.model || "unnamed";
|
|
1737
|
+
console.log(`Starting agent ${label} (${agentId2})...`);
|
|
1724
1738
|
startAgent(agentId2, config.platformUrl, apiKey2, reviewDeps2, consumptionDeps2, {
|
|
1725
1739
|
verbose: opts.verbose,
|
|
1726
1740
|
stabilityThresholdMs,
|
|
1727
|
-
|
|
1728
|
-
|
|
1741
|
+
repoConfig: selected.local.repos,
|
|
1742
|
+
label
|
|
1729
1743
|
});
|
|
1730
1744
|
startedCount++;
|
|
1731
1745
|
}
|
|
@@ -1754,12 +1768,14 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
|
|
|
1754
1768
|
limits: config.limits,
|
|
1755
1769
|
session: createSessionTracker()
|
|
1756
1770
|
};
|
|
1757
|
-
|
|
1771
|
+
const anonLabel = anon.name || anon.model || "anonymous";
|
|
1772
|
+
console.log(`Starting anonymous agent ${anonLabel} (${anon.agentId})...`);
|
|
1758
1773
|
startAgent(anon.agentId, config.platformUrl, anon.apiKey, reviewDeps2, consumptionDeps2, {
|
|
1759
1774
|
verbose: opts.verbose,
|
|
1760
1775
|
stabilityThresholdMs,
|
|
1761
1776
|
displayName: anon.name,
|
|
1762
|
-
repoConfig: anon.repoConfig
|
|
1777
|
+
repoConfig: anon.repoConfig,
|
|
1778
|
+
label: anonLabel
|
|
1763
1779
|
});
|
|
1764
1780
|
startedCount++;
|
|
1765
1781
|
}
|
|
@@ -1818,7 +1834,8 @@ agentCommand.command("start [agentIdOrModel]").description("Connect agent to pla
|
|
|
1818
1834
|
console.log(`Starting agent ${agentId}...`);
|
|
1819
1835
|
startAgent(agentId, config.platformUrl, apiKey, reviewDeps, consumptionDeps, {
|
|
1820
1836
|
verbose: opts.verbose,
|
|
1821
|
-
stabilityThresholdMs
|
|
1837
|
+
stabilityThresholdMs,
|
|
1838
|
+
label: agentId
|
|
1822
1839
|
});
|
|
1823
1840
|
}
|
|
1824
1841
|
);
|
|
@@ -1999,7 +2016,7 @@ var statsCommand = new Command3("stats").description("Display agent dashboard: t
|
|
|
1999
2016
|
});
|
|
2000
2017
|
|
|
2001
2018
|
// src/index.ts
|
|
2002
|
-
var program = new Command4().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.
|
|
2019
|
+
var program = new Command4().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.8.0");
|
|
2003
2020
|
program.addCommand(loginCommand);
|
|
2004
2021
|
program.addCommand(agentCommand);
|
|
2005
2022
|
program.addCommand(statsCommand);
|