opencara 0.12.1 → 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 +104 -38
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -137,20 +137,20 @@ 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 };
@@ -158,7 +158,7 @@ function parseRepoConfig(obj, index) {
158
158
  if (mode === "whitelist" || mode === "blacklist") {
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
164
  }
@@ -166,7 +166,7 @@ function parseRepoConfig(obj, index) {
166
166
  for (let j = 0; j < list.length; j++) {
167
167
  if (typeof list[j] !== "string" || !REPO_PATTERN.test(list[j])) {
168
168
  throw new RepoConfigError(
169
- `agents[${index}].repos.list[${j}] must match 'owner/repo' format`
169
+ `agents[${index}].${field}.list[${j}] must match 'owner/repo' format`
170
170
  );
171
171
  }
172
172
  }
@@ -213,10 +213,18 @@ function parseAgents(data) {
213
213
  if (typeof obj.command === "string") agent.command = obj.command;
214
214
  if (obj.router === true) agent.router = true;
215
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
+ }
216
222
  if (typeof obj.github_token === "string") agent.github_token = obj.github_token;
217
223
  if (typeof obj.codebase_dir === "string") agent.codebase_dir = obj.codebase_dir;
218
224
  const repoConfig = parseRepoConfig(obj, i);
219
225
  if (repoConfig) agent.repos = repoConfig;
226
+ const synthesizeRepoConfig = parseRepoConfig(obj, i, "synthesize_repos");
227
+ if (synthesizeRepoConfig) agent.synthesize_repos = synthesizeRepoConfig;
220
228
  agents.push(agent);
221
229
  }
222
230
  return agents;
@@ -257,6 +265,7 @@ function loadConfig() {
257
265
  maxDiffSizeKb: DEFAULT_MAX_DIFF_SIZE_KB,
258
266
  maxConsecutiveErrors: DEFAULT_MAX_CONSECUTIVE_ERRORS,
259
267
  githubToken: null,
268
+ githubUsername: null,
260
269
  codebaseDir: null,
261
270
  agentCommand: null,
262
271
  agents: null
@@ -275,6 +284,7 @@ function loadConfig() {
275
284
  maxDiffSizeKb: overrides.maxDiffSizeKb ?? (typeof data.max_diff_size_kb === "number" ? data.max_diff_size_kb : DEFAULT_MAX_DIFF_SIZE_KB),
276
285
  maxConsecutiveErrors: overrides.maxConsecutiveErrors ?? (typeof data.max_consecutive_errors === "number" ? data.max_consecutive_errors : DEFAULT_MAX_CONSECUTIVE_ERRORS),
277
286
  githubToken: typeof data.github_token === "string" ? data.github_token : null,
287
+ githubUsername: typeof data.github_username === "string" ? data.github_username : null,
278
288
  codebaseDir: typeof data.codebase_dir === "string" ? data.codebase_dir : null,
279
289
  agentCommand: typeof data.agent_command === "string" ? data.agent_command : null,
280
290
  agents: parseAgents(data)
@@ -291,6 +301,22 @@ function resolveCodebaseDir(agentDir, globalDir) {
291
301
  }
292
302
  return path.resolve(raw);
293
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
+ }
294
320
 
295
321
  // src/codebase.ts
296
322
  import { execFileSync } from "child_process";
@@ -500,15 +526,15 @@ function sleep(ms, signal) {
500
526
  resolve2();
501
527
  return;
502
528
  }
503
- const timer = setTimeout(resolve2, ms);
504
- signal?.addEventListener(
505
- "abort",
506
- () => {
507
- clearTimeout(timer);
508
- resolve2();
509
- },
510
- { once: true }
511
- );
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 });
512
538
  });
513
539
  }
514
540
 
@@ -1313,6 +1339,11 @@ function toApiDiffUrl(webUrl) {
1313
1339
  const [, owner, repo, prNumber] = match;
1314
1340
  return `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`;
1315
1341
  }
