claude-code-controller 0.3.0 → 0.4.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/api/index.js CHANGED
@@ -19,7 +19,7 @@ var ActionTracker = class {
19
19
  requestId: parsed.requestId,
20
20
  timestamp: parsed.timestamp,
21
21
  planContent: parsed.planContent,
22
- action: `POST /agents/${agent}/approve-plan`
22
+ action: `POST /agents/${agent}/approve`
23
23
  });
24
24
  };
25
25
  const onPermission = (agent, parsed) => {
@@ -30,7 +30,7 @@ var ActionTracker = class {
30
30
  timestamp: parsed.timestamp,
31
31
  toolName: parsed.toolName,
32
32
  description: parsed.description,
33
- action: `POST /agents/${agent}/approve-permission`
33
+ action: `POST /agents/${agent}/approve`
34
34
  });
35
35
  };
36
36
  const onIdle = (agent) => {
@@ -789,6 +789,11 @@ function createLogger(level = "info") {
789
789
  }
790
790
 
791
791
  // src/controller.ts
792
+ var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
793
+ "shutdown_approved",
794
+ "plan_approval_response",
795
+ "permission_response"
796
+ ]);
792
797
  var AGENT_COLORS = [
793
798
  "#00FF00",
794
799
  "#00BFFF",
@@ -982,11 +987,7 @@ var ClaudeCodeController = class extends EventEmitter {
982
987
  const unread = await readUnread(this.teamName, "controller");
983
988
  const fromAgent = unread.filter((m) => m.from === agentName);
984
989
  if (fromAgent.length > 0) {
985
- const PROTOCOL_TYPES = /* @__PURE__ */ new Set([
986
- "shutdown_approved",
987
- "plan_approval_response",
988
- "permission_response"
989
- ]);
990
+ const PROTOCOL_TYPES = PROTOCOL_ONLY_TYPES;
990
991
  const meaningful = fromAgent.filter((m) => {
991
992
  const parsed = parseMessage(m);
992
993
  return parsed.type !== "idle_notification" && !PROTOCOL_TYPES.has(parsed.type);
@@ -1019,7 +1020,7 @@ var ClaudeCodeController = class extends EventEmitter {
1019
1020
  const unread = await readUnread(this.teamName, "controller");
1020
1021
  const meaningful = unread.filter((m) => {
1021
1022
  const parsed = parseMessage(m);
1022
- return parsed.type !== "idle_notification";
1023
+ return parsed.type !== "idle_notification" && !PROTOCOL_ONLY_TYPES.has(parsed.type);
1023
1024
  });
1024
1025
  if (meaningful.length > 0) {
1025
1026
  return meaningful[0];
@@ -1181,6 +1182,454 @@ function sleep2(ms) {
1181
1182
  return new Promise((r) => setTimeout(r, ms));
1182
1183
  }
1183
1184
 
1185
+ // src/claude.ts
1186
+ import { EventEmitter as EventEmitter2 } from "events";
1187
+ import { randomUUID as randomUUID3 } from "crypto";
1188
+ Symbol.asyncDispose ??= /* @__PURE__ */ Symbol("Symbol.asyncDispose");
1189
+ function buildEnv(opts) {
1190
+ const env = { ...opts.env };
1191
+ if (opts.apiKey) env.ANTHROPIC_AUTH_TOKEN = opts.apiKey;
1192
+ if (opts.baseUrl) env.ANTHROPIC_BASE_URL = opts.baseUrl;
1193
+ if (opts.timeout != null) env.API_TIMEOUT_MS = String(opts.timeout);
1194
+ return env;
1195
+ }
1196
+ function resolvePermissions(preset) {
1197
+ switch (preset) {
1198
+ case "edit":
1199
+ return { permissionMode: "acceptEdits" };
1200
+ case "plan":
1201
+ return { permissionMode: "plan" };
1202
+ case "ask":
1203
+ return { permissionMode: "default" };
1204
+ case "full":
1205
+ default:
1206
+ return { permissionMode: void 0 };
1207
+ }
1208
+ }
1209
+ function waitForReady(controller, agentName, timeoutMs = 15e3) {
1210
+ return new Promise((resolve, reject) => {
1211
+ let settled = false;
1212
+ const timer = setTimeout(() => {
1213
+ if (settled) return;
1214
+ cleanup();
1215
+ reject(
1216
+ new Error(
1217
+ `Agent "${agentName}" did not become ready within ${timeoutMs}ms`
1218
+ )
1219
+ );
1220
+ }, timeoutMs);
1221
+ const onReady = (name) => {
1222
+ if (name === agentName && !settled) {
1223
+ settled = true;
1224
+ cleanup();
1225
+ resolve();
1226
+ }
1227
+ };
1228
+ const onExit = (name, code) => {
1229
+ if (name === agentName && !settled) {
1230
+ settled = true;
1231
+ cleanup();
1232
+ reject(
1233
+ new Error(
1234
+ `Agent "${agentName}" exited before becoming ready (code=${code})`
1235
+ )
1236
+ );
1237
+ }
1238
+ };
1239
+ const onSpawned = (name) => {
1240
+ if (name === agentName && !settled) {
1241
+ settled = true;
1242
+ cleanup();
1243
+ resolve();
1244
+ }
1245
+ };
1246
+ const cleanup = () => {
1247
+ clearTimeout(timer);
1248
+ controller.removeListener("idle", onReady);
1249
+ controller.removeListener("message", onReady);
1250
+ controller.removeListener("agent:spawned", onSpawned);
1251
+ controller.removeListener("agent:exited", onExit);
1252
+ };
1253
+ controller.on("idle", onReady);
1254
+ controller.on("message", onReady);
1255
+ controller.on("agent:spawned", onSpawned);
1256
+ controller.on("agent:exited", onExit);
1257
+ });
1258
+ }
1259
+ var Agent = class _Agent extends EventEmitter2 {
1260
+ controller;
1261
+ handle;
1262
+ ownsController;
1263
+ disposed = false;
1264
+ boundListeners = [];
1265
+ constructor(controller, handle, ownsController, behavior) {
1266
+ super();
1267
+ this.controller = controller;
1268
+ this.handle = handle;
1269
+ this.ownsController = ownsController;
1270
+ this.wireEvents();
1271
+ this.wireBehavior(behavior);
1272
+ }
1273
+ /** Create a standalone agent (owns its own controller). */
1274
+ static async create(opts = {}) {
1275
+ const name = opts.name ?? `agent-${randomUUID3().slice(0, 8)}`;
1276
+ const env = buildEnv(opts);
1277
+ const { permissionMode } = resolvePermissions(opts.permissions);
1278
+ const controller = new ClaudeCodeController({
1279
+ teamName: `claude-${randomUUID3().slice(0, 8)}`,
1280
+ cwd: opts.cwd,
1281
+ claudeBinary: opts.claudeBinary,
1282
+ env,
1283
+ logLevel: opts.logLevel ?? "warn",
1284
+ logger: opts.logger
1285
+ });
1286
+ await controller.init();
1287
+ const ready = waitForReady(controller, name, opts.readyTimeout);
1288
+ try {
1289
+ const handle = await controller.spawnAgent({
1290
+ name,
1291
+ type: opts.type ?? "general-purpose",
1292
+ model: opts.model,
1293
+ cwd: opts.cwd,
1294
+ permissionMode
1295
+ });
1296
+ const agent = new _Agent(controller, handle, true, {
1297
+ autoApprove: opts.autoApprove,
1298
+ onPermission: opts.onPermission,
1299
+ onPlan: opts.onPlan
1300
+ });
1301
+ await ready;
1302
+ return agent;
1303
+ } catch (err) {
1304
+ await controller.shutdown().catch(() => {
1305
+ });
1306
+ throw err;
1307
+ }
1308
+ }
1309
+ /** Create an agent within an existing session (session owns the controller). */
1310
+ static async createInSession(controller, name, opts = {}) {
1311
+ const { permissionMode } = resolvePermissions(opts.permissions);
1312
+ const ready = waitForReady(controller, name, opts.readyTimeout);
1313
+ const handle = await controller.spawnAgent({
1314
+ name,
1315
+ type: opts.type ?? "general-purpose",
1316
+ model: opts.model,
1317
+ cwd: opts.cwd,
1318
+ permissionMode,
1319
+ env: opts.env
1320
+ });
1321
+ const agent = new _Agent(controller, handle, false, {
1322
+ autoApprove: opts.autoApprove,
1323
+ onPermission: opts.onPermission,
1324
+ onPlan: opts.onPlan
1325
+ });
1326
+ await ready;
1327
+ return agent;
1328
+ }
1329
+ /** The agent's name. */
1330
+ get name() {
1331
+ return this.handle.name;
1332
+ }
1333
+ /** The agent process PID. */
1334
+ get pid() {
1335
+ return this.handle.pid;
1336
+ }
1337
+ /** Whether the agent process is still running. */
1338
+ get isRunning() {
1339
+ return this.handle.isRunning;
1340
+ }
1341
+ /**
1342
+ * Send a message and wait for the response.
1343
+ *
1344
+ * Uses event-based waiting (via the controller's InboxPoller) instead of
1345
+ * polling `readUnread()` directly, which avoids a race condition where the
1346
+ * poller marks inbox messages as read before `receive()` can see them.
1347
+ */
1348
+ async ask(question, opts) {
1349
+ this.ensureNotDisposed();
1350
+ const timeout = opts?.timeout ?? 12e4;
1351
+ const responsePromise = new Promise((resolve, reject) => {
1352
+ const timer = setTimeout(() => {
1353
+ cleanup();
1354
+ reject(new Error(`Timeout (${timeout}ms) waiting for response`));
1355
+ }, timeout);
1356
+ const onMsg = (text) => {
1357
+ cleanup();
1358
+ resolve(text);
1359
+ };
1360
+ const onExit = (code) => {
1361
+ cleanup();
1362
+ reject(new Error(`Agent exited (code=${code}) before responding`));
1363
+ };
1364
+ const cleanup = () => {
1365
+ clearTimeout(timer);
1366
+ this.removeListener("message", onMsg);
1367
+ this.removeListener("exit", onExit);
1368
+ };
1369
+ this.on("message", onMsg);
1370
+ this.on("exit", onExit);
1371
+ });
1372
+ const wrapped = `${question}
1373
+
1374
+ 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.`;
1375
+ await this.handle.send(wrapped);
1376
+ return responsePromise;
1377
+ }
1378
+ /** Send a message without waiting for a response. */
1379
+ async send(message) {
1380
+ this.ensureNotDisposed();
1381
+ return this.handle.send(message);
1382
+ }
1383
+ /** Wait for the next response from this agent. */
1384
+ async receive(opts) {
1385
+ this.ensureNotDisposed();
1386
+ const timeout = opts?.timeout ?? 12e4;
1387
+ return new Promise((resolve, reject) => {
1388
+ const timer = setTimeout(() => {
1389
+ cleanup();
1390
+ reject(new Error(`Timeout (${timeout}ms) waiting for response`));
1391
+ }, timeout);
1392
+ const onMsg = (text) => {
1393
+ cleanup();
1394
+ resolve(text);
1395
+ };
1396
+ const onExit = (code) => {
1397
+ cleanup();
1398
+ reject(new Error(`Agent exited (code=${code}) before responding`));
1399
+ };
1400
+ const cleanup = () => {
1401
+ clearTimeout(timer);
1402
+ this.removeListener("message", onMsg);
1403
+ this.removeListener("exit", onExit);
1404
+ };
1405
+ this.on("message", onMsg);
1406
+ this.on("exit", onExit);
1407
+ });
1408
+ }
1409
+ /**
1410
+ * Close this agent. If standalone, shuts down the entire controller.
1411
+ * If session-owned, kills only this agent's process.
1412
+ */
1413
+ async close() {
1414
+ if (this.disposed) return;
1415
+ this.disposed = true;
1416
+ this.unwireEvents();
1417
+ if (this.ownsController) {
1418
+ await this.controller.shutdown();
1419
+ } else {
1420
+ await this.handle.kill();
1421
+ }
1422
+ }
1423
+ /** Mark as disposed (used by Session when it closes). */
1424
+ markDisposed() {
1425
+ this.disposed = true;
1426
+ this.unwireEvents();
1427
+ }
1428
+ async [Symbol.asyncDispose]() {
1429
+ await this.close();
1430
+ }
1431
+ wireEvents() {
1432
+ const agentName = this.handle.name;
1433
+ const onMessage = (name, msg) => {
1434
+ if (name === agentName) this.emit("message", msg.text);
1435
+ };
1436
+ const onIdle = (name) => {
1437
+ if (name === agentName) this.emit("idle");
1438
+ };
1439
+ const onPermission = (name, parsed) => {
1440
+ if (name !== agentName) return;
1441
+ let handled = false;
1442
+ const guard = (fn) => () => {
1443
+ if (handled) return Promise.resolve();
1444
+ handled = true;
1445
+ return fn();
1446
+ };
1447
+ this.emit("permission", {
1448
+ requestId: parsed.requestId,
1449
+ toolName: parsed.toolName,
1450
+ description: parsed.description,
1451
+ input: parsed.input,
1452
+ approve: guard(
1453
+ () => this.controller.sendPermissionResponse(agentName, parsed.requestId, true)
1454
+ ),
1455
+ reject: guard(
1456
+ () => this.controller.sendPermissionResponse(agentName, parsed.requestId, false)
1457
+ )
1458
+ });
1459
+ };
1460
+ const onPlan = (name, parsed) => {
1461
+ if (name !== agentName) return;
1462
+ let handled = false;
1463
+ const guard = (fn) => (...args) => {
1464
+ if (handled) return Promise.resolve();
1465
+ handled = true;
1466
+ return fn(...args);
1467
+ };
1468
+ this.emit("plan", {
1469
+ requestId: parsed.requestId,
1470
+ planContent: parsed.planContent,
1471
+ approve: guard(
1472
+ (feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, true, feedback)
1473
+ ),
1474
+ reject: guard(
1475
+ (feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, false, feedback)
1476
+ )
1477
+ });
1478
+ };
1479
+ const onExit = (name, code) => {
1480
+ if (name === agentName) this.emit("exit", code);
1481
+ };
1482
+ const onError = (err) => {
1483
+ this.emit("error", err);
1484
+ };
1485
+ this.controller.on("message", onMessage);
1486
+ this.controller.on("idle", onIdle);
1487
+ this.controller.on("permission:request", onPermission);
1488
+ this.controller.on("plan:approval_request", onPlan);
1489
+ this.controller.on("agent:exited", onExit);
1490
+ this.controller.on("error", onError);
1491
+ this.boundListeners = [
1492
+ { event: "message", fn: onMessage },
1493
+ { event: "idle", fn: onIdle },
1494
+ { event: "permission:request", fn: onPermission },
1495
+ { event: "plan:approval_request", fn: onPlan },
1496
+ { event: "agent:exited", fn: onExit },
1497
+ { event: "error", fn: onError }
1498
+ ];
1499
+ }
1500
+ unwireEvents() {
1501
+ for (const { event, fn } of this.boundListeners) {
1502
+ this.controller.removeListener(event, fn);
1503
+ }
1504
+ this.boundListeners = [];
1505
+ }
1506
+ wireBehavior(behavior) {
1507
+ if (!behavior) return;
1508
+ const { autoApprove, onPermission, onPlan } = behavior;
1509
+ if (autoApprove != null) {
1510
+ this.on("permission", (req) => {
1511
+ if (autoApprove === true) {
1512
+ req.approve();
1513
+ } else if (Array.isArray(autoApprove)) {
1514
+ autoApprove.includes(req.toolName) ? req.approve() : req.reject();
1515
+ }
1516
+ });
1517
+ if (autoApprove === true) {
1518
+ this.on("plan", (req) => req.approve());
1519
+ }
1520
+ }
1521
+ if (onPermission) {
1522
+ this.on("permission", onPermission);
1523
+ }
1524
+ if (onPlan) {
1525
+ this.on("plan", onPlan);
1526
+ }
1527
+ }
1528
+ ensureNotDisposed() {
1529
+ if (this.disposed) {
1530
+ throw new Error("Agent has been closed");
1531
+ }
1532
+ }
1533
+ };
1534
+ var Session = class _Session {
1535
+ controller;
1536
+ defaults;
1537
+ agents = /* @__PURE__ */ new Map();
1538
+ disposed = false;
1539
+ constructor(controller, defaults) {
1540
+ this.controller = controller;
1541
+ this.defaults = defaults;
1542
+ }
1543
+ static async create(opts = {}) {
1544
+ const env = buildEnv(opts);
1545
+ const controller = new ClaudeCodeController({
1546
+ teamName: opts.teamName ?? `session-${randomUUID3().slice(0, 8)}`,
1547
+ cwd: opts.cwd,
1548
+ claudeBinary: opts.claudeBinary,
1549
+ env,
1550
+ logLevel: opts.logLevel ?? "warn",
1551
+ logger: opts.logger
1552
+ });
1553
+ await controller.init();
1554
+ return new _Session(controller, opts);
1555
+ }
1556
+ /** Spawn a named agent in this session. Inherits session defaults. */
1557
+ async agent(name, opts = {}) {
1558
+ this.ensureNotDisposed();
1559
+ const merged = {
1560
+ model: this.defaults.model,
1561
+ cwd: this.defaults.cwd,
1562
+ permissions: this.defaults.permissions,
1563
+ readyTimeout: this.defaults.readyTimeout,
1564
+ autoApprove: this.defaults.autoApprove,
1565
+ onPermission: this.defaults.onPermission,
1566
+ onPlan: this.defaults.onPlan,
1567
+ ...opts
1568
+ };
1569
+ const agent = await Agent.createInSession(
1570
+ this.controller,
1571
+ name,
1572
+ merged
1573
+ );
1574
+ this.agents.set(name, agent);
1575
+ return agent;
1576
+ }
1577
+ /** Get an existing agent by name. */
1578
+ get(name) {
1579
+ return this.agents.get(name);
1580
+ }
1581
+ /** Close all agents and shut down the session. */
1582
+ async close() {
1583
+ if (this.disposed) return;
1584
+ this.disposed = true;
1585
+ for (const agent of this.agents.values()) {
1586
+ agent.markDisposed();
1587
+ }
1588
+ await this.controller.shutdown();
1589
+ }
1590
+ async [Symbol.asyncDispose]() {
1591
+ await this.close();
1592
+ }
1593
+ ensureNotDisposed() {
1594
+ if (this.disposed) {
1595
+ throw new Error("Session has been closed");
1596
+ }
1597
+ }
1598
+ };
1599
+ async function claudeCall(prompt, opts = {}) {
1600
+ const agent = await Agent.create(opts);
1601
+ try {
1602
+ return await agent.ask(prompt, { timeout: opts.timeout ?? 12e4 });
1603
+ } finally {
1604
+ await agent.close();
1605
+ }
1606
+ }
1607
+ var claude = Object.assign(claudeCall, {
1608
+ /**
1609
+ * Create a persistent agent for multi-turn conversations.
1610
+ *
1611
+ * @example
1612
+ * ```ts
1613
+ * const agent = await claude.agent({ model: "sonnet" });
1614
+ * const answer = await agent.ask("What is 2+2?");
1615
+ * await agent.close();
1616
+ * ```
1617
+ */
1618
+ agent: (opts) => Agent.create(opts),
1619
+ /**
1620
+ * Create a multi-agent session.
1621
+ *
1622
+ * @example
1623
+ * ```ts
1624
+ * const session = await claude.session({ model: "sonnet" });
1625
+ * const reviewer = await session.agent("reviewer", { model: "opus" });
1626
+ * const coder = await session.agent("coder");
1627
+ * await session.close();
1628
+ * ```
1629
+ */
1630
+ session: (opts) => Session.create(opts)
1631
+ });
1632
+
1184
1633
  // src/api/routes.ts
1185
1634
  var SAFE_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
1186
1635
  var SAFE_TASK_ID_RE = /^[0-9]{1,10}$/;
@@ -1222,6 +1671,28 @@ function buildRoutes(state) {
1222
1671
  session: state.controller !== null
1223
1672
  });
1224
1673
  });
1674
+ api.post("/ask", async (c) => {
1675
+ const body = await c.req.json();
1676
+ if (!body.prompt) {
1677
+ return c.json({ error: "prompt is required" }, 400);
1678
+ }
1679
+ try {
1680
+ const response = await claude(body.prompt, {
1681
+ model: body.model,
1682
+ apiKey: body.apiKey,
1683
+ baseUrl: body.baseUrl,
1684
+ timeout: body.timeout,
1685
+ cwd: body.cwd,
1686
+ permissions: body.permissions,
1687
+ env: body.env,
1688
+ logLevel: "warn"
1689
+ });
1690
+ return c.json({ response });
1691
+ } catch (err) {
1692
+ const message = err instanceof Error ? err.message : "Agent failed to respond";
1693
+ return c.json({ error: message }, 500);
1694
+ }
1695
+ });
1225
1696
  api.get("/session", (c) => {
1226
1697
  if (!state.controller) {
1227
1698
  return c.json({ initialized: false, teamName: "" });
@@ -1247,11 +1718,15 @@ function buildRoutes(state) {
1247
1718
  await oldController.shutdown();
1248
1719
  }
1249
1720
  }
1721
+ const env = { ...body.env };
1722
+ if (body.apiKey) env.ANTHROPIC_AUTH_TOKEN = body.apiKey;
1723
+ if (body.baseUrl) env.ANTHROPIC_BASE_URL = body.baseUrl;
1724
+ if (body.timeout != null) env.API_TIMEOUT_MS = String(body.timeout);
1250
1725
  const controller = new ClaudeCodeController({
1251
1726
  teamName: body.teamName,
1252
1727
  cwd: body.cwd,
1253
1728
  claudeBinary: body.claudeBinary,
1254
- env: body.env,
1729
+ env,
1255
1730
  logLevel: body.logLevel ?? "info"
1256
1731
  });
1257
1732
  try {
@@ -1308,26 +1783,6 @@ function buildRoutes(state) {
1308
1783
  const pending = approvals.length + unassignedTasks.length + idleAgents.length;
1309
1784
  return c.json({ pending, approvals, unassignedTasks, idleAgents });
1310
1785
  });
1311
- api.get("/actions/approvals", (_c) => {
1312
- getController(state);
1313
- return _c.json(state.tracker.getPendingApprovals());
1314
- });
1315
- api.get("/actions/tasks", async (c) => {
1316
- const ctrl = getController(state);
1317
- const tasks = await ctrl.tasks.list();
1318
- const unassigned = tasks.filter((t) => !t.owner && t.status !== "completed").map((t) => ({
1319
- id: t.id,
1320
- subject: t.subject,
1321
- description: t.description,
1322
- status: t.status,
1323
- action: `POST /tasks/${t.id}/assign`
1324
- }));
1325
- return c.json(unassigned);
1326
- });
1327
- api.get("/actions/idle-agents", (_c) => {
1328
- getController(state);
1329
- return _c.json(state.tracker.getIdleAgents());
1330
- });
1331
1786
  api.get("/agents", async (c) => {
1332
1787
  const ctrl = getController(state);
1333
1788
  const config = await ctrl.team.getConfig();
@@ -1348,13 +1803,25 @@ function buildRoutes(state) {
1348
1803
  validateName(body.name, "name");
1349
1804
  const agentType = body.type || "general-purpose";
1350
1805
  state.tracker.registerAgentType(body.name, agentType);
1806
+ const agentEnv = { ...body.env };
1807
+ if (body.apiKey) agentEnv.ANTHROPIC_AUTH_TOKEN = body.apiKey;
1808
+ if (body.baseUrl) agentEnv.ANTHROPIC_BASE_URL = body.baseUrl;
1809
+ if (body.timeout != null) agentEnv.API_TIMEOUT_MS = String(body.timeout);
1810
+ const permissionsArray = Array.isArray(body.permissions) ? body.permissions : void 0;
1811
+ const PRESET_MAP = {
1812
+ edit: "acceptEdits",
1813
+ plan: "plan",
1814
+ ask: "default"
1815
+ };
1816
+ const permissionMode = typeof body.permissions === "string" && !Array.isArray(body.permissions) ? PRESET_MAP[body.permissions] : void 0;
1351
1817
  const handle = await ctrl.spawnAgent({
1352
1818
  name: body.name,
1353
1819
  type: body.type,
1354
1820
  model: body.model,
1355
1821
  cwd: body.cwd,
1356
- permissions: body.permissions,
1357
- env: body.env
1822
+ permissions: permissionsArray,
1823
+ permissionMode,
1824
+ env: Object.keys(agentEnv).length > 0 ? agentEnv : void 0
1358
1825
  });
1359
1826
  return c.json(
1360
1827
  {
@@ -1406,7 +1873,7 @@ function buildRoutes(state) {
1406
1873
  await ctrl.sendShutdownRequest(name);
1407
1874
  return c.json({ ok: true });
1408
1875
  });
1409
- api.post("/agents/:name/approve-plan", async (c) => {
1876
+ api.post("/agents/:name/approve", async (c) => {
1410
1877
  const ctrl = getController(state);
1411
1878
  const name = c.req.param("name");
1412
1879
  validateName(name, "name");
@@ -1414,28 +1881,14 @@ function buildRoutes(state) {
1414
1881
  if (!body.requestId) {
1415
1882
  return c.json({ error: "requestId is required" }, 400);
1416
1883
  }
1417
- await ctrl.sendPlanApproval(
1418
- name,
1419
- body.requestId,
1420
- body.approve ?? true,
1421
- body.feedback
1422
- );
1423
- state.tracker.resolveApproval(body.requestId);
1424
- return c.json({ ok: true });
1425
- });
1426
- api.post("/agents/:name/approve-permission", async (c) => {
1427
- const ctrl = getController(state);
1428
- const name = c.req.param("name");
1429
- validateName(name, "name");
1430
- const body = await c.req.json();
1431
- if (!body.requestId) {
1432
- return c.json({ error: "requestId is required" }, 400);
1884
+ if (!body.type || !["plan", "permission"].includes(body.type)) {
1885
+ return c.json({ error: 'type must be "plan" or "permission"' }, 400);
1886
+ }
1887
+ if (body.type === "plan") {
1888
+ await ctrl.sendPlanApproval(name, body.requestId, body.approve ?? true, body.feedback);
1889
+ } else {
1890
+ await ctrl.sendPermissionResponse(name, body.requestId, body.approve ?? true);
1433
1891
  }
1434
- await ctrl.sendPermissionResponse(
1435
- name,
1436
- body.requestId,
1437
- body.approve ?? true
1438
- );
1439
1892
  state.tracker.resolveApproval(body.requestId);
1440
1893
  return c.json({ ok: true });
1441
1894
  });