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.js CHANGED
@@ -1,3 +1,7 @@
1
+ // src/claude.ts
2
+ import { EventEmitter as EventEmitter2 } from "events";
3
+ import { randomUUID as randomUUID3 } from "crypto";
4
+
1
5
  // src/controller.ts
2
6
  import { EventEmitter } from "events";
3
7
  import { execSync as execSync2 } from "child_process";
@@ -368,6 +372,7 @@ else:
368
372
  stdio: ["pipe", "pipe", "pipe"],
369
373
  env: {
370
374
  ...process.env,
375
+ CLAUDECODE: "1",
371
376
  CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1",
372
377
  ...opts.env
373
378
  }
@@ -698,6 +703,14 @@ var silentLogger = {
698
703
  };
699
704
 
700
705
  // src/controller.ts
706
+ var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
707
+ "shutdown_approved",
708
+ "plan_approval_response",
709
+ "permission_response",
710
+ "task_completed",
711
+ "sandbox_permission_request",
712
+ "sandbox_permission_response"
713
+ ]);
701
714
  var AGENT_COLORS = [
702
715
  "#00FF00",
703
716
  "#00BFFF",
@@ -804,6 +817,10 @@ var ClaudeCodeController = class extends EventEmitter {
804
817
  name: opts.name,
805
818
  agentType: opts.type || "general-purpose",
806
819
  model: opts.model,
820
+ prompt: opts.prompt,
821
+ color,
822
+ planModeRequired: false,
823
+ backendType: "in-process",
807
824
  joinedAt: Date.now(),
808
825
  tmuxPaneId: "",
809
826
  cwd,
@@ -891,11 +908,7 @@ var ClaudeCodeController = class extends EventEmitter {
891
908
  const unread = await readUnread(this.teamName, "controller");
892
909
  const fromAgent = unread.filter((m) => m.from === agentName);
893
910
  if (fromAgent.length > 0) {
894
- const PROTOCOL_TYPES = /* @__PURE__ */ new Set([
895
- "shutdown_approved",
896
- "plan_approval_response",
897
- "permission_response"
898
- ]);
911
+ const PROTOCOL_TYPES = PROTOCOL_ONLY_TYPES;
899
912
  const meaningful = fromAgent.filter((m) => {
900
913
  const parsed = parseMessage(m);
901
914
  return parsed.type !== "idle_notification" && !PROTOCOL_TYPES.has(parsed.type);
@@ -928,7 +941,7 @@ var ClaudeCodeController = class extends EventEmitter {
928
941
  const unread = await readUnread(this.teamName, "controller");
929
942
  const meaningful = unread.filter((m) => {
930
943
  const parsed = parseMessage(m);
931
- return parsed.type !== "idle_notification";
944
+ return parsed.type !== "idle_notification" && !PROTOCOL_ONLY_TYPES.has(parsed.type);
932
945
  });
933
946
  if (meaningful.length > 0) {
934
947
  return meaningful[0];
@@ -1050,7 +1063,7 @@ var ClaudeCodeController = class extends EventEmitter {
1050
1063
  const { raw, parsed } = event;
1051
1064
  switch (parsed.type) {
1052
1065
  case "idle_notification":
1053
- this.emit("idle", raw.from);
1066
+ this.emit("idle", raw.from, parsed);
1054
1067
  break;
1055
1068
  case "shutdown_approved":
1056
1069
  this.log.info(
@@ -1070,6 +1083,9 @@ var ClaudeCodeController = class extends EventEmitter {
1070
1083
  );
1071
1084
  this.emit("permission:request", raw.from, parsed);
1072
1085
  break;
1086
+ case "task_completed":
1087
+ this.emit("message", raw.from, raw);
1088
+ break;
1073
1089
  case "plain_text":
1074
1090
  this.emit("message", raw.from, raw);
1075
1091
  break;
@@ -1089,13 +1105,462 @@ var ClaudeCodeController = class extends EventEmitter {
1089
1105
  function sleep2(ms) {
1090
1106
  return new Promise((r) => setTimeout(r, ms));
1091
1107
  }
1108
+
1109
+ // src/claude.ts
1110
+ Symbol.asyncDispose ??= /* @__PURE__ */ Symbol("Symbol.asyncDispose");
1111
+ function buildEnv(opts) {
1112
+ const env = { ...opts.env };
1113
+ if (opts.apiKey) env.ANTHROPIC_AUTH_TOKEN = opts.apiKey;
1114
+ if (opts.baseUrl) env.ANTHROPIC_BASE_URL = opts.baseUrl;
1115
+ if (opts.timeout != null) env.API_TIMEOUT_MS = String(opts.timeout);
1116
+ return env;
1117
+ }
1118
+ function resolvePermissions(preset) {
1119
+ switch (preset) {
1120
+ case "edit":
1121
+ return { permissionMode: "acceptEdits" };
1122
+ case "plan":
1123
+ return { permissionMode: "plan" };
1124
+ case "ask":
1125
+ return { permissionMode: "default" };
1126
+ case "full":
1127
+ default:
1128
+ return { permissionMode: void 0 };
1129
+ }
1130
+ }
1131
+ function waitForReady(controller, agentName, timeoutMs = 15e3) {
1132
+ return new Promise((resolve, reject) => {
1133
+ let settled = false;
1134
+ const timer = setTimeout(() => {
1135
+ if (settled) return;
1136
+ cleanup();
1137
+ reject(
1138
+ new Error(
1139
+ `Agent "${agentName}" did not become ready within ${timeoutMs}ms`
1140
+ )
1141
+ );
1142
+ }, timeoutMs);
1143
+ const onReady = (name, ..._rest) => {
1144
+ if (name === agentName && !settled) {
1145
+ settled = true;
1146
+ cleanup();
1147
+ resolve();
1148
+ }
1149
+ };
1150
+ const onExit = (name, code) => {
1151
+ if (name === agentName && !settled) {
1152
+ settled = true;
1153
+ cleanup();
1154
+ reject(
1155
+ new Error(
1156
+ `Agent "${agentName}" exited before becoming ready (code=${code})`
1157
+ )
1158
+ );
1159
+ }
1160
+ };
1161
+ const onSpawned = (name) => {
1162
+ if (name === agentName && !settled) {
1163
+ settled = true;
1164
+ cleanup();
1165
+ resolve();
1166
+ }
1167
+ };
1168
+ const cleanup = () => {
1169
+ clearTimeout(timer);
1170
+ controller.removeListener("idle", onReady);
1171
+ controller.removeListener("message", onReady);
1172
+ controller.removeListener("agent:spawned", onSpawned);
1173
+ controller.removeListener("agent:exited", onExit);
1174
+ };
1175
+ controller.on("idle", onReady);
1176
+ controller.on("message", onReady);
1177
+ controller.on("agent:spawned", onSpawned);
1178
+ controller.on("agent:exited", onExit);
1179
+ });
1180
+ }
1181
+ var Agent = class _Agent extends EventEmitter2 {
1182
+ controller;
1183
+ handle;
1184
+ ownsController;
1185
+ disposed = false;
1186
+ boundListeners = [];
1187
+ constructor(controller, handle, ownsController, behavior) {
1188
+ super();
1189
+ this.controller = controller;
1190
+ this.handle = handle;
1191
+ this.ownsController = ownsController;
1192
+ this.wireEvents();
1193
+ this.wireBehavior(behavior);
1194
+ }
1195
+ /** Create a standalone agent (owns its own controller). */
1196
+ static async create(opts = {}) {
1197
+ const name = opts.name ?? `agent-${randomUUID3().slice(0, 8)}`;
1198
+ const env = buildEnv(opts);
1199
+ const { permissionMode } = resolvePermissions(opts.permissions);
1200
+ const controller = new ClaudeCodeController({
1201
+ teamName: `claude-${randomUUID3().slice(0, 8)}`,
1202
+ cwd: opts.cwd,
1203
+ claudeBinary: opts.claudeBinary,
1204
+ env,
1205
+ logLevel: opts.logLevel ?? "warn",
1206
+ logger: opts.logger
1207
+ });
1208
+ await controller.init();
1209
+ const ready = waitForReady(controller, name, opts.readyTimeout);
1210
+ try {
1211
+ const handle = await controller.spawnAgent({
1212
+ name,
1213
+ type: opts.type ?? "general-purpose",
1214
+ model: opts.model,
1215
+ cwd: opts.cwd,
1216
+ permissionMode
1217
+ });
1218
+ const agent = new _Agent(controller, handle, true, {
1219
+ autoApprove: opts.autoApprove,
1220
+ onPermission: opts.onPermission,
1221
+ onPlan: opts.onPlan
1222
+ });
1223
+ await ready;
1224
+ return agent;
1225
+ } catch (err) {
1226
+ await controller.shutdown().catch(() => {
1227
+ });
1228
+ throw err;
1229
+ }
1230
+ }
1231
+ /** Create an agent within an existing session (session owns the controller). */
1232
+ static async createInSession(controller, name, opts = {}) {
1233
+ const { permissionMode } = resolvePermissions(opts.permissions);
1234
+ const ready = waitForReady(controller, name, opts.readyTimeout);
1235
+ const handle = await controller.spawnAgent({
1236
+ name,
1237
+ type: opts.type ?? "general-purpose",
1238
+ model: opts.model,
1239
+ cwd: opts.cwd,
1240
+ permissionMode,
1241
+ env: opts.env
1242
+ });
1243
+ const agent = new _Agent(controller, handle, false, {
1244
+ autoApprove: opts.autoApprove,
1245
+ onPermission: opts.onPermission,
1246
+ onPlan: opts.onPlan
1247
+ });
1248
+ await ready;
1249
+ return agent;
1250
+ }
1251
+ /** The agent's name. */
1252
+ get name() {
1253
+ return this.handle.name;
1254
+ }
1255
+ /** The agent process PID. */
1256
+ get pid() {
1257
+ return this.handle.pid;
1258
+ }
1259
+ /** Whether the agent process is still running. */
1260
+ get isRunning() {
1261
+ return this.handle.isRunning;
1262
+ }
1263
+ /**
1264
+ * Send a message and wait for the response.
1265
+ *
1266
+ * Uses event-based waiting (via the controller's InboxPoller) instead of
1267
+ * polling `readUnread()` directly, which avoids a race condition where the
1268
+ * poller marks inbox messages as read before `receive()` can see them.
1269
+ */
1270
+ async ask(question, opts) {
1271
+ this.ensureNotDisposed();
1272
+ const timeout = opts?.timeout ?? 12e4;
1273
+ const responsePromise = new Promise((resolve, reject) => {
1274
+ const timer = setTimeout(() => {
1275
+ cleanup();
1276
+ reject(new Error(`Timeout (${timeout}ms) waiting for response`));
1277
+ }, timeout);
1278
+ const onMsg = (text) => {
1279
+ cleanup();
1280
+ resolve(text);
1281
+ };
1282
+ const onExit = (code) => {
1283
+ cleanup();
1284
+ reject(new Error(`Agent exited (code=${code}) before responding`));
1285
+ };
1286
+ const cleanup = () => {
1287
+ clearTimeout(timer);
1288
+ this.removeListener("message", onMsg);
1289
+ this.removeListener("exit", onExit);
1290
+ };
1291
+ this.on("message", onMsg);
1292
+ this.on("exit", onExit);
1293
+ });
1294
+ const wrapped = `${question}
1295
+
1296
+ 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.`;
1297
+ await this.handle.send(wrapped);
1298
+ return responsePromise;
1299
+ }
1300
+ /** Send a message without waiting for a response. */
1301
+ async send(message) {
1302
+ this.ensureNotDisposed();
1303
+ return this.handle.send(message);
1304
+ }
1305
+ /** Wait for the next response from this agent. */
1306
+ async receive(opts) {
1307
+ this.ensureNotDisposed();
1308
+ const timeout = opts?.timeout ?? 12e4;
1309
+ return new Promise((resolve, reject) => {
1310
+ const timer = setTimeout(() => {
1311
+ cleanup();
1312
+ reject(new Error(`Timeout (${timeout}ms) waiting for response`));
1313
+ }, timeout);
1314
+ const onMsg = (text) => {
1315
+ cleanup();
1316
+ resolve(text);
1317
+ };
1318
+ const onExit = (code) => {
1319
+ cleanup();
1320
+ reject(new Error(`Agent exited (code=${code}) before responding`));
1321
+ };
1322
+ const cleanup = () => {
1323
+ clearTimeout(timer);
1324
+ this.removeListener("message", onMsg);
1325
+ this.removeListener("exit", onExit);
1326
+ };
1327
+ this.on("message", onMsg);
1328
+ this.on("exit", onExit);
1329
+ });
1330
+ }
1331
+ /**
1332
+ * Close this agent. If standalone, shuts down the entire controller.
1333
+ * If session-owned, kills only this agent's process.
1334
+ */
1335
+ async close() {
1336
+ if (this.disposed) return;
1337
+ this.disposed = true;
1338
+ this.unwireEvents();
1339
+ if (this.ownsController) {
1340
+ await this.controller.shutdown();
1341
+ } else {
1342
+ await this.handle.kill();
1343
+ }
1344
+ }
1345
+ /** Mark as disposed (used by Session when it closes). */
1346
+ markDisposed() {
1347
+ this.disposed = true;
1348
+ this.unwireEvents();
1349
+ }
1350
+ async [Symbol.asyncDispose]() {
1351
+ await this.close();
1352
+ }
1353
+ wireEvents() {
1354
+ const agentName = this.handle.name;
1355
+ const onMessage = (name, msg) => {
1356
+ if (name === agentName) this.emit("message", msg.text);
1357
+ };
1358
+ const onIdle = (name, _details) => {
1359
+ if (name === agentName) this.emit("idle");
1360
+ };
1361
+ const onPermission = (name, parsed) => {
1362
+ if (name !== agentName) return;
1363
+ let handled = false;
1364
+ const guard = (fn) => () => {
1365
+ if (handled) return Promise.resolve();
1366
+ handled = true;
1367
+ return fn();
1368
+ };
1369
+ this.emit("permission", {
1370
+ requestId: parsed.requestId,
1371
+ toolName: parsed.toolName,
1372
+ description: parsed.description,
1373
+ input: parsed.input,
1374
+ approve: guard(
1375
+ () => this.controller.sendPermissionResponse(agentName, parsed.requestId, true)
1376
+ ),
1377
+ reject: guard(
1378
+ () => this.controller.sendPermissionResponse(agentName, parsed.requestId, false)
1379
+ )
1380
+ });
1381
+ };
1382
+ const onPlan = (name, parsed) => {
1383
+ if (name !== agentName) return;
1384
+ let handled = false;
1385
+ const guard = (fn) => (...args) => {
1386
+ if (handled) return Promise.resolve();
1387
+ handled = true;
1388
+ return fn(...args);
1389
+ };
1390
+ this.emit("plan", {
1391
+ requestId: parsed.requestId,
1392
+ planContent: parsed.planContent,
1393
+ approve: guard(
1394
+ (feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, true, feedback)
1395
+ ),
1396
+ reject: guard(
1397
+ (feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, false, feedback)
1398
+ )
1399
+ });
1400
+ };
1401
+ const onExit = (name, code) => {
1402
+ if (name === agentName) this.emit("exit", code);
1403
+ };
1404
+ const onError = (err) => {
1405
+ this.emit("error", err);
1406
+ };
1407
+ this.controller.on("message", onMessage);
1408
+ this.controller.on("idle", onIdle);
1409
+ this.controller.on("permission:request", onPermission);
1410
+ this.controller.on("plan:approval_request", onPlan);
1411
+ this.controller.on("agent:exited", onExit);
1412
+ this.controller.on("error", onError);
1413
+ this.boundListeners = [
1414
+ { event: "message", fn: onMessage },
1415
+ { event: "idle", fn: onIdle },
1416
+ { event: "permission:request", fn: onPermission },
1417
+ { event: "plan:approval_request", fn: onPlan },
1418
+ { event: "agent:exited", fn: onExit },
1419
+ { event: "error", fn: onError }
1420
+ ];
1421
+ }
1422
+ unwireEvents() {
1423
+ for (const { event, fn } of this.boundListeners) {
1424
+ this.controller.removeListener(event, fn);
1425
+ }
1426
+ this.boundListeners = [];
1427
+ }
1428
+ wireBehavior(behavior) {
1429
+ if (!behavior) return;
1430
+ const { autoApprove, onPermission, onPlan } = behavior;
1431
+ if (autoApprove != null) {
1432
+ this.on("permission", (req) => {
1433
+ if (autoApprove === true) {
1434
+ req.approve();
1435
+ } else if (Array.isArray(autoApprove)) {
1436
+ autoApprove.includes(req.toolName) ? req.approve() : req.reject();
1437
+ }
1438
+ });
1439
+ if (autoApprove === true) {
1440
+ this.on("plan", (req) => req.approve());
1441
+ }
1442
+ }
1443
+ if (onPermission) {
1444
+ this.on("permission", onPermission);
1445
+ }
1446
+ if (onPlan) {
1447
+ this.on("plan", onPlan);
1448
+ }
1449
+ }
1450
+ ensureNotDisposed() {
1451
+ if (this.disposed) {
1452
+ throw new Error("Agent has been closed");
1453
+ }
1454
+ }
1455
+ };
1456
+ var Session = class _Session {
1457
+ controller;
1458
+ defaults;
1459
+ agents = /* @__PURE__ */ new Map();
1460
+ disposed = false;
1461
+ constructor(controller, defaults) {
1462
+ this.controller = controller;
1463
+ this.defaults = defaults;
1464
+ }
1465
+ static async create(opts = {}) {
1466
+ const env = buildEnv(opts);
1467
+ const controller = new ClaudeCodeController({
1468
+ teamName: opts.teamName ?? `session-${randomUUID3().slice(0, 8)}`,
1469
+ cwd: opts.cwd,
1470
+ claudeBinary: opts.claudeBinary,
1471
+ env,
1472
+ logLevel: opts.logLevel ?? "warn",
1473
+ logger: opts.logger
1474
+ });
1475
+ await controller.init();
1476
+ return new _Session(controller, opts);
1477
+ }
1478
+ /** Spawn a named agent in this session. Inherits session defaults. */
1479
+ async agent(name, opts = {}) {
1480
+ this.ensureNotDisposed();
1481
+ const merged = {
1482
+ model: this.defaults.model,
1483
+ cwd: this.defaults.cwd,
1484
+ permissions: this.defaults.permissions,
1485
+ readyTimeout: this.defaults.readyTimeout,
1486
+ autoApprove: this.defaults.autoApprove,
1487
+ onPermission: this.defaults.onPermission,
1488
+ onPlan: this.defaults.onPlan,
1489
+ ...opts
1490
+ };
1491
+ const agent = await Agent.createInSession(
1492
+ this.controller,
1493
+ name,
1494
+ merged
1495
+ );
1496
+ this.agents.set(name, agent);
1497
+ return agent;
1498
+ }
1499
+ /** Get an existing agent by name. */
1500
+ get(name) {
1501
+ return this.agents.get(name);
1502
+ }
1503
+ /** Close all agents and shut down the session. */
1504
+ async close() {
1505
+ if (this.disposed) return;
1506
+ this.disposed = true;
1507
+ for (const agent of this.agents.values()) {
1508
+ agent.markDisposed();
1509
+ }
1510
+ await this.controller.shutdown();
1511
+ }
1512
+ async [Symbol.asyncDispose]() {
1513
+ await this.close();
1514
+ }
1515
+ ensureNotDisposed() {
1516
+ if (this.disposed) {
1517
+ throw new Error("Session has been closed");
1518
+ }
1519
+ }
1520
+ };
1521
+ async function claudeCall(prompt, opts = {}) {
1522
+ const agent = await Agent.create(opts);
1523
+ try {
1524
+ return await agent.ask(prompt, { timeout: opts.timeout ?? 12e4 });
1525
+ } finally {
1526
+ await agent.close();
1527
+ }
1528
+ }
1529
+ var claude = Object.assign(claudeCall, {
1530
+ /**
1531
+ * Create a persistent agent for multi-turn conversations.
1532
+ *
1533
+ * @example
1534
+ * ```ts
1535
+ * const agent = await claude.agent({ model: "sonnet" });
1536
+ * const answer = await agent.ask("What is 2+2?");
1537
+ * await agent.close();
1538
+ * ```
1539
+ */
1540
+ agent: (opts) => Agent.create(opts),
1541
+ /**
1542
+ * Create a multi-agent session.
1543
+ *
1544
+ * @example
1545
+ * ```ts
1546
+ * const session = await claude.session({ model: "sonnet" });
1547
+ * const reviewer = await session.agent("reviewer", { model: "opus" });
1548
+ * const coder = await session.agent("coder");
1549
+ * await session.close();
1550
+ * ```
1551
+ */
1552
+ session: (opts) => Session.create(opts)
1553
+ });
1092
1554
  export {
1555
+ Agent,
1093
1556
  AgentHandle,
1094
1557
  ClaudeCodeController,
1095
1558
  InboxPoller,
1096
1559
  ProcessManager,
1560
+ Session,
1097
1561
  TaskManager,
1098
1562
  TeamManager,
1563
+ claude,
1099
1564
  createLogger,
1100
1565
  inboxPath,
1101
1566
  inboxesDir,