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/README.md +285 -302
- package/dist/api/index.cjs +518 -54
- package/dist/api/index.cjs.map +1 -1
- package/dist/api/index.d.cts +36 -8
- package/dist/api/index.d.ts +36 -8
- package/dist/api/index.js +518 -54
- package/dist/api/index.js.map +1 -1
- package/dist/{controller-CqCBbQYK.d.cts → claude-CSXlMCvP.d.cts} +271 -4
- package/dist/{controller-CqCBbQYK.d.ts → claude-CSXlMCvP.d.ts} +271 -4
- package/dist/index.cjs +475 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +472 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/api/index.cjs
CHANGED
|
@@ -44,7 +44,7 @@ var ActionTracker = class {
|
|
|
44
44
|
requestId: parsed.requestId,
|
|
45
45
|
timestamp: parsed.timestamp,
|
|
46
46
|
planContent: parsed.planContent,
|
|
47
|
-
action: `POST /agents/${agent}/approve
|
|
47
|
+
action: `POST /agents/${agent}/approve`
|
|
48
48
|
});
|
|
49
49
|
};
|
|
50
50
|
const onPermission = (agent, parsed) => {
|
|
@@ -55,7 +55,7 @@ var ActionTracker = class {
|
|
|
55
55
|
timestamp: parsed.timestamp,
|
|
56
56
|
toolName: parsed.toolName,
|
|
57
57
|
description: parsed.description,
|
|
58
|
-
action: `POST /agents/${agent}/approve
|
|
58
|
+
action: `POST /agents/${agent}/approve`
|
|
59
59
|
});
|
|
60
60
|
};
|
|
61
61
|
const onIdle = (agent) => {
|
|
@@ -500,6 +500,7 @@ else:
|
|
|
500
500
|
stdio: ["pipe", "pipe", "pipe"],
|
|
501
501
|
env: {
|
|
502
502
|
...process.env,
|
|
503
|
+
CLAUDECODE: "1",
|
|
503
504
|
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1",
|
|
504
505
|
...opts.env
|
|
505
506
|
}
|
|
@@ -814,6 +815,14 @@ function createLogger(level = "info") {
|
|
|
814
815
|
}
|
|
815
816
|
|
|
816
817
|
// src/controller.ts
|
|
818
|
+
var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
819
|
+
"shutdown_approved",
|
|
820
|
+
"plan_approval_response",
|
|
821
|
+
"permission_response",
|
|
822
|
+
"task_completed",
|
|
823
|
+
"sandbox_permission_request",
|
|
824
|
+
"sandbox_permission_response"
|
|
825
|
+
]);
|
|
817
826
|
var AGENT_COLORS = [
|
|
818
827
|
"#00FF00",
|
|
819
828
|
"#00BFFF",
|
|
@@ -920,6 +929,10 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
920
929
|
name: opts.name,
|
|
921
930
|
agentType: opts.type || "general-purpose",
|
|
922
931
|
model: opts.model,
|
|
932
|
+
prompt: opts.prompt,
|
|
933
|
+
color,
|
|
934
|
+
planModeRequired: false,
|
|
935
|
+
backendType: "in-process",
|
|
923
936
|
joinedAt: Date.now(),
|
|
924
937
|
tmuxPaneId: "",
|
|
925
938
|
cwd,
|
|
@@ -1007,11 +1020,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
1007
1020
|
const unread = await readUnread(this.teamName, "controller");
|
|
1008
1021
|
const fromAgent = unread.filter((m) => m.from === agentName);
|
|
1009
1022
|
if (fromAgent.length > 0) {
|
|
1010
|
-
const PROTOCOL_TYPES =
|
|
1011
|
-
"shutdown_approved",
|
|
1012
|
-
"plan_approval_response",
|
|
1013
|
-
"permission_response"
|
|
1014
|
-
]);
|
|
1023
|
+
const PROTOCOL_TYPES = PROTOCOL_ONLY_TYPES;
|
|
1015
1024
|
const meaningful = fromAgent.filter((m) => {
|
|
1016
1025
|
const parsed = parseMessage(m);
|
|
1017
1026
|
return parsed.type !== "idle_notification" && !PROTOCOL_TYPES.has(parsed.type);
|
|
@@ -1044,7 +1053,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
1044
1053
|
const unread = await readUnread(this.teamName, "controller");
|
|
1045
1054
|
const meaningful = unread.filter((m) => {
|
|
1046
1055
|
const parsed = parseMessage(m);
|
|
1047
|
-
return parsed.type !== "idle_notification";
|
|
1056
|
+
return parsed.type !== "idle_notification" && !PROTOCOL_ONLY_TYPES.has(parsed.type);
|
|
1048
1057
|
});
|
|
1049
1058
|
if (meaningful.length > 0) {
|
|
1050
1059
|
return meaningful[0];
|
|
@@ -1166,7 +1175,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
1166
1175
|
const { raw, parsed } = event;
|
|
1167
1176
|
switch (parsed.type) {
|
|
1168
1177
|
case "idle_notification":
|
|
1169
|
-
this.emit("idle", raw.from);
|
|
1178
|
+
this.emit("idle", raw.from, parsed);
|
|
1170
1179
|
break;
|
|
1171
1180
|
case "shutdown_approved":
|
|
1172
1181
|
this.log.info(
|
|
@@ -1186,6 +1195,9 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
1186
1195
|
);
|
|
1187
1196
|
this.emit("permission:request", raw.from, parsed);
|
|
1188
1197
|
break;
|
|
1198
|
+
case "task_completed":
|
|
1199
|
+
this.emit("message", raw.from, raw);
|
|
1200
|
+
break;
|
|
1189
1201
|
case "plain_text":
|
|
1190
1202
|
this.emit("message", raw.from, raw);
|
|
1191
1203
|
break;
|
|
@@ -1206,6 +1218,454 @@ function sleep2(ms) {
|
|
|
1206
1218
|
return new Promise((r) => setTimeout(r, ms));
|
|
1207
1219
|
}
|
|
1208
1220
|
|
|
1221
|
+
// src/claude.ts
|
|
1222
|
+
var import_node_events2 = require("events");
|
|
1223
|
+
var import_node_crypto3 = require("crypto");
|
|
1224
|
+
Symbol.asyncDispose ??= /* @__PURE__ */ Symbol("Symbol.asyncDispose");
|
|
1225
|
+
function buildEnv(opts) {
|
|
1226
|
+
const env = { ...opts.env };
|
|
1227
|
+
if (opts.apiKey) env.ANTHROPIC_AUTH_TOKEN = opts.apiKey;
|
|
1228
|
+
if (opts.baseUrl) env.ANTHROPIC_BASE_URL = opts.baseUrl;
|
|
1229
|
+
if (opts.timeout != null) env.API_TIMEOUT_MS = String(opts.timeout);
|
|
1230
|
+
return env;
|
|
1231
|
+
}
|
|
1232
|
+
function resolvePermissions(preset) {
|
|
1233
|
+
switch (preset) {
|
|
1234
|
+
case "edit":
|
|
1235
|
+
return { permissionMode: "acceptEdits" };
|
|
1236
|
+
case "plan":
|
|
1237
|
+
return { permissionMode: "plan" };
|
|
1238
|
+
case "ask":
|
|
1239
|
+
return { permissionMode: "default" };
|
|
1240
|
+
case "full":
|
|
1241
|
+
default:
|
|
1242
|
+
return { permissionMode: void 0 };
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
function waitForReady(controller, agentName, timeoutMs = 15e3) {
|
|
1246
|
+
return new Promise((resolve, reject) => {
|
|
1247
|
+
let settled = false;
|
|
1248
|
+
const timer = setTimeout(() => {
|
|
1249
|
+
if (settled) return;
|
|
1250
|
+
cleanup();
|
|
1251
|
+
reject(
|
|
1252
|
+
new Error(
|
|
1253
|
+
`Agent "${agentName}" did not become ready within ${timeoutMs}ms`
|
|
1254
|
+
)
|
|
1255
|
+
);
|
|
1256
|
+
}, timeoutMs);
|
|
1257
|
+
const onReady = (name, ..._rest) => {
|
|
1258
|
+
if (name === agentName && !settled) {
|
|
1259
|
+
settled = true;
|
|
1260
|
+
cleanup();
|
|
1261
|
+
resolve();
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
const onExit = (name, code) => {
|
|
1265
|
+
if (name === agentName && !settled) {
|
|
1266
|
+
settled = true;
|
|
1267
|
+
cleanup();
|
|
1268
|
+
reject(
|
|
1269
|
+
new Error(
|
|
1270
|
+
`Agent "${agentName}" exited before becoming ready (code=${code})`
|
|
1271
|
+
)
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
const onSpawned = (name) => {
|
|
1276
|
+
if (name === agentName && !settled) {
|
|
1277
|
+
settled = true;
|
|
1278
|
+
cleanup();
|
|
1279
|
+
resolve();
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
const cleanup = () => {
|
|
1283
|
+
clearTimeout(timer);
|
|
1284
|
+
controller.removeListener("idle", onReady);
|
|
1285
|
+
controller.removeListener("message", onReady);
|
|
1286
|
+
controller.removeListener("agent:spawned", onSpawned);
|
|
1287
|
+
controller.removeListener("agent:exited", onExit);
|
|
1288
|
+
};
|
|
1289
|
+
controller.on("idle", onReady);
|
|
1290
|
+
controller.on("message", onReady);
|
|
1291
|
+
controller.on("agent:spawned", onSpawned);
|
|
1292
|
+
controller.on("agent:exited", onExit);
|
|
1293
|
+
});
|
|
1294
|
+
}
|
|
1295
|
+
var Agent = class _Agent extends import_node_events2.EventEmitter {
|
|
1296
|
+
controller;
|
|
1297
|
+
handle;
|
|
1298
|
+
ownsController;
|
|
1299
|
+
disposed = false;
|
|
1300
|
+
boundListeners = [];
|
|
1301
|
+
constructor(controller, handle, ownsController, behavior) {
|
|
1302
|
+
super();
|
|
1303
|
+
this.controller = controller;
|
|
1304
|
+
this.handle = handle;
|
|
1305
|
+
this.ownsController = ownsController;
|
|
1306
|
+
this.wireEvents();
|
|
1307
|
+
this.wireBehavior(behavior);
|
|
1308
|
+
}
|
|
1309
|
+
/** Create a standalone agent (owns its own controller). */
|
|
1310
|
+
static async create(opts = {}) {
|
|
1311
|
+
const name = opts.name ?? `agent-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`;
|
|
1312
|
+
const env = buildEnv(opts);
|
|
1313
|
+
const { permissionMode } = resolvePermissions(opts.permissions);
|
|
1314
|
+
const controller = new ClaudeCodeController({
|
|
1315
|
+
teamName: `claude-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`,
|
|
1316
|
+
cwd: opts.cwd,
|
|
1317
|
+
claudeBinary: opts.claudeBinary,
|
|
1318
|
+
env,
|
|
1319
|
+
logLevel: opts.logLevel ?? "warn",
|
|
1320
|
+
logger: opts.logger
|
|
1321
|
+
});
|
|
1322
|
+
await controller.init();
|
|
1323
|
+
const ready = waitForReady(controller, name, opts.readyTimeout);
|
|
1324
|
+
try {
|
|
1325
|
+
const handle = await controller.spawnAgent({
|
|
1326
|
+
name,
|
|
1327
|
+
type: opts.type ?? "general-purpose",
|
|
1328
|
+
model: opts.model,
|
|
1329
|
+
cwd: opts.cwd,
|
|
1330
|
+
permissionMode
|
|
1331
|
+
});
|
|
1332
|
+
const agent = new _Agent(controller, handle, true, {
|
|
1333
|
+
autoApprove: opts.autoApprove,
|
|
1334
|
+
onPermission: opts.onPermission,
|
|
1335
|
+
onPlan: opts.onPlan
|
|
1336
|
+
});
|
|
1337
|
+
await ready;
|
|
1338
|
+
return agent;
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
await controller.shutdown().catch(() => {
|
|
1341
|
+
});
|
|
1342
|
+
throw err;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
/** Create an agent within an existing session (session owns the controller). */
|
|
1346
|
+
static async createInSession(controller, name, opts = {}) {
|
|
1347
|
+
const { permissionMode } = resolvePermissions(opts.permissions);
|
|
1348
|
+
const ready = waitForReady(controller, name, opts.readyTimeout);
|
|
1349
|
+
const handle = await controller.spawnAgent({
|
|
1350
|
+
name,
|
|
1351
|
+
type: opts.type ?? "general-purpose",
|
|
1352
|
+
model: opts.model,
|
|
1353
|
+
cwd: opts.cwd,
|
|
1354
|
+
permissionMode,
|
|
1355
|
+
env: opts.env
|
|
1356
|
+
});
|
|
1357
|
+
const agent = new _Agent(controller, handle, false, {
|
|
1358
|
+
autoApprove: opts.autoApprove,
|
|
1359
|
+
onPermission: opts.onPermission,
|
|
1360
|
+
onPlan: opts.onPlan
|
|
1361
|
+
});
|
|
1362
|
+
await ready;
|
|
1363
|
+
return agent;
|
|
1364
|
+
}
|
|
1365
|
+
/** The agent's name. */
|
|
1366
|
+
get name() {
|
|
1367
|
+
return this.handle.name;
|
|
1368
|
+
}
|
|
1369
|
+
/** The agent process PID. */
|
|
1370
|
+
get pid() {
|
|
1371
|
+
return this.handle.pid;
|
|
1372
|
+
}
|
|
1373
|
+
/** Whether the agent process is still running. */
|
|
1374
|
+
get isRunning() {
|
|
1375
|
+
return this.handle.isRunning;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Send a message and wait for the response.
|
|
1379
|
+
*
|
|
1380
|
+
* Uses event-based waiting (via the controller's InboxPoller) instead of
|
|
1381
|
+
* polling `readUnread()` directly, which avoids a race condition where the
|
|
1382
|
+
* poller marks inbox messages as read before `receive()` can see them.
|
|
1383
|
+
*/
|
|
1384
|
+
async ask(question, opts) {
|
|
1385
|
+
this.ensureNotDisposed();
|
|
1386
|
+
const timeout = opts?.timeout ?? 12e4;
|
|
1387
|
+
const responsePromise = 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
|
+
const wrapped = `${question}
|
|
1409
|
+
|
|
1410
|
+
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.`;
|
|
1411
|
+
await this.handle.send(wrapped);
|
|
1412
|
+
return responsePromise;
|
|
1413
|
+
}
|
|
1414
|
+
/** Send a message without waiting for a response. */
|
|
1415
|
+
async send(message) {
|
|
1416
|
+
this.ensureNotDisposed();
|
|
1417
|
+
return this.handle.send(message);
|
|
1418
|
+
}
|
|
1419
|
+
/** Wait for the next response from this agent. */
|
|
1420
|
+
async receive(opts) {
|
|
1421
|
+
this.ensureNotDisposed();
|
|
1422
|
+
const timeout = opts?.timeout ?? 12e4;
|
|
1423
|
+
return new Promise((resolve, reject) => {
|
|
1424
|
+
const timer = setTimeout(() => {
|
|
1425
|
+
cleanup();
|
|
1426
|
+
reject(new Error(`Timeout (${timeout}ms) waiting for response`));
|
|
1427
|
+
}, timeout);
|
|
1428
|
+
const onMsg = (text) => {
|
|
1429
|
+
cleanup();
|
|
1430
|
+
resolve(text);
|
|
1431
|
+
};
|
|
1432
|
+
const onExit = (code) => {
|
|
1433
|
+
cleanup();
|
|
1434
|
+
reject(new Error(`Agent exited (code=${code}) before responding`));
|
|
1435
|
+
};
|
|
1436
|
+
const cleanup = () => {
|
|
1437
|
+
clearTimeout(timer);
|
|
1438
|
+
this.removeListener("message", onMsg);
|
|
1439
|
+
this.removeListener("exit", onExit);
|
|
1440
|
+
};
|
|
1441
|
+
this.on("message", onMsg);
|
|
1442
|
+
this.on("exit", onExit);
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Close this agent. If standalone, shuts down the entire controller.
|
|
1447
|
+
* If session-owned, kills only this agent's process.
|
|
1448
|
+
*/
|
|
1449
|
+
async close() {
|
|
1450
|
+
if (this.disposed) return;
|
|
1451
|
+
this.disposed = true;
|
|
1452
|
+
this.unwireEvents();
|
|
1453
|
+
if (this.ownsController) {
|
|
1454
|
+
await this.controller.shutdown();
|
|
1455
|
+
} else {
|
|
1456
|
+
await this.handle.kill();
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
/** Mark as disposed (used by Session when it closes). */
|
|
1460
|
+
markDisposed() {
|
|
1461
|
+
this.disposed = true;
|
|
1462
|
+
this.unwireEvents();
|
|
1463
|
+
}
|
|
1464
|
+
async [Symbol.asyncDispose]() {
|
|
1465
|
+
await this.close();
|
|
1466
|
+
}
|
|
1467
|
+
wireEvents() {
|
|
1468
|
+
const agentName = this.handle.name;
|
|
1469
|
+
const onMessage = (name, msg) => {
|
|
1470
|
+
if (name === agentName) this.emit("message", msg.text);
|
|
1471
|
+
};
|
|
1472
|
+
const onIdle = (name, _details) => {
|
|
1473
|
+
if (name === agentName) this.emit("idle");
|
|
1474
|
+
};
|
|
1475
|
+
const onPermission = (name, parsed) => {
|
|
1476
|
+
if (name !== agentName) return;
|
|
1477
|
+
let handled = false;
|
|
1478
|
+
const guard = (fn) => () => {
|
|
1479
|
+
if (handled) return Promise.resolve();
|
|
1480
|
+
handled = true;
|
|
1481
|
+
return fn();
|
|
1482
|
+
};
|
|
1483
|
+
this.emit("permission", {
|
|
1484
|
+
requestId: parsed.requestId,
|
|
1485
|
+
toolName: parsed.toolName,
|
|
1486
|
+
description: parsed.description,
|
|
1487
|
+
input: parsed.input,
|
|
1488
|
+
approve: guard(
|
|
1489
|
+
() => this.controller.sendPermissionResponse(agentName, parsed.requestId, true)
|
|
1490
|
+
),
|
|
1491
|
+
reject: guard(
|
|
1492
|
+
() => this.controller.sendPermissionResponse(agentName, parsed.requestId, false)
|
|
1493
|
+
)
|
|
1494
|
+
});
|
|
1495
|
+
};
|
|
1496
|
+
const onPlan = (name, parsed) => {
|
|
1497
|
+
if (name !== agentName) return;
|
|
1498
|
+
let handled = false;
|
|
1499
|
+
const guard = (fn) => (...args) => {
|
|
1500
|
+
if (handled) return Promise.resolve();
|
|
1501
|
+
handled = true;
|
|
1502
|
+
return fn(...args);
|
|
1503
|
+
};
|
|
1504
|
+
this.emit("plan", {
|
|
1505
|
+
requestId: parsed.requestId,
|
|
1506
|
+
planContent: parsed.planContent,
|
|
1507
|
+
approve: guard(
|
|
1508
|
+
(feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, true, feedback)
|
|
1509
|
+
),
|
|
1510
|
+
reject: guard(
|
|
1511
|
+
(feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, false, feedback)
|
|
1512
|
+
)
|
|
1513
|
+
});
|
|
1514
|
+
};
|
|
1515
|
+
const onExit = (name, code) => {
|
|
1516
|
+
if (name === agentName) this.emit("exit", code);
|
|
1517
|
+
};
|
|
1518
|
+
const onError = (err) => {
|
|
1519
|
+
this.emit("error", err);
|
|
1520
|
+
};
|
|
1521
|
+
this.controller.on("message", onMessage);
|
|
1522
|
+
this.controller.on("idle", onIdle);
|
|
1523
|
+
this.controller.on("permission:request", onPermission);
|
|
1524
|
+
this.controller.on("plan:approval_request", onPlan);
|
|
1525
|
+
this.controller.on("agent:exited", onExit);
|
|
1526
|
+
this.controller.on("error", onError);
|
|
1527
|
+
this.boundListeners = [
|
|
1528
|
+
{ event: "message", fn: onMessage },
|
|
1529
|
+
{ event: "idle", fn: onIdle },
|
|
1530
|
+
{ event: "permission:request", fn: onPermission },
|
|
1531
|
+
{ event: "plan:approval_request", fn: onPlan },
|
|
1532
|
+
{ event: "agent:exited", fn: onExit },
|
|
1533
|
+
{ event: "error", fn: onError }
|
|
1534
|
+
];
|
|
1535
|
+
}
|
|
1536
|
+
unwireEvents() {
|
|
1537
|
+
for (const { event, fn } of this.boundListeners) {
|
|
1538
|
+
this.controller.removeListener(event, fn);
|
|
1539
|
+
}
|
|
1540
|
+
this.boundListeners = [];
|
|
1541
|
+
}
|
|
1542
|
+
wireBehavior(behavior) {
|
|
1543
|
+
if (!behavior) return;
|
|
1544
|
+
const { autoApprove, onPermission, onPlan } = behavior;
|
|
1545
|
+
if (autoApprove != null) {
|
|
1546
|
+
this.on("permission", (req) => {
|
|
1547
|
+
if (autoApprove === true) {
|
|
1548
|
+
req.approve();
|
|
1549
|
+
} else if (Array.isArray(autoApprove)) {
|
|
1550
|
+
autoApprove.includes(req.toolName) ? req.approve() : req.reject();
|
|
1551
|
+
}
|
|
1552
|
+
});
|
|
1553
|
+
if (autoApprove === true) {
|
|
1554
|
+
this.on("plan", (req) => req.approve());
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
if (onPermission) {
|
|
1558
|
+
this.on("permission", onPermission);
|
|
1559
|
+
}
|
|
1560
|
+
if (onPlan) {
|
|
1561
|
+
this.on("plan", onPlan);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
ensureNotDisposed() {
|
|
1565
|
+
if (this.disposed) {
|
|
1566
|
+
throw new Error("Agent has been closed");
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
var Session = class _Session {
|
|
1571
|
+
controller;
|
|
1572
|
+
defaults;
|
|
1573
|
+
agents = /* @__PURE__ */ new Map();
|
|
1574
|
+
disposed = false;
|
|
1575
|
+
constructor(controller, defaults) {
|
|
1576
|
+
this.controller = controller;
|
|
1577
|
+
this.defaults = defaults;
|
|
1578
|
+
}
|
|
1579
|
+
static async create(opts = {}) {
|
|
1580
|
+
const env = buildEnv(opts);
|
|
1581
|
+
const controller = new ClaudeCodeController({
|
|
1582
|
+
teamName: opts.teamName ?? `session-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`,
|
|
1583
|
+
cwd: opts.cwd,
|
|
1584
|
+
claudeBinary: opts.claudeBinary,
|
|
1585
|
+
env,
|
|
1586
|
+
logLevel: opts.logLevel ?? "warn",
|
|
1587
|
+
logger: opts.logger
|
|
1588
|
+
});
|
|
1589
|
+
await controller.init();
|
|
1590
|
+
return new _Session(controller, opts);
|
|
1591
|
+
}
|
|
1592
|
+
/** Spawn a named agent in this session. Inherits session defaults. */
|
|
1593
|
+
async agent(name, opts = {}) {
|
|
1594
|
+
this.ensureNotDisposed();
|
|
1595
|
+
const merged = {
|
|
1596
|
+
model: this.defaults.model,
|
|
1597
|
+
cwd: this.defaults.cwd,
|
|
1598
|
+
permissions: this.defaults.permissions,
|
|
1599
|
+
readyTimeout: this.defaults.readyTimeout,
|
|
1600
|
+
autoApprove: this.defaults.autoApprove,
|
|
1601
|
+
onPermission: this.defaults.onPermission,
|
|
1602
|
+
onPlan: this.defaults.onPlan,
|
|
1603
|
+
...opts
|
|
1604
|
+
};
|
|
1605
|
+
const agent = await Agent.createInSession(
|
|
1606
|
+
this.controller,
|
|
1607
|
+
name,
|
|
1608
|
+
merged
|
|
1609
|
+
);
|
|
1610
|
+
this.agents.set(name, agent);
|
|
1611
|
+
return agent;
|
|
1612
|
+
}
|
|
1613
|
+
/** Get an existing agent by name. */
|
|
1614
|
+
get(name) {
|
|
1615
|
+
return this.agents.get(name);
|
|
1616
|
+
}
|
|
1617
|
+
/** Close all agents and shut down the session. */
|
|
1618
|
+
async close() {
|
|
1619
|
+
if (this.disposed) return;
|
|
1620
|
+
this.disposed = true;
|
|
1621
|
+
for (const agent of this.agents.values()) {
|
|
1622
|
+
agent.markDisposed();
|
|
1623
|
+
}
|
|
1624
|
+
await this.controller.shutdown();
|
|
1625
|
+
}
|
|
1626
|
+
async [Symbol.asyncDispose]() {
|
|
1627
|
+
await this.close();
|
|
1628
|
+
}
|
|
1629
|
+
ensureNotDisposed() {
|
|
1630
|
+
if (this.disposed) {
|
|
1631
|
+
throw new Error("Session has been closed");
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
async function claudeCall(prompt, opts = {}) {
|
|
1636
|
+
const agent = await Agent.create(opts);
|
|
1637
|
+
try {
|
|
1638
|
+
return await agent.ask(prompt, { timeout: opts.timeout ?? 12e4 });
|
|
1639
|
+
} finally {
|
|
1640
|
+
await agent.close();
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
var claude = Object.assign(claudeCall, {
|
|
1644
|
+
/**
|
|
1645
|
+
* Create a persistent agent for multi-turn conversations.
|
|
1646
|
+
*
|
|
1647
|
+
* @example
|
|
1648
|
+
* ```ts
|
|
1649
|
+
* const agent = await claude.agent({ model: "sonnet" });
|
|
1650
|
+
* const answer = await agent.ask("What is 2+2?");
|
|
1651
|
+
* await agent.close();
|
|
1652
|
+
* ```
|
|
1653
|
+
*/
|
|
1654
|
+
agent: (opts) => Agent.create(opts),
|
|
1655
|
+
/**
|
|
1656
|
+
* Create a multi-agent session.
|
|
1657
|
+
*
|
|
1658
|
+
* @example
|
|
1659
|
+
* ```ts
|
|
1660
|
+
* const session = await claude.session({ model: "sonnet" });
|
|
1661
|
+
* const reviewer = await session.agent("reviewer", { model: "opus" });
|
|
1662
|
+
* const coder = await session.agent("coder");
|
|
1663
|
+
* await session.close();
|
|
1664
|
+
* ```
|
|
1665
|
+
*/
|
|
1666
|
+
session: (opts) => Session.create(opts)
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1209
1669
|
// src/api/routes.ts
|
|
1210
1670
|
var SAFE_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
1211
1671
|
var SAFE_TASK_ID_RE = /^[0-9]{1,10}$/;
|
|
@@ -1247,6 +1707,28 @@ function buildRoutes(state) {
|
|
|
1247
1707
|
session: state.controller !== null
|
|
1248
1708
|
});
|
|
1249
1709
|
});
|
|
1710
|
+
api.post("/ask", async (c) => {
|
|
1711
|
+
const body = await c.req.json();
|
|
1712
|
+
if (!body.prompt) {
|
|
1713
|
+
return c.json({ error: "prompt is required" }, 400);
|
|
1714
|
+
}
|
|
1715
|
+
try {
|
|
1716
|
+
const response = await claude(body.prompt, {
|
|
1717
|
+
model: body.model,
|
|
1718
|
+
apiKey: body.apiKey,
|
|
1719
|
+
baseUrl: body.baseUrl,
|
|
1720
|
+
timeout: body.timeout,
|
|
1721
|
+
cwd: body.cwd,
|
|
1722
|
+
permissions: body.permissions,
|
|
1723
|
+
env: body.env,
|
|
1724
|
+
logLevel: "warn"
|
|
1725
|
+
});
|
|
1726
|
+
return c.json({ response });
|
|
1727
|
+
} catch (err) {
|
|
1728
|
+
const message = err instanceof Error ? err.message : "Agent failed to respond";
|
|
1729
|
+
return c.json({ error: message }, 500);
|
|
1730
|
+
}
|
|
1731
|
+
});
|
|
1250
1732
|
api.get("/session", (c) => {
|
|
1251
1733
|
if (!state.controller) {
|
|
1252
1734
|
return c.json({ initialized: false, teamName: "" });
|
|
@@ -1272,11 +1754,15 @@ function buildRoutes(state) {
|
|
|
1272
1754
|
await oldController.shutdown();
|
|
1273
1755
|
}
|
|
1274
1756
|
}
|
|
1757
|
+
const env = { ...body.env };
|
|
1758
|
+
if (body.apiKey) env.ANTHROPIC_AUTH_TOKEN = body.apiKey;
|
|
1759
|
+
if (body.baseUrl) env.ANTHROPIC_BASE_URL = body.baseUrl;
|
|
1760
|
+
if (body.timeout != null) env.API_TIMEOUT_MS = String(body.timeout);
|
|
1275
1761
|
const controller = new ClaudeCodeController({
|
|
1276
1762
|
teamName: body.teamName,
|
|
1277
1763
|
cwd: body.cwd,
|
|
1278
1764
|
claudeBinary: body.claudeBinary,
|
|
1279
|
-
env
|
|
1765
|
+
env,
|
|
1280
1766
|
logLevel: body.logLevel ?? "info"
|
|
1281
1767
|
});
|
|
1282
1768
|
try {
|
|
@@ -1333,26 +1819,6 @@ function buildRoutes(state) {
|
|
|
1333
1819
|
const pending = approvals.length + unassignedTasks.length + idleAgents.length;
|
|
1334
1820
|
return c.json({ pending, approvals, unassignedTasks, idleAgents });
|
|
1335
1821
|
});
|
|
1336
|
-
api.get("/actions/approvals", (_c) => {
|
|
1337
|
-
getController(state);
|
|
1338
|
-
return _c.json(state.tracker.getPendingApprovals());
|
|
1339
|
-
});
|
|
1340
|
-
api.get("/actions/tasks", async (c) => {
|
|
1341
|
-
const ctrl = getController(state);
|
|
1342
|
-
const tasks = await ctrl.tasks.list();
|
|
1343
|
-
const unassigned = tasks.filter((t) => !t.owner && t.status !== "completed").map((t) => ({
|
|
1344
|
-
id: t.id,
|
|
1345
|
-
subject: t.subject,
|
|
1346
|
-
description: t.description,
|
|
1347
|
-
status: t.status,
|
|
1348
|
-
action: `POST /tasks/${t.id}/assign`
|
|
1349
|
-
}));
|
|
1350
|
-
return c.json(unassigned);
|
|
1351
|
-
});
|
|
1352
|
-
api.get("/actions/idle-agents", (_c) => {
|
|
1353
|
-
getController(state);
|
|
1354
|
-
return _c.json(state.tracker.getIdleAgents());
|
|
1355
|
-
});
|
|
1356
1822
|
api.get("/agents", async (c) => {
|
|
1357
1823
|
const ctrl = getController(state);
|
|
1358
1824
|
const config = await ctrl.team.getConfig();
|
|
@@ -1373,13 +1839,25 @@ function buildRoutes(state) {
|
|
|
1373
1839
|
validateName(body.name, "name");
|
|
1374
1840
|
const agentType = body.type || "general-purpose";
|
|
1375
1841
|
state.tracker.registerAgentType(body.name, agentType);
|
|
1842
|
+
const agentEnv = { ...body.env };
|
|
1843
|
+
if (body.apiKey) agentEnv.ANTHROPIC_AUTH_TOKEN = body.apiKey;
|
|
1844
|
+
if (body.baseUrl) agentEnv.ANTHROPIC_BASE_URL = body.baseUrl;
|
|
1845
|
+
if (body.timeout != null) agentEnv.API_TIMEOUT_MS = String(body.timeout);
|
|
1846
|
+
const permissionsArray = Array.isArray(body.permissions) ? body.permissions : void 0;
|
|
1847
|
+
const PRESET_MAP = {
|
|
1848
|
+
edit: "acceptEdits",
|
|
1849
|
+
plan: "plan",
|
|
1850
|
+
ask: "default"
|
|
1851
|
+
};
|
|
1852
|
+
const permissionMode = typeof body.permissions === "string" && !Array.isArray(body.permissions) ? PRESET_MAP[body.permissions] : void 0;
|
|
1376
1853
|
const handle = await ctrl.spawnAgent({
|
|
1377
1854
|
name: body.name,
|
|
1378
1855
|
type: body.type,
|
|
1379
1856
|
model: body.model,
|
|
1380
1857
|
cwd: body.cwd,
|
|
1381
|
-
permissions:
|
|
1382
|
-
|
|
1858
|
+
permissions: permissionsArray,
|
|
1859
|
+
permissionMode,
|
|
1860
|
+
env: Object.keys(agentEnv).length > 0 ? agentEnv : void 0
|
|
1383
1861
|
});
|
|
1384
1862
|
return c.json(
|
|
1385
1863
|
{
|
|
@@ -1431,7 +1909,7 @@ function buildRoutes(state) {
|
|
|
1431
1909
|
await ctrl.sendShutdownRequest(name);
|
|
1432
1910
|
return c.json({ ok: true });
|
|
1433
1911
|
});
|
|
1434
|
-
api.post("/agents/:name/approve
|
|
1912
|
+
api.post("/agents/:name/approve", async (c) => {
|
|
1435
1913
|
const ctrl = getController(state);
|
|
1436
1914
|
const name = c.req.param("name");
|
|
1437
1915
|
validateName(name, "name");
|
|
@@ -1439,28 +1917,14 @@ function buildRoutes(state) {
|
|
|
1439
1917
|
if (!body.requestId) {
|
|
1440
1918
|
return c.json({ error: "requestId is required" }, 400);
|
|
1441
1919
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
body.feedback
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
return c.json({ ok: true });
|
|
1450
|
-
});
|
|
1451
|
-
api.post("/agents/:name/approve-permission", async (c) => {
|
|
1452
|
-
const ctrl = getController(state);
|
|
1453
|
-
const name = c.req.param("name");
|
|
1454
|
-
validateName(name, "name");
|
|
1455
|
-
const body = await c.req.json();
|
|
1456
|
-
if (!body.requestId) {
|
|
1457
|
-
return c.json({ error: "requestId is required" }, 400);
|
|
1920
|
+
if (!body.type || !["plan", "permission"].includes(body.type)) {
|
|
1921
|
+
return c.json({ error: 'type must be "plan" or "permission"' }, 400);
|
|
1922
|
+
}
|
|
1923
|
+
if (body.type === "plan") {
|
|
1924
|
+
await ctrl.sendPlanApproval(name, body.requestId, body.approve ?? true, body.feedback);
|
|
1925
|
+
} else {
|
|
1926
|
+
await ctrl.sendPermissionResponse(name, body.requestId, body.approve ?? true);
|
|
1458
1927
|
}
|
|
1459
|
-
await ctrl.sendPermissionResponse(
|
|
1460
|
-
name,
|
|
1461
|
-
body.requestId,
|
|
1462
|
-
body.approve ?? true
|
|
1463
|
-
);
|
|
1464
1928
|
state.tracker.resolveApproval(body.requestId);
|
|
1465
1929
|
return c.json({ ok: true });
|
|
1466
1930
|
});
|