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.
- package/README.md +57 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1621 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/index.d.ts +63 -0
- package/dist/client/index.js +145 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.d.ts +256 -0
- package/dist/index.js +879 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.js +861 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +90 -0
- package/scripts/install.sh +67 -0
|
@@ -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
|