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.
package/dist/index.js ADDED
@@ -0,0 +1,661 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createLimitedConcurrencyTaskQueue,
4
+ createMemoryTaskQueue,
5
+ createPluginSystem
6
+ } from "./chunk-OIYJYLCB.js";
7
+ import {
8
+ APP_NAME,
9
+ APP_VERSION,
10
+ applyLogLevelAliasForObservability,
11
+ createServer,
12
+ getAgentLabel,
13
+ listAgents,
14
+ loadConfig,
15
+ normalizeAgent
16
+ } from "./chunk-H2IXGHNA.js";
17
+
18
+ // src/core/event-bus.ts
19
+ var AsyncEventBus = class {
20
+ constructor(log = console) {
21
+ this.log = log;
22
+ }
23
+ log;
24
+ handlers = /* @__PURE__ */ new Map();
25
+ /**
26
+ * Register a listener for an event.
27
+ */
28
+ on(event, handler) {
29
+ const list = this.handlers.get(event) || [];
30
+ list.push(handler);
31
+ this.handlers.set(event, list);
32
+ }
33
+ /**
34
+ * Unregister a listener for an event.
35
+ */
36
+ off(event, handler) {
37
+ const list = this.handlers.get(event) || [];
38
+ const index = list.indexOf(handler);
39
+ if (index !== -1) {
40
+ list.splice(index, 1);
41
+ if (list.length === 0) {
42
+ this.handlers.delete(event);
43
+ } else {
44
+ this.handlers.set(event, list);
45
+ }
46
+ }
47
+ }
48
+ /**
49
+ * Emit an event without waiting for responses (fire-and-forget).
50
+ */
51
+ emit(event, ...args) {
52
+ const list = this.handlers.get(event) || [];
53
+ for (const handler of list) {
54
+ try {
55
+ void handler(...args);
56
+ } catch (err) {
57
+ this.log.error(`Error in event handler for ${event}: ${err.message}`);
58
+ }
59
+ }
60
+ }
61
+ /**
62
+ * Invoke all handlers asynchronously and gather their return values.
63
+ * Useful for hook-like behavior where handlers provide data.
64
+ */
65
+ async invokeAsync(event, ...args) {
66
+ const list = this.handlers.get(event) || [];
67
+ const promises = list.map(async (handler) => {
68
+ try {
69
+ return await handler(...args);
70
+ } catch (err) {
71
+ this.log.error(
72
+ `Error in async event handler for ${event}: ${err instanceof Error ? err.message : String(err)}`
73
+ );
74
+ return null;
75
+ }
76
+ });
77
+ const results = await Promise.all(promises);
78
+ return results.filter((r) => r !== null && r !== void 0);
79
+ }
80
+ };
81
+ function createEventBus(log) {
82
+ return new AsyncEventBus(log);
83
+ }
84
+
85
+ // src/core/orchestrator.ts
86
+ import { StandardEvents } from "@agent-detective/sdk";
87
+
88
+ // src/core/run-records.ts
89
+ import { appendFile } from "fs/promises";
90
+ var RUN_RECORD_SCHEMA = "agent-detective.run-record/v1";
91
+ function createRunRecordWriter(absolutePath, logger) {
92
+ return {
93
+ async append(record) {
94
+ try {
95
+ await appendFile(absolutePath, `${JSON.stringify(record)}
96
+ `, "utf8");
97
+ } catch (err) {
98
+ logger.warn(`runRecords: append failed (${absolutePath}): ${err.message}`);
99
+ }
100
+ }
101
+ };
102
+ }
103
+ function buildRunRecordBase(task) {
104
+ const issueKey = task.replyTo?.type === "issue" && typeof task.replyTo.id === "string" ? task.replyTo.id : void 0;
105
+ return {
106
+ taskId: task.id,
107
+ ...typeof task.source === "string" && task.source.length > 0 ? { source: task.source } : {},
108
+ ...issueKey ? { issueKey } : {}
109
+ };
110
+ }
111
+
112
+ // src/core/orchestrator.ts
113
+ function createOrchestrator(deps) {
114
+ const { eventBus, agentRunner, enqueue, logger, maxWallTimeMs, runRecords } = deps;
115
+ function start() {
116
+ eventBus.on(StandardEvents.TASK_CREATED, handleTaskCreated);
117
+ }
118
+ async function handleTaskCreated(task) {
119
+ const queueKey = task.id;
120
+ await enqueue(queueKey, async () => {
121
+ const startedAt = Date.now();
122
+ const base = buildRunRecordBase(task);
123
+ if (runRecords) {
124
+ await runRecords.append({
125
+ schema: RUN_RECORD_SCHEMA,
126
+ phase: "started",
127
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
128
+ ...base
129
+ });
130
+ }
131
+ try {
132
+ const contextPieces = await eventBus.invokeAsync(
133
+ StandardEvents.TASK_GATHER_CONTEXT,
134
+ task
135
+ );
136
+ let finalPrompt = task.message;
137
+ if (contextPieces.length > 0) {
138
+ finalPrompt += "\n\nAdditional Context:\n" + contextPieces.join("\n\n");
139
+ }
140
+ const runPromise = agentRunner.runAgentForChat(task.id, finalPrompt, {
141
+ repoPath: task.context.repoPath,
142
+ model: task.context.model,
143
+ cwd: task.context.cwd,
144
+ readOnly: task.metadata?.readOnly === true,
145
+ threadId: task.context.threadId ?? void 0
146
+ });
147
+ const result = maxWallTimeMs !== void 0 && maxWallTimeMs > 0 ? await raceWithWallTimeout(runPromise, maxWallTimeMs) : await runPromise;
148
+ eventBus.emit(StandardEvents.TASK_COMPLETED, {
149
+ event: task,
150
+ result: result.text
151
+ });
152
+ if (runRecords) {
153
+ await runRecords.append({
154
+ schema: RUN_RECORD_SCHEMA,
155
+ phase: "completed",
156
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
157
+ ...base,
158
+ durationMs: Date.now() - startedAt
159
+ });
160
+ }
161
+ } catch (err) {
162
+ const message = err instanceof Error ? err.message : String(err);
163
+ logger.error(`Orchestrator error for task ${task.id}: ${message}`);
164
+ eventBus.emit(StandardEvents.TASK_FAILED, {
165
+ event: task,
166
+ error: message
167
+ });
168
+ if (runRecords) {
169
+ await runRecords.append({
170
+ schema: RUN_RECORD_SCHEMA,
171
+ phase: "failed",
172
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
173
+ ...base,
174
+ durationMs: Date.now() - startedAt,
175
+ error: message
176
+ });
177
+ }
178
+ }
179
+ });
180
+ }
181
+ return {
182
+ start
183
+ };
184
+ }
185
+ async function raceWithWallTimeout(work, maxWallTimeMs) {
186
+ let timeoutId;
187
+ const timeout = new Promise((_, reject) => {
188
+ timeoutId = setTimeout(() => {
189
+ reject(new Error(`Task exceeded orchestrator wall time (${maxWallTimeMs}ms)`));
190
+ }, maxWallTimeMs);
191
+ });
192
+ try {
193
+ return await Promise.race([work, timeout]);
194
+ } finally {
195
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
196
+ }
197
+ }
198
+
199
+ // src/core/process.ts
200
+ import {
201
+ shellQuote,
202
+ wrapCommandWithPty,
203
+ terminateChildProcess,
204
+ execLocal,
205
+ execLocalStreaming
206
+ } from "@agent-detective/process-utils";
207
+
208
+ // src/core/agent-runner.ts
209
+ var DEFAULT_TIMEOUT_MS = 12e4;
210
+ var DEFAULT_MAX_BUFFER = 10 * 1024 * 1024;
211
+ function createAgentRunner(options) {
212
+ const {
213
+ agentTimeoutMs = DEFAULT_TIMEOUT_MS,
214
+ agentMaxBuffer = DEFAULT_MAX_BUFFER,
215
+ execLocal: execLocal2,
216
+ execLocalStreaming: execLocalStreaming2,
217
+ terminateChildProcess: terminateChildProcess2,
218
+ defaultModels,
219
+ postFinalGraceMs = 3e4,
220
+ forceKillDelayMs = 1e3,
221
+ logger: log = console,
222
+ defaultAgentId
223
+ } = options;
224
+ const agents = /* @__PURE__ */ new Map();
225
+ const activeRuns = /* @__PURE__ */ new Map();
226
+ const agentSerializationTails = /* @__PURE__ */ new Map();
227
+ function buildActiveRunKey(taskId, contextKey) {
228
+ return `${taskId}:${contextKey || "default"}`;
229
+ }
230
+ async function acquireAgentSlot(agentId, taskId) {
231
+ const prev = agentSerializationTails.get(agentId) ?? Promise.resolve();
232
+ let release;
233
+ const ours = new Promise((resolve2) => {
234
+ release = resolve2;
235
+ });
236
+ agentSerializationTails.set(agentId, ours);
237
+ const waitStartedAt = Date.now();
238
+ await prev;
239
+ const waitMs = Date.now() - waitStartedAt;
240
+ if (waitMs > 10) {
241
+ log.info(
242
+ `Agent queued task=${taskId} agent=${agentId} singleInstance=true waitMs=${waitMs}`
243
+ );
244
+ }
245
+ return () => {
246
+ release();
247
+ if (agentSerializationTails.get(agentId) === ours) {
248
+ agentSerializationTails.delete(agentId);
249
+ }
250
+ };
251
+ }
252
+ async function runAgentForChat(taskId, prompt, runOptions = {}) {
253
+ const {
254
+ contextKey,
255
+ repoPath,
256
+ cwd = process.cwd(),
257
+ agentId: overrideAgentId,
258
+ model: modelOverride,
259
+ onFinal,
260
+ onProgress,
261
+ onStdout: callerOnStdout,
262
+ readOnly,
263
+ timeoutMs: runTimeoutMs,
264
+ threadId,
265
+ inputFiles
266
+ } = runOptions;
267
+ const effectiveAgentId = overrideAgentId || defaultAgentId || "opencode";
268
+ const agent = agents.get(effectiveAgentId);
269
+ if (!agent) {
270
+ throw new Error(`Unknown agent: ${effectiveAgentId}. Ensure it is registered.`);
271
+ }
272
+ const activeKey = buildActiveRunKey(taskId, contextKey);
273
+ const releaseAgentSlot = agent.singleInstance ? await acquireAgentSlot(agent.id, taskId) : null;
274
+ log.info(
275
+ `Agent start task=${taskId} agent=${effectiveAgentId} repo=${repoPath || "none"}${readOnly ? " readOnly=true" : ""}`
276
+ );
277
+ const startedAt = Date.now();
278
+ const run = {
279
+ child: null,
280
+ finalEmitted: false,
281
+ settled: false,
282
+ stopPending: false
283
+ };
284
+ activeRuns.set(activeKey, run);
285
+ const schedulePostFinalKill = () => {
286
+ if (!run.child || run.finalEmitted) return;
287
+ const delayMs = Math.max(0, Number(postFinalGraceMs) || 0);
288
+ setTimeout(() => {
289
+ if (run.settled || !run.child) return;
290
+ terminateChildProcess2?.(run.child, "SIGTERM");
291
+ setTimeout(() => {
292
+ if (run.settled || !run.child) return;
293
+ terminateChildProcess2?.(run.child, "SIGKILL");
294
+ }, Math.max(0, Number(forceKillDelayMs) || 0));
295
+ }, delayMs);
296
+ };
297
+ const emitFinal = (text) => {
298
+ const normalizedText = String(text || "").trim();
299
+ if (!normalizedText || run.finalEmitted) return;
300
+ run.finalEmitted = true;
301
+ onFinal?.(normalizedText);
302
+ schedulePostFinalKill();
303
+ };
304
+ const emitProgress = (payload) => {
305
+ onProgress?.(payload);
306
+ };
307
+ try {
308
+ const result = await runShellAgent(agent, {
309
+ prompt,
310
+ cwd,
311
+ model: modelOverride,
312
+ defaultModels,
313
+ run,
314
+ emitFinal,
315
+ emitProgress,
316
+ callerOnStdout,
317
+ readOnly,
318
+ timeoutMs: runTimeoutMs,
319
+ threadId: threadId && String(threadId).trim() ? String(threadId).trim() : void 0,
320
+ inputFiles
321
+ });
322
+ run.settled = true;
323
+ const elapsedMs = Date.now() - startedAt;
324
+ result.usage = { ...result.usage, wallTimeMs: elapsedMs };
325
+ log.info(`Agent finished task=${taskId} durationMs=${elapsedMs}${result.threadId ? ` threadId=${result.threadId}` : ""}`);
326
+ return result;
327
+ } catch (err) {
328
+ run.settled = true;
329
+ const elapsedMs = Date.now() - startedAt;
330
+ log.error(
331
+ `Agent error task=${taskId} durationMs=${elapsedMs} error=${err.message}`
332
+ );
333
+ throw err;
334
+ } finally {
335
+ activeRuns.delete(activeKey);
336
+ releaseAgentSlot?.();
337
+ }
338
+ }
339
+ async function runShellAgent(agent, {
340
+ prompt,
341
+ cwd,
342
+ model,
343
+ defaultModels: defaultModels2,
344
+ run,
345
+ emitFinal,
346
+ emitProgress,
347
+ callerOnStdout,
348
+ readOnly,
349
+ timeoutMs: shellTimeoutMs,
350
+ threadId,
351
+ inputFiles
352
+ }) {
353
+ const effectiveTimeoutMs = typeof shellTimeoutMs === "number" && shellTimeoutMs > 0 ? shellTimeoutMs : agentTimeoutMs;
354
+ const promptBase64 = Buffer.from(prompt, "utf8").toString("base64");
355
+ const promptExpression = '"$PROMPT"';
356
+ const effectiveModel = model || defaultModels2?.[agent.id]?.defaultModel || agent.defaultModel;
357
+ const agentCmd = agent.buildCommand?.({
358
+ prompt,
359
+ promptExpression,
360
+ model: effectiveModel,
361
+ thinking: void 0,
362
+ readOnly,
363
+ threadId,
364
+ inputFiles
365
+ }) || `${agent.command} ${promptExpression}`;
366
+ const command = [
367
+ `PROMPT_B64=${shellQuote(promptBase64)};`,
368
+ 'PROMPT=$(printf %s "$PROMPT_B64" | base64 --decode);',
369
+ agentCmd
370
+ ].join(" ");
371
+ let commandToRun = command;
372
+ if (agent.needsPty) {
373
+ commandToRun = wrapCommandWithPty(commandToRun);
374
+ }
375
+ if (agent.mergeStderr) {
376
+ commandToRun = `${commandToRun} 2>&1`;
377
+ }
378
+ const canStream = typeof emitProgress === "function" || typeof agent.parseStreamingOutput === "function";
379
+ if (canStream) {
380
+ let streamedOutput = "";
381
+ await execLocalStreaming2("bash", ["-lc", commandToRun], {
382
+ timeout: effectiveTimeoutMs,
383
+ maxBuffer: agentMaxBuffer,
384
+ cwd,
385
+ onSpawn: (child) => {
386
+ run.child = child;
387
+ },
388
+ onStdout: (chunk) => {
389
+ streamedOutput += chunk;
390
+ callerOnStdout?.(chunk);
391
+ if (agent.parseStreamingOutput) {
392
+ const partial = agent.parseStreamingOutput(streamedOutput);
393
+ if (partial.commentaryMessages?.length) {
394
+ emitProgress(partial.commentaryMessages);
395
+ }
396
+ if (partial.sawFinal && partial.text) {
397
+ emitFinal(partial.text);
398
+ }
399
+ }
400
+ }
401
+ });
402
+ const parsed = agent.parseOutput?.(streamedOutput) || { text: streamedOutput, sawJson: false };
403
+ return { text: parsed.text || streamedOutput, sawJson: parsed.sawJson, threadId: parsed.threadId, usage: parsed.usage };
404
+ } else {
405
+ const output = await execLocal2("bash", ["-lc", commandToRun], {
406
+ timeout: effectiveTimeoutMs,
407
+ maxBuffer: agentMaxBuffer,
408
+ cwd
409
+ });
410
+ const parsed = agent.parseOutput?.(output) || { text: output, sawJson: false };
411
+ if (parsed.text) {
412
+ emitFinal(parsed.text);
413
+ }
414
+ return { text: parsed.text || output, sawJson: parsed.sawJson, threadId: parsed.threadId, usage: parsed.usage };
415
+ }
416
+ }
417
+ return {
418
+ runAgentForChat,
419
+ stopActiveRun: async (taskId, contextKey) => {
420
+ const run = activeRuns.get(buildActiveRunKey(taskId, contextKey));
421
+ if (!run || run.settled) return { status: "idle" };
422
+ if (run.stopPending) return { status: "stopping" };
423
+ run.stopPending = true;
424
+ terminateChildProcess2?.(run.child, "SIGTERM");
425
+ return { status: "stopping" };
426
+ },
427
+ registerAgent: (agent) => {
428
+ if (agents.has(agent.id)) {
429
+ log.warn(`Agent ${agent.id} already registered, overwriting`);
430
+ }
431
+ agents.set(agent.id, agent);
432
+ },
433
+ listAgents: async () => {
434
+ const results = [];
435
+ for (const agent of agents.values()) {
436
+ const available = agent.checkAvailable ? await agent.checkAvailable() : true;
437
+ results.push({
438
+ id: agent.id,
439
+ label: agent.label,
440
+ defaultModel: agent.defaultModel,
441
+ available,
442
+ needsPty: agent.needsPty,
443
+ mergeStderr: agent.mergeStderr
444
+ });
445
+ }
446
+ return results;
447
+ },
448
+ shutdown: () => {
449
+ for (const [key, run] of activeRuns) {
450
+ if (!run.settled && run.child) {
451
+ log.info(`Shutting down active agent run: ${key}`);
452
+ terminateChildProcess2?.(run.child, "SIGTERM");
453
+ }
454
+ }
455
+ }
456
+ };
457
+ }
458
+
459
+ // src/index.ts
460
+ import { createObservability } from "@agent-detective/observability";
461
+ import { isAbsolute, resolve } from "path";
462
+ import { homedir } from "os";
463
+ function resolveCliArgs(argv) {
464
+ const args = argv.slice(2);
465
+ if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
466
+ return { command: "help" };
467
+ }
468
+ if (args.includes("--version") || args[0] === "version") {
469
+ return { command: "version" };
470
+ }
471
+ const command = args[0] === "doctor" ? "doctor" : args[0] === "init" ? "init" : args[0] === "validate-config" || args.includes("--validate-config") ? "validate-config" : "serve";
472
+ let configRoot;
473
+ for (let i = 0; i < args.length; i++) {
474
+ const a = args[i];
475
+ if (a === "--config-root") {
476
+ configRoot = args[i + 1];
477
+ i++;
478
+ } else if (a?.startsWith("--config-root=")) {
479
+ configRoot = a.slice("--config-root=".length);
480
+ }
481
+ }
482
+ return { command, configRoot };
483
+ }
484
+ function printHelp() {
485
+ console.log(`${APP_NAME} ${APP_VERSION}
486
+
487
+ Usage:
488
+ ${APP_NAME} [--config-root <dir>] Start server (default)
489
+ ${APP_NAME} doctor [--config-root <dir>] [--json] [--verbose]
490
+ Validate config/tools/plugins and exit
491
+ ${APP_NAME} validate-config [--config-root <dir>] [--json] [--verbose]
492
+ Validate config only and exit
493
+ ${APP_NAME} init [--config-root <dir>] [--repo-path <dir>] [--repo-name <name>]
494
+ [--agent <id>] [--force] [--json]
495
+ Scaffold config/local.json for a mock first run
496
+ ${APP_NAME} --version Print version and exit
497
+ ${APP_NAME} --help Print this help and exit
498
+
499
+ Config:
500
+ --config-root <dir> Directory containing config/ (or the config/ dir itself)
501
+ AGENT_DETECTIVE_CONFIG_ROOT can be used instead of --config-root
502
+ `);
503
+ }
504
+ function resolveInstallRoot(cliConfigRoot) {
505
+ const expandHome = (value) => {
506
+ if (value === "~") return homedir();
507
+ if (value.startsWith("~/")) return resolve(homedir(), value.slice(2));
508
+ return value;
509
+ };
510
+ if (cliConfigRoot) return resolve(expandHome(cliConfigRoot));
511
+ if (process.env.AGENT_DETECTIVE_CONFIG_ROOT) return resolve(expandHome(process.env.AGENT_DETECTIVE_CONFIG_ROOT));
512
+ return void 0;
513
+ }
514
+ function resolveConfigDirFromInstallRoot(installRoot) {
515
+ if (!installRoot) return void 0;
516
+ if (installRoot.split(/[\\/]/).pop() === "config") return installRoot;
517
+ return resolve(installRoot, "config");
518
+ }
519
+ async function serve(installRoot) {
520
+ applyLogLevelAliasForObservability();
521
+ const configRoot = resolveConfigDirFromInstallRoot(installRoot);
522
+ const configRootUsed = configRoot ?? resolve(process.cwd(), "config");
523
+ const config = loadConfig({ configRoot });
524
+ const observability = createObservability(config.observability || {});
525
+ const logger = observability.logger;
526
+ const serverLogger = logger.child("server");
527
+ serverLogger.info("Starting agent-detective...", {
528
+ agent: getAgentLabel(config.agent || "opencode"),
529
+ port: config.port || 3001,
530
+ configRoot: configRootUsed
531
+ });
532
+ const defaultModels = {};
533
+ let runnerConfig;
534
+ if (config.agents) {
535
+ for (const [key, value] of Object.entries(config.agents)) {
536
+ if (key === "runner") {
537
+ runnerConfig = value;
538
+ } else {
539
+ defaultModels[key] = value;
540
+ }
541
+ }
542
+ }
543
+ const agentRunner = createAgentRunner({
544
+ execLocal,
545
+ execLocalStreaming,
546
+ terminateChildProcess,
547
+ defaultModels,
548
+ agentTimeoutMs: runnerConfig?.timeoutMs,
549
+ agentMaxBuffer: runnerConfig?.maxBufferBytes,
550
+ postFinalGraceMs: runnerConfig?.postFinalGraceMs,
551
+ forceKillDelayMs: runnerConfig?.forceKillDelayMs,
552
+ logger: logger.child("agent-runner"),
553
+ defaultAgentId: normalizeAgent(config.agent)
554
+ });
555
+ for (const agent of listAgents()) {
556
+ agentRunner.registerAgent(agent);
557
+ }
558
+ const eventBus = createEventBus(logger.child("events"));
559
+ const memoryQueue = createMemoryTaskQueue(logger.child("task-queue"));
560
+ const taskQueue = config.tasks?.maxConcurrent !== void 0 && config.tasks.maxConcurrent > 0 ? createLimitedConcurrencyTaskQueue(memoryQueue, config.tasks.maxConcurrent, logger.child("task-queue")) : memoryQueue;
561
+ const runRecordsPath = config.runRecords?.path !== void 0 && config.runRecords.path.trim().length > 0 ? isAbsolute(config.runRecords.path) ? config.runRecords.path : resolve(configRootUsed, config.runRecords.path) : void 0;
562
+ const runRecords = runRecordsPath ? createRunRecordWriter(runRecordsPath, logger.child("run-records")) : void 0;
563
+ const pluginSystem = createPluginSystem({
564
+ agentRunner,
565
+ events: eventBus,
566
+ logger: logger.child("plugin-system"),
567
+ metrics: observability.metrics,
568
+ health: observability.health,
569
+ failOnContractErrors: config.pluginSystem?.failOnContractErrors ?? false,
570
+ failOnDependencyErrors: config.pluginSystem?.failOnDependencyErrors ?? true,
571
+ failOnPluginLoadErrors: config.pluginSystem?.failOnPluginLoadErrors ?? true,
572
+ pathResolutionRoot: installRoot ?? process.cwd(),
573
+ taskQueue
574
+ });
575
+ const enqueue = pluginSystem.enqueue;
576
+ const orchestrator = createOrchestrator({
577
+ eventBus,
578
+ agentRunner,
579
+ enqueue,
580
+ logger: logger.child("orchestrator"),
581
+ maxWallTimeMs: config.tasks?.maxWallTimeMs,
582
+ runRecords
583
+ });
584
+ orchestrator.start();
585
+ const { app } = await createServer(config, observability, defaultModels, agentRunner, enqueue, {
586
+ getPluginTags: () => pluginSystem.getPluginTags(),
587
+ getPluginStatus: () => ({
588
+ loaded: pluginSystem.getLoadedPlugins().map((p) => `${p.name}@${p.version}`),
589
+ failures: pluginSystem.getPluginLoadFailures()
590
+ })
591
+ });
592
+ await pluginSystem.loadAll(app, config);
593
+ const loaded = pluginSystem.getLoadedPlugins();
594
+ if (loaded.length > 0) {
595
+ serverLogger.info("Loaded plugins", {
596
+ plugins: loaded.map((p) => `${p.name}@${p.version}`)
597
+ });
598
+ } else {
599
+ serverLogger.info("No plugins loaded");
600
+ }
601
+ const PORT = config.port || 3001;
602
+ async function gracefulShutdown(signal) {
603
+ serverLogger.info(`Received ${signal}, shutting down...`);
604
+ const timeout = setTimeout(() => process.exit(1), 1e4);
605
+ timeout.unref();
606
+ await pluginSystem.shutdown();
607
+ await app.close();
608
+ agentRunner.shutdown();
609
+ clearTimeout(timeout);
610
+ process.exit(0);
611
+ }
612
+ process.on("SIGINT", () => {
613
+ void gracefulShutdown("SIGINT");
614
+ });
615
+ process.on("SIGTERM", () => {
616
+ void gracefulShutdown("SIGTERM");
617
+ });
618
+ await app.listen({ port: PORT, host: "0.0.0.0" });
619
+ serverLogger.info("Server started", {
620
+ port: PORT,
621
+ listeningOn: `http://localhost:${PORT}`
622
+ });
623
+ const spec = app.swagger();
624
+ serverLogger.info("Generated OpenAPI spec", {
625
+ paths: Object.keys(spec.paths ?? {}).length,
626
+ tags: (spec.tags ?? []).map((t) => t.name).join(", ")
627
+ });
628
+ }
629
+ async function main() {
630
+ const cli = resolveCliArgs(process.argv);
631
+ const installRoot = resolveInstallRoot(cli.configRoot);
632
+ if (cli.command === "help") {
633
+ printHelp();
634
+ return;
635
+ }
636
+ if (cli.command === "version") {
637
+ console.log(APP_VERSION);
638
+ return;
639
+ }
640
+ if (cli.command === "doctor") {
641
+ const mod = await import("./doctor-3ZMDZLW6.js");
642
+ await mod.runDoctor({ installRoot, argv: process.argv });
643
+ return;
644
+ }
645
+ if (cli.command === "validate-config") {
646
+ const mod = await import("./doctor-3ZMDZLW6.js");
647
+ await mod.runValidateConfig({ installRoot, argv: process.argv });
648
+ return;
649
+ }
650
+ if (cli.command === "init") {
651
+ const mod = await import("./init-IPCDLNHA.js");
652
+ await mod.runInit({ installRoot, argv: process.argv });
653
+ return;
654
+ }
655
+ await serve(installRoot);
656
+ }
657
+ void main().catch((err) => {
658
+ const message = err instanceof Error ? err.message : String(err);
659
+ console.error(message);
660
+ process.exit(1);
661
+ });