opencara 0.11.0 → 0.12.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 (3) hide show
  1. package/README.md +126 -0
  2. package/dist/index.js +447 -86
  3. package/package.json +29 -5
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # opencara
2
+
3
+ Distributed AI code review agent for GitHub pull requests. Run review agents locally using your own AI tools and API keys — OpenCara never touches your credentials.
4
+
5
+ ## How It Works
6
+
7
+ ```
8
+ GitHub PR → OpenCara server creates review task
9
+ → Your agent polls for tasks → Claims one → Fetches diff from GitHub
10
+ → Reviews locally with your AI tool → Submits result
11
+ → Server posts the review to the PR
12
+ ```
13
+
14
+ ## Quick Start
15
+
16
+ ```bash
17
+ # 1. Install
18
+ npm i -g opencara
19
+
20
+ # 2. Create config
21
+ mkdir -p ~/.opencara
22
+ cat > ~/.opencara/config.yml << 'EOF'
23
+ platform_url: https://opencara-server.opencara.workers.dev
24
+ agents:
25
+ - model: claude-sonnet-4-6
26
+ tool: claude
27
+ command: claude --model claude-sonnet-4-6 --allowedTools '*' --print
28
+ EOF
29
+
30
+ # 3. Start
31
+ opencara agent start
32
+ ```
33
+
34
+ Your agent is now polling for review tasks every 10 seconds.
35
+
36
+ ## Supported AI Tools
37
+
38
+ | Tool | Install | Example models |
39
+ | ---------- | ------------------------------------ | ---------------------------------- |
40
+ | Claude | `npm i -g @anthropic-ai/claude-code` | claude-sonnet-4-6, claude-opus-4-6 |
41
+ | Codex | `npm i -g @openai/codex` | gpt-5.4-codex |
42
+ | Gemini CLI | `npm i -g @google/gemini-cli` | gemini-2.5-pro, gemini-2.5-flash |
43
+ | Qwen CLI | `npm i -g qwen` | qwen3.5-plus, glm-5, kimi-k2.5 |
44
+
45
+ Each tool requires its own API key configured per its documentation.
46
+
47
+ ## Configuration
48
+
49
+ Edit `~/.opencara/config.yml`:
50
+
51
+ ```yaml
52
+ platform_url: https://opencara-server.opencara.workers.dev
53
+
54
+ agents:
55
+ - model: claude-sonnet-4-6
56
+ tool: claude
57
+ command: claude --model claude-sonnet-4-6 --allowedTools '*' --print
58
+
59
+ - model: gemini-2.5-pro
60
+ tool: gemini
61
+ command: gemini -m gemini-2.5-pro
62
+ ```
63
+
64
+ Review prompts are delivered via **stdin** to your command. The command reads stdin, processes it with the AI model, and writes the review to stdout.
65
+
66
+ ### Agent Config Fields
67
+
68
+ | Field | Required | Default | Description |
69
+ | -------------- | -------- | ------- | ------------------------------------------------------ |
70
+ | `model` | Yes | -- | AI model identifier (e.g., `claude-sonnet-4-6`) |
71
+ | `tool` | Yes | -- | AI tool identifier (e.g., `claude`, `codex`, `gemini`) |
72
+ | `command` | Yes\* | -- | Shell command to execute reviews (stdin -> stdout) |
73
+ | `name` | No | -- | Display name in CLI logs (local only) |
74
+ | `review_only` | No | `false` | If `true`, agent only reviews, never synthesizes |
75
+ | `github_token` | No | -- | Per-agent GitHub token for private repos |
76
+ | `codebase_dir` | No | -- | Local clone directory for context-aware reviews |
77
+ | `repos` | No | -- | Repo filtering (mode: all/own/whitelist/blacklist) |
78
+
79
+ \*Required unless `agent_command` is set globally.
80
+
81
+ ### Global Config Fields
82
+
83
+ | Field | Default | Description |
84
+ | ------------------------ | -------------------------- | ------------------------------------- |
85
+ | `platform_url` | `https://api.opencara.dev` | OpenCara server URL |
86
+ | `github_token` | -- | GitHub token for private repo diffs |
87
+ | `codebase_dir` | -- | Default clone directory for repos |
88
+ | `max_diff_size_kb` | `100` | Skip PRs with diffs larger than this |
89
+ | `max_consecutive_errors` | `10` | Stop agent after N consecutive errors |
90
+
91
+ ## CLI Reference
92
+
93
+ ### Commands
94
+
95
+ | Command | Description |
96
+ | ---------------------- | ----------------------------------------------- |
97
+ | `opencara` | Start agent in router mode (stdin/stdout relay) |
98
+ | `opencara agent start` | Start an agent in polling mode |
99
+
100
+ ### `opencara agent start` Options
101
+
102
+ | Option | Default | Description |
103
+ | --------------------------- | ------- | ---------------------------------------- |
104
+ | `--agent <index>` | `0` | Agent index from config.yml (0-based) |
105
+ | `--all` | -- | Start all configured agents concurrently |
106
+ | `--poll-interval <seconds>` | `10` | Poll interval in seconds |
107
+
108
+ ## Environment Variables
109
+
110
+ | Variable | Description |
111
+ | ----------------------- | ------------------------------------------------------------------ |
112
+ | `OPENCARA_CONFIG` | Path to alternate config file (overrides `~/.opencara/config.yml`) |
113
+ | `OPENCARA_PLATFORM_URL` | Override the platform URL from config |
114
+ | `GITHUB_TOKEN` | GitHub token (fallback for private repo access) |
115
+
116
+ ## Private Repos
117
+
118
+ The CLI resolves GitHub tokens using a fallback chain:
119
+
120
+ 1. `GITHUB_TOKEN` environment variable
121
+ 2. `gh auth token` (if GitHub CLI is installed)
122
+ 3. `github_token` in config.yml (global or per-agent)
123
+
124
+ ## License
125
+
126
+ MIT
package/dist/index.js CHANGED
@@ -28,6 +28,84 @@ function isRepoAllowed(repoConfig, targetOwner, targetRepo, agentOwner) {
28
28
  }
29
29
  }
30
30
 
