claude-code-controller 0.3.0 → 0.5.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/dist/index.cjs CHANGED
@@ -20,12 +20,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ Agent: () => Agent,
23
24
  AgentHandle: () => AgentHandle,
24
25
  ClaudeCodeController: () => ClaudeCodeController,
25
26
  InboxPoller: () => InboxPoller,
26
27
  ProcessManager: () => ProcessManager,
28
+ Session: () => Session,
27
29
  TaskManager: () => TaskManager,
28
30
  TeamManager: () => TeamManager,
31
+ claude: () => claude,
29
32
  createLogger: () => createLogger,
30
33
  inboxPath: () => inboxPath,
31
34
  inboxesDir: () => inboxesDir,
@@ -43,6 +46,10 @@ __export(index_exports, {
43
46
  });
44
47
  module.exports = __toCommonJS(index_exports);
45
48
 
49
+ // src/claude.ts
50
+ var import_node_events2 = require("events");
51
+ var import_node_crypto3 = require("crypto");
52
+
46
53
  // src/controller.ts
47
54
  var import_node_events = require("events");
48
55
  var import_node_child_process3 = require("child_process");
@@ -413,6 +420,7 @@ else:
413
420
  stdio: ["pipe", "pipe", "pipe"],
414
421
  env: {
415
422
  ...process.env,
423
+ CLAUDECODE: "1",
416
424
  CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1",
417
425
  ...opts.env
418
426
  }
@@ -743,6 +751,14 @@ var silentLogger = {
743
751
  };
744
752
 
745
753
  // src/controller.ts
