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.
Files changed (2) hide show
  1. package/dist/index.js +62 -45
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -14,9 +14,10 @@ import * as os from "os";
14
14
  import { parse, stringify } from "yaml";
15
15
  var DEFAULT_PLATFORM_URL = "https://api.opencara.dev";
16
16
  var CONFIG_DIR = path.join(os.homedir(), ".opencara");
17
- var CONFIG_FILE = path.join(CONFIG_DIR, "config.yml");
17
+ var CONFIG_FILE = process.env.OPENCARA_CONFIG && process.env.OPENCARA_CONFIG.trim() ? path.resolve(process.env.OPENCARA_CONFIG) : path.join(CONFIG_DIR, "config.yml");
18
18
  function ensureConfigDir() {
19
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
19
+ const dir = path.dirname(CONFIG_FILE);
20
+ fs.mkdirSync(dir, { recursive: true });
20
21
  }
21
22
  var DEFAULT_MAX_DIFF_SIZE_KB = 100;
22
23
  function parseLimits(data) {
@@ -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((resolve) => setTimeout(resolve, ms));
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((resolve) => {
308
+ return new Promise((resolve2) => {
308
309
  rl.question(question, (answer) => {
309
310
  rl.close();
310
311
  const normalized = answer.trim().toLowerCase();
311
- resolve(normalized === "" || normalized === "y" || normalized === "yes");
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((resolve, reject) => {
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
- resolve({ stdout, stderr, tokensUsed: usage2.tokens, tokensParsed: usage2.parsed });
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
- resolve({ stdout, stderr, tokensUsed: usage.tokens, tokensParsed: usage.parsed });
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
- console.log("Disconnected.");
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
- console.log("No heartbeat received in 90s. Reconnecting...");
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
- console.log("Connected to platform.");
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
- console.log(`[verbose] Connection opened at ${new Date(connectionOpenedAt).toISOString()}`);
1023
+ log(`[verbose] Connection opened at ${new Date(connectionOpenedAt).toISOString()}`);
1020
1024
  }
1021
1025
  clearStabilityTimer();
1022
1026
  stabilityTimer = setTimeout(() => {
1023
1027
  if (verbose) {
1024
- console.log(
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
- console.log(
1063
+ log(
1059
1064
  `Disconnected (code=${code}, reason=${reason.toString()}). Connection was alive for ${lifetimeSec}s.`
1060
1065
  );
1061
1066
  } else {
1062
- console.log(`Disconnected (code=${code}, reason=${reason.toString()}).`);
1067
+ log(`Disconnected (code=${code}, reason=${reason.toString()}).`);
1063
1068
  }
1064
1069
  if (code === 4002) {
1065
- console.log("Connection replaced by server \u2014 not reconnecting.");
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
- console.log(`[verbose] WS pong received at ${(/* @__PURE__ */ new Date()).toISOString()}`);
1078
+ log(`[verbose] WS pong received at ${(/* @__PURE__ */ new Date()).toISOString()}`);
1074
1079
  }
1075
1080
  });
1076
1081
  ws.on("error", (err) => {
1077
- console.error(`WebSocket error: ${err.message}`);
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
- console.log(`Reconnecting in ${delaySec}s... (attempt ${attempt})`);
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(formatPostReviewStats(tokensUsed, consumptionDeps.session, consumptionDeps.limits));
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(`Authenticated. Protocol v${msg.version ?? "unknown"}`);
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(`[verbose] Heartbeat ping received, pong sent at ${(/* @__PURE__ */ new Date()).toISOString()}`);
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
- `Review request: task ${request.taskId} for ${request.project.owner}/${request.project.repo}#${request.pr.number}`
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(`Review rejected: ${limitResult.reason}`);
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("Review failed:", err);
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
- `Summary request: task ${summaryRequest.taskId} for ${summaryRequest.project.owner}/${summaryRequest.project.repo}#${summaryRequest.pr.number} (${summaryRequest.reviews.length} reviews)`
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(`Summary rejected: ${limitResult.reason}`);
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("Summary failed:", err);
1323
+ console.error(`${pfx}Summary failed:`, err);
1311
1324
  }
1312
1325
  })();
1313
1326
  break;
1314
1327
  }
1315
1328
  case "error":
1316
- console.error(`Platform error: ${msg.code ?? "unknown"}`);
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
- console.log(`Starting agent ${selected.local.model} (${agentId2})...`);
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
- displayName: selected.local.name,
1728
- repoConfig: selected.local.repos
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
- console.log(`Starting anonymous agent ${anon.model} (${anon.agentId})...`);
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.6.1");
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",