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.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
|
|
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
|
|
33
|
+
action: `POST /agents/${agent}/approve`
|
|
34
34
|
});
|
|
35
35
|
};
|
|
36
36
|
const onIdle = (agent) => {
|
|
@@ -475,6 +475,7 @@ else:
|
|
|
475
475
|
stdio: ["pipe", "pipe", "pipe"],
|
|
476
476
|
env: {
|
|
477
477
|
...process.env,
|
|
478
|
+
CLAUDECODE: "1",
|
|
478
479
|
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "1",
|
|
479
480
|
...opts.env
|
|
480
481
|
}
|
|
@@ -789,6 +790,14 @@ function createLogger(level = "info") {
|
|
|
789
790
|
}
|
|
790
791
|
|
|
791
792
|
// src/controller.ts
|
|
793
|
+
var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
794
|
+
"shutdown_approved",
|
|
795
|
+
"plan_approval_response",
|
|
796
|
+
"permission_response",
|
|
797
|
+
"task_completed",
|
|
798
|
+
"sandbox_permission_request",
|
|
799
|
+
"sandbox_permission_response"
|
|
800
|
+
]);
|
|
792
801
|
var AGENT_COLORS = [
|
|
793
802
|
"#00FF00",
|
|
794
803
|
"#00BFFF",
|
|
@@ -895,6 +904,10 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
895
904
|
name: opts.name,
|
|
896
905
|
agentType: opts.type || "general-purpose",
|
|
897
906
|
model: opts.model,
|
|
907
|
+
prompt: opts.prompt,
|
|
908
|
+
color,
|
|
909
|
+
planModeRequired: false,
|
|
910
|
+
backendType: "in-process",
|
|
898
911
|
joinedAt: Date.now(),
|
|
899
912
|
tmuxPaneId: "",
|
|
900
913
|
cwd,
|
|
@@ -982,11 +995,7 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
982
995
|
const unread = await readUnread(this.teamName, "controller");
|
|
983
996
|
const fromAgent = unread.filter((m) => m.from === agentName);
|
|
984
997
|
if (fromAgent.length > 0) {
|
|
985
|
-
const PROTOCOL_TYPES =
|
|
986
|
-
"shutdown_approved",
|
|
987
|
-
"plan_approval_response",
|
|
988
|
-
"permission_response"
|
|
989
|
-
]);
|
|
998
|
+
const PROTOCOL_TYPES = PROTOCOL_ONLY_TYPES;
|
|
990
999
|
const meaningful = fromAgent.filter((m) => {
|
|
991
1000
|
const parsed = parseMessage(m);
|
|
992
1001
|
return parsed.type !== "idle_notification" && !PROTOCOL_TYPES.has(parsed.type);
|
|
@@ -1019,7 +1028,7 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
1019
1028
|
const unread = await readUnread(this.teamName, "controller");
|
|
1020
1029
|
const meaningful = unread.filter((m) => {
|
|
1021
1030
|
const parsed = parseMessage(m);
|
|
1022
|
-
return parsed.type !== "idle_notification";
|
|
1031
|
+
return parsed.type !== "idle_notification" && !PROTOCOL_ONLY_TYPES.has(parsed.type);
|
|
1023
1032
|
});
|
|
1024
1033
|
if (meaningful.length > 0) {
|
|
1025
1034
|
return meaningful[0];
|
|
@@ -1141,7 +1150,7 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
1141
1150
|
const { raw, parsed } = event;
|
|
1142
1151
|
switch (parsed.type) {
|
|
1143
1152
|
case "idle_notification":
|
|
1144
|
-
this.emit("idle", raw.from);
|
|
1153
|
+
this.emit("idle", raw.from, parsed);
|
|
1145
1154
|
break;
|
|
1146
1155
|
case "shutdown_approved":
|
|
1147
1156
|
this.log.info(
|
|
@@ -1161,6 +1170,9 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
1161
1170
|
);
|
|
1162
1171
|
this.emit("permission:request", raw.from, parsed);
|
|
1163
1172
|
break;
|
|
1173
|
+
case "task_completed":
|
|
1174
|
+
this.emit("message", raw.from, raw);
|
|
1175
|
+
break;
|
|
1164
1176
|
case "plain_text":
|
|
1165
1177
|
this.emit("message", raw.from, raw);
|
|
1166
1178
|
break;
|
|
@@ -1181,6 +1193,454 @@ function sleep2(ms) {
|
|
|
1181
1193
|
return new Promise((r) => setTimeout(r, ms));
|
|
1182
1194
|
}
|
|
1183
1195
|
|
|
1196
|
+
// src/claude.ts
|
|
1197
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
1198
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1199
|
+
Symbol.asyncDispose ??= /* @__PURE__ */ Symbol("Symbol.asyncDispose");
|
|
1200
|
+
function buildEnv(opts) {
|
|
1201
|
+
const env = { ...opts.env };
|
|
1202
|
+
if (opts.apiKey) env.ANTHROPIC_AUTH_TOKEN = opts.apiKey;
|
|
1203
|
+
if (opts.baseUrl) env.ANTHROPIC_BASE_URL = opts.baseUrl;
|
|
1204
|
+
if (opts.timeout != null) env.API_TIMEOUT_MS = String(opts.timeout);
|
|
1205
|
+
return env;
|
|
1206
|
+
}
|
|
1207
|
+
function resolvePermissions(preset) {
|
|
1208
|
+
switch (preset) {
|
|
1209
|
+
case "edit":
|
|
1210
|
+
return { permissionMode: "acceptEdits" };
|
|
1211
|
+
case "plan":
|
|
1212
|
+
return { permissionMode: "plan" };
|
|
1213
|
+
case "ask":
|
|
1214
|
+
return { permissionMode: "default" };
|
|
1215
|
+
case "full":
|
|
1216
|
+
default:
|
|
1217
|
+
return { permissionMode: void 0 };
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function waitForReady(controller, agentName, timeoutMs = 15e3) {
|
|
1221
|
+
return new Promise((resolve, reject) => {
|
|
1222
|
+
let settled = false;
|
|
1223
|
+
const timer = setTimeout(() => {
|
|
1224
|
+
if (settled) return;
|
|
1225
|
+
cleanup();
|
|
1226
|
+
reject(
|
|
1227
|
+
new Error(
|
|
1228
|
+
`Agent "${agentName}" did not become ready within ${timeoutMs}ms`
|
|
1229
|
+
)
|
|
1230
|
+
);
|
|
1231
|
+
}, timeoutMs);
|
|
1232
|
+
const onReady = (name, ..._rest) => {
|
|
1233
|
+
if (name === agentName && !settled) {
|
|
1234
|
+
settled = true;
|
|
1235
|
+
cleanup();
|
|
1236
|
+
resolve();
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
const onExit = (name, code) => {
|
|
1240
|
+
if (name === agentName && !settled) {
|
|
1241
|
+
settled = true;
|
|
1242
|
+
cleanup();
|
|
1243
|
+
reject(
|
|
1244
|
+
new Error(
|
|
1245
|
+
`Agent "${agentName}" exited before becoming ready (code=${code})`
|
|
1246
|
+
)
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
};
|
|
1250
|
+
const onSpawned = (name) => {
|
|
1251
|
+
if (name === agentName && !settled) {
|
|
1252
|
+
settled = true;
|
|
1253
|
+
cleanup();
|
|
1254
|
+
resolve();
|
|
1255
|
+
}
|
|
1256
|
+
};
|
|
1257
|
+
const cleanup = () => {
|
|
1258
|
+
clearTimeout(timer);
|
|
1259
|
+
controller.removeListener("idle", onReady);
|
|
1260
|
+
controller.removeListener("message", onReady);
|
|
1261
|
+
controller.removeListener("agent:spawned", onSpawned);
|
|
1262
|
+
controller.removeListener("agent:exited", onExit);
|
|
1263
|
+
};
|
|
1264
|
+
controller.on("idle", onReady);
|
|
1265
|
+
controller.on("message", onReady);
|
|
1266
|
+
controller.on("agent:spawned", onSpawned);
|
|
1267
|
+
controller.on("agent:exited", onExit);
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
var Agent = class _Agent extends EventEmitter2 {
|
|
1271
|
+
controller;
|
|
1272
|
+
handle;
|
|
1273
|
+
ownsController;
|
|
1274
|
+
disposed = false;
|
|
1275
|
+
boundListeners = [];
|
|
1276
|
+
constructor(controller, handle, ownsController, behavior) {
|
|
1277
|
+
super();
|
|
1278
|
+
this.controller = controller;
|
|
1279
|
+
this.handle = handle;
|
|
1280
|
+
this.ownsController = ownsController;
|
|
1281
|
+
this.wireEvents();
|
|
1282
|
+
this.wireBehavior(behavior);
|
|
1283
|
+
}
|
|
1284
|
+
/** Create a standalone agent (owns its own controller). */
|
|
1285
|
+
static async create(opts = {}) {
|
|
1286
|
+
const name = opts.name ?? `agent-${randomUUID3().slice(0, 8)}`;
|
|
1287
|
+
const env = buildEnv(opts);
|
|
1288
|
+
const { permissionMode } = resolvePermissions(opts.permissions);
|
|
1289
|
+
const controller = new ClaudeCodeController({
|
|
1290
|
+
teamName: `claude-${randomUUID3().slice(0, 8)}`,
|
|
1291
|
+
cwd: opts.cwd,
|
|
1292
|
+
claudeBinary: opts.claudeBinary,
|
|
1293
|
+
env,
|
|
1294
|
+
logLevel: opts.logLevel ?? "warn",
|
|
1295
|
+
logger: opts.logger
|
|
1296
|
+
});
|
|
1297
|
+
await controller.init();
|
|
1298
|
+
const ready = waitForReady(controller, name, opts.readyTimeout);
|
|
1299
|
+
try {
|
|
1300
|
+
const handle = await controller.spawnAgent({
|
|
1301
|
+
name,
|
|
1302
|
+
type: opts.type ?? "general-purpose",
|
|
1303
|
+
model: opts.model,
|
|
1304
|
+
cwd: opts.cwd,
|
|
1305
|
+
permissionMode
|
|
1306
|
+
});
|
|
1307
|
+
const agent = new _Agent(controller, handle, true, {
|
|
1308
|
+
autoApprove: opts.autoApprove,
|
|
1309
|
+
onPermission: opts.onPermission,
|
|
1310
|
+
onPlan: opts.onPlan
|
|
1311
|
+
});
|
|
1312
|
+
await ready;
|
|
1313
|
+
return agent;
|
|
1314
|
+
} catch (err) {
|
|
1315
|
+
await controller.shutdown().catch(() => {
|
|
1316
|
+
});
|
|
1317
|
+
throw err;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
/** Create an agent within an existing session (session owns the controller). */
|
|
1321
|
+
static async createInSession(controller, name, opts = {}) {
|
|
1322
|
+
const { permissionMode } = resolvePermissions(opts.permissions);
|
|
1323
|
+
const ready = waitForReady(controller, name, opts.readyTimeout);
|
|
1324
|
+
const handle = await controller.spawnAgent({
|
|
1325
|
+
name,
|
|
1326
|
+
type: opts.type ?? "general-purpose",
|
|
1327
|
+
model: opts.model,
|
|
1328
|
+
cwd: opts.cwd,
|
|
1329
|
+
permissionMode,
|
|
1330
|
+
env: opts.env
|
|
1331
|
+
});
|
|
1332
|
+
const agent = new _Agent(controller, handle, false, {
|
|
1333
|
+
autoApprove: opts.autoApprove,
|
|
1334
|
+
onPermission: opts.onPermission,
|
|
1335
|
+
onPlan: opts.onPlan
|
|
1336
|
+
});
|
|
1337
|
+
await ready;
|
|
1338
|
+
return agent;
|
|
1339
|
+
}
|
|
1340
|
+
/** The agent's name. */
|
|
1341
|
+
get name() {
|
|
1342
|
+
return this.handle.name;
|
|
1343
|
+
}
|
|
1344
|
+
/** The agent process PID. */
|
|
1345
|
+
get pid() {
|
|
1346
|
+
return this.handle.pid;
|
|
1347
|
+
}
|
|
1348
|
+
/** Whether the agent process is still running. */
|
|
1349
|
+
get isRunning() {
|
|
1350
|
+
return this.handle.isRunning;
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Send a message and wait for the response.
|
|
1354
|
+
*
|
|
1355
|
+
* Uses event-based waiting (via the controller's InboxPoller) instead of
|
|
1356
|
+
* polling `readUnread()` directly, which avoids a race condition where the
|
|
1357
|
+
* poller marks inbox messages as read before `receive()` can see them.
|
|
1358
|
+
*/
|
|
1359
|
+
async ask(question, opts) {
|
|
1360
|
+
this.ensureNotDisposed();
|
|
1361
|
+
const timeout = opts?.timeout ?? 12e4;
|
|
1362
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
1363
|
+
const timer = setTimeout(() => {
|
|
1364
|
+
cleanup();
|
|
1365
|
+
reject(new Error(`Timeout (${timeout}ms) waiting for response`));
|
|
1366
|
+
}, timeout);
|
|
1367
|
+
const onMsg = (text) => {
|
|
1368
|
+
cleanup();
|
|
1369
|
+
resolve(text);
|
|
1370
|
+
};
|
|
1371
|
+
const onExit = (code) => {
|
|
1372
|
+
cleanup();
|
|
1373
|
+
reject(new Error(`Agent exited (code=${code}) before responding`));
|
|
1374
|
+
};
|
|
1375
|
+
const cleanup = () => {
|
|
1376
|
+
clearTimeout(timer);
|
|
1377
|
+
this.removeListener("message", onMsg);
|
|
1378
|
+
this.removeListener("exit", onExit);
|
|
1379
|
+
};
|
|
1380
|
+
this.on("message", onMsg);
|
|
1381
|
+
this.on("exit", onExit);
|
|
1382
|
+
});
|
|
1383
|
+
const wrapped = `${question}
|
|
1384
|
+
|
|
1385
|
+
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.`;
|
|
1386
|
+
await this.handle.send(wrapped);
|
|
1387
|
+
return responsePromise;
|
|
1388
|
+
}
|
|
1389
|
+
/** Send a message without waiting for a response. */
|
|
1390
|
+
async send(message) {
|
|
1391
|
+
this.ensureNotDisposed();
|
|
1392
|
+
return this.handle.send(message);
|
|
1393
|
+
}
|
|
1394
|
+
/** Wait for the next response from this agent. */
|
|
1395
|
+
async receive(opts) {
|
|
1396
|
+
this.ensureNotDisposed();
|
|
1397
|
+
const timeout = opts?.timeout ?? 12e4;
|
|
1398
|
+
return new Promise((resolve, reject) => {
|
|
1399
|
+
const timer = setTimeout(() => {
|
|
1400
|
+
cleanup();
|
|
1401
|
+
reject(new Error(`Timeout (${timeout}ms) waiting for response`));
|
|
1402
|
+
}, timeout);
|
|
1403
|
+
const onMsg = (text) => {
|
|
1404
|
+
cleanup();
|
|
1405
|
+
resolve(text);
|
|
1406
|
+
};
|
|
1407
|
+
const onExit = (code) => {
|
|
1408
|
+
cleanup();
|
|
1409
|
+
reject(new Error(`Agent exited (code=${code}) before responding`));
|
|
1410
|
+
};
|
|
1411
|
+
const cleanup = () => {
|
|
1412
|
+
clearTimeout(timer);
|
|
1413
|
+
this.removeListener("message", onMsg);
|
|
1414
|
+
this.removeListener("exit", onExit);
|
|
1415
|
+
};
|
|
1416
|
+
this.on("message", onMsg);
|
|
1417
|
+
this.on("exit", onExit);
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Close this agent. If standalone, shuts down the entire controller.
|
|
1422
|
+
* If session-owned, kills only this agent's process.
|
|
1423
|
+
*/
|
|
1424
|
+
async close() {
|
|
1425
|
+
if (this.disposed) return;
|
|
1426
|
+
this.disposed = true;
|
|
1427
|
+
this.unwireEvents();
|
|
1428
|
+
if (this.ownsController) {
|
|
1429
|
+
await this.controller.shutdown();
|
|
1430
|
+
} else {
|
|
1431
|
+
await this.handle.kill();
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
/** Mark as disposed (used by Session when it closes). */
|
|
1435
|
+
markDisposed() {
|
|
1436
|
+
this.disposed = true;
|
|
1437
|
+
this.unwireEvents();
|
|
1438
|
+
}
|
|
1439
|
+
async [Symbol.asyncDispose]() {
|
|
1440
|
+
await this.close();
|
|
1441
|
+
}
|
|
1442
|
+
wireEvents() {
|
|
1443
|
+
const agentName = this.handle.name;
|
|
1444
|
+
const onMessage = (name, msg) => {
|
|
1445
|
+
if (name === agentName) this.emit("message", msg.text);
|
|
1446
|
+
};
|
|
1447
|
+
const onIdle = (name, _details) => {
|
|
1448
|
+
if (name === agentName) this.emit("idle");
|
|
1449
|
+
};
|
|
1450
|
+
const onPermission = (name, parsed) => {
|
|
1451
|
+
if (name !== agentName) return;
|
|
1452
|
+
let handled = false;
|
|
1453
|
+
const guard = (fn) => () => {
|
|
1454
|
+
if (handled) return Promise.resolve();
|
|
1455
|
+
handled = true;
|
|
1456
|
+
return fn();
|
|
1457
|
+
};
|
|
1458
|
+
this.emit("permission", {
|
|
1459
|
+
requestId: parsed.requestId,
|
|
1460
|
+
toolName: parsed.toolName,
|
|
1461
|
+
description: parsed.description,
|
|
1462
|
+
input: parsed.input,
|
|
1463
|
+
approve: guard(
|
|
1464
|
+
() => this.controller.sendPermissionResponse(agentName, parsed.requestId, true)
|
|
1465
|
+
),
|
|
1466
|
+
reject: guard(
|
|
1467
|
+
() => this.controller.sendPermissionResponse(agentName, parsed.requestId, false)
|
|
1468
|
+
)
|
|
1469
|
+
});
|
|
1470
|
+
};
|
|
1471
|
+
const onPlan = (name, parsed) => {
|
|
1472
|
+
if (name !== agentName) return;
|
|
1473
|
+
let handled = false;
|
|
1474
|
+
const guard = (fn) => (...args) => {
|
|
1475
|
+
if (handled) return Promise.resolve();
|
|
1476
|
+
handled = true;
|
|
1477
|
+
return fn(...args);
|
|
1478
|
+
};
|
|
1479
|
+
this.emit("plan", {
|
|
1480
|
+
requestId: parsed.requestId,
|
|
1481
|
+
planContent: parsed.planContent,
|
|
1482
|
+
approve: guard(
|
|
1483
|
+
(feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, true, feedback)
|
|
1484
|
+
),
|
|
1485
|
+
reject: guard(
|
|
1486
|
+
(feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, false, feedback)
|
|
1487
|
+
)
|
|
1488
|
+
});
|
|
1489
|
+
};
|
|
1490
|
+
const onExit = (name, code) => {
|
|
1491
|
+
if (name === agentName) this.emit("exit", code);
|
|
1492
|
+
};
|
|
1493
|
+
const onError = (err) => {
|
|
1494
|
+
this.emit("error", err);
|
|
1495
|
+
};
|
|
1496
|
+
this.controller.on("message", onMessage);
|
|
1497
|
+
this.controller.on("idle", onIdle);
|
|
1498
|
+
this.controller.on("permission:request", onPermission);
|
|
1499
|
+
this.controller.on("plan:approval_request", onPlan);
|
|
1500
|
+
this.controller.on("agent:exited", onExit);
|
|
1501
|
+
this.controller.on("error", onError);
|
|
1502
|
+
this.boundListeners = [
|
|
1503
|
+
{ event: "message", fn: onMessage },
|
|
1504
|
+
{ event: "idle", fn: onIdle },
|
|
1505
|
+
{ event: "permission:request", fn: onPermission },
|
|
1506
|
+
{ event: "plan:approval_request", fn: onPlan },
|
|
1507
|
+
{ event: "agent:exited", fn: onExit },
|
|
1508
|
+
{ event: "error", fn: onError }
|
|
1509
|
+
];
|
|
1510
|
+
}
|
|
1511
|
+
unwireEvents() {
|
|
1512
|
+
for (const { event, fn } of this.boundListeners) {
|
|
1513
|
+
this.controller.removeListener(event, fn);
|
|
1514
|
+
}
|
|
1515
|
+
this.boundListeners = [];
|
|
1516
|
+
}
|
|
1517
|
+
wireBehavior(behavior) {
|
|
1518
|
+
if (!behavior) return;
|
|
1519
|
+
const { autoApprove, onPermission, onPlan } = behavior;
|
|
1520
|
+
if (autoApprove != null) {
|
|
1521
|
+
this.on("permission", (req) => {
|
|
1522
|
+
if (autoApprove === true) {
|
|
1523
|
+
req.approve();
|
|
1524
|
+
} else if (Array.isArray(autoApprove)) {
|
|
1525
|
+
autoApprove.includes(req.toolName) ? req.approve() : req.reject();
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
if (autoApprove === true) {
|
|
1529
|
+
this.on("plan", (req) => req.approve());
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (onPermission) {
|
|
1533
|
+
this.on("permission", onPermission);
|
|
1534
|
+
}
|
|
1535
|
+
if (onPlan) {
|
|
1536
|
+
this.on("plan", onPlan);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
ensureNotDisposed() {
|
|
1540
|
+
if (this.disposed) {
|
|
1541
|
+
throw new Error("Agent has been closed");
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
var Session = class _Session {
|
|
1546
|
+
controller;
|
|
1547
|
+
defaults;
|
|
1548
|
+
agents = /* @__PURE__ */ new Map();
|
|
1549
|
+
disposed = false;
|
|
1550
|
+
constructor(controller, defaults) {
|
|
1551
|
+
this.controller = controller;
|
|
1552
|
+
this.defaults = defaults;
|
|
1553
|
+
}
|
|
1554
|
+
static async create(opts = {}) {
|
|
1555
|
+
const env = buildEnv(opts);
|
|
1556
|
+
const controller = new ClaudeCodeController({
|
|
1557
|
+
teamName: opts.teamName ?? `session-${randomUUID3().slice(0, 8)}`,
|
|
1558
|
+
cwd: opts.cwd,
|
|
1559
|
+
claudeBinary: opts.claudeBinary,
|
|
1560
|
+
env,
|
|
1561
|
+
logLevel: opts.logLevel ?? "warn",
|
|
1562
|
+
logger: opts.logger
|
|
1563
|
+
});
|
|
1564
|
+
await controller.init();
|
|
1565
|
+
return new _Session(controller, opts);
|
|
1566
|
+
}
|
|
1567
|
+
/** Spawn a named agent in this session. Inherits session defaults. */
|
|
1568
|
+
async agent(name, opts = {}) {
|
|
1569
|
+
this.ensureNotDisposed();
|
|
1570
|
+
const merged = {
|
|
1571
|
+
model: this.defaults.model,
|
|
1572
|
+
cwd: this.defaults.cwd,
|
|
1573
|
+
permissions: this.defaults.permissions,
|
|
1574
|
+
readyTimeout: this.defaults.readyTimeout,
|
|
1575
|
+
autoApprove: this.defaults.autoApprove,
|
|
1576
|
+
onPermission: this.defaults.onPermission,
|
|
1577
|
+
onPlan: this.defaults.onPlan,
|
|
1578
|
+
...opts
|
|
1579
|
+
};
|
|
1580
|
+
const agent = await Agent.createInSession(
|
|
1581
|
+
this.controller,
|
|
1582
|
+
name,
|
|
1583
|
+
merged
|
|
1584
|
+
);
|
|
1585
|
+
this.agents.set(name, agent);
|
|
1586
|
+
return agent;
|
|
1587
|
+
}
|
|
1588
|
+
/** Get an existing agent by name. */
|
|
1589
|
+
get(name) {
|
|
1590
|
+
return this.agents.get(name);
|
|
1591
|
+
}
|
|
1592
|
+
/** Close all agents and shut down the session. */
|
|
1593
|
+
async close() {
|
|
1594
|
+
if (this.disposed) return;
|
|
1595
|
+
this.disposed = true;
|
|
1596
|
+
for (const agent of this.agents.values()) {
|
|
1597
|
+
agent.markDisposed();
|
|
1598
|
+
}
|
|
1599
|
+
await this.controller.shutdown();
|
|
1600
|
+
}
|
|
1601
|
+
async [Symbol.asyncDispose]() {
|
|
1602
|
+
await this.close();
|
|
1603
|
+
}
|
|
1604
|
+
ensureNotDisposed() {
|
|
1605
|
+
if (this.disposed) {
|
|
1606
|
+
throw new Error("Session has been closed");
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
};
|
|
1610
|
+
async function claudeCall(prompt, opts = {}) {
|
|
1611
|
+
const agent = await Agent.create(opts);
|
|
1612
|
+
try {
|
|
1613
|
+
return await agent.ask(prompt, { timeout: opts.timeout ?? 12e4 });
|
|
1614
|
+
} finally {
|
|
1615
|
+
await agent.close();
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
var claude = Object.assign(claudeCall, {
|
|
1619
|
+
/**
|
|
1620
|
+
* Create a persistent agent for multi-turn conversations.
|
|
1621
|
+
*
|
|
1622
|
+
* @example
|
|
1623
|
+
* ```ts
|
|
1624
|
+
* const agent = await claude.agent({ model: "sonnet" });
|
|
1625
|
+
* const answer = await agent.ask("What is 2+2?");
|
|
1626
|
+
* await agent.close();
|
|
1627
|
+
* ```
|
|
1628
|
+
*/
|
|
1629
|
+
agent: (opts) => Agent.create(opts),
|
|
1630
|
+
/**
|
|
1631
|
+
* Create a multi-agent session.
|
|
1632
|
+
*
|
|
1633
|
+
* @example
|
|
1634
|
+
* ```ts
|
|
1635
|
+
* const session = await claude.session({ model: "sonnet" });
|
|
1636
|
+
* const reviewer = await session.agent("reviewer", { model: "opus" });
|
|
1637
|
+
* const coder = await session.agent("coder");
|
|
1638
|
+
* await session.close();
|
|
1639
|
+
* ```
|
|
1640
|
+
*/
|
|
1641
|
+
session: (opts) => Session.create(opts)
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1184
1644
|
// src/api/routes.ts
|
|
1185
1645
|
var SAFE_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
1186
1646
|
var SAFE_TASK_ID_RE = /^[0-9]{1,10}$/;
|
|
@@ -1222,6 +1682,28 @@ function buildRoutes(state) {
|
|
|
1222
1682
|
session: state.controller !== null
|
|
1223
1683
|
});
|
|
1224
1684
|
});
|
|
1685
|
+
api.post("/ask", async (c) => {
|
|
1686
|
+
const body = await c.req.json();
|
|
1687
|
+
if (!body.prompt) {
|
|
1688
|
+
return c.json({ error: "prompt is required" }, 400);
|
|
1689
|
+
}
|
|
1690
|
+
try {
|
|
1691
|
+
const response = await claude(body.prompt, {
|
|
1692
|
+
model: body.model,
|
|
1693
|
+
apiKey: body.apiKey,
|
|
1694
|
+
baseUrl: body.baseUrl,
|
|
1695
|
+
timeout: body.timeout,
|
|
1696
|
+
cwd: body.cwd,
|
|
1697
|
+
permissions: body.permissions,
|
|
1698
|
+
env: body.env,
|
|
1699
|
+
logLevel: "warn"
|
|
1700
|
+
});
|
|
1701
|
+
return c.json({ response });
|
|
1702
|
+
} catch (err) {
|
|
1703
|
+
const message = err instanceof Error ? err.message : "Agent failed to respond";
|
|
1704
|
+
return c.json({ error: message }, 500);
|
|
1705
|
+
}
|
|
1706
|
+
});
|
|
1225
1707
|
api.get("/session", (c) => {
|
|
1226
1708
|
if (!state.controller) {
|
|
1227
1709
|
return c.json({ initialized: false, teamName: "" });
|
|
@@ -1247,11 +1729,15 @@ function buildRoutes(state) {
|
|
|
1247
1729
|
await oldController.shutdown();
|
|
1248
1730
|
}
|
|
1249
1731
|
}
|
|
1732
|
+
const env = { ...body.env };
|
|
1733
|
+
if (body.apiKey) env.ANTHROPIC_AUTH_TOKEN = body.apiKey;
|
|
1734
|
+
if (body.baseUrl) env.ANTHROPIC_BASE_URL = body.baseUrl;
|
|
1735
|
+
if (body.timeout != null) env.API_TIMEOUT_MS = String(body.timeout);
|
|
1250
1736
|
const controller = new ClaudeCodeController({
|
|
1251
1737
|
teamName: body.teamName,
|
|
1252
1738
|
cwd: body.cwd,
|
|
1253
1739
|
claudeBinary: body.claudeBinary,
|
|
1254
|
-
env
|
|
1740
|
+
env,
|
|
1255
1741
|
logLevel: body.logLevel ?? "info"
|
|
1256
1742
|
});
|
|
1257
1743
|
try {
|
|
@@ -1308,26 +1794,6 @@ function buildRoutes(state) {
|
|
|
1308
1794
|
const pending = approvals.length + unassignedTasks.length + idleAgents.length;
|
|
1309
1795
|
return c.json({ pending, approvals, unassignedTasks, idleAgents });
|
|
1310
1796
|
});
|
|
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
1797
|
api.get("/agents", async (c) => {
|
|
1332
1798
|
const ctrl = getController(state);
|
|
1333
1799
|
const config = await ctrl.team.getConfig();
|
|
@@ -1348,13 +1814,25 @@ function buildRoutes(state) {
|
|
|
1348
1814
|
validateName(body.name, "name");
|
|
1349
1815
|
const agentType = body.type || "general-purpose";
|
|
1350
1816
|
state.tracker.registerAgentType(body.name, agentType);
|
|
1817
|
+
const agentEnv = { ...body.env };
|
|
1818
|
+
if (body.apiKey) agentEnv.ANTHROPIC_AUTH_TOKEN = body.apiKey;
|
|
1819
|
+
if (body.baseUrl) agentEnv.ANTHROPIC_BASE_URL = body.baseUrl;
|
|
1820
|
+
if (body.timeout != null) agentEnv.API_TIMEOUT_MS = String(body.timeout);
|
|
1821
|
+
const permissionsArray = Array.isArray(body.permissions) ? body.permissions : void 0;
|
|
1822
|
+
const PRESET_MAP = {
|
|
1823
|
+
edit: "acceptEdits",
|
|
1824
|
+
plan: "plan",
|
|
1825
|
+
ask: "default"
|
|
1826
|
+
};
|
|
1827
|
+
const permissionMode = typeof body.permissions === "string" && !Array.isArray(body.permissions) ? PRESET_MAP[body.permissions] : void 0;
|
|
1351
1828
|
const handle = await ctrl.spawnAgent({
|
|
1352
1829
|
name: body.name,
|
|
1353
1830
|
type: body.type,
|
|
1354
1831
|
model: body.model,
|
|
1355
1832
|
cwd: body.cwd,
|
|
1356
|
-
permissions:
|
|
1357
|
-
|
|
1833
|
+
permissions: permissionsArray,
|
|
1834
|
+
permissionMode,
|
|
1835
|
+
env: Object.keys(agentEnv).length > 0 ? agentEnv : void 0
|
|
1358
1836
|
});
|
|
1359
1837
|
return c.json(
|
|
1360
1838
|
{
|
|
@@ -1406,7 +1884,7 @@ function buildRoutes(state) {
|
|
|
1406
1884
|
await ctrl.sendShutdownRequest(name);
|
|
1407
1885
|
return c.json({ ok: true });
|
|
1408
1886
|
});
|
|
1409
|
-
api.post("/agents/:name/approve
|
|
1887
|
+
api.post("/agents/:name/approve", async (c) => {
|
|
1410
1888
|
const ctrl = getController(state);
|
|
1411
1889
|
const name = c.req.param("name");
|
|
1412
1890
|
validateName(name, "name");
|
|
@@ -1414,28 +1892,14 @@ function buildRoutes(state) {
|
|
|
1414
1892
|
if (!body.requestId) {
|
|
1415
1893
|
return c.json({ error: "requestId is required" }, 400);
|
|
1416
1894
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
body.feedback
|
|
1422
|
-
|
|
1423
|
-
|
|
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);
|
|
1895
|
+
if (!body.type || !["plan", "permission"].includes(body.type)) {
|
|
1896
|
+
return c.json({ error: 'type must be "plan" or "permission"' }, 400);
|
|
1897
|
+
}
|
|
1898
|
+
if (body.type === "plan") {
|
|
1899
|
+
await ctrl.sendPlanApproval(name, body.requestId, body.approve ?? true, body.feedback);
|
|
1900
|
+
} else {
|
|
1901
|
+
await ctrl.sendPermissionResponse(name, body.requestId, body.approve ?? true);
|
|
1433
1902
|
}
|
|
1434
|
-
await ctrl.sendPermissionResponse(
|
|
1435
|
-
name,
|
|
1436
|
-
body.requestId,
|
|
1437
|
-
body.approve ?? true
|
|
1438
|
-
);
|
|
1439
1903
|
state.tracker.resolveApproval(body.requestId);
|
|
1440
1904
|
return c.json({ ok: true });
|
|
1441
1905
|
});
|