1342
+ function computeRoles(agent) {
1343
+ if (agent.review_only) return ["review"];
1344
+ if (agent.synthesizer_only) return ["summary"];
1345
+ return ["review", "summary"];
1346
+ }
1316
1347
  async function fetchDiff(diffUrl, githubToken, signal) {
1317
1348
  return withRetry(
1318
1349
  async () => {
@@ -1346,7 +1377,17 @@ async function fetchDiff(diffUrl, githubToken, signal) {
1346
1377
  }
1347
1378
  var MAX_DIFF_FETCH_ATTEMPTS = 3;
1348
1379
  async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, options) {
1349
- 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;
1350
1391
  const { log, logError, logWarn } = logger;
1351
1392
  log(`${icons.polling} Polling every ${pollIntervalMs / 1e3}s...`);
1352
1393
  let consecutiveAuthErrors = 0;
@@ -1355,10 +1396,13 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1355
1396
  while (!signal?.aborted) {
1356
1397
  try {
1357
1398
  const pollBody = { agent_id: agentId };
1399
+ if (githubUsername) pollBody.github_username = githubUsername;
1400
+ if (roles) pollBody.roles = roles;
1358
1401
  if (reviewOnly) pollBody.review_only = true;
1359
1402
  if (repoConfig?.list?.length) {
1360
1403
  pollBody.repos = repoConfig.list;
1361
1404
  }
1405
+ if (synthesizeRepos) pollBody.synthesize_repos = synthesizeRepos;
1362
1406
  const pollResponse = await client.post("/api/tasks/poll", pollBody);
1363
1407
  consecutiveAuthErrors = 0;
1364
1408
  consecutiveErrors = 0;
@@ -1377,7 +1421,8 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1377
1421
  logger,
1378
1422
  agentSession,
1379
1423
  routerRelay,
1380
- signal
1424
+ signal,
1425
+ githubUsername
1381
1426
  );
1382
1427
  if (result.diffFetchFailed) {
1383
1428
  agentSession.errorsEncountered++;
@@ -1430,20 +1475,22 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1430
1475
  await sleep2(pollIntervalMs, signal);
1431
1476
  }
1432
1477
  }
1433
- 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) {
1434
1479
  const { task_id, owner, repo, pr_number, diff_url, timeout_seconds, prompt, role } = task;
1435
1480
  const { log, logError, logWarn } = logger;
1436
1481
  log(`${icons.success} Claimed task ${task_id} (${role}) \u2014 ${owner}/${repo}#${pr_number}`);
1437
1482
  log(` https://github.com/${owner}/${repo}/pull/${pr_number}`);
1438
1483
  let claimResponse;
1439
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;
1440
1492
  claimResponse = await withRetry(
1441
- () => client.post(`/api/tasks/${task_id}/claim`, {
1442
- agent_id: agentId,
1443
- role,
1444
- model: agentInfo.model,
1445
- tool: agentInfo.tool
1446
- }),
1493
+ () => client.post(`/api/tasks/${task_id}/claim`, claimBody),
1447
1494
  { maxAttempts: 2 },
1448
1495
  signal
1449
1496
  );
@@ -1800,15 +1847,15 @@ function sleep2(ms, signal) {
1800
1847
  resolve2();
1801
1848
  return;
1802
1849
  }
1803
- const timer = setTimeout(resolve2, ms);
1804
- signal?.addEventListener(
1805
- "abort",
1806
- () => {
1807
- clearTimeout(timer);
1808
- resolve2();
1809
- },
1810
- { once: true }
1811
- );
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 });
1812
1859
  });
1813
1860
  }
1814
1861
  async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
@@ -1846,6 +1893,9 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
1846
1893
  routerRelay: options?.routerRelay,
1847
1894
  reviewOnly: options?.reviewOnly,
1848
1895
  repoConfig: options?.repoConfig,
1896
+ roles: options?.roles,
1897
+ synthesizeRepos: options?.synthesizeRepos,
1898
+ githubUsername: options?.githubUsername,
1849
1899
  signal: abortController.signal
1850
1900
  });
1851
1901
  log(formatExitSummary(agentSession));
@@ -1867,6 +1917,10 @@ async function startAgentRouter() {
1867
1917
  const auth = resolveGithubToken2(configToken);
1868
1918
  const logger = createLogger(agentConfig?.name ?? "agent[0]");
1869
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
+ }
1870
1924
  const codebaseDir = resolveCodebaseDir(agentConfig?.codebase_dir, config.codebaseDir);
