claude-code-controller 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,523 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { ChildProcess } from 'node:child_process';
3
+
4
+ interface InboxMessage {
5
+ from: string;
6
+ text: string;
7
+ timestamp: string;
8
+ color?: string;
9
+ read: boolean;
10
+ summary?: string;
11
+ }
12
+ type StructuredMessage = TaskAssignmentMessage | ShutdownRequestMessage | ShutdownApprovedMessage | IdleNotificationMessage | PlanApprovalRequestMessage | PlanApprovalResponseMessage | PermissionRequestMessage | PermissionResponseMessage | PlainTextMessage;
13
+ interface TaskAssignmentMessage {
14
+ type: "task_assignment";
15
+ taskId: string;
16
+ subject: string;
17
+ description: string;
18
+ assignedBy: string;
19
+ timestamp: string;
20
+ }
21
+ interface ShutdownRequestMessage {
22
+ type: "shutdown_request";
23
+ requestId: string;
24
+ from: string;
25
+ reason?: string;
26
+ timestamp: string;
27
+ }
28
+ interface ShutdownApprovedMessage {
29
+ type: "shutdown_approved";
30
+ requestId: string;
31
+ from: string;
32
+ timestamp: string;
33
+ paneId?: string;
34
+ backendType?: string;
35
+ }
36
+ interface IdleNotificationMessage {
37
+ type: "idle_notification";
38
+ from: string;
39
+ timestamp: string;
40
+ idleReason: string;
41
+ }
42
+ interface PlanApprovalRequestMessage {
43
+ type: "plan_approval_request";
44
+ requestId: string;
45
+ from: string;
46
+ planContent?: string;
47
+ timestamp: string;
48
+ }
49
+ interface PlanApprovalResponseMessage {
50
+ type: "plan_approval_response";
51
+ requestId: string;
52
+ from: string;
53
+ approved: boolean;
54
+ feedback?: string;
55
+ timestamp: string;
56
+ }
57
+ interface PermissionRequestMessage {
58
+ type: "permission_request";
59
+ requestId: string;
60
+ from: string;
61
+ toolName: string;
62
+ toolUseId?: string;
63
+ description: string;
64
+ input?: unknown;
65
+ permissionSuggestions?: string[];
66
+ timestamp: string;
67
+ }
68
+ interface PermissionResponseMessage {
69
+ type: "permission_response";
70
+ requestId: string;
71
+ from: string;
72
+ approved: boolean;
73
+ timestamp: string;
74
+ }
75
+ interface PlainTextMessage {
76
+ type: "plain_text";
77
+ text: string;
78
+ }
79
+ interface TeamConfig {
80
+ name: string;
81
+ description?: string;
82
+ createdAt: number;
83
+ leadAgentId: string;
84
+ leadSessionId: string;
85
+ members: TeamMember[];
86
+ }
87
+ interface TeamMember {
88
+ agentId: string;
89
+ name: string;
90
+ agentType: string;
91
+ model?: string;
92
+ joinedAt: number;
93
+ tmuxPaneId?: string;
94
+ cwd: string;
95
+ subscriptions?: string[];
96
+ }
97
+ interface TaskFile {
98
+ id: string;
99
+ subject: string;
100
+ description: string;
101
+ activeForm?: string;
102
+ owner?: string;
103
+ status: TaskStatus;
104
+ blocks: string[];
105
+ blockedBy: string[];
106
+ metadata?: Record<string, unknown>;
107
+ }
108
+ type TaskStatus = "pending" | "in_progress" | "completed";
109
+ type AgentType = "general-purpose" | "Bash" | "Explore" | "Plan" | string;
110
+ interface SpawnAgentOptions {
111
+ name: string;
112
+ type?: AgentType;
113
+ model?: string;
114
+ cwd?: string;
115
+ prompt?: string;
116
+ permissions?: string[];
117
+ /** Environment variables to inject into the agent's process */
118
+ env?: Record<string, string>;
119
+ }
120
+ interface ControllerOptions {
121
+ teamName?: string;
122
+ cwd?: string;
123
+ claudeBinary?: string;
124
+ logger?: Logger;
125
+ /** Default environment variables for all spawned agents */
126
+ env?: Record<string, string>;
127
+ }
128
+ interface ReceiveOptions {
129
+ timeout?: number;
130
+ pollInterval?: number;
131
+ /** If true, return all unread messages, not just the first one */
132
+ all?: boolean;
133
+ }
134
+ interface ControllerEvents {
135
+ message: [agentName: string, message: InboxMessage];
136
+ idle: [agentName: string];
137
+ "shutdown:approved": [agentName: string, message: ShutdownApprovedMessage];
138
+ "plan:approval_request": [agentName: string, message: PlanApprovalRequestMessage];
139
+ "permission:request": [agentName: string, message: PermissionRequestMessage];
140
+ "task:completed": [task: TaskFile];
141
+ "agent:spawned": [agentName: string, pid: number];
142
+ "agent:exited": [agentName: string, code: number | null];
143
+ error: [error: Error];
144
+ }
145
+ type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
146
+ interface Logger {
147
+ debug(msg: string, ...args: unknown[]): void;
148
+ info(msg: string, ...args: unknown[]): void;
149
+ warn(msg: string, ...args: unknown[]): void;
150
+ error(msg: string, ...args: unknown[]): void;
151
+ }
152
+
153
+ declare class TeamManager {
154
+ readonly teamName: string;
155
+ readonly sessionId: string;
156
+ private log;
157
+ constructor(teamName: string, logger: Logger);
158
+ /**
159
+ * Create the team directory structure and config.json.
160
+ * The controller registers itself as the lead member.
161
+ */
162
+ create(opts?: {
163
+ description?: string;
164
+ cwd?: string;
165
+ }): Promise<TeamConfig>;
166
+ /**
167
+ * Add a member to the team config.
168
+ */
169
+ addMember(member: TeamMember): Promise<void>;
170
+ /**
171
+ * Remove a member from the team config.
172
+ */
173
+ removeMember(name: string): Promise<void>;
174
+ /**
175
+ * Read the current team config.
176
+ */
177
+ getConfig(): Promise<TeamConfig>;
178
+ /**
179
+ * Check if the team already exists on disk.
180
+ */
181
+ exists(): boolean;
182
+ /**
183
+ * Destroy the team: remove all team directories and task directories.
184
+ */
185
+ destroy(): Promise<void>;
186
+ private writeConfig;
187
+ }
188
+
189
+ declare class TaskManager {
190
+ private teamName;
191
+ private log;
192
+ private nextId;
193
+ constructor(teamName: string, log: Logger);
194
+ /**
195
+ * Initialize the task directory. Call after team creation.
196
+ * Also scans for existing tasks to set the next ID correctly.
197
+ */
198
+ init(): Promise<void>;
199
+ /**
200
+ * Create a new task. Returns the assigned task ID.
201
+ */
202
+ create(task: Omit<TaskFile, "id" | "blocks" | "blockedBy" | "status"> & {
203
+ blocks?: string[];
204
+ blockedBy?: string[];
205
+ status?: TaskStatus;
206
+ }): Promise<string>;
207
+ /**
208
+ * Get a task by ID.
209
+ */
210
+ get(taskId: string): Promise<TaskFile>;
211
+ /**
212
+ * Update a task. Merges the provided fields.
213
+ */
214
+ update(taskId: string, updates: Partial<Pick<TaskFile, "subject" | "description" | "activeForm" | "owner" | "status" | "blocks" | "blockedBy" | "metadata">>): Promise<TaskFile>;
215
+ /**
216
+ * Add blocking relationships.
217
+ */
218
+ addBlocks(taskId: string, blockedTaskIds: string[]): Promise<void>;
219
+ /**
220
+ * List all tasks.
221
+ */
222
+ list(): Promise<TaskFile[]>;
223
+ /**
224
+ * Delete a task file.
225
+ */
226
+ delete(taskId: string): Promise<void>;
227
+ /**
228
+ * Wait for a task to reach a target status.
229
+ */
230
+ waitFor(taskId: string, targetStatus?: TaskStatus, opts?: {
231
+ timeout?: number;
232
+ pollInterval?: number;
233
+ }): Promise<TaskFile>;
234
+ private writeTask;
235
+ }
236
+
237
+ /**
238
+ * Interface for the controller methods that AgentHandle needs.
239
+ * This avoids a circular dependency with the full controller.
240
+ */
241
+ interface AgentController {
242
+ send(agentName: string, message: string, summary?: string): Promise<void>;
243
+ receive(agentName: string, opts?: ReceiveOptions): Promise<InboxMessage[]>;
244
+ killAgent(agentName: string): Promise<void>;
245
+ sendShutdownRequest(agentName: string): Promise<void>;
246
+ isAgentRunning(agentName: string): boolean;
247
+ }
248
+ /**
249
+ * Proxy object for interacting with a specific agent.
250
+ */
251
+ declare class AgentHandle {
252
+ readonly name: string;
253
+ readonly pid: number | undefined;
254
+ private controller;
255
+ constructor(controller: AgentController, name: string, pid: number | undefined);
256
+ /**
257
+ * Send a message to this agent.
258
+ */
259
+ send(message: string, summary?: string): Promise<void>;
260
+ /**
261
+ * Wait for a response from this agent.
262
+ * Returns the text of the first unread plain-text message.
263
+ */
264
+ receive(opts?: ReceiveOptions): Promise<string>;
265
+ /**
266
+ * Send a message and wait for the response. Convenience method.
267
+ */
268
+ ask(question: string, opts?: ReceiveOptions): Promise<string>;
269
+ /**
270
+ * Check if the agent process is still running.
271
+ */
272
+ get isRunning(): boolean;
273
+ /**
274
+ * Request the agent to shut down gracefully.
275
+ */
276
+ shutdown(): Promise<void>;
277
+ /**
278
+ * Force-kill the agent process.
279
+ */
280
+ kill(): Promise<void>;
281
+ /**
282
+ * Async iterator for agent events (messages from this agent).
283
+ * Polls the controller's inbox for messages from this agent.
284
+ */
285
+ events(opts?: {
286
+ pollInterval?: number;
287
+ timeout?: number;
288
+ }): AsyncGenerator<InboxMessage>;
289
+ }
290
+
291
+ declare class ClaudeCodeController extends EventEmitter<ControllerEvents> implements AgentController {
292
+ readonly teamName: string;
293
+ readonly team: TeamManager;
294
+ readonly tasks: TaskManager;
295
+ private processes;
296
+ private poller;
297
+ private log;
298
+ private cwd;
299
+ private claudeBinary;
300
+ private defaultEnv;
301
+ private colorIndex;
302
+ private initialized;
303
+ constructor(opts?: ControllerOptions & {
304
+ logLevel?: LogLevel;
305
+ });
306
+ /**
307
+ * Initialize the controller: create the team and start polling.
308
+ * Must be called before any other operations.
309
+ */
310
+ init(): Promise<this>;
311
+ /**
312
+ * Graceful shutdown:
313
+ * 1. Send shutdown requests to all agents
314
+ * 2. Wait briefly for acknowledgment
315
+ * 3. Kill remaining processes
316
+ * 4. Clean up team files
317
+ */
318
+ shutdown(): Promise<void>;
319
+ /**
320
+ * Spawn a new Claude Code agent.
321
+ */
322
+ spawnAgent(opts: SpawnAgentOptions): Promise<AgentHandle>;
323
+ /**
324
+ * Send a message to a specific agent.
325
+ */
326
+ send(agentName: string, message: string, summary?: string): Promise<void>;
327
+ /**
328
+ * Send a structured shutdown request to an agent.
329
+ */
330
+ sendShutdownRequest(agentName: string): Promise<void>;
331
+ /**
332
+ * Broadcast a message to all registered agents (except controller).
333
+ */
334
+ broadcast(message: string, summary?: string): Promise<void>;
335
+ /**
336
+ * Wait for messages from a specific agent.
337
+ * Polls the controller's inbox for messages from the given agent.
338
+ *
339
+ * Returns when:
340
+ * - A non-idle message is received (SendMessage from agent), OR
341
+ * - An idle_notification is received (agent finished its turn),
342
+ * in which case the idle message is returned.
343
+ */
344
+ receive(agentName: string, opts?: ReceiveOptions): Promise<InboxMessage[]>;
345
+ /**
346
+ * Wait for any message from any agent.
347
+ */
348
+ receiveAny(opts?: ReceiveOptions): Promise<InboxMessage>;
349
+ /**
350
+ * Create a task and optionally notify the assigned agent.
351
+ */
352
+ createTask(task: Omit<TaskFile, "id" | "blocks" | "blockedBy" | "status"> & {
353
+ blocks?: string[];
354
+ blockedBy?: string[];
355
+ status?: TaskStatus;
356
+ }): Promise<string>;
357
+ /**
358
+ * Assign a task to an agent.
359
+ */
360
+ assignTask(taskId: string, agentName: string): Promise<void>;
361
+ /**
362
+ * Approve or reject a teammate's plan.
363
+ * Send this in response to a `plan:approval_request` event.
364
+ */
365
+ sendPlanApproval(agentName: string, requestId: string, approve: boolean, feedback?: string): Promise<void>;
366
+ /**
367
+ * Approve or reject a teammate's permission/tool-use request.
368
+ * Send this in response to a `permission:request` event.
369
+ */
370
+ sendPermissionResponse(agentName: string, requestId: string, approve: boolean): Promise<void>;
371
+ /**
372
+ * Wait for a task to be completed.
373
+ */
374
+ waitForTask(taskId: string, timeout?: number): Promise<TaskFile>;
375
+ /**
376
+ * Check if an agent process is still running.
377
+ */
378
+ isAgentRunning(name: string): boolean;
379
+ /**
380
+ * Kill a specific agent.
381
+ */
382
+ killAgent(name: string): Promise<void>;
383
+ /**
384
+ * Get the installed Claude Code version.
385
+ */
386
+ getClaudeVersion(): string | null;
387
+ /**
388
+ * Verify that the required CLI flags exist in the installed version.
389
+ */
390
+ verifyCompatibility(): {
391
+ compatible: boolean;
392
+ version: string | null;
393
+ };
394
+ private handlePollEvents;
395
+ private ensureInitialized;
396
+ }
397
+
398
+ interface SpawnOptions {
399
+ teamName: string;
400
+ agentName: string;
401
+ agentId: string;
402
+ agentType?: string;
403
+ model?: string;
404
+ cwd?: string;
405
+ parentSessionId?: string;
406
+ color?: string;
407
+ claudeBinary?: string;
408
+ permissions?: string[];
409
+ teammateMode?: "auto" | "tmux" | "in-process";
410
+ /** Extra environment variables to inject into the agent process */
411
+ env?: Record<string, string>;
412
+ }
413
+ /**
414
+ * Manages Claude Code agent processes.
415
+ * Spawns agents inside a Python-based PTY wrapper since the Claude Code TUI
416
+ * binary requires a real terminal to function.
417
+ */
418
+ declare class ProcessManager {
419
+ private processes;
420
+ private log;
421
+ constructor(logger: Logger);
422
+ /**
423
+ * Spawn a new claude CLI process in teammate mode.
424
+ * Uses a Python PTY wrapper to provide the terminal the TUI needs.
425
+ */
426
+ spawn(opts: SpawnOptions): ChildProcess;
427
+ /**
428
+ * Register a callback for when an agent process exits.
429
+ */
430
+ onExit(name: string, callback: (code: number | null) => void): void;
431
+ /**
432
+ * Get the process for a named agent.
433
+ */
434
+ get(name: string): ChildProcess | undefined;
435
+ /**
436
+ * Check if an agent process is still running.
437
+ */
438
+ isRunning(name: string): boolean;
439
+ /**
440
+ * Get the PID of a running agent.
441
+ */
442
+ getPid(name: string): number | undefined;
443
+ /**
444
+ * Kill a specific agent process.
445
+ */
446
+ kill(name: string, signal?: NodeJS.Signals): Promise<void>;
447
+ /**
448
+ * Kill all agent processes.
449
+ */
450
+ killAll(): Promise<void>;
451
+ /**
452
+ * Get all running agent names.
453
+ */
454
+ runningAgents(): string[];
455
+ }
456
+
457
+ interface PollEvent {
458
+ raw: InboxMessage;
459
+ parsed: StructuredMessage;
460
+ }
461
+ /**
462
+ * Polls an agent's inbox for new messages.
463
+ * Used by the controller to watch its own inbox for responses from agents.
464
+ */
465
+ declare class InboxPoller {
466
+ private teamName;
467
+ private agentName;
468
+ private interval;
469
+ private timer;
470
+ private log;
471
+ private handlers;
472
+ constructor(teamName: string, agentName: string, logger: Logger, opts?: {
473
+ pollInterval?: number;
474
+ });
475
+ /**
476
+ * Register a handler for new messages.
477
+ */
478
+ onMessages(handler: (events: PollEvent[]) => void): void;
479
+ /**
480
+ * Start polling.
481
+ */
482
+ start(): void;
483
+ /**
484
+ * Stop polling.
485
+ */
486
+ stop(): void;
487
+ /**
488
+ * Poll once for new messages.
489
+ */
490
+ poll(): Promise<PollEvent[]>;
491
+ }
492
+
493
+ /**
494
+ * Write a message to an agent's inbox with file-locking.
495
+ */
496
+ declare function writeInbox(teamName: string, agentName: string, message: Omit<InboxMessage, "read">, logger?: Logger): Promise<void>;
497
+ /**
498
+ * Read all messages from an agent's inbox.
499
+ */
500
+ declare function readInbox(teamName: string, agentName: string): Promise<InboxMessage[]>;
501
+ /**
502
+ * Read unread messages from an agent's inbox and mark them as read.
503
+ */
504
+ declare function readUnread(teamName: string, agentName: string): Promise<InboxMessage[]>;
505
+ /**
506
+ * Parse a structured message from an inbox message's text field.
507
+ * Messages can be either JSON-encoded structured messages or plain text.
508
+ */
509
+ declare function parseMessage(msg: InboxMessage): StructuredMessage;
510
+
511
+ declare function teamsDir(): string;
512
+ declare function teamDir(teamName: string): string;
513
+ declare function teamConfigPath(teamName: string): string;
514
+ declare function inboxesDir(teamName: string): string;
515
+ declare function inboxPath(teamName: string, agentName: string): string;
516
+ declare function tasksBaseDir(): string;
517
+ declare function tasksDir(teamName: string): string;
518
+ declare function taskPath(teamName: string, taskId: string): string;
519
+
520
+ declare function createLogger(level?: LogLevel): Logger;
521
+ declare const silentLogger: Logger;
522
+
523
+ export { AgentHandle, type AgentType, ClaudeCodeController, type ControllerEvents, type ControllerOptions, type IdleNotificationMessage, type InboxMessage, InboxPoller, type LogLevel, type Logger, type PermissionRequestMessage, type PermissionResponseMessage, type PlainTextMessage, type PlanApprovalRequestMessage, type PlanApprovalResponseMessage, ProcessManager, type ReceiveOptions, type ShutdownApprovedMessage, type ShutdownRequestMessage, type SpawnAgentOptions, type StructuredMessage, type TaskAssignmentMessage, type TaskFile, TaskManager, type TaskStatus, type TeamConfig, TeamManager, type TeamMember, createLogger, inboxPath, inboxesDir, parseMessage, readInbox, readUnread, silentLogger, taskPath, tasksBaseDir, tasksDir, teamConfigPath, teamDir, teamsDir, writeInbox };