31
+ // ../shared/dist/api.js
32
+ var DEFAULT_REGISTRY = {
33
+ tools: [
34
+ {
35
+ name: "claude",
36
+ displayName: "Claude",
37
+ binary: "claude",
38
+ commandTemplate: "claude --model ${MODEL} --allowedTools '*' --print",
39
+ tokenParser: "claude"
40
+ },
41
+ {
42
+ name: "codex",
43
+ displayName: "Codex",
44
+ binary: "codex",
45
+ commandTemplate: "codex --model ${MODEL} exec",
46
+ tokenParser: "codex"
47
+ },
48
+ {
49
+ name: "gemini",
50
+ displayName: "Gemini",
51
+ binary: "gemini",
52
+ commandTemplate: "gemini -m ${MODEL}",
53
+ tokenParser: "gemini"
54
+ },
55
+ {
56
+ name: "qwen",
57
+ displayName: "Qwen",
58
+ binary: "qwen",
59
+ commandTemplate: "qwen --model ${MODEL} -y",
60
+ tokenParser: "qwen"
61
+ }
62
+ ],
63
+ models: [
64
+ {
65
+ name: "claude-opus-4-6",
66
+ displayName: "Claude Opus 4.6",
67
+ tools: ["claude"]
68
+ },
69
+ {
70
+ name: "claude-opus-4-6[1m]",
71
+ displayName: "Claude Opus 4.6 (1M context)",
72
+ tools: ["claude"]
73
+ },
74
+ {
75
+ name: "claude-sonnet-4-6",
76
+ displayName: "Claude Sonnet 4.6",
77
+ tools: ["claude"]
78
+ },
79
+ {
80
+ name: "claude-sonnet-4-6[1m]",
81
+ displayName: "Claude Sonnet 4.6 (1M context)",
82
+ tools: ["claude"]
83
+ },
84
+ {
85
+ name: "gpt-5-codex",
86
+ displayName: "GPT-5 Codex",
87
+ tools: ["codex"]
88
+ },
89
+ {
90
+ name: "gemini-2.5-pro",
91
+ displayName: "Gemini 2.5 Pro",
92
+ tools: ["gemini"]
93
+ },
94
+ {
95
+ name: "qwen3.5-plus",
96
+ displayName: "Qwen 3.5 Plus",
97
+ tools: ["qwen"]
98
+ },
99
+ { name: "glm-5", displayName: "GLM-5", tools: ["qwen"] },
100
+ { name: "kimi-k2.5", displayName: "Kimi K2.5", tools: ["qwen"] },
101
+ {
102
+ name: "minimax-m2.5",
103
+ displayName: "Minimax M2.5",
104
+ tools: ["qwen"]
105
+ }
106
+ ]
107
+ };
108
+
31
109
  // ../shared/dist/review-config.js
32
110
  import { parse as parseYaml } from "yaml";
33
111
 
@@ -49,6 +127,16 @@ var RepoConfigError = class extends Error {
49
127
  this.name = "RepoConfigError";
50
128
  }
51
129
  };