1871
1925
  const reviewDeps = {
1872
1926
  commandTemplate: commandTemplate ?? "",
@@ -1878,6 +1932,7 @@ async function startAgentRouter() {
1878
1932
  const model = agentConfig?.model ?? "unknown";
1879
1933
  const tool = agentConfig?.tool ?? "unknown";
1880
1934
  const label = agentConfig?.name ?? "agent[0]";
1935
+ const roles = agentConfig ? computeRoles(agentConfig) : void 0;
1881
1936
  await startAgent(
1882
1937
  agentId,
1883
1938
  config.platformUrl,
@@ -1892,12 +1947,15 @@ async function startAgentRouter() {
1892
1947
  routerRelay: router,
1893
1948
  reviewOnly: agentConfig?.review_only,
1894
1949
  repoConfig: agentConfig?.repos,
1950
+ roles,
1951
+ synthesizeRepos: agentConfig?.synthesize_repos,
1952
+ githubUsername,
1895
1953
  label
1896
1954
  }
1897
1955
  );
1898
1956
  router.stop();
1899
1957
  }
1900
- function startAgentByIndex(config, agentIndex, pollIntervalMs, auth) {
1958
+ function startAgentByIndex(config, agentIndex, pollIntervalMs, auth, githubUsername) {
1901
1959
  const agentId = crypto.randomUUID();
1902
1960
  let commandTemplate;
1903
1961
  let agentConfig;
@@ -1941,6 +1999,7 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, auth) {
1941
1999
  const session = createSessionTracker();
1942
2000
  const model = agentConfig?.model ?? "unknown";
1943
2001
  const tool = agentConfig?.tool ?? "unknown";
2002
+ const roles = agentConfig ? computeRoles(agentConfig) : void 0;
1944
2003
  const agentPromise = startAgent(
1945
2004
  agentId,
1946
2005
  config.platformUrl,
@@ -1953,6 +2012,9 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, auth) {
1953
2012
  routerRelay,
1954
2013
  reviewOnly: agentConfig?.review_only,
1955
2014
  repoConfig: agentConfig?.repos,
2015
+ roles,
2016
+ synthesizeRepos: agentConfig?.synthesize_repos,
2017
+ githubUsername,
1956
2018
  label
1957
2019
  }
1958
2020
  ).finally(() => {
@@ -1967,6 +2029,10 @@ agentCommand.command("start").description("Start agents in polling mode").option
1967
2029
  const configToken = resolveGithubToken(void 0, config.githubToken);
1968
2030
  const auth = resolveGithubToken2(configToken);
1969
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
+ }
1970
2036
  if (opts.all) {
1971
2037
  if (!config.agents || config.agents.length === 0) {
1972
2038
  console.error("No agents configured in ~/.opencara/config.yml");
@@ -1977,7 +2043,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
1977
2043
  const promises = [];
1978
2044
  let startFailed = false;
1979
2045
  for (let i = 0; i < config.agents.length; i++) {
1980
- const p = startAgentByIndex(config, i, pollIntervalMs, auth);
2046
+ const p = startAgentByIndex(config, i, pollIntervalMs, auth, githubUsername);
1981
2047
  if (p) {
1982
2048
  promises.push(p);
1983
2049
  } else {
@@ -2014,7 +2080,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
2014
2080
  process.exit(1);
2015
2081
  return;
2016
2082
  }
2017
- const p = startAgentByIndex(config, agentIndex, pollIntervalMs, auth);
2083
+ const p = startAgentByIndex(config, agentIndex, pollIntervalMs, auth, githubUsername);
2018
2084
  if (!p) {
2019
2085
  process.exit(1);
2020
2086
  return;
@@ -2024,7 +2090,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
2024
2090
  });
2025
2091
 
2026
2092
  // src/index.ts
2027
- var program = new Command2().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.12.1");
2093
+ var program = new Command2().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.13.0");
2028
2094
  program.addCommand(agentCommand);
2029
2095
  program.action(() => {
2030
2096
  startAgentRouter();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.12.1",
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",