opencara 0.18.0 → 0.18.1

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +167 -33
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -84,7 +84,7 @@ Review prompts are delivered via **stdin** to your command. The command reads st
84
84
 
85
85
  | Field | Default | Description |
86
86
  | ------------------------ | -------------------------- | ------------------------------------- |
87
- | `platform_url` | `https://api.opencara.dev` | OpenCara server URL |
87
+ | `platform_url` | `https://api.opencara.com` | OpenCara server URL |
88
88
  | `github_token` | -- | GitHub token for private repo diffs |
89
89
  | `codebase_dir` | -- | Default clone directory for repos |
90
90
  | `max_diff_size_kb` | `100` | Skip PRs with diffs larger than this |
package/dist/index.js CHANGED
@@ -378,7 +378,7 @@ import * as fs from "fs";
378
378
  import * as path from "path";
379
379
  import * as os from "os";
380
380
  import { parse as parseToml2, stringify as stringifyToml } from "smol-toml";
381
- var DEFAULT_PLATFORM_URL = "https://api.opencara.dev";
381
+ var DEFAULT_PLATFORM_URL = "https://api.opencara.com";
382
382
  var CONFIG_DIR = path.join(os.homedir(), ".opencara");
383
383
  var CONFIG_FILE = process.env.OPENCARA_CONFIG && process.env.OPENCARA_CONFIG.trim() ? path.resolve(process.env.OPENCARA_CONFIG) : path.join(CONFIG_DIR, "config.toml");
