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,1621 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/server/config.ts
|
|
14
|
+
var config_exports = {};
|
|
15
|
+
__export(config_exports, {
|
|
16
|
+
loadConfig: () => loadConfig,
|
|
17
|
+
validateAuth: () => validateAuth
|
|
18
|
+
});
|
|
19
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
20
|
+
import { join as join2 } from "path";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
function loadConfig(configPath) {
|
|
23
|
+
if (cachedConfig) return cachedConfig;
|
|
24
|
+
const path = configPath || findConfigPath();
|
|
25
|
+
if (!path || !existsSync2(path)) {
|
|
26
|
+
cachedConfig = ConfigSchema.parse({});
|
|
27
|
+
return cachedConfig;
|
|
28
|
+
}
|
|
29
|
+
const raw = readFileSync(path, "utf-8");
|
|
30
|
+
const parsed = JSON.parse(raw);
|
|
31
|
+
cachedConfig = ConfigSchema.parse(parsed);
|
|
32
|
+
return cachedConfig;
|
|
33
|
+
}
|
|
34
|
+
function findConfigPath() {
|
|
35
|
+
const candidates = [
|
|
36
|
+
join2(process.cwd(), ".dirigent", "config.json"),
|
|
37
|
+
join2(process.cwd(), "dirigent.json"),
|
|
38
|
+
join2(process.env.HOME || "", ".dirigent", "config.json")
|
|
39
|
+
];
|
|
40
|
+
for (const candidate of candidates) {
|
|
41
|
+
if (existsSync2(candidate)) return candidate;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function validateAuth(token, config) {
|
|
46
|
+
if (!config.auth.enabled) return true;
|
|
47
|
+
if (config.auth.adminToken && token === config.auth.adminToken) return true;
|
|
48
|
+
if (config.auth.apiKeys?.includes(token)) return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
var AgentTemplateSchema, ConfigSchema, cachedConfig;
|
|
52
|
+
var init_config = __esm({
|
|
53
|
+
"src/server/config.ts"() {
|
|
54
|
+
"use strict";
|
|
55
|
+
AgentTemplateSchema = z.object({
|
|
56
|
+
driver: z.string(),
|
|
57
|
+
model: z.string().optional(),
|
|
58
|
+
maxTokens: z.number().optional(),
|
|
59
|
+
command: z.array(z.string()).optional(),
|
|
60
|
+
env: z.record(z.string()).optional()
|
|
61
|
+
});
|
|
62
|
+
ConfigSchema = z.object({
|
|
63
|
+
version: z.string().default("1"),
|
|
64
|
+
server: z.object({
|
|
65
|
+
port: z.number().default(3e3),
|
|
66
|
+
host: z.string().default("0.0.0.0")
|
|
67
|
+
}).default({}),
|
|
68
|
+
auth: z.object({
|
|
69
|
+
enabled: z.boolean().default(true),
|
|
70
|
+
adminToken: z.string().optional(),
|
|
71
|
+
apiKeys: z.array(z.string()).optional()
|
|
72
|
+
}).default({}),
|
|
73
|
+
agents: z.object({
|
|
74
|
+
maxConcurrent: z.number().default(10),
|
|
75
|
+
defaultTimeout: z.number().default(3600),
|
|
76
|
+
templates: z.record(AgentTemplateSchema).default({})
|
|
77
|
+
}).default({}),
|
|
78
|
+
logging: z.object({
|
|
79
|
+
level: z.enum(["debug", "info", "warn", "error"]).default("info"),
|
|
80
|
+
format: z.enum(["pretty", "json"]).default("pretty"),
|
|
81
|
+
file: z.string().optional()
|
|
82
|
+
}).default({}),
|
|
83
|
+
database: z.object({
|
|
84
|
+
path: z.string().default("data/dirigent.db")
|
|
85
|
+
}).default({})
|
|
86
|
+
});
|
|
87
|
+
cachedConfig = null;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// src/server/db/index.ts
|
|
92
|
+
import Database from "better-sqlite3";
|
|
93
|
+
function initDatabase(path) {
|
|
94
|
+
db = new Database(path);
|
|
95
|
+
db.pragma("journal_mode = WAL");
|
|
96
|
+
db.exec(`
|
|
97
|
+
-- Agents table
|
|
98
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
name TEXT NOT NULL,
|
|
101
|
+
template TEXT NOT NULL,
|
|
102
|
+
status TEXT NOT NULL DEFAULT 'created',
|
|
103
|
+
workspace TEXT,
|
|
104
|
+
model TEXT,
|
|
105
|
+
config TEXT,
|
|
106
|
+
pid INTEGER,
|
|
107
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
108
|
+
started_at INTEGER,
|
|
109
|
+
stopped_at INTEGER,
|
|
110
|
+
error TEXT
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
-- Agent logs table
|
|
114
|
+
CREATE TABLE IF NOT EXISTS agent_logs (
|
|
115
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
116
|
+
agent_id TEXT NOT NULL,
|
|
117
|
+
level TEXT NOT NULL,
|
|
118
|
+
message TEXT NOT NULL,
|
|
119
|
+
timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
|
120
|
+
metadata TEXT,
|
|
121
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
-- Tasks table
|
|
125
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
126
|
+
id TEXT PRIMARY KEY,
|
|
127
|
+
agent_id TEXT,
|
|
128
|
+
content TEXT NOT NULL,
|
|
129
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
130
|
+
result TEXT,
|
|
131
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
132
|
+
started_at INTEGER,
|
|
133
|
+
completed_at INTEGER,
|
|
134
|
+
error TEXT,
|
|
135
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
-- Audit log table
|
|
139
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
140
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
141
|
+
action TEXT NOT NULL,
|
|
142
|
+
actor TEXT,
|
|
143
|
+
target_type TEXT,
|
|
144
|
+
target_id TEXT,
|
|
145
|
+
details TEXT,
|
|
146
|
+
timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
-- Sessions table (for WebSocket connections)
|
|
150
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
151
|
+
id TEXT PRIMARY KEY,
|
|
152
|
+
user_id TEXT,
|
|
153
|
+
connected_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
154
|
+
last_activity INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
155
|
+
metadata TEXT
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
-- Indexes
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_agent_logs_agent_id ON agent_logs(agent_id);
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_agent_logs_timestamp ON agent_logs(timestamp);
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_agent_id ON tasks(agent_id);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_audit_log_timestamp ON audit_log(timestamp);
|
|
164
|
+
`);
|
|
165
|
+
return db;
|
|
166
|
+
}
|
|
167
|
+
function getDatabase() {
|
|
168
|
+
return db;
|
|
169
|
+
}
|
|
170
|
+
function insertAgent(agent2) {
|
|
171
|
+
const stmt = db.prepare(`
|
|
172
|
+
INSERT INTO agents (id, name, template, workspace, model, config)
|
|
173
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
174
|
+
`);
|
|
175
|
+
stmt.run(
|
|
176
|
+
agent2.id,
|
|
177
|
+
agent2.name,
|
|
178
|
+
agent2.template,
|
|
179
|
+
agent2.workspace || null,
|
|
180
|
+
agent2.model || null,
|
|
181
|
+
agent2.config ? JSON.stringify(agent2.config) : null
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
function updateAgent(id, updates) {
|
|
185
|
+
const keys = Object.keys(updates);
|
|
186
|
+
const values = keys.map((k) => typeof updates[k] === "object" ? JSON.stringify(updates[k]) : updates[k]);
|
|
187
|
+
const stmt = db.prepare(`
|
|
188
|
+
UPDATE agents SET ${keys.map((k) => `${k} = ?`).join(", ")}
|
|
189
|
+
WHERE id = ?
|
|
190
|
+
`);
|
|
191
|
+
stmt.run(...values, id);
|
|
192
|
+
}
|
|
193
|
+
function getAgents(includesStopped = false) {
|
|
194
|
+
const stmt = db.prepare(
|
|
195
|
+
includesStopped ? "SELECT * FROM agents ORDER BY created_at DESC" : "SELECT * FROM agents WHERE status != 'stopped' ORDER BY created_at DESC"
|
|
196
|
+
);
|
|
197
|
+
return stmt.all().map((row) => {
|
|
198
|
+
if (row.config) row.config = JSON.parse(row.config);
|
|
199
|
+
return row;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function insertLog(log) {
|
|
203
|
+
const stmt = db.prepare(`
|
|
204
|
+
INSERT INTO agent_logs (agent_id, level, message, metadata)
|
|
205
|
+
VALUES (?, ?, ?, ?)
|
|
206
|
+
`);
|
|
207
|
+
stmt.run(log.agentId, log.level, log.message, log.metadata ? JSON.stringify(log.metadata) : null);
|
|
208
|
+
}
|
|
209
|
+
function getLogs(agentId, limit = 100) {
|
|
210
|
+
const stmt = db.prepare(`
|
|
211
|
+
SELECT * FROM agent_logs
|
|
212
|
+
WHERE agent_id = ?
|
|
213
|
+
ORDER BY timestamp DESC
|
|
214
|
+
LIMIT ?
|
|
215
|
+
`);
|
|
216
|
+
return stmt.all(agentId, limit).reverse().map((row) => {
|
|
217
|
+
if (row.metadata) row.metadata = JSON.parse(row.metadata);
|
|
218
|
+
return row;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function audit(action, actor, targetType, targetId, details) {
|
|
222
|
+
const stmt = db.prepare(`
|
|
223
|
+
INSERT INTO audit_log (action, actor, target_type, target_id, details)
|
|
224
|
+
VALUES (?, ?, ?, ?, ?)
|
|
225
|
+
`);
|
|
226
|
+
stmt.run(action, actor, targetType, targetId, details ? JSON.stringify(details) : null);
|
|
227
|
+
}
|
|
228
|
+
var db;
|
|
229
|
+
var init_db = __esm({
|
|
230
|
+
"src/server/db/index.ts"() {
|
|
231
|
+
"use strict";
|
|
232
|
+
db = null;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// src/server/agents/manager.ts
|
|
237
|
+
import { EventEmitter } from "events";
|
|
238
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
239
|
+
import { spawn } from "child_process";
|
|
240
|
+
import treeKill from "tree-kill";
|
|
241
|
+
import { join as join3 } from "path";
|
|
242
|
+
import { mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
243
|
+
var AgentManager;
|
|
244
|
+
var init_manager = __esm({
|
|
245
|
+
"src/server/agents/manager.ts"() {
|
|
246
|
+
"use strict";
|
|
247
|
+
init_db();
|
|
248
|
+
AgentManager = class extends EventEmitter {
|
|
249
|
+
agents = /* @__PURE__ */ new Map();
|
|
250
|
+
processes = /* @__PURE__ */ new Map();
|
|
251
|
+
dataDir;
|
|
252
|
+
config;
|
|
253
|
+
logger;
|
|
254
|
+
constructor(options) {
|
|
255
|
+
super();
|
|
256
|
+
this.dataDir = options.dataDir;
|
|
257
|
+
this.config = options.config;
|
|
258
|
+
this.logger = options.logger;
|
|
259
|
+
this.loadAgents();
|
|
260
|
+
}
|
|
261
|
+
loadAgents() {
|
|
262
|
+
const dbAgents = getAgents(false);
|
|
263
|
+
for (const dbAgent of dbAgents) {
|
|
264
|
+
if (dbAgent.status !== "stopped") {
|
|
265
|
+
this.agents.set(dbAgent.id, {
|
|
266
|
+
id: dbAgent.id,
|
|
267
|
+
name: dbAgent.name,
|
|
268
|
+
template: dbAgent.template,
|
|
269
|
+
status: "stopped",
|
|
270
|
+
// Reset to stopped since we restarted
|
|
271
|
+
workspace: dbAgent.workspace,
|
|
272
|
+
model: dbAgent.model,
|
|
273
|
+
createdAt: dbAgent.created_at * 1e3
|
|
274
|
+
});
|
|
275
|
+
updateAgent(dbAgent.id, { status: "stopped" });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async spawn(options) {
|
|
280
|
+
const { template, name, workspace, task, model } = options;
|
|
281
|
+
const templateConfig = this.config.agents.templates[template];
|
|
282
|
+
if (!templateConfig) {
|
|
283
|
+
throw new Error(`Unknown template: ${template}`);
|
|
284
|
+
}
|
|
285
|
+
const runningCount = Array.from(this.agents.values()).filter(
|
|
286
|
+
(a) => a.status === "running" || a.status === "starting"
|
|
287
|
+
).length;
|
|
288
|
+
if (runningCount >= this.config.agents.maxConcurrent) {
|
|
289
|
+
throw new Error(`Max concurrent agents (${this.config.agents.maxConcurrent}) reached`);
|
|
290
|
+
}
|
|
291
|
+
const id = nanoid2(12);
|
|
292
|
+
const agentName = name || `${template}-${id.slice(0, 6)}`;
|
|
293
|
+
const agentWorkspace = workspace || join3(this.dataDir, "workspaces", id);
|
|
294
|
+
if (!existsSync3(agentWorkspace)) {
|
|
295
|
+
mkdirSync2(agentWorkspace, { recursive: true });
|
|
296
|
+
}
|
|
297
|
+
const agent2 = {
|
|
298
|
+
id,
|
|
299
|
+
name: agentName,
|
|
300
|
+
template,
|
|
301
|
+
status: "created",
|
|
302
|
+
workspace: agentWorkspace,
|
|
303
|
+
model: model || templateConfig.model,
|
|
304
|
+
currentTask: task,
|
|
305
|
+
createdAt: Date.now()
|
|
306
|
+
};
|
|
307
|
+
insertAgent({
|
|
308
|
+
id: agent2.id,
|
|
309
|
+
name: agent2.name,
|
|
310
|
+
template: agent2.template,
|
|
311
|
+
workspace: agent2.workspace,
|
|
312
|
+
model: agent2.model
|
|
313
|
+
});
|
|
314
|
+
audit("agent.created", null, "agent", id, { name: agentName, template });
|
|
315
|
+
this.agents.set(id, agent2);
|
|
316
|
+
this.emit("agent:created", agent2);
|
|
317
|
+
await this.startAgent(agent2, templateConfig, task);
|
|
318
|
+
return agent2;
|
|
319
|
+
}
|
|
320
|
+
async startAgent(agent2, template, initialTask) {
|
|
321
|
+
agent2.status = "starting";
|
|
322
|
+
updateAgent(agent2.id, { status: "starting" });
|
|
323
|
+
this.emit("agent:starting", agent2);
|
|
324
|
+
try {
|
|
325
|
+
const { command, env } = this.buildCommand(agent2, template);
|
|
326
|
+
this.logger.info({ agentId: agent2.id, command }, "Starting agent");
|
|
327
|
+
const proc = spawn(command[0], command.slice(1), {
|
|
328
|
+
cwd: agent2.workspace,
|
|
329
|
+
env: {
|
|
330
|
+
...process.env,
|
|
331
|
+
...env,
|
|
332
|
+
DIRIGENT_AGENT_ID: agent2.id,
|
|
333
|
+
DIRIGENT_AGENT_NAME: agent2.name
|
|
334
|
+
},
|
|
335
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
336
|
+
});
|
|
337
|
+
agent2.pid = proc.pid;
|
|
338
|
+
agent2.process = proc;
|
|
339
|
+
agent2.status = "running";
|
|
340
|
+
agent2.startedAt = Date.now();
|
|
341
|
+
updateAgent(agent2.id, {
|
|
342
|
+
status: "running",
|
|
343
|
+
pid: proc.pid,
|
|
344
|
+
started_at: Math.floor(Date.now() / 1e3)
|
|
345
|
+
});
|
|
346
|
+
this.processes.set(agent2.id, proc);
|
|
347
|
+
proc.stdout?.on("data", (data) => {
|
|
348
|
+
const message = data.toString().trim();
|
|
349
|
+
if (message) {
|
|
350
|
+
this.log(agent2.id, "info", message);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
proc.stderr?.on("data", (data) => {
|
|
354
|
+
const message = data.toString().trim();
|
|
355
|
+
if (message) {
|
|
356
|
+
this.log(agent2.id, "error", message);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
proc.on("exit", (code, signal) => {
|
|
360
|
+
this.logger.info({ agentId: agent2.id, code, signal }, "Agent exited");
|
|
361
|
+
agent2.status = code === 0 ? "stopped" : "error";
|
|
362
|
+
agent2.stoppedAt = Date.now();
|
|
363
|
+
if (code !== 0) {
|
|
364
|
+
agent2.error = `Exited with code ${code}`;
|
|
365
|
+
}
|
|
366
|
+
updateAgent(agent2.id, {
|
|
367
|
+
status: agent2.status,
|
|
368
|
+
stopped_at: Math.floor(Date.now() / 1e3),
|
|
369
|
+
error: agent2.error || null
|
|
370
|
+
});
|
|
371
|
+
this.processes.delete(agent2.id);
|
|
372
|
+
this.emit("agent:stopped", agent2);
|
|
373
|
+
audit("agent.stopped", null, "agent", agent2.id, { code, signal });
|
|
374
|
+
});
|
|
375
|
+
proc.on("error", (err) => {
|
|
376
|
+
this.logger.error({ agentId: agent2.id, err }, "Agent process error");
|
|
377
|
+
agent2.status = "error";
|
|
378
|
+
agent2.error = err.message;
|
|
379
|
+
updateAgent(agent2.id, { status: "error", error: err.message });
|
|
380
|
+
this.emit("agent:error", agent2, err);
|
|
381
|
+
});
|
|
382
|
+
this.emit("agent:running", agent2);
|
|
383
|
+
audit("agent.started", null, "agent", agent2.id, { pid: proc.pid });
|
|
384
|
+
if (initialTask && proc.stdin) {
|
|
385
|
+
proc.stdin.write(initialTask + "\n");
|
|
386
|
+
}
|
|
387
|
+
} catch (err) {
|
|
388
|
+
agent2.status = "error";
|
|
389
|
+
agent2.error = err.message;
|
|
390
|
+
updateAgent(agent2.id, { status: "error", error: err.message });
|
|
391
|
+
this.emit("agent:error", agent2, err);
|
|
392
|
+
throw err;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
buildCommand(agent2, template) {
|
|
396
|
+
const env = { ...template.env };
|
|
397
|
+
switch (template.driver) {
|
|
398
|
+
case "claude-code":
|
|
399
|
+
return {
|
|
400
|
+
command: ["claude", "--dangerously-skip-permissions"],
|
|
401
|
+
env: {
|
|
402
|
+
...env,
|
|
403
|
+
ANTHROPIC_MODEL: agent2.model || template.model || "claude-sonnet-4-5"
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
case "codex":
|
|
407
|
+
return {
|
|
408
|
+
command: ["codex", "--model", agent2.model || template.model || "codex-1"],
|
|
409
|
+
env
|
|
410
|
+
};
|
|
411
|
+
case "clawdbot":
|
|
412
|
+
return {
|
|
413
|
+
command: ["clawdbot", "agent", "--headless"],
|
|
414
|
+
env: {
|
|
415
|
+
...env,
|
|
416
|
+
CLAWDBOT_MODEL: agent2.model || template.model || "claude-sonnet-4-5"
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
case "subprocess":
|
|
420
|
+
if (!template.command || template.command.length === 0) {
|
|
421
|
+
throw new Error("subprocess driver requires command");
|
|
422
|
+
}
|
|
423
|
+
return { command: template.command, env };
|
|
424
|
+
default:
|
|
425
|
+
throw new Error(`Unknown driver: ${template.driver}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
async stop(id, force = false) {
|
|
429
|
+
const agent2 = this.agents.get(id);
|
|
430
|
+
if (!agent2) {
|
|
431
|
+
throw new Error(`Agent not found: ${id}`);
|
|
432
|
+
}
|
|
433
|
+
if (agent2.status !== "running" && agent2.status !== "starting") {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
agent2.status = "stopping";
|
|
437
|
+
this.emit("agent:stopping", agent2);
|
|
438
|
+
const proc = this.processes.get(id);
|
|
439
|
+
if (proc && proc.pid) {
|
|
440
|
+
await new Promise((resolve, reject) => {
|
|
441
|
+
treeKill(proc.pid, force ? "SIGKILL" : "SIGTERM", (err) => {
|
|
442
|
+
if (err) reject(err);
|
|
443
|
+
else resolve();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
audit("agent.stop_requested", null, "agent", id, { force });
|
|
448
|
+
}
|
|
449
|
+
async stopAll() {
|
|
450
|
+
const promises = Array.from(this.agents.values()).filter((a) => a.status === "running" || a.status === "starting").map((a) => this.stop(a.id, true));
|
|
451
|
+
await Promise.allSettled(promises);
|
|
452
|
+
}
|
|
453
|
+
async send(id, message) {
|
|
454
|
+
const agent2 = this.agents.get(id);
|
|
455
|
+
if (!agent2) {
|
|
456
|
+
throw new Error(`Agent not found: ${id}`);
|
|
457
|
+
}
|
|
458
|
+
if (agent2.status !== "running") {
|
|
459
|
+
throw new Error(`Agent is not running: ${agent2.status}`);
|
|
460
|
+
}
|
|
461
|
+
const proc = this.processes.get(id);
|
|
462
|
+
if (!proc || !proc.stdin) {
|
|
463
|
+
throw new Error("Agent process not available");
|
|
464
|
+
}
|
|
465
|
+
proc.stdin.write(message + "\n");
|
|
466
|
+
this.log(id, "info", `[USER] ${message}`);
|
|
467
|
+
audit("agent.message_sent", null, "agent", id, { length: message.length });
|
|
468
|
+
}
|
|
469
|
+
get(id) {
|
|
470
|
+
return this.agents.get(id);
|
|
471
|
+
}
|
|
472
|
+
list(includeStopped = false) {
|
|
473
|
+
const agents = Array.from(this.agents.values());
|
|
474
|
+
if (includeStopped) return agents;
|
|
475
|
+
return agents.filter((a) => a.status !== "stopped");
|
|
476
|
+
}
|
|
477
|
+
log(agentId, level, message) {
|
|
478
|
+
insertLog({ agentId, level, message });
|
|
479
|
+
this.emit("agent:log", { agentId, level, message, timestamp: Date.now() });
|
|
480
|
+
}
|
|
481
|
+
getLogs(agentId, limit = 100) {
|
|
482
|
+
return getLogs(agentId, limit);
|
|
483
|
+
}
|
|
484
|
+
getStats() {
|
|
485
|
+
const agents = Array.from(this.agents.values());
|
|
486
|
+
return {
|
|
487
|
+
total: agents.length,
|
|
488
|
+
running: agents.filter((a) => a.status === "running").length,
|
|
489
|
+
idle: agents.filter((a) => a.status === "idle").length,
|
|
490
|
+
stopped: agents.filter((a) => a.status === "stopped").length,
|
|
491
|
+
error: agents.filter((a) => a.status === "error").length
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// src/server/api/routes.ts
|
|
499
|
+
function registerApiRoutes(server2, options) {
|
|
500
|
+
const { config, agentManager: agentManager2, startTime: startTime2 } = options;
|
|
501
|
+
server2.addHook("onRequest", async (request, reply) => {
|
|
502
|
+
if (request.url === "/health") return;
|
|
503
|
+
if (!request.url.startsWith("/api")) return;
|
|
504
|
+
if (config.auth.enabled) {
|
|
505
|
+
const authHeader = request.headers.authorization;
|
|
506
|
+
const token = authHeader?.replace("Bearer ", "");
|
|
507
|
+
if (!token || !validateAuth(token, config)) {
|
|
508
|
+
reply.code(401).send({ error: "Unauthorized" });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
server2.get("/api/status", async () => {
|
|
514
|
+
const stats = agentManager2.getStats();
|
|
515
|
+
const agents = agentManager2.list(false);
|
|
516
|
+
return {
|
|
517
|
+
status: "ok",
|
|
518
|
+
uptime: Math.floor((Date.now() - startTime2) / 1e3),
|
|
519
|
+
agents: {
|
|
520
|
+
...stats,
|
|
521
|
+
list: agents.map((a) => ({
|
|
522
|
+
id: a.id,
|
|
523
|
+
name: a.name,
|
|
524
|
+
template: a.template,
|
|
525
|
+
status: a.status,
|
|
526
|
+
workspace: a.workspace,
|
|
527
|
+
currentTask: a.currentTask
|
|
528
|
+
}))
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
server2.get("/api/agents", async (request) => {
|
|
533
|
+
const includeStopped = request.query.all === "true";
|
|
534
|
+
const agents = agentManager2.list(includeStopped);
|
|
535
|
+
return {
|
|
536
|
+
agents: agents.map((a) => ({
|
|
537
|
+
id: a.id,
|
|
538
|
+
name: a.name,
|
|
539
|
+
template: a.template,
|
|
540
|
+
status: a.status,
|
|
541
|
+
workspace: a.workspace,
|
|
542
|
+
model: a.model,
|
|
543
|
+
pid: a.pid,
|
|
544
|
+
currentTask: a.currentTask,
|
|
545
|
+
createdAt: a.createdAt,
|
|
546
|
+
startedAt: a.startedAt,
|
|
547
|
+
stoppedAt: a.stoppedAt,
|
|
548
|
+
error: a.error
|
|
549
|
+
}))
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
server2.post("/api/agents", async (request, reply) => {
|
|
553
|
+
const { template, name, workspace, task, model } = request.body;
|
|
554
|
+
if (!template) {
|
|
555
|
+
reply.code(400).send({ error: "template is required" });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
const agent2 = await agentManager2.spawn({ template, name, workspace, task, model });
|
|
560
|
+
return {
|
|
561
|
+
agent: {
|
|
562
|
+
id: agent2.id,
|
|
563
|
+
name: agent2.name,
|
|
564
|
+
template: agent2.template,
|
|
565
|
+
status: agent2.status,
|
|
566
|
+
workspace: agent2.workspace,
|
|
567
|
+
model: agent2.model
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
} catch (err) {
|
|
571
|
+
reply.code(400).send({ error: err.message });
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
server2.get("/api/agents/:id", async (request, reply) => {
|
|
575
|
+
const agent2 = agentManager2.get(request.params.id);
|
|
576
|
+
if (!agent2) {
|
|
577
|
+
reply.code(404).send({ error: "Agent not found" });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
agent: {
|
|
582
|
+
id: agent2.id,
|
|
583
|
+
name: agent2.name,
|
|
584
|
+
template: agent2.template,
|
|
585
|
+
status: agent2.status,
|
|
586
|
+
workspace: agent2.workspace,
|
|
587
|
+
model: agent2.model,
|
|
588
|
+
pid: agent2.pid,
|
|
589
|
+
currentTask: agent2.currentTask,
|
|
590
|
+
createdAt: agent2.createdAt,
|
|
591
|
+
startedAt: agent2.startedAt,
|
|
592
|
+
stoppedAt: agent2.stoppedAt,
|
|
593
|
+
error: agent2.error
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
});
|
|
597
|
+
server2.delete(
|
|
598
|
+
"/api/agents/:id",
|
|
599
|
+
async (request, reply) => {
|
|
600
|
+
const agent2 = agentManager2.get(request.params.id);
|
|
601
|
+
if (!agent2) {
|
|
602
|
+
reply.code(404).send({ error: "Agent not found" });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const force = request.query.force === "true";
|
|
606
|
+
try {
|
|
607
|
+
await agentManager2.stop(request.params.id, force);
|
|
608
|
+
return { ok: true };
|
|
609
|
+
} catch (err) {
|
|
610
|
+
reply.code(500).send({ error: err.message });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
);
|
|
614
|
+
server2.post(
|
|
615
|
+
"/api/agents/:id/send",
|
|
616
|
+
async (request, reply) => {
|
|
617
|
+
const agent2 = agentManager2.get(request.params.id);
|
|
618
|
+
if (!agent2) {
|
|
619
|
+
reply.code(404).send({ error: "Agent not found" });
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const { message } = request.body;
|
|
623
|
+
if (!message) {
|
|
624
|
+
reply.code(400).send({ error: "message is required" });
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
await agentManager2.send(request.params.id, message);
|
|
629
|
+
return { ok: true };
|
|
630
|
+
} catch (err) {
|
|
631
|
+
reply.code(400).send({ error: err.message });
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
);
|
|
635
|
+
server2.get(
|
|
636
|
+
"/api/agents/:id/logs",
|
|
637
|
+
async (request, reply) => {
|
|
638
|
+
const agent2 = agentManager2.get(request.params.id);
|
|
639
|
+
if (!agent2) {
|
|
640
|
+
reply.code(404).send({ error: "Agent not found" });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
const limit = parseInt(request.query.lines || "100");
|
|
644
|
+
const logs = agentManager2.getLogs(request.params.id, limit);
|
|
645
|
+
return { logs };
|
|
646
|
+
}
|
|
647
|
+
);
|
|
648
|
+
server2.get("/api/templates", async () => {
|
|
649
|
+
return {
|
|
650
|
+
templates: Object.entries(config.agents.templates).map(([name, template]) => ({
|
|
651
|
+
name,
|
|
652
|
+
driver: template.driver,
|
|
653
|
+
model: template.model
|
|
654
|
+
}))
|
|
655
|
+
};
|
|
656
|
+
});
|
|
657
|
+
server2.get("/api/audit", async (request) => {
|
|
658
|
+
return { logs: [] };
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
var init_routes = __esm({
|
|
662
|
+
"src/server/api/routes.ts"() {
|
|
663
|
+
"use strict";
|
|
664
|
+
init_config();
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// src/server/ws/index.ts
|
|
669
|
+
function registerWebSocket(server2, options) {
|
|
670
|
+
const { config, agentManager: agentManager2 } = options;
|
|
671
|
+
const clients = /* @__PURE__ */ new Map();
|
|
672
|
+
server2.register(async (fastify) => {
|
|
673
|
+
fastify.get("/ws", { websocket: true }, (connection, req) => {
|
|
674
|
+
const ws = connection;
|
|
675
|
+
const clientId = Math.random().toString(36).slice(2);
|
|
676
|
+
const client = {
|
|
677
|
+
ws,
|
|
678
|
+
subscriptions: /* @__PURE__ */ new Set(),
|
|
679
|
+
authenticated: !config.auth.enabled
|
|
680
|
+
};
|
|
681
|
+
clients.set(clientId, client);
|
|
682
|
+
ws.on("message", (data) => {
|
|
683
|
+
try {
|
|
684
|
+
const msg = JSON.parse(data.toString());
|
|
685
|
+
handleMessage(clientId, client, msg);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
send(ws, { type: "error", error: "Invalid JSON" });
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
ws.on("close", () => {
|
|
691
|
+
clients.delete(clientId);
|
|
692
|
+
});
|
|
693
|
+
ws.on("error", (err) => {
|
|
694
|
+
console.error("WebSocket error:", err);
|
|
695
|
+
clients.delete(clientId);
|
|
696
|
+
});
|
|
697
|
+
send(ws, {
|
|
698
|
+
type: "welcome",
|
|
699
|
+
clientId,
|
|
700
|
+
authenticated: client.authenticated
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
function handleMessage(clientId, client, msg) {
|
|
705
|
+
if (msg.type === "auth") {
|
|
706
|
+
if (validateAuth(msg.token, config)) {
|
|
707
|
+
client.authenticated = true;
|
|
708
|
+
send(client.ws, { type: "auth", success: true });
|
|
709
|
+
} else {
|
|
710
|
+
send(client.ws, { type: "auth", success: false, error: "Invalid token" });
|
|
711
|
+
}
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (!client.authenticated) {
|
|
715
|
+
send(client.ws, { type: "error", error: "Not authenticated" });
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
switch (msg.type) {
|
|
719
|
+
case "subscribe:logs":
|
|
720
|
+
if (msg.agentId) {
|
|
721
|
+
client.subscriptions.add(`logs:${msg.agentId}`);
|
|
722
|
+
send(client.ws, { type: "subscribed", channel: `logs:${msg.agentId}` });
|
|
723
|
+
}
|
|
724
|
+
break;
|
|
725
|
+
case "unsubscribe:logs":
|
|
726
|
+
if (msg.agentId) {
|
|
727
|
+
client.subscriptions.delete(`logs:${msg.agentId}`);
|
|
728
|
+
send(client.ws, { type: "unsubscribed", channel: `logs:${msg.agentId}` });
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
case "subscribe:agents":
|
|
732
|
+
client.subscriptions.add("agents");
|
|
733
|
+
send(client.ws, { type: "subscribed", channel: "agents" });
|
|
734
|
+
break;
|
|
735
|
+
case "unsubscribe:agents":
|
|
736
|
+
client.subscriptions.delete("agents");
|
|
737
|
+
send(client.ws, { type: "unsubscribed", channel: "agents" });
|
|
738
|
+
break;
|
|
739
|
+
case "ping":
|
|
740
|
+
send(client.ws, { type: "pong", ts: Date.now() });
|
|
741
|
+
break;
|
|
742
|
+
default:
|
|
743
|
+
send(client.ws, { type: "error", error: `Unknown message type: ${msg.type}` });
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
agentManager2.on("agent:created", (agent2) => {
|
|
747
|
+
broadcast("agents", { type: "agent:created", agent: sanitizeAgent(agent2) });
|
|
748
|
+
});
|
|
749
|
+
agentManager2.on("agent:running", (agent2) => {
|
|
750
|
+
broadcast("agents", { type: "agent:running", agent: sanitizeAgent(agent2) });
|
|
751
|
+
});
|
|
752
|
+
agentManager2.on("agent:stopped", (agent2) => {
|
|
753
|
+
broadcast("agents", { type: "agent:stopped", agent: sanitizeAgent(agent2) });
|
|
754
|
+
});
|
|
755
|
+
agentManager2.on("agent:error", (agent2, error) => {
|
|
756
|
+
broadcast("agents", { type: "agent:error", agent: sanitizeAgent(agent2), error: error.message });
|
|
757
|
+
});
|
|
758
|
+
agentManager2.on("agent:log", (data) => {
|
|
759
|
+
broadcast(`logs:${data.agentId}`, { type: "agent:log", ...data });
|
|
760
|
+
});
|
|
761
|
+
function broadcast(channel, message) {
|
|
762
|
+
for (const client of clients.values()) {
|
|
763
|
+
if (client.authenticated && client.subscriptions.has(channel)) {
|
|
764
|
+
send(client.ws, message);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function send(ws, message) {
|
|
769
|
+
if (ws.readyState === ws.OPEN) {
|
|
770
|
+
ws.send(JSON.stringify(message));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function sanitizeAgent(agent2) {
|
|
774
|
+
return {
|
|
775
|
+
id: agent2.id,
|
|
776
|
+
name: agent2.name,
|
|
777
|
+
template: agent2.template,
|
|
778
|
+
status: agent2.status,
|
|
779
|
+
workspace: agent2.workspace,
|
|
780
|
+
model: agent2.model,
|
|
781
|
+
pid: agent2.pid,
|
|
782
|
+
currentTask: agent2.currentTask,
|
|
783
|
+
createdAt: agent2.createdAt,
|
|
784
|
+
startedAt: agent2.startedAt,
|
|
785
|
+
stoppedAt: agent2.stoppedAt,
|
|
786
|
+
error: agent2.error
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
var init_ws = __esm({
|
|
791
|
+
"src/server/ws/index.ts"() {
|
|
792
|
+
"use strict";
|
|
793
|
+
init_config();
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
// src/server/index.ts
|
|
798
|
+
var server_exports = {};
|
|
799
|
+
__export(server_exports, {
|
|
800
|
+
startServer: () => startServer
|
|
801
|
+
});
|
|
802
|
+
import Fastify from "fastify";
|
|
803
|
+
import fastifyWebsocket from "@fastify/websocket";
|
|
804
|
+
import fastifyCors from "@fastify/cors";
|
|
805
|
+
import fastifyStatic from "@fastify/static";
|
|
806
|
+
import pino from "pino";
|
|
807
|
+
import { join as join4, dirname } from "path";
|
|
808
|
+
import { fileURLToPath } from "url";
|
|
809
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
810
|
+
async function startServer(options) {
|
|
811
|
+
const config = loadConfig(options.configPath);
|
|
812
|
+
const logPath = join4(options.dataDir, "logs", "dirigent.log");
|
|
813
|
+
mkdirSync3(dirname(logPath), { recursive: true });
|
|
814
|
+
const logger = pino({
|
|
815
|
+
level: config.logging?.level || "info",
|
|
816
|
+
transport: {
|
|
817
|
+
targets: [
|
|
818
|
+
{
|
|
819
|
+
target: "pino-pretty",
|
|
820
|
+
options: { colorize: true },
|
|
821
|
+
level: "info"
|
|
822
|
+
},
|
|
823
|
+
{
|
|
824
|
+
target: "pino/file",
|
|
825
|
+
options: { destination: logPath },
|
|
826
|
+
level: "debug"
|
|
827
|
+
}
|
|
828
|
+
]
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
const dbPath = join4(options.dataDir, "data", "dirigent.db");
|
|
832
|
+
mkdirSync3(dirname(dbPath), { recursive: true });
|
|
833
|
+
initDatabase(dbPath);
|
|
834
|
+
server = Fastify({ logger });
|
|
835
|
+
await server.register(fastifyCors, {
|
|
836
|
+
origin: true,
|
|
837
|
+
credentials: true
|
|
838
|
+
});
|
|
839
|
+
await server.register(fastifyWebsocket);
|
|
840
|
+
const dashboardPath = join4(__dirname, "..", "dashboard", "dist");
|
|
841
|
+
if (existsSync4(dashboardPath)) {
|
|
842
|
+
await server.register(fastifyStatic, {
|
|
843
|
+
root: dashboardPath,
|
|
844
|
+
prefix: "/"
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
agentManager = new AgentManager({
|
|
848
|
+
dataDir: options.dataDir,
|
|
849
|
+
config,
|
|
850
|
+
logger
|
|
851
|
+
});
|
|
852
|
+
registerApiRoutes(server, {
|
|
853
|
+
config,
|
|
854
|
+
agentManager,
|
|
855
|
+
startTime
|
|
856
|
+
});
|
|
857
|
+
registerWebSocket(server, {
|
|
858
|
+
config,
|
|
859
|
+
agentManager
|
|
860
|
+
});
|
|
861
|
+
server.get("/health", async () => ({ status: "ok", uptime: Math.floor((Date.now() - startTime) / 1e3) }));
|
|
862
|
+
const shutdown = async (signal) => {
|
|
863
|
+
logger.info(`Received ${signal}, shutting down...`);
|
|
864
|
+
if (agentManager) {
|
|
865
|
+
await agentManager.stopAll();
|
|
866
|
+
}
|
|
867
|
+
if (server) {
|
|
868
|
+
await server.close();
|
|
869
|
+
}
|
|
870
|
+
const db2 = getDatabase();
|
|
871
|
+
if (db2) db2.close();
|
|
872
|
+
process.exit(0);
|
|
873
|
+
};
|
|
874
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
875
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
876
|
+
try {
|
|
877
|
+
await server.listen({ port: options.port, host: config.server?.host || "0.0.0.0" });
|
|
878
|
+
console.log(`
|
|
879
|
+
\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
|
|
880
|
+
\u2551 \u2551
|
|
881
|
+
\u2551 \u{1F3AD} DIRIGENT SERVER RUNNING \u2551
|
|
882
|
+
\u2551 \u2551
|
|
883
|
+
\u2551 Dashboard: http://localhost:${options.port.toString().padEnd(25)}\u2551
|
|
884
|
+
\u2551 API: http://localhost:${options.port}/api${" ".repeat(21)}\u2551
|
|
885
|
+
\u2551 WebSocket: ws://localhost:${options.port}/ws${" ".repeat(22)}\u2551
|
|
886
|
+
\u2551 \u2551
|
|
887
|
+
\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
|
|
888
|
+
`);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
logger.error(err);
|
|
891
|
+
process.exit(1);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
var __filename, __dirname, server, agentManager, startTime;
|
|
895
|
+
var init_server = __esm({
|
|
896
|
+
"src/server/index.ts"() {
|
|
897
|
+
"use strict";
|
|
898
|
+
init_config();
|
|
899
|
+
init_db();
|
|
900
|
+
init_manager();
|
|
901
|
+
init_routes();
|
|
902
|
+
init_ws();
|
|
903
|
+
__filename = fileURLToPath(import.meta.url);
|
|
904
|
+
__dirname = dirname(__filename);
|
|
905
|
+
server = null;
|
|
906
|
+
agentManager = null;
|
|
907
|
+
startTime = Date.now();
|
|
908
|
+
if (process.env.DIRIGENT_CONFIG) {
|
|
909
|
+
startServer({
|
|
910
|
+
port: parseInt(process.env.DIRIGENT_PORT || "3000"),
|
|
911
|
+
configPath: process.env.DIRIGENT_CONFIG,
|
|
912
|
+
dataDir: process.env.DIRIGENT_DATA_DIR || ".dirigent"
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
// src/cli/index.ts
|
|
919
|
+
import { Command } from "commander";
|
|
920
|
+
import chalk8 from "chalk";
|
|
921
|
+
|
|
922
|
+
// src/cli/commands/init.ts
|
|
923
|
+
import chalk from "chalk";
|
|
924
|
+
import ora from "ora";
|
|
925
|
+
import * as readline from "readline";
|
|
926
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
927
|
+
import { join } from "path";
|
|
928
|
+
import { nanoid } from "nanoid";
|
|
929
|
+
async function prompt(question, defaultValue) {
|
|
930
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
931
|
+
return new Promise((resolve) => {
|
|
932
|
+
const q = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
|
|
933
|
+
rl.question(q, (answer) => {
|
|
934
|
+
rl.close();
|
|
935
|
+
resolve(answer || defaultValue || "");
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
async function confirm(question, defaultValue = true) {
|
|
940
|
+
const answer = await prompt(`${question} (${defaultValue ? "Y/n" : "y/N"})`, defaultValue ? "y" : "n");
|
|
941
|
+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
942
|
+
}
|
|
943
|
+
var DEFAULT_CONFIG = {
|
|
944
|
+
version: "1",
|
|
945
|
+
server: {
|
|
946
|
+
port: 3e3,
|
|
947
|
+
host: "0.0.0.0"
|
|
948
|
+
},
|
|
949
|
+
auth: {
|
|
950
|
+
enabled: true,
|
|
951
|
+
adminToken: ""
|
|
952
|
+
// Generated during init
|
|
953
|
+
},
|
|
954
|
+
agents: {
|
|
955
|
+
maxConcurrent: 10,
|
|
956
|
+
defaultTimeout: 3600,
|
|
957
|
+
templates: {
|
|
958
|
+
claude: {
|
|
959
|
+
driver: "claude-code",
|
|
960
|
+
model: "claude-sonnet-4-5",
|
|
961
|
+
maxTokens: 16e3
|
|
962
|
+
},
|
|
963
|
+
codex: {
|
|
964
|
+
driver: "codex",
|
|
965
|
+
model: "codex-1"
|
|
966
|
+
},
|
|
967
|
+
custom: {
|
|
968
|
+
driver: "subprocess",
|
|
969
|
+
command: []
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
},
|
|
973
|
+
logging: {
|
|
974
|
+
level: "info",
|
|
975
|
+
format: "pretty",
|
|
976
|
+
file: "logs/dirigent.log"
|
|
977
|
+
},
|
|
978
|
+
database: {
|
|
979
|
+
path: "data/dirigent.db"
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
async function initCommand(options) {
|
|
983
|
+
console.log(chalk.cyan("\n\u{1F3AD} Initializing Dirigent workspace\n"));
|
|
984
|
+
const dataDir = options.dataDir || ".dirigent";
|
|
985
|
+
const configPath = join(process.cwd(), dataDir, "config.json");
|
|
986
|
+
if (existsSync(configPath)) {
|
|
987
|
+
const overwrite = options.yes ? true : await confirm("Dirigent is already initialized. Overwrite config?", false);
|
|
988
|
+
if (!overwrite) {
|
|
989
|
+
console.log(chalk.yellow("Aborted."));
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
let config = { ...DEFAULT_CONFIG };
|
|
994
|
+
if (!options.yes) {
|
|
995
|
+
const portStr = await prompt("Server port", options.port || "3000");
|
|
996
|
+
const authEnabled = await confirm("Enable authentication?", true);
|
|
997
|
+
const maxAgentsStr = await prompt("Max concurrent agents", "10");
|
|
998
|
+
config.server.port = parseInt(portStr) || 3e3;
|
|
999
|
+
config.auth.enabled = authEnabled;
|
|
1000
|
+
config.agents.maxConcurrent = parseInt(maxAgentsStr) || 10;
|
|
1001
|
+
} else {
|
|
1002
|
+
config.server.port = parseInt(options.port || "3000");
|
|
1003
|
+
}
|
|
1004
|
+
config.auth.adminToken = nanoid(32);
|
|
1005
|
+
const spinner = ora("Creating workspace...").start();
|
|
1006
|
+
try {
|
|
1007
|
+
const dirs = [
|
|
1008
|
+
dataDir,
|
|
1009
|
+
join(dataDir, "data"),
|
|
1010
|
+
join(dataDir, "logs"),
|
|
1011
|
+
join(dataDir, "agents"),
|
|
1012
|
+
join(dataDir, "workspaces")
|
|
1013
|
+
];
|
|
1014
|
+
for (const dir of dirs) {
|
|
1015
|
+
mkdirSync(dir, { recursive: true });
|
|
1016
|
+
}
|
|
1017
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1018
|
+
writeFileSync(
|
|
1019
|
+
join(dataDir, ".gitignore"),
|
|
1020
|
+
`# Dirigent data
|
|
1021
|
+
data/
|
|
1022
|
+
logs/
|
|
1023
|
+
agents/
|
|
1024
|
+
workspaces/
|
|
1025
|
+
*.db
|
|
1026
|
+
*.db-journal
|
|
1027
|
+
`
|
|
1028
|
+
);
|
|
1029
|
+
spinner.succeed("Workspace created");
|
|
1030
|
+
console.log(chalk.green("\n\u2705 Dirigent initialized successfully!\n"));
|
|
1031
|
+
console.log(chalk.dim("Configuration:"));
|
|
1032
|
+
console.log(` ${chalk.cyan("Config:")} ${configPath}`);
|
|
1033
|
+
console.log(` ${chalk.cyan("Port:")} ${config.server.port}`);
|
|
1034
|
+
console.log(` ${chalk.cyan("Auth:")} ${config.auth.enabled ? "Enabled" : "Disabled"}`);
|
|
1035
|
+
if (config.auth.enabled) {
|
|
1036
|
+
console.log(chalk.yellow("\n\u26A0\uFE0F Save your admin token (shown only once):"));
|
|
1037
|
+
console.log(chalk.bold.white(` ${config.auth.adminToken}
|
|
1038
|
+
`));
|
|
1039
|
+
}
|
|
1040
|
+
console.log(chalk.dim("Next steps:"));
|
|
1041
|
+
console.log(` ${chalk.cyan("1.")} Start the server: ${chalk.bold("dirigent up")}`);
|
|
1042
|
+
console.log(` ${chalk.cyan("2.")} Open dashboard: ${chalk.bold(`http://localhost:${config.server.port}`)}`);
|
|
1043
|
+
console.log(` ${chalk.cyan("3.")} Spawn an agent: ${chalk.bold("dirigent agent spawn claude")}
|
|
1044
|
+
`);
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
spinner.fail("Failed to create workspace");
|
|
1047
|
+
console.error(chalk.red(err));
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// src/cli/commands/up.ts
|
|
1053
|
+
import chalk2 from "chalk";
|
|
1054
|
+
import ora2 from "ora";
|
|
1055
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
1056
|
+
import { join as join5 } from "path";
|
|
1057
|
+
import { spawn as spawn2 } from "child_process";
|
|
1058
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1059
|
+
import { dirname as dirname2 } from "path";
|
|
1060
|
+
async function upCommand(options) {
|
|
1061
|
+
const dataDir = ".dirigent";
|
|
1062
|
+
const configPath = join5(process.cwd(), dataDir, "config.json");
|
|
1063
|
+
const pidPath = join5(process.cwd(), dataDir, "dirigent.pid");
|
|
1064
|
+
if (!existsSync5(configPath)) {
|
|
1065
|
+
console.log(chalk2.red("\n\u274C Dirigent not initialized. Run `dirigent init` first.\n"));
|
|
1066
|
+
process.exit(1);
|
|
1067
|
+
}
|
|
1068
|
+
if (existsSync5(pidPath)) {
|
|
1069
|
+
const pid = parseInt(readFileSync3(pidPath, "utf-8").trim());
|
|
1070
|
+
try {
|
|
1071
|
+
process.kill(pid, 0);
|
|
1072
|
+
console.log(chalk2.yellow(`
|
|
1073
|
+
\u26A0\uFE0F Dirigent is already running (PID: ${pid})`));
|
|
1074
|
+
console.log(chalk2.dim(" Use `dirigent down` to stop it first.\n"));
|
|
1075
|
+
return;
|
|
1076
|
+
} catch {
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1080
|
+
const port = options.port || config.server.port;
|
|
1081
|
+
console.log(chalk2.cyan("\n\u{1F3AD} Starting Dirigent server...\n"));
|
|
1082
|
+
const spinner = ora2("Starting server...").start();
|
|
1083
|
+
try {
|
|
1084
|
+
const __filename2 = fileURLToPath2(import.meta.url);
|
|
1085
|
+
const __dirname2 = dirname2(__filename2);
|
|
1086
|
+
const serverPath = join5(__dirname2, "..", "..", "server", "index.js");
|
|
1087
|
+
const env = {
|
|
1088
|
+
...process.env,
|
|
1089
|
+
DIRIGENT_PORT: port.toString(),
|
|
1090
|
+
DIRIGENT_CONFIG: configPath,
|
|
1091
|
+
DIRIGENT_DATA_DIR: join5(process.cwd(), dataDir)
|
|
1092
|
+
};
|
|
1093
|
+
if (options.detach) {
|
|
1094
|
+
const child = spawn2(process.execPath, [serverPath], {
|
|
1095
|
+
detached: true,
|
|
1096
|
+
stdio: "ignore",
|
|
1097
|
+
env
|
|
1098
|
+
});
|
|
1099
|
+
child.unref();
|
|
1100
|
+
writeFileSync3(pidPath, child.pid.toString());
|
|
1101
|
+
spinner.succeed(`Server started (PID: ${child.pid})`);
|
|
1102
|
+
console.log(chalk2.green(`
|
|
1103
|
+
\u2705 Dirigent is running
|
|
1104
|
+
`));
|
|
1105
|
+
console.log(` ${chalk2.cyan("Dashboard:")} http://localhost:${port}`);
|
|
1106
|
+
console.log(` ${chalk2.cyan("API:")} http://localhost:${port}/api`);
|
|
1107
|
+
console.log(` ${chalk2.cyan("WebSocket:")} ws://localhost:${port}/ws
|
|
1108
|
+
`);
|
|
1109
|
+
console.log(chalk2.dim(` Stop with: dirigent down
|
|
1110
|
+
`));
|
|
1111
|
+
} else {
|
|
1112
|
+
spinner.succeed("Server starting...");
|
|
1113
|
+
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
1114
|
+
await startServer2({
|
|
1115
|
+
port: parseInt(port),
|
|
1116
|
+
configPath,
|
|
1117
|
+
dataDir: join5(process.cwd(), dataDir)
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
} catch (err) {
|
|
1121
|
+
spinner.fail("Failed to start server");
|
|
1122
|
+
console.error(chalk2.red(err));
|
|
1123
|
+
process.exit(1);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// src/cli/commands/down.ts
|
|
1128
|
+
import chalk3 from "chalk";
|
|
1129
|
+
import ora3 from "ora";
|
|
1130
|
+
import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync } from "fs";
|
|
1131
|
+
import { join as join6 } from "path";
|
|
1132
|
+
import treeKill2 from "tree-kill";
|
|
1133
|
+
async function downCommand(options) {
|
|
1134
|
+
const dataDir = ".dirigent";
|
|
1135
|
+
const pidPath = join6(process.cwd(), dataDir, "dirigent.pid");
|
|
1136
|
+
if (!existsSync6(pidPath)) {
|
|
1137
|
+
console.log(chalk3.yellow("\n\u26A0\uFE0F Dirigent is not running.\n"));
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const pid = parseInt(readFileSync4(pidPath, "utf-8").trim());
|
|
1141
|
+
const spinner = ora3("Stopping Dirigent...").start();
|
|
1142
|
+
try {
|
|
1143
|
+
try {
|
|
1144
|
+
process.kill(pid, 0);
|
|
1145
|
+
} catch {
|
|
1146
|
+
spinner.warn("Dirigent was not running (stale PID file)");
|
|
1147
|
+
unlinkSync(pidPath);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
await new Promise((resolve, reject) => {
|
|
1151
|
+
const signal = options.force ? "SIGKILL" : "SIGTERM";
|
|
1152
|
+
treeKill2(pid, signal, (err) => {
|
|
1153
|
+
if (err) reject(err);
|
|
1154
|
+
else resolve();
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
unlinkSync(pidPath);
|
|
1158
|
+
spinner.succeed("Dirigent stopped");
|
|
1159
|
+
console.log(chalk3.green("\n\u2705 Dirigent has been stopped.\n"));
|
|
1160
|
+
} catch (err) {
|
|
1161
|
+
spinner.fail("Failed to stop Dirigent");
|
|
1162
|
+
console.error(chalk3.red(err));
|
|
1163
|
+
if (options.force) {
|
|
1164
|
+
try {
|
|
1165
|
+
unlinkSync(pidPath);
|
|
1166
|
+
} catch {
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
process.exit(1);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/cli/commands/status.ts
|
|
1174
|
+
import chalk4 from "chalk";
|
|
1175
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5 } from "fs";
|
|
1176
|
+
import { join as join7 } from "path";
|
|
1177
|
+
import boxen from "boxen";
|
|
1178
|
+
async function statusCommand(options) {
|
|
1179
|
+
const dataDir = ".dirigent";
|
|
1180
|
+
const configPath = join7(process.cwd(), dataDir, "config.json");
|
|
1181
|
+
const pidPath = join7(process.cwd(), dataDir, "dirigent.pid");
|
|
1182
|
+
if (!existsSync7(configPath)) {
|
|
1183
|
+
if (options.json) {
|
|
1184
|
+
console.log(JSON.stringify({ initialized: false, running: false }));
|
|
1185
|
+
} else {
|
|
1186
|
+
console.log(chalk4.yellow("\n\u26A0\uFE0F Dirigent not initialized. Run `dirigent init` first.\n"));
|
|
1187
|
+
}
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
const config = JSON.parse(readFileSync5(configPath, "utf-8"));
|
|
1191
|
+
let running = false;
|
|
1192
|
+
let pid = null;
|
|
1193
|
+
let serverStatus = null;
|
|
1194
|
+
if (existsSync7(pidPath)) {
|
|
1195
|
+
pid = parseInt(readFileSync5(pidPath, "utf-8").trim());
|
|
1196
|
+
try {
|
|
1197
|
+
process.kill(pid, 0);
|
|
1198
|
+
running = true;
|
|
1199
|
+
try {
|
|
1200
|
+
const res = await fetch(`http://localhost:${config.server.port}/api/status`);
|
|
1201
|
+
if (res.ok) {
|
|
1202
|
+
serverStatus = await res.json();
|
|
1203
|
+
}
|
|
1204
|
+
} catch {
|
|
1205
|
+
}
|
|
1206
|
+
} catch {
|
|
1207
|
+
running = false;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (options.json) {
|
|
1211
|
+
console.log(
|
|
1212
|
+
JSON.stringify({
|
|
1213
|
+
initialized: true,
|
|
1214
|
+
running,
|
|
1215
|
+
pid,
|
|
1216
|
+
port: config.server.port,
|
|
1217
|
+
agents: serverStatus?.agents || [],
|
|
1218
|
+
uptime: serverStatus?.uptime || null
|
|
1219
|
+
})
|
|
1220
|
+
);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const statusIcon = running ? chalk4.green("\u25CF") : chalk4.red("\u25CF");
|
|
1224
|
+
const statusText = running ? chalk4.green("Running") : chalk4.red("Stopped");
|
|
1225
|
+
console.log(
|
|
1226
|
+
boxen(
|
|
1227
|
+
`${chalk4.bold.white("\u{1F3AD} DIRIGENT STATUS")}
|
|
1228
|
+
|
|
1229
|
+
${chalk4.dim("Status:")} ${statusIcon} ${statusText}${pid ? chalk4.dim(` (PID: ${pid})`) : ""}
|
|
1230
|
+
${chalk4.dim("Port:")} ${config.server.port}
|
|
1231
|
+
${chalk4.dim("Auth:")} ${config.auth.enabled ? "Enabled" : "Disabled"}
|
|
1232
|
+
` + (serverStatus ? `${chalk4.dim("Uptime:")} ${formatUptime(serverStatus.uptime)}
|
|
1233
|
+
${chalk4.dim("Agents:")} ${serverStatus.agents.running}/${serverStatus.agents.total} running` : ""),
|
|
1234
|
+
{
|
|
1235
|
+
padding: 1,
|
|
1236
|
+
margin: 1,
|
|
1237
|
+
borderStyle: "round",
|
|
1238
|
+
borderColor: running ? "green" : "red"
|
|
1239
|
+
}
|
|
1240
|
+
)
|
|
1241
|
+
);
|
|
1242
|
+
if (running && serverStatus?.agents?.list?.length > 0) {
|
|
1243
|
+
console.log(chalk4.dim("\nActive Agents:\n"));
|
|
1244
|
+
for (const agent2 of serverStatus.agents.list) {
|
|
1245
|
+
const agentIcon = agent2.status === "running" ? chalk4.green("\u25CF") : chalk4.yellow("\u25CF");
|
|
1246
|
+
console.log(` ${agentIcon} ${chalk4.bold(agent2.name)} ${chalk4.dim(`(${agent2.id})`)}`);
|
|
1247
|
+
console.log(` ${chalk4.dim("Template:")} ${agent2.template}`);
|
|
1248
|
+
console.log(` ${chalk4.dim("Workspace:")} ${agent2.workspace}`);
|
|
1249
|
+
console.log("");
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (!running) {
|
|
1253
|
+
console.log(chalk4.dim(`
|
|
1254
|
+
Start with: ${chalk4.cyan("dirigent up")}
|
|
1255
|
+
`));
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function formatUptime(seconds) {
|
|
1259
|
+
if (seconds < 60) return `${seconds}s`;
|
|
1260
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
1261
|
+
const h = Math.floor(seconds / 3600);
|
|
1262
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
1263
|
+
return `${h}h ${m}m`;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// src/cli/commands/agent.ts
|
|
1267
|
+
import chalk5 from "chalk";
|
|
1268
|
+
import ora4 from "ora";
|
|
1269
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
1270
|
+
import { join as join8 } from "path";
|
|
1271
|
+
async function getConfig() {
|
|
1272
|
+
const dataDir = ".dirigent";
|
|
1273
|
+
const configPath = join8(process.cwd(), dataDir, "config.json");
|
|
1274
|
+
if (!existsSync8(configPath)) {
|
|
1275
|
+
console.log(chalk5.red("\n\u274C Dirigent not initialized. Run `dirigent init` first.\n"));
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1278
|
+
return JSON.parse(readFileSync6(configPath, "utf-8"));
|
|
1279
|
+
}
|
|
1280
|
+
async function apiCall(method, path, body) {
|
|
1281
|
+
const config = await getConfig();
|
|
1282
|
+
const url = `http://localhost:${config.server.port}/api${path}`;
|
|
1283
|
+
const res = await fetch(url, {
|
|
1284
|
+
method,
|
|
1285
|
+
headers: {
|
|
1286
|
+
"Content-Type": "application/json",
|
|
1287
|
+
Authorization: `Bearer ${config.auth.adminToken}`
|
|
1288
|
+
},
|
|
1289
|
+
body: body ? JSON.stringify(body) : void 0
|
|
1290
|
+
});
|
|
1291
|
+
if (!res.ok) {
|
|
1292
|
+
const error = await res.json().catch(() => ({ error: res.statusText }));
|
|
1293
|
+
throw new Error(error.error || error.message || "API call failed");
|
|
1294
|
+
}
|
|
1295
|
+
return res.json();
|
|
1296
|
+
}
|
|
1297
|
+
async function agentCommand(action, options) {
|
|
1298
|
+
switch (action) {
|
|
1299
|
+
case "list":
|
|
1300
|
+
await listAgents(options);
|
|
1301
|
+
break;
|
|
1302
|
+
case "spawn":
|
|
1303
|
+
await spawnAgent(options);
|
|
1304
|
+
break;
|
|
1305
|
+
case "stop":
|
|
1306
|
+
await stopAgent(options);
|
|
1307
|
+
break;
|
|
1308
|
+
case "send":
|
|
1309
|
+
await sendToAgent(options);
|
|
1310
|
+
break;
|
|
1311
|
+
case "logs":
|
|
1312
|
+
await agentLogs(options);
|
|
1313
|
+
break;
|
|
1314
|
+
default:
|
|
1315
|
+
console.log(chalk5.red(`Unknown action: ${action}`));
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
async function listAgents(options) {
|
|
1319
|
+
try {
|
|
1320
|
+
const result = await apiCall("GET", `/agents${options.all ? "?all=true" : ""}`);
|
|
1321
|
+
if (options.json) {
|
|
1322
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
if (result.agents.length === 0) {
|
|
1326
|
+
console.log(chalk5.yellow("\nNo agents found.\n"));
|
|
1327
|
+
console.log(chalk5.dim(" Spawn one with: dirigent agent spawn claude\n"));
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
console.log(chalk5.cyan("\n\u{1F916} Agents\n"));
|
|
1331
|
+
for (const agent2 of result.agents) {
|
|
1332
|
+
const icon = agent2.status === "running" ? chalk5.green("\u25CF") : agent2.status === "idle" ? chalk5.yellow("\u25CF") : chalk5.red("\u25CF");
|
|
1333
|
+
console.log(`${icon} ${chalk5.bold(agent2.name)} ${chalk5.dim(`[${agent2.id}]`)}`);
|
|
1334
|
+
console.log(` ${chalk5.dim("Template:")} ${agent2.template}`);
|
|
1335
|
+
console.log(` ${chalk5.dim("Status:")} ${agent2.status}`);
|
|
1336
|
+
console.log(` ${chalk5.dim("Workspace:")} ${agent2.workspace || "N/A"}`);
|
|
1337
|
+
if (agent2.currentTask) {
|
|
1338
|
+
console.log(` ${chalk5.dim("Task:")} ${agent2.currentTask.substring(0, 60)}...`);
|
|
1339
|
+
}
|
|
1340
|
+
console.log("");
|
|
1341
|
+
}
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
console.log(chalk5.red(`
|
|
1344
|
+
\u274C ${err.message}
|
|
1345
|
+
`));
|
|
1346
|
+
console.log(chalk5.dim(" Make sure Dirigent is running: dirigent up\n"));
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
async function spawnAgent(options) {
|
|
1350
|
+
const spinner = ora4("Spawning agent...").start();
|
|
1351
|
+
try {
|
|
1352
|
+
const result = await apiCall("POST", "/agents", {
|
|
1353
|
+
template: options.template,
|
|
1354
|
+
name: options.name,
|
|
1355
|
+
workspace: options.workspace,
|
|
1356
|
+
task: options.task,
|
|
1357
|
+
model: options.model
|
|
1358
|
+
});
|
|
1359
|
+
spinner.succeed(`Agent spawned: ${chalk5.bold(result.agent.name)}`);
|
|
1360
|
+
console.log(`
|
|
1361
|
+
${chalk5.dim("ID:")} ${result.agent.id}`);
|
|
1362
|
+
console.log(` ${chalk5.dim("Template:")} ${result.agent.template}`);
|
|
1363
|
+
console.log(` ${chalk5.dim("Workspace:")} ${result.agent.workspace}`);
|
|
1364
|
+
if (options.task) {
|
|
1365
|
+
console.log(` ${chalk5.dim("Task:")} ${options.task.substring(0, 60)}...`);
|
|
1366
|
+
}
|
|
1367
|
+
console.log(chalk5.dim(`
|
|
1368
|
+
View logs: dirigent agent logs ${result.agent.id}
|
|
1369
|
+
`));
|
|
1370
|
+
} catch (err) {
|
|
1371
|
+
spinner.fail("Failed to spawn agent");
|
|
1372
|
+
console.log(chalk5.red(`
|
|
1373
|
+
\u274C ${err.message}
|
|
1374
|
+
`));
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
async function stopAgent(options) {
|
|
1378
|
+
const spinner = ora4("Stopping agent...").start();
|
|
1379
|
+
try {
|
|
1380
|
+
await apiCall("DELETE", `/agents/${options.id}${options.force ? "?force=true" : ""}`);
|
|
1381
|
+
spinner.succeed(`Agent stopped: ${options.id}`);
|
|
1382
|
+
} catch (err) {
|
|
1383
|
+
spinner.fail("Failed to stop agent");
|
|
1384
|
+
console.log(chalk5.red(`
|
|
1385
|
+
\u274C ${err.message}
|
|
1386
|
+
`));
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
async function sendToAgent(options) {
|
|
1390
|
+
try {
|
|
1391
|
+
const result = await apiCall("POST", `/agents/${options.id}/send`, {
|
|
1392
|
+
message: options.message
|
|
1393
|
+
});
|
|
1394
|
+
console.log(chalk5.green(`
|
|
1395
|
+
\u2709\uFE0F Message sent to agent ${options.id}
|
|
1396
|
+
`));
|
|
1397
|
+
if (result.response) {
|
|
1398
|
+
console.log(chalk5.dim("Response:"));
|
|
1399
|
+
console.log(result.response);
|
|
1400
|
+
}
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
console.log(chalk5.red(`
|
|
1403
|
+
\u274C ${err.message}
|
|
1404
|
+
`));
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
async function agentLogs(options) {
|
|
1408
|
+
const config = await getConfig();
|
|
1409
|
+
if (options.follow) {
|
|
1410
|
+
const { io } = await import("socket.io-client");
|
|
1411
|
+
const socket = io(`http://localhost:${config.server.port}`, {
|
|
1412
|
+
auth: { token: config.auth.adminToken }
|
|
1413
|
+
});
|
|
1414
|
+
socket.on("connect", () => {
|
|
1415
|
+
socket.emit("subscribe:logs", { agentId: options.id });
|
|
1416
|
+
console.log(chalk5.dim(`
|
|
1417
|
+
Streaming logs for agent ${options.id}...
|
|
1418
|
+
`));
|
|
1419
|
+
});
|
|
1420
|
+
socket.on("agent:log", (data) => {
|
|
1421
|
+
const timestamp = chalk5.dim(new Date(data.timestamp).toISOString().split("T")[1].slice(0, 8));
|
|
1422
|
+
const level = data.level === "error" ? chalk5.red("ERR") : data.level === "warn" ? chalk5.yellow("WRN") : chalk5.blue("INF");
|
|
1423
|
+
console.log(`${timestamp} ${level} ${data.message}`);
|
|
1424
|
+
});
|
|
1425
|
+
socket.on("disconnect", () => {
|
|
1426
|
+
console.log(chalk5.dim("\nDisconnected.\n"));
|
|
1427
|
+
process.exit(0);
|
|
1428
|
+
});
|
|
1429
|
+
process.on("SIGINT", () => {
|
|
1430
|
+
socket.disconnect();
|
|
1431
|
+
});
|
|
1432
|
+
} else {
|
|
1433
|
+
try {
|
|
1434
|
+
const result = await apiCall(
|
|
1435
|
+
"GET",
|
|
1436
|
+
`/agents/${options.id}/logs?lines=${options.lines || 50}`
|
|
1437
|
+
);
|
|
1438
|
+
console.log(chalk5.dim(`
|
|
1439
|
+
Logs for agent ${options.id}:
|
|
1440
|
+
`));
|
|
1441
|
+
for (const log of result.logs) {
|
|
1442
|
+
const timestamp = chalk5.dim(new Date(log.timestamp).toISOString().split("T")[1].slice(0, 8));
|
|
1443
|
+
const level = log.level === "error" ? chalk5.red("ERR") : log.level === "warn" ? chalk5.yellow("WRN") : chalk5.blue("INF");
|
|
1444
|
+
console.log(`${timestamp} ${level} ${log.message}`);
|
|
1445
|
+
}
|
|
1446
|
+
console.log(chalk5.dim(`
|
|
1447
|
+
Stream with: dirigent agent logs ${options.id} -f
|
|
1448
|
+
`));
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
console.log(chalk5.red(`
|
|
1451
|
+
\u274C ${err.message}
|
|
1452
|
+
`));
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/cli/commands/logs.ts
|
|
1458
|
+
import chalk6 from "chalk";
|
|
1459
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
1460
|
+
import { join as join9 } from "path";
|
|
1461
|
+
async function logsCommand(options) {
|
|
1462
|
+
const dataDir = ".dirigent";
|
|
1463
|
+
const logPath = join9(process.cwd(), dataDir, "logs", "dirigent.log");
|
|
1464
|
+
if (!existsSync9(logPath)) {
|
|
1465
|
+
console.log(chalk6.yellow("\n\u26A0\uFE0F No logs found.\n"));
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
const lines = parseInt(options.lines || "100");
|
|
1469
|
+
if (options.follow) {
|
|
1470
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
1471
|
+
const tail = spawn4("tail", ["-f", "-n", lines.toString(), logPath], {
|
|
1472
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1473
|
+
});
|
|
1474
|
+
tail.stdout.on("data", (data) => {
|
|
1475
|
+
process.stdout.write(formatLog(data.toString()));
|
|
1476
|
+
});
|
|
1477
|
+
tail.stderr.on("data", (data) => {
|
|
1478
|
+
process.stderr.write(data);
|
|
1479
|
+
});
|
|
1480
|
+
process.on("SIGINT", () => {
|
|
1481
|
+
tail.kill();
|
|
1482
|
+
process.exit(0);
|
|
1483
|
+
});
|
|
1484
|
+
} else {
|
|
1485
|
+
const content = readFileSync7(logPath, "utf-8");
|
|
1486
|
+
const allLines = content.trim().split("\n");
|
|
1487
|
+
const lastLines = allLines.slice(-lines);
|
|
1488
|
+
for (const line of lastLines) {
|
|
1489
|
+
console.log(formatLog(line));
|
|
1490
|
+
}
|
|
1491
|
+
console.log(chalk6.dim(`
|
|
1492
|
+
Stream with: dirigent logs -f
|
|
1493
|
+
`));
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
function formatLog(line) {
|
|
1497
|
+
try {
|
|
1498
|
+
const parsed = JSON.parse(line);
|
|
1499
|
+
const time = chalk6.dim(
|
|
1500
|
+
new Date(parsed.time).toISOString().split("T")[1].slice(0, 8)
|
|
1501
|
+
);
|
|
1502
|
+
const level = parsed.level <= 20 ? chalk6.dim("DBG") : parsed.level <= 30 ? chalk6.blue("INF") : parsed.level <= 40 ? chalk6.yellow("WRN") : chalk6.red("ERR");
|
|
1503
|
+
const msg = parsed.msg || "";
|
|
1504
|
+
return `${time} ${level} ${msg}`;
|
|
1505
|
+
} catch {
|
|
1506
|
+
return line;
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/cli/commands/config.ts
|
|
1511
|
+
import chalk7 from "chalk";
|
|
1512
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
1513
|
+
import { join as join10 } from "path";
|
|
1514
|
+
import { spawn as spawn3 } from "child_process";
|
|
1515
|
+
async function configCommand(options) {
|
|
1516
|
+
const dataDir = ".dirigent";
|
|
1517
|
+
const configPath = join10(process.cwd(), dataDir, "config.json");
|
|
1518
|
+
if (!existsSync10(configPath)) {
|
|
1519
|
+
console.log(chalk7.red("\n\u274C Dirigent not initialized. Run `dirigent init` first.\n"));
|
|
1520
|
+
process.exit(1);
|
|
1521
|
+
}
|
|
1522
|
+
const config = JSON.parse(readFileSync8(configPath, "utf-8"));
|
|
1523
|
+
if (options.get) {
|
|
1524
|
+
const value = getNestedValue(config, options.get);
|
|
1525
|
+
if (value === void 0) {
|
|
1526
|
+
console.log(chalk7.yellow(`
|
|
1527
|
+
\u26A0\uFE0F Key not found: ${options.get}
|
|
1528
|
+
`));
|
|
1529
|
+
} else {
|
|
1530
|
+
console.log(typeof value === "object" ? JSON.stringify(value, null, 2) : value);
|
|
1531
|
+
}
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
if (options.set) {
|
|
1535
|
+
const [key, ...valueParts] = options.set.split("=");
|
|
1536
|
+
const value = valueParts.join("=");
|
|
1537
|
+
if (!key || value === void 0) {
|
|
1538
|
+
console.log(chalk7.red("\n\u274C Invalid format. Use: --set key=value\n"));
|
|
1539
|
+
process.exit(1);
|
|
1540
|
+
}
|
|
1541
|
+
let parsedValue = value;
|
|
1542
|
+
if (value === "true") parsedValue = true;
|
|
1543
|
+
else if (value === "false") parsedValue = false;
|
|
1544
|
+
else if (!isNaN(Number(value))) parsedValue = Number(value);
|
|
1545
|
+
else {
|
|
1546
|
+
try {
|
|
1547
|
+
parsedValue = JSON.parse(value);
|
|
1548
|
+
} catch {
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
setNestedValue(config, key, parsedValue);
|
|
1552
|
+
writeFileSync4(configPath, JSON.stringify(config, null, 2));
|
|
1553
|
+
console.log(chalk7.green(`
|
|
1554
|
+
\u2705 Set ${key} = ${JSON.stringify(parsedValue)}
|
|
1555
|
+
`));
|
|
1556
|
+
console.log(chalk7.dim(" Restart Dirigent for changes to take effect: dirigent down && dirigent up\n"));
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (options.edit) {
|
|
1560
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vim";
|
|
1561
|
+
const child = spawn3(editor, [configPath], {
|
|
1562
|
+
stdio: "inherit"
|
|
1563
|
+
});
|
|
1564
|
+
child.on("exit", () => {
|
|
1565
|
+
console.log(chalk7.dim("\n Restart Dirigent for changes to take effect: dirigent down && dirigent up\n"));
|
|
1566
|
+
});
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
console.log(chalk7.cyan("\n\u{1F527} Configuration\n"));
|
|
1570
|
+
console.log(JSON.stringify(config, null, 2));
|
|
1571
|
+
console.log(chalk7.dim(`
|
|
1572
|
+
File: ${configPath}
|
|
1573
|
+
`));
|
|
1574
|
+
}
|
|
1575
|
+
function getNestedValue(obj, path) {
|
|
1576
|
+
return path.split(".").reduce((acc, key) => acc?.[key], obj);
|
|
1577
|
+
}
|
|
1578
|
+
function setNestedValue(obj, path, value) {
|
|
1579
|
+
const keys = path.split(".");
|
|
1580
|
+
const lastKey = keys.pop();
|
|
1581
|
+
const target = keys.reduce((acc, key) => {
|
|
1582
|
+
if (!(key in acc)) acc[key] = {};
|
|
1583
|
+
return acc[key];
|
|
1584
|
+
}, obj);
|
|
1585
|
+
target[lastKey] = value;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/cli/index.ts
|
|
1589
|
+
var version = "0.1.0";
|
|
1590
|
+
var program = new Command();
|
|
1591
|
+
var banner = `
|
|
1592
|
+
${chalk8.bold.cyan("\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\u2557")}
|
|
1593
|
+
${chalk8.bold.cyan("\u2551")} ${chalk8.bold.white("\u{1F3AD} DIRIGENT")} ${chalk8.bold.cyan("\u2551")}
|
|
1594
|
+
${chalk8.bold.cyan("\u2551")} ${chalk8.dim("AI Agent Orchestration Platform")} ${chalk8.bold.cyan("\u2551")}
|
|
1595
|
+
${chalk8.bold.cyan("\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\u255D")}
|
|
1596
|
+
`;
|
|
1597
|
+
program.name("dirigent").description("AI Agent Orchestration Platform - Enterprise-grade orchestration for AI coding agents").version(version).addHelpText("beforeAll", banner);
|
|
1598
|
+
program.command("init").description("Initialize a new Dirigent workspace").option("-y, --yes", "Accept all defaults").option("--port <port>", "Server port", "3000").option("--data-dir <dir>", "Data directory", ".dirigent").action(initCommand);
|
|
1599
|
+
program.command("up").description("Start the Dirigent server").option("-d, --detach", "Run in background").option("--port <port>", "Override server port").action(upCommand);
|
|
1600
|
+
program.command("down").description("Stop the Dirigent server").option("-f, --force", "Force stop all agents").action(downCommand);
|
|
1601
|
+
program.command("status").description("Show server and agent status").option("-j, --json", "Output as JSON").action(statusCommand);
|
|
1602
|
+
var agent = program.command("agent").description("Manage agents");
|
|
1603
|
+
agent.command("list").description("List all agents").option("-a, --all", "Include stopped agents").option("-j, --json", "Output as JSON").action((opts) => agentCommand("list", opts));
|
|
1604
|
+
agent.command("spawn <template>").description("Spawn a new agent from template").option("-n, --name <name>", "Agent name").option("-w, --workspace <path>", "Workspace directory").option("-t, --task <task>", "Initial task").option("--model <model>", "Model override").action((template, opts) => agentCommand("spawn", { template, ...opts }));
|
|
1605
|
+
agent.command("stop <id>").description("Stop an agent").option("-f, --force", "Force stop").action((id, opts) => agentCommand("stop", { id, ...opts }));
|
|
1606
|
+
agent.command("send <id> <message>").description("Send a message to an agent").action((id, message) => agentCommand("send", { id, message }));
|
|
1607
|
+
agent.command("logs <id>").description("Stream agent logs").option("-f, --follow", "Follow log output").option("-n, --lines <n>", "Number of lines", "50").action((id, opts) => agentCommand("logs", { id, ...opts }));
|
|
1608
|
+
program.command("logs").description("View server logs").option("-f, --follow", "Follow log output").option("-n, --lines <n>", "Number of lines", "100").action(logsCommand);
|
|
1609
|
+
program.command("config").description("View or edit configuration").option("--get <key>", "Get a config value").option("--set <key=value>", "Set a config value").option("--list", "List all config").option("--edit", "Open config in editor").action(configCommand);
|
|
1610
|
+
program.command("dashboard").alias("ui").description("Open the web dashboard").option("-p, --port <port>", "Dashboard port").action(async () => {
|
|
1611
|
+
const config = await Promise.resolve().then(() => (init_config(), config_exports)).then((m) => m.loadConfig());
|
|
1612
|
+
console.log(chalk8.cyan(`
|
|
1613
|
+
\u{1F310} Dashboard: ${chalk8.bold(`http://localhost:${config.server.port}`)}
|
|
1614
|
+
`));
|
|
1615
|
+
});
|
|
1616
|
+
program.parse();
|
|
1617
|
+
if (!process.argv.slice(2).length) {
|
|
1618
|
+
console.log(banner);
|
|
1619
|
+
program.outputHelp();
|
|
1620
|
+
}
|
|
1621
|
+
//# sourceMappingURL=index.js.map
|