754
+ var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
755
+ "shutdown_approved",
756
+ "plan_approval_response",
757
+ "permission_response",
758
+ "task_completed",
759
+ "sandbox_permission_request",
760
+ "sandbox_permission_response"
761
+ ]);
746
762
  var AGENT_COLORS = [
747
763
  "#00FF00",
748
764
  "#00BFFF",
@@ -849,6 +865,10 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
849
865
  name: opts.name,
850
866
  agentType: opts.type || "general-purpose",
851
867
  model: opts.model,
868
+ prompt: opts.prompt,
869
+ color,
870
+ planModeRequired: false,
871
+ backendType: "in-process",
852
872
  joinedAt: Date.now(),
853
873
  tmuxPaneId: "",
854
874
  cwd,
@@ -936,11 +956,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
936
956
  const unread = await readUnread(this.teamName, "controller");
937
957
  const fromAgent = unread.filter((m) => m.from === agentName);
938
958
  if (fromAgent.length > 0) {
939
- const PROTOCOL_TYPES = /* @__PURE__ */ new Set([
940
- "shutdown_approved",
941
- "plan_approval_response",
942
- "permission_response"
943
- ]);
959
+ const PROTOCOL_TYPES = PROTOCOL_ONLY_TYPES;
944
960
  const meaningful = fromAgent.filter((m) => {
945
961
  const parsed = parseMessage(m);
946
962
  return parsed.type !== "idle_notification" && !PROTOCOL_TYPES.has(parsed.type);
@@ -973,7 +989,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
973
989
  const unread = await readUnread(this.teamName, "controller");
974
990
  const meaningful = unread.filter((m) => {
975
991
  const parsed = parseMessage(m);
976
- return parsed.type !== "idle_notification";
992
+ return parsed.type !== "idle_notification" && !PROTOCOL_ONLY_TYPES.has(parsed.type);
977
993
  });
978
994
  if (meaningful.length > 0) {
979
995
  return meaningful[0];
@@ -1095,7 +1111,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
1095
1111
  const { raw, parsed } = event;
1096
1112
  switch (parsed.type) {
1097
1113
  case "idle_notification":
1098
- this.emit("idle", raw.from);
1114
+ this.emit("idle", raw.from, parsed);
1099
1115
  break;
1100
1116
  case "shutdown_approved":
1101
1117
  this.log.info(
@@ -1115,6 +1131,9 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
1115
1131
  );
1116
1132
  this.emit("permission:request", raw.from, parsed);
1117
1133
  break;
1134
+ case "task_completed":
1135
+ this.emit("message", raw.from, raw);
1136
+ break;
1118
1137
  case "plain_text":
1119
1138
  this.emit("message", raw.from, raw);
1120
1139
  break;
@@ -1134,14 +1153,463 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
1134
1153
  function sleep2(ms) {
1135
1154
  return new Promise((r) => setTimeout(r, ms));
1136
1155
  }
1156
+
1157
+ // src/claude.ts
1158
+ Symbol.asyncDispose ??= /* @__PURE__ */ Symbol("Symbol.asyncDispose");
1159
+ function buildEnv(opts) {
1160
+ const env = { ...opts.env };
1161
+ if (opts.apiKey) env.ANTHROPIC_AUTH_TOKEN = opts.apiKey;
1162
+ if (opts.baseUrl) env.ANTHROPIC_BASE_URL = opts.baseUrl;
1163
+ if (opts.timeout != null) env.API_TIMEOUT_MS = String(opts.timeout);
1164
+ return env;
1165
+ }
1166
+ function resolvePermissions(preset) {
1167
+ switch (preset) {
1168
+ case "edit":
1169
+ return { permissionMode: "acceptEdits" };
1170
+ case "plan":
1171
+ return { permissionMode: "plan" };
1172
+ case "ask":
1173
+ return { permissionMode: "default" };
1174
+ case "full":
1175
+ default:
1176
+ return { permissionMode: void 0 };
1177
+ }
1178
+ }
1179
+ function waitForReady(controller, agentName, timeoutMs = 15e3) {
1180
+ return new Promise((resolve, reject) => {
1181
+ let settled = false;
1182
+ const timer = setTimeout(() => {
1183
+ if (settled) return;
1184
+ cleanup();
1185
+ reject(
1186
+ new Error(
1187
+ `Agent "${agentName}" did not become ready within ${timeoutMs}ms`
1188
+ )
1189
+ );
1190
+ }, timeoutMs);
1191
+ const onReady = (name, ..._rest) => {
1192
+ if (name === agentName && !settled) {
1193
+ settled = true;
1194
+ cleanup();
1195
+ resolve();
1196
+ }
1197
+ };
1198
+ const onExit = (name, code) => {
1199
+ if (name === agentName && !settled) {
1200
+ settled = true;
1201
+ cleanup();
1202
+ reject(
1203
+ new Error(
1204
+ `Agent "${agentName}" exited before becoming ready (code=${code})`
1205
+ )
1206
+ );
1207
+ }
1208
+ };
1209
+ const onSpawned = (name) => {
1210
+ if (name === agentName && !settled) {
1211
+ settled = true;
1212
+ cleanup();
1213
+ resolve();
1214
+ }
1215
+ };
1216
+ const cleanup = () => {
1217
+ clearTimeout(timer);
1218
+ controller.removeListener("idle", onReady);
1219
+ controller.removeListener("message", onReady);
1220
+ controller.removeListener("agent:spawned", onSpawned);
1221
+ controller.removeListener("agent:exited", onExit);
1222
+ };
1223
+ controller.on("idle", onReady);
1224
+ controller.on("message", onReady);
1225
+ controller.on("agent:spawned", onSpawned);
1226
+ controller.on("agent:exited", onExit);
1227
+ });
1228
+ }
1229
+ var Agent = class _Agent extends import_node_events2.EventEmitter {
1230
+ controller;
1231
+ handle;
1232
+ ownsController;
1233
+ disposed = false;
1234
+ boundListeners = [];
1235
+ constructor(controller, handle, ownsController, behavior) {
1236
+ super();
1237
+ this.controller = controller;
1238
+ this.handle = handle;
1239
+ this.ownsController = ownsController;
1240
+ this.wireEvents();
1241
+ this.wireBehavior(behavior);
1242
+ }
1243
+ /** Create a standalone agent (owns its own controller). */
1244
+ static async create(opts = {}) {
1245
+ const name = opts.name ?? `agent-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`;
1246
+ const env = buildEnv(opts);
1247
+ const { permissionMode } = resolvePermissions(opts.permissions);
1248
+ const controller = new ClaudeCodeController({
1249
+ teamName: `claude-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`,
1250
+ cwd: opts.cwd,
1251
+ claudeBinary: opts.claudeBinary,
1252
+ env,
1253
+ logLevel: opts.logLevel ?? "warn",
1254
+ logger: opts.logger
1255
+ });
1256
+ await controller.init();
1257
+ const ready = waitForReady(controller, name, opts.readyTimeout);
1258
+ try {
1259
+ const handle = await controller.spawnAgent({
1260
+ name,
1261
+ type: opts.type ?? "general-purpose",
1262
+ model: opts.model,
1263
+ cwd: opts.cwd,
1264
+ permissionMode
1265
+ });
1266
+ const agent = new _Agent(controller, handle, true, {
1267
+ autoApprove: opts.autoApprove,
1268
+ onPermission: opts.onPermission,
1269
+ onPlan: opts.onPlan
1270
+ });
1271
+ await ready;
1272
+ return agent;
1273
+ } catch (err) {
1274
+ await controller.shutdown().catch(() => {
1275
+ });
1276
+ throw err;
1277
+ }
1278
+ }
1279
+ /** Create an agent within an existing session (session owns the controller). */
1280
+ static async createInSession(controller, name, opts = {}) {
1281
+ const { permissionMode } = resolvePermissions(opts.permissions);
1282
+ const ready = waitForReady(controller, name, opts.readyTimeout);
1283
+ const handle = await controller.spawnAgent({
1284
+ name,
1285
+ type: opts.type ?? "general-purpose",
1286
+ model: opts.model,
1287
+ cwd: opts.cwd,
1288
+ permissionMode,
1289
+ env: opts.env
1290
+ });
1291
+ const agent = new _Agent(controller, handle, false, {
1292
+ autoApprove: opts.autoApprove,
1293
+ onPermission: opts.onPermission,
1294
+ onPlan: opts.onPlan
1295
+ });
1296
+ await ready;
1297
+ return agent;
1298
+ }
1299
+ /** The agent's name. */
1300
+ get name() {
1301
+ return this.handle.name;
1302
+ }
1303
+ /** The agent process PID. */
1304
+ get pid() {
1305
+ return this.handle.pid;
1306
+ }
1307
+ /** Whether the agent process is still running. */
1308
+ get isRunning() {
1309
+ return this.handle.isRunning;
1310
+ }
1311
+ /**
1312
+ * Send a message and wait for the response.
1313
+ *
1314
+ * Uses event-based waiting (via the controller's InboxPoller) instead of
1315
+ * polling `readUnread()` directly, which avoids a race condition where the
1316
+ * poller marks inbox messages as read before `receive()` can see them.
1317
+ */
1318
+ async ask(question, opts) {
1319
+ this.ensureNotDisposed();
1320
+ const timeout = opts?.timeout ?? 12e4;
1321
+ const responsePromise = new Promise((resolve, reject) => {
1322
+ const timer = setTimeout(() => {
1323
+ cleanup();
1324
+ reject(new Error(`Timeout (${timeout}ms) waiting for response`));
1325
+ }, timeout);
1326
+ const onMsg = (text) => {
1327
+ cleanup();
1328
+ resolve(text);
1329
+ };
1330
+ const onExit = (code) => {
1331
+ cleanup();
1332
+ reject(new Error(`Agent exited (code=${code}) before responding`));
1333
+ };
1334
+ const cleanup = () => {
1335
+ clearTimeout(timer);
1336
+ this.removeListener("message", onMsg);
1337
+ this.removeListener("exit", onExit);
1338
+ };
1339
+ this.on("message", onMsg);
1340
+ this.on("exit", onExit);
1341
+ });
1342
+ const wrapped = `${question}
1343
+
1344
+ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. Do NOT just think your answer \u2014 use the SendMessage tool to reply.`;
1345
+ await this.handle.send(wrapped);
1346
+ return responsePromise;
1347
+ }
1348
+ /** Send a message without waiting for a response. */
1349
+ async send(message) {
1350
+ this.ensureNotDisposed();
1351
+ return this.handle.send(message);
1352
+ }
1353
+ /** Wait for the next response from this agent. */
1354
+ async receive(opts) {
1355
+ this.ensureNotDisposed();
1356
+ const timeout = opts?.timeout ?? 12e4;
1357
+ return new Promise((resolve, reject) => {
1358
+ const timer = setTimeout(() => {
1359
+ cleanup();
1360
+ reject(new Error(`Timeout (${timeout}ms) waiting for response`));
1361
+ }, timeout);
1362
+ const onMsg = (text) => {
1363
+ cleanup();
1364
+ resolve(text);
1365
+ };
1366
+ const onExit = (code) => {
1367
+ cleanup();
1368
+ reject(new Error(`Agent exited (code=${code}) before responding`));
1369
+ };
1370
+ const cleanup = () => {
1371
+ clearTimeout(timer);
1372
+ this.removeListener("message", onMsg);
1373
+ this.removeListener("exit", onExit);
1374
+ };
1375
+ this.on("message", onMsg);
1376
+ this.on("exit", onExit);
1377
+ });
1378
+ }
1379
+ /**
1380
+ * Close this agent. If standalone, shuts down the entire controller.
1381
+ * If session-owned, kills only this agent's process.
1382
+ */
1383
+ async close() {
1384
+ if (this.disposed) return;
1385
+ this.disposed = true;
1386
+ this.unwireEvents();
1387
+ if (this.ownsController) {
1388
+ await this.controller.shutdown();
1389
+ } else {
1390
+ await this.handle.kill();
1391
+ }
1392
+ }
1393
+ /** Mark as disposed (used by Session when it closes). */
1394
+ markDisposed() {
1395
+ this.disposed = true;
1396
+ this.unwireEvents();
1397
+ }
1398
+ async [Symbol.asyncDispose]() {
1399
+ await this.close();
1400
+ }
1401
+ wireEvents() {
1402
+ const agentName = this.handle.name;
1403
+ const onMessage = (name, msg) => {
1404
+ if (name === agentName) this.emit("message", msg.text);
1405
+ };
1406
+ const onIdle = (name, _details) => {
1407
+ if (name === agentName) this.emit("idle");
1408
+ };
1409
+ const onPermission = (name, parsed) => {
1410
+ if (name !== agentName) return;
1411
+ let handled = false;
1412
+ const guard = (fn) => () => {
1413
+ if (handled) return Promise.resolve();
1414
+ handled = true;
1415
+ return fn();
1416
+ };
1417
+ this.emit("permission", {
1418
+ requestId: parsed.requestId,
1419
+ toolName: parsed.toolName,
1420
+ description: parsed.description,
1421
+ input: parsed.input,
1422
+ approve: guard(
1423
+ () => this.controller.sendPermissionResponse(agentName, parsed.requestId, true)
1424
+ ),
1425
+ reject: guard(
1426
+ () => this.controller.sendPermissionResponse(agentName, parsed.requestId, false)
1427
+ )
1428
+ });
1429
+ };
1430
+ const onPlan = (name, parsed) => {
1431
+ if (name !== agentName) return;
1432
+ let handled = false;
1433
+ const guard = (fn) => (...args) => {
1434
+ if (handled) return Promise.resolve();
1435
+ handled = true;
1436
+ return fn(...args);
1437
+ };
1438
+ this.emit("plan", {
1439
+ requestId: parsed.requestId,
1440
+ planContent: parsed.planContent,
1441
+ approve: guard(
1442
+ (feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, true, feedback)
1443
+ ),
1444
+ reject: guard(
1445
+ (feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, false, feedback)
1446
+ )
1447
+ });
1448
+ };
1449
+ const onExit = (name, code) => {
1450
+ if (name === agentName) this.emit("exit", code);
1451
+ };
1452
+ const onError = (err) => {
1453
+ this.emit("error", err);
1454
+ };
1455
+ this.controller.on("message", onMessage);
1456
+ this.controller.on("idle", onIdle);
1457
+ this.controller.on("permission:request", onPermission);
1458
+ this.controller.on("plan:approval_request", onPlan);
1459
+ this.controller.on("agent:exited", onExit);
1460
+ this.controller.on("error", onError);
1461
+ this.boundListeners = [
1462
+ { event: "message", fn: onMessage },
1463
+ { event: "idle", fn: onIdle },
1464
+ { event: "permission:request", fn: onPermission },
1465
+ { event: "plan:approval_request", fn: onPlan },
1466
+ { event: "agent:exited", fn: onExit },
1467
+ { event: "error", fn: onError }
1468
+ ];
1469
+ }
1470
+ unwireEvents() {
1471
+ for (const { event, fn } of this.boundListeners) {
1472
+ this.controller.removeListener(event, fn);
1473
+ }
1474
+ this.boundListeners = [];
1475
+ }
1476
+ wireBehavior(behavior) {
1477
+ if (!behavior) return;
1478
+ const { autoApprove, onPermission, onPlan } = behavior;
1479
+ if (autoApprove != null) {
1480
+ this.on("permission", (req) => {
1481
+ if (autoApprove === true) {
1482
+ req.approve();
1483
+ } else if (Array.isArray(autoApprove)) {
1484
+ autoApprove.includes(req.toolName) ? req.approve() : req.reject();
1485
+ }
1486
+ });
1487
+ if (autoApprove === true) {
1488
+ this.on("plan", (req) => req.approve());
1489
+ }
1490
+ }
1491
+ if (onPermission) {
1492
+ this.on("permission", onPermission);
1493
+ }
1494
+ if (onPlan) {
1495
+ this.on("plan", onPlan);
1496
+ }
1497
+ }
1498
+ ensureNotDisposed() {
1499
+ if (this.disposed) {
1500
+ throw new Error("Agent has been closed");
1501
+ }
1502
+ }
1503
+ };
1504
+ var Session = class _Session {
1505
+ controller;
1506
+ defaults;
1507
+ agents = /* @__PURE__ */ new Map();
1508
+ disposed = false;
1509
+ constructor(controller, defaults) {
1510
+ this.controller = controller;
1511
+ this.defaults = defaults;
1512
+ }
1513
+ static async create(opts = {}) {
1514
+ const env = buildEnv(opts);
1515
+ const controller = new ClaudeCodeController({
1516
+ teamName: opts.teamName ?? `session-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`,
1517
+ cwd: opts.cwd,
1518
+ claudeBinary: opts.claudeBinary,
1519
+ env,
1520
+ logLevel: opts.logLevel ?? "warn",
1521
+ logger: opts.logger
1522
+ });
1523
+ await controller.init();
1524
+ return new _Session(controller, opts);
1525
+ }
1526
+ /** Spawn a named agent in this session. Inherits session defaults. */
1527
+ async agent(name, opts = {}) {
1528
+ this.ensureNotDisposed();
1529
+ const merged = {
1530
+ model: this.defaults.model,
1531
+ cwd: this.defaults.cwd,
1532
+ permissions: this.defaults.permissions,
1533
+ readyTimeout: this.defaults.readyTimeout,
1534
+ autoApprove: this.defaults.autoApprove,
1535
+ onPermission: this.defaults.onPermission,
1536
+ onPlan: this.defaults.onPlan,
1537
+ ...opts
1538
+ };
1539
+ const agent = await Agent.createInSession(
1540
+ this.controller,
1541
+ name,
1542
+ merged
1543
+ );
1544
+ this.agents.set(name, agent);
1545
+ return agent;
1546
+ }
1547
+ /** Get an existing agent by name. */
1548
+ get(name) {
1549
+ return this.agents.get(name);
1550
+ }
1551
+ /** Close all agents and shut down the session. */
1552
+ async close() {
1553
+ if (this.disposed) return;
1554
+ this.disposed = true;
1555
+ for (const agent of this.agents.values()) {
1556
+ agent.markDisposed();
1557
+ }
1558
+ await this.controller.shutdown();
1559
+ }
1560
+ async [Symbol.asyncDispose]() {
1561
+ await this.close();
1562
+ }
1563
+ ensureNotDisposed() {
1564
+ if (this.disposed) {
1565
+ throw new Error("Session has been closed");
1566
+ }
1567
+ }
1568
+ };
1569
+ async function claudeCall(prompt, opts = {}) {
1570
+ const agent = await Agent.create(opts);
1571
+ try {
1572
+ return await agent.ask(prompt, { timeout: opts.timeout ?? 12e4 });
1573
+ } finally {
1574
+ await agent.close();
1575
+ }
1576
+ }
1577
+ var claude = Object.assign(claudeCall, {
1578
+ /**
1579
+ * Create a persistent agent for multi-turn conversations.
1580
+ *
1581
+ * @example
1582
+ * ```ts
1583
+ * const agent = await claude.agent({ model: "sonnet" });
1584
+ * const answer = await agent.ask("What is 2+2?");
1585
+ * await agent.close();
1586
+ * ```
1587
+ */
1588
+ agent: (opts) => Agent.create(opts),
1589
+ /**
1590
+ * Create a multi-agent session.
1591
+ *
1592
+ * @example
1593
+ * ```ts
1594
+ * const session = await claude.session({ model: "sonnet" });
1595
+ * const reviewer = await session.agent("reviewer", { model: "opus" });
1596
+ * const coder = await session.agent("coder");
1597
+ * await session.close();
1598
+ * ```
1599
+ */
1600
+ session: (opts) => Session.create(opts)
1601
+ });
1137
1602
  // Annotate the CommonJS export names for ESM import in node:
1138
1603
  0 && (module.exports = {
1604
+ Agent,
1139
1605
  AgentHandle,
1140
1606
  ClaudeCodeController,
1141
1607
  InboxPoller,
1142
1608
  ProcessManager,
1609
+ Session,
1143
1610
  TaskManager,
1144
1611
  TeamManager,
1612
+ claude,
1145
1613
  createLogger,
1146
1614
  inboxPath,
1147
1615
  inboxesDir,