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.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) => {
|
|
@@ -444,6 +444,9 @@ var ProcessManager = class {
|
|
|
444
444
|
if (opts.model) {
|
|
445
445
|
claudeArgs.push("--model", opts.model);
|
|
446
446
|
}
|
|
447
|
+
if (opts.permissionMode) {
|
|
448
|
+
claudeArgs.push("--permission-mode", opts.permissionMode);
|
|
449
|
+
}
|
|
447
450
|
if (opts.permissions) {
|
|
448
451
|
for (const perm of opts.permissions) {
|
|
449
452
|
claudeArgs.push("--allowedTools", perm);
|
|
@@ -811,6 +814,11 @@ function createLogger(level = "info") {
|
|
|
811
814
|
}
|
|
812
815
|
|
|
813
816
|
// src/controller.ts
|
|
817
|
+
var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
818
|
+
"shutdown_approved",
|
|
819
|
+
"plan_approval_response",
|
|
820
|
+
"permission_response"
|
|
821
|
+
]);
|
|
814
822
|
var AGENT_COLORS = [
|
|
815
823
|
"#00FF00",
|
|
816
824
|
"#00BFFF",
|
|
@@ -935,6 +943,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
935
943
|
color,
|
|
936
944
|
claudeBinary: this.claudeBinary,
|
|
937
945
|
permissions: opts.permissions,
|
|
946
|
+
permissionMode: opts.permissionMode,
|
|
938
947
|
env
|
|
939
948
|
});
|
|
940
949
|
this.emit("agent:spawned", opts.name, proc.pid ?? 0);
|
|
@@ -1003,11 +1012,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
1003
1012
|
const unread = await readUnread(this.teamName, "controller");
|
|
1004
1013
|
const fromAgent = unread.filter((m) => m.from === agentName);
|
|
1005
1014
|
if (fromAgent.length > 0) {
|
|
1006
|
-
const PROTOCOL_TYPES =
|
|
1007
|
-
"shutdown_approved",
|
|
1008
|
-
"plan_approval_response",
|
|
1009
|
-
"permission_response"
|
|
1010
|
-
]);
|
|
1015
|
+
const PROTOCOL_TYPES = PROTOCOL_ONLY_TYPES;
|
|
1011
1016
|
const meaningful = fromAgent.filter((m) => {
|
|
1012
1017
|
const parsed = parseMessage(m);
|
|
1013
1018
|
return parsed.type !== "idle_notification" && !PROTOCOL_TYPES.has(parsed.type);
|
|
@@ -1040,7 +1045,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
1040
1045
|
const unread = await readUnread(this.teamName, "controller");
|
|
1041
1046
|
const meaningful = unread.filter((m) => {
|
|
1042
1047
|
const parsed = parseMessage(m);
|
|
1043
|
-
return parsed.type !== "idle_notification";
|
|
1048
|
+
return parsed.type !== "idle_notification" && !PROTOCOL_ONLY_TYPES.has(parsed.type);
|
|
1044
1049
|
});
|
|
1045
1050
|
if (meaningful.length > 0) {
|
|
1046
1051
|
return meaningful[0];
|
|
@@ -1202,6 +1207,454 @@ function sleep2(ms) {
|
|
|
1202
1207
|
return new Promise((r) => setTimeout(r, ms));
|
|
1203
1208
|
}
|
|
1204
1209
|
|
|
1210
|
+
// src/claude.ts
|
|
1211
|
+
var import_node_events2 = require("events");
|
|
1212
|
+
var import_node_crypto3 = require("crypto");
|
|
1213
|
+
Symbol.asyncDispose ??= /* @__PURE__ */ Symbol("Symbol.asyncDispose");
|
|
1214
|
+
function buildEnv(opts) {
|
|
1215
|
+
const env = { ...opts.env };
|
|
1216
|
+
if (opts.apiKey) env.ANTHROPIC_AUTH_TOKEN = opts.apiKey;
|
|
1217
|
+
if (opts.baseUrl) env.ANTHROPIC_BASE_URL = opts.baseUrl;
|
|
1218
|
+
if (opts.timeout != null) env.API_TIMEOUT_MS = String(opts.timeout);
|
|
1219
|
+
return env;
|
|
1220
|
+
}
|
|
1221
|
+
function resolvePermissions(preset) {
|
|
1222
|
+
switch (preset) {
|
|
1223
|
+
case "edit":
|
|
1224
|
+
return { permissionMode: "acceptEdits" };
|
|
1225
|
+
case "plan":
|
|
1226
|
+
return { permissionMode: "plan" };
|
|
1227
|
+
case "ask":
|
|
1228
|
+
return { permissionMode: "default" };
|
|
1229
|
+
case "full":
|
|
1230
|
+
default:
|
|
1231
|
+
return { permissionMode: void 0 };
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
function waitForReady(controller, agentName, timeoutMs = 15e3) {
|
|
1235
|
+
return new Promise((resolve, reject) => {
|
|
1236
|
+
let settled = false;
|
|
1237
|
+
const timer = setTimeout(() => {
|
|
1238
|
+
if (settled) return;
|
|
1239
|
+
cleanup();
|
|
1240
|
+
reject(
|
|
1241
|
+
new Error(
|
|
1242
|
+
`Agent "${agentName}" did not become ready within ${timeoutMs}ms`
|
|
1243
|
+
)
|
|
1244
|
+
);
|
|
1245
|
+
}, timeoutMs);
|
|
1246
|
+
const onReady = (name) => {
|
|
1247
|
+
if (name === agentName && !settled) {
|
|
1248
|
+
settled = true;
|
|
1249
|
+
cleanup();
|
|
1250
|
+
resolve();
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
const onExit = (name, code) => {
|
|
1254
|
+
if (name === agentName && !settled) {
|
|
1255
|
+
settled = true;
|
|
1256
|
+
cleanup();
|
|
1257
|
+
reject(
|
|
1258
|
+
new Error(
|
|
1259
|
+
`Agent "${agentName}" exited before becoming ready (code=${code})`
|
|
1260
|
+
)
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
const onSpawned = (name) => {
|
|
1265
|
+
if (name === agentName && !settled) {
|
|
1266
|
+
settled = true;
|
|
1267
|
+
cleanup();
|
|
1268
|
+
resolve();
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
const cleanup = () => {
|
|
1272
|
+
clearTimeout(timer);
|
|
1273
|
+
controller.removeListener("idle", onReady);
|
|
1274
|
+
controller.removeListener("message", onReady);
|
|
1275
|
+
controller.removeListener("agent:spawned", onSpawned);
|
|
1276
|
+
controller.removeListener("agent:exited", onExit);
|
|
1277
|
+
};
|
|
1278
|
+
controller.on("idle", onReady);
|
|
1279
|
+
controller.on("message", onReady);
|
|
1280
|
+
controller.on("agent:spawned", onSpawned);
|
|
1281
|
+
controller.on("agent:exited", onExit);
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
var Agent = class _Agent extends import_node_events2.EventEmitter {
|
|
1285
|
+
controller;
|
|
1286
|
+
handle;
|
|
1287
|
+
ownsController;
|
|
1288
|
+
disposed = false;
|
|
1289
|
+
boundListeners = [];
|
|
1290
|
+
constructor(controller, handle, ownsController, behavior) {
|
|
1291
|
+
super();
|
|
1292
|
+
this.controller = controller;
|
|
1293
|
+
this.handle = handle;
|
|
1294
|
+
this.ownsController = ownsController;
|
|
1295
|
+
this.wireEvents();
|
|
1296
|
+
this.wireBehavior(behavior);
|
|
1297
|
+
}
|
|
1298
|
+
/** Create a standalone agent (owns its own controller). */
|
|
1299
|
+
static async create(opts = {}) {
|
|
1300
|
+
const name = opts.name ?? `agent-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`;
|
|
1301
|
+
const env = buildEnv(opts);
|
|
1302
|
+
const { permissionMode } = resolvePermissions(opts.permissions);
|
|
1303
|
+
const controller = new ClaudeCodeController({
|
|
1304
|
+
teamName: `claude-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`,
|
|
1305
|
+
cwd: opts.cwd,
|
|
1306
|
+
claudeBinary: opts.claudeBinary,
|
|
1307
|
+
env,
|
|
1308
|
+
logLevel: opts.logLevel ?? "warn",
|
|
1309
|
+
logger: opts.logger
|
|
1310
|
+
});
|
|
1311
|
+
await controller.init();
|
|
1312
|
+
const ready = waitForReady(controller, name, opts.readyTimeout);
|
|
1313
|
+
try {
|
|
1314
|
+
const handle = await controller.spawnAgent({
|
|
1315
|
+
name,
|
|
1316
|
+
type: opts.type ?? "general-purpose",
|
|
1317
|
+
model: opts.model,
|
|
1318
|
+
cwd: opts.cwd,
|
|
1319
|
+
permissionMode
|
|
1320
|
+
});
|
|
1321
|
+
const agent = new _Agent(controller, handle, true, {
|
|
1322
|
+
autoApprove: opts.autoApprove,
|
|
1323
|
+
onPermission: opts.onPermission,
|
|
1324
|
+
onPlan: opts.onPlan
|
|
1325
|
+
});
|
|
1326
|
+
await ready;
|
|
1327
|
+
return agent;
|
|
1328
|
+
} catch (err) {
|
|
1329
|
+
await controller.shutdown().catch(() => {
|
|
1330
|
+
});
|
|
1331
|
+
throw err;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
/** Create an agent within an existing session (session owns the controller). */
|
|
1335
|
+
static async createInSession(controller, name, opts = {}) {
|
|
1336
|
+
const { permissionMode } = resolvePermissions(opts.permissions);
|
|
1337
|
+
const ready = waitForReady(controller, name, opts.readyTimeout);
|
|
1338
|
+
const handle = await controller.spawnAgent({
|
|
1339
|
+
name,
|
|
1340
|
+
type: opts.type ?? "general-purpose",
|
|
1341
|
+
model: opts.model,
|
|
1342
|
+
cwd: opts.cwd,
|
|
1343
|
+
permissionMode,
|
|
1344
|
+
env: opts.env
|
|
1345
|
+
});
|
|
1346
|
+
const agent = new _Agent(controller, handle, false, {
|
|
1347
|
+
autoApprove: opts.autoApprove,
|
|
1348
|
+
onPermission: opts.onPermission,
|
|
1349
|
+
onPlan: opts.onPlan
|
|
1350
|
+
});
|
|
1351
|
+
await ready;
|
|
1352
|
+
return agent;
|
|
1353
|
+
}
|
|
1354
|
+
/** The agent's name. */
|
|
1355
|
+
get name() {
|
|
1356
|
+
return this.handle.name;
|
|
1357
|
+
}
|
|
1358
|
+
/** The agent process PID. */
|
|
1359
|
+
get pid() {
|
|
1360
|
+
return this.handle.pid;
|
|
1361
|
+
}
|
|
1362
|
+
/** Whether the agent process is still running. */
|
|
1363
|
+
get isRunning() {
|
|
1364
|
+
return this.handle.isRunning;
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Send a message and wait for the response.
|
|
1368
|
+
*
|
|
1369
|
+
* Uses event-based waiting (via the controller's InboxPoller) instead of
|
|
1370
|
+
* polling `readUnread()` directly, which avoids a race condition where the
|
|
1371
|
+
* poller marks inbox messages as read before `receive()` can see them.
|
|
1372
|
+
*/
|
|
1373
|
+
async ask(question, opts) {
|
|
1374
|
+
this.ensureNotDisposed();
|
|
1375
|
+
const timeout = opts?.timeout ?? 12e4;
|
|
1376
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
1377
|
+
const timer = setTimeout(() => {
|
|
1378
|
+
cleanup();
|
|
1379
|
+
reject(new Error(`Timeout (${timeout}ms) waiting for response`));
|
|
1380
|
+
}, timeout);
|
|
1381
|
+
const onMsg = (text) => {
|
|
1382
|
+
cleanup();
|
|
1383
|
+
resolve(text);
|
|
1384
|
+
};
|
|
1385
|
+
const onExit = (code) => {
|
|
1386
|
+
cleanup();
|
|
1387
|
+
reject(new Error(`Agent exited (code=${code}) before responding`));
|
|
1388
|
+
};
|
|
1389
|
+
const cleanup = () => {
|
|
1390
|
+
clearTimeout(timer);
|
|
1391
|
+
this.removeListener("message", onMsg);
|
|
1392
|
+
this.removeListener("exit", onExit);
|
|
1393
|
+
};
|
|
1394
|
+
this.on("message", onMsg);
|
|
1395
|
+
this.on("exit", onExit);
|
|
1396
|
+
});
|
|
1397
|
+
const wrapped = `${question}
|
|
1398
|
+
|
|
1399
|
+
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.`;
|
|
1400
|
+
await this.handle.send(wrapped);
|
|
1401
|
+
return responsePromise;
|
|
1402
|
+
}
|
|
1403
|
+
/** Send a message without waiting for a response. */
|
|
1404
|
+
async send(message) {
|
|
1405
|
+
this.ensureNotDisposed();
|
|
1406
|
+
return this.handle.send(message);
|
|
1407
|
+
}
|
|
1408
|
+
/** Wait for the next response from this agent. */
|
|
1409
|
+
async receive(opts) {
|
|
1410
|
+
this.ensureNotDisposed();
|
|
1411
|
+
const timeout = opts?.timeout ?? 12e4;
|
|
1412
|
+
return new Promise((resolve, reject) => {
|
|
1413
|
+
const timer = setTimeout(() => {
|
|
1414
|
+
cleanup();
|
|
1415
|
+
reject(new Error(`Timeout (${timeout}ms) waiting for response`));
|
|
1416
|
+
}, timeout);
|
|
1417
|
+
const onMsg = (text) => {
|
|
1418
|
+
cleanup();
|
|
1419
|
+
resolve(text);
|
|
1420
|
+
};
|
|
1421
|
+
const onExit = (code) => {
|
|
1422
|
+
cleanup();
|
|
1423
|
+
reject(new Error(`Agent exited (code=${code}) before responding`));
|
|
1424
|
+
};
|
|
1425
|
+
const cleanup = () => {
|
|
1426
|
+
clearTimeout(timer);
|
|
1427
|
+
this.removeListener("message", onMsg);
|
|
1428
|
+
this.removeListener("exit", onExit);
|
|
1429
|
+
};
|
|
1430
|
+
this.on("message", onMsg);
|
|
1431
|
+
this.on("exit", onExit);
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Close this agent. If standalone, shuts down the entire controller.
|
|
1436
|
+
* If session-owned, kills only this agent's process.
|
|
1437
|
+
*/
|
|
1438
|
+
async close() {
|
|
1439
|
+
if (this.disposed) return;
|
|
1440
|
+
this.disposed = true;
|
|
1441
|
+
this.unwireEvents();
|
|
1442
|
+
if (this.ownsController) {
|
|
1443
|
+
await this.controller.shutdown();
|
|
1444
|
+
} else {
|
|
1445
|
+
await this.handle.kill();
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
/** Mark as disposed (used by Session when it closes). */
|
|
1449
|
+
markDisposed() {
|
|
1450
|
+
this.disposed = true;
|
|
1451
|
+
this.unwireEvents();
|
|
1452
|
+
}
|
|
1453
|
+
async [Symbol.asyncDispose]() {
|
|
1454
|
+
await this.close();
|
|
1455
|
+
}
|
|
1456
|
+
wireEvents() {
|
|
1457
|
+
const agentName = this.handle.name;
|
|
1458
|
+
const onMessage = (name, msg) => {
|
|
1459
|
+
if (name === agentName) this.emit("message", msg.text);
|
|
1460
|
+
};
|
|
1461
|
+
const onIdle = (name) => {
|
|
1462
|
+
if (name === agentName) this.emit("idle");
|
|
1463
|
+
};
|
|
1464
|
+
const onPermission = (name, parsed) => {
|
|
1465
|
+
if (name !== agentName) return;
|
|
1466
|
+
let handled = false;
|
|
1467
|
+
const guard = (fn) => () => {
|
|
1468
|
+
if (handled) return Promise.resolve();
|
|
1469
|
+
handled = true;
|
|
1470
|
+
return fn();
|
|
1471
|
+
};
|
|
1472
|
+
this.emit("permission", {
|
|
1473
|
+
requestId: parsed.requestId,
|
|
1474
|
+
toolName: parsed.toolName,
|
|
1475
|
+
description: parsed.description,
|
|
1476
|
+
input: parsed.input,
|
|
1477
|
+
approve: guard(
|
|
1478
|
+
() => this.controller.sendPermissionResponse(agentName, parsed.requestId, true)
|
|
1479
|
+
),
|
|
1480
|
+
reject: guard(
|
|
1481
|
+
() => this.controller.sendPermissionResponse(agentName, parsed.requestId, false)
|
|
1482
|
+
)
|
|
1483
|
+
});
|
|
1484
|
+
};
|
|
1485
|
+
const onPlan = (name, parsed) => {
|
|
1486
|
+
if (name !== agentName) return;
|
|
1487
|
+
let handled = false;
|
|
1488
|
+
const guard = (fn) => (...args) => {
|
|
1489
|
+
if (handled) return Promise.resolve();
|
|
1490
|
+
handled = true;
|
|
1491
|
+
return fn(...args);
|
|
1492
|
+
};
|
|
1493
|
+
this.emit("plan", {
|
|
1494
|
+
requestId: parsed.requestId,
|
|
1495
|
+
planContent: parsed.planContent,
|
|
1496
|
+
approve: guard(
|
|
1497
|
+
(feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, true, feedback)
|
|
1498
|
+
),
|
|
1499
|
+
reject: guard(
|
|
1500
|
+
(feedback) => this.controller.sendPlanApproval(agentName, parsed.requestId, false, feedback)
|
|
1501
|
+
)
|
|
1502
|
+
});
|
|
1503
|
+
};
|
|
1504
|
+
const onExit = (name, code) => {
|
|
1505
|
+
if (name === agentName) this.emit("exit", code);
|
|
1506
|
+
};
|
|
1507
|
+
const onError = (err) => {
|
|
1508
|
+
this.emit("error", err);
|
|
1509
|
+
};
|
|
1510
|
+
this.controller.on("message", onMessage);
|
|
1511
|
+
this.controller.on("idle", onIdle);
|
|
1512
|
+
this.controller.on("permission:request", onPermission);
|
|
1513
|
+
this.controller.on("plan:approval_request", onPlan);
|
|
1514
|
+
this.controller.on("agent:exited", onExit);
|
|
1515
|
+
this.controller.on("error", onError);
|
|
1516
|
+
this.boundListeners = [
|
|
1517
|
+
{ event: "message", fn: onMessage },
|
|
1518
|
+
{ event: "idle", fn: onIdle },
|
|
1519
|
+
{ event: "permission:request", fn: onPermission },
|
|
1520
|
+
{ event: "plan:approval_request", fn: onPlan },
|
|
1521
|
+
{ event: "agent:exited", fn: onExit },
|
|
1522
|
+
{ event: "error", fn: onError }
|
|
1523
|
+
];
|
|
1524
|
+
}
|
|
1525
|
+
unwireEvents() {
|
|
1526
|
+
for (const { event, fn } of this.boundListeners) {
|
|
1527
|
+
this.controller.removeListener(event, fn);
|
|
1528
|
+
}
|
|
1529
|
+
this.boundListeners = [];
|
|
1530
|
+
}
|
|
1531
|
+
wireBehavior(behavior) {
|
|
1532
|
+
if (!behavior) return;
|
|
1533
|
+
const { autoApprove, onPermission, onPlan } = behavior;
|
|
1534
|
+
if (autoApprove != null) {
|
|
1535
|
+
this.on("permission", (req) => {
|
|
1536
|
+
if (autoApprove === true) {
|
|
1537
|
+
req.approve();
|
|
1538
|
+
} else if (Array.isArray(autoApprove)) {
|
|
1539
|
+
autoApprove.includes(req.toolName) ? req.approve() : req.reject();
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
if (autoApprove === true) {
|
|
1543
|
+
this.on("plan", (req) => req.approve());
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (onPermission) {
|
|
1547
|
+
this.on("permission", onPermission);
|
|
1548
|
+
}
|
|
1549
|
+
if (onPlan) {
|
|
1550
|
+
this.on("plan", onPlan);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
ensureNotDisposed() {
|
|
1554
|
+
if (this.disposed) {
|
|
1555
|
+
throw new Error("Agent has been closed");
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
var Session = class _Session {
|
|
1560
|
+
controller;
|
|
1561
|
+
defaults;
|
|
1562
|
+
agents = /* @__PURE__ */ new Map();
|
|
1563
|
+
disposed = false;
|
|
1564
|
+
constructor(controller, defaults) {
|
|
1565
|
+
this.controller = controller;
|
|
1566
|
+
this.defaults = defaults;
|
|
1567
|
+
}
|
|
1568
|
+
static async create(opts = {}) {
|
|
1569
|
+
const env = buildEnv(opts);
|
|
1570
|
+
const controller = new ClaudeCodeController({
|
|
1571
|
+
teamName: opts.teamName ?? `session-${(0, import_node_crypto3.randomUUID)().slice(0, 8)}`,
|
|
1572
|
+
cwd: opts.cwd,
|
|
1573
|
+
claudeBinary: opts.claudeBinary,
|
|
1574
|
+
env,
|
|
1575
|
+
logLevel: opts.logLevel ?? "warn",
|
|
1576
|
+
logger: opts.logger
|
|
1577
|
+
});
|
|
1578
|
+
await controller.init();
|
|
1579
|
+
return new _Session(controller, opts);
|
|
1580
|
+
}
|
|
1581
|
+
/** Spawn a named agent in this session. Inherits session defaults. */
|
|
1582
|
+
async agent(name, opts = {}) {
|
|
1583
|
+
this.ensureNotDisposed();
|
|
1584
|
+
const merged = {
|
|
1585
|
+
model: this.defaults.model,
|
|
1586
|
+
cwd: this.defaults.cwd,
|
|
1587
|
+
permissions: this.defaults.permissions,
|
|
1588
|
+
readyTimeout: this.defaults.readyTimeout,
|
|
1589
|
+
autoApprove: this.defaults.autoApprove,
|
|
1590
|
+
onPermission: this.defaults.onPermission,
|
|
1591
|
+
onPlan: this.defaults.onPlan,
|
|
1592
|
+
...opts
|
|
1593
|
+
};
|
|
1594
|
+
const agent = await Agent.createInSession(
|
|
1595
|
+
this.controller,
|
|
1596
|
+
name,
|
|
1597
|
+
merged
|
|
1598
|
+
);
|
|
1599
|
+
this.agents.set(name, agent);
|
|
1600
|
+
return agent;
|
|
1601
|
+
}
|
|
1602
|
+
/** Get an existing agent by name. */
|
|
1603
|
+
get(name) {
|
|
1604
|
+
return this.agents.get(name);
|
|
1605
|
+
}
|
|
1606
|
+
/** Close all agents and shut down the session. */
|
|
1607
|
+
async close() {
|
|
1608
|
+
if (this.disposed) return;
|
|
1609
|
+
this.disposed = true;
|
|
1610
|
+
for (const agent of this.agents.values()) {
|
|
1611
|
+
agent.markDisposed();
|
|
1612
|
+
}
|
|
1613
|
+
await this.controller.shutdown();
|
|
1614
|
+
}
|
|
1615
|
+
async [Symbol.asyncDispose]() {
|
|
1616
|
+
await this.close();
|
|
1617
|
+
}
|
|
1618
|
+
ensureNotDisposed() {
|
|
1619
|
+
if (this.disposed) {
|
|
1620
|
+
throw new Error("Session has been closed");
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
};
|
|
1624
|
+
async function claudeCall(prompt, opts = {}) {
|
|
1625
|
+
const agent = await Agent.create(opts);
|
|
1626
|
+
try {
|
|
1627
|
+
return await agent.ask(prompt, { timeout: opts.timeout ?? 12e4 });
|
|
1628
|
+
} finally {
|
|
1629
|
+
await agent.close();
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
var claude = Object.assign(claudeCall, {
|
|
1633
|
+
/**
|
|
1634
|
+
* Create a persistent agent for multi-turn conversations.
|
|
1635
|
+
*
|
|
1636
|
+
* @example
|
|
1637
|
+
* ```ts
|
|
1638
|
+
* const agent = await claude.agent({ model: "sonnet" });
|
|
1639
|
+
* const answer = await agent.ask("What is 2+2?");
|
|
1640
|
+
* await agent.close();
|
|
1641
|
+
* ```
|
|
1642
|
+
*/
|
|
1643
|
+
agent: (opts) => Agent.create(opts),
|
|
1644
|
+
/**
|
|
1645
|
+
* Create a multi-agent session.
|
|
1646
|
+
*
|
|
1647
|
+
* @example
|
|
1648
|
+
* ```ts
|
|
1649
|
+
* const session = await claude.session({ model: "sonnet" });
|
|
1650
|
+
* const reviewer = await session.agent("reviewer", { model: "opus" });
|
|
1651
|
+
* const coder = await session.agent("coder");
|
|
1652
|
+
* await session.close();
|
|
1653
|
+
* ```
|
|
1654
|
+
*/
|
|
1655
|
+
session: (opts) => Session.create(opts)
|
|
1656
|
+
});
|
|
1657
|
+
|
|
1205
1658
|
// src/api/routes.ts
|
|
1206
1659
|
var SAFE_NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
|
|
1207
1660
|
var SAFE_TASK_ID_RE = /^[0-9]{1,10}$/;
|
|
@@ -1243,6 +1696,28 @@ function buildRoutes(state) {
|
|
|
1243
1696
|
session: state.controller !== null
|
|
1244
1697
|
});
|
|
1245
1698
|
});
|
|
1699
|
+
api.post("/ask", async (c) => {
|
|
1700
|
+
const body = await c.req.json();
|
|
1701
|
+
if (!body.prompt) {
|
|
1702
|
+
return c.json({ error: "prompt is required" }, 400);
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1705
|
+
const response = await claude(body.prompt, {
|
|
1706
|
+
model: body.model,
|
|
1707
|
+
apiKey: body.apiKey,
|
|
1708
|
+
baseUrl: body.baseUrl,
|
|
1709
|
+
timeout: body.timeout,
|
|
1710
|
+
cwd: body.cwd,
|
|
1711
|
+
permissions: body.permissions,
|
|
1712
|
+
env: body.env,
|
|
1713
|
+
logLevel: "warn"
|
|
1714
|
+
});
|
|
1715
|
+
return c.json({ response });
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
const message = err instanceof Error ? err.message : "Agent failed to respond";
|
|
1718
|
+
return c.json({ error: message }, 500);
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1246
1721
|
api.get("/session", (c) => {
|
|
1247
1722
|
if (!state.controller) {
|
|
1248
1723
|
return c.json({ initialized: false, teamName: "" });
|
|
@@ -1268,11 +1743,15 @@ function buildRoutes(state) {
|
|
|
1268
1743
|
await oldController.shutdown();
|
|
1269
1744
|
}
|
|
1270
1745
|
}
|
|
1746
|
+
const env = { ...body.env };
|
|
1747
|
+
if (body.apiKey) env.ANTHROPIC_AUTH_TOKEN = body.apiKey;
|
|
1748
|
+
if (body.baseUrl) env.ANTHROPIC_BASE_URL = body.baseUrl;
|
|
1749
|
+
if (body.timeout != null) env.API_TIMEOUT_MS = String(body.timeout);
|
|
1271
1750
|
const controller = new ClaudeCodeController({
|
|
1272
1751
|
teamName: body.teamName,
|
|
1273
1752
|
cwd: body.cwd,
|
|
1274
1753
|
claudeBinary: body.claudeBinary,
|
|
1275
|
-
env
|
|
1754
|
+
env,
|
|
1276
1755
|
logLevel: body.logLevel ?? "info"
|
|
1277
1756
|
});
|
|
1278
1757
|
try {
|
|
@@ -1329,26 +1808,6 @@ function buildRoutes(state) {
|
|
|
1329
1808
|
const pending = approvals.length + unassignedTasks.length + idleAgents.length;
|
|
1330
1809
|
return c.json({ pending, approvals, unassignedTasks, idleAgents });
|
|
1331
1810
|
});
|
|
1332
|
-
api.get("/actions/approvals", (_c) => {
|
|
1333
|
-
getController(state);
|
|
1334
|
-
return _c.json(state.tracker.getPendingApprovals());
|
|
1335
|
-
});
|
|
1336
|
-
api.get("/actions/tasks", async (c) => {
|
|
1337
|
-
const ctrl = getController(state);
|
|
1338
|
-
const tasks = await ctrl.tasks.list();
|
|
1339
|
-
const unassigned = tasks.filter((t) => !t.owner && t.status !== "completed").map((t) => ({
|
|
1340
|
-
id: t.id,
|
|
1341
|
-
subject: t.subject,
|
|
1342
|
-
description: t.description,
|
|
1343
|
-
status: t.status,
|
|
1344
|
-
action: `POST /tasks/${t.id}/assign`
|
|
1345
|
-
}));
|
|
1346
|
-
return c.json(unassigned);
|
|
1347
|
-
});
|
|
1348
|
-
api.get("/actions/idle-agents", (_c) => {
|
|
1349
|
-
getController(state);
|
|
1350
|
-
return _c.json(state.tracker.getIdleAgents());
|
|
1351
|
-
});
|
|
1352
1811
|
api.get("/agents", async (c) => {
|
|
1353
1812
|
const ctrl = getController(state);
|
|
1354
1813
|
const config = await ctrl.team.getConfig();
|
|
@@ -1369,13 +1828,25 @@ function buildRoutes(state) {
|
|
|
1369
1828
|
validateName(body.name, "name");
|
|
1370
1829
|
const agentType = body.type || "general-purpose";
|
|
1371
1830
|
state.tracker.registerAgentType(body.name, agentType);
|
|
1831
|
+
const agentEnv = { ...body.env };
|
|
1832
|
+
if (body.apiKey) agentEnv.ANTHROPIC_AUTH_TOKEN = body.apiKey;
|
|
1833
|
+
if (body.baseUrl) agentEnv.ANTHROPIC_BASE_URL = body.baseUrl;
|
|
1834
|
+
if (body.timeout != null) agentEnv.API_TIMEOUT_MS = String(body.timeout);
|
|
1835
|
+
const permissionsArray = Array.isArray(body.permissions) ? body.permissions : void 0;
|
|
1836
|
+
const PRESET_MAP = {
|
|
1837
|
+
edit: "acceptEdits",
|
|
1838
|
+
plan: "plan",
|
|
1839
|
+
ask: "default"
|
|
1840
|
+
};
|
|
1841
|
+
const permissionMode = typeof body.permissions === "string" && !Array.isArray(body.permissions) ? PRESET_MAP[body.permissions] : void 0;
|
|
1372
1842
|
const handle = await ctrl.spawnAgent({
|
|
1373
1843
|
name: body.name,
|
|
1374
1844
|
type: body.type,
|
|
1375
1845
|
model: body.model,
|
|
1376
1846
|
cwd: body.cwd,
|
|
1377
|
-
permissions:
|
|
1378
|
-
|
|
1847
|
+
permissions: permissionsArray,
|
|
1848
|
+
permissionMode,
|
|
1849
|
+
env: Object.keys(agentEnv).length > 0 ? agentEnv : void 0
|
|
1379
1850
|
});
|
|
1380
1851
|
return c.json(
|
|
1381
1852
|
{
|
|
@@ -1427,7 +1898,7 @@ function buildRoutes(state) {
|
|
|
1427
1898
|
await ctrl.sendShutdownRequest(name);
|
|
1428
1899
|
return c.json({ ok: true });
|
|
1429
1900
|
});
|
|
1430
|
-
api.post("/agents/:name/approve
|
|
1901
|
+
api.post("/agents/:name/approve", async (c) => {
|
|
1431
1902
|
const ctrl = getController(state);
|
|
1432
1903
|
const name = c.req.param("name");
|
|
1433
1904
|
validateName(name, "name");
|
|
@@ -1435,28 +1906,14 @@ function buildRoutes(state) {
|
|
|
1435
1906
|
if (!body.requestId) {
|
|
1436
1907
|
return c.json({ error: "requestId is required" }, 400);
|
|
1437
1908
|
}
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
body.feedback
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
return c.json({ ok: true });
|
|
1446
|
-
});
|
|
1447
|
-
api.post("/agents/:name/approve-permission", async (c) => {
|
|
1448
|
-
const ctrl = getController(state);
|
|
1449
|
-
const name = c.req.param("name");
|
|
1450
|
-
validateName(name, "name");
|
|
1451
|
-
const body = await c.req.json();
|
|
1452
|
-
if (!body.requestId) {
|
|
1453
|
-
return c.json({ error: "requestId is required" }, 400);
|
|
1909
|
+
if (!body.type || !["plan", "permission"].includes(body.type)) {
|
|
1910
|
+
return c.json({ error: 'type must be "plan" or "permission"' }, 400);
|
|
1911
|
+
}
|
|
1912
|
+
if (body.type === "plan") {
|
|
1913
|
+
await ctrl.sendPlanApproval(name, body.requestId, body.approve ?? true, body.feedback);
|
|
1914
|
+
} else {
|
|
1915
|
+
await ctrl.sendPermissionResponse(name, body.requestId, body.approve ?? true);
|
|
1454
1916
|
}
|
|
1455
|
-
await ctrl.sendPermissionResponse(
|
|
1456
|
-
name,
|
|
1457
|
-
body.requestId,
|
|
1458
|
-
body.approve ?? true
|
|
1459
|
-
);
|
|
1460
1917
|
state.tracker.resolveApproval(body.requestId);
|
|
1461
1918
|
return c.json({ ok: true });
|
|
1462
1919
|
});
|