opencara 0.2.0 → 0.2.2
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 +1938 -10
- package/package.json +4 -2
- package/dist/commands/agent.d.ts +0 -46
- package/dist/commands/agent.d.ts.map +0 -1
- package/dist/commands/agent.js +0 -924
- package/dist/commands/agent.js.map +0 -1
- package/dist/commands/login.d.ts +0 -5
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js +0 -102
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/stats.d.ts +0 -9
- package/dist/commands/stats.d.ts.map +0 -1
- package/dist/commands/stats.js +0 -187
- package/dist/commands/stats.js.map +0 -1
- package/dist/config.d.ts +0 -48
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -227
- package/dist/config.js.map +0 -1
- package/dist/consumption.d.ts +0 -21
- package/dist/consumption.d.ts.map +0 -1
- package/dist/consumption.js +0 -18
- package/dist/consumption.js.map +0 -1
- package/dist/http.d.ts +0 -14
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -59
- package/dist/http.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/reconnect.d.ts +0 -10
- package/dist/reconnect.d.ts.map +0 -1
- package/dist/reconnect.js +0 -17
- package/dist/reconnect.js.map +0 -1
- package/dist/review.d.ts +0 -34
- package/dist/review.d.ts.map +0 -1
- package/dist/review.js +0 -109
- package/dist/review.js.map +0 -1
- package/dist/summary.d.ts +0 -34
- package/dist/summary.d.ts.map +0 -1
- package/dist/summary.js +0 -90
- package/dist/summary.js.map +0 -1
- package/dist/tool-executor.d.ts +0 -50
- package/dist/tool-executor.d.ts.map +0 -1
- package/dist/tool-executor.js +0 -232
- package/dist/tool-executor.js.map +0 -1
package/dist/commands/agent.js
DELETED
|
@@ -1,924 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import WebSocket from 'ws';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
|
-
import { DEFAULT_REGISTRY, } from '@opencara/shared';
|
|
5
|
-
import { loadConfig, saveConfig, requireApiKey, resolveAgentLimits, } from '../config.js';
|
|
6
|
-
import { ApiClient } from '../http.js';
|
|
7
|
-
import { calculateDelay, sleep, DEFAULT_RECONNECT_OPTIONS } from '../reconnect.js';
|
|
8
|
-
import { executeReview, DiffTooLargeError } from '../review.js';
|
|
9
|
-
import { executeSummary, InputTooLargeError } from '../summary.js';
|
|
10
|
-
import { resolveCommandTemplate, validateCommandBinary } from '../tool-executor.js';
|
|
11
|
-
import { checkConsumptionLimits, createSessionTracker, recordSessionUsage, formatPostReviewStats, } from '../consumption.js';
|
|
12
|
-
/** Minimum time (ms) a connection must be alive before we reset the attempt counter */
|
|
13
|
-
const CONNECTION_STABILITY_THRESHOLD_MS = 30_000;
|
|
14
|
-
function formatTable(agents, trustLabels) {
|
|
15
|
-
if (agents.length === 0) {
|
|
16
|
-
console.log('No agents registered. Run `opencara agent create` to register one.');
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const header = [
|
|
20
|
-
'ID'.padEnd(38),
|
|
21
|
-
'Model'.padEnd(22),
|
|
22
|
-
'Tool'.padEnd(16),
|
|
23
|
-
'Status'.padEnd(10),
|
|
24
|
-
'Trust',
|
|
25
|
-
].join('');
|
|
26
|
-
console.log(header);
|
|
27
|
-
for (const a of agents) {
|
|
28
|
-
const trust = trustLabels?.get(a.id) ?? '--';
|
|
29
|
-
console.log([a.id.padEnd(38), a.model.padEnd(22), a.tool.padEnd(16), a.status.padEnd(10), trust].join(''));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function buildWsUrl(platformUrl, agentId, apiKey) {
|
|
33
|
-
return (platformUrl.replace(/^https:\/\//, 'wss://').replace(/^http:\/\//, 'ws://') +
|
|
34
|
-
`/ws/agent/${agentId}?token=${encodeURIComponent(apiKey)}`);
|
|
35
|
-
}
|
|
36
|
-
export { buildWsUrl };
|
|
37
|
-
const HEARTBEAT_TIMEOUT_MS = 90_000;
|
|
38
|
-
export const STABILITY_THRESHOLD_MIN_MS = 5_000;
|
|
39
|
-
export const STABILITY_THRESHOLD_MAX_MS = 300_000;
|
|
40
|
-
/** Interval for sending RFC 6455 WebSocket ping frames to keep the proxy layer alive */
|
|
41
|
-
const WS_PING_INTERVAL_MS = 20_000;
|
|
42
|
-
export function startAgent(agentId, platformUrl, apiKey, reviewDeps, consumptionDeps, options) {
|
|
43
|
-
const verbose = options?.verbose ?? false;
|
|
44
|
-
const stabilityThreshold = options?.stabilityThresholdMs ?? CONNECTION_STABILITY_THRESHOLD_MS;
|
|
45
|
-
const repoConfig = options?.repoConfig;
|
|
46
|
-
let attempt = 0;
|
|
47
|
-
let intentionalClose = false;
|
|
48
|
-
let heartbeatTimer = null;
|
|
49
|
-
let wsPingTimer = null;
|
|
50
|
-
let currentWs = null;
|
|
51
|
-
let connectionOpenedAt = null;
|
|
52
|
-
let stabilityTimer = null;
|
|
53
|
-
function clearHeartbeatTimer() {
|
|
54
|
-
if (heartbeatTimer) {
|
|
55
|
-
clearTimeout(heartbeatTimer);
|
|
56
|
-
heartbeatTimer = null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
function clearStabilityTimer() {
|
|
60
|
-
if (stabilityTimer) {
|
|
61
|
-
clearTimeout(stabilityTimer);
|
|
62
|
-
stabilityTimer = null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
function clearWsPingTimer() {
|
|
66
|
-
if (wsPingTimer) {
|
|
67
|
-
clearInterval(wsPingTimer);
|
|
68
|
-
wsPingTimer = null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
function shutdown() {
|
|
72
|
-
intentionalClose = true;
|
|
73
|
-
clearHeartbeatTimer();
|
|
74
|
-
clearStabilityTimer();
|
|
75
|
-
clearWsPingTimer();
|
|
76
|
-
if (currentWs)
|
|
77
|
-
currentWs.close();
|
|
78
|
-
console.log('Disconnected.');
|
|
79
|
-
process.exit(0);
|
|
80
|
-
}
|
|
81
|
-
process.once('SIGINT', shutdown);
|
|
82
|
-
process.once('SIGTERM', shutdown);
|
|
83
|
-
function connect() {
|
|
84
|
-
const url = buildWsUrl(platformUrl, agentId, apiKey);
|
|
85
|
-
const ws = new WebSocket(url);
|
|
86
|
-
currentWs = ws;
|
|
87
|
-
function resetHeartbeatTimer() {
|
|
88
|
-
clearHeartbeatTimer();
|
|
89
|
-
heartbeatTimer = setTimeout(() => {
|
|
90
|
-
console.log('No heartbeat received in 90s. Reconnecting...');
|
|
91
|
-
ws.terminate();
|
|
92
|
-
}, HEARTBEAT_TIMEOUT_MS);
|
|
93
|
-
}
|
|
94
|
-
ws.on('open', () => {
|
|
95
|
-
connectionOpenedAt = Date.now();
|
|
96
|
-
console.log('Connected to platform.');
|
|
97
|
-
resetHeartbeatTimer();
|
|
98
|
-
// Send RFC 6455 WebSocket ping frames to keep the Cloudflare proxy layer alive
|
|
99
|
-
clearWsPingTimer();
|
|
100
|
-
wsPingTimer = setInterval(() => {
|
|
101
|
-
try {
|
|
102
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
103
|
-
ws.ping();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
// Swallow ping errors — socket may be closing
|
|
108
|
-
}
|
|
109
|
-
}, WS_PING_INTERVAL_MS);
|
|
110
|
-
if (verbose) {
|
|
111
|
-
console.log(`[verbose] Connection opened at ${new Date(connectionOpenedAt).toISOString()}`);
|
|
112
|
-
}
|
|
113
|
-
// Deferred attempt reset: only reset after connection is stable
|
|
114
|
-
clearStabilityTimer();
|
|
115
|
-
stabilityTimer = setTimeout(() => {
|
|
116
|
-
if (verbose) {
|
|
117
|
-
console.log(`[verbose] Connection stable for ${stabilityThreshold / 1000}s — resetting reconnect counter`);
|
|
118
|
-
}
|
|
119
|
-
attempt = 0;
|
|
120
|
-
}, stabilityThreshold);
|
|
121
|
-
});
|
|
122
|
-
ws.on('message', (data) => {
|
|
123
|
-
let msg;
|
|
124
|
-
try {
|
|
125
|
-
msg = JSON.parse(data.toString());
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
handleMessage(ws, msg, resetHeartbeatTimer, reviewDeps, consumptionDeps, verbose, repoConfig);
|
|
131
|
-
});
|
|
132
|
-
ws.on('close', (code, reason) => {
|
|
133
|
-
if (intentionalClose)
|
|
134
|
-
return;
|
|
135
|
-
if (ws !== currentWs)
|
|
136
|
-
return; // Stale WS — don't clear active timers
|
|
137
|
-
clearHeartbeatTimer();
|
|
138
|
-
clearStabilityTimer();
|
|
139
|
-
clearWsPingTimer();
|
|
140
|
-
// Log connection lifetime
|
|
141
|
-
if (connectionOpenedAt) {
|
|
142
|
-
const lifetimeMs = Date.now() - connectionOpenedAt;
|
|
143
|
-
const lifetimeSec = (lifetimeMs / 1000).toFixed(1);
|
|
144
|
-
console.log(`Disconnected (code=${code}, reason=${reason.toString()}). Connection was alive for ${lifetimeSec}s.`);
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
console.log(`Disconnected (code=${code}, reason=${reason.toString()}).`);
|
|
148
|
-
}
|
|
149
|
-
if (code === 4002) {
|
|
150
|
-
console.log('Connection replaced by server — not reconnecting.');
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
connectionOpenedAt = null;
|
|
154
|
-
reconnect();
|
|
155
|
-
});
|
|
156
|
-
ws.on('pong', () => {
|
|
157
|
-
if (verbose) {
|
|
158
|
-
console.log(`[verbose] WS pong received at ${new Date().toISOString()}`);
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
ws.on('error', (err) => {
|
|
162
|
-
console.error(`WebSocket error: ${err.message}`);
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
async function reconnect() {
|
|
166
|
-
const delay = calculateDelay(attempt, DEFAULT_RECONNECT_OPTIONS);
|
|
167
|
-
const delaySec = (delay / 1000).toFixed(1);
|
|
168
|
-
attempt++;
|
|
169
|
-
console.log(`Reconnecting in ${delaySec}s... (attempt ${attempt})`);
|
|
170
|
-
await sleep(delay);
|
|
171
|
-
connect();
|
|
172
|
-
}
|
|
173
|
-
connect();
|
|
174
|
-
}
|
|
175
|
-
function trySend(ws, data) {
|
|
176
|
-
try {
|
|
177
|
-
ws.send(JSON.stringify(data));
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
console.error('Failed to send message — WebSocket may be closed');
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
async function logPostReviewStats(type, verdict, tokensUsed, tokensEstimated, consumptionDeps) {
|
|
184
|
-
const estimateTag = tokensEstimated ? ' ~' : ' ';
|
|
185
|
-
if (!consumptionDeps) {
|
|
186
|
-
if (verdict) {
|
|
187
|
-
console.log(`${type} complete: ${verdict} (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ', estimated' : ''})`);
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
console.log(`${type} complete (${estimateTag}${tokensUsed} tokens${tokensEstimated ? ', estimated' : ''})`);
|
|
191
|
-
}
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
recordSessionUsage(consumptionDeps.session, tokensUsed);
|
|
195
|
-
if (verdict) {
|
|
196
|
-
console.log(`${type} complete: ${verdict} (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ', estimated' : ''})`);
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
console.log(`${type} complete (${estimateTag}${tokensUsed.toLocaleString()} tokens${tokensEstimated ? ', estimated' : ''})`);
|
|
200
|
-
}
|
|
201
|
-
console.log(formatPostReviewStats(tokensUsed, consumptionDeps.session, consumptionDeps.limits));
|
|
202
|
-
}
|
|
203
|
-
export function handleMessage(ws, msg, resetHeartbeat, reviewDeps, consumptionDeps, verbose, repoConfig) {
|
|
204
|
-
switch (msg.type) {
|
|
205
|
-
case 'connected':
|
|
206
|
-
console.log(`Authenticated. Protocol v${msg.version ?? 'unknown'}`);
|
|
207
|
-
// Send agent preferences (repo config) to the platform
|
|
208
|
-
trySend(ws, {
|
|
209
|
-
type: 'agent_preferences',
|
|
210
|
-
id: crypto.randomUUID(),
|
|
211
|
-
timestamp: Date.now(),
|
|
212
|
-
repoConfig: repoConfig ?? { mode: 'all' },
|
|
213
|
-
});
|
|
214
|
-
break;
|
|
215
|
-
case 'heartbeat_ping':
|
|
216
|
-
ws.send(JSON.stringify({ type: 'heartbeat_pong', timestamp: Date.now() }));
|
|
217
|
-
if (verbose) {
|
|
218
|
-
console.log(`[verbose] Heartbeat ping received, pong sent at ${new Date().toISOString()}`);
|
|
219
|
-
}
|
|
220
|
-
if (resetHeartbeat)
|
|
221
|
-
resetHeartbeat();
|
|
222
|
-
break;
|
|
223
|
-
case 'review_request': {
|
|
224
|
-
const request = msg;
|
|
225
|
-
console.log(`Review request: task ${request.taskId} for ${request.project.owner}/${request.project.repo}#${request.pr.number}`);
|
|
226
|
-
if (!reviewDeps) {
|
|
227
|
-
ws.send(JSON.stringify({
|
|
228
|
-
type: 'review_rejected',
|
|
229
|
-
id: crypto.randomUUID(),
|
|
230
|
-
timestamp: Date.now(),
|
|
231
|
-
taskId: request.taskId,
|
|
232
|
-
reason: 'Review execution not configured',
|
|
233
|
-
}));
|
|
234
|
-
break;
|
|
235
|
-
}
|
|
236
|
-
void (async () => {
|
|
237
|
-
// Check consumption limits before executing
|
|
238
|
-
if (consumptionDeps) {
|
|
239
|
-
const limitResult = await checkConsumptionLimits(consumptionDeps.agentId, consumptionDeps.limits);
|
|
240
|
-
if (!limitResult.allowed) {
|
|
241
|
-
trySend(ws, {
|
|
242
|
-
type: 'review_rejected',
|
|
243
|
-
id: crypto.randomUUID(),
|
|
244
|
-
timestamp: Date.now(),
|
|
245
|
-
taskId: request.taskId,
|
|
246
|
-
reason: limitResult.reason ?? 'consumption_limit_exceeded',
|
|
247
|
-
});
|
|
248
|
-
console.log(`Review rejected: ${limitResult.reason}`);
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
try {
|
|
253
|
-
const result = await executeReview({
|
|
254
|
-
taskId: request.taskId,
|
|
255
|
-
diffContent: request.diffContent,
|
|
256
|
-
prompt: request.project.prompt,
|
|
257
|
-
owner: request.project.owner,
|
|
258
|
-
repo: request.project.repo,
|
|
259
|
-
prNumber: request.pr.number,
|
|
260
|
-
timeout: request.timeout,
|
|
261
|
-
reviewMode: request.reviewMode ?? 'full',
|
|
262
|
-
}, reviewDeps);
|
|
263
|
-
trySend(ws, {
|
|
264
|
-
type: 'review_complete',
|
|
265
|
-
id: crypto.randomUUID(),
|
|
266
|
-
timestamp: Date.now(),
|
|
267
|
-
taskId: request.taskId,
|
|
268
|
-
review: result.review,
|
|
269
|
-
verdict: result.verdict,
|
|
270
|
-
tokensUsed: result.tokensUsed,
|
|
271
|
-
});
|
|
272
|
-
await logPostReviewStats('Review', result.verdict, result.tokensUsed, result.tokensEstimated, consumptionDeps);
|
|
273
|
-
}
|
|
274
|
-
catch (err) {
|
|
275
|
-
if (err instanceof DiffTooLargeError) {
|
|
276
|
-
trySend(ws, {
|
|
277
|
-
type: 'review_rejected',
|
|
278
|
-
id: crypto.randomUUID(),
|
|
279
|
-
timestamp: Date.now(),
|
|
280
|
-
taskId: request.taskId,
|
|
281
|
-
reason: err.message,
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
trySend(ws, {
|
|
286
|
-
type: 'review_error',
|
|
287
|
-
id: crypto.randomUUID(),
|
|
288
|
-
timestamp: Date.now(),
|
|
289
|
-
taskId: request.taskId,
|
|
290
|
-
error: err instanceof Error ? err.message : 'Unknown error',
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
console.error('Review failed:', err);
|
|
294
|
-
}
|
|
295
|
-
})();
|
|
296
|
-
break;
|
|
297
|
-
}
|
|
298
|
-
case 'summary_request': {
|
|
299
|
-
const summaryRequest = msg;
|
|
300
|
-
console.log(`Summary request: task ${summaryRequest.taskId} for ${summaryRequest.project.owner}/${summaryRequest.project.repo}#${summaryRequest.pr.number} (${summaryRequest.reviews.length} reviews)`);
|
|
301
|
-
if (!reviewDeps) {
|
|
302
|
-
trySend(ws, {
|
|
303
|
-
type: 'review_rejected',
|
|
304
|
-
id: crypto.randomUUID(),
|
|
305
|
-
timestamp: Date.now(),
|
|
306
|
-
taskId: summaryRequest.taskId,
|
|
307
|
-
reason: 'Review tool not configured',
|
|
308
|
-
});
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
void (async () => {
|
|
312
|
-
// Check consumption limits before executing
|
|
313
|
-
if (consumptionDeps) {
|
|
314
|
-
const limitResult = await checkConsumptionLimits(consumptionDeps.agentId, consumptionDeps.limits);
|
|
315
|
-
if (!limitResult.allowed) {
|
|
316
|
-
trySend(ws, {
|
|
317
|
-
type: 'review_rejected',
|
|
318
|
-
id: crypto.randomUUID(),
|
|
319
|
-
timestamp: Date.now(),
|
|
320
|
-
taskId: summaryRequest.taskId,
|
|
321
|
-
reason: limitResult.reason ?? 'consumption_limit_exceeded',
|
|
322
|
-
});
|
|
323
|
-
console.log(`Summary rejected: ${limitResult.reason}`);
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
try {
|
|
328
|
-
const result = await executeSummary({
|
|
329
|
-
taskId: summaryRequest.taskId,
|
|
330
|
-
reviews: summaryRequest.reviews,
|
|
331
|
-
prompt: summaryRequest.project.prompt,
|
|
332
|
-
owner: summaryRequest.project.owner,
|
|
333
|
-
repo: summaryRequest.project.repo,
|
|
334
|
-
prNumber: summaryRequest.pr.number,
|
|
335
|
-
timeout: summaryRequest.timeout,
|
|
336
|
-
diffContent: summaryRequest.diffContent ?? '',
|
|
337
|
-
}, reviewDeps);
|
|
338
|
-
trySend(ws, {
|
|
339
|
-
type: 'summary_complete',
|
|
340
|
-
id: crypto.randomUUID(),
|
|
341
|
-
timestamp: Date.now(),
|
|
342
|
-
taskId: summaryRequest.taskId,
|
|
343
|
-
summary: result.summary,
|
|
344
|
-
tokensUsed: result.tokensUsed,
|
|
345
|
-
});
|
|
346
|
-
await logPostReviewStats('Summary', undefined, result.tokensUsed, result.tokensEstimated, consumptionDeps);
|
|
347
|
-
}
|
|
348
|
-
catch (err) {
|
|
349
|
-
if (err instanceof InputTooLargeError) {
|
|
350
|
-
trySend(ws, {
|
|
351
|
-
type: 'review_rejected',
|
|
352
|
-
id: crypto.randomUUID(),
|
|
353
|
-
timestamp: Date.now(),
|
|
354
|
-
taskId: summaryRequest.taskId,
|
|
355
|
-
reason: err.message,
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
else {
|
|
359
|
-
trySend(ws, {
|
|
360
|
-
type: 'review_error',
|
|
361
|
-
id: crypto.randomUUID(),
|
|
362
|
-
timestamp: Date.now(),
|
|
363
|
-
taskId: summaryRequest.taskId,
|
|
364
|
-
error: err instanceof Error ? err.message : 'Summary failed',
|
|
365
|
-
});
|
|
366
|
-
}
|
|
367
|
-
console.error('Summary failed:', err);
|
|
368
|
-
}
|
|
369
|
-
})();
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
case 'error':
|
|
373
|
-
console.error(`Platform error: ${msg.code ?? 'unknown'}`);
|
|
374
|
-
if (msg.code === 'auth_revoked')
|
|
375
|
-
process.exit(1);
|
|
376
|
-
break;
|
|
377
|
-
default:
|
|
378
|
-
break;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
/** Sync a local agent to the server: find existing by model+tool or create new */
|
|
382
|
-
async function syncAgentToServer(client, serverAgents, localAgent) {
|
|
383
|
-
const existing = serverAgents.find((a) => a.model === localAgent.model && a.tool === localAgent.tool);
|
|
384
|
-
if (existing) {
|
|
385
|
-
return { agentId: existing.id, created: false };
|
|
386
|
-
}
|
|
387
|
-
const body = { model: localAgent.model, tool: localAgent.tool };
|
|
388
|
-
if (localAgent.repos) {
|
|
389
|
-
body.repoConfig = localAgent.repos;
|
|
390
|
-
}
|
|
391
|
-
const created = await client.post('/api/agents', body);
|
|
392
|
-
return { agentId: created.id, created: true };
|
|
393
|
-
}
|
|
394
|
-
/** Resolve the effective command template for a local agent */
|
|
395
|
-
function resolveLocalAgentCommand(localAgent, globalAgentCommand) {
|
|
396
|
-
const effectiveCommand = localAgent.command ?? globalAgentCommand;
|
|
397
|
-
return resolveCommandTemplate(effectiveCommand);
|
|
398
|
-
}
|
|
399
|
-
export { syncAgentToServer, resolveLocalAgentCommand };
|
|
400
|
-
export const agentCommand = new Command('agent').description('Manage review agents');
|
|
401
|
-
agentCommand
|
|
402
|
-
.command('create')
|
|
403
|
-
.description('Add an agent to local config (interactive or via flags)')
|
|
404
|
-
.option('--model <model>', 'AI model name (e.g., claude-opus-4-6)')
|
|
405
|
-
.option('--tool <tool>', 'Review tool name (e.g., claude-code)')
|
|
406
|
-
.option('--command <cmd>', 'Custom command template (bypasses registry lookup)')
|
|
407
|
-
.action(async (opts) => {
|
|
408
|
-
const config = loadConfig();
|
|
409
|
-
requireApiKey(config);
|
|
410
|
-
let model;
|
|
411
|
-
let tool;
|
|
412
|
-
let command = opts.command;
|
|
413
|
-
if (opts.model && opts.tool) {
|
|
414
|
-
// Non-interactive mode
|
|
415
|
-
model = opts.model;
|
|
416
|
-
tool = opts.tool;
|
|
417
|
-
}
|
|
418
|
-
else if (opts.model || opts.tool) {
|
|
419
|
-
console.error('Both --model and --tool are required in non-interactive mode.');
|
|
420
|
-
process.exit(1);
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
// Interactive mode: fetch registry and prompt
|
|
424
|
-
const client = new ApiClient(config.platformUrl, config.apiKey);
|
|
425
|
-
let registry;
|
|
426
|
-
try {
|
|
427
|
-
registry = await client.get('/api/registry');
|
|
428
|
-
}
|
|
429
|
-
catch {
|
|
430
|
-
console.warn('Could not fetch registry from server. Using built-in defaults.');
|
|
431
|
-
registry = DEFAULT_REGISTRY;
|
|
432
|
-
}
|
|
433
|
-
const { search, input } = await import('@inquirer/prompts');
|
|
434
|
-
const searchTheme = {
|
|
435
|
-
style: {
|
|
436
|
-
keysHelpTip: (keys) => keys.map(([key, action]) => `${key} ${action}`).join(', ') + ', ^C exit',
|
|
437
|
-
},
|
|
438
|
-
};
|
|
439
|
-
const existingAgents = config.agents ?? [];
|
|
440
|
-
const toolChoices = registry.tools.map((t) => ({
|
|
441
|
-
name: t.displayName,
|
|
442
|
-
value: t.name,
|
|
443
|
-
}));
|
|
444
|
-
try {
|
|
445
|
-
// Loop: select tool → select model → check duplicate → if dup, restart
|
|
446
|
-
while (true) {
|
|
447
|
-
// Step 1: Select tool
|
|
448
|
-
tool = await search({
|
|
449
|
-
message: 'Select a tool:',
|
|
450
|
-
theme: searchTheme,
|
|
451
|
-
source: (term) => {
|
|
452
|
-
const q = (term ?? '').toLowerCase();
|
|
453
|
-
return toolChoices.filter((c) => c.name.toLowerCase().includes(q) || c.value.toLowerCase().includes(q));
|
|
454
|
-
},
|
|
455
|
-
});
|
|
456
|
-
// Step 2: Select model — compatible first, others dimmed
|
|
457
|
-
const compatible = registry.models.filter((m) => m.tools.includes(tool));
|
|
458
|
-
const incompatible = registry.models.filter((m) => !m.tools.includes(tool));
|
|
459
|
-
const modelChoices = [
|
|
460
|
-
...compatible.map((m) => ({
|
|
461
|
-
name: m.displayName,
|
|
462
|
-
value: m.name,
|
|
463
|
-
})),
|
|
464
|
-
...incompatible.map((m) => ({
|
|
465
|
-
name: `\x1b[38;5;249m${m.displayName}\x1b[0m`,
|
|
466
|
-
value: m.name,
|
|
467
|
-
})),
|
|
468
|
-
];
|
|
469
|
-
model = await search({
|
|
470
|
-
message: 'Select a model:',
|
|
471
|
-
theme: searchTheme,
|
|
472
|
-
source: (term) => {
|
|
473
|
-
const q = (term ?? '').toLowerCase();
|
|
474
|
-
return modelChoices.filter((c) => c.value.toLowerCase().includes(q) || c.name.toLowerCase().includes(q));
|
|
475
|
-
},
|
|
476
|
-
});
|
|
477
|
-
// Check duplicate before proceeding
|
|
478
|
-
const isDup = existingAgents.some((a) => a.model === model && a.tool === tool);
|
|
479
|
-
if (isDup) {
|
|
480
|
-
console.warn(`"${model}" / "${tool}" already exists in config. Choose again.`);
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
// Warn if model isn't compatible with selected tool
|
|
484
|
-
const modelEntry = registry.models.find((m) => m.name === model);
|
|
485
|
-
if (modelEntry && !modelEntry.tools.includes(tool)) {
|
|
486
|
-
console.warn(`Warning: "${model}" is not listed as compatible with "${tool}".`);
|
|
487
|
-
}
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
// Step 3: Resolve default command and let user edit it
|
|
491
|
-
const toolEntry = registry.tools.find((t) => t.name === tool);
|
|
492
|
-
const defaultCommand = toolEntry
|
|
493
|
-
? toolEntry.commandTemplate.replaceAll('${MODEL}', model)
|
|
494
|
-
: `${tool} --model ${model} -p \${PROMPT}`;
|
|
495
|
-
command = await input({
|
|
496
|
-
message: 'Command:',
|
|
497
|
-
default: defaultCommand,
|
|
498
|
-
prefill: 'editable',
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
catch (err) {
|
|
502
|
-
if (err && typeof err === 'object' && 'name' in err && err.name === 'ExitPromptError') {
|
|
503
|
-
console.log('Cancelled.');
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
throw err;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
// Resolve command from registry if not set (non-interactive mode)
|
|
510
|
-
if (!command) {
|
|
511
|
-
const toolEntry = DEFAULT_REGISTRY.tools.find((t) => t.name === tool);
|
|
512
|
-
if (toolEntry) {
|
|
513
|
-
command = toolEntry.commandTemplate.replaceAll('${MODEL}', model);
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
console.error(`No command template for tool "${tool}". Use --command to specify one.`);
|
|
517
|
-
process.exit(1);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
// Validate binary
|
|
521
|
-
if (validateCommandBinary(command)) {
|
|
522
|
-
console.log(`Verifying... binary found.`);
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
console.warn(`Warning: binary for command "${command.split(' ')[0]}" not found on this machine.`);
|
|
526
|
-
}
|
|
527
|
-
// Write to local config
|
|
528
|
-
const newAgent = { model, tool, command };
|
|
529
|
-
if (config.agents === null) {
|
|
530
|
-
config.agents = [];
|
|
531
|
-
}
|
|
532
|
-
const isDuplicate = config.agents.some((a) => a.model === model && a.tool === tool);
|
|
533
|
-
if (isDuplicate) {
|
|
534
|
-
console.error(`Agent with model "${model}" and tool "${tool}" already exists in config.`);
|
|
535
|
-
process.exit(1);
|
|
536
|
-
}
|
|
537
|
-
config.agents.push(newAgent);
|
|
538
|
-
saveConfig(config);
|
|
539
|
-
console.log('Agent added to config:');
|
|
540
|
-
console.log(` Model: ${model}`);
|
|
541
|
-
console.log(` Tool: ${tool}`);
|
|
542
|
-
console.log(` Command: ${command}`);
|
|
543
|
-
});
|
|
544
|
-
agentCommand
|
|
545
|
-
.command('init')
|
|
546
|
-
.description('Import server-side agents into local config')
|
|
547
|
-
.action(async () => {
|
|
548
|
-
const config = loadConfig();
|
|
549
|
-
const apiKey = requireApiKey(config);
|
|
550
|
-
const client = new ApiClient(config.platformUrl, apiKey);
|
|
551
|
-
let res;
|
|
552
|
-
try {
|
|
553
|
-
res = await client.get('/api/agents');
|
|
554
|
-
}
|
|
555
|
-
catch (err) {
|
|
556
|
-
console.error('Failed to list agents:', err instanceof Error ? err.message : err);
|
|
557
|
-
process.exit(1);
|
|
558
|
-
}
|
|
559
|
-
if (res.agents.length === 0) {
|
|
560
|
-
console.log('No server-side agents found. Use `opencara agent create` to add one.');
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
// Fetch registry for command templates
|
|
564
|
-
let registry;
|
|
565
|
-
try {
|
|
566
|
-
registry = await client.get('/api/registry');
|
|
567
|
-
}
|
|
568
|
-
catch {
|
|
569
|
-
registry = DEFAULT_REGISTRY;
|
|
570
|
-
}
|
|
571
|
-
const toolCommands = new Map(registry.tools.map((t) => [t.name, t.commandTemplate]));
|
|
572
|
-
const existing = config.agents ?? [];
|
|
573
|
-
let imported = 0;
|
|
574
|
-
for (const agent of res.agents) {
|
|
575
|
-
const isDuplicate = existing.some((e) => e.model === agent.model && e.tool === agent.tool);
|
|
576
|
-
if (isDuplicate)
|
|
577
|
-
continue;
|
|
578
|
-
let command = toolCommands.get(agent.tool);
|
|
579
|
-
if (command) {
|
|
580
|
-
command = command.replaceAll('${MODEL}', agent.model);
|
|
581
|
-
}
|
|
582
|
-
else {
|
|
583
|
-
console.warn(`Warning: no command template for ${agent.model}/${agent.tool} — set command manually in config`);
|
|
584
|
-
}
|
|
585
|
-
existing.push({ model: agent.model, tool: agent.tool, command });
|
|
586
|
-
imported++;
|
|
587
|
-
}
|
|
588
|
-
config.agents = existing;
|
|
589
|
-
saveConfig(config);
|
|
590
|
-
console.log(`Imported ${imported} agent(s) to local config.`);
|
|
591
|
-
if (imported > 0) {
|
|
592
|
-
console.log('Edit ~/.opencara/config.yml to adjust commands for your system.');
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
agentCommand
|
|
596
|
-
.command('list')
|
|
597
|
-
.description('List registered agents')
|
|
598
|
-
.action(async () => {
|
|
599
|
-
const config = loadConfig();
|
|
600
|
-
const apiKey = requireApiKey(config);
|
|
601
|
-
const client = new ApiClient(config.platformUrl, apiKey);
|
|
602
|
-
let res;
|
|
603
|
-
try {
|
|
604
|
-
res = await client.get('/api/agents');
|
|
605
|
-
}
|
|
606
|
-
catch (err) {
|
|
607
|
-
console.error('Failed to list agents:', err instanceof Error ? err.message : err);
|
|
608
|
-
process.exit(1);
|
|
609
|
-
}
|
|
610
|
-
// Fetch trust tier labels for each agent
|
|
611
|
-
const trustLabels = new Map();
|
|
612
|
-
for (const agent of res.agents) {
|
|
613
|
-
try {
|
|
614
|
-
const stats = await client.get(`/api/stats/${agent.id}`);
|
|
615
|
-
trustLabels.set(agent.id, stats.agent.trustTier.label);
|
|
616
|
-
}
|
|
617
|
-
catch {
|
|
618
|
-
// Leave as '--' if stats unavailable
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
formatTable(res.agents, trustLabels);
|
|
622
|
-
});
|
|
623
|
-
/** Register or reuse an anonymous agent, returning credentials and command template */
|
|
624
|
-
async function resolveAnonymousAgent(config, model, tool) {
|
|
625
|
-
// Check for stored anonymous agent with matching model+tool
|
|
626
|
-
const existing = config.anonymousAgents.find((a) => a.model === model && a.tool === tool);
|
|
627
|
-
if (existing) {
|
|
628
|
-
console.log(`Reusing stored anonymous agent ${existing.agentId} (${model} / ${tool})`);
|
|
629
|
-
const command = resolveCommandTemplate(DEFAULT_REGISTRY.tools
|
|
630
|
-
.find((t) => t.name === tool)
|
|
631
|
-
?.commandTemplate.replaceAll('${MODEL}', model) ?? null);
|
|
632
|
-
return { entry: existing, command };
|
|
633
|
-
}
|
|
634
|
-
// Register new anonymous agent
|
|
635
|
-
console.log('Registering anonymous agent...');
|
|
636
|
-
const client = new ApiClient(config.platformUrl);
|
|
637
|
-
const body = { model, tool };
|
|
638
|
-
const res = await client.post('/api/agents/anonymous', body);
|
|
639
|
-
const entry = {
|
|
640
|
-
agentId: res.agentId,
|
|
641
|
-
apiKey: res.apiKey,
|
|
642
|
-
model,
|
|
643
|
-
tool,
|
|
644
|
-
};
|
|
645
|
-
// Store in config
|
|
646
|
-
config.anonymousAgents.push(entry);
|
|
647
|
-
saveConfig(config);
|
|
648
|
-
console.log(`Agent registered: ${res.agentId} (${model} / ${tool})`);
|
|
649
|
-
console.log('Credentials saved to ~/.opencara/config.yml');
|
|
650
|
-
const command = resolveCommandTemplate(DEFAULT_REGISTRY.tools
|
|
651
|
-
.find((t) => t.name === tool)
|
|
652
|
-
?.commandTemplate.replaceAll('${MODEL}', model) ?? null);
|
|
653
|
-
return { entry, command };
|
|
654
|
-
}
|
|
655
|
-
export { resolveAnonymousAgent };
|
|
656
|
-
agentCommand
|
|
657
|
-
.command('start [agentIdOrModel]')
|
|
658
|
-
.description('Connect agent to platform via WebSocket')
|
|
659
|
-
.option('--all', 'Start all agents from local config concurrently')
|
|
660
|
-
.option('-a, --anonymous', 'Start an anonymous agent (no login required)')
|
|
661
|
-
.option('--model <model>', 'AI model name (used with --anonymous)')
|
|
662
|
-
.option('--tool <tool>', 'Review tool name (used with --anonymous)')
|
|
663
|
-
.option('--verbose', 'Enable detailed WebSocket diagnostic logging')
|
|
664
|
-
.option('--stability-threshold <ms>', `Connection stability threshold in ms (${STABILITY_THRESHOLD_MIN_MS}–${STABILITY_THRESHOLD_MAX_MS}, default: ${CONNECTION_STABILITY_THRESHOLD_MS})`)
|
|
665
|
-
.action(async (agentIdOrModel, opts) => {
|
|
666
|
-
let stabilityThresholdMs;
|
|
667
|
-
if (opts.stabilityThreshold !== undefined) {
|
|
668
|
-
const val = Number(opts.stabilityThreshold);
|
|
669
|
-
if (!Number.isInteger(val) ||
|
|
670
|
-
val < STABILITY_THRESHOLD_MIN_MS ||
|
|
671
|
-
val > STABILITY_THRESHOLD_MAX_MS) {
|
|
672
|
-
console.error(`Invalid --stability-threshold: must be an integer between ${STABILITY_THRESHOLD_MIN_MS} and ${STABILITY_THRESHOLD_MAX_MS}`);
|
|
673
|
-
process.exit(1);
|
|
674
|
-
}
|
|
675
|
-
stabilityThresholdMs = val;
|
|
676
|
-
}
|
|
677
|
-
const config = loadConfig();
|
|
678
|
-
// === Path C: Anonymous mode ===
|
|
679
|
-
if (opts.anonymous) {
|
|
680
|
-
if (!opts.model || !opts.tool) {
|
|
681
|
-
console.error('Both --model and --tool are required with --anonymous.');
|
|
682
|
-
process.exit(1);
|
|
683
|
-
}
|
|
684
|
-
let resolved;
|
|
685
|
-
try {
|
|
686
|
-
resolved = await resolveAnonymousAgent(config, opts.model, opts.tool);
|
|
687
|
-
}
|
|
688
|
-
catch (err) {
|
|
689
|
-
console.error('Failed to register anonymous agent:', err instanceof Error ? err.message : err);
|
|
690
|
-
process.exit(1);
|
|
691
|
-
}
|
|
692
|
-
const { entry, command } = resolved;
|
|
693
|
-
let reviewDeps;
|
|
694
|
-
if (validateCommandBinary(command)) {
|
|
695
|
-
reviewDeps = { commandTemplate: command, maxDiffSizeKb: config.maxDiffSizeKb };
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
console.warn(`Warning: binary "${command.split(' ')[0]}" not found. Reviews will be rejected.`);
|
|
699
|
-
}
|
|
700
|
-
const consumptionDeps = {
|
|
701
|
-
agentId: entry.agentId,
|
|
702
|
-
limits: config.limits,
|
|
703
|
-
session: createSessionTracker(),
|
|
704
|
-
};
|
|
705
|
-
console.log(`Starting anonymous agent ${entry.agentId}...`);
|
|
706
|
-
startAgent(entry.agentId, config.platformUrl, entry.apiKey, reviewDeps, consumptionDeps, {
|
|
707
|
-
verbose: opts.verbose,
|
|
708
|
-
stabilityThresholdMs,
|
|
709
|
-
repoConfig: entry.repoConfig,
|
|
710
|
-
});
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
// === Path B: Local-config mode (agents section exists) ===
|
|
714
|
-
if (config.agents !== null) {
|
|
715
|
-
// Validate and filter agents by binary availability
|
|
716
|
-
const validAgents = [];
|
|
717
|
-
for (const local of config.agents) {
|
|
718
|
-
let cmd;
|
|
719
|
-
try {
|
|
720
|
-
cmd = resolveLocalAgentCommand(local, config.agentCommand);
|
|
721
|
-
}
|
|
722
|
-
catch (err) {
|
|
723
|
-
console.warn(`Skipping ${local.model}/${local.tool}: ${err instanceof Error ? err.message : 'no command template available'}`);
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
if (!validateCommandBinary(cmd)) {
|
|
727
|
-
console.warn(`Skipping ${local.model}/${local.tool}: binary "${cmd.split(' ')[0]}" not found`);
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
validAgents.push({ local, command: cmd });
|
|
731
|
-
}
|
|
732
|
-
if (validAgents.length === 0 && config.anonymousAgents.length === 0) {
|
|
733
|
-
console.error('No valid agents in config. Check that tool binaries are installed.');
|
|
734
|
-
process.exit(1);
|
|
735
|
-
}
|
|
736
|
-
// Determine which agents to start
|
|
737
|
-
let agentsToStart;
|
|
738
|
-
const anonAgentsToStart = [];
|
|
739
|
-
if (opts.all) {
|
|
740
|
-
agentsToStart = validAgents;
|
|
741
|
-
// Also include anonymous agents when --all is used
|
|
742
|
-
anonAgentsToStart.push(...config.anonymousAgents);
|
|
743
|
-
}
|
|
744
|
-
else if (agentIdOrModel) {
|
|
745
|
-
const match = validAgents.find((a) => a.local.model === agentIdOrModel);
|
|
746
|
-
if (!match) {
|
|
747
|
-
console.error(`No agent with model "${agentIdOrModel}" found in local config.`);
|
|
748
|
-
console.error('Available agents:');
|
|
749
|
-
for (const a of validAgents) {
|
|
750
|
-
console.error(` ${a.local.model} (${a.local.tool})`);
|
|
751
|
-
}
|
|
752
|
-
process.exit(1);
|
|
753
|
-
}
|
|
754
|
-
agentsToStart = [match];
|
|
755
|
-
}
|
|
756
|
-
else if (validAgents.length === 1) {
|
|
757
|
-
agentsToStart = [validAgents[0]];
|
|
758
|
-
console.log(`Using agent ${validAgents[0].local.model} (${validAgents[0].local.tool})`);
|
|
759
|
-
}
|
|
760
|
-
else if (validAgents.length === 0) {
|
|
761
|
-
console.error('No valid authenticated agents in config. Use --anonymous or --all.');
|
|
762
|
-
process.exit(1);
|
|
763
|
-
}
|
|
764
|
-
else {
|
|
765
|
-
console.error('Multiple agents in config. Specify a model name or use --all:');
|
|
766
|
-
for (const a of validAgents) {
|
|
767
|
-
console.error(` ${a.local.model} (${a.local.tool})`);
|
|
768
|
-
}
|
|
769
|
-
process.exit(1);
|
|
770
|
-
}
|
|
771
|
-
// Increase listener limit when starting multiple agents
|
|
772
|
-
const totalAgents = agentsToStart.length + anonAgentsToStart.length;
|
|
773
|
-
if (totalAgents > 1) {
|
|
774
|
-
process.setMaxListeners(process.getMaxListeners() + totalAgents * 2);
|
|
775
|
-
}
|
|
776
|
-
// Start each authenticated agent (requires login)
|
|
777
|
-
let startedCount = 0;
|
|
778
|
-
let apiKey;
|
|
779
|
-
let client;
|
|
780
|
-
let serverAgents;
|
|
781
|
-
if (agentsToStart.length > 0) {
|
|
782
|
-
apiKey = requireApiKey(config);
|
|
783
|
-
client = new ApiClient(config.platformUrl, apiKey);
|
|
784
|
-
try {
|
|
785
|
-
const res = await client.get('/api/agents');
|
|
786
|
-
serverAgents = res.agents;
|
|
787
|
-
}
|
|
788
|
-
catch (err) {
|
|
789
|
-
console.error('Failed to fetch agents:', err instanceof Error ? err.message : err);
|
|
790
|
-
process.exit(1);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
for (const selected of agentsToStart) {
|
|
794
|
-
let agentId;
|
|
795
|
-
try {
|
|
796
|
-
const sync = await syncAgentToServer(client, serverAgents, selected.local);
|
|
797
|
-
agentId = sync.agentId;
|
|
798
|
-
if (sync.created) {
|
|
799
|
-
console.log(`Registered new agent ${agentId} on platform`);
|
|
800
|
-
// Update snapshot to prevent duplicate registrations
|
|
801
|
-
serverAgents.push({
|
|
802
|
-
id: agentId,
|
|
803
|
-
model: selected.local.model,
|
|
804
|
-
tool: selected.local.tool,
|
|
805
|
-
isAnonymous: false,
|
|
806
|
-
status: 'offline',
|
|
807
|
-
repoConfig: null,
|
|
808
|
-
createdAt: new Date().toISOString(),
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
catch (err) {
|
|
813
|
-
console.error(`Failed to sync agent ${selected.local.model} to server:`, err instanceof Error ? err.message : err);
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
const reviewDeps = {
|
|
817
|
-
commandTemplate: selected.command,
|
|
818
|
-
maxDiffSizeKb: config.maxDiffSizeKb,
|
|
819
|
-
};
|
|
820
|
-
const consumptionDeps = {
|
|
821
|
-
agentId,
|
|
822
|
-
limits: resolveAgentLimits(selected.local.limits, config.limits),
|
|
823
|
-
session: createSessionTracker(),
|
|
824
|
-
};
|
|
825
|
-
console.log(`Starting agent ${selected.local.model} (${agentId})...`);
|
|
826
|
-
startAgent(agentId, config.platformUrl, apiKey, reviewDeps, consumptionDeps, {
|
|
827
|
-
verbose: opts.verbose,
|
|
828
|
-
stabilityThresholdMs,
|
|
829
|
-
repoConfig: selected.local.repos,
|
|
830
|
-
});
|
|
831
|
-
startedCount++;
|
|
832
|
-
}
|
|
833
|
-
// Start anonymous agents (for --all)
|
|
834
|
-
for (const anon of anonAgentsToStart) {
|
|
835
|
-
let command;
|
|
836
|
-
try {
|
|
837
|
-
command = resolveCommandTemplate(DEFAULT_REGISTRY.tools
|
|
838
|
-
.find((t) => t.name === anon.tool)
|
|
839
|
-
?.commandTemplate.replaceAll('${MODEL}', anon.model) ?? null);
|
|
840
|
-
}
|
|
841
|
-
catch {
|
|
842
|
-
console.warn(`Skipping anonymous agent ${anon.agentId}: no command template for tool "${anon.tool}"`);
|
|
843
|
-
continue;
|
|
844
|
-
}
|
|
845
|
-
let reviewDeps;
|
|
846
|
-
if (validateCommandBinary(command)) {
|
|
847
|
-
reviewDeps = { commandTemplate: command, maxDiffSizeKb: config.maxDiffSizeKb };
|
|
848
|
-
}
|
|
849
|
-
else {
|
|
850
|
-
console.warn(`Warning: binary "${command.split(' ')[0]}" not found for anonymous agent ${anon.agentId}. Reviews will be rejected.`);
|
|
851
|
-
}
|
|
852
|
-
const consumptionDeps = {
|
|
853
|
-
agentId: anon.agentId,
|
|
854
|
-
limits: config.limits,
|
|
855
|
-
session: createSessionTracker(),
|
|
856
|
-
};
|
|
857
|
-
console.log(`Starting anonymous agent ${anon.model} (${anon.agentId})...`);
|
|
858
|
-
startAgent(anon.agentId, config.platformUrl, anon.apiKey, reviewDeps, consumptionDeps, {
|
|
859
|
-
verbose: opts.verbose,
|
|
860
|
-
stabilityThresholdMs,
|
|
861
|
-
repoConfig: anon.repoConfig,
|
|
862
|
-
});
|
|
863
|
-
startedCount++;
|
|
864
|
-
}
|
|
865
|
-
if (startedCount === 0) {
|
|
866
|
-
console.error('No agents could be started.');
|
|
867
|
-
process.exit(1);
|
|
868
|
-
}
|
|
869
|
-
return;
|
|
870
|
-
}
|
|
871
|
-
// === Path A: Old server-side behavior (no agents section) ===
|
|
872
|
-
const apiKey = requireApiKey(config);
|
|
873
|
-
const client = new ApiClient(config.platformUrl, apiKey);
|
|
874
|
-
console.log('Hint: No agents in local config. Run `opencara agent init` to import, or `opencara agent create` to add agents.');
|
|
875
|
-
let agentId = agentIdOrModel;
|
|
876
|
-
if (!agentId) {
|
|
877
|
-
let res;
|
|
878
|
-
try {
|
|
879
|
-
res = await client.get('/api/agents');
|
|
880
|
-
}
|
|
881
|
-
catch (err) {
|
|
882
|
-
console.error('Failed to list agents:', err instanceof Error ? err.message : err);
|
|
883
|
-
process.exit(1);
|
|
884
|
-
}
|
|
885
|
-
if (res.agents.length === 0) {
|
|
886
|
-
console.error('No agents registered. Run `opencara agent create` first.');
|
|
887
|
-
process.exit(1);
|
|
888
|
-
}
|
|
889
|
-
if (res.agents.length === 1) {
|
|
890
|
-
agentId = res.agents[0].id;
|
|
891
|
-
console.log(`Using agent ${agentId}`);
|
|
892
|
-
}
|
|
893
|
-
else {
|
|
894
|
-
console.error('Multiple agents found. Please specify an agent ID:');
|
|
895
|
-
for (const a of res.agents) {
|
|
896
|
-
console.error(` ${a.id} ${a.model} / ${a.tool}`);
|
|
897
|
-
}
|
|
898
|
-
process.exit(1);
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
let reviewDeps;
|
|
902
|
-
try {
|
|
903
|
-
const commandTemplate = resolveCommandTemplate(config.agentCommand);
|
|
904
|
-
reviewDeps = {
|
|
905
|
-
commandTemplate,
|
|
906
|
-
maxDiffSizeKb: config.maxDiffSizeKb,
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
catch (err) {
|
|
910
|
-
console.warn(`Warning: ${err instanceof Error ? err.message : 'Could not determine agent command.'}` +
|
|
911
|
-
' Reviews will be rejected.');
|
|
912
|
-
}
|
|
913
|
-
const consumptionDeps = {
|
|
914
|
-
agentId,
|
|
915
|
-
limits: config.limits,
|
|
916
|
-
session: createSessionTracker(),
|
|
917
|
-
};
|
|
918
|
-
console.log(`Starting agent ${agentId}...`);
|
|
919
|
-
startAgent(agentId, config.platformUrl, apiKey, reviewDeps, consumptionDeps, {
|
|
920
|
-
verbose: opts.verbose,
|
|
921
|
-
stabilityThresholdMs,
|
|
922
|
-
});
|
|
923
|
-
});
|
|
924
|
-
//# sourceMappingURL=agent.js.map
|