opencara 0.18.5 → 0.18.7

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 +127 -16
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16,15 +16,24 @@ function isDedupRole(role) {
16
16
  function isTriageRole(role) {
17
17
  return role === "pr_triage" || role === "issue_triage";
18
18
  }
19
- function isRepoAllowed(repoConfig, targetOwner, targetRepo, agentOwner) {
19
+ function isRepoAllowed(repoConfig, targetOwner, targetRepo, agentOwner, userOrgs) {
20
20
  if (!repoConfig)
21
21
  return true;
22
22
  const fullRepo = `${targetOwner}/${targetRepo}`;
23
23
  switch (repoConfig.mode) {
24
24
  case "public":
25
25
  return true;
26
- case "private":
27
- return agentOwner === targetOwner;
26
+ case "private": {
27
+ const normalizedTarget = targetOwner.toLowerCase();
28
+ const normalizedOwner = agentOwner?.toLowerCase();
29
+ const hasAccess = normalizedOwner === normalizedTarget || userOrgs != null && userOrgs.has(normalizedTarget);
30
+ if (!hasAccess)
31
+ return false;
32
+ if (repoConfig.list && repoConfig.list.length > 0) {
33
+ return repoConfig.list.includes(fullRepo);
34
+ }
35
+ return true;
36
+ }
28
37
  case "whitelist":
29
38
  return (repoConfig.list ?? []).includes(fullRepo);
30
39
  case "blacklist":
@@ -978,6 +987,7 @@ import * as fs5 from "fs";
978
987
  import * as path5 from "path";
979
988
  import * as os2 from "os";
980
989
  import * as crypto from "crypto";
990
+ import { execFileSync as execFileSync3 } from "child_process";
981
991
  var AUTH_DIR = path5.join(os2.homedir(), ".opencara");
982
992
  function getAuthFilePath() {
983
993
  const envPath = process.env.OPENCARA_AUTH_FILE?.trim();
@@ -1189,6 +1199,59 @@ async function resolveUser(token, fetchFn = fetch) {
1189
1199
  }
1190
1200
  return { login: data.login, id: data.id };
1191
1201
  }
1202
+ async function fetchUserOrgs(token, fetchFn = fetch, expectedLogin) {
1203
+ const ghOrgs = fetchUserOrgsViaGh(expectedLogin);
1204
+ if (ghOrgs.size > 0) return ghOrgs;
1205
+ try {
1206
+ const res = await fetchFn("https://api.github.com/user/orgs?per_page=100", {
1207
+ headers: {
1208
+ Authorization: `Bearer ${token}`,
1209
+ Accept: "application/vnd.github+json",
1210
+ "X-GitHub-Api-Version": "2022-11-28"
1211
+ }
1212
+ });
1213
+ if (!res.ok) {
1214
+ return /* @__PURE__ */ new Set();
1215
+ }
1216
+ const data = await res.json();
1217
+ const orgs = /* @__PURE__ */ new Set();
1218
+ for (const org of data) {
1219
+ if (typeof org.login === "string") {
1220
+ orgs.add(org.login.toLowerCase());
1221
+ }
1222
+ }
1223
+ return orgs;
1224
+ } catch {
1225
+ return /* @__PURE__ */ new Set();
1226
+ }
1227
+ }
1228
+ function fetchUserOrgsViaGh(expectedLogin) {
1229
+ try {
1230
+ if (expectedLogin) {
1231
+ const ghUser = execFileSync3("gh", ["api", "/user", "--jq", ".login"], {
1232
+ encoding: "utf-8",
1233
+ timeout: 1e4,
1234
+ stdio: ["ignore", "pipe", "pipe"]
1235
+ }).trim();
1236
+ if (ghUser.toLowerCase() !== expectedLogin.toLowerCase()) {
1237
+ return /* @__PURE__ */ new Set();
1238
+ }
1239
+ }
1240
+ const output = execFileSync3("gh", ["api", "/user/orgs", "--paginate", "--jq", ".[].login"], {
1241
+ encoding: "utf-8",
1242
+ timeout: 15e3,
1243
+ stdio: ["ignore", "pipe", "pipe"]
1244
+ });
1245
+ const orgs = /* @__PURE__ */ new Set();
1246
+ for (const line of output.trim().split("\n")) {
1247
+ const name = line.trim();
1248
+ if (name) orgs.add(name.toLowerCase());
1249
+ }
1250
+ return orgs;
1251
+ } catch {
1252
+ return /* @__PURE__ */ new Set();
1253
+ }
1254
+ }
1192
1255
 
1193
1256
  // src/http.ts
1194
1257
  var HttpError = class extends Error {
@@ -1396,7 +1459,7 @@ function sleep(ms, signal) {
1396
1459
  }
1397
1460
 
1398
1461
  // src/tool-executor.ts
1399
- import { spawn, execFileSync as execFileSync3 } from "child_process";
1462
+ import { spawn, execFileSync as execFileSync4 } from "child_process";
1400
1463
  import * as fs6 from "fs";
1401
1464
  import * as path6 from "path";
1402
1465
  var ToolTimeoutError = class extends Error {
@@ -1421,9 +1484,9 @@ function validateCommandBinary(commandTemplate) {
1421
1484
  try {
1422
1485
  const isWindows = process.platform === "win32";
1423
1486
  if (isWindows) {
1424
- execFileSync3("where", [command], { stdio: "pipe" });
1487
+ execFileSync4("where", [command], { stdio: "pipe" });
1425
1488
  } else {
1426
- execFileSync3("sh", ["-c", 'command -v -- "$1"', "_", command], { stdio: "pipe" });
1489
+ execFileSync4("sh", ["-c", 'command -v -- "$1"', "_", command], { stdio: "pipe" });
1427
1490
  }
1428
1491
  return true;
1429
1492
  } catch {
@@ -3207,7 +3270,9 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
3207
3270
  synthesizeRepos,
3208
3271
  signal,
3209
3272
  cleanupTracker,
3210
- verbose
3273
+ verbose,
3274
+ agentOwner,
3275
+ userOrgs
3211
3276
  } = options;
3212
3277
  const { log, logError, logWarn } = logger;
3213
3278
  log(`${icons.polling} Polling every ${pollIntervalMs / 1e3}s...`);
@@ -3239,7 +3304,9 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
3239
3304
  const pollResponse = await client.post("/api/tasks/poll", pollBody);
3240
3305
  consecutiveAuthErrors = 0;
3241
3306
  consecutiveErrors = 0;
3242
- const eligibleTasks = repoConfig ? pollResponse.tasks.filter((t) => isRepoAllowed(repoConfig, t.owner, t.repo)) : pollResponse.tasks;
3307
+ const eligibleTasks = repoConfig ? pollResponse.tasks.filter(
3308
+ (t) => isRepoAllowed(repoConfig, t.owner, t.repo, agentOwner, userOrgs)
3309
+ ) : pollResponse.tasks;
3243
3310
  const task = eligibleTasks.find(
3244
3311
  (t) => (diffFailCounts.get(t.task_id) ?? 0) < MAX_DIFF_FETCH_ATTEMPTS
3245
3312
  );
@@ -3904,7 +3971,7 @@ function sleep2(ms, signal) {
3904
3971
  async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
3905
3972
  const client = new ApiClient(platformUrl, {
3906
3973
  authToken: options?.authToken,
3907
- cliVersion: "0.18.5",
3974
+ cliVersion: "0.18.7",
3908
3975
  versionOverride: options?.versionOverride,
3909
3976
  onTokenRefresh: options?.onTokenRefresh
3910
3977
  });
@@ -3972,7 +4039,9 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
3972
4039
  synthesizeRepos: options?.synthesizeRepos,
3973
4040
  signal: abortController.signal,
3974
4041
  cleanupTracker,
3975
- verbose: options?.verbose
4042
+ verbose: options?.verbose,
4043
+ agentOwner: options?.agentOwner,
4044
+ userOrgs: options?.userOrgs
3976
4045
  });
3977
4046
  if (cleanupTracker && cleanupTracker.size > 0) {
3978
4047
  const finalSwept = await cleanupTracker.sweep(cleanupWorktree);
@@ -4014,9 +4083,12 @@ async function startAgentRouter() {
4014
4083
  throw err;
4015
4084
  }
4016
4085
  const storedAuth = loadAuth();
4086
+ const agentOwner = storedAuth?.github_username;
4017
4087
  if (storedAuth) {
4018
4088
  logger.log(`Authenticated as ${storedAuth.github_username}`);
4019
4089
  }
4090
+ const repoConfig = agentConfig?.repos;
4091
+ const userOrgs = repoConfig?.mode === "private" ? await fetchUserOrgs(oauthToken) : /* @__PURE__ */ new Set();
4020
4092
  const codebaseDir = resolveCodebaseDir(agentConfig?.codebase_dir, config.codebaseDir);
4021
4093
  const reviewDeps = {
4022
4094
  commandTemplate: commandTemplate ?? "",
@@ -4046,12 +4118,14 @@ async function startAgentRouter() {
4046
4118
  maxConsecutiveErrors: config.maxConsecutiveErrors,
4047
4119
  routerRelay: router,
4048
4120
  reviewOnly: agentConfig?.review_only,
4049
- repoConfig: agentConfig?.repos,
4121
+ repoConfig,
4050
4122
  roles,
4051
4123
  synthesizeRepos: agentConfig?.synthesize_repos,
4052
4124
  label,
4053
4125
  authToken: oauthToken,
4054
4126
  onTokenRefresh: () => getValidToken(config.platformUrl),
4127
+ agentOwner,
4128
+ userOrgs,
4055
4129
  usageLimits: config.usageLimits,
4056
4130
  versionOverride,
4057
4131
  codebaseTtl: config.codebaseTtl
@@ -4059,7 +4133,7 @@ async function startAgentRouter() {
4059
4133
  );
4060
4134
  router.stop();
4061
4135
  }
4062
- function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versionOverride, verbose, instancesOverride) {
4136
+ function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versionOverride, verbose, instancesOverride, agentOwner, userOrgs) {
4063
4137
  let commandTemplate;
4064
4138
  let agentConfig;
4065
4139
  if (config.agents && config.agents.length > agentIndex) {
@@ -4122,7 +4196,9 @@ function startAgentByIndex(config, agentIndex, pollIntervalMs, oauthToken, versi
4122
4196
  usageLimits: config.usageLimits,
4123
4197
  versionOverride,
4124
4198
  codebaseTtl: config.codebaseTtl,
4125
- verbose
4199
+ verbose,
4200
+ agentOwner,
4201
+ userOrgs
4126
4202
  }
4127
4203
  ).finally(() => {
4128
4204
  routerRelay?.stop();
@@ -4161,9 +4237,40 @@ agentCommand.command("start").description("Start agents in polling mode").option
4161
4237
  throw err;
4162
4238
  }
4163
4239
  const storedAuth = loadAuth();
4240
+ const agentOwner = storedAuth?.github_username;
4164
4241
  if (storedAuth) {
4165
4242
  console.log(`Authenticated as ${storedAuth.github_username}`);
4166
4243
  }
4244
+ const needsOrgs = config.agents?.some((a) => a.repos?.mode === "private") ?? false;
4245
+ let userOrgs = needsOrgs ? await fetchUserOrgs(oauthToken, fetch, agentOwner) : /* @__PURE__ */ new Set();
4246
+ if (needsOrgs && userOrgs.size === 0 && config.agents) {
4247
+ const currentLogin = agentOwner?.toLowerCase();
4248
+ const fallbackOrgs = /* @__PURE__ */ new Set();
4249
+ for (const a of config.agents) {
4250
+ if (a.repos?.list) {
4251
+ for (const repo of a.repos.list) {
4252
+ const owner = repo.split("/")[0]?.toLowerCase();
4253
+ if (owner && owner !== currentLogin) fallbackOrgs.add(owner);
4254
+ }
4255
+ }
4256
+ if (a.synthesize_repos?.list) {
4257
+ for (const repo of a.synthesize_repos.list) {
4258
+ const owner = repo.split("/")[0]?.toLowerCase();
4259
+ if (owner && owner !== currentLogin) fallbackOrgs.add(owner);
4260
+ }
4261
+ }
4262
+ }
4263
+ if (fallbackOrgs.size > 0) {
4264
+ userOrgs = fallbackOrgs;
4265
+ console.log(`Org memberships (from config): ${[...userOrgs].join(", ")}`);
4266
+ } else {
4267
+ console.warn(
4268
+ "\u26A0 Failed to fetch org memberships \u2014 private mode agents may not see org repos"
4269
+ );
4270
+ }
4271
+ } else if (needsOrgs && userOrgs.size > 0) {
4272
+ console.log(`Org memberships: ${[...userOrgs].join(", ")}`);
4273
+ }
4167
4274
  if (opts.all) {
4168
4275
  if (!config.agents || config.agents.length === 0) {
4169
4276
  console.error("No agents configured in ~/.opencara/config.toml");
@@ -4181,7 +4288,9 @@ agentCommand.command("start").description("Start agents in polling mode").option
4181
4288
  oauthToken,
4182
4289
  versionOverride,
4183
4290
  opts.verbose,
4184
- instancesOverride
4291
+ instancesOverride,
4292
+ agentOwner,
4293
+ userOrgs
4185
4294
  );
4186
4295
  if (agentPromises) {
4187
4296
  promises.push(...agentPromises);
@@ -4226,7 +4335,9 @@ agentCommand.command("start").description("Start agents in polling mode").option
4226
4335
  oauthToken,
4227
4336
  versionOverride,
4228
4337
  opts.verbose,
4229
- instancesOverride
4338
+ instancesOverride,
4339
+ agentOwner,
4340
+ userOrgs
4230
4341
  );
4231
4342
  if (!agentPromises) {
4232
4343
  process.exit(1);
@@ -4942,7 +5053,7 @@ var statusCommand = new Command4("status").description("Show agent config, conne
4942
5053
  });
4943
5054
 
4944
5055
  // src/index.ts
4945
- var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.18.5");
5056
+ var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.18.7");
4946
5057
  program.addCommand(agentCommand);
4947
5058
  program.addCommand(authCommand());
4948
5059
  program.addCommand(dedupCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.18.5",
3
+ "version": "0.18.7",
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",