opencara 0.12.0 → 0.13.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 +108 -40
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -137,34 +137,36 @@ var KNOWN_TOOL_NAMES = new Set(DEFAULT_REGISTRY.tools.map((t) => t.name));
137
137
  var TOOL_ALIASES = {
138
138
  "claude-code": "claude"
139
139
  };
140
- function parseRepoConfig(obj, index) {
141
- const raw = obj.repos;
140
+ function parseRepoConfig(obj, index, field = "repos") {
141
+ const raw = obj[field];
142
142
  if (raw === void 0 || raw === null) return void 0;
143
143
  if (typeof raw !== "object") {
144
- throw new RepoConfigError(`agents[${index}].repos must be an object`);
144
+ throw new RepoConfigError(`agents[${index}].${field} must be an object`);
145
145
  }
146
146
  const reposObj = raw;
147
147
  const mode = reposObj.mode;
148
148
  if (mode === void 0) {
149
- throw new RepoConfigError(`agents[${index}].repos.mode is required`);
149
+ throw new RepoConfigError(`agents[${index}].${field}.mode is required`);
150
150
  }
151
151
  if (typeof mode !== "string" || !VALID_REPO_MODES.includes(mode)) {
152
152
  throw new RepoConfigError(
153
- `agents[${index}].repos.mode must be one of: ${VALID_REPO_MODES.join(", ")}`
153
+ `agents[${index}].${field}.mode must be one of: ${VALID_REPO_MODES.join(", ")}`
154
154
  );
155
155
  }
156
156
  const config = { mode };
157
+ const list = reposObj.list;
157
158
  if (mode === "whitelist" || mode === "blacklist") {
158
- const list = reposObj.list;
159
159
  if (!Array.isArray(list) || list.length === 0) {
160
160
  throw new RepoConfigError(
161
- `agents[${index}].repos.list is required and must be non-empty for mode '${mode}'`
161
+ `agents[${index}].${field}.list is required and must be non-empty for mode '${mode}'`
162
162
  );
163
163
  }
164
+ }
165
+ if (Array.isArray(list) && list.length > 0) {
164
166
  for (let j = 0; j < list.length; j++) {
165
167
  if (typeof list[j] !== "string" || !REPO_PATTERN.test(list[j])) {
166
168
  throw new RepoConfigError(
167
- `agents[${index}].repos.list[${j}] must match 'owner/repo' format`
169
+ `agents[${index}].${field}.list[${j}] must match 'owner/repo' format`
168
170
  );
169
171
  }
170
172
  }
@@ -211,10 +213,18 @@ function parseAgents(data) {
211
213
  if (typeof obj.command === "string") agent.command = obj.command;
212
214
  if (obj.router === true) agent.router = true;
213
215
  if (obj.review_only === true) agent.review_only = true;
216
+ if (obj.synthesizer_only === true) agent.synthesizer_only = true;
217
+ if (agent.review_only && agent.synthesizer_only) {
218
+ throw new ConfigValidationError(
219
+ `agents[${i}]: review_only and synthesizer_only cannot both be true`
220
+ );
221
+ }
214
222
  if (typeof obj.github_token === "string") agent.github_token = obj.github_token;
215
223
  if (typeof obj.codebase_dir === "string") agent.codebase_dir = obj.codebase_dir;
216
224
  const repoConfig = parseRepoConfig(obj, i);
217
225
  if (repoConfig) agent.repos = repoConfig;
226
+ const synthesizeRepoConfig = parseRepoConfig(obj, i, "synthesize_repos");
227
+ if (synthesizeRepoConfig) agent.synthesize_repos = synthesizeRepoConfig;
218
228
  agents.push(agent);
219
229
  }
220
230
  return agents;
@@ -255,6 +265,7 @@ function loadConfig() {
255
265
  maxDiffSizeKb: DEFAULT_MAX_DIFF_SIZE_KB,
256
266
  maxConsecutiveErrors: DEFAULT_MAX_CONSECUTIVE_ERRORS,
257
267
  githubToken: null,
268
+ githubUsername: null,
258
269
  codebaseDir: null,
259
270
  agentCommand: null,
260
271
  agents: null
@@ -273,6 +284,7 @@ function loadConfig() {
273
284
  maxDiffSizeKb: overrides.maxDiffSizeKb ?? (typeof data.max_diff_size_kb === "number" ? data.max_diff_size_kb : DEFAULT_MAX_DIFF_SIZE_KB),
274
285
  maxConsecutiveErrors: overrides.maxConsecutiveErrors ?? (typeof data.max_consecutive_errors === "number" ? data.max_consecutive_errors : DEFAULT_MAX_CONSECUTIVE_ERRORS),
275
286
  githubToken: typeof data.github_token === "string" ? data.github_token : null,
287
+ githubUsername: typeof data.github_username === "string" ? data.github_username : null,
276
288
  codebaseDir: typeof data.codebase_dir === "string" ? data.codebase_dir : null,
277
289
  agentCommand: typeof data.agent_command === "string" ? data.agent_command : null,
278
290
  agents: parseAgents(data)
@@ -289,6 +301,22 @@ function resolveCodebaseDir(agentDir, globalDir) {
289
301
  }
290
302
  return path.resolve(raw);
291
303
  }
304
+ async function resolveGithubUsername(githubToken, fetchFn = fetch) {
305
+ if (!githubToken) return null;
306
+ try {
307
+ const response = await fetchFn("https://api.github.com/user", {
308
+ headers: {
309
+ Authorization: `Bearer ${githubToken}`,
310
+ Accept: "application/vnd.github+json"
311
+ }
312
+ });
313
+ if (!response.ok) return null;
314
+ const data = await response.json();
315
+ return typeof data.login === "string" ? data.login : null;
316
+ } catch {
317
+ return null;
318
+ }
319
+ }
292
320
 
293
321
  // src/codebase.ts
294
322
  import { execFileSync } from "child_process";
@@ -498,15 +526,15 @@ function sleep(ms, signal) {
498
526
  resolve2();
499
527
  return;
500
528
  }
501
- const timer = setTimeout(resolve2, ms);
502
- signal?.addEventListener(
503
- "abort",
504
- () => {
505
- clearTimeout(timer);
506
- resolve2();
507
- },
508
- { once: true }
509
- );
529
+ const onAbort = () => {
530
+ clearTimeout(timer);
531
+ resolve2();
532
+ };
533
+ const timer = setTimeout(() => {
534
+ signal?.removeEventListener("abort", onAbort);
535
+ resolve2();
536
+ }, ms);
537
+ signal?.addEventListener("abort", onAbort, { once: true });
510
538
  });
511
539
  }
512
540
 
@@ -1311,6 +1339,11 @@ function toApiDiffUrl(webUrl) {
1311
1339
  const [, owner, repo, prNumber] = match;
1312
1340
  return `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`;
1313
1341
  }
1342
+ function computeRoles(agent) {
1343
+ if (agent.review_only) return ["review"];
1344
+ if (agent.synthesizer_only) return ["summary"];
1345
+ return ["review", "summary"];
1346
+ }
1314
1347
  async function fetchDiff(diffUrl, githubToken, signal) {
1315
1348
  return withRetry(
1316
1349
  async () => {
@@ -1344,7 +1377,17 @@ async function fetchDiff(diffUrl, githubToken, signal) {
1344
1377
  }
1345
1378
  var MAX_DIFF_FETCH_ATTEMPTS = 3;
1346
1379
  async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, options) {
1347
- const { pollIntervalMs, maxConsecutiveErrors, routerRelay, reviewOnly, repoConfig, signal } = options;
1380
+ const {
1381
+ pollIntervalMs,
1382
+ maxConsecutiveErrors,
1383
+ routerRelay,
1384
+ reviewOnly,
1385
+ repoConfig,
1386
+ roles,
1387
+ synthesizeRepos,
1388
+ githubUsername,
1389
+ signal
1390
+ } = options;
1348
1391
  const { log, logError, logWarn } = logger;
1349
1392
  log(`${icons.polling} Polling every ${pollIntervalMs / 1e3}s...`);
1350
1393
  let consecutiveAuthErrors = 0;
@@ -1353,10 +1396,13 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1353
1396
  while (!signal?.aborted) {
1354
1397
  try {
1355
1398
  const pollBody = { agent_id: agentId };
1399
+ if (githubUsername) pollBody.github_username = githubUsername;
1400
+ if (roles) pollBody.roles = roles;
1356
1401
  if (reviewOnly) pollBody.review_only = true;
1357
- if (repoConfig?.mode === "whitelist" && repoConfig.list?.length) {
1402
+ if (repoConfig?.list?.length) {
1358
1403
  pollBody.repos = repoConfig.list;
1359
1404
  }
1405
+ if (synthesizeRepos) pollBody.synthesize_repos = synthesizeRepos;
1360
1406
  const pollResponse = await client.post("/api/tasks/poll", pollBody);
1361
1407
  consecutiveAuthErrors = 0;
1362
1408
  consecutiveErrors = 0;
@@ -1375,7 +1421,8 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1375
1421
  logger,
1376
1422
  agentSession,
1377
1423
  routerRelay,
1378
- signal
1424
+ signal,
1425
+ githubUsername
1379
1426
  );
1380
1427
  if (result.diffFetchFailed) {
1381
1428
  agentSession.errorsEncountered++;
@@ -1428,20 +1475,22 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1428
1475
  await sleep2(pollIntervalMs, signal);
1429
1476
  }
1430
1477
  }
1431
- async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, routerRelay, signal) {
1478
+ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, routerRelay, signal, githubUsername) {
1432
1479
  const { task_id, owner, repo, pr_number, diff_url, timeout_seconds, prompt, role } = task;
1433
1480
  const { log, logError, logWarn } = logger;
1434
1481
  log(`${icons.success} Claimed task ${task_id} (${role}) \u2014 ${owner}/${repo}#${pr_number}`);
1435
1482
  log(` https://github.com/${owner}/${repo}/pull/${pr_number}`);
1436
1483
  let claimResponse;
1437
1484
  try {
1485
+ const claimBody = {
1486
+ agent_id: agentId,
1487
+ role,
1488
+ model: agentInfo.model,
1489
+ tool: agentInfo.tool
1490
+ };
1491
+ if (githubUsername) claimBody.github_username = githubUsername;
1438
1492
  claimResponse = await withRetry(
1439
- () => client.post(`/api/tasks/${task_id}/claim`, {
1440
- agent_id: agentId,
1441
- role,
1442
- model: agentInfo.model,
1443
- tool: agentInfo.tool
1444
- }),
1493
+ () => client.post(`/api/tasks/${task_id}/claim`, claimBody),
1445
1494
  { maxAttempts: 2 },
1446
1495
  signal
1447
1496
  );
@@ -1798,15 +1847,15 @@ function sleep2(ms, signal) {
1798
1847
  resolve2();
1799
1848
  return;
1800
1849
  }
1801
- const timer = setTimeout(resolve2, ms);
1802
- signal?.addEventListener(
1803
- "abort",
1804
- () => {
1805
- clearTimeout(timer);
1806
- resolve2();
1807
- },
1808
- { once: true }
1809
- );
1850
+ const onAbort = () => {
1851
+ clearTimeout(timer);
1852
+ resolve2();
1853
+ };
1854
+ const timer = setTimeout(() => {
1855
+ signal?.removeEventListener("abort", onAbort);
1856
+ resolve2();
1857
+ }, ms);
1858
+ signal?.addEventListener("abort", onAbort, { once: true });
1810
1859
  });
1811
1860
  }
1812
1861
  async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
@@ -1844,6 +1893,9 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
1844
1893
  routerRelay: options?.routerRelay,
1845
1894
  reviewOnly: options?.reviewOnly,
1846
1895
  repoConfig: options?.repoConfig,
1896
+ roles: options?.roles,
1897
+ synthesizeRepos: options?.synthesizeRepos,
1898
+ githubUsername: options?.githubUsername,
1847
1899
  signal: abortController.signal
1848
1900
  });
1849
1901
  log(formatExitSummary(agentSession));
@@ -1865,6 +1917,10 @@ async function startAgentRouter() {
1865
1917
  const auth = resolveGithubToken2(configToken);
1866
1918
  const logger = createLogger(agentConfig?.name ?? "agent[0]");
1867
1919
  logAuthMethod(auth.method, logger.log);
1920
+ const githubUsername = config.githubUsername ?? await resolveGithubUsername(auth.token) ?? void 0;
1921
+ if (githubUsername) {
1922
+ logger.log(`GitHub identity: ${githubUsername}`);
1923
+ }
1868
1924
  const codebaseDir = resolveCodebaseDir(agentConfig?.codebase_dir, config.codebaseDir);
1869
1925
  const reviewDeps = {
1870
1926
  commandTemplate: commandTemplate ?? "",
@@ -1876,6 +1932,7 @@ async function startAgentRouter() {
1876
1932
  const model = agentConfig?.model ?? "unknown";
1877
1933
  const tool = agentConfig?.tool ?? "unknown";
1878
1934
  const label = agentConfig?.name ?? "agent[0]";
1935
+ const roles = agentConfig ? computeRoles(agentConfig) : void 0;
1879
1936
  await startAgent(
1880
1937
  agentId,
1881
1938
  config.platformUrl,
@@ -1890,12 +1947,15 @@ async function startAgentRouter() {
1890
1947
  routerRelay: router,
1891
1948
  reviewOnly: agentConfig?.review_only,
1892
1949
  repoConfig: agentConfig?.repos,
1950
+ roles,
1951
+ synthesizeRepos: agentConfig?.synthesize_repos,
1952
+ githubUsername,
1893
1953
  label
1894
1954
  }
1895
1955
  );
1896
1956
  router.stop();
1897
1957
  }
1898
- function startAgentByIndex(config, agentIndex, pollIntervalMs, auth) {
1958
+ function startAgentByIndex(config, agentIndex, pollIntervalMs, auth, githubUsername) {
1899
1959
  const agentId = crypto.randomUUID();
1900
1960
  let commandTemplate;
1901
1961
  let agentConfig;
@@ -1939,6 +1999,7 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, auth) {
1939
1999
  const session = createSessionTracker();
1940
2000
  const model = agentConfig?.model ?? "unknown";
1941
2001
  const tool = agentConfig?.tool ?? "unknown";
2002
+ const roles = agentConfig ? computeRoles(agentConfig) : void 0;
1942
2003
  const agentPromise = startAgent(
1943
2004
  agentId,
1944
2005
  config.platformUrl,
@@ -1951,6 +2012,9 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, auth) {
1951
2012
  routerRelay,
1952
2013
  reviewOnly: agentConfig?.review_only,
1953
2014
  repoConfig: agentConfig?.repos,
2015
+ roles,
2016
+ synthesizeRepos: agentConfig?.synthesize_repos,
2017
+ githubUsername,
1954
2018
  label
1955
2019
  }
1956
2020
  ).finally(() => {
@@ -1965,6 +2029,10 @@ agentCommand.command("start").description("Start agents in polling mode").option
1965
2029
  const configToken = resolveGithubToken(void 0, config.githubToken);
1966
2030
  const auth = resolveGithubToken2(configToken);
1967
2031
  logAuthMethod(auth.method, console.log.bind(console));
2032
+ const githubUsername = config.githubUsername ?? await resolveGithubUsername(auth.token) ?? void 0;
2033
+ if (githubUsername) {
2034
+ console.log(`GitHub identity: ${githubUsername}`);
2035
+ }
1968
2036
  if (opts.all) {
1969
2037
  if (!config.agents || config.agents.length === 0) {
1970
2038
  console.error("No agents configured in ~/.opencara/config.yml");
@@ -1975,7 +2043,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
1975
2043
  const promises = [];
1976
2044
  let startFailed = false;
1977
2045
  for (let i = 0; i < config.agents.length; i++) {
1978
- const p = startAgentByIndex(config, i, pollIntervalMs, auth);
2046
+ const p = startAgentByIndex(config, i, pollIntervalMs, auth, githubUsername);
1979
2047
  if (p) {
1980
2048
  promises.push(p);
1981
2049
  } else {
@@ -2012,7 +2080,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
2012
2080
  process.exit(1);
2013
2081
  return;
2014
2082
  }
2015
- const p = startAgentByIndex(config, agentIndex, pollIntervalMs, auth);
2083
+ const p = startAgentByIndex(config, agentIndex, pollIntervalMs, auth, githubUsername);
2016
2084
  if (!p) {
2017
2085
  process.exit(1);
2018
2086
  return;
@@ -2022,7 +2090,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
2022
2090
  });
2023
2091
 
2024
2092
  // src/index.ts
2025
- var program = new Command2().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.12.0");
2093
+ var program = new Command2().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.13.0");
2026
2094
  program.addCommand(agentCommand);
2027
2095
  program.action(() => {
2028
2096
  startAgentRouter();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Distributed AI code review agent — poll, review, and submit PR reviews using your own AI tools",
5
5
  "type": "module",
6
6
  "license": "MIT",