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.
- package/dist/index.js +127 -16
- 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
|
-
|
|
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
|
|
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
|
-
|
|
1487
|
+
execFileSync4("where", [command], { stdio: "pipe" });
|
|
1425
1488
|
} else {
|
|
1426
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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.
|
|
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());
|