claude-code-controller 0.2.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/README.md +285 -302
- package/dist/api/index.cjs +510 -53
- 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 +510 -53
- package/dist/api/index.js.map +1 -1
- package/dist/{controller-C9bh_59H.d.cts → claude-DectLQVR.d.cts} +239 -1
- package/dist/{controller-C9bh_59H.d.ts → claude-DectLQVR.d.ts} +239 -1
- package/dist/index.cjs +467 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +464 -6
- 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) => {
|
|
@@ -419,6 +419,9 @@ var ProcessManager = class {
|
|
|
419
419
|
if (opts.model) {
|
|
420
420
|
claudeArgs.push("--model", opts.model);
|
|
421
421
|
}
|
|
422
|
+
if (opts.permissionMode) {
|
|
423
|
+
claudeArgs.push("--permission-mode", opts.permissionMode);
|
|
424
|
+
}
|
|
422
425
|
if (opts.permissions) {
|
|
423
426
|
for (const perm of opts.permissions) {
|
|
424
427
|
claudeArgs.push("--allowedTools", perm);
|
|
@@ -786,6 +789,11 @@ function createLogger(level = "info") {
|
|
|
786
789
|
}
|
|
787
790
|
|
|
788
791
|
// src/controller.ts
|
|
792
|
+
var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
793
|
+
"shutdown_approved",
|
|
794
|
+
"plan_approval_response",
|
|
795
|
+
"permission_response"
|
|
796
|
+
]);
|
|
789
797
|
var AGENT_COLORS = [
|
|
790
798
|
"#00FF00",
|
|
791
799
|
"#00BFFF",
|
|
@@ -910,6 +918,7 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
910
918
|
color,
|
|
911
919
|
claudeBinary: this.claudeBinary,
|
|
912
920
|
permissions: opts.permissions,
|
|
921
|
+
permissionMode: opts.permissionMode,
|
|
913
922
|
env
|
|
914
923
|
});
|
|
915
924
|
this.emit("agent:spawned", opts.name, proc.pid ?? 0);
|
|
@@ -978,11 +987,7 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
978
987
|
const unread = await readUnread(this.teamName, "controller");
|
|
979
988
|
const fromAgent = unread.filter((m) => m.from === agentName);
|
|
980
989
|
if (fromAgent.length > 0) {
|
|
981
|
-
const PROTOCOL_TYPES =
|
|
982
|
-
"shutdown_approved",
|
|
983
|
-
"plan_approval_response",
|
|
984
|
-
"permission_response"
|
|
985
|
-
]);
|
|
990
|
+
const PROTOCOL_TYPES = PROTOCOL_ONLY_TYPES;
|
|
986
991
|
const meaningful = fromAgent.filter((m) => {
|
|
987
992
|
const parsed = parseMessage(m);
|
|
988
993
|
return parsed.type !== "idle_notification" && !PROTOCOL_TYPES.has(parsed.type);
|
|
@@ -1015,7 +1020,7 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
1015
1020
|
const unread = await readUnread(this.teamName, "controller");
|
|
1016
1021
|
const meaningful = unread.filter((m) => {
|
|
1017
1022
|
const parsed = parseMessage(m);
|
|
1018
|
-
return parsed.type !== "idle_notification";
|
|
1023
|
+
return parsed.type !== "idle_notification" && !PROTOCOL_ONLY_TYPES.has(parsed.type);
|
|
1019
1024
|
});
|
|
1020
1025
|
if (meaningful.length > 0) {
|
|
1021
1026
|
return meaningful[0];
|
|
@@ -1177,6 +1182,454 @@ function sleep2(ms) {
|
|
|
1177
1182
|
return new Promise((r) => setTimeout(r, ms));
|
|
1178
1183
|
}
|
|
1179
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
|
+
|
|
1180
1633
|
// src/api/routes.ts
|
|
1181
1634
|
var SAFE_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
1182
1635
|
var SAFE_TASK_ID_RE = /^[0-9]{1,10}$/;
|
|
@@ -1218,6 +1671,28 @@ function buildRoutes(state) {
|
|
|
1218
1671
|
session: state.controller !== null
|
|
1219
1672
|
});
|
|
1220
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
|
+
});
|
|
1221
1696
|
api.get("/session", (c) => {
|
|
1222
1697
|
if (!state.controller) {
|
|
1223
1698
|
return c.json({ initialized: false, teamName: "" });
|
|
@@ -1243,11 +1718,15 @@ function buildRoutes(state) {
|
|
|
1243
1718
|
await oldController.shutdown();
|
|
1244
1719
|
}
|
|
1245
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);
|
|
1246
1725
|
const controller = new ClaudeCodeController({
|
|
1247
1726
|
teamName: body.teamName,
|
|
1248
1727
|
cwd: body.cwd,
|
|
1249
1728
|
claudeBinary: body.claudeBinary,
|
|
1250
|
-
env
|
|
1729
|
+
env,
|
|
1251
1730
|
logLevel: body.logLevel ?? "info"
|
|
1252
1731
|
});
|
|
1253
1732
|
try {
|
|
@@ -1304,26 +1783,6 @@ function buildRoutes(state) {
|
|
|
1304
1783
|
const pending = approvals.length + unassignedTasks.length + idleAgents.length;
|
|
1305
1784
|
return c.json({ pending, approvals, unassignedTasks, idleAgents });
|
|
1306
1785
|
});
|
|
1307
|
-
api.get("/actions/approvals", (_c) => {
|
|
1308
|
-
getController(state);
|
|
1309
|
-
return _c.json(state.tracker.getPendingApprovals());
|
|
1310
|
-
});
|
|
1311
|
-
api.get("/actions/tasks", async (c) => {
|
|
1312
|
-
const ctrl = getController(state);
|
|
1313
|
-
const tasks = await ctrl.tasks.list();
|
|
1314
|
-
const unassigned = tasks.filter((t) => !t.owner && t.status !== "completed").map((t) => ({
|
|
1315
|
-
id: t.id,
|
|
1316
|
-
subject: t.subject,
|
|
1317
|
-
description: t.description,
|
|
1318
|
-
status: t.status,
|
|
1319
|
-
action: `POST /tasks/${t.id}/assign`
|
|
1320
|
-
}));
|
|
1321
|
-
return c.json(unassigned);
|
|
1322
|
-
});
|
|
1323
|
-
api.get("/actions/idle-agents", (_c) => {
|
|
1324
|
-
getController(state);
|
|
1325
|
-
return _c.json(state.tracker.getIdleAgents());
|
|
1326
|
-
});
|
|
1327
1786
|
api.get("/agents", async (c) => {
|
|
1328
1787
|
const ctrl = getController(state);
|
|
1329
1788
|
const config = await ctrl.team.getConfig();
|
|
@@ -1344,13 +1803,25 @@ function buildRoutes(state) {
|
|
|
1344
1803
|
validateName(body.name, "name");
|
|
1345
1804
|
const agentType = body.type || "general-purpose";
|
|
1346
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;
|
|
1347
1817
|
const handle = await ctrl.spawnAgent({
|
|
1348
1818
|
name: body.name,
|
|
1349
1819
|
type: body.type,
|
|
1350
1820
|
model: body.model,
|
|
1351
1821
|
cwd: body.cwd,
|
|
1352
|
-
permissions:
|
|
1353
|
-
|
|
1822
|
+
permissions: permissionsArray,
|
|
1823
|
+
permissionMode,
|
|
1824
|
+
env: Object.keys(agentEnv).length > 0 ? agentEnv : void 0
|
|
1354
1825
|
});
|
|
1355
1826
|
return c.json(
|
|
1356
1827
|
{
|
|
@@ -1402,7 +1873,7 @@ function buildRoutes(state) {
|
|
|
1402
1873
|
await ctrl.sendShutdownRequest(name);
|
|
1403
1874
|
return c.json({ ok: true });
|
|
1404
1875
|
});
|
|
1405
|
-
api.post("/agents/:name/approve
|
|
1876
|
+
api.post("/agents/:name/approve", async (c) => {
|
|
1406
1877
|
const ctrl = getController(state);
|
|
1407
1878
|
const name = c.req.param("name");
|
|
1408
1879
|
validateName(name, "name");
|
|
@@ -1410,28 +1881,14 @@ function buildRoutes(state) {
|
|
|
1410
1881
|
if (!body.requestId) {
|
|
1411
1882
|
return c.json({ error: "requestId is required" }, 400);
|
|
1412
1883
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
body.feedback
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
return c.json({ ok: true });
|
|
1421
|
-
});
|
|
1422
|
-
api.post("/agents/:name/approve-permission", async (c) => {
|
|
1423
|
-
const ctrl = getController(state);
|
|
1424
|
-
const name = c.req.param("name");
|
|
1425
|
-
validateName(name, "name");
|
|
1426
|
-
const body = await c.req.json();
|
|
1427
|
-
if (!body.requestId) {
|
|
1428
|
-
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);
|
|
1429
1891
|
}
|
|
1430
|
-
await ctrl.sendPermissionResponse(
|
|
1431
|
-
name,
|
|
1432
|
-
body.requestId,
|
|
1433
|
-
body.approve ?? true
|
|
1434
|
-
);
|
|
1435
1892
|
state.tracker.resolveApproval(body.requestId);
|
|
1436
1893
|
return c.json({ ok: true });
|
|
1437
1894
|
});
|