diragent 0.1.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,861 @@
1
+ import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
+
3
+ // src/server/index.ts
4
+ import Fastify from "fastify";
5
+ import fastifyWebsocket from "@fastify/websocket";
6
+ import fastifyCors from "@fastify/cors";
7
+ import fastifyStatic from "@fastify/static";
8
+ import pino from "pino";
9
+ import { join as join3, dirname } from "path";
10
+ import { fileURLToPath } from "url";
11
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
12
+
13
+ // src/server/config.ts
14
+ import { readFileSync, existsSync } from "fs";
15
+ import { join } from "path";
16
+ import { z } from "zod";
17
+ var AgentTemplateSchema = z.object({
18
+ driver: z.string(),
19
+ model: z.string().optional(),
20
+ maxTokens: z.number().optional(),
21
+ command: z.array(z.string()).optional(),
22
+ env: z.record(z.string()).optional()
23
+ });
24
+ var ConfigSchema = z.object({
25
+ version: z.string().default("1"),
26
+ server: z.object({
27
+ port: z.number().default(3e3),
28
+ host: z.string().default("0.0.0.0")
29
+ }).default({}),
30
+ auth: z.object({
31
+ enabled: z.boolean().default(true),
32
+ adminToken: z.string().optional(),
33
+ apiKeys: z.array(z.string()).optional()
34
+ }).default({}),
35
+ agents: z.object({
36
+ maxConcurrent: z.number().default(10),
37
+ defaultTimeout: z.number().default(3600),
38
+ templates: z.record(AgentTemplateSchema).default({})
39
+ }).default({}),
40
+ logging: z.object({
41
+ level: z.enum(["debug", "info", "warn", "error"]).default("info"),
42
+ format: z.enum(["pretty", "json"]).default("pretty"),
43
+ file: z.string().optional()
44
+ }).default({}),
45
+ database: z.object({
46
+ path: z.string().default("data/dirigent.db")
47
+ }).default({})
48
+ });
49
+ var cachedConfig = null;
50
+ function loadConfig(configPath) {
51
+ if (cachedConfig) return cachedConfig;
52
+ const path = configPath || findConfigPath();
53
+ if (!path || !existsSync(path)) {
54
+ cachedConfig = ConfigSchema.parse({});
55
+ return cachedConfig;
56
+ }
57
+ const raw = readFileSync(path, "utf-8");
58
+ const parsed = JSON.parse(raw);
59
+ cachedConfig = ConfigSchema.parse(parsed);
60
+ return cachedConfig;
61
+ }
62
+ function findConfigPath() {
63
+ const candidates = [
64
+ join(process.cwd(), ".dirigent", "config.json"),
65
+ join(process.cwd(), "dirigent.json"),
66
+ join(process.env.HOME || "", ".dirigent", "config.json")
67
+ ];
68
+ for (const candidate of candidates) {
69
+ if (existsSync(candidate)) return candidate;
70
+ }
71
+ return null;
72
+ }
73
+ function validateAuth(token, config) {
74
+ if (!config.auth.enabled) return true;
75
+ if (config.auth.adminToken && token === config.auth.adminToken) return true;
76
+ if (config.auth.apiKeys?.includes(token)) return true;
77
+ return false;
78
+ }
79
+
80
+ // src/server/db/index.ts
81
+ import Database from "better-sqlite3";
82
+ var db = null;
83
+ function initDatabase(path) {
84
+ db = new Database(path);
85
+ db.pragma("journal_mode = WAL");
86
+ db.exec(`
87
+ -- Agents table
88
+ CREATE TABLE IF NOT EXISTS agents (
89
+ id TEXT PRIMARY KEY,
90
+ name TEXT NOT NULL,
91
+ template TEXT NOT NULL,
92
+ status TEXT NOT NULL DEFAULT 'created',
93
+ workspace TEXT,
94
+ model TEXT,
95
+ config TEXT,
96
+ pid INTEGER,
97
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
98
+ started_at INTEGER,
99
+ stopped_at INTEGER,
100
+ error TEXT
101
+ );
102
+
103
+ -- Agent logs table
104
+ CREATE TABLE IF NOT EXISTS agent_logs (
105
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
106
+ agent_id TEXT NOT NULL,
107
+ level TEXT NOT NULL,
108
+ message TEXT NOT NULL,
109
+ timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
110
+ metadata TEXT,
111
+ FOREIGN KEY (agent_id) REFERENCES agents(id)
112
+ );
113
+
114
+ -- Tasks table
115
+ CREATE TABLE IF NOT EXISTS tasks (
116
+ id TEXT PRIMARY KEY,
117
+ agent_id TEXT,
118
+ content TEXT NOT NULL,
119
+ status TEXT NOT NULL DEFAULT 'pending',
120
+ result TEXT,
121
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
122
+ started_at INTEGER,
123
+ completed_at INTEGER,
124
+ error TEXT,
125
+ FOREIGN KEY (agent_id) REFERENCES agents(id)
126
+ );
127
+
128
+ -- Audit log table
129
+ CREATE TABLE IF NOT EXISTS audit_log (
130
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
131
+ action TEXT NOT NULL,
132
+ actor TEXT,
133
+ target_type TEXT,
134
+ target_id TEXT,
135
+ details TEXT,
136
+ timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
137
+ );
138
+
139
+ -- Sessions table (for WebSocket connections)
140
+ CREATE TABLE IF NOT EXISTS sessions (
141
+ id TEXT PRIMARY KEY,
142
+ user_id TEXT,
143
+ connected_at INTEGER NOT NULL DEFAULT (unixepoch()),
144
+ last_activity INTEGER NOT NULL DEFAULT (unixepoch()),
145
+ metadata TEXT
146
+ );
147
+
148
+ -- Indexes
149
+ CREATE INDEX IF NOT EXISTS idx_agent_logs_agent_id ON agent_logs(agent_id);
150
+ CREATE INDEX IF NOT EXISTS idx_agent_logs_timestamp ON agent_logs(timestamp);
151
+ CREATE INDEX IF NOT EXISTS idx_tasks_agent_id ON tasks(agent_id);
152
+ CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
153
+ CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON audit_log(timestamp);
154
+ `);
155
+ return db;
156
+ }
157
+ function getDatabase() {
158
+ return db;
159
+ }
160
+ function insertAgent(agent) {
161
+ const stmt = db.prepare(`
162
+ INSERT INTO agents (id, name, template, workspace, model, config)
163
+ VALUES (?, ?, ?, ?, ?, ?)
164
+ `);
165
+ stmt.run(
166
+ agent.id,
167
+ agent.name,
168
+ agent.template,
169
+ agent.workspace || null,
170
+ agent.model || null,
171
+ agent.config ? JSON.stringify(agent.config) : null
172
+ );
173
+ }
174
+ function updateAgent(id, updates) {
175
+ const keys = Object.keys(updates);
176
+ const values = keys.map((k) => typeof updates[k] === "object" ? JSON.stringify(updates[k]) : updates[k]);
177
+ const stmt = db.prepare(`
178
+ UPDATE agents SET ${keys.map((k) => `${k} = ?`).join(", ")}
179
+ WHERE id = ?
180
+ `);
181
+ stmt.run(...values, id);
182
+ }
183
+ function getAgents(includesStopped = false) {
184
+ const stmt = db.prepare(
185
+ includesStopped ? "SELECT * FROM agents ORDER BY created_at DESC" : "SELECT * FROM agents WHERE status != 'stopped' ORDER BY created_at DESC"
186
+ );
187
+ return stmt.all().map((row) => {
188
+ if (row.config) row.config = JSON.parse(row.config);
189
+ return row;
190
+ });
191
+ }
192
+ function insertLog(log) {
193
+ const stmt = db.prepare(`
194
+ INSERT INTO agent_logs (agent_id, level, message, metadata)
195
+ VALUES (?, ?, ?, ?)
196
+ `);
197
+ stmt.run(log.agentId, log.level, log.message, log.metadata ? JSON.stringify(log.metadata) : null);
198
+ }
199
+ function getLogs(agentId, limit = 100) {
200
+ const stmt = db.prepare(`
201
+ SELECT * FROM agent_logs
202
+ WHERE agent_id = ?
203
+ ORDER BY timestamp DESC
204
+ LIMIT ?
205
+ `);
206
+ return stmt.all(agentId, limit).reverse().map((row) => {
207
+ if (row.metadata) row.metadata = JSON.parse(row.metadata);
208
+ return row;
209
+ });
210
+ }
211
+ function audit(action, actor, targetType, targetId, details) {
212
+ const stmt = db.prepare(`
213
+ INSERT INTO audit_log (action, actor, target_type, target_id, details)
214
+ VALUES (?, ?, ?, ?, ?)
215
+ `);
216
+ stmt.run(action, actor, targetType, targetId, details ? JSON.stringify(details) : null);
217
+ }
218
+
219
+ // src/server/agents/manager.ts
220
+ import { EventEmitter } from "events";
221
+ import { nanoid } from "nanoid";
222
+ import { spawn } from "child_process";
223
+ import treeKill from "tree-kill";
224
+ import { join as join2 } from "path";
225
+ import { mkdirSync, existsSync as existsSync2 } from "fs";
226
+ var AgentManager = class extends EventEmitter {
227
+ agents = /* @__PURE__ */ new Map();
228
+ processes = /* @__PURE__ */ new Map();
229
+ dataDir;
230
+ config;
231
+ logger;
232
+ constructor(options) {
233
+ super();
234
+ this.dataDir = options.dataDir;
235
+ this.config = options.config;
236
+ this.logger = options.logger;
237
+ this.loadAgents();
238
+ }
239
+ loadAgents() {
240
+ const dbAgents = getAgents(false);
241
+ for (const dbAgent of dbAgents) {
242
+ if (dbAgent.status !== "stopped") {
243
+ this.agents.set(dbAgent.id, {
244
+ id: dbAgent.id,
245
+ name: dbAgent.name,
246
+ template: dbAgent.template,
247
+ status: "stopped",
248
+ // Reset to stopped since we restarted
249
+ workspace: dbAgent.workspace,
250
+ model: dbAgent.model,
251
+ createdAt: dbAgent.created_at * 1e3
252
+ });
253
+ updateAgent(dbAgent.id, { status: "stopped" });
254
+ }
255
+ }
256
+ }
257
+ async spawn(options) {
258
+ const { template, name, workspace, task, model } = options;
259
+ const templateConfig = this.config.agents.templates[template];
260
+ if (!templateConfig) {
261
+ throw new Error(`Unknown template: ${template}`);
262
+ }
263
+ const runningCount = Array.from(this.agents.values()).filter(
264
+ (a) => a.status === "running" || a.status === "starting"
265
+ ).length;
266
+ if (runningCount >= this.config.agents.maxConcurrent) {
267
+ throw new Error(`Max concurrent agents (${this.config.agents.maxConcurrent}) reached`);
268
+ }
269
+ const id = nanoid(12);
270
+ const agentName = name || `${template}-${id.slice(0, 6)}`;
271
+ const agentWorkspace = workspace || join2(this.dataDir, "workspaces", id);
272
+ if (!existsSync2(agentWorkspace)) {
273
+ mkdirSync(agentWorkspace, { recursive: true });
274
+ }
275
+ const agent = {
276
+ id,
277
+ name: agentName,
278
+ template,
279
+ status: "created",
280
+ workspace: agentWorkspace,
281
+ model: model || templateConfig.model,
282
+ currentTask: task,
283
+ createdAt: Date.now()
284
+ };
285
+ insertAgent({
286
+ id: agent.id,
287
+ name: agent.name,
288
+ template: agent.template,
289
+ workspace: agent.workspace,
290
+ model: agent.model
291
+ });
292
+ audit("agent.created", null, "agent", id, { name: agentName, template });
293
+ this.agents.set(id, agent);
294
+ this.emit("agent:created", agent);
295
+ await this.startAgent(agent, templateConfig, task);
296
+ return agent;
297
+ }
298
+ async startAgent(agent, template, initialTask) {
299
+ agent.status = "starting";
300
+ updateAgent(agent.id, { status: "starting" });
301
+ this.emit("agent:starting", agent);
302
+ try {
303
+ const { command, env } = this.buildCommand(agent, template);
304
+ this.logger.info({ agentId: agent.id, command }, "Starting agent");
305
+ const proc = spawn(command[0], command.slice(1), {
306
+ cwd: agent.workspace,
307
+ env: {
308
+ ...process.env,
309
+ ...env,
310
+ DIRIGENT_AGENT_ID: agent.id,
311
+ DIRIGENT_AGENT_NAME: agent.name
312
+ },
313
+ stdio: ["pipe", "pipe", "pipe"]
314
+ });
315
+ agent.pid = proc.pid;
316
+ agent.process = proc;
317
+ agent.status = "running";
318
+ agent.startedAt = Date.now();
319
+ updateAgent(agent.id, {
320
+ status: "running",
321
+ pid: proc.pid,
322
+ started_at: Math.floor(Date.now() / 1e3)
323
+ });
324
+ this.processes.set(agent.id, proc);
325
+ proc.stdout?.on("data", (data) => {
326
+ const message = data.toString().trim();
327
+ if (message) {
328
+ this.log(agent.id, "info", message);
329
+ }
330
+ });
331
+ proc.stderr?.on("data", (data) => {
332
+ const message = data.toString().trim();
333
+ if (message) {
334
+ this.log(agent.id, "error", message);
335
+ }
336
+ });
337
+ proc.on("exit", (code, signal) => {
338
+ this.logger.info({ agentId: agent.id, code, signal }, "Agent exited");
339
+ agent.status = code === 0 ? "stopped" : "error";
340
+ agent.stoppedAt = Date.now();
341
+ if (code !== 0) {
342
+ agent.error = `Exited with code ${code}`;
343
+ }
344
+ updateAgent(agent.id, {
345
+ status: agent.status,
346
+ stopped_at: Math.floor(Date.now() / 1e3),
347
+ error: agent.error || null
348
+ });
349
+ this.processes.delete(agent.id);
350
+ this.emit("agent:stopped", agent);
351
+ audit("agent.stopped", null, "agent", agent.id, { code, signal });
352
+ });
353
+ proc.on("error", (err) => {
354
+ this.logger.error({ agentId: agent.id, err }, "Agent process error");
355
+ agent.status = "error";
356
+ agent.error = err.message;
357
+ updateAgent(agent.id, { status: "error", error: err.message });
358
+ this.emit("agent:error", agent, err);
359
+ });
360
+ this.emit("agent:running", agent);
361
+ audit("agent.started", null, "agent", agent.id, { pid: proc.pid });
362
+ if (initialTask && proc.stdin) {
363
+ proc.stdin.write(initialTask + "\n");
364
+ }
365
+ } catch (err) {
366
+ agent.status = "error";
367
+ agent.error = err.message;
368
+ updateAgent(agent.id, { status: "error", error: err.message });
369
+ this.emit("agent:error", agent, err);
370
+ throw err;
371
+ }
372
+ }
373
+ buildCommand(agent, template) {
374
+ const env = { ...template.env };
375
+ switch (template.driver) {
376
+ case "claude-code":
377
+ return {
378
+ command: ["claude", "--dangerously-skip-permissions"],
379
+ env: {
380
+ ...env,
381
+ ANTHROPIC_MODEL: agent.model || template.model || "claude-sonnet-4-5"
382
+ }
383
+ };
384
+ case "codex":
385
+ return {
386
+ command: ["codex", "--model", agent.model || template.model || "codex-1"],
387
+ env
388
+ };
389
+ case "clawdbot":
390
+ return {
391
+ command: ["clawdbot", "agent", "--headless"],
392
+ env: {
393
+ ...env,
394
+ CLAWDBOT_MODEL: agent.model || template.model || "claude-sonnet-4-5"
395
+ }
396
+ };
397
+ case "subprocess":
398
+ if (!template.command || template.command.length === 0) {
399
+ throw new Error("subprocess driver requires command");
400
+ }
401
+ return { command: template.command, env };
402
+ default:
403
+ throw new Error(`Unknown driver: ${template.driver}`);
404
+ }
405
+ }
406
+ async stop(id, force = false) {
407
+ const agent = this.agents.get(id);
408
+ if (!agent) {
409
+ throw new Error(`Agent not found: ${id}`);
410
+ }
411
+ if (agent.status !== "running" && agent.status !== "starting") {
412
+ return;
413
+ }
414
+ agent.status = "stopping";
415
+ this.emit("agent:stopping", agent);
416
+ const proc = this.processes.get(id);
417
+ if (proc && proc.pid) {
418
+ await new Promise((resolve, reject) => {
419
+ treeKill(proc.pid, force ? "SIGKILL" : "SIGTERM", (err) => {
420
+ if (err) reject(err);
421
+ else resolve();
422
+ });
423
+ });
424
+ }
425
+ audit("agent.stop_requested", null, "agent", id, { force });
426
+ }
427
+ async stopAll() {
428
+ const promises = Array.from(this.agents.values()).filter((a) => a.status === "running" || a.status === "starting").map((a) => this.stop(a.id, true));
429
+ await Promise.allSettled(promises);
430
+ }
431
+ async send(id, message) {
432
+ const agent = this.agents.get(id);
433
+ if (!agent) {
434
+ throw new Error(`Agent not found: ${id}`);
435
+ }
436
+ if (agent.status !== "running") {
437
+ throw new Error(`Agent is not running: ${agent.status}`);
438
+ }
439
+ const proc = this.processes.get(id);
440
+ if (!proc || !proc.stdin) {
441
+ throw new Error("Agent process not available");
442
+ }
443
+ proc.stdin.write(message + "\n");
444
+ this.log(id, "info", `[USER] ${message}`);
445
+ audit("agent.message_sent", null, "agent", id, { length: message.length });
446
+ }
447
+ get(id) {
448
+ return this.agents.get(id);
449
+ }
450
+ list(includeStopped = false) {
451
+ const agents = Array.from(this.agents.values());
452
+ if (includeStopped) return agents;
453
+ return agents.filter((a) => a.status !== "stopped");
454
+ }
455
+ log(agentId, level, message) {
456
+ insertLog({ agentId, level, message });
457
+ this.emit("agent:log", { agentId, level, message, timestamp: Date.now() });
458
+ }
459
+ getLogs(agentId, limit = 100) {
460
+ return getLogs(agentId, limit);
461
+ }
462
+ getStats() {
463
+ const agents = Array.from(this.agents.values());
464
+ return {
465
+ total: agents.length,
466
+ running: agents.filter((a) => a.status === "running").length,
467
+ idle: agents.filter((a) => a.status === "idle").length,
468
+ stopped: agents.filter((a) => a.status === "stopped").length,
469
+ error: agents.filter((a) => a.status === "error").length
470
+ };
471
+ }
472
+ };
473
+
474
+ // src/server/api/routes.ts
475
+ function registerApiRoutes(server2, options) {
476
+ const { config, agentManager: agentManager2, startTime: startTime2 } = options;
477
+ server2.addHook("onRequest", async (request, reply) => {
478
+ if (request.url === "/health") return;
479
+ if (!request.url.startsWith("/api")) return;
480
+ if (config.auth.enabled) {
481
+ const authHeader = request.headers.authorization;
482
+ const token = authHeader?.replace("Bearer ", "");
483
+ if (!token || !validateAuth(token, config)) {
484
+ reply.code(401).send({ error: "Unauthorized" });
485
+ return;
486
+ }
487
+ }
488
+ });
489
+ server2.get("/api/status", async () => {
490
+ const stats = agentManager2.getStats();
491
+ const agents = agentManager2.list(false);
492
+ return {
493
+ status: "ok",
494
+ uptime: Math.floor((Date.now() - startTime2) / 1e3),
495
+ agents: {
496
+ ...stats,
497
+ list: agents.map((a) => ({
498
+ id: a.id,
499
+ name: a.name,
500
+ template: a.template,
501
+ status: a.status,
502
+ workspace: a.workspace,
503
+ currentTask: a.currentTask
504
+ }))
505
+ }
506
+ };
507
+ });
508
+ server2.get("/api/agents", async (request) => {
509
+ const includeStopped = request.query.all === "true";
510
+ const agents = agentManager2.list(includeStopped);
511
+ return {
512
+ agents: agents.map((a) => ({
513
+ id: a.id,
514
+ name: a.name,
515
+ template: a.template,
516
+ status: a.status,
517
+ workspace: a.workspace,
518
+ model: a.model,
519
+ pid: a.pid,
520
+ currentTask: a.currentTask,
521
+ createdAt: a.createdAt,
522
+ startedAt: a.startedAt,
523
+ stoppedAt: a.stoppedAt,
524
+ error: a.error
525
+ }))
526
+ };
527
+ });
528
+ server2.post("/api/agents", async (request, reply) => {
529
+ const { template, name, workspace, task, model } = request.body;
530
+ if (!template) {
531
+ reply.code(400).send({ error: "template is required" });
532
+ return;
533
+ }
534
+ try {
535
+ const agent = await agentManager2.spawn({ template, name, workspace, task, model });
536
+ return {
537
+ agent: {
538
+ id: agent.id,
539
+ name: agent.name,
540
+ template: agent.template,
541
+ status: agent.status,
542
+ workspace: agent.workspace,
543
+ model: agent.model
544
+ }
545
+ };
546
+ } catch (err) {
547
+ reply.code(400).send({ error: err.message });
548
+ }
549
+ });
550
+ server2.get("/api/agents/:id", async (request, reply) => {
551
+ const agent = agentManager2.get(request.params.id);
552
+ if (!agent) {
553
+ reply.code(404).send({ error: "Agent not found" });
554
+ return;
555
+ }
556
+ return {
557
+ agent: {
558
+ id: agent.id,
559
+ name: agent.name,
560
+ template: agent.template,
561
+ status: agent.status,
562
+ workspace: agent.workspace,
563
+ model: agent.model,
564
+ pid: agent.pid,
565
+ currentTask: agent.currentTask,
566
+ createdAt: agent.createdAt,
567
+ startedAt: agent.startedAt,
568
+ stoppedAt: agent.stoppedAt,
569
+ error: agent.error
570
+ }
571
+ };
572
+ });
573
+ server2.delete(
574
+ "/api/agents/:id",
575
+ async (request, reply) => {
576
+ const agent = agentManager2.get(request.params.id);
577
+ if (!agent) {
578
+ reply.code(404).send({ error: "Agent not found" });
579
+ return;
580
+ }
581
+ const force = request.query.force === "true";
582
+ try {
583
+ await agentManager2.stop(request.params.id, force);
584
+ return { ok: true };
585
+ } catch (err) {
586
+ reply.code(500).send({ error: err.message });
587
+ }
588
+ }
589
+ );
590
+ server2.post(
591
+ "/api/agents/:id/send",
592
+ async (request, reply) => {
593
+ const agent = agentManager2.get(request.params.id);
594
+ if (!agent) {
595
+ reply.code(404).send({ error: "Agent not found" });
596
+ return;
597
+ }
598
+ const { message } = request.body;
599
+ if (!message) {
600
+ reply.code(400).send({ error: "message is required" });
601
+ return;
602
+ }
603
+ try {
604
+ await agentManager2.send(request.params.id, message);
605
+ return { ok: true };
606
+ } catch (err) {
607
+ reply.code(400).send({ error: err.message });
608
+ }
609
+ }
610
+ );
611
+ server2.get(
612
+ "/api/agents/:id/logs",
613
+ async (request, reply) => {
614
+ const agent = agentManager2.get(request.params.id);
615
+ if (!agent) {
616
+ reply.code(404).send({ error: "Agent not found" });
617
+ return;
618
+ }
619
+ const limit = parseInt(request.query.lines || "100");
620
+ const logs = agentManager2.getLogs(request.params.id, limit);
621
+ return { logs };
622
+ }
623
+ );
624
+ server2.get("/api/templates", async () => {
625
+ return {
626
+ templates: Object.entries(config.agents.templates).map(([name, template]) => ({
627
+ name,
628
+ driver: template.driver,
629
+ model: template.model
630
+ }))
631
+ };
632
+ });
633
+ server2.get("/api/audit", async (request) => {
634
+ return { logs: [] };
635
+ });
636
+ }
637
+
638
+ // src/server/ws/index.ts
639
+ function registerWebSocket(server2, options) {
640
+ const { config, agentManager: agentManager2 } = options;
641
+ const clients = /* @__PURE__ */ new Map();
642
+ server2.register(async (fastify) => {
643
+ fastify.get("/ws", { websocket: true }, (connection, req) => {
644
+ const ws = connection;
645
+ const clientId = Math.random().toString(36).slice(2);
646
+ const client = {
647
+ ws,
648
+ subscriptions: /* @__PURE__ */ new Set(),
649
+ authenticated: !config.auth.enabled
650
+ };
651
+ clients.set(clientId, client);
652
+ ws.on("message", (data) => {
653
+ try {
654
+ const msg = JSON.parse(data.toString());
655
+ handleMessage(clientId, client, msg);
656
+ } catch (err) {
657
+ send(ws, { type: "error", error: "Invalid JSON" });
658
+ }
659
+ });
660
+ ws.on("close", () => {
661
+ clients.delete(clientId);
662
+ });
663
+ ws.on("error", (err) => {
664
+ console.error("WebSocket error:", err);
665
+ clients.delete(clientId);
666
+ });
667
+ send(ws, {
668
+ type: "welcome",
669
+ clientId,
670
+ authenticated: client.authenticated
671
+ });
672
+ });
673
+ });
674
+ function handleMessage(clientId, client, msg) {
675
+ if (msg.type === "auth") {
676
+ if (validateAuth(msg.token, config)) {
677
+ client.authenticated = true;
678
+ send(client.ws, { type: "auth", success: true });
679
+ } else {
680
+ send(client.ws, { type: "auth", success: false, error: "Invalid token" });
681
+ }
682
+ return;
683
+ }
684
+ if (!client.authenticated) {
685
+ send(client.ws, { type: "error", error: "Not authenticated" });
686
+ return;
687
+ }
688
+ switch (msg.type) {
689
+ case "subscribe:logs":
690
+ if (msg.agentId) {
691
+ client.subscriptions.add(`logs:${msg.agentId}`);
692
+ send(client.ws, { type: "subscribed", channel: `logs:${msg.agentId}` });
693
+ }
694
+ break;
695
+ case "unsubscribe:logs":
696
+ if (msg.agentId) {
697
+ client.subscriptions.delete(`logs:${msg.agentId}`);
698
+ send(client.ws, { type: "unsubscribed", channel: `logs:${msg.agentId}` });
699
+ }
700
+ break;
701
+ case "subscribe:agents":
702
+ client.subscriptions.add("agents");
703
+ send(client.ws, { type: "subscribed", channel: "agents" });
704
+ break;
705
+ case "unsubscribe:agents":
706
+ client.subscriptions.delete("agents");
707
+ send(client.ws, { type: "unsubscribed", channel: "agents" });
708
+ break;
709
+ case "ping":
710
+ send(client.ws, { type: "pong", ts: Date.now() });
711
+ break;
712
+ default:
713
+ send(client.ws, { type: "error", error: `Unknown message type: ${msg.type}` });
714
+ }
715
+ }
716
+ agentManager2.on("agent:created", (agent) => {
717
+ broadcast("agents", { type: "agent:created", agent: sanitizeAgent(agent) });
718
+ });
719
+ agentManager2.on("agent:running", (agent) => {
720
+ broadcast("agents", { type: "agent:running", agent: sanitizeAgent(agent) });
721
+ });
722
+ agentManager2.on("agent:stopped", (agent) => {
723
+ broadcast("agents", { type: "agent:stopped", agent: sanitizeAgent(agent) });
724
+ });
725
+ agentManager2.on("agent:error", (agent, error) => {
726
+ broadcast("agents", { type: "agent:error", agent: sanitizeAgent(agent), error: error.message });
727
+ });
728
+ agentManager2.on("agent:log", (data) => {
729
+ broadcast(`logs:${data.agentId}`, { type: "agent:log", ...data });
730
+ });
731
+ function broadcast(channel, message) {
732
+ for (const client of clients.values()) {
733
+ if (client.authenticated && client.subscriptions.has(channel)) {
734
+ send(client.ws, message);
735
+ }
736
+ }
737
+ }
738
+ function send(ws, message) {
739
+ if (ws.readyState === ws.OPEN) {
740
+ ws.send(JSON.stringify(message));
741
+ }
742
+ }
743
+ function sanitizeAgent(agent) {
744
+ return {
745
+ id: agent.id,
746
+ name: agent.name,
747
+ template: agent.template,
748
+ status: agent.status,
749
+ workspace: agent.workspace,
750
+ model: agent.model,
751
+ pid: agent.pid,
752
+ currentTask: agent.currentTask,
753
+ createdAt: agent.createdAt,
754
+ startedAt: agent.startedAt,
755
+ stoppedAt: agent.stoppedAt,
756
+ error: agent.error
757
+ };
758
+ }
759
+ }
760
+
761
+ // src/server/index.ts
762
+ var __filename = fileURLToPath(import.meta.url);
763
+ var __dirname = dirname(__filename);
764
+ var server = null;
765
+ var agentManager = null;
766
+ var startTime = Date.now();
767
+ async function startServer(options) {
768
+ const config = loadConfig(options.configPath);
769
+ const logPath = join3(options.dataDir, "logs", "dirigent.log");
770
+ mkdirSync2(dirname(logPath), { recursive: true });
771
+ const logger = pino({
772
+ level: config.logging?.level || "info",
773
+ transport: {
774
+ targets: [
775
+ {
776
+ target: "pino-pretty",
777
+ options: { colorize: true },
778
+ level: "info"
779
+ },
780
+ {
781
+ target: "pino/file",
782
+ options: { destination: logPath },
783
+ level: "debug"
784
+ }
785
+ ]
786
+ }
787
+ });
788
+ const dbPath = join3(options.dataDir, "data", "dirigent.db");
789
+ mkdirSync2(dirname(dbPath), { recursive: true });
790
+ initDatabase(dbPath);
791
+ server = Fastify({ logger });
792
+ await server.register(fastifyCors, {
793
+ origin: true,
794
+ credentials: true
795
+ });
796
+ await server.register(fastifyWebsocket);
797
+ const dashboardPath = join3(__dirname, "..", "dashboard", "dist");
798
+ if (existsSync3(dashboardPath)) {
799
+ await server.register(fastifyStatic, {
800
+ root: dashboardPath,
801
+ prefix: "/"
802
+ });
803
+ }
804
+ agentManager = new AgentManager({
805
+ dataDir: options.dataDir,
806
+ config,
807
+ logger
808
+ });
809
+ registerApiRoutes(server, {
810
+ config,
811
+ agentManager,
812
+ startTime
813
+ });
814
+ registerWebSocket(server, {
815
+ config,
816
+ agentManager
817
+ });
818
+ server.get("/health", async () => ({ status: "ok", uptime: Math.floor((Date.now() - startTime) / 1e3) }));
819
+ const shutdown = async (signal) => {
820
+ logger.info(`Received ${signal}, shutting down...`);
821
+ if (agentManager) {
822
+ await agentManager.stopAll();
823
+ }
824
+ if (server) {
825
+ await server.close();
826
+ }
827
+ const db2 = getDatabase();
828
+ if (db2) db2.close();
829
+ process.exit(0);
830
+ };
831
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
832
+ process.on("SIGINT", () => shutdown("SIGINT"));
833
+ try {
834
+ await server.listen({ port: options.port, host: config.server?.host || "0.0.0.0" });
835
+ console.log(`
836
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
837
+ \u2551 \u2551
838
+ \u2551 \u{1F3AD} DIRIGENT SERVER RUNNING \u2551
839
+ \u2551 \u2551
840
+ \u2551 Dashboard: http://localhost:${options.port.toString().padEnd(25)}\u2551
841
+ \u2551 API: http://localhost:${options.port}/api${" ".repeat(21)}\u2551
842
+ \u2551 WebSocket: ws://localhost:${options.port}/ws${" ".repeat(22)}\u2551
843
+ \u2551 \u2551
844
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
845
+ `);
846
+ } catch (err) {
847
+ logger.error(err);
848
+ process.exit(1);
849
+ }
850
+ }
851
+ if (process.env.DIRIGENT_CONFIG) {
852
+ startServer({
853
+ port: parseInt(process.env.DIRIGENT_PORT || "3000"),
854
+ configPath: process.env.DIRIGENT_CONFIG,
855
+ dataDir: process.env.DIRIGENT_DATA_DIR || ".dirigent"
856
+ });
857
+ }
858
+ export {
859
+ startServer
860
+ };
861
+ //# sourceMappingURL=index.js.map