130
+ var ConfigValidationError = class extends Error {
131
+ constructor(message) {
132
+ super(message);
133
+ this.name = "ConfigValidationError";
134
+ }
135
+ };
136
+ var KNOWN_TOOL_NAMES = new Set(DEFAULT_REGISTRY.tools.map((t) => t.name));
137
+ var TOOL_ALIASES = {
138
+ "claude-code": "claude"
139
+ };
52
140
  function parseRepoConfig(obj, index) {
53
141
  const raw = obj.repos;
54
142
  if (raw === void 0 || raw === null) return void 0;
@@ -92,15 +180,33 @@ function parseAgents(data) {
92
180
  for (let i = 0; i < raw.length; i++) {
93
181
  const entry = raw[i];
94
182
  if (!entry || typeof entry !== "object") {
95
- console.warn(`Warning: agents[${i}] is not an object, skipping`);
183
+ console.warn(`\u26A0 Config warning: agents[${i}] is not an object, skipping agent`);
96
184
  continue;
97
185
  }
98
186
  const obj = entry;
99
187
  if (typeof obj.model !== "string" || typeof obj.tool !== "string") {
100
- console.warn(`Warning: agents[${i}] missing required model/tool fields, skipping`);
188
+ console.warn(
189
+ `\u26A0 Config warning: agents[${i}] missing required model/tool fields, skipping agent`
190
+ );
101
191
  continue;
102
192
  }
103
- const agent = { model: obj.model, tool: obj.tool };
193
+ let resolvedTool = obj.tool;
194
+ if (!KNOWN_TOOL_NAMES.has(resolvedTool)) {
195
+ const alias = TOOL_ALIASES[resolvedTool];
196
+ if (alias) {
197
+ console.warn(
198
+ `\u26A0 Config warning: agents[${i}].tool "${resolvedTool}" is deprecated, using "${alias}" instead`
199
+ );
200
+ resolvedTool = alias;
201
+ } else {
202
+ const toolNames = [...KNOWN_TOOL_NAMES].join(", ");
203
+ console.warn(
204
+ `\u26A0 Config warning: agents[${i}].tool "${resolvedTool}" not in registry (known: ${toolNames}), skipping agent`
205
+ );
206
+ continue;
207
+ }
208
+ }
209
+ const agent = { model: obj.model, tool: resolvedTool };
104
210
  if (typeof obj.name === "string") agent.name = obj.name;
105
211
  if (typeof obj.command === "string") agent.command = obj.command;
106
212
  if (obj.router === true) agent.router = true;
@@ -113,6 +219,35 @@ function parseAgents(data) {
113
219
  }
114
220
  return agents;
115
221
  }
222
+ function isValidHttpUrl(value) {
223
+ try {
224
+ const url = new URL(value);
225
+ return url.protocol === "http:" || url.protocol === "https:";
226
+ } catch {
227
+ return false;
228
+ }
229
+ }
230
+ function validateConfigData(data, envPlatformUrl) {
231
+ const overrides = {};
232
+ if (!envPlatformUrl && typeof data.platform_url === "string" && !isValidHttpUrl(data.platform_url)) {
233
+ throw new ConfigValidationError(
234
+ `\u2717 Config error: platform_url "${data.platform_url}" is not a valid URL`
235
+ );
236
+ }
237
+ if (typeof data.max_diff_size_kb === "number" && data.max_diff_size_kb <= 0) {
238
+ console.warn(
239
+ `\u26A0 Config warning: max_diff_size_kb must be > 0, got ${data.max_diff_size_kb}, using default (${DEFAULT_MAX_DIFF_SIZE_KB})`
240
+ );
241
+ overrides.maxDiffSizeKb = DEFAULT_MAX_DIFF_SIZE_KB;
242
+ }
243
+ if (typeof data.max_consecutive_errors === "number" && data.max_consecutive_errors <= 0) {
244
+ console.warn(
245
+ `\u26A0 Config warning: max_consecutive_errors must be > 0, got ${data.max_consecutive_errors}, using default (${DEFAULT_MAX_CONSECUTIVE_ERRORS})`
246
+ );
247
+ overrides.maxConsecutiveErrors = DEFAULT_MAX_CONSECUTIVE_ERRORS;
248
+ }
249
+ return overrides;
250
+ }
116
251
  function loadConfig() {
117
252
  const envPlatformUrl = process.env.OPENCARA_PLATFORM_URL?.trim() || null;
118
253
  const defaults = {
@@ -132,10 +267,11 @@ function loadConfig() {
132
267
  if (!data || typeof data !== "object") {
133
268
  return defaults;
134
269
  }
270
+ const overrides = validateConfigData(data, envPlatformUrl);
135
271
  return {
136
272
  platformUrl: envPlatformUrl || (typeof data.platform_url === "string" ? data.platform_url : DEFAULT_PLATFORM_URL),
137
- maxDiffSizeKb: typeof data.max_diff_size_kb === "number" ? data.max_diff_size_kb : DEFAULT_MAX_DIFF_SIZE_KB,
138
- maxConsecutiveErrors: typeof data.max_consecutive_errors === "number" ? data.max_consecutive_errors : DEFAULT_MAX_CONSECUTIVE_ERRORS,
273
+ maxDiffSizeKb: overrides.maxDiffSizeKb ?? (typeof data.max_diff_size_kb === "number" ? data.max_diff_size_kb : DEFAULT_MAX_DIFF_SIZE_KB),
274
+ maxConsecutiveErrors: overrides.maxConsecutiveErrors ?? (typeof data.max_consecutive_errors === "number" ? data.max_consecutive_errors : DEFAULT_MAX_CONSECUTIVE_ERRORS),
139
275
  githubToken: typeof data.github_token === "string" ? data.github_token : null,
140
276
  codebaseDir: typeof data.codebase_dir === "string" ? data.codebase_dir : null,
141
277
  agentCommand: typeof data.agent_command === "string" ? data.agent_command : null,
@@ -267,9 +403,10 @@ function logAuthMethod(method, log) {
267
403
 
268
404
  // src/http.ts
269
405
  var HttpError = class extends Error {
270
- constructor(status, message) {
406
+ constructor(status, message, errorCode) {
271
407
  super(message);
272
408
  this.status = status;
409
+ this.errorCode = errorCode;
273
410
  this.name = "HttpError";
274
411
  }
275
412
  };
@@ -307,13 +444,17 @@ var ApiClient = class {
307
444
  async handleResponse(res, path5) {
308
445
  if (!res.ok) {
309
446
  let message = `HTTP ${res.status}`;
447
+ let errorCode;
310
448
  try {
311
449
  const body = await res.json();
312
- if (body.error) message = body.error;
450
+ if (body.error && typeof body.error === "object" && "code" in body.error) {
451
+ errorCode = body.error.code;
452
+ message = body.error.message;
453
+ }
313
454
  } catch {
314
455
  }
315
456
  this.log(`${res.status} ${message} (${path5})`);
316
- throw new HttpError(res.status, message);
457
+ throw new HttpError(res.status, message, errorCode);
317
458
  }
318
459
  this.log(`${res.status} OK (${path5})`);
319
460
  return await res.json();
@@ -611,12 +752,13 @@ function buildSystemPrompt(owner, repo, mode = "full") {
611
752
  const template = mode === "compact" ? COMPACT_SYSTEM_PROMPT_TEMPLATE : FULL_SYSTEM_PROMPT_TEMPLATE;
612
753
  return template.replace("{owner}", owner).replace("{repo}", repo);
613
754
  }
614
- function buildUserMessage(prompt, diffContent) {
615
- return `${prompt}
616
-
617
- ---
618
-
619
- ${diffContent}`;
755
+ function buildUserMessage(prompt, diffContent, contextBlock) {
756
+ const parts = [prompt];
757
+ if (contextBlock) {
758
+ parts.push(contextBlock);
759
+ }
760
+ parts.push(diffContent);
761
+ return parts.join("\n\n---\n\n");
620
762
  }
621
763
  var SECTION_VERDICT_PATTERN = /##\s*Verdict\s*\n+\s*(APPROVE|REQUEST_CHANGES|COMMENT)\b/im;
622
764
  var LEGACY_VERDICT_PATTERN = /^VERDICT:\s*(APPROVE|REQUEST_CHANGES|COMMENT)\s*$/m;
@@ -656,7 +798,7 @@ async function executeReview(req, deps, runTool = executeTool) {
656
798
  }, effectiveTimeout);
657
799
  try {
658
800
  const systemPrompt = buildSystemPrompt(req.owner, req.repo, req.reviewMode);
659
- const userMessage = buildUserMessage(req.prompt, req.diffContent);
801
+ const userMessage = buildUserMessage(req.prompt, req.diffContent, req.contextBlock);
660
802
  const fullPrompt = `${systemPrompt}
661
803
 
662
804
  ${userMessage}`;
@@ -728,27 +870,28 @@ For each finding, explain clearly what the problem is and how to fix it.
728
870
  ## Verdict
729
871
  APPROVE | REQUEST_CHANGES | COMMENT`;
730
872
  }
731
- function buildSummaryUserMessage(prompt, reviews, diffContent) {
873
+ function buildSummaryUserMessage(prompt, reviews, diffContent, contextBlock) {
732
874
  const reviewSections = reviews.map((r) => `### Review by ${r.model}/${r.tool} (Verdict: ${r.verdict})
733
875
  ${r.review}`).join("\n\n");
734
- return `Project review guidelines:
735
- ${prompt}
736
-
737
- ---
738
-
739
- Pull request diff:
740
-
741
- ${diffContent}
742
-
743
- ---
876
+ const parts = [`Project review guidelines:
877
+ ${prompt}`];
878
+ if (contextBlock) {
879
+ parts.push(contextBlock);
880
+ }
881
+ parts.push(`Pull request diff:
744
882
 
745
- Compact reviews from other agents:
883
+ ${diffContent}`);
884
+ parts.push(`Compact reviews from other agents:
746
885
 
747
- ${reviewSections}`;
886
+ ${reviewSections}`);
887
+ return parts.join("\n\n---\n\n");
748
888
  }
749
- function calculateInputSize(prompt, reviews, diffContent) {
889
+ function calculateInputSize(prompt, reviews, diffContent, contextBlock) {
750
890
  let size = Buffer.byteLength(prompt, "utf-8");
751
891
  size += Buffer.byteLength(diffContent, "utf-8");
892
+ if (contextBlock) {
893
+ size += Buffer.byteLength(contextBlock, "utf-8");
894
+ }
752
895
  for (const r of reviews) {
753
896
  size += Buffer.byteLength(r.review, "utf-8");
754
897
  size += Buffer.byteLength(r.model, "utf-8");
@@ -758,7 +901,7 @@ function calculateInputSize(prompt, reviews, diffContent) {
758
901
  return size;
759
902
  }
760
903
  async function executeSummary(req, deps, runTool = executeTool) {
761
- const inputSize = calculateInputSize(req.prompt, req.reviews, req.diffContent);
904
+ const inputSize = calculateInputSize(req.prompt, req.reviews, req.diffContent, req.contextBlock);
762
905
  if (inputSize > MAX_INPUT_SIZE_BYTES) {
763
906
  throw new InputTooLargeError(
764
907
  `Summary input too large (${Math.round(inputSize / 1024)}KB > ${Math.round(MAX_INPUT_SIZE_BYTES / 1024)}KB limit)`
@@ -775,7 +918,12 @@ async function executeSummary(req, deps, runTool = executeTool) {
775
918
  }, effectiveTimeout);
776
919
  try {
777
920
  const systemPrompt = buildSummarySystemPrompt(req.owner, req.repo, req.reviews.length);
778
- const userMessage = buildSummaryUserMessage(req.prompt, req.reviews, req.diffContent);
921
+ const userMessage = buildSummaryUserMessage(
922
+ req.prompt,
923
+ req.reviews,
924
+ req.diffContent,
925
+ req.contextBlock
926
+ );
779
927
  const fullPrompt = `${systemPrompt}
780
928
 
781
929
  ${userMessage}`;
@@ -874,7 +1022,7 @@ var RouterRelay = class {
874
1022
  req.repo,
875
1023
  req.reviewMode
876
1024
  );
877
- const userMessage = buildUserMessage(req.prompt, req.diffContent);
1025
+ const userMessage = buildUserMessage(req.prompt, req.diffContent, req.contextBlock);
878
1026
  return `${systemPrompt}
879
1027
 
880
1028
  ${userMessage}`;
@@ -882,7 +1030,12 @@ ${userMessage}`;
882
1030
  /** Build the full prompt for a summary request */
883
1031
  buildSummaryPrompt(req) {
884
1032
  const systemPrompt = buildSummarySystemPrompt(req.owner, req.repo, req.reviews.length);
885
- const userMessage = buildSummaryUserMessage(req.prompt, req.reviews, req.diffContent);
1033
+ const userMessage = buildSummaryUserMessage(
1034
+ req.prompt,
1035
+ req.reviews,
1036
+ req.diffContent,
1037
+ req.contextBlock
1038
+ );
886
1039
  return `${systemPrompt}
887
1040
 
888
1041
  ${userMessage}`;
@@ -961,18 +1114,196 @@ function formatPostReviewStats(session) {
961
1114
  return ` Session: ${session.tokens.toLocaleString()} tokens / ${session.reviews} reviews`;
962
1115
  }
963
1116
 
964
- // src/commands/agent.ts
965
- var DEFAULT_POLL_INTERVAL_MS = 1e4;
966
- var MAX_CONSECUTIVE_AUTH_ERRORS = 3;
967
- var MAX_POLL_BACKOFF_MS = 3e5;
1117
+ // src/pr-context.ts
1118
+ async function githubGet(url, deps) {
1119
+ const headers = {
1120
+ Accept: "application/vnd.github+json"
1121
+ };
1122
+ if (deps.githubToken) {
1123
+ headers["Authorization"] = `Bearer ${deps.githubToken}`;
1124
+ }
1125
+ const response = await fetch(url, { headers, signal: deps.signal });
1126
+ if (!response.ok) {
1127
+ throw new Error(`GitHub API ${response.status}: ${response.statusText}`);
1128
+ }
1129
+ return response.json();
1130
+ }
1131
+ async function fetchPRMetadata(owner, repo, prNumber, deps) {
1132
+ try {
1133
+ const pr = await githubGet(
1134
+ `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}`,
1135
+ deps
1136
+ );
1137
+ return {
1138
+ title: pr.title,
1139
+ body: pr.body ?? "",
1140
+ author: pr.user?.login ?? "unknown",
1141
+ labels: pr.labels.map((l) => l.name),
1142
+ baseBranch: pr.base.ref,
1143
+ headBranch: pr.head.ref
1144
+ };
1145
+ } catch {
1146
+ return null;
1147
+ }
1148
+ }
1149
+ async function fetchIssueComments(owner, repo, prNumber, deps) {
1150
+ try {
1151
+ const comments = await githubGet(
1152
+ `https://api.github.com/repos/${owner}/${repo}/issues/${prNumber}/comments`,
1153
+ deps
1154
+ );
1155
+ return comments.map((c) => ({
1156
+ author: c.user?.login ?? "unknown",
1157
+ body: c.body,
1158
+ createdAt: c.created_at
1159
+ }));
1160
+ } catch {
1161
+ return [];
1162
+ }
1163
+ }
1164
+ async function fetchReviewComments(owner, repo, prNumber, deps) {
1165
+ try {
1166
+ const comments = await githubGet(
1167
+ `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/comments`,
1168
+ deps
1169
+ );
1170
+ return comments.map((c) => ({
1171
+ author: c.user?.login ?? "unknown",
1172
+ body: c.body,
1173
+ path: c.path,
1174
+ line: c.line,
1175
+ createdAt: c.created_at
1176
+ }));
1177
+ } catch {
1178
+ return [];
1179
+ }
1180
+ }
1181
+ async function fetchExistingReviews(owner, repo, prNumber, deps) {
1182
+ try {
1183
+ const reviews = await githubGet(
1184
+ `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`,
1185
+ deps
1186
+ );
1187
+ return reviews.filter((r) => r.state !== "PENDING").map((r) => ({
1188
+ author: r.user?.login ?? "unknown",
1189
+ state: r.state,
1190
+ body: r.body ?? ""
1191
+ }));
1192
+ } catch {
1193
+ return [];
1194
+ }
1195
+ }
1196
+ async function fetchPRContext(owner, repo, prNumber, deps) {
1197
+ const [metadata, comments, reviewThreads, existingReviews] = await Promise.all([
1198
+ fetchPRMetadata(owner, repo, prNumber, deps),
1199
+ fetchIssueComments(owner, repo, prNumber, deps),
1200
+ fetchReviewComments(owner, repo, prNumber, deps),
1201
+ fetchExistingReviews(owner, repo, prNumber, deps)
1202
+ ]);
1203
+ return { metadata, comments, reviewThreads, existingReviews };
1204
+ }
1205
+ function formatPRContext(context, codebaseDir) {
1206
+ const sections = [];
1207
+ if (context.metadata) {
1208
+ const m = context.metadata;
1209
+ const lines = ["## PR Context", `**Title**: ${m.title}`, `**Author**: @${m.author}`];
1210
+ if (m.body) {
1211
+ lines.push(`**Description**: ${m.body}`);
1212
+ }
1213
+ if (m.labels.length > 0) {
1214
+ lines.push(`**Labels**: ${m.labels.join(", ")}`);
1215
+ }
1216
+ lines.push(`**Branches**: ${m.headBranch} \u2192 ${m.baseBranch}`);
1217
+ sections.push(lines.join("\n"));
1218
+ }
1219
+ if (context.comments.length > 0) {
1220
+ const commentLines = context.comments.map((c) => `@${c.author}: ${c.body}`);
1221
+ sections.push(
1222
+ `## Discussion (${context.comments.length} comment${context.comments.length === 1 ? "" : "s"})
1223
+ ${commentLines.join("\n")}`
1224
+ );
1225
+ }
1226
+ if (context.reviewThreads.length > 0) {
1227
+ const threadLines = context.reviewThreads.map((t) => {
1228
+ const location = t.line ? `\`${t.path}:${t.line}\`` : `\`${t.path}\``;
1229
+ return `@${t.author} on ${location}: ${t.body}`;
1230
+ });
1231
+ sections.push(`## Review Threads (${context.reviewThreads.length})
1232
+ ${threadLines.join("\n")}`);
1233
+ }
1234
+ if (context.existingReviews.length > 0) {
1235
+ const reviewLines = context.existingReviews.map((r) => {
1236
+ const body = r.body ? ` ${r.body}` : "";
1237
+ return `@${r.author}: [${r.state}]${body}`;
1238
+ });
1239
+ sections.push(
1240
+ `## Existing Reviews (${context.existingReviews.length})
1241
+ ${reviewLines.join("\n")}`
1242
+ );
1243
+ }
1244
+ if (codebaseDir) {
1245
+ sections.push(`## Local Codebase
1246
+ The full repository is available at: ${codebaseDir}`);
1247
+ }
1248
+ return sanitizeTokens(sections.join("\n\n"));
1249
+ }
1250
+ function hasContent(context) {
1251
+ return context.metadata !== null || context.comments.length > 0 || context.reviewThreads.length > 0 || context.existingReviews.length > 0;
1252
+ }
1253
+
1254
+ // src/logger.ts
1255
+ import pc from "picocolors";
1256
+ var icons = {
1257
+ start: pc.green("\u25CF"),
1258
+ polling: pc.cyan("\u21BB"),
1259
+ success: pc.green("\u2713"),
1260
+ running: pc.blue("\u25B6"),
1261
+ stop: pc.red("\u25A0"),
1262
+ warn: pc.yellow("\u26A0"),
1263
+ error: pc.red("\u2717")
1264
+ };
1265
+ function timestamp() {
1266
+ const now = /* @__PURE__ */ new Date();
1267
+ const h = String(now.getHours()).padStart(2, "0");
1268
+ const m = String(now.getMinutes()).padStart(2, "0");
1269
+ const s = String(now.getSeconds()).padStart(2, "0");
1270
+ return `${h}:${m}:${s}`;
1271
+ }
968
1272
  function createLogger(label) {
969
- const prefix = label ? `[${label}] ` : "";
1273
+ const labelStr = label ? ` ${pc.dim(`[${label}]`)}` : "";
970
1274
  return {
971
- log: (msg) => console.log(`${prefix}${sanitizeTokens(msg)}`),
972
- logError: (msg) => console.error(`${prefix}${sanitizeTokens(msg)}`),
973
- logWarn: (msg) => console.warn(`${prefix}${sanitizeTokens(msg)}`)
1275
+ log: (msg) => console.log(`${pc.dim(`[${timestamp()}]`)}${labelStr} ${sanitizeTokens(msg)}`),
1276
+ logError: (msg) => console.error(`${pc.dim(`[${timestamp()}]`)}${labelStr} ${pc.red(sanitizeTokens(msg))}`),
1277
+ logWarn: (msg) => console.warn(`${pc.dim(`[${timestamp()}]`)}${labelStr} ${pc.yellow(sanitizeTokens(msg))}`)
974
1278
  };
975
1279
  }
1280
+ function createAgentSession() {
1281
+ return {
1282
+ startTime: Date.now(),
1283
+ tasksCompleted: 0,
1284
+ errorsEncountered: 0
1285
+ };
1286
+ }
1287
+ function formatUptime(ms) {
1288
+ const totalSeconds = Math.floor(ms / 1e3);
1289
+ const hours = Math.floor(totalSeconds / 3600);
1290
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1291
+ const seconds = totalSeconds % 60;
1292
+ if (hours > 0) return `${hours}h${minutes}m${seconds}s`;
1293
+ if (minutes > 0) return `${minutes}m${seconds}s`;
1294
+ return `${seconds}s`;
1295
+ }
1296
+ function formatExitSummary(stats) {
1297
+ const uptime = formatUptime(Date.now() - stats.startTime);
1298
+ const tasks = stats.tasksCompleted === 1 ? "1 task" : `${stats.tasksCompleted} tasks`;
1299
+ const errors = stats.errorsEncountered === 1 ? "1 error" : `${stats.errorsEncountered} errors`;
1300
+ return `${icons.stop} Shutting down \u2014 ${tasks} completed, ${errors}, uptime ${uptime}`;
1301
+ }
1302
+
1303
+ // src/commands/agent.ts
1304
+ var DEFAULT_POLL_INTERVAL_MS = 1e4;
1305
+ var MAX_CONSECUTIVE_AUTH_ERRORS = 3;
1306
+ var MAX_POLL_BACKOFF_MS = 3e5;
976
1307
  var NON_RETRYABLE_STATUSES = /* @__PURE__ */ new Set([401, 403, 404]);
977
1308
  function toApiDiffUrl(webUrl) {
978
1309
  const match = webUrl.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:\.diff)?$/);
@@ -1012,10 +1343,10 @@ async function fetchDiff(diffUrl, githubToken, signal) {
1012
1343
  );
1013
1344
  }
1014
1345
  var MAX_DIFF_FETCH_ATTEMPTS = 3;
1015
- async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo, logger, options) {
1346
+ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, options) {
1016
1347
  const { pollIntervalMs, maxConsecutiveErrors, routerRelay, reviewOnly, repoConfig, signal } = options;
1017
1348
  const { log, logError, logWarn } = logger;
1018
- log(`Agent ${agentId} polling every ${pollIntervalMs / 1e3}s...`);
1349
+ log(`${icons.polling} Polling every ${pollIntervalMs / 1e3}s...`);
1019
1350
  let consecutiveAuthErrors = 0;
1020
1351
  let consecutiveErrors = 0;
1021
1352
  const diffFailCounts = /* @__PURE__ */ new Map();
@@ -1023,6 +1354,9 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1023
1354
  try {
1024
1355
  const pollBody = { agent_id: agentId };
1025
1356
  if (reviewOnly) pollBody.review_only = true;
1357
+ if (repoConfig?.mode === "whitelist" && repoConfig.list?.length) {
1358
+ pollBody.repos = repoConfig.list;
1359
+ }
1026
1360
  const pollResponse = await client.post("/api/tasks/poll", pollBody);
1027
1361
  consecutiveAuthErrors = 0;
1028
1362
  consecutiveErrors = 0;
@@ -1039,10 +1373,12 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1039
1373
  consumptionDeps,
1040
1374
  agentInfo,
1041
1375
  logger,
1376
+ agentSession,
1042
1377
  routerRelay,
1043
1378
  signal
1044
1379
  );
1045
1380
  if (result.diffFetchFailed) {
1381
+ agentSession.errorsEncountered++;
1046
1382
  const count = (diffFailCounts.get(task.task_id) ?? 0) + 1;
1047
1383
  diffFailCounts.set(task.task_id, count);
1048
1384
  if (count >= MAX_DIFF_FETCH_ATTEMPTS) {
@@ -1052,20 +1388,21 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1052
1388
  }
1053
1389
  } catch (err) {
1054
1390
  if (signal?.aborted) break;
1391
+ agentSession.errorsEncountered++;
1055
1392
  if (err instanceof HttpError && (err.status === 401 || err.status === 403)) {
1056
1393
  consecutiveAuthErrors++;
1057
1394
  consecutiveErrors++;
1058
1395
  logError(
1059
- `Auth error (${err.status}): ${err.message} [${consecutiveAuthErrors}/${MAX_CONSECUTIVE_AUTH_ERRORS}]`
1396
+ `${icons.error} Auth error (${err.status}): ${err.message} [${consecutiveAuthErrors}/${MAX_CONSECUTIVE_AUTH_ERRORS}]`
1060
1397
  );
1061
1398
  if (consecutiveAuthErrors >= MAX_CONSECUTIVE_AUTH_ERRORS) {
1062
- logError("Authentication failed repeatedly. Exiting.");
1399
+ logError(`${icons.error} Authentication failed repeatedly. Exiting.`);
1063
1400
  break;
1064
1401
  }
1065
1402
  } else {
1066
1403
  consecutiveAuthErrors = 0;
1067
1404
  consecutiveErrors++;
1068
- logError(`Poll error: ${err.message}`);
1405
+ logError(`${icons.error} Poll error: ${err.message}`);
1069
1406
  }
1070
1407
  if (consecutiveErrors >= maxConsecutiveErrors) {
1071
1408
  logError(
@@ -1091,11 +1428,10 @@ async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo,
1091
1428
  await sleep2(pollIntervalMs, signal);
1092
1429
  }
1093
1430
  }
1094
- async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, logger, routerRelay, signal) {
1431
+ async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, logger, agentSession, routerRelay, signal) {
1095
1432
  const { task_id, owner, repo, pr_number, diff_url, timeout_seconds, prompt, role } = task;
1096
1433
  const { log, logError, logWarn } = logger;
1097
- log(`
1098
- Task ${task_id}: PR #${pr_number} on ${owner}/${repo} (role: ${role})`);
1434
+ log(`${icons.success} Claimed task ${task_id} (${role}) \u2014 ${owner}/${repo}#${pr_number}`);
1099
1435
  log(` https://github.com/${owner}/${repo}/pull/${pr_number}`);
1100
1436
  let claimResponse;
1101
1437
  try {
@@ -1110,15 +1446,14 @@ Task ${task_id}: PR #${pr_number} on ${owner}/${repo} (role: ${role})`);
1110
1446
  signal
1111
1447
  );
1112
1448
  } catch (err) {
1113
- const status = err instanceof HttpError ? ` (${err.status})` : "";
1114
- logError(` Failed to claim task ${task_id}${status}: ${err.message}`);
1115
- return {};
1116
- }
1117
- if (!claimResponse.claimed) {
1118
- log(` Claim rejected: ${claimResponse.reason}`);
1449
+ if (err instanceof HttpError) {
1450
+ const codeInfo = err.errorCode ? ` [${err.errorCode}]` : "";
1451
+ logError(` Claim rejected${codeInfo}: ${err.message}`);
1452
+ } else {
1453
+ logError(` Failed to claim task ${task_id}: ${err.message}`);
1454
+ }
1119
1455
  return {};
1120
1456
  }
1121
- log(` Claimed as ${role}`);
1122
1457
  let diffContent;
1123
1458
  try {
1124
1459
  diffContent = await fetchDiff(diff_url, reviewDeps.githubToken, signal);
@@ -1171,6 +1506,21 @@ Task ${task_id}: PR #${pr_number} on ${owner}/${repo} (role: ${role})`);
1171
1506
  );
1172
1507
  }
1173
1508
  }
1509
+ let contextBlock;
1510
+ try {
1511
+ const prContext = await fetchPRContext(owner, repo, pr_number, {
1512
+ githubToken: reviewDeps.githubToken,
1513
+ signal
1514
+ });
1515
+ if (hasContent(prContext)) {
1516
+ contextBlock = formatPRContext(prContext, taskReviewDeps.codebaseDir);
1517
+ log(" PR context fetched");
1518
+ }
1519
+ } catch (err) {
1520
+ logWarn(
1521
+ ` Warning: failed to fetch PR context: ${err.message}. Continuing without.`
1522
+ );
1523
+ }
1174
1524
  try {
1175
1525
  if (role === "summary" && "reviews" in claimResponse && claimResponse.reviews) {
1176
1526
  await executeSummaryTask(
@@ -1188,7 +1538,8 @@ Task ${task_id}: PR #${pr_number} on ${owner}/${repo} (role: ${role})`);
1188
1538
  consumptionDeps,
1189
1539
  logger,
1190
1540
  routerRelay,
1191
- signal
1541
+ signal,
1542
+ contextBlock
1192
1543
  );
1193
1544
  } else {
1194
1545
  await executeReviewTask(
@@ -1205,15 +1556,18 @@ Task ${task_id}: PR #${pr_number} on ${owner}/${repo} (role: ${role})`);
1205
1556
  consumptionDeps,
1206
1557
  logger,
1207
1558
  routerRelay,
1208
- signal
1559
+ signal,
1560
+ contextBlock
1209
1561
  );
1210
1562
  }
1563
+ agentSession.tasksCompleted++;
1211
1564
  } catch (err) {
1565
+ agentSession.errorsEncountered++;
1212
1566
  if (err instanceof DiffTooLargeError || err instanceof InputTooLargeError) {
1213
- logError(` ${err.message}`);
1567
+ logError(` ${icons.error} ${err.message}`);
1214
1568
  await safeReject(client, task_id, agentId, err.message, logger);
1215
1569
  } else {
1216
- logError(` Error on task ${task_id}: ${err.message}`);
1570
+ logError(` ${icons.error} Error on task ${task_id}: ${err.message}`);
1217
1571
  await safeError(client, task_id, agentId, err.message, logger);
1218
1572
  }
1219
1573
  } finally {
@@ -1253,18 +1607,19 @@ async function safeError(client, taskId, agentId, error, logger) {
1253
1607
  );
1254
1608
  }
1255
1609
  }
1256
- async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviewDeps, consumptionDeps, logger, routerRelay, signal) {
1610
+ async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviewDeps, consumptionDeps, logger, routerRelay, signal, contextBlock) {
1257
1611
  let reviewText;
1258
1612
  let verdict;
1259
1613
  let tokensUsed;
1260
1614
  if (routerRelay) {
1261
- logger.log(` Executing review command: [router mode]`);
1615
+ logger.log(` ${icons.running} Executing review: [router mode]`);
1262
1616
  const fullPrompt = routerRelay.buildReviewPrompt({
1263
1617
  owner,
1264
1618
  repo,
1265
1619
  reviewMode: "full",
1266
1620
  prompt,
1267
- diffContent
1621
+ diffContent,
1622
+ contextBlock
1268
1623
  });
1269
1624
  const response = await routerRelay.sendPrompt(
1270
1625
  "review_request",
@@ -1277,7 +1632,7 @@ async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber,
1277
1632
  verdict = parsed.verdict;
1278
1633
  tokensUsed = estimateTokens(fullPrompt) + estimateTokens(response);
1279
1634
  } else {
1280
- logger.log(` Executing review command: ${reviewDeps.commandTemplate}`);
1635
+ logger.log(` ${icons.running} Executing review: ${reviewDeps.commandTemplate}`);
1281
1636
  const result = await executeReview(
1282
1637
  {
1283
1638
  taskId,
@@ -1287,7 +1642,8 @@ async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber,
1287
1642
  repo,
1288
1643
  prNumber,
1289
1644
  timeout: timeoutSeconds,
1290
- reviewMode: "full"
1645
+ reviewMode: "full",
1646
+ contextBlock
1291
1647
  },
1292
1648
  reviewDeps
1293
1649
  );
@@ -1308,22 +1664,23 @@ async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber,
1308
1664
  signal
1309
1665
  );
1310
1666
  recordSessionUsage(consumptionDeps.session, tokensUsed);
1311
- logger.log(` Review submitted (${tokensUsed.toLocaleString()} tokens)`);
1667
+ logger.log(` ${icons.success} Review submitted (${tokensUsed.toLocaleString()} tokens)`);
1312
1668
  logger.log(formatPostReviewStats(consumptionDeps.session));
1313
1669
  }
1314
- async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviews, reviewDeps, consumptionDeps, logger, routerRelay, signal) {
1670
+ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviews, reviewDeps, consumptionDeps, logger, routerRelay, signal, contextBlock) {
1315
1671
  if (reviews.length === 0) {
1316
1672
  let reviewText;
1317
1673
  let verdict;
1318
1674
  let tokensUsed2;
1319
1675
  if (routerRelay) {
1320
- logger.log(` Executing summary command: [router mode]`);
1676
+ logger.log(` ${icons.running} Executing summary: [router mode]`);
1321
1677
  const fullPrompt = routerRelay.buildReviewPrompt({
1322
1678
  owner,
1323
1679
  repo,
1324
1680
  reviewMode: "full",
1325
1681
  prompt,
1326
- diffContent
1682
+ diffContent,
1683
+ contextBlock
1327
1684
  });
1328
1685
  const response = await routerRelay.sendPrompt(
1329
1686
  "review_request",
@@ -1336,7 +1693,7 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
1336
1693
  verdict = parsed.verdict;
1337
1694
  tokensUsed2 = estimateTokens(fullPrompt) + estimateTokens(response);
1338
1695
  } else {
1339
- logger.log(` Executing summary command: ${reviewDeps.commandTemplate}`);
1696
+ logger.log(` ${icons.running} Executing summary: ${reviewDeps.commandTemplate}`);
1340
1697
  const result = await executeReview(
1341
1698
  {
1342
1699
  taskId,
@@ -1346,7 +1703,8 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
1346
1703
  repo,
1347
1704
  prNumber,
1348
1705
  timeout: timeoutSeconds,
1349
- reviewMode: "full"
1706
+ reviewMode: "full",
1707
+ contextBlock
1350
1708
  },
1351
1709
  reviewDeps
1352
1710
  );
@@ -1367,7 +1725,9 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
1367
1725
  signal
1368
1726
  );
1369
1727
  recordSessionUsage(consumptionDeps.session, tokensUsed2);
1370
- logger.log(` Review submitted as summary (${tokensUsed2.toLocaleString()} tokens)`);
1728
+ logger.log(
1729
+ ` ${icons.success} Review submitted as summary (${tokensUsed2.toLocaleString()} tokens)`
1730
+ );
1371
1731
  logger.log(formatPostReviewStats(consumptionDeps.session));
1372
1732
  return;
1373
1733
  }
@@ -1381,13 +1741,14 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
1381
1741
  let summaryText;
1382
1742
  let tokensUsed;
1383
1743
  if (routerRelay) {
1384
- logger.log(` Executing summary command: [router mode]`);
1744
+ logger.log(` ${icons.running} Executing summary: [router mode]`);
1385
1745
  const fullPrompt = routerRelay.buildSummaryPrompt({
1386
1746
  owner,
1387
1747
  repo,
1388
1748
  prompt,
1389
1749
  reviews: summaryReviews,
1390
- diffContent
1750
+ diffContent,
1751
+ contextBlock
1391
1752
  });
1392
1753
  const response = await routerRelay.sendPrompt(
1393
1754
  "summary_request",
@@ -1398,7 +1759,7 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
1398
1759
  summaryText = response;
1399
1760
  tokensUsed = estimateTokens(fullPrompt) + estimateTokens(response);
1400
1761
  } else {
1401
- logger.log(` Executing summary command: ${reviewDeps.commandTemplate}`);
1762
+ logger.log(` ${icons.running} Executing summary: ${reviewDeps.commandTemplate}`);
1402
1763
  const result = await executeSummary(
1403
1764
  {
1404
1765
  taskId,
@@ -1408,7 +1769,8 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
1408
1769
  repo,
1409
1770
  prNumber,
1410
1771
  timeout: timeoutSeconds,
1411
- diffContent
1772
+ diffContent,
1773
+ contextBlock
1412
1774
  },
1413
1775
  reviewDeps
1414
1776
  );
@@ -1427,7 +1789,7 @@ async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber
1427
1789
  signal
1428
1790
  );
1429
1791
  recordSessionUsage(consumptionDeps.session, tokensUsed);
1430
- logger.log(` Summary submitted (${tokensUsed.toLocaleString()} tokens)`);
1792
+ logger.log(` ${icons.success} Summary submitted (${tokensUsed.toLocaleString()} tokens)`);
1431
1793
  logger.log(formatPostReviewStats(consumptionDeps.session));
1432
1794
  }
1433
1795
  function sleep2(ms, signal) {
@@ -1453,31 +1815,30 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
1453
1815
  const deps = consumptionDeps ?? { agentId, session };
1454
1816
  const logger = createLogger(options?.label);
1455
1817
  const { log, logError, logWarn } = logger;
1456
- log(`Agent ${agentId} starting...`);
1457
- log(`Platform: ${platformUrl}`);
1818
+ const agentSession = createAgentSession();
1819
+ log(`${icons.start} Agent started (polling ${platformUrl})`);
1458
1820
  log(`Model: ${agentInfo.model} | Tool: ${agentInfo.tool}`);
1459
1821
  if (!reviewDeps) {
1460
- logError("No review command configured. Set command in config.yml");
1822
+ logError(`${icons.error} No review command configured. Set command in config.yml`);
1461
1823
  return;
1462
1824
  }
1463
1825
  if (reviewDeps.commandTemplate && !options?.routerRelay) {
1464
1826
  log("Testing command...");
1465
1827
  const result = await testCommand(reviewDeps.commandTemplate);
1466
1828
  if (result.ok) {
1467
- log(`Testing command... ok (${(result.elapsedMs / 1e3).toFixed(1)}s)`);
1829
+ log(`${icons.success} Command test ok (${(result.elapsedMs / 1e3).toFixed(1)}s)`);
1468
1830
  } else {
1469
- logWarn(`Warning: command test failed (${result.error}). Reviews may fail.`);
1831
+ logWarn(`${icons.warn} Command test failed (${result.error}). Reviews may fail.`);
1470
1832
  }
1471
1833
  }
1472
1834
  const abortController = new AbortController();
1473
1835
  process.on("SIGINT", () => {
1474
- log("\nShutting down...");
1475
1836
  abortController.abort();
1476
1837
  });
1477
1838
  process.on("SIGTERM", () => {
1478
1839
  abortController.abort();
1479
1840
  });
1480
- await pollLoop(client, agentId, reviewDeps, deps, agentInfo, logger, {
1841
+ await pollLoop(client, agentId, reviewDeps, deps, agentInfo, logger, agentSession, {
1481
1842
  pollIntervalMs: options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
1482
1843
  maxConsecutiveErrors: options?.maxConsecutiveErrors ?? DEFAULT_MAX_CONSECUTIVE_ERRORS,
1483
1844
  routerRelay: options?.routerRelay,
@@ -1485,7 +1846,7 @@ async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumpti
1485
1846
  repoConfig: options?.repoConfig,
1486
1847
  signal: abortController.signal
1487
1848
  });
1488
- log("Agent stopped.");
1849
+ log(formatExitSummary(agentSession));
1489
1850
  }
1490
1851
  async function startAgentRouter() {
1491
1852
  const config = loadConfig();
@@ -1661,7 +2022,7 @@ agentCommand.command("start").description("Start agents in polling mode").option
1661
2022
  });
1662
2023
 
1663
2024
  // src/index.ts
1664
- var program = new Command2().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.11.0");
2025
+ var program = new Command2().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.12.0");
1665
2026
  program.addCommand(agentCommand);
1666
2027
  program.action(() => {
1667
2028
  startAgentRouter();
package/package.json CHANGED
@@ -1,17 +1,40 @@
1
1
  {
2
2
  "name": "opencara",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
+ "description": "Distributed AI code review agent — poll, review, and submit PR reviews using your own AI tools",
4
5
  "type": "module",
6
+ "license": "MIT",
7
+ "author": "OpenCara <https://github.com/OpenCara>",
8
+ "homepage": "https://github.com/OpenCara/OpenCara#readme",
5
9
  "repository": {
6
10
  "type": "git",
7
11
  "url": "https://github.com/OpenCara/OpenCara.git",
8
12
  "directory": "packages/cli"
9
13
  },
14
+ "bugs": {
15
+ "url": "https://github.com/OpenCara/OpenCara/issues"
16
+ },
17
+ "keywords": [
18
+ "ai",
19
+ "code-review",
20
+ "github",
21
+ "pull-request",
22
+ "cli",
23
+ "agent",
24
+ "review",
25
+ "openai",
26
+ "claude",
27
+ "gemini"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
10
32
  "bin": {
11
33
  "opencara": "dist/index.js"
12
34
  },
13
35
  "files": [
14
- "dist"
36
+ "dist",
37
+ "README.md"
15
38
  ],
16
39
  "scripts": {
17
40
  "build": "tsup",
@@ -20,14 +43,15 @@
20
43
  },
21
44
  "dependencies": {
22
45
  "commander": "^13.0.0",
46
+ "picocolors": "^1.1.1",
23
47
  "yaml": "^2.7.0"
24
48
  },
25
49
  "devDependencies": {
26
- "@opencara/shared": "workspace:*",
27
- "@opencara/server": "workspace:*",
28
50
  "@cloudflare/workers-types": "^4.20250214.0",
29
- "hono": "^4.7.0",
51
+ "@opencara/server": "workspace:*",
52
+ "@opencara/shared": "workspace:*",
30
53
  "@types/node": "^22.0.0",
54
+ "hono": "^4.7.0",
31
55
  "tsup": "^8.5.1",
32
56
  "tsx": "^4.0.0"
33
57
  }