agent-detective 1.0.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.
@@ -0,0 +1,1237 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/version.ts
4
+ var APP_NAME = "agent-detective";
5
+ var APP_VERSION = "1.0.0";
6
+
7
+ // src/config/env-whitelist.ts
8
+ var JIRA_PACKAGE = "@agent-detective/jira-adapter";
9
+ var LINEAR_PACKAGE = "@agent-detective/linear-adapter";
10
+ var LOCAL_REPOS_PACKAGE = "@agent-detective/local-repos-plugin";
11
+ var PR_PIPELINE_PACKAGE = "@agent-detective/pr-pipeline";
12
+ function getExistingPluginOptions(config, packageName) {
13
+ if (!Array.isArray(config.plugins)) {
14
+ return null;
15
+ }
16
+ const entry = config.plugins.find((p) => p.package === packageName);
17
+ if (!entry) return null;
18
+ if (!entry.options) {
19
+ entry.options = {};
20
+ }
21
+ return entry.options;
22
+ }
23
+ function setNested(options, path, value) {
24
+ let cur = options;
25
+ for (let i = 0; i < path.length - 1; i++) {
26
+ const k = path[i];
27
+ const next = cur[k];
28
+ if (!next || typeof next !== "object" || Array.isArray(next)) {
29
+ cur[k] = {};
30
+ }
31
+ cur = cur[k];
32
+ }
33
+ cur[path[path.length - 1]] = value;
34
+ }
35
+ function applyCoreEnvWhitelist(config) {
36
+ if (process.env.PORT) {
37
+ const n = parseInt(process.env.PORT, 10);
38
+ if (!Number.isNaN(n)) {
39
+ config.port = n;
40
+ }
41
+ }
42
+ if (process.env.AGENT) {
43
+ config.agent = process.env.AGENT;
44
+ }
45
+ const agentModelEnvVars = ["AGENTS_OPENCODE_MODEL", "AGENTS_CLAUDE_MODEL", "AGENTS_CURSOR_MODEL"];
46
+ for (const envVar of agentModelEnvVars) {
47
+ const raw = process.env[envVar];
48
+ if (!raw) continue;
49
+ const agentId = envVar.replace("AGENTS_", "").replace("_MODEL", "").toLowerCase();
50
+ if (agentId === "runner") continue;
51
+ if (!config.agents) {
52
+ config.agents = {};
53
+ }
54
+ if (!config.agents[agentId]) {
55
+ config.agents[agentId] = { defaultModel: raw };
56
+ } else {
57
+ config.agents[agentId].defaultModel = raw;
58
+ }
59
+ }
60
+ for (const [key, value] of Object.entries(process.env)) {
61
+ const m = /^AGENTS_([A-Z0-9]+)_MODEL$/.exec(key);
62
+ if (!m || !value) continue;
63
+ const agentId = m[1].toLowerCase();
64
+ if (agentId === "runner") continue;
65
+ if (agentModelEnvVars.includes(key)) continue;
66
+ if (!config.agents) {
67
+ config.agents = {};
68
+ }
69
+ if (!config.agents[agentId]) {
70
+ config.agents[agentId] = { defaultModel: value };
71
+ } else {
72
+ config.agents[agentId].defaultModel = value;
73
+ }
74
+ }
75
+ if (process.env.DOCS_AUTH_REQUIRED === "true" || process.env.DOCS_AUTH_REQUIRED === "false") {
76
+ config.docsAuthRequired = process.env.DOCS_AUTH_REQUIRED === "true";
77
+ }
78
+ if (process.env.DOCS_API_KEY) {
79
+ config.docsApiKey = process.env.DOCS_API_KEY;
80
+ }
81
+ if (process.env.PLUGINS_FAIL_ON_CONTRACT_ERRORS === "true" || process.env.PLUGINS_FAIL_ON_CONTRACT_ERRORS === "false") {
82
+ if (!config.pluginSystem) config.pluginSystem = {};
83
+ config.pluginSystem.failOnContractErrors = process.env.PLUGINS_FAIL_ON_CONTRACT_ERRORS === "true";
84
+ }
85
+ if (process.env.PLUGINS_FAIL_ON_DEPENDENCY_ERRORS === "true" || process.env.PLUGINS_FAIL_ON_DEPENDENCY_ERRORS === "false") {
86
+ if (!config.pluginSystem) config.pluginSystem = {};
87
+ config.pluginSystem.failOnDependencyErrors = process.env.PLUGINS_FAIL_ON_DEPENDENCY_ERRORS === "true";
88
+ }
89
+ if (process.env.PLUGINS_FAIL_ON_PLUGIN_LOAD_ERRORS === "true" || process.env.PLUGINS_FAIL_ON_PLUGIN_LOAD_ERRORS === "false") {
90
+ if (!config.pluginSystem) config.pluginSystem = {};
91
+ config.pluginSystem.failOnPluginLoadErrors = process.env.PLUGINS_FAIL_ON_PLUGIN_LOAD_ERRORS === "true";
92
+ }
93
+ const parseMs = (raw) => {
94
+ if (raw === void 0 || raw === "") return void 0;
95
+ const n = parseInt(raw, 10);
96
+ return !Number.isNaN(n) && n >= 0 ? n : void 0;
97
+ };
98
+ const parseBytes = (raw) => {
99
+ if (raw === void 0 || raw === "") return void 0;
100
+ const n = parseInt(raw, 10);
101
+ return !Number.isNaN(n) && n > 0 ? n : void 0;
102
+ };
103
+ const t = parseMs(process.env.AGENTS_RUNNER_TIMEOUT_MS);
104
+ const buf = parseBytes(process.env.AGENTS_RUNNER_MAX_BUFFER_BYTES);
105
+ const grace = parseMs(process.env.AGENTS_RUNNER_POST_FINAL_GRACE_MS);
106
+ const forceKill = parseMs(process.env.AGENTS_RUNNER_FORCE_KILL_DELAY_MS);
107
+ if (t !== void 0 || buf !== void 0 || grace !== void 0 || forceKill !== void 0) {
108
+ if (!config.agents) {
109
+ config.agents = {};
110
+ }
111
+ const runnerPath = "runner";
112
+ if (!config.agents[runnerPath]) {
113
+ config.agents[runnerPath] = {};
114
+ }
115
+ const runner = config.agents[runnerPath];
116
+ if (t !== void 0) runner.timeoutMs = t;
117
+ if (buf !== void 0) runner.maxBufferBytes = buf;
118
+ if (grace !== void 0) runner.postFinalGraceMs = grace;
119
+ if (forceKill !== void 0) runner.forceKillDelayMs = forceKill;
120
+ }
121
+ const obsExclude = process.env.OBSERVABILITY_REQUEST_LOGGER_EXCLUDE_PATHS;
122
+ if (obsExclude !== void 0 && obsExclude !== "") {
123
+ if (!config.observability) {
124
+ config.observability = {};
125
+ }
126
+ const obs = config.observability;
127
+ if (!obs.requestLogger) {
128
+ obs.requestLogger = {};
129
+ }
130
+ obs.requestLogger.excludePaths = obsExclude.split(",").map((s) => s.trim()).filter(Boolean);
131
+ }
132
+ const tasksMaxConcurrentRaw = process.env.TASKS_MAX_CONCURRENT;
133
+ if (tasksMaxConcurrentRaw !== void 0 && tasksMaxConcurrentRaw !== "") {
134
+ const n = parseInt(tasksMaxConcurrentRaw, 10);
135
+ if (!Number.isNaN(n) && n > 0 && n <= 1e3) {
136
+ if (!config.tasks) config.tasks = {};
137
+ config.tasks.maxConcurrent = n;
138
+ }
139
+ }
140
+ const wallFromEnv = parseMs(process.env.TASKS_MAX_WALL_TIME_MS);
141
+ if (wallFromEnv !== void 0 && wallFromEnv > 0) {
142
+ if (!config.tasks) config.tasks = {};
143
+ config.tasks.maxWallTimeMs = wallFromEnv;
144
+ }
145
+ const runRecordsPath = process.env.RUN_RECORDS_PATH?.trim();
146
+ if (runRecordsPath) {
147
+ config.runRecords = { path: runRecordsPath };
148
+ }
149
+ }
150
+ function applyPluginEnvWhitelist(config) {
151
+ const jiraToken = process.env.JIRA_API_TOKEN;
152
+ const jiraEmail = process.env.JIRA_EMAIL;
153
+ const jiraBase = process.env.JIRA_BASE_URL;
154
+ const jiraOAuthClientId = process.env.JIRA_OAUTH_CLIENT_ID;
155
+ const jiraOAuthClientSecret = process.env.JIRA_OAUTH_CLIENT_SECRET;
156
+ const jiraOAuthRedirectBase = process.env.JIRA_OAUTH_REDIRECT_BASE_URL;
157
+ const jiraOAuthRefreshToken = process.env.JIRA_OAUTH_REFRESH_TOKEN;
158
+ const jiraCloudId = process.env.JIRA_CLOUD_ID;
159
+ if (jiraToken || jiraEmail || jiraBase || jiraOAuthClientId || jiraOAuthClientSecret || jiraOAuthRedirectBase || jiraOAuthRefreshToken || jiraCloudId) {
160
+ const opts = getExistingPluginOptions(config, JIRA_PACKAGE);
161
+ if (opts) {
162
+ if (jiraToken) opts.apiToken = jiraToken;
163
+ if (jiraEmail) opts.email = jiraEmail;
164
+ if (jiraBase) opts.baseUrl = jiraBase;
165
+ if (jiraOAuthClientId) opts.oauthClientId = jiraOAuthClientId;
166
+ if (jiraOAuthClientSecret) opts.oauthClientSecret = jiraOAuthClientSecret;
167
+ if (jiraOAuthRedirectBase) opts.oauthRedirectBaseUrl = jiraOAuthRedirectBase;
168
+ if (jiraOAuthRefreshToken) opts.oauthRefreshToken = jiraOAuthRefreshToken;
169
+ if (jiraCloudId) opts.cloudId = jiraCloudId;
170
+ }
171
+ }
172
+ const linearApiKey = process.env.LINEAR_API_KEY;
173
+ const linearWebhookSecret = process.env.LINEAR_WEBHOOK_SIGNING_SECRET;
174
+ const linearOAuthClientId = process.env.LINEAR_OAUTH_CLIENT_ID;
175
+ const linearOAuthClientSecret = process.env.LINEAR_OAUTH_CLIENT_SECRET;
176
+ const linearOAuthRedirectBase = process.env.LINEAR_OAUTH_REDIRECT_BASE_URL;
177
+ const linearOAuthRefreshToken = process.env.LINEAR_OAUTH_REFRESH_TOKEN;
178
+ const linearOAuthActor = process.env.LINEAR_OAUTH_ACTOR;
179
+ if (linearApiKey || linearWebhookSecret || linearOAuthClientId || linearOAuthClientSecret || linearOAuthRedirectBase || linearOAuthRefreshToken || linearOAuthActor) {
180
+ const opts = getExistingPluginOptions(config, LINEAR_PACKAGE);
181
+ if (opts) {
182
+ if (linearApiKey) opts.apiKey = linearApiKey;
183
+ if (linearWebhookSecret) opts.webhookSigningSecret = linearWebhookSecret;
184
+ if (linearOAuthClientId) opts.oauthClientId = linearOAuthClientId;
185
+ if (linearOAuthClientSecret) opts.oauthClientSecret = linearOAuthClientSecret;
186
+ if (linearOAuthRedirectBase) opts.oauthRedirectBaseUrl = linearOAuthRedirectBase;
187
+ if (linearOAuthRefreshToken) opts.oauthRefreshToken = linearOAuthRefreshToken;
188
+ if (linearOAuthActor === "app" || linearOAuthActor === "user") {
189
+ opts.oauthActor = linearOAuthActor;
190
+ }
191
+ }
192
+ }
193
+ const maxCommitsRaw = process.env.REPO_CONTEXT_GIT_LOG_MAX_COMMITS;
194
+ if (maxCommitsRaw !== void 0 && maxCommitsRaw !== "") {
195
+ const n = parseInt(maxCommitsRaw, 10);
196
+ if (!Number.isNaN(n) && n > 0) {
197
+ const opts = getExistingPluginOptions(config, LOCAL_REPOS_PACKAGE);
198
+ if (opts) {
199
+ setNested(opts, ["repoContext", "gitLogMaxCommits"], n);
200
+ }
201
+ }
202
+ }
203
+ const gitTimeout = process.env.REPO_CONTEXT_GIT_COMMAND_TIMEOUT_MS;
204
+ if (gitTimeout !== void 0 && gitTimeout !== "") {
205
+ const n = parseInt(gitTimeout, 10);
206
+ if (!Number.isNaN(n) && n > 0) {
207
+ const opts = getExistingPluginOptions(config, LOCAL_REPOS_PACKAGE);
208
+ if (opts) {
209
+ setNested(opts, ["repoContext", "gitCommandTimeoutMs"], n);
210
+ }
211
+ }
212
+ }
213
+ const gitMaxBuf = process.env.REPO_CONTEXT_GIT_MAX_BUFFER_BYTES;
214
+ if (gitMaxBuf !== void 0 && gitMaxBuf !== "") {
215
+ const n = parseInt(gitMaxBuf, 10);
216
+ if (!Number.isNaN(n) && n > 0) {
217
+ const opts = getExistingPluginOptions(config, LOCAL_REPOS_PACKAGE);
218
+ if (opts) {
219
+ setNested(opts, ["repoContext", "gitMaxBufferBytes"], n);
220
+ }
221
+ }
222
+ }
223
+ const diffFromRef = process.env.REPO_CONTEXT_DIFF_FROM_REF;
224
+ if (diffFromRef && diffFromRef.trim()) {
225
+ const opts = getExistingPluginOptions(config, LOCAL_REPOS_PACKAGE);
226
+ if (opts) {
227
+ setNested(opts, ["repoContext", "diffFromRef"], diffFromRef.trim());
228
+ }
229
+ }
230
+ const maxOut = process.env.SUMMARY_MAX_OUTPUT_CHARS;
231
+ if (maxOut !== void 0 && maxOut !== "") {
232
+ const n = parseInt(maxOut, 10);
233
+ if (!Number.isNaN(n) && n > 0) {
234
+ const opts = getExistingPluginOptions(config, LOCAL_REPOS_PACKAGE);
235
+ if (opts) {
236
+ setNested(opts, ["summaryGeneration", "maxOutputChars"], n);
237
+ }
238
+ }
239
+ }
240
+ const jiraAutoCd = process.env.JIRA_AUTO_ANALYSIS_COOLDOWN_MS;
241
+ if (jiraAutoCd !== void 0 && jiraAutoCd !== "") {
242
+ const n = parseInt(jiraAutoCd, 10);
243
+ if (!Number.isNaN(n) && n >= 0) {
244
+ const opts = getExistingPluginOptions(config, JIRA_PACKAGE);
245
+ if (opts) {
246
+ opts.autoAnalysisCooldownMs = n;
247
+ }
248
+ }
249
+ }
250
+ const jiraRemCd = process.env.JIRA_MISSING_LABELS_REMINDER_COOLDOWN_MS;
251
+ if (jiraRemCd !== void 0 && jiraRemCd !== "") {
252
+ const n = parseInt(jiraRemCd, 10);
253
+ if (!Number.isNaN(n) && n >= 0) {
254
+ const opts = getExistingPluginOptions(config, JIRA_PACKAGE);
255
+ if (opts) {
256
+ opts.missingLabelsReminderCooldownMs = n;
257
+ }
258
+ }
259
+ }
260
+ const prPipelineOpts = getExistingPluginOptions(config, PR_PIPELINE_PACKAGE);
261
+ if (prPipelineOpts) {
262
+ if (process.env.GITHUB_TOKEN) {
263
+ prPipelineOpts.githubToken = process.env.GITHUB_TOKEN;
264
+ } else if (process.env.GH_TOKEN) {
265
+ prPipelineOpts.githubToken = process.env.GH_TOKEN;
266
+ }
267
+ if (process.env.BITBUCKET_TOKEN) {
268
+ prPipelineOpts.bitbucketToken = process.env.BITBUCKET_TOKEN;
269
+ }
270
+ if (process.env.BITBUCKET_USERNAME) {
271
+ prPipelineOpts.bitbucketUsername = process.env.BITBUCKET_USERNAME;
272
+ }
273
+ if (process.env.BITBUCKET_APP_PASSWORD) {
274
+ prPipelineOpts.bitbucketAppPassword = process.env.BITBUCKET_APP_PASSWORD;
275
+ }
276
+ }
277
+ }
278
+ function applyLogLevelAliasForObservability() {
279
+ const log = process.env.LOG_LEVEL;
280
+ if (!log || process.env.OBSERVABILITY_LOG_LEVEL) return;
281
+ if (["debug", "info", "warn", "error"].includes(log)) {
282
+ process.env.OBSERVABILITY_LOG_LEVEL = log;
283
+ }
284
+ }
285
+
286
+ // src/server.ts
287
+ import Fastify from "fastify";
288
+ import {
289
+ jsonSchemaTransform,
290
+ serializerCompiler,
291
+ validatorCompiler
292
+ } from "fastify-type-provider-zod";
293
+ import swagger from "@fastify/swagger";
294
+
295
+ // src/core/openapi/tags.ts
296
+ var CORE_PLUGIN_TAG = "@agent-detective/core";
297
+ var SCALAR_TAG_GROUPS = {
298
+ CORE: "Core",
299
+ PLUGINS: "Plugins"
300
+ };
301
+ function createTagDescription(tag) {
302
+ switch (tag) {
303
+ case CORE_PLUGIN_TAG:
304
+ return "Core API endpoints";
305
+ default:
306
+ return `${tag} plugin endpoints`;
307
+ }
308
+ }
309
+
310
+ // src/core/openapi/tag-groups.ts
311
+ function applyTagGroups(spec, options = {}) {
312
+ const doc = spec;
313
+ doc.tags = doc.tags ?? [];
314
+ const byName = new Map(doc.tags.map((t) => [t.name, t]));
315
+ const ensureTag = (name) => {
316
+ const existing = byName.get(name);
317
+ const desc = options.descriptions?.[name] ?? createTagDescription(name);
318
+ if (!existing) {
319
+ const tag = { name, description: desc };
320
+ doc.tags.push(tag);
321
+ byName.set(name, tag);
322
+ } else if (!existing.description) {
323
+ existing.description = desc;
324
+ }
325
+ };
326
+ ensureTag(CORE_PLUGIN_TAG);
327
+ const pluginTags = options.pluginTags ?? [...byName.keys()].filter((n) => n !== CORE_PLUGIN_TAG);
328
+ for (const name of pluginTags) ensureTag(name);
329
+ doc["x-tagGroups"] = [
330
+ { name: SCALAR_TAG_GROUPS.CORE, tags: [CORE_PLUGIN_TAG] },
331
+ { name: SCALAR_TAG_GROUPS.PLUGINS, tags: pluginTags }
332
+ ];
333
+ return doc;
334
+ }
335
+
336
+ // src/server.ts
337
+ import { createRequestLogger } from "@agent-detective/observability";
338
+
339
+ // src/core/core-api-controller.ts
340
+ import { randomUUID } from "crypto";
341
+ import { z } from "zod";
342
+ import { defineRoute, registerRoutes } from "@agent-detective/sdk";
343
+ var ServerInfoResponse = z.object({
344
+ name: z.string(),
345
+ version: z.string(),
346
+ plugins: z.array(z.string())
347
+ });
348
+ var HealthCheckResponse = z.object({ status: z.enum(["ok", "degraded", "error"]) }).loose();
349
+ var AgentInfoResponse = z.array(
350
+ z.object({
351
+ id: z.string(),
352
+ label: z.string(),
353
+ defaultModel: z.string().optional(),
354
+ available: z.boolean().optional(),
355
+ needsPty: z.boolean().optional(),
356
+ mergeStderr: z.boolean().optional()
357
+ }).loose()
358
+ );
359
+ var QueueStatusResponse = z.object({
360
+ status: z.literal("ok"),
361
+ backend: z.string()
362
+ });
363
+ var PluginStatusResponse = z.object({
364
+ configured: z.array(z.string()),
365
+ loaded: z.array(z.string()),
366
+ failures: z.array(
367
+ z.object({
368
+ plugin: z.string(),
369
+ stage: z.string(),
370
+ message: z.string()
371
+ })
372
+ )
373
+ });
374
+ var ErrorResponse = z.object({
375
+ error: z.string(),
376
+ availableAgents: z.array(z.string()).optional(),
377
+ agentId: z.string().optional()
378
+ });
379
+ var AgentRunBody = z.object({
380
+ agentId: z.string().min(1, "agentId is required"),
381
+ prompt: z.string().min(1, "prompt is required"),
382
+ options: z.object({
383
+ model: z.string().optional(),
384
+ repoPath: z.string().nullable().optional(),
385
+ cwd: z.string().optional(),
386
+ threadId: z.string().optional()
387
+ }).optional()
388
+ });
389
+ var AgentRunOk = z.object({
390
+ taskId: z.string(),
391
+ output: z.string(),
392
+ sawJson: z.boolean(),
393
+ threadId: z.string().optional()
394
+ });
395
+ var ProcessEventBody = z.object({
396
+ type: z.enum(["incident", "question", "command"]),
397
+ message: z.string().min(1),
398
+ context: z.object({
399
+ repoPath: z.string().nullable().optional(),
400
+ threadId: z.string().nullable().optional(),
401
+ cwd: z.string().optional(),
402
+ model: z.string().optional()
403
+ }).optional()
404
+ });
405
+ var ProcessEventOk = z.object({ taskId: z.string() });
406
+ function startSseHeaders(reply) {
407
+ reply.hijack();
408
+ const raw = reply.raw;
409
+ raw.setHeader("Content-Type", "text/event-stream");
410
+ raw.setHeader("Cache-Control", "no-cache");
411
+ raw.setHeader("Connection", "keep-alive");
412
+ raw.setHeader("X-Accel-Buffering", "no");
413
+ raw.flushHeaders?.();
414
+ return raw;
415
+ }
416
+ function writeSse(stream, payload) {
417
+ stream.write(`data: ${JSON.stringify(payload)}
418
+
419
+ `);
420
+ }
421
+ function buildCoreApiRoutes(deps) {
422
+ const { agentModels, agentRunner, enqueue, observability, config, pluginStatus } = deps;
423
+ const getServerInfo = defineRoute({
424
+ method: "GET",
425
+ url: "/api/",
426
+ schema: {
427
+ tags: [CORE_PLUGIN_TAG],
428
+ summary: "Get server info",
429
+ description: "Returns basic information about the agent-detective server",
430
+ response: { 200: ServerInfoResponse }
431
+ },
432
+ handler() {
433
+ const plugins = (config.plugins ?? []).map((p) => p.package).filter((p) => Boolean(p));
434
+ return { name: APP_NAME, version: APP_VERSION, plugins };
435
+ }
436
+ });
437
+ const getHealth = defineRoute({
438
+ method: "GET",
439
+ url: "/api/health",
440
+ schema: {
441
+ tags: [CORE_PLUGIN_TAG],
442
+ summary: "Health check",
443
+ description: "Returns the health status of the server",
444
+ response: { 200: HealthCheckResponse, 503: ErrorResponse }
445
+ },
446
+ async handler(_req, reply) {
447
+ const healthStatus = await observability.health.check();
448
+ const statusCode = healthStatus.status === "unhealthy" ? 503 : 200;
449
+ return reply.code(statusCode).send(healthStatus);
450
+ }
451
+ });
452
+ const listAgents2 = defineRoute({
453
+ method: "GET",
454
+ url: "/api/agent/list",
455
+ schema: {
456
+ tags: [CORE_PLUGIN_TAG],
457
+ summary: "List available agents",
458
+ description: "Returns a list of all available AI agents and their status",
459
+ response: { 200: AgentInfoResponse, 503: ErrorResponse }
460
+ },
461
+ async handler(_req, reply) {
462
+ if (!agentRunner) {
463
+ return reply.code(503).send({ error: "Agent runner not available" });
464
+ }
465
+ const list = await agentRunner.listAgents();
466
+ if (!agentModels) return list;
467
+ return list.map((a) => ({
468
+ ...a,
469
+ defaultModel: agentModels[a.id]?.defaultModel ?? a.defaultModel
470
+ }));
471
+ }
472
+ });
473
+ const queueStatus = defineRoute({
474
+ method: "GET",
475
+ url: "/api/queue/status",
476
+ schema: {
477
+ tags: [CORE_PLUGIN_TAG],
478
+ summary: "Queue status",
479
+ description: "In-process task queue: serializes work per task key. No depth metrics exposed yet.",
480
+ response: { 200: QueueStatusResponse }
481
+ },
482
+ handler() {
483
+ return { status: "ok", backend: "memory" };
484
+ }
485
+ });
486
+ const pluginsStatus = defineRoute({
487
+ method: "GET",
488
+ url: "/api/plugins",
489
+ schema: {
490
+ tags: [CORE_PLUGIN_TAG],
491
+ summary: "Plugin status",
492
+ description: "Returns configured plugins, loaded plugins, and recent plugin load failures",
493
+ response: { 200: PluginStatusResponse }
494
+ },
495
+ handler() {
496
+ const configured = (config.plugins ?? []).map((p) => p.package).filter((p) => Boolean(p));
497
+ const status = pluginStatus?.() ?? { loaded: [], failures: [] };
498
+ return {
499
+ configured,
500
+ loaded: status.loaded,
501
+ failures: status.failures
502
+ };
503
+ }
504
+ });
505
+ const runAgent = defineRoute({
506
+ method: "POST",
507
+ url: "/api/agent/run",
508
+ schema: {
509
+ tags: [CORE_PLUGIN_TAG],
510
+ summary: "Run AI agent",
511
+ description: "Executes an AI agent with the provided prompt and options",
512
+ body: AgentRunBody,
513
+ response: { 200: AgentRunOk, 404: ErrorResponse, 503: ErrorResponse }
514
+ },
515
+ async handler(req, reply) {
516
+ const { agentId, prompt, options } = req.body;
517
+ if (!agentRunner) {
518
+ return reply.code(503).send({ error: "Agent runner not available" });
519
+ }
520
+ const list = await agentRunner.listAgents();
521
+ const agentInfo = list.find((a) => a.id === agentId);
522
+ if (!agentInfo) {
523
+ return reply.code(404).send({
524
+ error: `Unknown agent: ${agentId}`,
525
+ availableAgents: list.map((a) => a.id)
526
+ });
527
+ }
528
+ if (!agentInfo.available) {
529
+ return reply.code(404).send({
530
+ error: `Agent '${agentId}' is not installed or not available on this system`,
531
+ agentId
532
+ });
533
+ }
534
+ const wantsStream = req.headers.accept?.includes("text/event-stream");
535
+ const taskId = randomUUID();
536
+ if (wantsStream) {
537
+ const stream = startSseHeaders(reply);
538
+ try {
539
+ await agentRunner.runAgentForChat(taskId, prompt, {
540
+ agentId,
541
+ repoPath: options?.repoPath,
542
+ cwd: options?.cwd,
543
+ model: options?.model,
544
+ threadId: options?.threadId,
545
+ onProgress: (messages) => {
546
+ for (const msg of messages) {
547
+ writeSse(stream, { type: "progress", content: msg });
548
+ }
549
+ },
550
+ onFinal: (text) => {
551
+ writeSse(stream, { type: "final", content: text });
552
+ stream.end();
553
+ }
554
+ });
555
+ } catch (err) {
556
+ writeSse(stream, {
557
+ type: "final",
558
+ content: `Error: ${err.message}`
559
+ });
560
+ stream.end();
561
+ }
562
+ return reply;
563
+ }
564
+ try {
565
+ const output = await agentRunner.runAgentForChat(taskId, prompt, {
566
+ agentId,
567
+ repoPath: options?.repoPath,
568
+ cwd: options?.cwd,
569
+ model: options?.model,
570
+ threadId: options?.threadId
571
+ });
572
+ return {
573
+ taskId,
574
+ output: output.text,
575
+ sawJson: output.sawJson,
576
+ threadId: output.threadId
577
+ };
578
+ } catch (err) {
579
+ return reply.code(500).send({ error: err.message });
580
+ }
581
+ }
582
+ });
583
+ const processEvent = defineRoute({
584
+ method: "POST",
585
+ url: "/api/events",
586
+ schema: {
587
+ tags: [CORE_PLUGIN_TAG],
588
+ summary: "Process event",
589
+ description: "Processes a TaskEvent and executes the appropriate agent",
590
+ body: ProcessEventBody,
591
+ response: { 200: ProcessEventOk, 503: ErrorResponse }
592
+ },
593
+ async handler(req, reply) {
594
+ const body = req.body;
595
+ const taskId = randomUUID();
596
+ if (!agentRunner || !enqueue) {
597
+ return reply.code(503).send({ error: "Agent runner or queue not available" });
598
+ }
599
+ const wantsStream = req.headers.accept?.includes("text/event-stream");
600
+ if (wantsStream) {
601
+ const stream = startSseHeaders(reply);
602
+ try {
603
+ await enqueue(taskId, async () => {
604
+ writeSse(stream, { type: "progress", content: `Processing event ${taskId}...` });
605
+ await agentRunner.runAgentForChat(taskId, body.message, {
606
+ repoPath: body.context?.repoPath ?? void 0,
607
+ cwd: body.context?.cwd || process.cwd(),
608
+ threadId: body.context?.threadId ?? void 0,
609
+ onProgress: (messages) => {
610
+ for (const msg of messages) {
611
+ writeSse(stream, { type: "progress", content: msg });
612
+ }
613
+ },
614
+ onFinal: (text) => {
615
+ writeSse(stream, { type: "final", content: text });
616
+ stream.end();
617
+ }
618
+ });
619
+ });
620
+ } catch (err) {
621
+ writeSse(stream, { type: "final", content: `Error: ${err.message}` });
622
+ stream.end();
623
+ }
624
+ return reply;
625
+ }
626
+ try {
627
+ await enqueue(taskId, async () => {
628
+ await agentRunner.runAgentForChat(taskId, body.message, {
629
+ repoPath: body.context?.repoPath ?? void 0,
630
+ cwd: body.context?.cwd || process.cwd(),
631
+ threadId: body.context?.threadId ?? void 0
632
+ });
633
+ });
634
+ return { taskId };
635
+ } catch (err) {
636
+ return reply.code(500).send({ error: err.message });
637
+ }
638
+ }
639
+ });
640
+ return [getServerInfo, getHealth, listAgents2, queueStatus, pluginsStatus, runAgent, processEvent];
641
+ }
642
+ function registerCoreApiRoutes(app, deps) {
643
+ registerRoutes(app, buildCoreApiRoutes(deps));
644
+ }
645
+
646
+ // src/config/load.ts
647
+ import { readFileSync, existsSync } from "fs";
648
+ import { resolve } from "path";
649
+
650
+ // src/config/deep-merge.ts
651
+ function deepMerge(target, source) {
652
+ const result = { ...target };
653
+ for (const key of Object.keys(source)) {
654
+ const sourceValue = source[key];
655
+ const targetValue = result[key];
656
+ if (sourceValue !== void 0 && typeof sourceValue === "object" && !Array.isArray(sourceValue) && typeof targetValue === "object" && targetValue !== null && !Array.isArray(targetValue)) {
657
+ result[key] = deepMerge(
658
+ targetValue,
659
+ sourceValue
660
+ );
661
+ } else if (sourceValue !== void 0) {
662
+ result[key] = sourceValue;
663
+ }
664
+ }
665
+ return result;
666
+ }
667
+
668
+ // src/config/load.ts
669
+ import * as z3 from "zod";
670
+
671
+ // src/config/schema.ts
672
+ import * as z2 from "zod";
673
+ var pluginEntrySchema = z2.object({
674
+ package: z2.string().optional(),
675
+ options: z2.record(z2.string(), z2.unknown()).optional()
676
+ }).strict();
677
+ var agentsRunnerConfigSchema = z2.object({
678
+ timeoutMs: z2.number().int().positive().optional(),
679
+ maxBufferBytes: z2.number().int().positive().optional(),
680
+ postFinalGraceMs: z2.number().int().nonnegative().optional(),
681
+ forceKillDelayMs: z2.number().int().nonnegative().optional()
682
+ }).strict();
683
+ var agentModelConfigSchema = z2.object({
684
+ defaultModel: z2.string().optional()
685
+ }).strict();
686
+ var agentsEntrySchema = z2.union([agentModelConfigSchema, agentsRunnerConfigSchema]);
687
+ var appConfigSchema = z2.object({
688
+ port: z2.number().optional(),
689
+ agent: z2.string().optional(),
690
+ agents: z2.record(z2.string(), agentsEntrySchema).optional(),
691
+ plugins: z2.array(pluginEntrySchema).optional(),
692
+ pluginSystem: z2.object({
693
+ /**
694
+ * When true, the host aborts startup if plugin contract validation
695
+ * detects missing capability-backed providers (e.g. requires capability
696
+ * but no mapped service is registered).
697
+ */
698
+ failOnContractErrors: z2.boolean().optional(),
699
+ /**
700
+ * When true, the host aborts startup if plugin dependency resolution
701
+ * detects missing dependencies or circular cycles.
702
+ */
703
+ failOnDependencyErrors: z2.boolean().optional(),
704
+ /**
705
+ * When true, the host aborts startup if any configured plugin fails
706
+ * to import, validate, or register.
707
+ */
708
+ failOnPluginLoadErrors: z2.boolean().optional()
709
+ }).strict().optional(),
710
+ /** Merged into `createObservability` / request logger; `requestLogger.excludePaths` is read in `server.ts`. */
711
+ observability: z2.record(z2.string(), z2.unknown()).optional(),
712
+ /**
713
+ * Operator guardrails for orchestrated tasks (webhook-driven and similar).
714
+ * `maxConcurrent` wraps the default in-memory task queue so at most N agent
715
+ * runs execute at once across all queue keys. `maxWallTimeMs` caps a single
716
+ * orchestrated `runAgentForChat` call (the subprocess may still shut down
717
+ * on its own schedule — see agent `agents.runner.timeoutMs`).
718
+ */
719
+ tasks: z2.object({
720
+ maxConcurrent: z2.number().int().positive().max(1e3).optional(),
721
+ maxWallTimeMs: z2.number().int().positive().optional()
722
+ }).strict().optional(),
723
+ /**
724
+ * When set, each task lifecycle line (start / completed / failed) is appended
725
+ * as one JSON object per line (JSONL) to `path` (absolute or relative to the
726
+ * config directory).
727
+ */
728
+ runRecords: z2.object({
729
+ path: z2.string().min(1)
730
+ }).strict().optional(),
731
+ docsAuthRequired: z2.boolean().optional(),
732
+ docsApiKey: z2.string().optional()
733
+ }).strict();
734
+
735
+ // src/config/load.ts
736
+ function loadConfig(options) {
737
+ const configDir = options?.configRoot ?? resolve(process.cwd(), "config");
738
+ let config = {};
739
+ const defaultConfigPath = resolve(configDir, "default.json");
740
+ if (existsSync(defaultConfigPath)) {
741
+ try {
742
+ config = JSON.parse(readFileSync(defaultConfigPath, "utf8"));
743
+ } catch (err) {
744
+ console.warn("Failed to load config/default.json, using defaults:", err.message);
745
+ }
746
+ }
747
+ const localConfigPath = resolve(configDir, "local.json");
748
+ if (existsSync(localConfigPath)) {
749
+ try {
750
+ const localConfig = JSON.parse(readFileSync(localConfigPath, "utf8"));
751
+ config = deepMerge(config, localConfig);
752
+ } catch (err) {
753
+ console.warn("Failed to load config/local.json:", err.message);
754
+ }
755
+ }
756
+ const merged = config;
757
+ applyCoreEnvWhitelist(merged);
758
+ applyPluginEnvWhitelist(merged);
759
+ const parsed = appConfigSchema.safeParse(merged);
760
+ if (!parsed.success) {
761
+ throw new Error(`Invalid application config: ${JSON.stringify(z3.treeifyError(parsed.error))}`);
762
+ }
763
+ return parsed.data;
764
+ }
765
+
766
+ // src/server.ts
767
+ var loadConfig2 = loadConfig;
768
+ async function createServer(config, observability, agentModels, agentRunner, enqueue, options = {}) {
769
+ const app = Fastify({
770
+ logger: false,
771
+ bodyLimit: 5 * 1024 * 1024
772
+ }).withTypeProvider();
773
+ app.setValidatorCompiler(validatorCompiler);
774
+ app.setSerializerCompiler(serializerCompiler);
775
+ app.setErrorHandler((err, _req, reply) => {
776
+ const error = err;
777
+ const status = error.statusCode ?? 500;
778
+ if (status >= 500) {
779
+ observability.logger.error("Unhandled error", error);
780
+ }
781
+ if (reply.sent) return;
782
+ void reply.code(status).send({ error: error.message });
783
+ });
784
+ const obsCfg = config.observability;
785
+ const excludePaths = obsCfg?.requestLogger?.excludePaths ?? ["/api/health", "/api/metrics"];
786
+ await app.register(
787
+ createRequestLogger({
788
+ logger: observability.logger,
789
+ tracing: observability.tracing,
790
+ metrics: observability.metrics,
791
+ excludePaths
792
+ })
793
+ );
794
+ await app.register(swagger, {
795
+ openapi: {
796
+ info: {
797
+ title: "Agent Detective API",
798
+ version: APP_VERSION,
799
+ description: `Core and plugin endpoints for the ${APP_NAME} server.`
800
+ },
801
+ servers: [{ url: "/" }],
802
+ tags: [
803
+ { name: CORE_PLUGIN_TAG, description: createTagDescription(CORE_PLUGIN_TAG) }
804
+ ]
805
+ },
806
+ transform: jsonSchemaTransform,
807
+ transformObject: (documentObject) => {
808
+ const doc = "openapiObject" in documentObject ? documentObject.openapiObject : documentObject.swaggerObject;
809
+ return applyTagGroups(doc, {
810
+ pluginTags: options.getPluginTags?.()
811
+ });
812
+ }
813
+ });
814
+ await registerDocsRoute(app, config, observability);
815
+ app.get("/", async () => {
816
+ const pluginPackages = (config.plugins ?? []).map((e) => e.package).filter((p) => Boolean(p));
817
+ return {
818
+ name: APP_NAME,
819
+ version: APP_VERSION,
820
+ plugins: pluginPackages
821
+ };
822
+ });
823
+ registerCoreApiRoutes(app, {
824
+ agentModels,
825
+ agentRunner,
826
+ enqueue,
827
+ observability,
828
+ config: { plugins: config.plugins },
829
+ pluginStatus: options.getPluginStatus
830
+ });
831
+ return { app };
832
+ }
833
+ async function registerDocsRoute(app, config, observability) {
834
+ const docsAuthRequired = config.docsAuthRequired ?? process.env.DOCS_AUTH_REQUIRED === "true";
835
+ const docsApiKey = config.docsApiKey ?? process.env.DOCS_API_KEY;
836
+ await app.register(
837
+ async (scope) => {
838
+ if (docsAuthRequired) {
839
+ scope.addHook("onRequest", async (req, reply) => {
840
+ const apiKey = req.headers["x-api-key"];
841
+ if (!apiKey || apiKey !== docsApiKey) {
842
+ await reply.code(401).send({ error: "Unauthorized. Provide X-API-KEY header." });
843
+ }
844
+ });
845
+ }
846
+ const { default: scalarReference } = await import("@scalar/fastify-api-reference");
847
+ await scope.register(scalarReference, {
848
+ routePrefix: "/",
849
+ configuration: {
850
+ metaData: { title: "Agent Detective API" }
851
+ }
852
+ });
853
+ },
854
+ { prefix: "/docs" }
855
+ );
856
+ observability.logger.info("API documentation available at /docs");
857
+ }
858
+
859
+ // src/agents/index.ts
860
+ import { execSync as execSync2 } from "child_process";
861
+
862
+ // src/agents/utils.ts
863
+ import { execSync } from "child_process";
864
+ function shellQuote(value) {
865
+ const escaped = String(value).replace(/'/g, String.raw`'\''`);
866
+ return `'${escaped}'`;
867
+ }
868
+ function resolvePromptValue(prompt, promptExpression) {
869
+ if (promptExpression) return promptExpression;
870
+ return shellQuote(prompt);
871
+ }
872
+ function isCommandAvailable(command) {
873
+ try {
874
+ execSync(`command -v ${command} > /dev/null 2>&1`, { stdio: "ignore" });
875
+ return true;
876
+ } catch {
877
+ return false;
878
+ }
879
+ }
880
+
881
+ // src/agents/claude.ts
882
+ var CLAUDE_CMD = "claude";
883
+ var CLAUDE_OUTPUT_FORMAT = "stream-json";
884
+ var CLAUDE_SESSION_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
885
+ var DEFAULT_MODEL = "sonnet";
886
+ function safeJsonParse(value) {
887
+ try {
888
+ return JSON.parse(value);
889
+ } catch {
890
+ return null;
891
+ }
892
+ }
893
+ var ESC = String.fromCharCode(27);
894
+ var BEL = String.fromCharCode(7);
895
+ var DEL = String.fromCharCode(127);
896
+ var C0_AND_DEL = [...Array(32).keys()].map((i) => String.fromCharCode(i)).join("") + DEL;
897
+ var ANSI_CSI_PATTERN = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
898
+ var ANSI_OSC_PATTERN = new RegExp(`${ESC}\\][^${BEL}]*(?:${BEL}|${ESC}\\\\)`, "g");
899
+ var CTRL_OR_DEL_PATTERN = new RegExp(`[${C0_AND_DEL}]`, "g");
900
+ function stripAnsi(value) {
901
+ return String(value || "").replace(ANSI_CSI_PATTERN, "").replace(ANSI_OSC_PATTERN, "");
902
+ }
903
+ function sanitizeSessionId(value) {
904
+ if (value === void 0 || value === null) return void 0;
905
+ const cleaned = String(value).replace(CTRL_OR_DEL_PATTERN, "").trim().replace(/^['"]+/, "").replace(/['"\\]+$/, "").trim();
906
+ if (!CLAUDE_SESSION_ID_REGEX.test(cleaned)) return void 0;
907
+ return cleaned;
908
+ }
909
+ function buildCommand({ prompt, promptExpression, threadId, model, readOnly }) {
910
+ const promptValue = resolvePromptValue(prompt, promptExpression);
911
+ const args = [
912
+ "-p",
913
+ promptValue,
914
+ "--output-format",
915
+ CLAUDE_OUTPUT_FORMAT,
916
+ "--verbose",
917
+ ...readOnly ? ["--allowedTools", "Read,LS,Glob,Grep,WebSearch,WebFetch"] : ["--dangerously-skip-permissions"]
918
+ ];
919
+ if (model) {
920
+ args.push("--model", shellQuote(model));
921
+ }
922
+ const safeThreadId = sanitizeSessionId(threadId);
923
+ if (safeThreadId) {
924
+ args.push("--resume", shellQuote(safeThreadId));
925
+ }
926
+ return `${CLAUDE_CMD} ${args.join(" ")}`.trim();
927
+ }
928
+ function extractUsage(evt) {
929
+ if (evt.type !== "result") return void 0;
930
+ const u = {};
931
+ if (typeof evt.duration_ms === "number") u.durationMs = evt.duration_ms;
932
+ if (typeof evt.duration_api_ms === "number") u.durationApiMs = evt.duration_api_ms;
933
+ if (typeof evt.num_turns === "number") u.numTurns = evt.num_turns;
934
+ if (typeof evt.total_cost_usd === "number") u.totalCostUsd = evt.total_cost_usd;
935
+ if (typeof evt.usage?.input_tokens === "number") u.inputTokens = evt.usage.input_tokens;
936
+ if (typeof evt.usage?.output_tokens === "number") u.outputTokens = evt.usage.output_tokens;
937
+ return Object.keys(u).length > 0 ? u : void 0;
938
+ }
939
+ function parseOutput(output) {
940
+ const cleaned = stripAnsi(output);
941
+ const lines = cleaned.split(/\r?\n/).filter((l) => l.trim());
942
+ let sessionId;
943
+ let resultText = "";
944
+ let usage;
945
+ for (const line of lines) {
946
+ const evt = safeJsonParse(line.trim());
947
+ if (!evt) continue;
948
+ if (evt.session_id) {
949
+ sessionId = sanitizeSessionId(evt.session_id) ?? sessionId;
950
+ }
951
+ if (evt.type === "result") {
952
+ resultText = typeof evt.result === "string" ? evt.result : "";
953
+ usage = extractUsage(evt);
954
+ }
955
+ }
956
+ if (resultText) {
957
+ return { text: resultText.trim(), threadId: sessionId, sawJson: true, usage };
958
+ }
959
+ let accumulated = "";
960
+ for (const line of lines) {
961
+ const evt = safeJsonParse(line.trim());
962
+ if (!evt || evt.type !== "stream_event") continue;
963
+ if (evt.event?.delta?.type === "text_delta" && evt.event.delta.text) {
964
+ accumulated += evt.event.delta.text;
965
+ }
966
+ }
967
+ if (accumulated) {
968
+ return { text: accumulated.trim(), threadId: sessionId, sawJson: true };
969
+ }
970
+ return { text: cleaned.trim(), threadId: sessionId, sawJson: false };
971
+ }
972
+ function parseStreamingOutput(output) {
973
+ const cleaned = stripAnsi(output);
974
+ const lines = cleaned.split(/\r?\n/).filter((l) => l.trim());
975
+ let sessionId;
976
+ let sawFinal = false;
977
+ let resultText = "";
978
+ let usage;
979
+ const commentary = [];
980
+ for (const line of lines) {
981
+ const evt = safeJsonParse(line.trim());
982
+ if (!evt) continue;
983
+ if (evt.session_id) {
984
+ sessionId = sanitizeSessionId(evt.session_id) ?? sessionId;
985
+ }
986
+ if (evt.type === "result") {
987
+ sawFinal = true;
988
+ resultText = typeof evt.result === "string" ? evt.result : "";
989
+ usage = extractUsage(evt);
990
+ }
991
+ if (evt.type === "stream_event" && evt.event) {
992
+ if (evt.event.type === "content_block_start" && evt.event.content_block?.type === "tool_use" && evt.event.content_block.name) {
993
+ commentary.push(`Tool: ${evt.event.content_block.name}`);
994
+ }
995
+ }
996
+ }
997
+ return {
998
+ text: resultText.trim(),
999
+ threadId: sessionId,
1000
+ sawJson: sawFinal,
1001
+ sawFinal,
1002
+ commentaryMessages: commentary,
1003
+ usage
1004
+ };
1005
+ }
1006
+ var claudeAgent = {
1007
+ id: "claude",
1008
+ label: "claude",
1009
+ needsPty: false,
1010
+ mergeStderr: false,
1011
+ command: CLAUDE_CMD,
1012
+ buildCommand,
1013
+ parseOutput,
1014
+ parseStreamingOutput,
1015
+ defaultModel: DEFAULT_MODEL,
1016
+ checkAvailable: () => isCommandAvailable(CLAUDE_CMD)
1017
+ };
1018
+ var claude_default = claudeAgent;
1019
+
1020
+ // src/agents/cursor.ts
1021
+ var CURSOR_AGENT_CMD = "agent";
1022
+ var OUTPUT_FORMAT = "json";
1023
+ var DEFAULT_MODEL2 = "composer-2.5-fast";
1024
+ function safeJsonParse2(value) {
1025
+ try {
1026
+ return JSON.parse(value);
1027
+ } catch {
1028
+ return null;
1029
+ }
1030
+ }
1031
+ function buildCommand2({
1032
+ prompt,
1033
+ promptExpression,
1034
+ threadId,
1035
+ model,
1036
+ readOnly
1037
+ }) {
1038
+ const promptValue = resolvePromptValue(prompt, promptExpression);
1039
+ const modelToUse = model || DEFAULT_MODEL2;
1040
+ const args = [
1041
+ "-p",
1042
+ promptValue,
1043
+ "--output-format",
1044
+ OUTPUT_FORMAT,
1045
+ "--model",
1046
+ shellQuote(modelToUse)
1047
+ ];
1048
+ if (readOnly) {
1049
+ args.push("--mode=ask");
1050
+ }
1051
+ if (threadId) {
1052
+ args.push("--resume", shellQuote(threadId));
1053
+ }
1054
+ return `${CURSOR_AGENT_CMD} ${args.join(" ")}`.trim();
1055
+ }
1056
+ function parseOutput2(output) {
1057
+ const trimmed = String(output || "").trim();
1058
+ if (!trimmed) {
1059
+ return { text: "", threadId: void 0, sawJson: false };
1060
+ }
1061
+ const payload = safeJsonParse2(trimmed);
1062
+ if (!payload || payload.type !== "result") {
1063
+ return { text: trimmed, threadId: void 0, sawJson: false };
1064
+ }
1065
+ const result = payload.result;
1066
+ const sessionId = payload.session_id;
1067
+ const text = typeof result === "string" ? result.trim() : "";
1068
+ const threadId = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : void 0;
1069
+ return { text, threadId, sawJson: true };
1070
+ }
1071
+ var cursorAgent = {
1072
+ id: "cursor",
1073
+ label: "cursor",
1074
+ needsPty: false,
1075
+ mergeStderr: false,
1076
+ command: CURSOR_AGENT_CMD,
1077
+ buildCommand: buildCommand2,
1078
+ parseOutput: parseOutput2,
1079
+ defaultModel: DEFAULT_MODEL2,
1080
+ checkAvailable: () => isCommandAvailable(CURSOR_AGENT_CMD)
1081
+ };
1082
+ var cursor_default = cursorAgent;
1083
+
1084
+ // src/agents/opencode.ts
1085
+ var OPENCODE_CMD = "opencode";
1086
+ var OPENCODE_OUTPUT_FORMAT = "json";
1087
+ var DEFAULT_MODEL3 = "opencode/deepseek-v4-flash-free";
1088
+ var OPENCODE_PERMISSION_FULL = '{"*": "allow"}';
1089
+ var OPENCODE_PERMISSION_READ_ONLY = '{"*":"allow","bash":"deny","edit":"deny","write":"deny","multiedit":"deny","patch":"deny"}';
1090
+ function pickPermission(readOnly) {
1091
+ return readOnly ? OPENCODE_PERMISSION_READ_ONLY : OPENCODE_PERMISSION_FULL;
1092
+ }
1093
+ function safeJsonParse3(value) {
1094
+ try {
1095
+ return JSON.parse(value);
1096
+ } catch {
1097
+ return null;
1098
+ }
1099
+ }
1100
+ function buildCommand3({
1101
+ prompt,
1102
+ promptExpression,
1103
+ threadId,
1104
+ model,
1105
+ readOnly
1106
+ }) {
1107
+ const promptValue = resolvePromptValue(prompt, promptExpression);
1108
+ const args = ["run", "--format", OPENCODE_OUTPUT_FORMAT];
1109
+ const modelToUse = model || DEFAULT_MODEL3;
1110
+ args.push("--model", shellQuote(modelToUse));
1111
+ if (threadId) {
1112
+ args.push("--continue");
1113
+ args.push("--session", shellQuote(threadId));
1114
+ }
1115
+ args.push(promptValue);
1116
+ const command = `${OPENCODE_CMD} ${args.join(" ")}`.trim();
1117
+ const permission = pickPermission(readOnly);
1118
+ return `OPENCODE_PERMISSION=${shellQuote(permission)} ${command} < /dev/null`;
1119
+ }
1120
+ function parseOutput3(output) {
1121
+ const lines = String(output || "").split(/\r?\n/);
1122
+ let threadId;
1123
+ const textParts = [];
1124
+ let sawJson = false;
1125
+ for (const line of lines) {
1126
+ const trimmed = line.trim();
1127
+ if (!trimmed) continue;
1128
+ if (!trimmed.startsWith("{")) continue;
1129
+ const payload = safeJsonParse3(trimmed);
1130
+ if (!payload || typeof payload !== "object") continue;
1131
+ sawJson = true;
1132
+ if (payload.sessionID) {
1133
+ threadId = payload.sessionID;
1134
+ }
1135
+ if (payload.type === "text" && payload.part && payload.part.text) {
1136
+ textParts.push(payload.part.text);
1137
+ }
1138
+ }
1139
+ const text = textParts.join("").trim();
1140
+ if (!sawJson) {
1141
+ return {
1142
+ text: String(output || "").trim(),
1143
+ threadId: void 0,
1144
+ sawJson: false
1145
+ };
1146
+ }
1147
+ return { text, threadId, sawJson: true };
1148
+ }
1149
+ function listModelsCommand() {
1150
+ return `OPENCODE_PERMISSION=${shellQuote(OPENCODE_PERMISSION_FULL)} ${OPENCODE_CMD} models < /dev/null`;
1151
+ }
1152
+ function parseModelList(output) {
1153
+ const lines = String(output || "").split(/\r?\n/);
1154
+ const models = [];
1155
+ for (const line of lines) {
1156
+ const trimmed = line.trim();
1157
+ if (!trimmed) continue;
1158
+ if (trimmed.startsWith("INFO")) continue;
1159
+ models.push(trimmed);
1160
+ }
1161
+ return models.join("\n");
1162
+ }
1163
+ var opencodeAgent = {
1164
+ id: "opencode",
1165
+ label: "opencode",
1166
+ needsPty: false,
1167
+ mergeStderr: false,
1168
+ command: OPENCODE_CMD,
1169
+ buildCommand: buildCommand3,
1170
+ parseOutput: parseOutput3,
1171
+ listModelsCommand,
1172
+ parseModelList,
1173
+ defaultModel: DEFAULT_MODEL3,
1174
+ checkAvailable: () => isCommandAvailable(OPENCODE_CMD),
1175
+ // opencode keeps a single-user SQLite DB at `~/.local/share/opencode/`
1176
+ // and crashes with `PRAGMA journal_mode = WAL` when two CLI instances
1177
+ // race to open it. See https://github.com/anomalyco/opencode/issues/21215.
1178
+ // The runner serializes opencode invocations to avoid that, at the cost of
1179
+ // losing parallelism for multi-repo Jira fan-out.
1180
+ singleInstance: true
1181
+ };
1182
+ var opencode_default = opencodeAgent;
1183
+
1184
+ // src/agents/index.ts
1185
+ var agents = /* @__PURE__ */ new Map([
1186
+ [claude_default.id, claude_default],
1187
+ [cursor_default.id, cursor_default],
1188
+ [opencode_default.id, opencode_default]
1189
+ ]);
1190
+ var DEFAULT_AGENT = opencode_default.id;
1191
+ var AGENT_CLAUDE = claude_default.id;
1192
+ var AGENT_CURSOR = cursor_default.id;
1193
+ var AGENT_OPENCODE = opencode_default.id;
1194
+ function normalizeAgent(value) {
1195
+ if (!value) return DEFAULT_AGENT;
1196
+ const normalized = String(value).trim().toLowerCase();
1197
+ if (agents.has(normalized)) return normalized;
1198
+ return DEFAULT_AGENT;
1199
+ }
1200
+ function isKnownAgent(value) {
1201
+ if (!value) return false;
1202
+ const normalized = String(value).trim().toLowerCase();
1203
+ return agents.has(normalized);
1204
+ }
1205
+ function getAgent(value) {
1206
+ return agents.get(normalizeAgent(value));
1207
+ }
1208
+ function getAgentLabel(value) {
1209
+ const agent = getAgent(value);
1210
+ return agent?.label ?? DEFAULT_AGENT;
1211
+ }
1212
+ function listAgents() {
1213
+ return Array.from(agents.values());
1214
+ }
1215
+ function isAgentInstalled(agentId) {
1216
+ const agent = agents.get(agentId);
1217
+ if (!agent || !agent.command) return false;
1218
+ try {
1219
+ execSync2(`command -v ${agent.command} > /dev/null 2>&1`, { stdio: "ignore" });
1220
+ return true;
1221
+ } catch {
1222
+ return false;
1223
+ }
1224
+ }
1225
+
1226
+ export {
1227
+ APP_NAME,
1228
+ APP_VERSION,
1229
+ applyLogLevelAliasForObservability,
1230
+ loadConfig2 as loadConfig,
1231
+ createServer,
1232
+ normalizeAgent,
1233
+ isKnownAgent,
1234
+ getAgentLabel,
1235
+ listAgents,
1236
+ isAgentInstalled
1237
+ };