384
384
  function ensureConfigDir() {
@@ -565,7 +565,6 @@ function loadConfig() {
565
565
  const envPlatformUrl = process.env.OPENCARA_PLATFORM_URL?.trim() || null;
566
566
  const defaults = {
567
567
  platformUrl: envPlatformUrl || DEFAULT_PLATFORM_URL,
568
- apiKey: null,
569
568
  maxDiffSizeKb: DEFAULT_MAX_DIFF_SIZE_KB,
570
569
  maxConsecutiveErrors: DEFAULT_MAX_CONSECUTIVE_ERRORS,
571
570
  codebaseDir: null,
@@ -609,7 +608,6 @@ function loadConfig() {
609
608
  }
610
609
  return {
611
610
  platformUrl: envPlatformUrl || (typeof data.platform_url === "string" ? data.platform_url : DEFAULT_PLATFORM_URL),
612
- apiKey: typeof data.api_key === "string" ? data.api_key.trim() || null : null,
613
611
  maxDiffSizeKb: overrides.maxDiffSizeKb ?? (typeof data.max_diff_size_kb === "number" ? data.max_diff_size_kb : DEFAULT_MAX_DIFF_SIZE_KB),
614
612
  maxConsecutiveErrors: overrides.maxConsecutiveErrors ?? (typeof data.max_consecutive_errors === "number" ? data.max_consecutive_errors : DEFAULT_MAX_CONSECUTIVE_ERRORS),
615
613
  codebaseDir: typeof data.codebase_dir === "string" ? data.codebase_dir : null,
@@ -2195,8 +2193,14 @@ You MUST output ONLY a valid JSON object matching this exact schema (no markdown
2195
2193
 
2196
2194
  - "duplicates": array of matches found (empty array if no duplicates)
2197
2195
  - "similarity": "exact" = identical intent/change, "high" = very similar with minor differences, "partial" = overlapping but distinct
2198
- - "index_entry": a single line in the format: \`- #<number> [label1] [label2] \u2014 <short description>\`
2196
+ - "index_entry": a single line in the format: \`- <number>(<label1>, <label2>, ...): <short description>\` where labels are inferred from GitHub labels, PR/issue title, body, and any available context`);
2197
+ if (task.customPrompt) {
2198
+ parts.push(`
2199
+ ## Repo-Specific Instructions
2199
2200
 
2201
+ ${task.customPrompt}`);
2202
+ }
2203
+ parts.push(`
2200
2204
  ## Index of Existing Items
2201
2205
 
2202
2206
  <UNTRUSTED_CONTENT>`);
@@ -2346,7 +2350,7 @@ async function executeDedup(prompt, timeoutSeconds, deps, runTool = executeTool,
2346
2350
  }
2347
2351
  async function executeDedupTask(client, agentId, taskId, task, diffContent, timeoutSeconds, reviewDeps, consumptionDeps, logger, signal, role = "pr_dedup") {
2348
2352
  logger.log(` ${icons.running} Executing dedup: ${reviewDeps.commandTemplate}`);
2349
- const prompt = buildDedupPrompt({ ...task, diffContent });
2353
+ const prompt = buildDedupPrompt({ ...task, diffContent, customPrompt: task.prompt });
2350
2354
  const result = await executeDedup(
2351
2355
  prompt,
2352
2356
  timeoutSeconds,
@@ -2607,6 +2611,11 @@ function buildTriagePrompt(task) {
2607
2611
  const title = task.issue_title ?? `PR #${task.pr_number}`;
2608
2612
  const rawBody = task.issue_body ?? "";
2609
2613
  const safeBody = truncateToBytes(rawBody, MAX_ISSUE_BODY_BYTES);
2614
+ const repoPromptSection = task.prompt ? `
2615
+
2616
+ ## Repo-Specific Instructions
2617
+
2618
+ ${task.prompt}` : "";
2610
2619
  const userMessage = [
2611
2620
  `## Issue Title`,
2612
2621
  title,
@@ -2616,7 +2625,7 @@ function buildTriagePrompt(task) {
2616
2625
  safeBody,
2617
2626
  "</UNTRUSTED_CONTENT>"
2618
2627
  ].join("\n");
2619
- return `${TRIAGE_SYSTEM_PROMPT}
2628
+ return `${TRIAGE_SYSTEM_PROMPT}${repoPromptSection}
2620
2629
 
2621
2630
  ${userMessage}`;
2622
2631
  }
@@ -3167,7 +3176,8 @@ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, ag
3167
3176
  issue_title: task.issue_title,
3168
3177
  issue_body: task.issue_body,
3169
3178
  diff_url,
3170
- index_issue_body: task.index_issue_body
3179
+ index_issue_body: task.index_issue_body,
3180
+ prompt
3171
3181
  },
3172
3182
  diffContent,
3173
3183
  timeout_seconds,
@@ -3568,7 +3578,7 @@ function sleep2(ms, signal) {
3568
3578
  async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
3569
3579
  const client = new ApiClient(platformUrl, {
3570
3580
  authToken: options?.authToken,
3571
- cliVersion: "0.18.0",
3581
+ cliVersion: "0.18.1",
3572
3582
  versionOverride: options?.versionOverride,
3573
3583
  onTokenRefresh: options?.onTokenRefresh
3574
3584
  });
@@ -4075,11 +4085,77 @@ async function updateIssueComment(owner, repo, commentId, body, token, fetchFn =
4075
4085
  }
4076
4086
  function formatEntry(item, compact = false) {
4077
4087
  if (compact) {
4078
- return `- #${item.number} \u2014 ${item.title}`;
4088
+ return `- ${item.number}(): ${item.title}`;
4089
+ }
4090
+ const labels = item.labels.map((l) => l.name).join(", ");
4091
+ return `- ${item.number}(${labels}): ${item.title}`;
4092
+ }
4093
+ var AI_ENTRY_TIMEOUT_MS = 6e4;
4094
+ function buildIndexEntryPrompt(item, kind) {
4095
+ const typeLabel = kind === "prs" ? "PR" : "Issue";
4096
+ const labels = item.labels.map((l) => l.name).join(", ");
4097
+ return `You are a dedup index entry generator. Given a GitHub ${typeLabel}, produce a concise one-line description suitable for duplicate detection.
4098
+
4099
+ ## Input
4100
+
4101
+ ${typeLabel} #${item.number}: ${item.title}
4102
+ Labels: ${labels || "(none)"}
4103
+ State: ${item.state}
4104
+
4105
+ ## Output Format
4106
+
4107
+ Respond with ONLY a JSON object (no markdown fences, no preamble):
4108
+
4109
+ {
4110
+ "description": "<concise one-line description for duplicate detection>"
4111
+ }
4112
+
4113
+ The description should capture the core intent/change of the ${typeLabel.toLowerCase()} in a way that helps identify duplicates. Keep it under 120 characters.`;
4114
+ }
4115
+ function parseIndexEntryResponse(stdout) {
4116
+ const jsonStr = extractJson(stdout);
4117
+ if (!jsonStr) return null;
4118
+ try {
4119
+ const parsed = JSON.parse(jsonStr);
4120
+ if (typeof parsed.description === "string" && parsed.description.length > 0) {
4121
+ return parsed.description;
4122
+ }
4123
+ } catch {
4124
+ }
4125
+ return null;
4126
+ }
4127
+ function resolveAgentCommand(toolName) {
4128
+ const config = loadConfig();
4129
+ if (config.agents) {
4130
+ const agent = config.agents.find((a) => a.tool === toolName);
4131
+ if (agent) {
4132
+ const cmd = agent.command ?? config.agentCommand;
4133
+ if (cmd) return cmd;
4134
+ }
4135
+ }
4136
+ const registryTool = DEFAULT_REGISTRY.tools.find((t) => t.name === toolName);
4137
+ if (registryTool) {
4138
+ const defaultModel = DEFAULT_REGISTRY.models.find((m) => m.tools.includes(toolName));
4139
+ const modelName = defaultModel?.name ?? "";
4140
+ return registryTool.commandTemplate.replaceAll("${MODEL}", modelName);
4141
+ }
4142
+ return null;
4143
+ }
4144
+ async function generateAIEntry(item, kind, commandTemplate, runTool = executeTool) {
4145
+ const prompt = buildIndexEntryPrompt(item, kind);
4146
+ try {
4147
+ const result = await runTool(commandTemplate, prompt, AI_ENTRY_TIMEOUT_MS);
4148
+ return parseIndexEntryResponse(result.stdout);
4149
+ } catch {
4150
+ return null;
4151
+ }
4152
+ }
4153
+ function formatEntryWithDescription(item, description, compact = false) {
4154
+ if (compact) {
4155
+ return `- ${item.number}(): ${description}`;
4079
4156
  }
4080
- const labels = item.labels.map((l) => `[${l.name}]`).join(" ");
4081
- const labelPart = labels ? ` ${labels}` : "";
4082
- return `- #${item.number}${labelPart} \u2014 ${item.title}`;
4157
+ const labels = item.labels.map((l) => l.name).join(", ");
4158
+ return `- ${item.number}(${labels}): ${description}`;
4083
4159
  }
4084
4160
  function categorizeItems(items, recentDays = DEFAULT_RECENT_DAYS, nowMs = Date.now()) {
4085
4161
  const cutoff = nowMs - recentDays * 24 * 60 * 60 * 1e3;
@@ -4099,22 +4175,28 @@ function categorizeItems(items, recentDays = DEFAULT_RECENT_DAYS, nowMs = Date.n
4099
4175
  }
4100
4176
  function parseExistingNumbers(body) {
4101
4177
  const numbers = /* @__PURE__ */ new Set();
4102
- const regex = /^- #(\d+)/gm;
4178
+ const regex = /^- #?(\d+)/gm;
4103
4179
  let match;
4104
4180
  while ((match = regex.exec(body)) !== null) {
4105
4181
  numbers.add(parseInt(match[1], 10));
4106
4182
  }
4107
4183
  return numbers;
4108
4184
  }
4109
- function buildCommentBody(marker, header, items, existingBody, compact = false) {
4185
+ function buildCommentBody(marker, header, items, existingBody, compact = false, descriptions) {
4110
4186
  const existingNumbers = existingBody ? parseExistingNumbers(existingBody) : /* @__PURE__ */ new Set();
4111
4187
  const newItems = items.filter((item) => !existingNumbers.has(item.number));
4112
4188
  let body = existingBody ?? `${marker}
4113
4189
  ## ${header}
4114
4190
  `;
4115
4191
  for (const item of newItems) {
4116
- body += `
4192
+ const aiDesc = descriptions?.get(item.number);
4193
+ if (aiDesc) {
4194
+ body += `
4195
+ ${formatEntryWithDescription(item, aiDesc, compact)}`;
4196
+ } else {
4197
+ body += `
4117
4198
  ${formatEntry(item, compact)}`;
4199
+ }
4118
4200
  }
4119
4201
  return body;
4120
4202
  }
@@ -4134,6 +4216,7 @@ async function initIndex(opts) {
4134
4216
  const fetchFn = opts.fetchFn ?? fetch;
4135
4217
  const log = opts.log ?? (() => {
4136
4218
  });
4219
+ const runTool = opts.runTool ?? executeTool;
4137
4220
  log(`Scanning ${kind}...`);
4138
4221
  const items = kind === "prs" ? await fetchAllPRs(owner, repo, token, fetchFn, log) : await fetchAllIssues(owner, repo, token, fetchFn, log);
4139
4222
  log(`${icons.info} Found ${items.length} ${kind}.`);
@@ -4143,34 +4226,64 @@ async function initIndex(opts) {
4143
4226
  );
4144
4227
  const comments = await fetchIssueComments2(owner, repo, indexIssue, token, fetchFn);
4145
4228
  const found = findIndexComments(comments);
4146
- const openBody = buildCommentBody(OPEN_MARKER, "Open Items", open, found.open?.body ?? null);
4229
+ const existingOpen = found.open ? parseExistingNumbers(found.open.body) : /* @__PURE__ */ new Set();
4230
+ const existingRecent = found.recent ? parseExistingNumbers(found.recent.body) : /* @__PURE__ */ new Set();
4231
+ const existingArchived = found.archived ? parseExistingNumbers(found.archived.body) : /* @__PURE__ */ new Set();
4232
+ const newOpenItems = open.filter((i) => !existingOpen.has(i.number));
4233
+ const newRecentItems = recentlyClosed.filter((i) => !existingRecent.has(i.number));
4234
+ const newArchivedItems = archived.filter((i) => !existingArchived.has(i.number));
4235
+ const newEntries = newOpenItems.length + newRecentItems.length + newArchivedItems.length;
4236
+ const descriptions = /* @__PURE__ */ new Map();
4237
+ if (opts.agentCommandTemplate && newEntries > 0) {
4238
+ const allNewItems = [...newOpenItems, ...newRecentItems, ...newArchivedItems];
4239
+ log(`
4240
+ Generating AI-enriched descriptions for ${allNewItems.length} items...`);
4241
+ for (let i = 0; i < allNewItems.length; i++) {
4242
+ const item = allNewItems[i];
4243
+ log(` Processing item ${i + 1}/${allNewItems.length} (#${item.number})...`);
4244
+ const desc = await generateAIEntry(item, kind, opts.agentCommandTemplate, runTool);
4245
+ if (desc) {
4246
+ descriptions.set(item.number, desc);
4247
+ } else {
4248
+ log(` ${icons.warn} AI failed for #${item.number}, using raw title`);
4249
+ }
4250
+ }
4251
+ const enriched = descriptions.size;
4252
+ log(
4253
+ `${icons.info} AI enrichment: ${enriched}/${allNewItems.length} items enriched successfully`
4254
+ );
4255
+ }
4256
+ const openBody = buildCommentBody(
4257
+ OPEN_MARKER,
4258
+ "Open Items",
4259
+ open,
4260
+ found.open?.body ?? null,
4261
+ false,
4262
+ descriptions
4263
+ );
4147
4264
  const recentBody = buildCommentBody(
4148
4265
  RECENT_MARKER,
4149
4266
  "Recently Closed Items",
4150
4267
  recentlyClosed,
4151
- found.recent?.body ?? null
4268
+ found.recent?.body ?? null,
4269
+ false,
4270
+ descriptions
4152
4271
  );
4153
4272
  const archivedBody = buildCommentBody(
4154
4273
  ARCHIVED_MARKER,
4155
4274
  "Archived Items",
4156
4275
  archived,
4157
4276
  found.archived?.body ?? null,
4158
- true
4277
+ true,
4159
4278
  // compact format
4279
+ descriptions
4160
4280
  );
4161
- const existingOpen = found.open ? parseExistingNumbers(found.open.body) : /* @__PURE__ */ new Set();
4162
- const existingRecent = found.recent ? parseExistingNumbers(found.recent.body) : /* @__PURE__ */ new Set();
4163
- const existingArchived = found.archived ? parseExistingNumbers(found.archived.body) : /* @__PURE__ */ new Set();
4164
- const newOpen = open.filter((i) => !existingOpen.has(i.number)).length;
4165
- const newRecent = recentlyClosed.filter((i) => !existingRecent.has(i.number)).length;
4166
- const newArchived = archived.filter((i) => !existingArchived.has(i.number)).length;
4167
- const newEntries = newOpen + newRecent + newArchived;
4168
4281
  if (dryRun) {
4169
4282
  log(`
4170
4283
  ${icons.info} Dry run \u2014 would update index issue #${indexIssue}:`);
4171
- log(` Open Items: ${open.length} entries (${newOpen} new)`);
4172
- log(` Recently Closed: ${recentlyClosed.length} entries (${newRecent} new)`);
4173
- log(` Archived: ${archived.length} entries (${newArchived} new)`);
4284
+ log(` Open Items: ${open.length} entries (${newOpenItems.length} new)`);
4285
+ log(` Recently Closed: ${recentlyClosed.length} entries (${newRecentItems.length} new)`);
4286
+ log(` Archived: ${archived.length} entries (${newArchivedItems.length} new)`);
4174
4287
  return {
4175
4288
  openCount: open.length,
4176
4289
  recentCount: recentlyClosed.length,
@@ -4209,6 +4322,7 @@ async function runDedupInit(options, deps = {}) {
4209
4322
  const log = deps.log ?? console.log;
4210
4323
  const logError = deps.logError ?? console.error;
4211
4324
  const loadAuthFn = deps.loadAuthFn ?? loadAuth;
4325
+ const resolveCmd = deps.resolveAgentCommandFn ?? resolveAgentCommand;
4212
4326
  const auth = loadAuthFn();
4213
4327
  if (!auth || auth.expires_at <= Date.now()) {
4214
4328
  logError(`${icons.error} Not authenticated. Run: ${pc3.cyan("opencara auth login")}`);
@@ -4273,6 +4387,19 @@ async function runDedupInit(options, deps = {}) {
4273
4387
  process.exitCode = 1;
4274
4388
  return;
4275
4389
  }
4390
+ let agentCommandTemplate;
4391
+ if (options.agent) {
4392
+ const cmd = resolveCmd(options.agent);
4393
+ if (!cmd) {
4394
+ logError(
4395
+ `${icons.error} Unknown agent tool "${options.agent}". Available: ${DEFAULT_REGISTRY.tools.map((t) => t.name).join(", ")}`
4396
+ );
4397
+ process.exitCode = 1;
4398
+ return;
4399
+ }
4400
+ agentCommandTemplate = cmd;
4401
+ log(`Using AI agent "${options.agent}" for enriched descriptions`);
4402
+ }
4276
4403
  for (const target of filteredTargets) {
4277
4404
  log(`
4278
4405
  ${pc3.bold(`Initializing ${target.kind} dedup index (issue #${target.indexIssue})...`)}`);
@@ -4284,16 +4411,23 @@ ${pc3.bold(`Initializing ${target.kind} dedup index (issue #${target.indexIssue}
4284
4411
  recentDays,
4285
4412
  dryRun: options.dryRun ?? false,
4286
4413
  token,
4414
+ agentCommandTemplate,
4287
4415
  fetchFn,
4288
- log
4416
+ log,
4417
+ runTool: deps.runTool
4289
4418
  });
4290
4419
  }
4291
4420
  }
4292
4421
  function dedupCommand() {
4293
4422
  const dedup = new Command3("dedup").description("Dedup index management");
4294
- dedup.command("init").description("Scan existing PRs/issues and populate dedup index").requiredOption("--repo <owner/repo>", "Target repository (e.g., OpenCara/OpenCara)").option("--all", "Initialize both PR and issue dedup indexes").option("--dry-run", "Show what would be done without making changes").option("--days <number>", "Recently closed window in days (default: 30)", "30").action(async (options) => {
4295
- await runDedupInit(options);
4296
- });
4423
+ dedup.command("init").description("Scan existing PRs/issues and populate dedup index").requiredOption("--repo <owner/repo>", "Target repository (e.g., OpenCara/OpenCara)").option("--all", "Initialize both PR and issue dedup indexes").option("--dry-run", "Show what would be done without making changes").option("--days <number>", "Recently closed window in days (default: 30)", "30").option(
4424
+ "--agent <tool-name>",
4425
+ "Use AI agent to generate enriched descriptions (e.g., claude, codex, gemini, qwen)"
4426
+ ).action(
4427
+ async (options) => {
4428
+ await runDedupInit(options);
4429
+ }
4430
+ );
4297
4431
  return dedup;
4298
4432
  }
4299
4433
 
@@ -4424,7 +4558,7 @@ var statusCommand = new Command4("status").description("Show agent config, conne
4424
4558
  });
4425
4559
 
4426
4560
  // src/index.ts
4427
- var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.18.0");
4561
+ var program = new Command5().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.18.1");
4428
4562
  program.addCommand(agentCommand);
4429
4563
  program.addCommand(authCommand());
4430
4564
  program.addCommand(dedupCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
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",