claude-code-controller 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1588 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/api/index.ts
21
+ var api_exports = {};
22
+ __export(api_exports, {
23
+ ActionTracker: () => ActionTracker,
24
+ createApi: () => createApi
25
+ });
26
+ module.exports = __toCommonJS(api_exports);
27
+ var import_hono2 = require("hono");
28
+ var import_cors = require("hono/cors");
29
+
30
+ // src/api/action-tracker.ts
31
+ var ActionTracker = class {
32
+ approvals = /* @__PURE__ */ new Map();
33
+ idles = /* @__PURE__ */ new Map();
34
+ agentTypes = /* @__PURE__ */ new Map();
35
+ listeners = [];
36
+ currentController = null;
37
+ attach(controller) {
38
+ this.detach();
39
+ this.currentController = controller;
40
+ const onPlan = (agent, parsed) => {
41
+ this.approvals.set(parsed.requestId, {
42
+ type: "plan",
43
+ agent,
44
+ requestId: parsed.requestId,
45
+ timestamp: parsed.timestamp,
46
+ planContent: parsed.planContent,
47
+ action: `POST /agents/${agent}/approve-plan`
48
+ });
49
+ };
50
+ const onPermission = (agent, parsed) => {
51
+ this.approvals.set(parsed.requestId, {
52
+ type: "permission",
53
+ agent,
54
+ requestId: parsed.requestId,
55
+ timestamp: parsed.timestamp,
56
+ toolName: parsed.toolName,
57
+ description: parsed.description,
58
+ action: `POST /agents/${agent}/approve-permission`
59
+ });
60
+ };
61
+ const onIdle = (agent) => {
62
+ this.idles.set(agent, {
63
+ name: agent,
64
+ type: this.agentTypes.get(agent) ?? "unknown",
65
+ idleSince: (/* @__PURE__ */ new Date()).toISOString(),
66
+ action: `POST /agents/${agent}/messages`
67
+ });
68
+ };
69
+ const onMessage = (agent) => {
70
+ this.idles.delete(agent);
71
+ };
72
+ const onSpawned = (agent) => {
73
+ this.idles.delete(agent);
74
+ };
75
+ const onExited = (agent) => {
76
+ this.idles.delete(agent);
77
+ for (const [id, approval] of this.approvals) {
78
+ if (approval.agent === agent) {
79
+ this.approvals.delete(id);
80
+ }
81
+ }
82
+ };
83
+ controller.on("plan:approval_request", onPlan);
84
+ controller.on("permission:request", onPermission);
85
+ controller.on("idle", onIdle);
86
+ controller.on("message", onMessage);
87
+ controller.on("agent:spawned", onSpawned);
88
+ controller.on("agent:exited", onExited);
89
+ this.listeners = [
90
+ { event: "plan:approval_request", fn: onPlan },
91
+ { event: "permission:request", fn: onPermission },
92
+ { event: "idle", fn: onIdle },
93
+ { event: "message", fn: onMessage },
94
+ { event: "agent:spawned", fn: onSpawned },
95
+ { event: "agent:exited", fn: onExited }
96
+ ];
97
+ }
98
+ /** Remove all event listeners from the current controller. */
99
+ detach() {
100
+ if (this.currentController) {
101
+ for (const { event, fn } of this.listeners) {
102
+ this.currentController.removeListener(event, fn);
103
+ }
104
+ }
105
+ this.listeners = [];
106
+ this.currentController = null;
107
+ }
108
+ /** Track agent type so idle entries have the right type. */
109
+ registerAgentType(name, type) {
110
+ this.agentTypes.set(name, type);
111
+ }
112
+ resolveApproval(requestId) {
113
+ this.approvals.delete(requestId);
114
+ }
115
+ getPendingApprovals() {
116
+ return Array.from(this.approvals.values());
117
+ }
118
+ getIdleAgents() {
119
+ return Array.from(this.idles.values());
120
+ }
121
+ /** Clear all tracked state AND detach from the controller. */
122
+ clear() {
123
+ this.detach();
124
+ this.approvals.clear();
125
+ this.idles.clear();
126
+ this.agentTypes.clear();
127
+ }
128
+ };
129
+
130
+ // src/api/routes.ts
131
+ var import_hono = require("hono");
132
+
133
+ // src/controller.ts
134
+ var import_node_events = require("events");
135
+ var import_node_child_process3 = require("child_process");
136
+ var import_node_crypto2 = require("crypto");
137
+
138
+ // src/team-manager.ts
139
+ var import_promises = require("fs/promises");
140
+ var import_node_fs = require("fs");
141
+ var import_node_crypto = require("crypto");
142
+
143
+ // src/paths.ts
144
+ var import_node_os = require("os");
145
+ var import_node_path = require("path");
146
+ var CLAUDE_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".claude");
147
+ function teamsDir() {
148
+ return (0, import_node_path.join)(CLAUDE_DIR, "teams");
149
+ }
150
+ function teamDir(teamName) {
151
+ return (0, import_node_path.join)(teamsDir(), teamName);
152
+ }
153
+ function teamConfigPath(teamName) {
154
+ return (0, import_node_path.join)(teamDir(teamName), "config.json");
155
+ }
156
+ function inboxesDir(teamName) {
157
+ return (0, import_node_path.join)(teamDir(teamName), "inboxes");
158
+ }
159
+ function inboxPath(teamName, agentName) {
160
+ return (0, import_node_path.join)(inboxesDir(teamName), `${agentName}.json`);
161
+ }
162
+ function tasksBaseDir() {
163
+ return (0, import_node_path.join)(CLAUDE_DIR, "tasks");
164
+ }
165
+ function tasksDir(teamName) {
166
+ return (0, import_node_path.join)(tasksBaseDir(), teamName);
167
+ }
168
+ function taskPath(teamName, taskId) {
169
+ return (0, import_node_path.join)(tasksDir(teamName), `${taskId}.json`);
170
+ }
171
+
172
+ // src/team-manager.ts
173
+ var TeamManager = class {
174
+ teamName;
175
+ sessionId;
176
+ log;
177
+ constructor(teamName, logger) {
178
+ this.teamName = teamName;
179
+ this.sessionId = (0, import_node_crypto.randomUUID)();
180
+ this.log = logger;
181
+ }
182
+ /**
183
+ * Create the team directory structure and config.json.
184
+ * The controller registers itself as the lead member.
185
+ */
186
+ async create(opts) {
187
+ const dir = teamDir(this.teamName);
188
+ const inboxDir = inboxesDir(this.teamName);
189
+ const taskDir = tasksDir(this.teamName);
190
+ await (0, import_promises.mkdir)(dir, { recursive: true });
191
+ await (0, import_promises.mkdir)(inboxDir, { recursive: true });
192
+ await (0, import_promises.mkdir)(taskDir, { recursive: true });
193
+ const leadName = "controller";
194
+ const leadAgentId = `${leadName}@${this.teamName}`;
195
+ const config = {
196
+ name: this.teamName,
197
+ description: opts?.description,
198
+ createdAt: Date.now(),
199
+ leadAgentId,
200
+ leadSessionId: this.sessionId,
201
+ members: [
202
+ {
203
+ agentId: leadAgentId,
204
+ name: leadName,
205
+ agentType: "controller",
206
+ joinedAt: Date.now(),
207
+ tmuxPaneId: "",
208
+ cwd: opts?.cwd || process.cwd(),
209
+ subscriptions: []
210
+ }
211
+ ]
212
+ };
213
+ await this.writeConfig(config);
214
+ this.log.info(`Team "${this.teamName}" created`);
215
+ return config;
216
+ }
217
+ /**
218
+ * Add a member to the team config.
219
+ */
220
+ async addMember(member) {
221
+ const config = await this.getConfig();
222
+ config.members = config.members.filter((m) => m.name !== member.name);
223
+ config.members.push(member);
224
+ await this.writeConfig(config);
225
+ this.log.debug(`Added member "${member.name}" to team`);
226
+ }
227
+ /**
228
+ * Remove a member from the team config.
229
+ */
230
+ async removeMember(name) {
231
+ const config = await this.getConfig();
232
+ config.members = config.members.filter((m) => m.name !== name);
233
+ await this.writeConfig(config);
234
+ this.log.debug(`Removed member "${name}" from team`);
235
+ }
236
+ /**
237
+ * Read the current team config.
238
+ */
239
+ async getConfig() {
240
+ const path = teamConfigPath(this.teamName);
241
+ if (!(0, import_node_fs.existsSync)(path)) {
242
+ throw new Error(
243
+ `Team "${this.teamName}" does not exist (no config.json)`
244
+ );
245
+ }
246
+ const raw = await (0, import_promises.readFile)(path, "utf-8");
247
+ return JSON.parse(raw);
248
+ }
249
+ /**
250
+ * Check if the team already exists on disk.
251
+ */
252
+ exists() {
253
+ return (0, import_node_fs.existsSync)(teamConfigPath(this.teamName));
254
+ }
255
+ /**
256
+ * Destroy the team: remove all team directories and task directories.
257
+ */
258
+ async destroy() {
259
+ const dir = teamDir(this.teamName);
260
+ const taskDir = tasksDir(this.teamName);
261
+ if ((0, import_node_fs.existsSync)(dir)) {
262
+ await (0, import_promises.rm)(dir, { recursive: true, force: true });
263
+ }
264
+ if ((0, import_node_fs.existsSync)(taskDir)) {
265
+ await (0, import_promises.rm)(taskDir, { recursive: true, force: true });
266
+ }
267
+ this.log.info(`Team "${this.teamName}" destroyed`);
268
+ }
269
+ async writeConfig(config) {
270
+ const path = teamConfigPath(this.teamName);
271
+ await (0, import_promises.writeFile)(path, JSON.stringify(config, null, 2), "utf-8");
272
+ }
273
+ };
274
+
275
+ // src/task-manager.ts
276
+ var import_promises2 = require("fs/promises");
277
+ var import_node_fs2 = require("fs");
278
+ var TaskManager = class {
279
+ constructor(teamName, log) {
280
+ this.teamName = teamName;
281
+ this.log = log;
282
+ }
283
+ nextId = 1;
284
+ /**
285
+ * Initialize the task directory. Call after team creation.
286
+ * Also scans for existing tasks to set the next ID correctly.
287
+ */
288
+ async init() {
289
+ const dir = tasksDir(this.teamName);
290
+ await (0, import_promises2.mkdir)(dir, { recursive: true });
291
+ const existing = await this.list();
292
+ if (existing.length > 0) {
293
+ const maxId = Math.max(...existing.map((t) => parseInt(t.id, 10)));
294
+ this.nextId = maxId + 1;
295
+ }
296
+ }
297
+ /**
298
+ * Create a new task. Returns the assigned task ID.
299
+ */
300
+ async create(task) {
301
+ const id = String(this.nextId++);
302
+ const full = {
303
+ id,
304
+ subject: task.subject,
305
+ description: task.description,
306
+ activeForm: task.activeForm,
307
+ owner: task.owner,
308
+ status: task.status || "pending",
309
+ blocks: task.blocks || [],
310
+ blockedBy: task.blockedBy || [],
311
+ metadata: task.metadata
312
+ };
313
+ await this.writeTask(full);
314
+ this.log.debug(`Created task #${id}: ${task.subject}`);
315
+ return id;
316
+ }
317
+ /**
318
+ * Get a task by ID.
319
+ */
320
+ async get(taskId) {
321
+ const path = taskPath(this.teamName, taskId);
322
+ if (!(0, import_node_fs2.existsSync)(path)) {
323
+ throw new Error(`Task #${taskId} not found`);
324
+ }
325
+ const raw = await (0, import_promises2.readFile)(path, "utf-8");
326
+ return JSON.parse(raw);
327
+ }
328
+ /**
329
+ * Update a task. Merges the provided fields.
330
+ */
331
+ async update(taskId, updates) {
332
+ const task = await this.get(taskId);
333
+ Object.assign(task, updates);
334
+ await this.writeTask(task);
335
+ this.log.debug(`Updated task #${taskId}: status=${task.status}`);
336
+ return task;
337
+ }
338
+ /**
339
+ * Add blocking relationships.
340
+ */
341
+ async addBlocks(taskId, blockedTaskIds) {
342
+ const task = await this.get(taskId);
343
+ const toAdd = blockedTaskIds.filter((id) => !task.blocks.includes(id));
344
+ task.blocks.push(...toAdd);
345
+ await this.writeTask(task);
346
+ for (const blockedId of toAdd) {
347
+ const blocked = await this.get(blockedId);
348
+ if (!blocked.blockedBy.includes(taskId)) {
349
+ blocked.blockedBy.push(taskId);
350
+ await this.writeTask(blocked);
351
+ }
352
+ }
353
+ }
354
+ /**
355
+ * List all tasks.
356
+ */
357
+ async list() {
358
+ const dir = tasksDir(this.teamName);
359
+ if (!(0, import_node_fs2.existsSync)(dir)) return [];
360
+ const files = await (0, import_promises2.readdir)(dir);
361
+ const tasks = [];
362
+ for (const file of files) {
363
+ if (!file.endsWith(".json")) continue;
364
+ const raw = await (0, import_promises2.readFile)(taskPath(this.teamName, file.replace(".json", "")), "utf-8");
365
+ tasks.push(JSON.parse(raw));
366
+ }
367
+ return tasks.sort((a, b) => parseInt(a.id) - parseInt(b.id));
368
+ }
369
+ /**
370
+ * Delete a task file.
371
+ */
372
+ async delete(taskId) {
373
+ const path = taskPath(this.teamName, taskId);
374
+ if ((0, import_node_fs2.existsSync)(path)) {
375
+ await (0, import_promises2.rm)(path);
376
+ this.log.debug(`Deleted task #${taskId}`);
377
+ }
378
+ }
379
+ /**
380
+ * Wait for a task to reach a target status.
381
+ */
382
+ async waitFor(taskId, targetStatus = "completed", opts) {
383
+ const timeout = opts?.timeout ?? 3e5;
384
+ const interval = opts?.pollInterval ?? 1e3;
385
+ const deadline = Date.now() + timeout;
386
+ while (Date.now() < deadline) {
387
+ const task = await this.get(taskId);
388
+ if (task.status === targetStatus) return task;
389
+ await sleep(interval);
390
+ }
391
+ throw new Error(
392
+ `Timeout waiting for task #${taskId} to reach "${targetStatus}"`
393
+ );
394
+ }
395
+ async writeTask(task) {
396
+ const path = taskPath(this.teamName, task.id);
397
+ await (0, import_promises2.writeFile)(path, JSON.stringify(task, null, 4), "utf-8");
398
+ }
399
+ };
400
+ function sleep(ms) {
401
+ return new Promise((r) => setTimeout(r, ms));
402
+ }
403
+
404
+ // src/process-manager.ts
405
+ var import_node_child_process = require("child_process");
406
+ var import_node_child_process2 = require("child_process");
407
+ var ProcessManager = class {
408
+ processes = /* @__PURE__ */ new Map();
409
+ log;
410
+ constructor(logger) {
411
+ this.log = logger;
412
+ }
413
+ /**
414
+ * Spawn a new claude CLI process in teammate mode.
415
+ * Uses a Python PTY wrapper to provide the terminal the TUI needs.
416
+ */
417
+ spawn(opts) {
418
+ let binary = opts.claudeBinary || "claude";
419
+ if (!binary.startsWith("/")) {
420
+ try {
421
+ binary = (0, import_node_child_process2.execSync)(`which ${binary}`, { encoding: "utf-8" }).trim();
422
+ } catch {
423
+ }
424
+ }
425
+ const claudeArgs = [
426
+ "--teammate-mode",
427
+ opts.teammateMode || "auto",
428
+ "--agent-id",
429
+ opts.agentId,
430
+ "--agent-name",
431
+ opts.agentName,
432
+ "--team-name",
433
+ opts.teamName
434
+ ];
435
+ if (opts.agentType) {
436
+ claudeArgs.push("--agent-type", opts.agentType);
437
+ }
438
+ if (opts.color) {
439
+ claudeArgs.push("--agent-color", opts.color);
440
+ }
441
+ if (opts.parentSessionId) {
442
+ claudeArgs.push("--parent-session-id", opts.parentSessionId);
443
+ }
444
+ if (opts.model) {
445
+ claudeArgs.push("--model", opts.model);
446
+ }
447
+ if (opts.permissions) {
448
+ for (const perm of opts.permissions) {
449
+ claudeArgs.push("--allowedTools", perm);
450
+ }
451
+ }
452
+ this.log.info(
453
+ `Spawning agent "${opts.agentName}": ${binary} ${claudeArgs.join(" ")}`
454
+ );
455
+ const cmdJson = JSON.stringify([binary, ...claudeArgs]);
456
+ const pythonScript = `
457
+ import pty, os, sys, json, signal, select
458
+
459
+ cmd = json.loads(sys.argv[1])
460
+ pid, fd = pty.fork()
461
+ if pid == 0:
462
+ os.execvp(cmd[0], cmd)
463
+ else:
464
+ signal.signal(signal.SIGTERM, lambda *a: (os.kill(pid, signal.SIGTERM), sys.exit(0)))
465
+ signal.signal(signal.SIGINT, lambda *a: (os.kill(pid, signal.SIGTERM), sys.exit(0)))
466
+ try:
467
+ while True:
468
+ r, _, _ = select.select([fd, 0], [], [], 1.0)
469
+ if fd in r:
470
+ try:
471
+ data = os.read(fd, 4096)
472
+ if not data:
473
+ break
474
+ os.write(1, data)
475
+ except OSError:
476
+ break
477
+ if 0 in r:
478
+ try:
479
+ data = os.read(0, 4096)
480
+ if not data:
481
+ break
482
+ os.write(fd, data)
483
+ except OSError:
484
+ break
485
+ except:
486
+ pass
487
+ finally:
488
+ try:
489
+ os.kill(pid, signal.SIGTERM)
490
+ except:
491
+ pass
492
+ _, status = os.waitpid(pid, 0)
493
+ sys.exit(os.WEXITSTATUS(status) if os.WIFEXITED(status) else 1)
494
+ `;
495
+ const proc = (0, import_node_child_process.spawn)("python3", ["-c", pythonScript, cmdJson], {
496
+ cwd: opts.cwd || process.cwd(),
497
+ stdio: ["pipe", "pipe", "pipe"],
498
+ env: {
499
+ ...process.env,
500
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1",
501
+ ...opts.env
502
+ }
503
+ });
504
+ this.processes.set(opts.agentName, proc);
505
+ proc.on("exit", (code, signal) => {
506
+ this.log.info(
507
+ `Agent "${opts.agentName}" exited (code=${code}, signal=${signal})`
508
+ );
509
+ this.processes.delete(opts.agentName);
510
+ });
511
+ proc.on("error", (err) => {
512
+ this.log.error(`Agent "${opts.agentName}" process error: ${err.message}`);
513
+ this.processes.delete(opts.agentName);
514
+ });
515
+ return proc;
516
+ }
517
+ /**
518
+ * Register a callback for when an agent process exits.
519
+ */
520
+ onExit(name, callback) {
521
+ const proc = this.processes.get(name);
522
+ if (proc) proc.on("exit", (code) => callback(code));
523
+ }
524
+ /**
525
+ * Get the process for a named agent.
526
+ */
527
+ get(name) {
528
+ return this.processes.get(name);
529
+ }
530
+ /**
531
+ * Check if an agent process is still running.
532
+ */
533
+ isRunning(name) {
534
+ const proc = this.processes.get(name);
535
+ return proc !== void 0 && proc.exitCode === null && !proc.killed;
536
+ }
537
+ /**
538
+ * Get the PID of a running agent.
539
+ */
540
+ getPid(name) {
541
+ return this.processes.get(name)?.pid;
542
+ }
543
+ /**
544
+ * Kill a specific agent process.
545
+ */
546
+ async kill(name, signal = "SIGTERM") {
547
+ const proc = this.processes.get(name);
548
+ if (!proc) return;
549
+ proc.kill(signal);
550
+ await Promise.race([
551
+ new Promise((resolve) => proc.on("exit", () => resolve())),
552
+ new Promise(
553
+ (resolve) => setTimeout(() => {
554
+ if (this.processes.has(name)) {
555
+ this.log.warn(`Force-killing agent "${name}" with SIGKILL`);
556
+ try {
557
+ proc.kill("SIGKILL");
558
+ } catch {
559
+ }
560
+ }
561
+ resolve();
562
+ }, 5e3)
563
+ )
564
+ ]);
565
+ this.processes.delete(name);
566
+ }
567
+ /**
568
+ * Kill all agent processes.
569
+ */
570
+ async killAll() {
571
+ const names = [...this.processes.keys()];
572
+ await Promise.all(names.map((name) => this.kill(name)));
573
+ }
574
+ /**
575
+ * Get all running agent names.
576
+ */
577
+ runningAgents() {
578
+ return [...this.processes.keys()].filter((n) => this.isRunning(n));
579
+ }
580
+ };
581
+
582
+ // src/inbox.ts
583
+ var import_promises3 = require("fs/promises");
584
+ var import_node_fs3 = require("fs");
585
+ var import_node_path2 = require("path");
586
+ var import_proper_lockfile = require("proper-lockfile");
587
+ var LOCK_OPTIONS = {
588
+ retries: { retries: 5, minTimeout: 50, maxTimeout: 500 },
589
+ stale: 1e4
590
+ };
591
+ async function ensureDir(filePath) {
592
+ const dir = (0, import_node_path2.dirname)(filePath);
593
+ if (!(0, import_node_fs3.existsSync)(dir)) {
594
+ await (0, import_promises3.mkdir)(dir, { recursive: true });
595
+ }
596
+ }
597
+ async function ensureFile(filePath) {
598
+ await ensureDir(filePath);
599
+ if (!(0, import_node_fs3.existsSync)(filePath)) {
600
+ await (0, import_promises3.writeFile)(filePath, "[]", "utf-8");
601
+ }
602
+ }
603
+ async function writeInbox(teamName, agentName, message, logger) {
604
+ const path = inboxPath(teamName, agentName);
605
+ await ensureFile(path);
606
+ let release;
607
+ try {
608
+ release = await (0, import_proper_lockfile.lock)(path, LOCK_OPTIONS);
609
+ const raw = await (0, import_promises3.readFile)(path, "utf-8");
610
+ const messages = JSON.parse(raw || "[]");
611
+ messages.push({ ...message, read: false });
612
+ await (0, import_promises3.writeFile)(path, JSON.stringify(messages, null, 2), "utf-8");
613
+ logger?.debug(`Wrote message to inbox ${agentName}`, message.from);
614
+ } finally {
615
+ if (release) await release();
616
+ }
617
+ }
618
+ async function readUnread(teamName, agentName) {
619
+ const path = inboxPath(teamName, agentName);
620
+ if (!(0, import_node_fs3.existsSync)(path)) return [];
621
+ let release;
622
+ try {
623
+ release = await (0, import_proper_lockfile.lock)(path, LOCK_OPTIONS);
624
+ const raw = await (0, import_promises3.readFile)(path, "utf-8");
625
+ const messages = JSON.parse(raw || "[]");
626
+ const unread = messages.filter((m) => !m.read);
627
+ if (unread.length === 0) return [];
628
+ for (const m of messages) {
629
+ m.read = true;
630
+ }
631
+ await (0, import_promises3.writeFile)(path, JSON.stringify(messages, null, 2), "utf-8");
632
+ return unread;
633
+ } finally {
634
+ if (release) await release();
635
+ }
636
+ }
637
+ function parseMessage(msg) {
638
+ try {
639
+ const parsed = JSON.parse(msg.text);
640
+ if (parsed && typeof parsed === "object" && "type" in parsed) {
641
+ return parsed;
642
+ }
643
+ } catch {
644
+ }
645
+ return { type: "plain_text", text: msg.text };
646
+ }
647
+
648
+ // src/inbox-poller.ts
649
+ var InboxPoller = class {
650
+ teamName;
651
+ agentName;
652
+ interval;
653
+ timer = null;
654
+ log;
655
+ handlers = [];
656
+ constructor(teamName, agentName, logger, opts) {
657
+ this.teamName = teamName;
658
+ this.agentName = agentName;
659
+ this.log = logger;
660
+ this.interval = opts?.pollInterval ?? 500;
661
+ }
662
+ /**
663
+ * Register a handler for new messages.
664
+ */
665
+ onMessages(handler) {
666
+ this.handlers.push(handler);
667
+ }
668
+ /**
669
+ * Start polling.
670
+ */
671
+ start() {
672
+ if (this.timer) return;
673
+ this.log.debug(
674
+ `Starting inbox poller for "${this.agentName}" (interval=${this.interval}ms)`
675
+ );
676
+ this.timer = setInterval(() => this.poll(), this.interval);
677
+ }
678
+ /**
679
+ * Stop polling.
680
+ */
681
+ stop() {
682
+ if (this.timer) {
683
+ clearInterval(this.timer);
684
+ this.timer = null;
685
+ this.log.debug(`Stopped inbox poller for "${this.agentName}"`);
686
+ }
687
+ }
688
+ /**
689
+ * Poll once for new messages.
690
+ */
691
+ async poll() {
692
+ try {
693
+ const unread = await readUnread(this.teamName, this.agentName);
694
+ if (unread.length === 0) return [];
695
+ const events = unread.map((raw) => ({
696
+ raw,
697
+ parsed: parseMessage(raw)
698
+ }));
699
+ for (const handler of this.handlers) {
700
+ try {
701
+ handler(events);
702
+ } catch (err) {
703
+ this.log.error("Inbox handler error:", String(err));
704
+ }
705
+ }
706
+ return events;
707
+ } catch (err) {
708
+ this.log.error("Inbox poll error:", String(err));
709
+ return [];
710
+ }
711
+ }
712
+ };
713
+
714
+ // src/agent-handle.ts
715
+ var AgentHandle = class {
716
+ name;
717
+ pid;
718
+ controller;
719
+ constructor(controller, name, pid) {
720
+ this.controller = controller;
721
+ this.name = name;
722
+ this.pid = pid;
723
+ }
724
+ /**
725
+ * Send a message to this agent.
726
+ */
727
+ async send(message, summary) {
728
+ return this.controller.send(this.name, message, summary);
729
+ }
730
+ /**
731
+ * Wait for a response from this agent.
732
+ * Returns the text of the first unread plain-text message.
733
+ */
734
+ async receive(opts) {
735
+ const messages = await this.controller.receive(this.name, opts);
736
+ const texts = messages.map((m) => m.text);
737
+ return texts.join("\n");
738
+ }
739
+ /**
740
+ * Send a message and wait for the response. Convenience method.
741
+ */
742
+ async ask(question, opts) {
743
+ await this.send(question);
744
+ return this.receive(opts);
745
+ }
746
+ /**
747
+ * Check if the agent process is still running.
748
+ */
749
+ get isRunning() {
750
+ return this.controller.isAgentRunning(this.name);
751
+ }
752
+ /**
753
+ * Request the agent to shut down gracefully.
754
+ */
755
+ async shutdown() {
756
+ return this.controller.sendShutdownRequest(this.name);
757
+ }
758
+ /**
759
+ * Force-kill the agent process.
760
+ */
761
+ async kill() {
762
+ return this.controller.killAgent(this.name);
763
+ }
764
+ /**
765
+ * Async iterator for agent events (messages from this agent).
766
+ * Polls the controller's inbox for messages from this agent.
767
+ */
768
+ async *events(opts) {
769
+ const interval = opts?.pollInterval ?? 500;
770
+ const timeout = opts?.timeout ?? 0;
771
+ const deadline = timeout > 0 ? Date.now() + timeout : Infinity;
772
+ while (Date.now() < deadline) {
773
+ try {
774
+ const messages = await this.controller.receive(this.name, {
775
+ timeout: interval,
776
+ pollInterval: interval
777
+ });
778
+ for (const msg of messages) {
779
+ yield msg;
780
+ }
781
+ } catch {
782
+ }
783
+ if (!this.isRunning) return;
784
+ }
785
+ }
786
+ };
787
+
788
+ // src/logger.ts
789
+ var LEVELS = {
790
+ debug: 0,
791
+ info: 1,
792
+ warn: 2,
793
+ error: 3,
794
+ silent: 4
795
+ };
796
+ function createLogger(level = "info") {
797
+ const threshold = LEVELS[level];
798
+ const noop = () => {
799
+ };
800
+ const make = (lvl, fn) => (msg, ...args) => {
801
+ if (LEVELS[lvl] >= threshold) {
802
+ fn(`[claude-ctrl:${lvl}]`, msg, ...args);
803
+ }
804
+ };
805
+ return {
806
+ debug: threshold <= LEVELS.debug ? make("debug", console.debug) : noop,
807
+ info: threshold <= LEVELS.info ? make("info", console.info) : noop,
808
+ warn: threshold <= LEVELS.warn ? make("warn", console.warn) : noop,
809
+ error: threshold <= LEVELS.error ? make("error", console.error) : noop
810
+ };
811
+ }
812
+
813
+ // src/controller.ts
814
+ var AGENT_COLORS = [
815
+ "#00FF00",
816
+ "#00BFFF",
817
+ "#FF6347",
818
+ "#FFD700",
819
+ "#DA70D6",
820
+ "#40E0D0",
821
+ "#FF69B4",
822
+ "#7B68EE"
823
+ ];
824
+ var ClaudeCodeController = class extends import_node_events.EventEmitter {
825
+ teamName;
826
+ team;
827
+ tasks;
828
+ processes;
829
+ poller;
830
+ log;
831
+ cwd;
832
+ claudeBinary;
833
+ defaultEnv;
834
+ colorIndex = 0;
835
+ initialized = false;
836
+ constructor(opts) {
837
+ super();
838
+ this.teamName = opts?.teamName || `ctrl-${(0, import_node_crypto2.randomUUID)().slice(0, 8)}`;
839
+ this.cwd = opts?.cwd || process.cwd();
840
+ this.claudeBinary = opts?.claudeBinary || "claude";
841
+ this.defaultEnv = opts?.env || {};
842
+ this.log = opts?.logger || createLogger(opts?.logLevel ?? "info");
843
+ this.team = new TeamManager(this.teamName, this.log);
844
+ this.tasks = new TaskManager(this.teamName, this.log);
845
+ this.processes = new ProcessManager(this.log);
846
+ this.poller = new InboxPoller(
847
+ this.teamName,
848
+ "controller",
849
+ this.log
850
+ );
851
+ this.poller.onMessages((events) => this.handlePollEvents(events));
852
+ }
853
+ // ─── Lifecycle ───────────────────────────────────────────────────────
854
+ /**
855
+ * Initialize the controller: create the team and start polling.
856
+ * Must be called before any other operations.
857
+ */
858
+ async init() {
859
+ if (this.initialized) return this;
860
+ await this.team.create({ cwd: this.cwd });
861
+ await this.tasks.init();
862
+ this.poller.start();
863
+ this.initialized = true;
864
+ this.log.info(
865
+ `Controller initialized (team="${this.teamName}")`
866
+ );
867
+ return this;
868
+ }
869
+ /**
870
+ * Graceful shutdown:
871
+ * 1. Send shutdown requests to all agents
872
+ * 2. Wait briefly for acknowledgment
873
+ * 3. Kill remaining processes
874
+ * 4. Clean up team files
875
+ */
876
+ async shutdown() {
877
+ this.log.info("Shutting down controller...");
878
+ const running = this.processes.runningAgents();
879
+ const shutdownPromises = [];
880
+ for (const name of running) {
881
+ try {
882
+ await this.sendShutdownRequest(name);
883
+ shutdownPromises.push(
884
+ new Promise((resolve) => {
885
+ const proc = this.processes.get(name);
886
+ if (!proc) return resolve();
887
+ const timer = setTimeout(() => resolve(), 1e4);
888
+ proc.on("exit", () => {
889
+ clearTimeout(timer);
890
+ resolve();
891
+ });
892
+ })
893
+ );
894
+ } catch {
895
+ }
896
+ }
897
+ if (shutdownPromises.length > 0) {
898
+ await Promise.all(shutdownPromises);
899
+ }
900
+ await this.processes.killAll();
901
+ this.poller.stop();
902
+ await this.team.destroy();
903
+ this.initialized = false;
904
+ this.log.info("Controller shut down");
905
+ }
906
+ // ─── Agent Management ────────────────────────────────────────────────
907
+ /**
908
+ * Spawn a new Claude Code agent.
909
+ */
910
+ async spawnAgent(opts) {
911
+ this.ensureInitialized();
912
+ const agentId = `${opts.name}@${this.teamName}`;
913
+ const color = AGENT_COLORS[this.colorIndex++ % AGENT_COLORS.length];
914
+ const cwd = opts.cwd || this.cwd;
915
+ const member = {
916
+ agentId,
917
+ name: opts.name,
918
+ agentType: opts.type || "general-purpose",
919
+ model: opts.model,
920
+ joinedAt: Date.now(),
921
+ tmuxPaneId: "",
922
+ cwd,
923
+ subscriptions: []
924
+ };
925
+ await this.team.addMember(member);
926
+ const env = Object.keys(this.defaultEnv).length > 0 || opts.env ? { ...this.defaultEnv, ...opts.env } : void 0;
927
+ const proc = this.processes.spawn({
928
+ teamName: this.teamName,
929
+ agentName: opts.name,
930
+ agentId,
931
+ agentType: opts.type || "general-purpose",
932
+ model: opts.model,
933
+ cwd,
934
+ parentSessionId: this.team.sessionId,
935
+ color,
936
+ claudeBinary: this.claudeBinary,
937
+ permissions: opts.permissions,
938
+ env
939
+ });
940
+ this.emit("agent:spawned", opts.name, proc.pid ?? 0);
941
+ this.processes.onExit(opts.name, (code) => {
942
+ this.emit("agent:exited", opts.name, code ?? null);
943
+ });
944
+ return new AgentHandle(this, opts.name, proc.pid);
945
+ }
946
+ // ─── Messaging ───────────────────────────────────────────────────────
947
+ /**
948
+ * Send a message to a specific agent.
949
+ */
950
+ async send(agentName, message, summary) {
951
+ this.ensureInitialized();
952
+ await writeInbox(
953
+ this.teamName,
954
+ agentName,
955
+ {
956
+ from: "controller",
957
+ text: message,
958
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
959
+ summary
960
+ },
961
+ this.log
962
+ );
963
+ }
964
+ /**
965
+ * Send a structured shutdown request to an agent.
966
+ */
967
+ async sendShutdownRequest(agentName) {
968
+ const requestId = `shutdown-${Date.now()}@${agentName}`;
969
+ const msg = JSON.stringify({
970
+ type: "shutdown_request",
971
+ requestId,
972
+ from: "controller",
973
+ reason: "Controller shutdown requested",
974
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
975
+ });
976
+ await this.send(agentName, msg);
977
+ }
978
+ /**
979
+ * Broadcast a message to all registered agents (except controller).
980
+ */
981
+ async broadcast(message, summary) {
982
+ this.ensureInitialized();
983
+ const config = await this.team.getConfig();
984
+ const agents = config.members.filter((m) => m.name !== "controller");
985
+ await Promise.all(
986
+ agents.map((a) => this.send(a.name, message, summary))
987
+ );
988
+ }
989
+ /**
990
+ * Wait for messages from a specific agent.
991
+ * Polls the controller's inbox for messages from the given agent.
992
+ *
993
+ * Returns when:
994
+ * - A non-idle message is received (SendMessage from agent), OR
995
+ * - An idle_notification is received (agent finished its turn),
996
+ * in which case the idle message is returned.
997
+ */
998
+ async receive(agentName, opts) {
999
+ const timeout = opts?.timeout ?? 6e4;
1000
+ const interval = opts?.pollInterval ?? 500;
1001
+ const deadline = Date.now() + timeout;
1002
+ while (Date.now() < deadline) {
1003
+ const unread = await readUnread(this.teamName, "controller");
1004
+ const fromAgent = unread.filter((m) => m.from === agentName);
1005
+ if (fromAgent.length > 0) {
1006
+ const PROTOCOL_TYPES = /* @__PURE__ */ new Set([
1007
+ "shutdown_approved",
1008
+ "plan_approval_response",
1009
+ "permission_response"
1010
+ ]);
1011
+ const meaningful = fromAgent.filter((m) => {
1012
+ const parsed = parseMessage(m);
1013
+ return parsed.type !== "idle_notification" && !PROTOCOL_TYPES.has(parsed.type);
1014
+ });
1015
+ if (meaningful.length > 0) {
1016
+ return opts?.all ? meaningful : [meaningful[0]];
1017
+ }
1018
+ const idles = fromAgent.filter((m) => {
1019
+ const parsed = parseMessage(m);
1020
+ return parsed.type === "idle_notification";
1021
+ });
1022
+ if (idles.length > 0) {
1023
+ return opts?.all ? idles : [idles[0]];
1024
+ }
1025
+ }
1026
+ await sleep2(interval);
1027
+ }
1028
+ throw new Error(
1029
+ `Timeout (${timeout}ms) waiting for message from "${agentName}"`
1030
+ );
1031
+ }
1032
+ /**
1033
+ * Wait for any message from any agent.
1034
+ */
1035
+ async receiveAny(opts) {
1036
+ const timeout = opts?.timeout ?? 6e4;
1037
+ const interval = opts?.pollInterval ?? 500;
1038
+ const deadline = Date.now() + timeout;
1039
+ while (Date.now() < deadline) {
1040
+ const unread = await readUnread(this.teamName, "controller");
1041
+ const meaningful = unread.filter((m) => {
1042
+ const parsed = parseMessage(m);
1043
+ return parsed.type !== "idle_notification";
1044
+ });
1045
+ if (meaningful.length > 0) {
1046
+ return meaningful[0];
1047
+ }
1048
+ await sleep2(interval);
1049
+ }
1050
+ throw new Error(`Timeout (${timeout}ms) waiting for any message`);
1051
+ }
1052
+ // ─── Tasks ───────────────────────────────────────────────────────────
1053
+ /**
1054
+ * Create a task and optionally notify the assigned agent.
1055
+ */
1056
+ async createTask(task) {
1057
+ this.ensureInitialized();
1058
+ const taskId = await this.tasks.create(task);
1059
+ if (task.owner) {
1060
+ const fullTask = await this.tasks.get(taskId);
1061
+ const assignmentMsg = JSON.stringify({
1062
+ type: "task_assignment",
1063
+ taskId,
1064
+ subject: fullTask.subject,
1065
+ description: fullTask.description,
1066
+ assignedBy: "controller",
1067
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1068
+ });
1069
+ await this.send(task.owner, assignmentMsg);
1070
+ }
1071
+ return taskId;
1072
+ }
1073
+ /**
1074
+ * Assign a task to an agent.
1075
+ */
1076
+ async assignTask(taskId, agentName) {
1077
+ const task = await this.tasks.update(taskId, { owner: agentName });
1078
+ const msg = JSON.stringify({
1079
+ type: "task_assignment",
1080
+ taskId,
1081
+ subject: task.subject,
1082
+ description: task.description,
1083
+ assignedBy: "controller",
1084
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1085
+ });
1086
+ await this.send(agentName, msg);
1087
+ }
1088
+ // ─── Protocol Responses ───────────────────────────────────────────
1089
+ /**
1090
+ * Approve or reject a teammate's plan.
1091
+ * Send this in response to a `plan:approval_request` event.
1092
+ */
1093
+ async sendPlanApproval(agentName, requestId, approve, feedback) {
1094
+ const msg = JSON.stringify({
1095
+ type: "plan_approval_response",
1096
+ requestId,
1097
+ from: "controller",
1098
+ approved: approve,
1099
+ feedback,
1100
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1101
+ });
1102
+ await this.send(agentName, msg);
1103
+ }
1104
+ /**
1105
+ * Approve or reject a teammate's permission/tool-use request.
1106
+ * Send this in response to a `permission:request` event.
1107
+ */
1108
+ async sendPermissionResponse(agentName, requestId, approve) {
1109
+ const msg = JSON.stringify({
1110
+ type: "permission_response",
1111
+ requestId,
1112
+ from: "controller",
1113
+ approved: approve,
1114
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1115
+ });
1116
+ await this.send(agentName, msg);
1117
+ }
1118
+ /**
1119
+ * Wait for a task to be completed.
1120
+ */
1121
+ async waitForTask(taskId, timeout) {
1122
+ return this.tasks.waitFor(taskId, "completed", { timeout });
1123
+ }
1124
+ // ─── Utilities ───────────────────────────────────────────────────────
1125
+ /**
1126
+ * Check if an agent process is still running.
1127
+ */
1128
+ isAgentRunning(name) {
1129
+ return this.processes.isRunning(name);
1130
+ }
1131
+ /**
1132
+ * Kill a specific agent.
1133
+ */
1134
+ async killAgent(name) {
1135
+ await this.processes.kill(name);
1136
+ await this.team.removeMember(name);
1137
+ }
1138
+ /**
1139
+ * Get the installed Claude Code version.
1140
+ */
1141
+ getClaudeVersion() {
1142
+ try {
1143
+ const version = (0, import_node_child_process3.execSync)(`${this.claudeBinary} --version`, {
1144
+ encoding: "utf-8",
1145
+ timeout: 5e3
1146
+ }).trim();
1147
+ return version;
1148
+ } catch {
1149
+ return null;
1150
+ }
1151
+ }
1152
+ /**
1153
+ * Verify that the required CLI flags exist in the installed version.
1154
+ */
1155
+ verifyCompatibility() {
1156
+ const version = this.getClaudeVersion();
1157
+ return { compatible: version !== null, version };
1158
+ }
1159
+ // ─── Internal ────────────────────────────────────────────────────────
1160
+ handlePollEvents(events) {
1161
+ for (const event of events) {
1162
+ const { raw, parsed } = event;
1163
+ switch (parsed.type) {
1164
+ case "idle_notification":
1165
+ this.emit("idle", raw.from);
1166
+ break;
1167
+ case "shutdown_approved":
1168
+ this.log.info(
1169
+ `Shutdown approved by "${raw.from}" (requestId=${parsed.requestId})`
1170
+ );
1171
+ this.emit("shutdown:approved", raw.from, parsed);
1172
+ break;
1173
+ case "plan_approval_request":
1174
+ this.log.info(
1175
+ `Plan approval request from "${raw.from}" (requestId=${parsed.requestId})`
1176
+ );
1177
+ this.emit("plan:approval_request", raw.from, parsed);
1178
+ break;
1179
+ case "permission_request":
1180
+ this.log.info(
1181
+ `Permission request from "${raw.from}": ${parsed.toolName} (requestId=${parsed.requestId})`
1182
+ );
1183
+ this.emit("permission:request", raw.from, parsed);
1184
+ break;
1185
+ case "plain_text":
1186
+ this.emit("message", raw.from, raw);
1187
+ break;
1188
+ default:
1189
+ this.emit("message", raw.from, raw);
1190
+ }
1191
+ }
1192
+ }
1193
+ ensureInitialized() {
1194
+ if (!this.initialized) {
1195
+ throw new Error(
1196
+ "Controller not initialized. Call init() first."
1197
+ );
1198
+ }
1199
+ }
1200
+ };
1201
+ function sleep2(ms) {
1202
+ return new Promise((r) => setTimeout(r, ms));
1203
+ }
1204
+
1205
+ // src/api/routes.ts
1206
+ var SAFE_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
1207
+ var SAFE_TASK_ID_RE = /^[0-9]{1,10}$/;
1208
+ function validateName(value, field) {
1209
+ if (!SAFE_NAME_RE.test(value)) {
1210
+ throw new ValidationError(
1211
+ `${field} must be 1-64 alphanumeric characters, hyphens, or underscores`
1212
+ );
1213
+ }
1214
+ }
1215
+ function validateTaskId(value) {
1216
+ if (!SAFE_TASK_ID_RE.test(value)) {
1217
+ throw new ValidationError("task id must be a numeric string (1-10 digits)");
1218
+ }
1219
+ }
1220
+ var ValidationError = class extends Error {
1221
+ constructor(message) {
1222
+ super(message);
1223
+ this.name = "ValidationError";
1224
+ }
1225
+ };
1226
+ function isNotFoundError(err) {
1227
+ return err instanceof Error && /not found/i.test(err.message);
1228
+ }
1229
+ function getController(state) {
1230
+ if (!state.controller) {
1231
+ throw new Error(
1232
+ "No active session. Call POST /session/init first."
1233
+ );
1234
+ }
1235
+ return state.controller;
1236
+ }
1237
+ function buildRoutes(state) {
1238
+ const api = new import_hono.Hono();
1239
+ api.get("/health", (c) => {
1240
+ return c.json({
1241
+ status: "ok",
1242
+ uptime: Date.now() - state.startTime,
1243
+ session: state.controller !== null
1244
+ });
1245
+ });
1246
+ api.get("/session", (c) => {
1247
+ if (!state.controller) {
1248
+ return c.json({ initialized: false, teamName: "" });
1249
+ }
1250
+ return c.json({
1251
+ initialized: true,
1252
+ teamName: state.controller.teamName
1253
+ });
1254
+ });
1255
+ api.post("/session/init", async (c) => {
1256
+ if (state.initLock) {
1257
+ return c.json({ error: "Session init already in progress" }, 409);
1258
+ }
1259
+ state.initLock = true;
1260
+ try {
1261
+ const body = await c.req.json().catch(() => ({}));
1262
+ if (body.teamName) validateName(body.teamName, "teamName");
1263
+ const oldController = state.controller;
1264
+ if (oldController) {
1265
+ state.tracker.clear();
1266
+ state.controller = null;
1267
+ if (state.owned) {
1268
+ await oldController.shutdown();
1269
+ }
1270
+ }
1271
+ const controller = new ClaudeCodeController({
1272
+ teamName: body.teamName,
1273
+ cwd: body.cwd,
1274
+ claudeBinary: body.claudeBinary,
1275
+ env: body.env,
1276
+ logLevel: body.logLevel ?? "info"
1277
+ });
1278
+ try {
1279
+ await controller.init();
1280
+ } catch (err) {
1281
+ try {
1282
+ await controller.shutdown();
1283
+ } catch {
1284
+ }
1285
+ throw err;
1286
+ }
1287
+ state.controller = controller;
1288
+ state.owned = true;
1289
+ state.tracker.attach(controller);
1290
+ return c.json({
1291
+ initialized: true,
1292
+ teamName: controller.teamName
1293
+ }, 201);
1294
+ } finally {
1295
+ state.initLock = false;
1296
+ }
1297
+ });
1298
+ api.post("/session/shutdown", async (c) => {
1299
+ if (state.initLock) {
1300
+ return c.json({ error: "Session operation in progress" }, 409);
1301
+ }
1302
+ state.initLock = true;
1303
+ try {
1304
+ const ctrl = getController(state);
1305
+ const wasOwned = state.owned;
1306
+ state.tracker.clear();
1307
+ state.controller = null;
1308
+ state.owned = false;
1309
+ if (wasOwned) {
1310
+ await ctrl.shutdown();
1311
+ }
1312
+ return c.json({ ok: true });
1313
+ } finally {
1314
+ state.initLock = false;
1315
+ }
1316
+ });
1317
+ api.get("/actions", async (c) => {
1318
+ const ctrl = getController(state);
1319
+ const approvals = state.tracker.getPendingApprovals();
1320
+ const idleAgents = state.tracker.getIdleAgents();
1321
+ const tasks = await ctrl.tasks.list();
1322
+ const unassignedTasks = tasks.filter((t) => !t.owner && t.status !== "completed").map((t) => ({
1323
+ id: t.id,
1324
+ subject: t.subject,
1325
+ description: t.description,
1326
+ status: t.status,
1327
+ action: `POST /tasks/${t.id}/assign`
1328
+ }));
1329
+ const pending = approvals.length + unassignedTasks.length + idleAgents.length;
1330
+ return c.json({ pending, approvals, unassignedTasks, idleAgents });
1331
+ });
1332
+ api.get("/actions/approvals", (_c) => {
1333
+ getController(state);
1334
+ return _c.json(state.tracker.getPendingApprovals());
1335
+ });
1336
+ api.get("/actions/tasks", async (c) => {
1337
+ const ctrl = getController(state);
1338
+ const tasks = await ctrl.tasks.list();
1339
+ const unassigned = tasks.filter((t) => !t.owner && t.status !== "completed").map((t) => ({
1340
+ id: t.id,
1341
+ subject: t.subject,
1342
+ description: t.description,
1343
+ status: t.status,
1344
+ action: `POST /tasks/${t.id}/assign`
1345
+ }));
1346
+ return c.json(unassigned);
1347
+ });
1348
+ api.get("/actions/idle-agents", (_c) => {
1349
+ getController(state);
1350
+ return _c.json(state.tracker.getIdleAgents());
1351
+ });
1352
+ api.get("/agents", async (c) => {
1353
+ const ctrl = getController(state);
1354
+ const config = await ctrl.team.getConfig();
1355
+ const agents = config.members.filter((m) => m.name !== "controller").map((m) => ({
1356
+ name: m.name,
1357
+ type: m.agentType,
1358
+ model: m.model,
1359
+ running: ctrl.isAgentRunning(m.name)
1360
+ }));
1361
+ return c.json(agents);
1362
+ });
1363
+ api.post("/agents", async (c) => {
1364
+ const ctrl = getController(state);
1365
+ const body = await c.req.json();
1366
+ if (!body.name) {
1367
+ return c.json({ error: "name is required" }, 400);
1368
+ }
1369
+ validateName(body.name, "name");
1370
+ const agentType = body.type || "general-purpose";
1371
+ state.tracker.registerAgentType(body.name, agentType);
1372
+ const handle = await ctrl.spawnAgent({
1373
+ name: body.name,
1374
+ type: body.type,
1375
+ model: body.model,
1376
+ cwd: body.cwd,
1377
+ permissions: body.permissions,
1378
+ env: body.env
1379
+ });
1380
+ return c.json(
1381
+ {
1382
+ name: handle.name,
1383
+ pid: handle.pid,
1384
+ running: handle.isRunning
1385
+ },
1386
+ 201
1387
+ );
1388
+ });
1389
+ api.get("/agents/:name", async (c) => {
1390
+ const ctrl = getController(state);
1391
+ const name = c.req.param("name");
1392
+ validateName(name, "name");
1393
+ const config = await ctrl.team.getConfig();
1394
+ const member = config.members.find((m) => m.name === name);
1395
+ if (!member) {
1396
+ return c.json({ error: `Agent "${name}" not found` }, 404);
1397
+ }
1398
+ return c.json({
1399
+ name: member.name,
1400
+ type: member.agentType,
1401
+ model: member.model,
1402
+ running: ctrl.isAgentRunning(name)
1403
+ });
1404
+ });
1405
+ api.post("/agents/:name/messages", async (c) => {
1406
+ const ctrl = getController(state);
1407
+ const name = c.req.param("name");
1408
+ validateName(name, "name");
1409
+ const body = await c.req.json();
1410
+ if (!body.message) {
1411
+ return c.json({ error: "message is required" }, 400);
1412
+ }
1413
+ await ctrl.send(name, body.message, body.summary);
1414
+ return c.json({ ok: true });
1415
+ });
1416
+ api.post("/agents/:name/kill", async (c) => {
1417
+ const ctrl = getController(state);
1418
+ const name = c.req.param("name");
1419
+ validateName(name, "name");
1420
+ await ctrl.killAgent(name);
1421
+ return c.json({ ok: true });
1422
+ });
1423
+ api.post("/agents/:name/shutdown", async (c) => {
1424
+ const ctrl = getController(state);
1425
+ const name = c.req.param("name");
1426
+ validateName(name, "name");
1427
+ await ctrl.sendShutdownRequest(name);
1428
+ return c.json({ ok: true });
1429
+ });
1430
+ api.post("/agents/:name/approve-plan", async (c) => {
1431
+ const ctrl = getController(state);
1432
+ const name = c.req.param("name");
1433
+ validateName(name, "name");
1434
+ const body = await c.req.json();
1435
+ if (!body.requestId) {
1436
+ return c.json({ error: "requestId is required" }, 400);
1437
+ }
1438
+ await ctrl.sendPlanApproval(
1439
+ name,
1440
+ body.requestId,
1441
+ body.approve ?? true,
1442
+ body.feedback
1443
+ );
1444
+ state.tracker.resolveApproval(body.requestId);
1445
+ return c.json({ ok: true });
1446
+ });
1447
+ api.post("/agents/:name/approve-permission", async (c) => {
1448
+ const ctrl = getController(state);
1449
+ const name = c.req.param("name");
1450
+ validateName(name, "name");
1451
+ const body = await c.req.json();
1452
+ if (!body.requestId) {
1453
+ return c.json({ error: "requestId is required" }, 400);
1454
+ }
1455
+ await ctrl.sendPermissionResponse(
1456
+ name,
1457
+ body.requestId,
1458
+ body.approve ?? true
1459
+ );
1460
+ state.tracker.resolveApproval(body.requestId);
1461
+ return c.json({ ok: true });
1462
+ });
1463
+ api.post("/broadcast", async (c) => {
1464
+ const ctrl = getController(state);
1465
+ const body = await c.req.json();
1466
+ if (!body.message) {
1467
+ return c.json({ error: "message is required" }, 400);
1468
+ }
1469
+ await ctrl.broadcast(body.message, body.summary);
1470
+ return c.json({ ok: true });
1471
+ });
1472
+ api.get("/tasks", async (c) => {
1473
+ const ctrl = getController(state);
1474
+ const tasks = await ctrl.tasks.list();
1475
+ return c.json(tasks);
1476
+ });
1477
+ api.post("/tasks", async (c) => {
1478
+ const ctrl = getController(state);
1479
+ const body = await c.req.json();
1480
+ if (!body.subject || !body.description) {
1481
+ return c.json({ error: "subject and description are required" }, 400);
1482
+ }
1483
+ if (body.owner) validateName(body.owner, "owner");
1484
+ const taskId = await ctrl.createTask(body);
1485
+ const task = await ctrl.tasks.get(taskId);
1486
+ return c.json(task, 201);
1487
+ });
1488
+ api.get("/tasks/:id", async (c) => {
1489
+ const ctrl = getController(state);
1490
+ const id = c.req.param("id");
1491
+ validateTaskId(id);
1492
+ try {
1493
+ const task = await ctrl.tasks.get(id);
1494
+ return c.json(task);
1495
+ } catch (err) {
1496
+ if (isNotFoundError(err)) {
1497
+ return c.json({ error: `Task "${id}" not found` }, 404);
1498
+ }
1499
+ throw err;
1500
+ }
1501
+ });
1502
+ api.patch("/tasks/:id", async (c) => {
1503
+ const ctrl = getController(state);
1504
+ const id = c.req.param("id");
1505
+ validateTaskId(id);
1506
+ const body = await c.req.json();
1507
+ try {
1508
+ const task = await ctrl.tasks.update(id, body);
1509
+ return c.json(task);
1510
+ } catch (err) {
1511
+ if (isNotFoundError(err)) {
1512
+ return c.json({ error: `Task "${id}" not found` }, 404);
1513
+ }
1514
+ throw err;
1515
+ }
1516
+ });
1517
+ api.delete("/tasks/:id", async (c) => {
1518
+ const ctrl = getController(state);
1519
+ const id = c.req.param("id");
1520
+ validateTaskId(id);
1521
+ try {
1522
+ await ctrl.tasks.get(id);
1523
+ } catch (err) {
1524
+ if (isNotFoundError(err)) {
1525
+ return c.json({ error: `Task "${id}" not found` }, 404);
1526
+ }
1527
+ throw err;
1528
+ }
1529
+ await ctrl.tasks.delete(id);
1530
+ return c.json({ ok: true });
1531
+ });
1532
+ api.post("/tasks/:id/assign", async (c) => {
1533
+ const ctrl = getController(state);
1534
+ const id = c.req.param("id");
1535
+ validateTaskId(id);
1536
+ const body = await c.req.json();
1537
+ if (!body.agent) {
1538
+ return c.json({ error: "agent is required" }, 400);
1539
+ }
1540
+ validateName(body.agent, "agent");
1541
+ try {
1542
+ await ctrl.tasks.get(id);
1543
+ } catch (err) {
1544
+ if (isNotFoundError(err)) {
1545
+ return c.json({ error: `Task "${id}" not found` }, 404);
1546
+ }
1547
+ throw err;
1548
+ }
1549
+ await ctrl.assignTask(id, body.agent);
1550
+ return c.json({ ok: true });
1551
+ });
1552
+ return api;
1553
+ }
1554
+
1555
+ // src/api/index.ts
1556
+ function createApi(controller, options) {
1557
+ const tracker = new ActionTracker();
1558
+ if (controller) {
1559
+ tracker.attach(controller);
1560
+ }
1561
+ const state = {
1562
+ controller: controller ?? null,
1563
+ tracker,
1564
+ owned: false,
1565
+ // externally-provided controllers are not owned by the API
1566
+ initLock: false,
1567
+ startTime: Date.now()
1568
+ };
1569
+ const app = new import_hono2.Hono();
1570
+ const basePath = options?.basePath ?? "/";
1571
+ if (options?.cors !== false) {
1572
+ app.use("*", (0, import_cors.cors)());
1573
+ }
1574
+ const routes = buildRoutes(state);
1575
+ app.route(basePath, routes);
1576
+ app.onError((err, c) => {
1577
+ const message = err instanceof Error ? err.message : "Internal server error";
1578
+ const status = err.name === "ValidationError" ? 400 : 500;
1579
+ return c.json({ error: message }, status);
1580
+ });
1581
+ return app;
1582
+ }
1583
+ // Annotate the CommonJS export names for ESM import in node:
1584
+ 0 && (module.exports = {
1585
+ ActionTracker,
1586
+ createApi
1587
+ });
1588
+ //# sourceMappingURL=index.cjs.map