opencara 0.9.0 → 0.10.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.
- package/dist/index.js +480 -1646
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command2 } from "commander";
|
|
5
5
|
|
|
6
|
-
// src/commands/
|
|
6
|
+
// src/commands/agent.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import
|
|
8
|
+
import crypto from "crypto";
|
|
9
9
|
|
|
10
10
|
// src/config.ts
|
|
11
11
|
import * as fs from "fs";
|
|
@@ -15,10 +15,6 @@ import { parse, stringify } from "yaml";
|
|
|
15
15
|
var DEFAULT_PLATFORM_URL = "https://api.opencara.dev";
|
|
16
16
|
var CONFIG_DIR = path.join(os.homedir(), ".opencara");
|
|
17
17
|
var CONFIG_FILE = process.env.OPENCARA_CONFIG && process.env.OPENCARA_CONFIG.trim() ? path.resolve(process.env.OPENCARA_CONFIG) : path.join(CONFIG_DIR, "config.yml");
|
|
18
|
-
function ensureConfigDir() {
|
|
19
|
-
const dir = path.dirname(CONFIG_FILE);
|
|
20
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
21
|
-
}
|
|
22
18
|
var DEFAULT_MAX_DIFF_SIZE_KB = 100;
|
|
23
19
|
function parseLimits(data) {
|
|
24
20
|
const raw = data.limits;
|
|
@@ -74,44 +70,6 @@ function parseRepoConfig(obj, index) {
|
|
|
74
70
|
}
|
|
75
71
|
return config;
|
|
76
72
|
}
|
|
77
|
-
function parseAnonymousAgents(data) {
|
|
78
|
-
const raw = data.anonymous_agents;
|
|
79
|
-
if (!Array.isArray(raw)) return [];
|
|
80
|
-
const entries = [];
|
|
81
|
-
for (let i = 0; i < raw.length; i++) {
|
|
82
|
-
const entry = raw[i];
|
|
83
|
-
if (!entry || typeof entry !== "object") {
|
|
84
|
-
console.warn(`Warning: anonymous_agents[${i}] is not an object, skipping`);
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
const obj = entry;
|
|
88
|
-
if (typeof obj.agent_id !== "string" || typeof obj.api_key !== "string" || typeof obj.model !== "string" || typeof obj.tool !== "string") {
|
|
89
|
-
console.warn(
|
|
90
|
-
`Warning: anonymous_agents[${i}] missing required agent_id/api_key/model/tool fields, skipping`
|
|
91
|
-
);
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
const anon = {
|
|
95
|
-
agentId: obj.agent_id,
|
|
96
|
-
apiKey: obj.api_key,
|
|
97
|
-
model: obj.model,
|
|
98
|
-
tool: obj.tool
|
|
99
|
-
};
|
|
100
|
-
if (typeof obj.name === "string") anon.name = obj.name;
|
|
101
|
-
if (obj.repo_config && typeof obj.repo_config === "object") {
|
|
102
|
-
const rc = obj.repo_config;
|
|
103
|
-
if (typeof rc.mode === "string" && VALID_REPO_MODES.includes(rc.mode)) {
|
|
104
|
-
const repoConfig = { mode: rc.mode };
|
|
105
|
-
if (Array.isArray(rc.list)) {
|
|
106
|
-
repoConfig.list = rc.list.filter((v) => typeof v === "string");
|
|
107
|
-
}
|
|
108
|
-
anon.repoConfig = repoConfig;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
entries.push(anon);
|
|
112
|
-
}
|
|
113
|
-
return entries;
|
|
114
|
-
}
|
|
115
73
|
function parseAgents(data) {
|
|
116
74
|
if (!("agents" in data)) return null;
|
|
117
75
|
const raw = data.agents;
|
|
@@ -132,6 +90,7 @@ function parseAgents(data) {
|
|
|
132
90
|
if (typeof obj.name === "string") agent.name = obj.name;
|
|
133
91
|
if (typeof obj.command === "string") agent.command = obj.command;
|
|
134
92
|
if (obj.router === true) agent.router = true;
|
|
93
|
+
if (obj.review_only === true) agent.review_only = true;
|
|
135
94
|
const agentLimits = parseLimits(obj);
|
|
136
95
|
if (agentLimits) agent.limits = agentLimits;
|
|
137
96
|
const repoConfig = parseRepoConfig(obj, i);
|
|
@@ -147,8 +106,7 @@ function loadConfig() {
|
|
|
147
106
|
maxDiffSizeKb: DEFAULT_MAX_DIFF_SIZE_KB,
|
|
148
107
|
limits: null,
|
|
149
108
|
agentCommand: null,
|
|
150
|
-
agents: null
|
|
151
|
-
anonymousAgents: []
|
|
109
|
+
agents: null
|
|
152
110
|
};
|
|
153
111
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
154
112
|
return defaults;
|
|
@@ -164,48 +122,8 @@ function loadConfig() {
|
|
|
164
122
|
maxDiffSizeKb: typeof data.max_diff_size_kb === "number" ? data.max_diff_size_kb : DEFAULT_MAX_DIFF_SIZE_KB,
|
|
165
123
|
limits: parseLimits(data),
|
|
166
124
|
agentCommand: typeof data.agent_command === "string" ? data.agent_command : null,
|
|
167
|
-
agents: parseAgents(data)
|
|
168
|
-
anonymousAgents: parseAnonymousAgents(data)
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
function saveConfig(config) {
|
|
172
|
-
ensureConfigDir();
|
|
173
|
-
const data = {
|
|
174
|
-
platform_url: config.platformUrl
|
|
125
|
+
agents: parseAgents(data)
|
|
175
126
|
};
|
|
176
|
-
if (config.apiKey) {
|
|
177
|
-
data.api_key = config.apiKey;
|
|
178
|
-
}
|
|
179
|
-
if (config.maxDiffSizeKb !== DEFAULT_MAX_DIFF_SIZE_KB) {
|
|
180
|
-
data.max_diff_size_kb = config.maxDiffSizeKb;
|
|
181
|
-
}
|
|
182
|
-
if (config.limits) {
|
|
183
|
-
data.limits = config.limits;
|
|
184
|
-
}
|
|
185
|
-
if (config.agentCommand) {
|
|
186
|
-
data.agent_command = config.agentCommand;
|
|
187
|
-
}
|
|
188
|
-
if (config.agents !== null) {
|
|
189
|
-
data.agents = config.agents;
|
|
190
|
-
}
|
|
191
|
-
if (config.anonymousAgents.length > 0) {
|
|
192
|
-
data.anonymous_agents = config.anonymousAgents.map((a) => {
|
|
193
|
-
const entry = {
|
|
194
|
-
agent_id: a.agentId,
|
|
195
|
-
api_key: a.apiKey,
|
|
196
|
-
model: a.model,
|
|
197
|
-
tool: a.tool
|
|
198
|
-
};
|
|
199
|
-
if (a.name) {
|
|
200
|
-
entry.name = a.name;
|
|
201
|
-
}
|
|
202
|
-
if (a.repoConfig) {
|
|
203
|
-
entry.repo_config = a.repoConfig;
|
|
204
|
-
}
|
|
205
|
-
return entry;
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
fs.writeFileSync(CONFIG_FILE, stringify(data), { encoding: "utf-8", mode: 384 });
|
|
209
127
|
}
|
|
210
128
|
function resolveAgentLimits(agentLimits, globalLimits) {
|
|
211
129
|
if (!agentLimits && !globalLimits) return null;
|
|
@@ -214,19 +132,6 @@ function resolveAgentLimits(agentLimits, globalLimits) {
|
|
|
214
132
|
const merged = { ...globalLimits, ...agentLimits };
|
|
215
133
|
return Object.keys(merged).length === 0 ? null : merged;
|
|
216
134
|
}
|
|
217
|
-
function findAnonymousAgent(config, agentId) {
|
|
218
|
-
return config.anonymousAgents.find((a) => a.agentId === agentId) ?? null;
|
|
219
|
-
}
|
|
220
|
-
function removeAnonymousAgent(config, agentId) {
|
|
221
|
-
config.anonymousAgents = config.anonymousAgents.filter((a) => a.agentId !== agentId);
|
|
222
|
-
}
|
|
223
|
-
function requireApiKey(config) {
|
|
224
|
-
if (!config.apiKey) {
|
|
225
|
-
console.error("Not authenticated. Run `opencara login` first.");
|
|
226
|
-
process.exit(1);
|
|
227
|
-
}
|
|
228
|
-
return config.apiKey;
|
|
229
|
-
}
|
|
230
135
|
|
|
231
136
|
// src/http.ts
|
|
232
137
|
var HttpError = class extends Error {
|
|
@@ -237,9 +142,14 @@ var HttpError = class extends Error {
|
|
|
237
142
|
}
|
|
238
143
|
};
|
|
239
144
|
var ApiClient = class {
|
|
240
|
-
constructor(baseUrl, apiKey = null) {
|
|
145
|
+
constructor(baseUrl, apiKey = null, debug) {
|
|
241
146
|
this.baseUrl = baseUrl;
|
|
242
147
|
this.apiKey = apiKey;
|
|
148
|
+
this.debug = debug ?? process.env.OPENCARA_DEBUG === "1";
|
|
149
|
+
}
|
|
150
|
+
debug;
|
|
151
|
+
log(msg) {
|
|
152
|
+
if (this.debug) console.debug(`[ApiClient] ${msg}`);
|
|
243
153
|
}
|
|
244
154
|
headers() {
|
|
245
155
|
const h = {
|
|
@@ -251,245 +161,78 @@ var ApiClient = class {
|
|
|
251
161
|
return h;
|
|
252
162
|
}
|
|
253
163
|
async get(path3) {
|
|
164
|
+
this.log(`GET ${path3}`);
|
|
254
165
|
const res = await fetch(`${this.baseUrl}${path3}`, {
|
|
255
166
|
method: "GET",
|
|
256
167
|
headers: this.headers()
|
|
257
168
|
});
|
|
258
|
-
return this.handleResponse(res);
|
|
169
|
+
return this.handleResponse(res, path3);
|
|
259
170
|
}
|
|
260
171
|
async post(path3, body) {
|
|
172
|
+
this.log(`POST ${path3}`);
|
|
261
173
|
const res = await fetch(`${this.baseUrl}${path3}`, {
|
|
262
174
|
method: "POST",
|
|
263
175
|
headers: this.headers(),
|
|
264
176
|
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
265
177
|
});
|
|
266
|
-
return this.handleResponse(res);
|
|
178
|
+
return this.handleResponse(res, path3);
|
|
267
179
|
}
|
|
268
|
-
async handleResponse(res) {
|
|
180
|
+
async handleResponse(res, path3) {
|
|
269
181
|
if (!res.ok) {
|
|
270
|
-
if (res.status === 401) {
|
|
271
|
-
throw new HttpError(401, "Not authenticated. Run `opencara login` first.");
|
|
272
|
-
}
|
|
273
182
|
let message = `HTTP ${res.status}`;
|
|
274
183
|
try {
|
|
275
184
|
const body = await res.json();
|
|
276
185
|
if (body.error) message = body.error;
|
|
277
186
|
} catch {
|
|
278
187
|
}
|
|
188
|
+
this.log(`${res.status} ${message} (${path3})`);
|
|
279
189
|
throw new HttpError(res.status, message);
|
|
280
190
|
}
|
|
191
|
+
this.log(`${res.status} OK (${path3})`);
|
|
281
192
|
return await res.json();
|
|
282
193
|
}
|
|
283
194
|
};
|
|
284
195
|
|
|
285
|
-
// src/
|
|
286
|
-
var
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
jitter: true
|
|
196
|
+
// src/retry.ts
|
|
197
|
+
var DEFAULT_RETRY = {
|
|
198
|
+
maxAttempts: 3,
|
|
199
|
+
baseDelayMs: 1e3,
|
|
200
|
+
maxDelayMs: 3e4
|
|
291
201
|
};
|
|
292
|
-
function
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (options.jitter) {
|
|
298
|
-
return base + Math.random() * 500;
|
|
299
|
-
}
|
|
300
|
-
return base;
|
|
301
|
-
}
|
|
302
|
-
function sleep(ms) {
|
|
303
|
-
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// src/commands/login.ts
|
|
307
|
-
function promptYesNo(question) {
|
|
308
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
309
|
-
return new Promise((resolve2) => {
|
|
310
|
-
rl.question(question, (answer) => {
|
|
311
|
-
rl.close();
|
|
312
|
-
const normalized = answer.trim().toLowerCase();
|
|
313
|
-
resolve2(normalized === "" || normalized === "y" || normalized === "yes");
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
var loginCommand = new Command("login").description("Authenticate with GitHub via device flow").action(async () => {
|
|
318
|
-
const config = loadConfig();
|
|
319
|
-
const client = new ApiClient(config.platformUrl);
|
|
320
|
-
let flow;
|
|
321
|
-
try {
|
|
322
|
-
flow = await client.post("/auth/device");
|
|
323
|
-
} catch (err) {
|
|
324
|
-
console.error("Failed to start device flow:", err instanceof Error ? err.message : err);
|
|
325
|
-
process.exit(1);
|
|
326
|
-
}
|
|
327
|
-
console.log();
|
|
328
|
-
console.log("To sign in, open this URL in your browser:");
|
|
329
|
-
console.log(` ${flow.verificationUri}`);
|
|
330
|
-
console.log();
|
|
331
|
-
console.log(`And enter this code: ${flow.userCode}`);
|
|
332
|
-
console.log();
|
|
333
|
-
console.log("Waiting for authorization...");
|
|
334
|
-
const intervalMs = flow.interval * 1e3;
|
|
335
|
-
const deadline = Date.now() + flow.expiresIn * 1e3;
|
|
336
|
-
while (Date.now() < deadline) {
|
|
337
|
-
await sleep(intervalMs);
|
|
338
|
-
let tokenRes;
|
|
202
|
+
async function withRetry(fn, options = {}, signal) {
|
|
203
|
+
const opts = { ...DEFAULT_RETRY, ...options };
|
|
204
|
+
let lastError;
|
|
205
|
+
for (let attempt = 0; attempt < opts.maxAttempts; attempt++) {
|
|
206
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
339
207
|
try {
|
|
340
|
-
|
|
341
|
-
deviceCode: flow.deviceCode
|
|
342
|
-
});
|
|
208
|
+
return await fn();
|
|
343
209
|
} catch (err) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
process.stdout.write(".");
|
|
349
|
-
continue;
|
|
350
|
-
}
|
|
351
|
-
if (tokenRes.status === "expired") {
|
|
352
|
-
console.error("\nDevice code expired. Please run `opencara login` again.");
|
|
353
|
-
process.exit(1);
|
|
354
|
-
}
|
|
355
|
-
if (tokenRes.status === "complete") {
|
|
356
|
-
config.apiKey = tokenRes.apiKey;
|
|
357
|
-
saveConfig(config);
|
|
358
|
-
console.log("\nLogged in successfully. API key saved to ~/.opencara/config.yml");
|
|
359
|
-
if (config.anonymousAgents.length > 0 && process.stdin.isTTY) {
|
|
360
|
-
console.log();
|
|
361
|
-
console.log(`Found ${config.anonymousAgents.length} anonymous agent(s):`);
|
|
362
|
-
for (const anon of config.anonymousAgents) {
|
|
363
|
-
console.log(` - ${anon.agentId} (${anon.model} / ${anon.tool})`);
|
|
364
|
-
}
|
|
365
|
-
const shouldLink = await promptYesNo("Link to your GitHub account? [Y/n] ");
|
|
366
|
-
if (shouldLink) {
|
|
367
|
-
const authedClient = new ApiClient(config.platformUrl, tokenRes.apiKey);
|
|
368
|
-
let linkedCount = 0;
|
|
369
|
-
const toRemove = [];
|
|
370
|
-
for (const anon of config.anonymousAgents) {
|
|
371
|
-
try {
|
|
372
|
-
await authedClient.post("/api/account/link", {
|
|
373
|
-
anonymousApiKey: anon.apiKey
|
|
374
|
-
});
|
|
375
|
-
toRemove.push(anon.agentId);
|
|
376
|
-
linkedCount++;
|
|
377
|
-
} catch (err) {
|
|
378
|
-
console.error(
|
|
379
|
-
`Failed to link agent ${anon.agentId}:`,
|
|
380
|
-
err instanceof Error ? err.message : err
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
for (const id of toRemove) {
|
|
385
|
-
removeAnonymousAgent(config, id);
|
|
386
|
-
}
|
|
387
|
-
saveConfig(config);
|
|
388
|
-
if (linkedCount > 0) {
|
|
389
|
-
console.log(`Linked ${linkedCount} agent(s) to your account.`);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
210
|
+
lastError = err;
|
|
211
|
+
if (attempt < opts.maxAttempts - 1) {
|
|
212
|
+
const delay = Math.min(opts.baseDelayMs * Math.pow(2, attempt), opts.maxDelayMs);
|
|
213
|
+
await sleep(delay, signal);
|
|
392
214
|
}
|
|
393
|
-
return;
|
|
394
215
|
}
|
|
395
216
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
import crypto2 from "crypto";
|
|
404
|
-
|
|
405
|
-
// ../shared/dist/api.js
|
|
406
|
-
var DEFAULT_REGISTRY = {
|
|
407
|
-
tools: [
|
|
408
|
-
{
|
|
409
|
-
name: "claude",
|
|
410
|
-
displayName: "Claude",
|
|
411
|
-
binary: "claude",
|
|
412
|
-
commandTemplate: "claude --model ${MODEL} --allowedTools '*' --print",
|
|
413
|
-
tokenParser: "claude"
|
|
414
|
-
},
|
|
415
|
-
{
|
|
416
|
-
name: "codex",
|
|
417
|
-
displayName: "Codex",
|
|
418
|
-
binary: "codex",
|
|
419
|
-
commandTemplate: "codex --model ${MODEL} exec",
|
|
420
|
-
tokenParser: "codex"
|
|
421
|
-
},
|
|
422
|
-
{
|
|
423
|
-
name: "gemini",
|
|
424
|
-
displayName: "Gemini",
|
|
425
|
-
binary: "gemini",
|
|
426
|
-
commandTemplate: "gemini -m ${MODEL}",
|
|
427
|
-
tokenParser: "gemini"
|
|
428
|
-
},
|
|
429
|
-
{
|
|
430
|
-
name: "qwen",
|
|
431
|
-
displayName: "Qwen",
|
|
432
|
-
binary: "qwen",
|
|
433
|
-
commandTemplate: "qwen --model ${MODEL} -y",
|
|
434
|
-
tokenParser: "qwen"
|
|
435
|
-
}
|
|
436
|
-
],
|
|
437
|
-
models: [
|
|
438
|
-
{
|
|
439
|
-
name: "claude-opus-4-6",
|
|
440
|
-
displayName: "Claude Opus 4.6",
|
|
441
|
-
tools: ["claude"],
|
|
442
|
-
defaultReputation: 0.8
|
|
443
|
-
},
|
|
444
|
-
{
|
|
445
|
-
name: "claude-opus-4-6[1m]",
|
|
446
|
-
displayName: "Claude Opus 4.6 (1M context)",
|
|
447
|
-
tools: ["claude"],
|
|
448
|
-
defaultReputation: 0.8
|
|
449
|
-
},
|
|
450
|
-
{
|
|
451
|
-
name: "claude-sonnet-4-6",
|
|
452
|
-
displayName: "Claude Sonnet 4.6",
|
|
453
|
-
tools: ["claude"],
|
|
454
|
-
defaultReputation: 0.7
|
|
455
|
-
},
|
|
456
|
-
{
|
|
457
|
-
name: "claude-sonnet-4-6[1m]",
|
|
458
|
-
displayName: "Claude Sonnet 4.6 (1M context)",
|
|
459
|
-
tools: ["claude"],
|
|
460
|
-
defaultReputation: 0.7
|
|
461
|
-
},
|
|
462
|
-
{
|
|
463
|
-
name: "gpt-5-codex",
|
|
464
|
-
displayName: "GPT-5 Codex",
|
|
465
|
-
tools: ["codex"],
|
|
466
|
-
defaultReputation: 0.7
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
name: "gemini-2.5-pro",
|
|
470
|
-
displayName: "Gemini 2.5 Pro",
|
|
471
|
-
tools: ["gemini"],
|
|
472
|
-
defaultReputation: 0.7
|
|
473
|
-
},
|
|
474
|
-
{
|
|
475
|
-
name: "qwen3.5-plus",
|
|
476
|
-
displayName: "Qwen 3.5 Plus",
|
|
477
|
-
tools: ["qwen"],
|
|
478
|
-
defaultReputation: 0.6
|
|
479
|
-
},
|
|
480
|
-
{ name: "glm-5", displayName: "GLM-5", tools: ["qwen"], defaultReputation: 0.5 },
|
|
481
|
-
{ name: "kimi-k2.5", displayName: "Kimi K2.5", tools: ["qwen"], defaultReputation: 0.5 },
|
|
482
|
-
{
|
|
483
|
-
name: "minimax-m2.5",
|
|
484
|
-
displayName: "Minimax M2.5",
|
|
485
|
-
tools: ["qwen"],
|
|
486
|
-
defaultReputation: 0.5
|
|
217
|
+
throw lastError;
|
|
218
|
+
}
|
|
219
|
+
function sleep(ms, signal) {
|
|
220
|
+
return new Promise((resolve2) => {
|
|
221
|
+
if (signal?.aborted) {
|
|
222
|
+
resolve2();
|
|
223
|
+
return;
|
|
487
224
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
225
|
+
const timer = setTimeout(resolve2, ms);
|
|
226
|
+
signal?.addEventListener(
|
|
227
|
+
"abort",
|
|
228
|
+
() => {
|
|
229
|
+
clearTimeout(timer);
|
|
230
|
+
resolve2();
|
|
231
|
+
},
|
|
232
|
+
{ once: true }
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
493
236
|
|
|
494
237
|
// src/tool-executor.ts
|
|
495
238
|
import { spawn, execFileSync } from "child_process";
|
|
@@ -560,14 +303,6 @@ function parseCommandTemplate(template, vars = {}) {
|
|
|
560
303
|
}
|
|
561
304
|
return { command: interpolated[0], args: interpolated.slice(1) };
|
|
562
305
|
}
|
|
563
|
-
function resolveCommandTemplate(agentCommand2) {
|
|
564
|
-
if (agentCommand2) {
|
|
565
|
-
return agentCommand2;
|
|
566
|
-
}
|
|
567
|
-
throw new Error(
|
|
568
|
-
"No command configured for this agent. Set command in ~/.opencara/config.yml agents section or run `opencara agent create`."
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
306
|
var CHARS_PER_TOKEN = 4;
|
|
572
307
|
function estimateTokens(text) {
|
|
573
308
|
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
@@ -741,6 +476,7 @@ function extractVerdict(text) {
|
|
|
741
476
|
const review = (before + after).replace(/\n{3,}/g, "\n\n").trim();
|
|
742
477
|
return { verdict: verdictStr, review };
|
|
743
478
|
}
|
|
479
|
+
console.warn("No verdict found in review output, defaulting to COMMENT");
|
|
744
480
|
return { verdict: "comment", review: text };
|
|
745
481
|
}
|
|
746
482
|
async function executeReview(req, deps, runTool = executeTool) {
|
|
@@ -900,7 +636,7 @@ ${userMessage}`;
|
|
|
900
636
|
}
|
|
901
637
|
|
|
902
638
|
// src/router.ts
|
|
903
|
-
import * as
|
|
639
|
+
import * as readline from "readline";
|
|
904
640
|
var END_OF_RESPONSE = "<<<OPENCARA_END_RESPONSE>>>";
|
|
905
641
|
var RouterRelay = class {
|
|
906
642
|
pending = null;
|
|
@@ -918,7 +654,7 @@ var RouterRelay = class {
|
|
|
918
654
|
/** Start listening for stdin input */
|
|
919
655
|
start() {
|
|
920
656
|
this.stopped = false;
|
|
921
|
-
this.rl =
|
|
657
|
+
this.rl = readline.createInterface({
|
|
922
658
|
input: this.stdin,
|
|
923
659
|
terminal: false
|
|
924
660
|
});
|
|
@@ -928,15 +664,16 @@ var RouterRelay = class {
|
|
|
928
664
|
this.rl.on("close", () => {
|
|
929
665
|
if (this.stopped) return;
|
|
930
666
|
if (this.pending) {
|
|
931
|
-
const response = this.responseLines.join("\n");
|
|
667
|
+
const response = this.responseLines.join("\n").trim();
|
|
932
668
|
this.responseLines = [];
|
|
933
669
|
clearTimeout(this.pending.timer);
|
|
934
670
|
const task = this.pending;
|
|
935
671
|
this.pending = null;
|
|
936
|
-
if (response.
|
|
672
|
+
if (response.length >= 100) {
|
|
673
|
+
console.warn("Router stdin closed \u2014 accepting partial response");
|
|
937
674
|
task.resolve(response);
|
|
938
675
|
} else {
|
|
939
|
-
task.reject(new Error("stdin closed with
|
|
676
|
+
task.reject(new Error("Router process died (stdin closed with insufficient response)"));
|
|
940
677
|
}
|
|
941
678
|
}
|
|
942
679
|
});
|
|
@@ -957,7 +694,11 @@ var RouterRelay = class {
|
|
|
957
694
|
}
|
|
958
695
|
/** Write the prompt as plain text to stdout */
|
|
959
696
|
writePrompt(prompt) {
|
|
960
|
-
|
|
697
|
+
try {
|
|
698
|
+
this.stdout.write(prompt + "\n");
|
|
699
|
+
} catch (err) {
|
|
700
|
+
throw new Error(`Failed to write to router: ${err.message}`);
|
|
701
|
+
}
|
|
961
702
|
}
|
|
962
703
|
/** Write a status message to stderr (doesn't interfere with prompt/response on stdout/stdin) */
|
|
963
704
|
writeStatus(message) {
|
|
@@ -1001,7 +742,14 @@ ${userMessage}`;
|
|
|
1001
742
|
reject(new RouterTimeoutError(`Response timeout (${timeoutSec}s)`));
|
|
1002
743
|
}, timeoutMs);
|
|
1003
744
|
this.pending = { resolve: resolve2, reject, timer };
|
|
1004
|
-
|
|
745
|
+
try {
|
|
746
|
+
this.writePrompt(prompt);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
clearTimeout(timer);
|
|
749
|
+
this.pending = null;
|
|
750
|
+
this.responseLines = [];
|
|
751
|
+
reject(err);
|
|
752
|
+
}
|
|
1005
753
|
});
|
|
1006
754
|
}
|
|
1007
755
|
/** Parse a review response: extract verdict and review text */
|
|
@@ -1016,11 +764,15 @@ ${userMessage}`;
|
|
|
1016
764
|
handleLine(line) {
|
|
1017
765
|
if (!this.pending) return;
|
|
1018
766
|
if (line.trim() === END_OF_RESPONSE) {
|
|
1019
|
-
const response = this.responseLines.join("\n");
|
|
767
|
+
const response = this.responseLines.join("\n").trim();
|
|
1020
768
|
this.responseLines = [];
|
|
1021
769
|
clearTimeout(this.pending.timer);
|
|
1022
770
|
const task = this.pending;
|
|
1023
771
|
this.pending = null;
|
|
772
|
+
if (!response || response.length < 10) {
|
|
773
|
+
task.reject(new Error("Router returned empty or trivially short response"));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
1024
776
|
task.resolve(response);
|
|
1025
777
|
return;
|
|
1026
778
|
}
|
|
@@ -1035,9 +787,6 @@ var RouterTimeoutError = class extends Error {
|
|
|
1035
787
|
};
|
|
1036
788
|
|
|
1037
789
|
// src/consumption.ts
|
|
1038
|
-
async function checkConsumptionLimits(_agentId, _limits) {
|
|
1039
|
-
return { allowed: true };
|
|
1040
|
-
}
|
|
1041
790
|
function createSessionTracker() {
|
|
1042
791
|
return { tokens: 0, reviews: 0 };
|
|
1043
792
|
}
|
|
@@ -1045,1382 +794,467 @@ function recordSessionUsage(session, tokensUsed) {
|
|
|
1045
794
|
session.tokens += tokensUsed;
|
|
1046
795
|
session.reviews += 1;
|
|
1047
796
|
}
|
|
1048
|
-
function formatPostReviewStats(
|
|
797
|
+
function formatPostReviewStats(session) {
|
|
1049
798
|
return ` Session: ${session.tokens.toLocaleString()} tokens / ${session.reviews} reviews`;
|
|
1050
799
|
}
|
|
1051
800
|
|
|
1052
801
|
// src/commands/agent.ts
|
|
1053
|
-
var
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
"Tool".padEnd(16),
|
|
1064
|
-
"Status".padEnd(10),
|
|
1065
|
-
"Trust"
|
|
1066
|
-
].join("");
|
|
1067
|
-
console.log(header);
|
|
1068
|
-
for (const a of agents) {
|
|
1069
|
-
const trust = trustLabels?.get(a.id) ?? "--";
|
|
1070
|
-
const name = a.displayName ?? "--";
|
|
1071
|
-
console.log(
|
|
1072
|
-
[
|
|
1073
|
-
a.id.padEnd(38),
|
|
1074
|
-
name.padEnd(20),
|
|
1075
|
-
a.model.padEnd(22),
|
|
1076
|
-
a.tool.padEnd(16),
|
|
1077
|
-
a.status.padEnd(10),
|
|
1078
|
-
trust
|
|
1079
|
-
].join("")
|
|
1080
|
-
);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
function buildWsUrl(platformUrl, agentId, apiKey) {
|
|
1084
|
-
return platformUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://") + `/ws/agent/${agentId}?token=${encodeURIComponent(apiKey)}`;
|
|
1085
|
-
}
|
|
1086
|
-
var HEARTBEAT_TIMEOUT_MS = 9e4;
|
|
1087
|
-
var STABILITY_THRESHOLD_MIN_MS = 5e3;
|
|
1088
|
-
var STABILITY_THRESHOLD_MAX_MS = 3e5;
|
|
1089
|
-
var WS_PING_INTERVAL_MS = 2e4;
|
|
1090
|
-
function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, options) {
|
|
1091
|
-
const verbose = options?.verbose ?? false;
|
|
1092
|
-
const stabilityThreshold = options?.stabilityThresholdMs ?? CONNECTION_STABILITY_THRESHOLD_MS;
|
|
1093
|
-
const repoConfig = options?.repoConfig;
|
|
1094
|
-
const displayName = options?.displayName;
|
|
1095
|
-
const routerRelay = options?.routerRelay;
|
|
1096
|
-
const prefix = options?.label ? `[${options.label}]` : "";
|
|
1097
|
-
const log = (...args) => console.log(...prefix ? [prefix, ...args] : args);
|
|
1098
|
-
const logError = (...args) => console.error(...prefix ? [prefix, ...args] : args);
|
|
1099
|
-
let attempt = 0;
|
|
1100
|
-
let intentionalClose = false;
|
|
1101
|
-
let heartbeatTimer = null;
|
|
1102
|
-
let wsPingTimer = null;
|
|
1103
|
-
let currentWs = null;
|
|
1104
|
-
let connectionOpenedAt = null;
|
|
1105
|
-
let stabilityTimer = null;
|
|
1106
|
-
function clearHeartbeatTimer() {
|
|
1107
|
-
if (heartbeatTimer) {
|
|
1108
|
-
clearTimeout(heartbeatTimer);
|
|
1109
|
-
heartbeatTimer = null;
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
function clearStabilityTimer() {
|
|
1113
|
-
if (stabilityTimer) {
|
|
1114
|
-
clearTimeout(stabilityTimer);
|
|
1115
|
-
stabilityTimer = null;
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
function clearWsPingTimer() {
|
|
1119
|
-
if (wsPingTimer) {
|
|
1120
|
-
clearInterval(wsPingTimer);
|
|
1121
|
-
wsPingTimer = null;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
function shutdown() {
|
|
1125
|
-
intentionalClose = true;
|
|
1126
|
-
clearHeartbeatTimer();
|
|
1127
|
-
clearStabilityTimer();
|
|
1128
|
-
clearWsPingTimer();
|
|
1129
|
-
if (currentWs) currentWs.close();
|
|
1130
|
-
log("Disconnected.");
|
|
1131
|
-
process.exit(0);
|
|
1132
|
-
}
|
|
1133
|
-
process.once("SIGINT", shutdown);
|
|
1134
|
-
process.once("SIGTERM", shutdown);
|
|
1135
|
-
function connect() {
|
|
1136
|
-
const url = buildWsUrl(platformUrl, agentId, apiKey);
|
|
1137
|
-
const ws = new WebSocket(url);
|
|
1138
|
-
currentWs = ws;
|
|
1139
|
-
function resetHeartbeatTimer() {
|
|
1140
|
-
clearHeartbeatTimer();
|
|
1141
|
-
heartbeatTimer = setTimeout(() => {
|
|
1142
|
-
log("No heartbeat received in 90s. Reconnecting...");
|
|
1143
|
-
ws.terminate();
|
|
1144
|
-
}, HEARTBEAT_TIMEOUT_MS);
|
|
1145
|
-
}
|
|
1146
|
-
ws.on("open", () => {
|
|
1147
|
-
connectionOpenedAt = Date.now();
|
|
1148
|
-
log("Connected to platform.");
|
|
1149
|
-
resetHeartbeatTimer();
|
|
1150
|
-
clearWsPingTimer();
|
|
1151
|
-
wsPingTimer = setInterval(() => {
|
|
1152
|
-
try {
|
|
1153
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1154
|
-
ws.ping();
|
|
1155
|
-
}
|
|
1156
|
-
} catch {
|
|
1157
|
-
}
|
|
1158
|
-
}, WS_PING_INTERVAL_MS);
|
|
1159
|
-
if (verbose) {
|
|
1160
|
-
log(`[verbose] Connection opened at ${new Date(connectionOpenedAt).toISOString()}`);
|
|
802
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
803
|
+
var MAX_CONSECUTIVE_AUTH_ERRORS = 3;
|
|
804
|
+
var MAX_POLL_BACKOFF_MS = 3e5;
|
|
805
|
+
async function fetchDiff(diffUrl, signal) {
|
|
806
|
+
const patchUrl = diffUrl.endsWith(".diff") ? diffUrl : `${diffUrl}.diff`;
|
|
807
|
+
return withRetry(
|
|
808
|
+
async () => {
|
|
809
|
+
const response = await fetch(patchUrl);
|
|
810
|
+
if (!response.ok) {
|
|
811
|
+
throw new Error(`Failed to fetch diff: ${response.status} ${response.statusText}`);
|
|
1161
812
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
`[verbose] Connection stable for ${stabilityThreshold / 1e3}s \u2014 resetting reconnect counter`
|
|
1167
|
-
);
|
|
1168
|
-
}
|
|
1169
|
-
attempt = 0;
|
|
1170
|
-
}, stabilityThreshold);
|
|
1171
|
-
});
|
|
1172
|
-
ws.on("message", (data) => {
|
|
1173
|
-
let msg;
|
|
1174
|
-
try {
|
|
1175
|
-
msg = JSON.parse(data.toString());
|
|
1176
|
-
} catch {
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
handleMessage(
|
|
1180
|
-
ws,
|
|
1181
|
-
msg,
|
|
1182
|
-
resetHeartbeatTimer,
|
|
1183
|
-
reviewDeps,
|
|
1184
|
-
consumptionDeps,
|
|
1185
|
-
verbose,
|
|
1186
|
-
repoConfig,
|
|
1187
|
-
displayName,
|
|
1188
|
-
prefix,
|
|
1189
|
-
routerRelay
|
|
1190
|
-
);
|
|
1191
|
-
});
|
|
1192
|
-
ws.on("close", (code, reason) => {
|
|
1193
|
-
if (intentionalClose) return;
|
|
1194
|
-
if (ws !== currentWs) return;
|
|
1195
|
-
clearHeartbeatTimer();
|
|
1196
|
-
clearStabilityTimer();
|
|
1197
|
-
clearWsPingTimer();
|
|
1198
|
-
if (connectionOpenedAt) {
|
|
1199
|
-
const lifetimeMs = Date.now() - connectionOpenedAt;
|
|
1200
|
-
const lifetimeSec = (lifetimeMs / 1e3).toFixed(1);
|
|
1201
|
-
log(
|
|
1202
|
-
`Disconnected (code=${code}, reason=${reason.toString()}). Connection was alive for ${lifetimeSec}s.`
|
|
1203
|
-
);
|
|
1204
|
-
} else {
|
|
1205
|
-
log(`Disconnected (code=${code}, reason=${reason.toString()}).`);
|
|
1206
|
-
}
|
|
1207
|
-
if (code === 4002) {
|
|
1208
|
-
log("Connection replaced by server \u2014 not reconnecting.");
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
connectionOpenedAt = null;
|
|
1212
|
-
reconnect();
|
|
1213
|
-
});
|
|
1214
|
-
ws.on("pong", () => {
|
|
1215
|
-
if (verbose) {
|
|
1216
|
-
log(`[verbose] WS pong received at ${(/* @__PURE__ */ new Date()).toISOString()}`);
|
|
1217
|
-
}
|
|
1218
|
-
});
|
|
1219
|
-
ws.on("error", (err) => {
|
|
1220
|
-
logError(`WebSocket error: ${err.message}`);
|
|
1221
|
-
});
|
|
1222
|
-
}
|
|
1223
|
-
async function reconnect() {
|
|
1224
|
-
const delay = calculateDelay(attempt, DEFAULT_RECONNECT_OPTIONS);
|
|
1225
|
-
const delaySec = (delay / 1e3).toFixed(1);
|
|
1226
|
-
attempt++;
|
|
1227
|
-
log(`Reconnecting in ${delaySec}s... (attempt ${attempt})`);
|
|
1228
|
-
await sleep(delay);
|
|
1229
|
-
connect();
|
|
1230
|
-
}
|
|
1231
|
-
connect();
|
|
1232
|
-
}
|
|
1233
|
-
function trySend(ws, data) {
|
|
1234
|
-
try {
|
|
1235
|
-
ws.send(JSON.stringify(data));
|
|
1236
|
-
} catch {
|
|
1237
|
-
console.error("Failed to send message \u2014 WebSocket may be closed");
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
async function logPostReviewStats(type, verdict, tokensUsed, tokensEstimated, consumptionDeps, logPrefix) {
|
|
1241
|
-
const pfx = logPrefix ? `${logPrefix} ` : "";
|
|
1242
|
-
const estimateTag = tokensEstimated ? " ~" : " ";
|
|
1243
|
-
if (!consumptionDeps) {
|
|
1244
|
-
if (verdict) {
|
|
1245
|
-
console.log(
|
|
1246
|
-
`${pfx}${type} complete: ${verdict} (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1247
|
-
);
|
|
1248
|
-
} else {
|
|
1249
|
-
console.log(
|
|
1250
|
-
`${pfx}${type} complete (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1251
|
-
);
|
|
1252
|
-
}
|
|
1253
|
-
return;
|
|
1254
|
-
}
|
|
1255
|
-
recordSessionUsage(consumptionDeps.session, tokensUsed);
|
|
1256
|
-
if (verdict) {
|
|
1257
|
-
console.log(
|
|
1258
|
-
`${pfx}${type} complete: ${verdict} (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1259
|
-
);
|
|
1260
|
-
} else {
|
|
1261
|
-
console.log(
|
|
1262
|
-
`${pfx}${type} complete (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ", estimated" : ""})`
|
|
1263
|
-
);
|
|
1264
|
-
}
|
|
1265
|
-
console.log(
|
|
1266
|
-
`${pfx}${formatPostReviewStats(tokensUsed, consumptionDeps.session, consumptionDeps.limits)}`
|
|
813
|
+
return response.text();
|
|
814
|
+
},
|
|
815
|
+
{ maxAttempts: 2 },
|
|
816
|
+
signal
|
|
1267
817
|
);
|
|
1268
818
|
}
|
|
1269
|
-
function
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
if (
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
819
|
+
async function pollLoop(client, agentId, reviewDeps, consumptionDeps, agentInfo, options) {
|
|
820
|
+
const { pollIntervalMs, routerRelay, reviewOnly, signal } = options;
|
|
821
|
+
console.log(`Agent ${agentId} polling every ${pollIntervalMs / 1e3}s...`);
|
|
822
|
+
let consecutiveAuthErrors = 0;
|
|
823
|
+
let consecutiveErrors = 0;
|
|
824
|
+
while (!signal?.aborted) {
|
|
825
|
+
try {
|
|
826
|
+
const pollBody = { agent_id: agentId };
|
|
827
|
+
if (reviewOnly) pollBody.review_only = true;
|
|
828
|
+
const pollResponse = await client.post("/api/tasks/poll", pollBody);
|
|
829
|
+
consecutiveAuthErrors = 0;
|
|
830
|
+
consecutiveErrors = 0;
|
|
831
|
+
if (pollResponse.tasks.length > 0) {
|
|
832
|
+
const task = pollResponse.tasks[0];
|
|
833
|
+
await handleTask(
|
|
834
|
+
client,
|
|
835
|
+
agentId,
|
|
836
|
+
task,
|
|
837
|
+
reviewDeps,
|
|
838
|
+
consumptionDeps,
|
|
839
|
+
agentInfo,
|
|
840
|
+
routerRelay,
|
|
841
|
+
signal
|
|
1290
842
|
);
|
|
1291
843
|
}
|
|
1292
|
-
|
|
1293
|
-
break;
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
if (routerRelay) {
|
|
1300
|
-
void (async () => {
|
|
1301
|
-
if (consumptionDeps) {
|
|
1302
|
-
const limitResult = await checkConsumptionLimits(
|
|
1303
|
-
consumptionDeps.agentId,
|
|
1304
|
-
consumptionDeps.limits
|
|
1305
|
-
);
|
|
1306
|
-
if (!limitResult.allowed) {
|
|
1307
|
-
trySend(ws, {
|
|
1308
|
-
type: "review_rejected",
|
|
1309
|
-
id: crypto2.randomUUID(),
|
|
1310
|
-
timestamp: Date.now(),
|
|
1311
|
-
taskId: request.taskId,
|
|
1312
|
-
reason: limitResult.reason ?? "consumption_limit_exceeded"
|
|
1313
|
-
});
|
|
1314
|
-
console.log(`${pfx}Review rejected: ${limitResult.reason}`);
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
try {
|
|
1319
|
-
const prompt = routerRelay.buildReviewPrompt({
|
|
1320
|
-
owner: request.project.owner,
|
|
1321
|
-
repo: request.project.repo,
|
|
1322
|
-
reviewMode: request.reviewMode ?? "full",
|
|
1323
|
-
prompt: request.project.prompt,
|
|
1324
|
-
diffContent: request.diffContent
|
|
1325
|
-
});
|
|
1326
|
-
const response = await routerRelay.sendPrompt(
|
|
1327
|
-
"review_request",
|
|
1328
|
-
request.taskId,
|
|
1329
|
-
prompt,
|
|
1330
|
-
request.timeout
|
|
1331
|
-
);
|
|
1332
|
-
const { verdict, review } = routerRelay.parseReviewResponse(response);
|
|
1333
|
-
const tokensUsed = estimateTokens(prompt) + estimateTokens(response);
|
|
1334
|
-
trySend(ws, {
|
|
1335
|
-
type: "review_complete",
|
|
1336
|
-
id: crypto2.randomUUID(),
|
|
1337
|
-
timestamp: Date.now(),
|
|
1338
|
-
taskId: request.taskId,
|
|
1339
|
-
review,
|
|
1340
|
-
verdict,
|
|
1341
|
-
tokensUsed
|
|
1342
|
-
});
|
|
1343
|
-
await logPostReviewStats(
|
|
1344
|
-
"Review",
|
|
1345
|
-
verdict,
|
|
1346
|
-
tokensUsed,
|
|
1347
|
-
true,
|
|
1348
|
-
consumptionDeps,
|
|
1349
|
-
logPrefix
|
|
1350
|
-
);
|
|
1351
|
-
} catch (err) {
|
|
1352
|
-
if (err instanceof RouterTimeoutError) {
|
|
1353
|
-
trySend(ws, {
|
|
1354
|
-
type: "review_error",
|
|
1355
|
-
id: crypto2.randomUUID(),
|
|
1356
|
-
timestamp: Date.now(),
|
|
1357
|
-
taskId: request.taskId,
|
|
1358
|
-
error: err.message
|
|
1359
|
-
});
|
|
1360
|
-
} else {
|
|
1361
|
-
trySend(ws, {
|
|
1362
|
-
type: "review_error",
|
|
1363
|
-
id: crypto2.randomUUID(),
|
|
1364
|
-
timestamp: Date.now(),
|
|
1365
|
-
taskId: request.taskId,
|
|
1366
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
1367
|
-
});
|
|
1368
|
-
}
|
|
1369
|
-
console.error(`${pfx}Review failed:`, err);
|
|
1370
|
-
}
|
|
1371
|
-
})();
|
|
1372
|
-
break;
|
|
1373
|
-
}
|
|
1374
|
-
if (!reviewDeps) {
|
|
1375
|
-
ws.send(
|
|
1376
|
-
JSON.stringify({
|
|
1377
|
-
type: "review_rejected",
|
|
1378
|
-
id: crypto2.randomUUID(),
|
|
1379
|
-
timestamp: Date.now(),
|
|
1380
|
-
taskId: request.taskId,
|
|
1381
|
-
reason: "Review execution not configured"
|
|
1382
|
-
})
|
|
844
|
+
} catch (err) {
|
|
845
|
+
if (signal?.aborted) break;
|
|
846
|
+
if (err instanceof HttpError && (err.status === 401 || err.status === 403)) {
|
|
847
|
+
consecutiveAuthErrors++;
|
|
848
|
+
consecutiveErrors++;
|
|
849
|
+
console.error(
|
|
850
|
+
`Auth error (${err.status}): ${err.message} [${consecutiveAuthErrors}/${MAX_CONSECUTIVE_AUTH_ERRORS}]`
|
|
1383
851
|
);
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
if (consumptionDeps) {
|
|
1388
|
-
const limitResult = await checkConsumptionLimits(
|
|
1389
|
-
consumptionDeps.agentId,
|
|
1390
|
-
consumptionDeps.limits
|
|
1391
|
-
);
|
|
1392
|
-
if (!limitResult.allowed) {
|
|
1393
|
-
trySend(ws, {
|
|
1394
|
-
type: "review_rejected",
|
|
1395
|
-
id: crypto2.randomUUID(),
|
|
1396
|
-
timestamp: Date.now(),
|
|
1397
|
-
taskId: request.taskId,
|
|
1398
|
-
reason: limitResult.reason ?? "consumption_limit_exceeded"
|
|
1399
|
-
});
|
|
1400
|
-
console.log(`${pfx}Review rejected: ${limitResult.reason}`);
|
|
1401
|
-
return;
|
|
1402
|
-
}
|
|
852
|
+
if (consecutiveAuthErrors >= MAX_CONSECUTIVE_AUTH_ERRORS) {
|
|
853
|
+
console.error("Authentication failed repeatedly. Exiting.");
|
|
854
|
+
break;
|
|
1403
855
|
}
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
diffContent: request.diffContent,
|
|
1409
|
-
prompt: request.project.prompt,
|
|
1410
|
-
owner: request.project.owner,
|
|
1411
|
-
repo: request.project.repo,
|
|
1412
|
-
prNumber: request.pr.number,
|
|
1413
|
-
timeout: request.timeout,
|
|
1414
|
-
reviewMode: request.reviewMode ?? "full"
|
|
1415
|
-
},
|
|
1416
|
-
reviewDeps
|
|
1417
|
-
);
|
|
1418
|
-
trySend(ws, {
|
|
1419
|
-
type: "review_complete",
|
|
1420
|
-
id: crypto2.randomUUID(),
|
|
1421
|
-
timestamp: Date.now(),
|
|
1422
|
-
taskId: request.taskId,
|
|
1423
|
-
review: result.review,
|
|
1424
|
-
verdict: result.verdict,
|
|
1425
|
-
tokensUsed: result.tokensUsed
|
|
1426
|
-
});
|
|
1427
|
-
await logPostReviewStats(
|
|
1428
|
-
"Review",
|
|
1429
|
-
result.verdict,
|
|
1430
|
-
result.tokensUsed,
|
|
1431
|
-
result.tokensEstimated,
|
|
1432
|
-
consumptionDeps,
|
|
1433
|
-
logPrefix
|
|
1434
|
-
);
|
|
1435
|
-
} catch (err) {
|
|
1436
|
-
if (err instanceof DiffTooLargeError) {
|
|
1437
|
-
trySend(ws, {
|
|
1438
|
-
type: "review_rejected",
|
|
1439
|
-
id: crypto2.randomUUID(),
|
|
1440
|
-
timestamp: Date.now(),
|
|
1441
|
-
taskId: request.taskId,
|
|
1442
|
-
reason: err.message
|
|
1443
|
-
});
|
|
1444
|
-
} else {
|
|
1445
|
-
trySend(ws, {
|
|
1446
|
-
type: "review_error",
|
|
1447
|
-
id: crypto2.randomUUID(),
|
|
1448
|
-
timestamp: Date.now(),
|
|
1449
|
-
taskId: request.taskId,
|
|
1450
|
-
error: err instanceof Error ? err.message : "Unknown error"
|
|
1451
|
-
});
|
|
1452
|
-
}
|
|
1453
|
-
console.error(`${pfx}Review failed:`, err);
|
|
1454
|
-
}
|
|
1455
|
-
})();
|
|
1456
|
-
break;
|
|
1457
|
-
}
|
|
1458
|
-
case "summary_request": {
|
|
1459
|
-
const summaryRequest = msg;
|
|
1460
|
-
console.log(
|
|
1461
|
-
`${pfx}Summary request: task ${summaryRequest.taskId} for ${summaryRequest.project.owner}/${summaryRequest.project.repo}#${summaryRequest.pr.number} (${summaryRequest.reviews.length} reviews)`
|
|
1462
|
-
);
|
|
1463
|
-
if (routerRelay) {
|
|
1464
|
-
void (async () => {
|
|
1465
|
-
if (consumptionDeps) {
|
|
1466
|
-
const limitResult = await checkConsumptionLimits(
|
|
1467
|
-
consumptionDeps.agentId,
|
|
1468
|
-
consumptionDeps.limits
|
|
1469
|
-
);
|
|
1470
|
-
if (!limitResult.allowed) {
|
|
1471
|
-
trySend(ws, {
|
|
1472
|
-
type: "review_rejected",
|
|
1473
|
-
id: crypto2.randomUUID(),
|
|
1474
|
-
timestamp: Date.now(),
|
|
1475
|
-
taskId: summaryRequest.taskId,
|
|
1476
|
-
reason: limitResult.reason ?? "consumption_limit_exceeded"
|
|
1477
|
-
});
|
|
1478
|
-
console.log(`${pfx}Summary rejected: ${limitResult.reason}`);
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
try {
|
|
1483
|
-
const prompt = routerRelay.buildSummaryPrompt({
|
|
1484
|
-
owner: summaryRequest.project.owner,
|
|
1485
|
-
repo: summaryRequest.project.repo,
|
|
1486
|
-
prompt: summaryRequest.project.prompt,
|
|
1487
|
-
reviews: summaryRequest.reviews,
|
|
1488
|
-
diffContent: summaryRequest.diffContent ?? ""
|
|
1489
|
-
});
|
|
1490
|
-
const response = await routerRelay.sendPrompt(
|
|
1491
|
-
"summary_request",
|
|
1492
|
-
summaryRequest.taskId,
|
|
1493
|
-
prompt,
|
|
1494
|
-
summaryRequest.timeout
|
|
1495
|
-
);
|
|
1496
|
-
const tokensUsed = estimateTokens(prompt) + estimateTokens(response);
|
|
1497
|
-
trySend(ws, {
|
|
1498
|
-
type: "summary_complete",
|
|
1499
|
-
id: crypto2.randomUUID(),
|
|
1500
|
-
timestamp: Date.now(),
|
|
1501
|
-
taskId: summaryRequest.taskId,
|
|
1502
|
-
summary: response,
|
|
1503
|
-
tokensUsed
|
|
1504
|
-
});
|
|
1505
|
-
await logPostReviewStats(
|
|
1506
|
-
"Summary",
|
|
1507
|
-
void 0,
|
|
1508
|
-
tokensUsed,
|
|
1509
|
-
true,
|
|
1510
|
-
consumptionDeps,
|
|
1511
|
-
logPrefix
|
|
1512
|
-
);
|
|
1513
|
-
} catch (err) {
|
|
1514
|
-
if (err instanceof RouterTimeoutError) {
|
|
1515
|
-
trySend(ws, {
|
|
1516
|
-
type: "review_error",
|
|
1517
|
-
id: crypto2.randomUUID(),
|
|
1518
|
-
timestamp: Date.now(),
|
|
1519
|
-
taskId: summaryRequest.taskId,
|
|
1520
|
-
error: err.message
|
|
1521
|
-
});
|
|
1522
|
-
} else {
|
|
1523
|
-
trySend(ws, {
|
|
1524
|
-
type: "review_error",
|
|
1525
|
-
id: crypto2.randomUUID(),
|
|
1526
|
-
timestamp: Date.now(),
|
|
1527
|
-
taskId: summaryRequest.taskId,
|
|
1528
|
-
error: err instanceof Error ? err.message : "Summary failed"
|
|
1529
|
-
});
|
|
1530
|
-
}
|
|
1531
|
-
console.error(`${pfx}Summary failed:`, err);
|
|
1532
|
-
}
|
|
1533
|
-
})();
|
|
1534
|
-
break;
|
|
1535
|
-
}
|
|
1536
|
-
if (!reviewDeps) {
|
|
1537
|
-
trySend(ws, {
|
|
1538
|
-
type: "review_rejected",
|
|
1539
|
-
id: crypto2.randomUUID(),
|
|
1540
|
-
timestamp: Date.now(),
|
|
1541
|
-
taskId: summaryRequest.taskId,
|
|
1542
|
-
reason: "Review tool not configured"
|
|
1543
|
-
});
|
|
1544
|
-
break;
|
|
856
|
+
} else {
|
|
857
|
+
consecutiveAuthErrors = 0;
|
|
858
|
+
consecutiveErrors++;
|
|
859
|
+
console.error(`Poll error: ${err.message}`);
|
|
1545
860
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
id: crypto2.randomUUID(),
|
|
1556
|
-
timestamp: Date.now(),
|
|
1557
|
-
taskId: summaryRequest.taskId,
|
|
1558
|
-
reason: limitResult.reason ?? "consumption_limit_exceeded"
|
|
1559
|
-
});
|
|
1560
|
-
console.log(`${pfx}Summary rejected: ${limitResult.reason}`);
|
|
1561
|
-
return;
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
try {
|
|
1565
|
-
const result = await executeSummary(
|
|
1566
|
-
{
|
|
1567
|
-
taskId: summaryRequest.taskId,
|
|
1568
|
-
reviews: summaryRequest.reviews,
|
|
1569
|
-
prompt: summaryRequest.project.prompt,
|
|
1570
|
-
owner: summaryRequest.project.owner,
|
|
1571
|
-
repo: summaryRequest.project.repo,
|
|
1572
|
-
prNumber: summaryRequest.pr.number,
|
|
1573
|
-
timeout: summaryRequest.timeout,
|
|
1574
|
-
diffContent: summaryRequest.diffContent ?? ""
|
|
1575
|
-
},
|
|
1576
|
-
reviewDeps
|
|
1577
|
-
);
|
|
1578
|
-
trySend(ws, {
|
|
1579
|
-
type: "summary_complete",
|
|
1580
|
-
id: crypto2.randomUUID(),
|
|
1581
|
-
timestamp: Date.now(),
|
|
1582
|
-
taskId: summaryRequest.taskId,
|
|
1583
|
-
summary: result.summary,
|
|
1584
|
-
tokensUsed: result.tokensUsed
|
|
1585
|
-
});
|
|
1586
|
-
await logPostReviewStats(
|
|
1587
|
-
"Summary",
|
|
1588
|
-
void 0,
|
|
1589
|
-
result.tokensUsed,
|
|
1590
|
-
result.tokensEstimated,
|
|
1591
|
-
consumptionDeps,
|
|
1592
|
-
logPrefix
|
|
861
|
+
if (consecutiveErrors > 0) {
|
|
862
|
+
const backoff = Math.min(
|
|
863
|
+
pollIntervalMs * Math.pow(2, consecutiveErrors - 1),
|
|
864
|
+
MAX_POLL_BACKOFF_MS
|
|
865
|
+
);
|
|
866
|
+
const extraDelay = backoff - pollIntervalMs;
|
|
867
|
+
if (extraDelay > 0) {
|
|
868
|
+
console.warn(
|
|
869
|
+
`Poll failed (${consecutiveErrors} consecutive). Next poll in ${Math.round(backoff / 1e3)}s`
|
|
1593
870
|
);
|
|
1594
|
-
|
|
1595
|
-
if (err instanceof InputTooLargeError) {
|
|
1596
|
-
trySend(ws, {
|
|
1597
|
-
type: "review_rejected",
|
|
1598
|
-
id: crypto2.randomUUID(),
|
|
1599
|
-
timestamp: Date.now(),
|
|
1600
|
-
taskId: summaryRequest.taskId,
|
|
1601
|
-
reason: err.message
|
|
1602
|
-
});
|
|
1603
|
-
} else {
|
|
1604
|
-
trySend(ws, {
|
|
1605
|
-
type: "review_error",
|
|
1606
|
-
id: crypto2.randomUUID(),
|
|
1607
|
-
timestamp: Date.now(),
|
|
1608
|
-
taskId: summaryRequest.taskId,
|
|
1609
|
-
error: err instanceof Error ? err.message : "Summary failed"
|
|
1610
|
-
});
|
|
1611
|
-
}
|
|
1612
|
-
console.error(`${pfx}Summary failed:`, err);
|
|
871
|
+
await sleep2(extraDelay, signal);
|
|
1613
872
|
}
|
|
1614
|
-
})();
|
|
1615
|
-
break;
|
|
1616
|
-
}
|
|
1617
|
-
case "error":
|
|
1618
|
-
console.error(`${pfx}Platform error: ${msg.code ?? "unknown"}`);
|
|
1619
|
-
if (msg.code === "auth_revoked") process.exit(1);
|
|
1620
|
-
break;
|
|
1621
|
-
default:
|
|
1622
|
-
break;
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
async function syncAgentToServer(client, serverAgents, localAgent) {
|
|
1626
|
-
const existing = serverAgents.find(
|
|
1627
|
-
(a) => a.model === localAgent.model && a.tool === localAgent.tool
|
|
1628
|
-
);
|
|
1629
|
-
if (existing) {
|
|
1630
|
-
return { agentId: existing.id, created: false };
|
|
1631
|
-
}
|
|
1632
|
-
const body = { model: localAgent.model, tool: localAgent.tool };
|
|
1633
|
-
if (localAgent.name) {
|
|
1634
|
-
body.displayName = localAgent.name;
|
|
1635
|
-
}
|
|
1636
|
-
if (localAgent.repos) {
|
|
1637
|
-
body.repoConfig = localAgent.repos;
|
|
1638
|
-
}
|
|
1639
|
-
const created = await client.post("/api/agents", body);
|
|
1640
|
-
return { agentId: created.id, created: true };
|
|
1641
|
-
}
|
|
1642
|
-
function resolveLocalAgentCommand(localAgent, globalAgentCommand) {
|
|
1643
|
-
const effectiveCommand = localAgent.command ?? globalAgentCommand;
|
|
1644
|
-
return resolveCommandTemplate(effectiveCommand);
|
|
1645
|
-
}
|
|
1646
|
-
function startAgentRouter() {
|
|
1647
|
-
void agentCommand.parseAsync(
|
|
1648
|
-
["start", "--router", "--anonymous", "--model", "router", "--tool", "opencara"],
|
|
1649
|
-
{ from: "user" }
|
|
1650
|
-
);
|
|
1651
|
-
}
|
|
1652
|
-
var agentCommand = new Command2("agent").description("Manage review agents");
|
|
1653
|
-
agentCommand.command("create").description("Add an agent to local config (interactive or via flags)").option("--model <model>", "AI model name (e.g., claude-opus-4-6)").option("--tool <tool>", "Review tool name (e.g., claude-code)").option("--command <cmd>", "Custom command template (bypasses registry lookup)").action(async (opts) => {
|
|
1654
|
-
const config = loadConfig();
|
|
1655
|
-
requireApiKey(config);
|
|
1656
|
-
let model;
|
|
1657
|
-
let tool;
|
|
1658
|
-
let command = opts.command;
|
|
1659
|
-
if (opts.model && opts.tool) {
|
|
1660
|
-
model = opts.model;
|
|
1661
|
-
tool = opts.tool;
|
|
1662
|
-
} else if (opts.model || opts.tool) {
|
|
1663
|
-
console.error("Both --model and --tool are required in non-interactive mode.");
|
|
1664
|
-
process.exit(1);
|
|
1665
|
-
} else {
|
|
1666
|
-
const client = new ApiClient(config.platformUrl, config.apiKey);
|
|
1667
|
-
let registry;
|
|
1668
|
-
try {
|
|
1669
|
-
registry = await client.get("/api/registry");
|
|
1670
|
-
} catch {
|
|
1671
|
-
console.warn("Could not fetch registry from server. Using built-in defaults.");
|
|
1672
|
-
registry = DEFAULT_REGISTRY;
|
|
1673
|
-
}
|
|
1674
|
-
const { search, input } = await import("@inquirer/prompts");
|
|
1675
|
-
const searchTheme = {
|
|
1676
|
-
style: {
|
|
1677
|
-
keysHelpTip: (keys) => keys.map(([key, action]) => `${key} ${action}`).join(", ") + ", ^C exit"
|
|
1678
|
-
}
|
|
1679
|
-
};
|
|
1680
|
-
const existingAgents = config.agents ?? [];
|
|
1681
|
-
const toolChoices = registry.tools.map((t) => ({
|
|
1682
|
-
name: t.displayName,
|
|
1683
|
-
value: t.name
|
|
1684
|
-
}));
|
|
1685
|
-
try {
|
|
1686
|
-
while (true) {
|
|
1687
|
-
tool = await search({
|
|
1688
|
-
message: "Select a tool:",
|
|
1689
|
-
theme: searchTheme,
|
|
1690
|
-
source: (term) => {
|
|
1691
|
-
const q = (term ?? "").toLowerCase();
|
|
1692
|
-
return toolChoices.filter(
|
|
1693
|
-
(c) => c.name.toLowerCase().includes(q) || c.value.toLowerCase().includes(q)
|
|
1694
|
-
);
|
|
1695
|
-
}
|
|
1696
|
-
});
|
|
1697
|
-
const compatible = registry.models.filter((m) => m.tools.includes(tool));
|
|
1698
|
-
const incompatible = registry.models.filter((m) => !m.tools.includes(tool));
|
|
1699
|
-
const modelChoices = [
|
|
1700
|
-
...compatible.map((m) => ({
|
|
1701
|
-
name: m.displayName,
|
|
1702
|
-
value: m.name
|
|
1703
|
-
})),
|
|
1704
|
-
...incompatible.map((m) => ({
|
|
1705
|
-
name: `\x1B[38;5;249m${m.displayName}\x1B[0m`,
|
|
1706
|
-
value: m.name
|
|
1707
|
-
}))
|
|
1708
|
-
];
|
|
1709
|
-
model = await search({
|
|
1710
|
-
message: "Select a model:",
|
|
1711
|
-
theme: searchTheme,
|
|
1712
|
-
source: (term) => {
|
|
1713
|
-
const q = (term ?? "").toLowerCase();
|
|
1714
|
-
return modelChoices.filter(
|
|
1715
|
-
(c) => c.value.toLowerCase().includes(q) || c.name.toLowerCase().includes(q)
|
|
1716
|
-
);
|
|
1717
|
-
}
|
|
1718
|
-
});
|
|
1719
|
-
const isDup = existingAgents.some((a) => a.model === model && a.tool === tool);
|
|
1720
|
-
if (isDup) {
|
|
1721
|
-
console.warn(`"${model}" / "${tool}" already exists in config. Choose again.`);
|
|
1722
|
-
continue;
|
|
1723
|
-
}
|
|
1724
|
-
const modelEntry = registry.models.find((m) => m.name === model);
|
|
1725
|
-
if (modelEntry && !modelEntry.tools.includes(tool)) {
|
|
1726
|
-
console.warn(`Warning: "${model}" is not listed as compatible with "${tool}".`);
|
|
1727
|
-
}
|
|
1728
|
-
break;
|
|
1729
873
|
}
|
|
1730
|
-
const toolEntry = registry.tools.find((t) => t.name === tool);
|
|
1731
|
-
const defaultCommand = toolEntry ? toolEntry.commandTemplate.replaceAll("${MODEL}", model) : `${tool} --model ${model}`;
|
|
1732
|
-
command = await input({
|
|
1733
|
-
message: "Command:",
|
|
1734
|
-
default: defaultCommand,
|
|
1735
|
-
prefill: "editable"
|
|
1736
|
-
});
|
|
1737
|
-
} catch (err) {
|
|
1738
|
-
if (err && typeof err === "object" && "name" in err && err.name === "ExitPromptError") {
|
|
1739
|
-
console.log("Cancelled.");
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
|
-
throw err;
|
|
1743
874
|
}
|
|
875
|
+
await sleep2(pollIntervalMs, signal);
|
|
1744
876
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
877
|
+
}
|
|
878
|
+
async function handleTask(client, agentId, task, reviewDeps, consumptionDeps, agentInfo, routerRelay, signal) {
|
|
879
|
+
const { task_id, owner, repo, pr_number, diff_url, timeout_seconds, prompt, role } = task;
|
|
880
|
+
console.log(`
|
|
881
|
+
Task ${task_id}: PR #${pr_number} on ${owner}/${repo} (role: ${role})`);
|
|
882
|
+
let claimResponse;
|
|
883
|
+
try {
|
|
884
|
+
claimResponse = await withRetry(
|
|
885
|
+
() => client.post(`/api/tasks/${task_id}/claim`, {
|
|
886
|
+
agent_id: agentId,
|
|
887
|
+
role,
|
|
888
|
+
model: agentInfo.model,
|
|
889
|
+
tool: agentInfo.tool
|
|
890
|
+
}),
|
|
891
|
+
{ maxAttempts: 2 },
|
|
892
|
+
signal
|
|
1759
893
|
);
|
|
894
|
+
} catch (err) {
|
|
895
|
+
const status = err instanceof HttpError ? ` (${err.status})` : "";
|
|
896
|
+
console.error(` Failed to claim task ${task_id}${status}: ${err.message}`);
|
|
897
|
+
return;
|
|
1760
898
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
}
|
|
1765
|
-
const isDuplicate = config.agents.some((a) => a.model === model && a.tool === tool);
|
|
1766
|
-
if (isDuplicate) {
|
|
1767
|
-
console.error(`Agent with model "${model}" and tool "${tool}" already exists in config.`);
|
|
1768
|
-
process.exit(1);
|
|
899
|
+
if (!claimResponse.claimed) {
|
|
900
|
+
console.log(` Claim rejected: ${claimResponse.reason}`);
|
|
901
|
+
return;
|
|
1769
902
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
console.log("Agent added to config:");
|
|
1773
|
-
console.log(` Model: ${model}`);
|
|
1774
|
-
console.log(` Tool: ${tool}`);
|
|
1775
|
-
console.log(` Command: ${command}`);
|
|
1776
|
-
});
|
|
1777
|
-
agentCommand.command("init").description("Import server-side agents into local config").action(async () => {
|
|
1778
|
-
const config = loadConfig();
|
|
1779
|
-
const apiKey = requireApiKey(config);
|
|
1780
|
-
const client = new ApiClient(config.platformUrl, apiKey);
|
|
1781
|
-
let res;
|
|
903
|
+
console.log(` Claimed as ${role}`);
|
|
904
|
+
let diffContent;
|
|
1782
905
|
try {
|
|
1783
|
-
|
|
906
|
+
diffContent = await fetchDiff(diff_url, signal);
|
|
907
|
+
console.log(` Diff fetched (${Math.round(diffContent.length / 1024)}KB)`);
|
|
1784
908
|
} catch (err) {
|
|
1785
|
-
console.error(
|
|
1786
|
-
|
|
1787
|
-
}
|
|
1788
|
-
if (res.agents.length === 0) {
|
|
1789
|
-
console.log("No server-side agents found. Use `opencara agent create` to add one.");
|
|
909
|
+
console.error(` Failed to fetch diff for task ${task_id}: ${err.message}`);
|
|
910
|
+
await safeReject(client, task_id, agentId, `Cannot access diff: ${err.message}`);
|
|
1790
911
|
return;
|
|
1791
912
|
}
|
|
1792
|
-
let registry;
|
|
1793
913
|
try {
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
914
|
+
if (role === "summary" && "reviews" in claimResponse && claimResponse.reviews) {
|
|
915
|
+
await executeSummaryTask(
|
|
916
|
+
client,
|
|
917
|
+
agentId,
|
|
918
|
+
task_id,
|
|
919
|
+
owner,
|
|
920
|
+
repo,
|
|
921
|
+
pr_number,
|
|
922
|
+
diffContent,
|
|
923
|
+
prompt,
|
|
924
|
+
timeout_seconds,
|
|
925
|
+
claimResponse.reviews,
|
|
926
|
+
reviewDeps,
|
|
927
|
+
consumptionDeps,
|
|
928
|
+
routerRelay,
|
|
929
|
+
signal
|
|
930
|
+
);
|
|
1807
931
|
} else {
|
|
1808
|
-
|
|
1809
|
-
|
|
932
|
+
await executeReviewTask(
|
|
933
|
+
client,
|
|
934
|
+
agentId,
|
|
935
|
+
task_id,
|
|
936
|
+
owner,
|
|
937
|
+
repo,
|
|
938
|
+
pr_number,
|
|
939
|
+
diffContent,
|
|
940
|
+
prompt,
|
|
941
|
+
timeout_seconds,
|
|
942
|
+
reviewDeps,
|
|
943
|
+
consumptionDeps,
|
|
944
|
+
routerRelay,
|
|
945
|
+
signal
|
|
1810
946
|
);
|
|
1811
947
|
}
|
|
1812
|
-
existing.push({ model: agent.model, tool: agent.tool, command });
|
|
1813
|
-
imported++;
|
|
1814
|
-
}
|
|
1815
|
-
config.agents = existing;
|
|
1816
|
-
saveConfig(config);
|
|
1817
|
-
console.log(`Imported ${imported} agent(s) to local config.`);
|
|
1818
|
-
if (imported > 0) {
|
|
1819
|
-
console.log("Edit ~/.opencara/config.yml to adjust commands for your system.");
|
|
1820
|
-
}
|
|
1821
|
-
});
|
|
1822
|
-
agentCommand.command("list").description("List registered agents").action(async () => {
|
|
1823
|
-
const config = loadConfig();
|
|
1824
|
-
const apiKey = requireApiKey(config);
|
|
1825
|
-
const client = new ApiClient(config.platformUrl, apiKey);
|
|
1826
|
-
let res;
|
|
1827
|
-
try {
|
|
1828
|
-
res = await client.get("/api/agents");
|
|
1829
948
|
} catch (err) {
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
const stats = await client.get(`/api/stats/${agent.id}`);
|
|
1837
|
-
trustLabels.set(agent.id, stats.agent.trustTier.label);
|
|
1838
|
-
} catch {
|
|
949
|
+
if (err instanceof DiffTooLargeError || err instanceof InputTooLargeError) {
|
|
950
|
+
console.error(` ${err.message}`);
|
|
951
|
+
await safeReject(client, task_id, agentId, err.message);
|
|
952
|
+
} else {
|
|
953
|
+
console.error(` Error on task ${task_id}: ${err.message}`);
|
|
954
|
+
await safeError(client, task_id, agentId, err.message);
|
|
1839
955
|
}
|
|
1840
956
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
957
|
+
}
|
|
958
|
+
async function safeReject(client, taskId, agentId, reason) {
|
|
959
|
+
try {
|
|
960
|
+
await withRetry(
|
|
961
|
+
() => client.post(`/api/tasks/${taskId}/reject`, { agent_id: agentId, reason }),
|
|
962
|
+
{ maxAttempts: 2 }
|
|
963
|
+
);
|
|
964
|
+
} catch (err) {
|
|
965
|
+
console.error(
|
|
966
|
+
` Failed to report rejection for task ${taskId}: ${err.message} (logged locally)`
|
|
1849
967
|
);
|
|
1850
|
-
return { entry: existing, command: command2 };
|
|
1851
968
|
}
|
|
1852
|
-
console.log("Registering anonymous agent...");
|
|
1853
|
-
const client = new ApiClient(config.platformUrl);
|
|
1854
|
-
const body = { model, tool };
|
|
1855
|
-
const res = await client.post("/api/agents/anonymous", body);
|
|
1856
|
-
const entry = {
|
|
1857
|
-
agentId: res.agentId,
|
|
1858
|
-
apiKey: res.apiKey,
|
|
1859
|
-
model,
|
|
1860
|
-
tool
|
|
1861
|
-
};
|
|
1862
|
-
config.anonymousAgents.push(entry);
|
|
1863
|
-
saveConfig(config);
|
|
1864
|
-
console.log(`Agent registered: ${res.agentId} (${model} / ${tool})`);
|
|
1865
|
-
console.log("Credentials saved to ~/.opencara/config.yml");
|
|
1866
|
-
const command = resolveCommandTemplate(
|
|
1867
|
-
DEFAULT_REGISTRY.tools.find((t) => t.name === tool)?.commandTemplate.replaceAll("${MODEL}", model) ?? null
|
|
1868
|
-
);
|
|
1869
|
-
return { entry, command };
|
|
1870
969
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
async (agentIdOrModel, opts) => {
|
|
1876
|
-
let stabilityThresholdMs;
|
|
1877
|
-
if (opts.stabilityThreshold !== void 0) {
|
|
1878
|
-
const val = Number(opts.stabilityThreshold);
|
|
1879
|
-
if (!Number.isInteger(val) || val < STABILITY_THRESHOLD_MIN_MS || val > STABILITY_THRESHOLD_MAX_MS) {
|
|
1880
|
-
console.error(
|
|
1881
|
-
`Invalid --stability-threshold: must be an integer between ${STABILITY_THRESHOLD_MIN_MS} and ${STABILITY_THRESHOLD_MAX_MS}`
|
|
1882
|
-
);
|
|
1883
|
-
process.exit(1);
|
|
1884
|
-
}
|
|
1885
|
-
stabilityThresholdMs = val;
|
|
1886
|
-
}
|
|
1887
|
-
const config = loadConfig();
|
|
1888
|
-
if (opts.anonymous) {
|
|
1889
|
-
if (!opts.model || !opts.tool) {
|
|
1890
|
-
console.error("Both --model and --tool are required with --anonymous.");
|
|
1891
|
-
process.exit(1);
|
|
1892
|
-
}
|
|
1893
|
-
let entry;
|
|
1894
|
-
let reviewDeps2;
|
|
1895
|
-
let relay2;
|
|
1896
|
-
if (opts.router) {
|
|
1897
|
-
const existing = config.anonymousAgents.find(
|
|
1898
|
-
(a) => a.model === opts.model && a.tool === opts.tool
|
|
1899
|
-
);
|
|
1900
|
-
if (existing) {
|
|
1901
|
-
console.log(
|
|
1902
|
-
`Reusing stored anonymous agent ${existing.agentId} (${opts.model} / ${opts.tool})`
|
|
1903
|
-
);
|
|
1904
|
-
entry = existing;
|
|
1905
|
-
} else {
|
|
1906
|
-
console.log("Registering anonymous agent...");
|
|
1907
|
-
const client2 = new ApiClient(config.platformUrl);
|
|
1908
|
-
const res = await client2.post("/api/agents/anonymous", {
|
|
1909
|
-
model: opts.model,
|
|
1910
|
-
tool: opts.tool
|
|
1911
|
-
});
|
|
1912
|
-
entry = {
|
|
1913
|
-
agentId: res.agentId,
|
|
1914
|
-
apiKey: res.apiKey,
|
|
1915
|
-
model: opts.model,
|
|
1916
|
-
tool: opts.tool
|
|
1917
|
-
};
|
|
1918
|
-
config.anonymousAgents.push(entry);
|
|
1919
|
-
saveConfig(config);
|
|
1920
|
-
console.log(`Agent registered: ${res.agentId} (${opts.model} / ${opts.tool})`);
|
|
1921
|
-
}
|
|
1922
|
-
relay2 = new RouterRelay();
|
|
1923
|
-
relay2.start();
|
|
1924
|
-
} else {
|
|
1925
|
-
let resolved;
|
|
1926
|
-
try {
|
|
1927
|
-
resolved = await resolveAnonymousAgent(config, opts.model, opts.tool);
|
|
1928
|
-
} catch (err) {
|
|
1929
|
-
console.error(
|
|
1930
|
-
"Failed to register anonymous agent:",
|
|
1931
|
-
err instanceof Error ? err.message : err
|
|
1932
|
-
);
|
|
1933
|
-
process.exit(1);
|
|
1934
|
-
}
|
|
1935
|
-
entry = resolved.entry;
|
|
1936
|
-
const command = resolved.command;
|
|
1937
|
-
if (validateCommandBinary(command)) {
|
|
1938
|
-
reviewDeps2 = { commandTemplate: command, maxDiffSizeKb: config.maxDiffSizeKb };
|
|
1939
|
-
} else {
|
|
1940
|
-
console.warn(
|
|
1941
|
-
`Warning: binary "${command.split(" ")[0]}" not found. Reviews will be rejected.`
|
|
1942
|
-
);
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
const consumptionDeps2 = {
|
|
1946
|
-
agentId: entry.agentId,
|
|
1947
|
-
limits: config.limits,
|
|
1948
|
-
session: createSessionTracker()
|
|
1949
|
-
};
|
|
1950
|
-
console.log(`Starting anonymous agent ${entry.agentId}...`);
|
|
1951
|
-
startAgent(entry.agentId, config.platformUrl, entry.apiKey, reviewDeps2, consumptionDeps2, {
|
|
1952
|
-
verbose: opts.verbose,
|
|
1953
|
-
stabilityThresholdMs,
|
|
1954
|
-
displayName: entry.name,
|
|
1955
|
-
repoConfig: entry.repoConfig,
|
|
1956
|
-
routerRelay: relay2
|
|
1957
|
-
});
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
if (config.agents !== null) {
|
|
1961
|
-
const routerAgents = [];
|
|
1962
|
-
const validAgents = [];
|
|
1963
|
-
for (const local of config.agents) {
|
|
1964
|
-
if (opts.router || local.router) {
|
|
1965
|
-
routerAgents.push(local);
|
|
1966
|
-
continue;
|
|
1967
|
-
}
|
|
1968
|
-
let cmd;
|
|
1969
|
-
try {
|
|
1970
|
-
cmd = resolveLocalAgentCommand(local, config.agentCommand);
|
|
1971
|
-
} catch (err) {
|
|
1972
|
-
console.warn(
|
|
1973
|
-
`Skipping ${local.model}/${local.tool}: ${err instanceof Error ? err.message : "no command template available"}`
|
|
1974
|
-
);
|
|
1975
|
-
continue;
|
|
1976
|
-
}
|
|
1977
|
-
if (!validateCommandBinary(cmd)) {
|
|
1978
|
-
console.warn(
|
|
1979
|
-
`Skipping ${local.model}/${local.tool}: binary "${cmd.split(" ")[0]}" not found`
|
|
1980
|
-
);
|
|
1981
|
-
continue;
|
|
1982
|
-
}
|
|
1983
|
-
validAgents.push({ local, command: cmd });
|
|
1984
|
-
}
|
|
1985
|
-
const totalValid = validAgents.length + routerAgents.length;
|
|
1986
|
-
if (totalValid === 0 && config.anonymousAgents.length === 0) {
|
|
1987
|
-
console.error("No valid agents in config. Check that tool binaries are installed.");
|
|
1988
|
-
process.exit(1);
|
|
1989
|
-
}
|
|
1990
|
-
let agentsToStart;
|
|
1991
|
-
let routerAgentsToStart;
|
|
1992
|
-
const anonAgentsToStart = [];
|
|
1993
|
-
if (opts.all) {
|
|
1994
|
-
agentsToStart = validAgents;
|
|
1995
|
-
routerAgentsToStart = routerAgents;
|
|
1996
|
-
anonAgentsToStart.push(...config.anonymousAgents);
|
|
1997
|
-
} else if (agentIdOrModel) {
|
|
1998
|
-
const cmdMatch = validAgents.find((a) => a.local.model === agentIdOrModel);
|
|
1999
|
-
const routerMatch = routerAgents.find((a) => a.model === agentIdOrModel);
|
|
2000
|
-
if (!cmdMatch && !routerMatch) {
|
|
2001
|
-
console.error(`No agent with model "${agentIdOrModel}" found in local config.`);
|
|
2002
|
-
console.error("Available agents:");
|
|
2003
|
-
for (const a of validAgents) {
|
|
2004
|
-
console.error(` ${a.local.model} (${a.local.tool})`);
|
|
2005
|
-
}
|
|
2006
|
-
for (const a of routerAgents) {
|
|
2007
|
-
console.error(` ${a.model} (${a.tool}) [router]`);
|
|
2008
|
-
}
|
|
2009
|
-
process.exit(1);
|
|
2010
|
-
}
|
|
2011
|
-
agentsToStart = cmdMatch ? [cmdMatch] : [];
|
|
2012
|
-
routerAgentsToStart = routerMatch ? [routerMatch] : [];
|
|
2013
|
-
} else if (totalValid === 1) {
|
|
2014
|
-
if (validAgents.length === 1) {
|
|
2015
|
-
agentsToStart = [validAgents[0]];
|
|
2016
|
-
routerAgentsToStart = [];
|
|
2017
|
-
console.log(`Using agent ${validAgents[0].local.model} (${validAgents[0].local.tool})`);
|
|
2018
|
-
} else {
|
|
2019
|
-
agentsToStart = [];
|
|
2020
|
-
routerAgentsToStart = [routerAgents[0]];
|
|
2021
|
-
console.log(`Using router agent ${routerAgents[0].model} (${routerAgents[0].tool})`);
|
|
2022
|
-
}
|
|
2023
|
-
} else if (totalValid === 0) {
|
|
2024
|
-
console.error("No valid authenticated agents in config. Use --anonymous or --all.");
|
|
2025
|
-
process.exit(1);
|
|
2026
|
-
} else {
|
|
2027
|
-
console.error("Multiple agents in config. Specify a model name or use --all:");
|
|
2028
|
-
for (const a of validAgents) {
|
|
2029
|
-
console.error(` ${a.local.model} (${a.local.tool})`);
|
|
2030
|
-
}
|
|
2031
|
-
for (const a of routerAgents) {
|
|
2032
|
-
console.error(` ${a.model} (${a.tool}) [router]`);
|
|
2033
|
-
}
|
|
2034
|
-
process.exit(1);
|
|
2035
|
-
}
|
|
2036
|
-
const totalAgents = agentsToStart.length + routerAgentsToStart.length + anonAgentsToStart.length;
|
|
2037
|
-
if (totalAgents > 1) {
|
|
2038
|
-
process.setMaxListeners(process.getMaxListeners() + totalAgents * 2);
|
|
2039
|
-
}
|
|
2040
|
-
let startedCount = 0;
|
|
2041
|
-
let apiKey2;
|
|
2042
|
-
let client2;
|
|
2043
|
-
let serverAgents;
|
|
2044
|
-
if (agentsToStart.length > 0 || routerAgentsToStart.length > 0) {
|
|
2045
|
-
apiKey2 = requireApiKey(config);
|
|
2046
|
-
client2 = new ApiClient(config.platformUrl, apiKey2);
|
|
2047
|
-
try {
|
|
2048
|
-
const res = await client2.get("/api/agents");
|
|
2049
|
-
serverAgents = res.agents;
|
|
2050
|
-
} catch (err) {
|
|
2051
|
-
console.error("Failed to fetch agents:", err instanceof Error ? err.message : err);
|
|
2052
|
-
process.exit(1);
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
for (const selected of agentsToStart) {
|
|
2056
|
-
let agentId2;
|
|
2057
|
-
try {
|
|
2058
|
-
const sync = await syncAgentToServer(client2, serverAgents, selected.local);
|
|
2059
|
-
agentId2 = sync.agentId;
|
|
2060
|
-
if (sync.created) {
|
|
2061
|
-
console.log(`Registered new agent ${agentId2} on platform`);
|
|
2062
|
-
serverAgents.push({
|
|
2063
|
-
id: agentId2,
|
|
2064
|
-
model: selected.local.model,
|
|
2065
|
-
tool: selected.local.tool,
|
|
2066
|
-
isAnonymous: false,
|
|
2067
|
-
status: "offline",
|
|
2068
|
-
repoConfig: null,
|
|
2069
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2070
|
-
});
|
|
2071
|
-
}
|
|
2072
|
-
} catch (err) {
|
|
2073
|
-
console.error(
|
|
2074
|
-
`Failed to sync agent ${selected.local.model} to server:`,
|
|
2075
|
-
err instanceof Error ? err.message : err
|
|
2076
|
-
);
|
|
2077
|
-
continue;
|
|
2078
|
-
}
|
|
2079
|
-
const reviewDeps2 = {
|
|
2080
|
-
commandTemplate: selected.command,
|
|
2081
|
-
maxDiffSizeKb: config.maxDiffSizeKb
|
|
2082
|
-
};
|
|
2083
|
-
const consumptionDeps2 = {
|
|
2084
|
-
agentId: agentId2,
|
|
2085
|
-
limits: resolveAgentLimits(selected.local.limits, config.limits),
|
|
2086
|
-
session: createSessionTracker()
|
|
2087
|
-
};
|
|
2088
|
-
const label = selected.local.name || selected.local.model || "unnamed";
|
|
2089
|
-
console.log(`Starting agent ${label} (${agentId2})...`);
|
|
2090
|
-
startAgent(agentId2, config.platformUrl, apiKey2, reviewDeps2, consumptionDeps2, {
|
|
2091
|
-
verbose: opts.verbose,
|
|
2092
|
-
stabilityThresholdMs,
|
|
2093
|
-
repoConfig: selected.local.repos,
|
|
2094
|
-
label
|
|
2095
|
-
});
|
|
2096
|
-
startedCount++;
|
|
2097
|
-
}
|
|
2098
|
-
for (const local of routerAgentsToStart) {
|
|
2099
|
-
let agentId2;
|
|
2100
|
-
try {
|
|
2101
|
-
const sync = await syncAgentToServer(client2, serverAgents, local);
|
|
2102
|
-
agentId2 = sync.agentId;
|
|
2103
|
-
if (sync.created) {
|
|
2104
|
-
console.log(`Registered new agent ${agentId2} on platform`);
|
|
2105
|
-
serverAgents.push({
|
|
2106
|
-
id: agentId2,
|
|
2107
|
-
model: local.model,
|
|
2108
|
-
tool: local.tool,
|
|
2109
|
-
isAnonymous: false,
|
|
2110
|
-
status: "offline",
|
|
2111
|
-
repoConfig: null,
|
|
2112
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2113
|
-
});
|
|
2114
|
-
}
|
|
2115
|
-
} catch (err) {
|
|
2116
|
-
console.error(
|
|
2117
|
-
`Failed to sync router agent ${local.model} to server:`,
|
|
2118
|
-
err instanceof Error ? err.message : err
|
|
2119
|
-
);
|
|
2120
|
-
continue;
|
|
2121
|
-
}
|
|
2122
|
-
const relay2 = new RouterRelay();
|
|
2123
|
-
relay2.start();
|
|
2124
|
-
const consumptionDeps2 = {
|
|
2125
|
-
agentId: agentId2,
|
|
2126
|
-
limits: resolveAgentLimits(local.limits, config.limits),
|
|
2127
|
-
session: createSessionTracker()
|
|
2128
|
-
};
|
|
2129
|
-
const label = local.name || local.model || "unnamed";
|
|
2130
|
-
console.log(`Starting router agent ${label} (${agentId2})...`);
|
|
2131
|
-
startAgent(agentId2, config.platformUrl, apiKey2, void 0, consumptionDeps2, {
|
|
2132
|
-
verbose: opts.verbose,
|
|
2133
|
-
stabilityThresholdMs,
|
|
2134
|
-
repoConfig: local.repos,
|
|
2135
|
-
label,
|
|
2136
|
-
routerRelay: relay2
|
|
2137
|
-
});
|
|
2138
|
-
startedCount++;
|
|
2139
|
-
}
|
|
2140
|
-
for (const anon of anonAgentsToStart) {
|
|
2141
|
-
let command;
|
|
2142
|
-
try {
|
|
2143
|
-
command = resolveCommandTemplate(
|
|
2144
|
-
DEFAULT_REGISTRY.tools.find((t) => t.name === anon.tool)?.commandTemplate.replaceAll("${MODEL}", anon.model) ?? null
|
|
2145
|
-
);
|
|
2146
|
-
} catch {
|
|
2147
|
-
console.warn(
|
|
2148
|
-
`Skipping anonymous agent ${anon.agentId}: no command template for tool "${anon.tool}"`
|
|
2149
|
-
);
|
|
2150
|
-
continue;
|
|
2151
|
-
}
|
|
2152
|
-
let reviewDeps2;
|
|
2153
|
-
if (validateCommandBinary(command)) {
|
|
2154
|
-
reviewDeps2 = { commandTemplate: command, maxDiffSizeKb: config.maxDiffSizeKb };
|
|
2155
|
-
} else {
|
|
2156
|
-
console.warn(
|
|
2157
|
-
`Warning: binary "${command.split(" ")[0]}" not found for anonymous agent ${anon.agentId}. Reviews will be rejected.`
|
|
2158
|
-
);
|
|
2159
|
-
}
|
|
2160
|
-
const consumptionDeps2 = {
|
|
2161
|
-
agentId: anon.agentId,
|
|
2162
|
-
limits: config.limits,
|
|
2163
|
-
session: createSessionTracker()
|
|
2164
|
-
};
|
|
2165
|
-
const anonLabel = anon.name || anon.model || "anonymous";
|
|
2166
|
-
console.log(`Starting anonymous agent ${anonLabel} (${anon.agentId})...`);
|
|
2167
|
-
startAgent(anon.agentId, config.platformUrl, anon.apiKey, reviewDeps2, consumptionDeps2, {
|
|
2168
|
-
verbose: opts.verbose,
|
|
2169
|
-
stabilityThresholdMs,
|
|
2170
|
-
displayName: anon.name,
|
|
2171
|
-
repoConfig: anon.repoConfig,
|
|
2172
|
-
label: anonLabel
|
|
2173
|
-
});
|
|
2174
|
-
startedCount++;
|
|
2175
|
-
}
|
|
2176
|
-
if (startedCount === 0) {
|
|
2177
|
-
console.error("No agents could be started.");
|
|
2178
|
-
process.exit(1);
|
|
2179
|
-
}
|
|
2180
|
-
return;
|
|
2181
|
-
}
|
|
2182
|
-
const apiKey = requireApiKey(config);
|
|
2183
|
-
const client = new ApiClient(config.platformUrl, apiKey);
|
|
2184
|
-
console.log(
|
|
2185
|
-
"Hint: No agents in local config. Run `opencara agent init` to import, or `opencara agent create` to add agents."
|
|
2186
|
-
);
|
|
2187
|
-
let agentId = agentIdOrModel;
|
|
2188
|
-
if (!agentId) {
|
|
2189
|
-
let res;
|
|
2190
|
-
try {
|
|
2191
|
-
res = await client.get("/api/agents");
|
|
2192
|
-
} catch (err) {
|
|
2193
|
-
console.error("Failed to list agents:", err instanceof Error ? err.message : err);
|
|
2194
|
-
process.exit(1);
|
|
2195
|
-
}
|
|
2196
|
-
if (res.agents.length === 0) {
|
|
2197
|
-
console.error("No agents registered. Run `opencara agent create` first.");
|
|
2198
|
-
process.exit(1);
|
|
2199
|
-
}
|
|
2200
|
-
if (res.agents.length === 1) {
|
|
2201
|
-
agentId = res.agents[0].id;
|
|
2202
|
-
console.log(`Using agent ${agentId}`);
|
|
2203
|
-
} else {
|
|
2204
|
-
console.error("Multiple agents found. Please specify an agent ID:");
|
|
2205
|
-
for (const a of res.agents) {
|
|
2206
|
-
console.error(` ${a.id} ${a.model} / ${a.tool}`);
|
|
2207
|
-
}
|
|
2208
|
-
process.exit(1);
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
let reviewDeps;
|
|
2212
|
-
let relay;
|
|
2213
|
-
if (opts.router) {
|
|
2214
|
-
relay = new RouterRelay();
|
|
2215
|
-
relay.start();
|
|
2216
|
-
} else {
|
|
2217
|
-
try {
|
|
2218
|
-
const commandTemplate = resolveCommandTemplate(config.agentCommand);
|
|
2219
|
-
reviewDeps = {
|
|
2220
|
-
commandTemplate,
|
|
2221
|
-
maxDiffSizeKb: config.maxDiffSizeKb
|
|
2222
|
-
};
|
|
2223
|
-
} catch (err) {
|
|
2224
|
-
console.warn(
|
|
2225
|
-
`Warning: ${err instanceof Error ? err.message : "Could not determine agent command."} Reviews will be rejected.`
|
|
2226
|
-
);
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
const consumptionDeps = {
|
|
2230
|
-
agentId,
|
|
2231
|
-
limits: config.limits,
|
|
2232
|
-
session: createSessionTracker()
|
|
2233
|
-
};
|
|
2234
|
-
console.log(`Starting agent ${agentId}...`);
|
|
2235
|
-
startAgent(agentId, config.platformUrl, apiKey, reviewDeps, consumptionDeps, {
|
|
2236
|
-
verbose: opts.verbose,
|
|
2237
|
-
stabilityThresholdMs,
|
|
2238
|
-
label: agentId,
|
|
2239
|
-
routerRelay: relay
|
|
970
|
+
async function safeError(client, taskId, agentId, error) {
|
|
971
|
+
try {
|
|
972
|
+
await withRetry(() => client.post(`/api/tasks/${taskId}/error`, { agent_id: agentId, error }), {
|
|
973
|
+
maxAttempts: 2
|
|
2240
974
|
});
|
|
975
|
+
} catch (err) {
|
|
976
|
+
console.error(
|
|
977
|
+
` Failed to report error for task ${taskId}: ${err.message} (logged locally)`
|
|
978
|
+
);
|
|
2241
979
|
}
|
|
2242
|
-
);
|
|
2243
|
-
|
|
2244
|
-
// src/commands/stats.ts
|
|
2245
|
-
import { Command as Command3 } from "commander";
|
|
2246
|
-
function formatTrustTier(tier) {
|
|
2247
|
-
const lines = [];
|
|
2248
|
-
const pctPositive = Math.round(tier.positiveRate * 100);
|
|
2249
|
-
lines.push(` Trust: ${tier.label} (${tier.reviewCount} reviews, ${pctPositive}% positive)`);
|
|
2250
|
-
if (tier.nextTier) {
|
|
2251
|
-
const pctProgress = Math.round(tier.progressToNext * 100);
|
|
2252
|
-
const nextLabel = tier.nextTier.charAt(0).toUpperCase() + tier.nextTier.slice(1);
|
|
2253
|
-
lines.push(` Progress to ${nextLabel}: ${pctProgress}%`);
|
|
2254
|
-
}
|
|
2255
|
-
return lines.join("\n");
|
|
2256
980
|
}
|
|
2257
|
-
function
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
if (
|
|
2262
|
-
const
|
|
2263
|
-
|
|
981
|
+
async function executeReviewTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviewDeps, consumptionDeps, routerRelay, signal) {
|
|
982
|
+
let reviewText;
|
|
983
|
+
let verdict;
|
|
984
|
+
let tokensUsed;
|
|
985
|
+
if (routerRelay) {
|
|
986
|
+
const fullPrompt = routerRelay.buildReviewPrompt({
|
|
987
|
+
owner,
|
|
988
|
+
repo,
|
|
989
|
+
reviewMode: "full",
|
|
990
|
+
prompt,
|
|
991
|
+
diffContent
|
|
992
|
+
});
|
|
993
|
+
const response = await routerRelay.sendPrompt(
|
|
994
|
+
"review_request",
|
|
995
|
+
taskId,
|
|
996
|
+
fullPrompt,
|
|
997
|
+
timeoutSeconds
|
|
998
|
+
);
|
|
999
|
+
const parsed = routerRelay.parseReviewResponse(response);
|
|
1000
|
+
reviewText = parsed.review;
|
|
1001
|
+
verdict = parsed.verdict;
|
|
1002
|
+
tokensUsed = estimateTokens(fullPrompt) + estimateTokens(response);
|
|
2264
1003
|
} else {
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
1004
|
+
const result = await executeReview(
|
|
1005
|
+
{
|
|
1006
|
+
taskId,
|
|
1007
|
+
diffContent,
|
|
1008
|
+
prompt,
|
|
1009
|
+
owner,
|
|
1010
|
+
repo,
|
|
1011
|
+
prNumber,
|
|
1012
|
+
timeout: timeoutSeconds,
|
|
1013
|
+
reviewMode: "full"
|
|
1014
|
+
},
|
|
1015
|
+
reviewDeps
|
|
1016
|
+
);
|
|
1017
|
+
reviewText = result.review;
|
|
1018
|
+
verdict = result.verdict;
|
|
1019
|
+
tokensUsed = result.tokensUsed;
|
|
1020
|
+
}
|
|
1021
|
+
await withRetry(
|
|
1022
|
+
() => client.post(`/api/tasks/${taskId}/result`, {
|
|
1023
|
+
agent_id: agentId,
|
|
1024
|
+
type: "review",
|
|
1025
|
+
review_text: reviewText,
|
|
1026
|
+
verdict,
|
|
1027
|
+
tokens_used: tokensUsed
|
|
1028
|
+
}),
|
|
1029
|
+
{ maxAttempts: 3 },
|
|
1030
|
+
signal
|
|
1031
|
+
);
|
|
1032
|
+
recordSessionUsage(consumptionDeps.session, tokensUsed);
|
|
1033
|
+
console.log(` Review submitted (${tokensUsed.toLocaleString()} tokens)`);
|
|
1034
|
+
console.log(formatPostReviewStats(consumptionDeps.session));
|
|
2268
1035
|
}
|
|
2269
|
-
function
|
|
2270
|
-
if (
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
1036
|
+
async function executeSummaryTask(client, agentId, taskId, owner, repo, prNumber, diffContent, prompt, timeoutSeconds, reviews, reviewDeps, consumptionDeps, routerRelay, signal) {
|
|
1037
|
+
if (reviews.length === 0) {
|
|
1038
|
+
return executeReviewTask(
|
|
1039
|
+
client,
|
|
1040
|
+
agentId,
|
|
1041
|
+
taskId,
|
|
1042
|
+
owner,
|
|
1043
|
+
repo,
|
|
1044
|
+
prNumber,
|
|
1045
|
+
diffContent,
|
|
1046
|
+
prompt,
|
|
1047
|
+
timeoutSeconds,
|
|
1048
|
+
reviewDeps,
|
|
1049
|
+
consumptionDeps,
|
|
1050
|
+
routerRelay,
|
|
1051
|
+
signal
|
|
1052
|
+
);
|
|
2282
1053
|
}
|
|
1054
|
+
const summaryReviews = reviews.map((r) => ({
|
|
1055
|
+
agentId: r.agent_id,
|
|
1056
|
+
model: "unknown",
|
|
1057
|
+
tool: "unknown",
|
|
1058
|
+
review: r.review_text,
|
|
1059
|
+
verdict: r.verdict
|
|
1060
|
+
}));
|
|
1061
|
+
let summaryText;
|
|
1062
|
+
let tokensUsed;
|
|
1063
|
+
if (routerRelay) {
|
|
1064
|
+
const fullPrompt = routerRelay.buildSummaryPrompt({
|
|
1065
|
+
owner,
|
|
1066
|
+
repo,
|
|
1067
|
+
prompt,
|
|
1068
|
+
reviews: summaryReviews,
|
|
1069
|
+
diffContent
|
|
1070
|
+
});
|
|
1071
|
+
const response = await routerRelay.sendPrompt(
|
|
1072
|
+
"summary_request",
|
|
1073
|
+
taskId,
|
|
1074
|
+
fullPrompt,
|
|
1075
|
+
timeoutSeconds
|
|
1076
|
+
);
|
|
1077
|
+
summaryText = response;
|
|
1078
|
+
tokensUsed = estimateTokens(fullPrompt) + estimateTokens(response);
|
|
1079
|
+
} else {
|
|
1080
|
+
const result = await executeSummary(
|
|
1081
|
+
{
|
|
1082
|
+
taskId,
|
|
1083
|
+
reviews: summaryReviews,
|
|
1084
|
+
prompt,
|
|
1085
|
+
owner,
|
|
1086
|
+
repo,
|
|
1087
|
+
prNumber,
|
|
1088
|
+
timeout: timeoutSeconds,
|
|
1089
|
+
diffContent
|
|
1090
|
+
},
|
|
1091
|
+
reviewDeps
|
|
1092
|
+
);
|
|
1093
|
+
summaryText = result.summary;
|
|
1094
|
+
tokensUsed = result.tokensUsed;
|
|
1095
|
+
}
|
|
1096
|
+
await withRetry(
|
|
1097
|
+
() => client.post(`/api/tasks/${taskId}/result`, {
|
|
1098
|
+
agent_id: agentId,
|
|
1099
|
+
type: "summary",
|
|
1100
|
+
review_text: summaryText,
|
|
1101
|
+
tokens_used: tokensUsed
|
|
1102
|
+
}),
|
|
1103
|
+
{ maxAttempts: 3 },
|
|
1104
|
+
signal
|
|
1105
|
+
);
|
|
1106
|
+
recordSessionUsage(consumptionDeps.session, tokensUsed);
|
|
1107
|
+
console.log(` Summary submitted (${tokensUsed.toLocaleString()} tokens)`);
|
|
1108
|
+
console.log(formatPostReviewStats(consumptionDeps.session));
|
|
2283
1109
|
}
|
|
2284
|
-
function
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
1110
|
+
function sleep2(ms, signal) {
|
|
1111
|
+
return new Promise((resolve2) => {
|
|
1112
|
+
if (signal?.aborted) {
|
|
1113
|
+
resolve2();
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const timer = setTimeout(resolve2, ms);
|
|
1117
|
+
signal?.addEventListener(
|
|
1118
|
+
"abort",
|
|
1119
|
+
() => {
|
|
1120
|
+
clearTimeout(timer);
|
|
1121
|
+
resolve2();
|
|
1122
|
+
},
|
|
1123
|
+
{ once: true }
|
|
1124
|
+
);
|
|
1125
|
+
});
|
|
2293
1126
|
}
|
|
2294
|
-
async function
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
1127
|
+
async function startAgent(agentId, platformUrl, agentInfo, reviewDeps, consumptionDeps, options) {
|
|
1128
|
+
const client = new ApiClient(platformUrl);
|
|
1129
|
+
const session = consumptionDeps?.session ?? createSessionTracker();
|
|
1130
|
+
const deps = consumptionDeps ?? { agentId, limits: null, session };
|
|
1131
|
+
console.log(`Agent ${agentId} starting...`);
|
|
1132
|
+
console.log(`Platform: ${platformUrl}`);
|
|
1133
|
+
console.log(`Model: ${agentInfo.model} | Tool: ${agentInfo.tool}`);
|
|
1134
|
+
if (!reviewDeps) {
|
|
1135
|
+
console.error("No review command configured. Set command in config.yml");
|
|
1136
|
+
return;
|
|
2299
1137
|
}
|
|
1138
|
+
const abortController = new AbortController();
|
|
1139
|
+
process.on("SIGINT", () => {
|
|
1140
|
+
console.log("\nShutting down...");
|
|
1141
|
+
abortController.abort();
|
|
1142
|
+
});
|
|
1143
|
+
process.on("SIGTERM", () => {
|
|
1144
|
+
abortController.abort();
|
|
1145
|
+
});
|
|
1146
|
+
await pollLoop(client, agentId, reviewDeps, deps, agentInfo, {
|
|
1147
|
+
pollIntervalMs: options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
1148
|
+
routerRelay: options?.routerRelay,
|
|
1149
|
+
reviewOnly: options?.reviewOnly,
|
|
1150
|
+
signal: abortController.signal
|
|
1151
|
+
});
|
|
1152
|
+
console.log("Agent stopped.");
|
|
2300
1153
|
}
|
|
2301
|
-
|
|
1154
|
+
async function startAgentRouter() {
|
|
2302
1155
|
const config = loadConfig();
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
isAnonymous: true,
|
|
2312
|
-
status: "offline",
|
|
2313
|
-
repoConfig: anonEntry.repoConfig ?? null,
|
|
2314
|
-
createdAt: ""
|
|
2315
|
-
};
|
|
2316
|
-
const agentStats = await fetchAgentStats(anonClient, anonEntry.agentId);
|
|
2317
|
-
console.log(formatAgentStats(agent, agentStats));
|
|
2318
|
-
return;
|
|
2319
|
-
}
|
|
1156
|
+
const agentId = crypto.randomUUID();
|
|
1157
|
+
let commandTemplate;
|
|
1158
|
+
let agentConfig;
|
|
1159
|
+
if (config.agents && config.agents.length > 0) {
|
|
1160
|
+
agentConfig = config.agents.find((a) => a.router) ?? config.agents[0];
|
|
1161
|
+
commandTemplate = agentConfig.command ?? config.agentCommand ?? void 0;
|
|
1162
|
+
} else {
|
|
1163
|
+
commandTemplate = config.agentCommand ?? void 0;
|
|
2320
1164
|
}
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
1165
|
+
const router = new RouterRelay();
|
|
1166
|
+
router.start();
|
|
1167
|
+
const reviewDeps = {
|
|
1168
|
+
commandTemplate: commandTemplate ?? "",
|
|
1169
|
+
maxDiffSizeKb: config.maxDiffSizeKb
|
|
1170
|
+
};
|
|
1171
|
+
const session = createSessionTracker();
|
|
1172
|
+
const limits = agentConfig ? resolveAgentLimits(agentConfig.limits, config.limits) : config.limits;
|
|
1173
|
+
const model = agentConfig?.model ?? "unknown";
|
|
1174
|
+
const tool = agentConfig?.tool ?? "unknown";
|
|
1175
|
+
await startAgent(
|
|
1176
|
+
agentId,
|
|
1177
|
+
config.platformUrl,
|
|
1178
|
+
{ model, tool },
|
|
1179
|
+
reviewDeps,
|
|
1180
|
+
{
|
|
1181
|
+
agentId,
|
|
1182
|
+
limits,
|
|
1183
|
+
session
|
|
1184
|
+
},
|
|
1185
|
+
{
|
|
1186
|
+
routerRelay: router,
|
|
1187
|
+
reviewOnly: agentConfig?.review_only
|
|
2343
1188
|
}
|
|
2344
|
-
|
|
2345
|
-
|
|
1189
|
+
);
|
|
1190
|
+
router.stop();
|
|
1191
|
+
}
|
|
1192
|
+
var agentCommand = new Command("agent").description("Manage review agents");
|
|
1193
|
+
agentCommand.command("start").description("Start an agent in polling mode").option("--poll-interval <seconds>", "Poll interval in seconds", "10").option("--agent <index>", "Agent index from config.yml (0-based)", "0").action(async (opts) => {
|
|
1194
|
+
const config = loadConfig();
|
|
1195
|
+
const pollIntervalMs = parseInt(opts.pollInterval, 10) * 1e3;
|
|
1196
|
+
const agentIndex = parseInt(opts.agent, 10);
|
|
1197
|
+
const agentId = crypto.randomUUID();
|
|
1198
|
+
let commandTemplate;
|
|
1199
|
+
let limits = config.limits;
|
|
1200
|
+
let agentConfig;
|
|
1201
|
+
if (config.agents && config.agents.length > agentIndex) {
|
|
1202
|
+
agentConfig = config.agents[agentIndex];
|
|
1203
|
+
commandTemplate = agentConfig.command ?? config.agentCommand ?? void 0;
|
|
1204
|
+
limits = resolveAgentLimits(agentConfig.limits, config.limits);
|
|
1205
|
+
} else {
|
|
1206
|
+
commandTemplate = config.agentCommand ?? void 0;
|
|
2346
1207
|
}
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
model: "unknown",
|
|
2353
|
-
tool: "unknown",
|
|
2354
|
-
isAnonymous: false,
|
|
2355
|
-
status: "offline",
|
|
2356
|
-
repoConfig: null,
|
|
2357
|
-
createdAt: ""
|
|
2358
|
-
};
|
|
2359
|
-
try {
|
|
2360
|
-
const agentsRes2 = await client.get("/api/agents");
|
|
2361
|
-
const found = agentsRes2.agents.find((a) => a.id === opts.agent);
|
|
2362
|
-
if (found) {
|
|
2363
|
-
agent.model = found.model;
|
|
2364
|
-
agent.tool = found.tool;
|
|
2365
|
-
}
|
|
2366
|
-
} catch {
|
|
2367
|
-
}
|
|
2368
|
-
const agentStats = await fetchAgentStats(client, opts.agent);
|
|
2369
|
-
console.log(formatAgentStats(agent, agentStats));
|
|
1208
|
+
if (!commandTemplate) {
|
|
1209
|
+
console.error(
|
|
1210
|
+
"No command configured. Set agent_command or agents[].command in ~/.opencara/config.yml"
|
|
1211
|
+
);
|
|
1212
|
+
process.exit(1);
|
|
2370
1213
|
return;
|
|
2371
1214
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
agentsRes = await client.get("/api/agents");
|
|
2375
|
-
} catch (err) {
|
|
2376
|
-
console.error("Failed to list agents:", err instanceof Error ? err.message : err);
|
|
1215
|
+
if (!validateCommandBinary(commandTemplate)) {
|
|
1216
|
+
console.error(`Command binary not found: ${commandTemplate.split(" ")[0]}`);
|
|
2377
1217
|
process.exit(1);
|
|
2378
|
-
}
|
|
2379
|
-
if (agentsRes.agents.length === 0 && config.anonymousAgents.length === 0) {
|
|
2380
|
-
console.log("No agents registered. Run `opencara agent create` to register one.");
|
|
2381
1218
|
return;
|
|
2382
1219
|
}
|
|
2383
|
-
const
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
tool
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
1220
|
+
const reviewDeps = {
|
|
1221
|
+
commandTemplate,
|
|
1222
|
+
maxDiffSizeKb: config.maxDiffSizeKb
|
|
1223
|
+
};
|
|
1224
|
+
const isRouter = agentConfig?.router === true;
|
|
1225
|
+
let routerRelay;
|
|
1226
|
+
if (isRouter) {
|
|
1227
|
+
routerRelay = new RouterRelay();
|
|
1228
|
+
routerRelay.start();
|
|
1229
|
+
}
|
|
1230
|
+
const session = createSessionTracker();
|
|
1231
|
+
const model = agentConfig?.model ?? "unknown";
|
|
1232
|
+
const tool = agentConfig?.tool ?? "unknown";
|
|
1233
|
+
try {
|
|
1234
|
+
await startAgent(
|
|
1235
|
+
agentId,
|
|
1236
|
+
config.platformUrl,
|
|
1237
|
+
{ model, tool },
|
|
1238
|
+
reviewDeps,
|
|
1239
|
+
{
|
|
1240
|
+
agentId,
|
|
1241
|
+
limits,
|
|
1242
|
+
session
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
pollIntervalMs,
|
|
1246
|
+
routerRelay,
|
|
1247
|
+
reviewOnly: agentConfig?.review_only
|
|
1248
|
+
}
|
|
1249
|
+
);
|
|
1250
|
+
} finally {
|
|
1251
|
+
routerRelay?.stop();
|
|
2415
1252
|
}
|
|
2416
|
-
console.log(outputs.join("\n\n"));
|
|
2417
1253
|
});
|
|
2418
1254
|
|
|
2419
1255
|
// src/index.ts
|
|
2420
|
-
var program = new
|
|
2421
|
-
program.addCommand(loginCommand);
|
|
1256
|
+
var program = new Command2().name("opencara").description("OpenCara \u2014 distributed AI code review agent").version("0.10.0");
|
|
2422
1257
|
program.addCommand(agentCommand);
|
|
2423
|
-
program.addCommand(statsCommand);
|
|
2424
1258
|
program.action(() => {
|
|
2425
1259
|
startAgentRouter();
|
|
2426
1260
|
});
|