claude-code-controller 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.cjs +204 -4
- package/dist/api/index.cjs.map +1 -1
- package/dist/api/index.d.cts +1 -1
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.js +204 -4
- package/dist/api/index.js.map +1 -1
- package/dist/{claude-CSXlMCvP.d.cts → claude-B7-oBjuE.d.cts} +52 -1
- package/dist/{claude-CSXlMCvP.d.ts → claude-B7-oBjuE.d.ts} +52 -1
- package/dist/index.cjs +214 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -4
- package/dist/index.d.ts +60 -4
- package/dist/index.js +209 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/api/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { C as ClaudeCodeController,
|
|
2
|
+
import { C as ClaudeCodeController, k as PermissionPreset, F as TaskStatus, b as LogLevel, e as AgentType } from '../claude-B7-oBjuE.cjs';
|
|
3
3
|
import 'node:events';
|
|
4
4
|
|
|
5
5
|
interface PendingApproval {
|
package/dist/api/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { C as ClaudeCodeController,
|
|
2
|
+
import { C as ClaudeCodeController, k as PermissionPreset, F as TaskStatus, b as LogLevel, e as AgentType } from '../claude-B7-oBjuE.js';
|
|
3
3
|
import 'node:events';
|
|
4
4
|
|
|
5
5
|
interface PendingApproval {
|
package/dist/api/index.js
CHANGED
|
@@ -106,9 +106,11 @@ var ActionTracker = class {
|
|
|
106
106
|
import { Hono } from "hono";
|
|
107
107
|
|
|
108
108
|
// src/controller.ts
|
|
109
|
-
import { EventEmitter } from "events";
|
|
109
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
110
110
|
import { execSync as execSync2 } from "child_process";
|
|
111
111
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
112
|
+
import { mkdirSync as mkdirSync2, existsSync as existsSync5, writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
|
|
113
|
+
import { join as join3 } from "path";
|
|
112
114
|
|
|
113
115
|
// src/team-manager.ts
|
|
114
116
|
import { readFile, writeFile, mkdir, rm } from "fs/promises";
|
|
@@ -441,6 +443,8 @@ if pid == 0:
|
|
|
441
443
|
else:
|
|
442
444
|
signal.signal(signal.SIGTERM, lambda *a: (os.kill(pid, signal.SIGTERM), sys.exit(0)))
|
|
443
445
|
signal.signal(signal.SIGINT, lambda *a: (os.kill(pid, signal.SIGTERM), sys.exit(0)))
|
|
446
|
+
buf = b""
|
|
447
|
+
trust_sent = False
|
|
444
448
|
try:
|
|
445
449
|
while True:
|
|
446
450
|
r, _, _ = select.select([fd, 0], [], [], 1.0)
|
|
@@ -450,6 +454,12 @@ else:
|
|
|
450
454
|
if not data:
|
|
451
455
|
break
|
|
452
456
|
os.write(1, data)
|
|
457
|
+
if not trust_sent:
|
|
458
|
+
buf += data
|
|
459
|
+
if b"Yes" in buf and b"trust" in buf:
|
|
460
|
+
os.write(fd, b"\\r")
|
|
461
|
+
trust_sent = True
|
|
462
|
+
buf = b""
|
|
453
463
|
except OSError:
|
|
454
464
|
break
|
|
455
465
|
if 0 in r:
|
|
@@ -789,6 +799,135 @@ function createLogger(level = "info") {
|
|
|
789
799
|
};
|
|
790
800
|
}
|
|
791
801
|
|
|
802
|
+
// src/statusline-capture.ts
|
|
803
|
+
import { EventEmitter } from "events";
|
|
804
|
+
import { watch, existsSync as existsSync4, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
805
|
+
import { join as join2 } from "path";
|
|
806
|
+
function statusLineDir(teamName) {
|
|
807
|
+
return join2(teamDir(teamName), "statusline");
|
|
808
|
+
}
|
|
809
|
+
function statusLineLogPath(teamName, agentName) {
|
|
810
|
+
return join2(statusLineDir(teamName), `${agentName}.jsonl`);
|
|
811
|
+
}
|
|
812
|
+
function buildStatusLineCommand(logFilePath) {
|
|
813
|
+
const escapedPath = logFilePath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
814
|
+
return [
|
|
815
|
+
"python3 -c",
|
|
816
|
+
`'import sys,json;`,
|
|
817
|
+
`d=json.load(sys.stdin);`,
|
|
818
|
+
`open("${escapedPath}","a").write(json.dumps(d)+"\\n");`,
|
|
819
|
+
`print(d.get("model",{}).get("display_name",""))'`
|
|
820
|
+
].join(" ");
|
|
821
|
+
}
|
|
822
|
+
function buildStatusLineSettings(logFilePath) {
|
|
823
|
+
return {
|
|
824
|
+
statusLine: {
|
|
825
|
+
type: "command",
|
|
826
|
+
command: buildStatusLineCommand(logFilePath)
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
var StatusLineWatcher = class extends EventEmitter {
|
|
831
|
+
teamName;
|
|
832
|
+
log;
|
|
833
|
+
watchers = /* @__PURE__ */ new Map();
|
|
834
|
+
fileOffsets = /* @__PURE__ */ new Map();
|
|
835
|
+
stopped = false;
|
|
836
|
+
constructor(teamName, logger) {
|
|
837
|
+
super();
|
|
838
|
+
this.teamName = teamName;
|
|
839
|
+
this.log = logger;
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Ensure the statusline directory exists.
|
|
843
|
+
*/
|
|
844
|
+
ensureDir() {
|
|
845
|
+
const dir = statusLineDir(this.teamName);
|
|
846
|
+
if (!existsSync4(dir)) {
|
|
847
|
+
mkdirSync(dir, { recursive: true });
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Start watching a specific agent's statusLine log file.
|
|
852
|
+
*/
|
|
853
|
+
watchAgent(agentName) {
|
|
854
|
+
if (this.stopped) return;
|
|
855
|
+
const filePath = statusLineLogPath(this.teamName, agentName);
|
|
856
|
+
if (!existsSync4(filePath)) {
|
|
857
|
+
writeFileSync(filePath, "");
|
|
858
|
+
}
|
|
859
|
+
try {
|
|
860
|
+
const stats = readFileSync(filePath);
|
|
861
|
+
this.fileOffsets.set(filePath, stats.length);
|
|
862
|
+
} catch {
|
|
863
|
+
this.fileOffsets.set(filePath, 0);
|
|
864
|
+
}
|
|
865
|
+
try {
|
|
866
|
+
const watcher = watch(filePath, (eventType) => {
|
|
867
|
+
if (eventType === "change") {
|
|
868
|
+
this.readNewLines(agentName, filePath);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
this.watchers.set(agentName, watcher);
|
|
872
|
+
this.log.debug(`Watching statusLine for agent "${agentName}" at ${filePath}`);
|
|
873
|
+
} catch (err) {
|
|
874
|
+
this.log.error(`Failed to watch statusLine for "${agentName}": ${err}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Stop watching a specific agent.
|
|
879
|
+
*/
|
|
880
|
+
unwatchAgent(agentName) {
|
|
881
|
+
const watcher = this.watchers.get(agentName);
|
|
882
|
+
if (watcher) {
|
|
883
|
+
watcher.close();
|
|
884
|
+
this.watchers.delete(agentName);
|
|
885
|
+
}
|
|
886
|
+
const filePath = statusLineLogPath(this.teamName, agentName);
|
|
887
|
+
this.fileOffsets.delete(filePath);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Stop all watchers.
|
|
891
|
+
*/
|
|
892
|
+
stop() {
|
|
893
|
+
this.stopped = true;
|
|
894
|
+
for (const [, watcher] of this.watchers) {
|
|
895
|
+
watcher.close();
|
|
896
|
+
}
|
|
897
|
+
this.watchers.clear();
|
|
898
|
+
this.fileOffsets.clear();
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Read new lines appended to the log file since last read.
|
|
902
|
+
*/
|
|
903
|
+
readNewLines(agentName, filePath) {
|
|
904
|
+
try {
|
|
905
|
+
const content = readFileSync(filePath, "utf-8");
|
|
906
|
+
const offset = this.fileOffsets.get(filePath) ?? 0;
|
|
907
|
+
const newContent = content.slice(offset);
|
|
908
|
+
this.fileOffsets.set(filePath, content.length);
|
|
909
|
+
if (!newContent.trim()) return;
|
|
910
|
+
const lines = newContent.trim().split("\n");
|
|
911
|
+
for (const line of lines) {
|
|
912
|
+
if (!line.trim()) continue;
|
|
913
|
+
try {
|
|
914
|
+
const data = JSON.parse(line);
|
|
915
|
+
const event = {
|
|
916
|
+
agentName,
|
|
917
|
+
data,
|
|
918
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
919
|
+
};
|
|
920
|
+
this.emit("update", event);
|
|
921
|
+
} catch (parseErr) {
|
|
922
|
+
this.log.debug(`Failed to parse statusLine JSON: ${line}`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
} catch (err) {
|
|
926
|
+
this.log.debug(`Error reading statusLine file: ${err}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
|
|
792
931
|
// src/controller.ts
|
|
793
932
|
var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
794
933
|
"shutdown_approved",
|
|
@@ -808,12 +947,13 @@ var AGENT_COLORS = [
|
|
|
808
947
|
"#FF69B4",
|
|
809
948
|
"#7B68EE"
|
|
810
949
|
];
|
|
811
|
-
var ClaudeCodeController = class extends
|
|
950
|
+
var ClaudeCodeController = class extends EventEmitter2 {
|
|
812
951
|
teamName;
|
|
813
952
|
team;
|
|
814
953
|
tasks;
|
|
815
954
|
processes;
|
|
816
955
|
poller;
|
|
956
|
+
statusLineWatcher;
|
|
817
957
|
log;
|
|
818
958
|
cwd;
|
|
819
959
|
claudeBinary;
|
|
@@ -835,7 +975,11 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
835
975
|
"controller",
|
|
836
976
|
this.log
|
|
837
977
|
);
|
|
978
|
+
this.statusLineWatcher = new StatusLineWatcher(this.teamName, this.log);
|
|
838
979
|
this.poller.onMessages((events) => this.handlePollEvents(events));
|
|
980
|
+
this.statusLineWatcher.on("update", (event) => {
|
|
981
|
+
this.emit("agent:statusline", event.agentName, event.data);
|
|
982
|
+
});
|
|
839
983
|
}
|
|
840
984
|
// ─── Lifecycle ───────────────────────────────────────────────────────
|
|
841
985
|
/**
|
|
@@ -846,6 +990,7 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
846
990
|
if (this.initialized) return this;
|
|
847
991
|
await this.team.create({ cwd: this.cwd });
|
|
848
992
|
await this.tasks.init();
|
|
993
|
+
this.statusLineWatcher.ensureDir();
|
|
849
994
|
this.poller.start();
|
|
850
995
|
this.initialized = true;
|
|
851
996
|
this.log.info(
|
|
@@ -886,6 +1031,7 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
886
1031
|
}
|
|
887
1032
|
await this.processes.killAll();
|
|
888
1033
|
this.poller.stop();
|
|
1034
|
+
this.statusLineWatcher.stop();
|
|
889
1035
|
await this.team.destroy();
|
|
890
1036
|
this.initialized = false;
|
|
891
1037
|
this.log.info("Controller shut down");
|
|
@@ -914,6 +1060,8 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
914
1060
|
subscriptions: []
|
|
915
1061
|
};
|
|
916
1062
|
await this.team.addMember(member);
|
|
1063
|
+
this.ensureWorkspaceTrusted(cwd, opts.name);
|
|
1064
|
+
this.statusLineWatcher.watchAgent(opts.name);
|
|
917
1065
|
const env = Object.keys(this.defaultEnv).length > 0 || opts.env ? { ...this.defaultEnv, ...opts.env } : void 0;
|
|
918
1066
|
const proc = this.processes.spawn({
|
|
919
1067
|
teamName: this.teamName,
|
|
@@ -1120,6 +1268,7 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
1120
1268
|
* Kill a specific agent.
|
|
1121
1269
|
*/
|
|
1122
1270
|
async killAgent(name) {
|
|
1271
|
+
this.statusLineWatcher.unwatchAgent(name);
|
|
1123
1272
|
await this.processes.kill(name);
|
|
1124
1273
|
await this.team.removeMember(name);
|
|
1125
1274
|
}
|
|
@@ -1181,6 +1330,32 @@ var ClaudeCodeController = class extends EventEmitter {
|
|
|
1181
1330
|
}
|
|
1182
1331
|
}
|
|
1183
1332
|
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Ensure the agent's cwd has a .claude/settings.local.json so the
|
|
1335
|
+
* CLI skips the interactive workspace trust prompt.
|
|
1336
|
+
* Also injects statusLine capture configuration.
|
|
1337
|
+
*/
|
|
1338
|
+
ensureWorkspaceTrusted(cwd, agentName) {
|
|
1339
|
+
const claudeDir = join3(cwd, ".claude");
|
|
1340
|
+
const settingsPath = join3(claudeDir, "settings.local.json");
|
|
1341
|
+
mkdirSync2(claudeDir, { recursive: true });
|
|
1342
|
+
let settings = {};
|
|
1343
|
+
if (existsSync5(settingsPath)) {
|
|
1344
|
+
try {
|
|
1345
|
+
settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
1346
|
+
} catch {
|
|
1347
|
+
settings = {};
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (agentName && !settings.statusLine) {
|
|
1351
|
+
const logPath = statusLineLogPath(this.teamName, agentName);
|
|
1352
|
+
const statusLineSettings = buildStatusLineSettings(logPath);
|
|
1353
|
+
settings = { ...settings, ...statusLineSettings };
|
|
1354
|
+
this.log.debug(`Injected statusLine capture for "${agentName}"`);
|
|
1355
|
+
}
|
|
1356
|
+
writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1357
|
+
this.log.debug(`Updated ${settingsPath}`);
|
|
1358
|
+
}
|
|
1184
1359
|
ensureInitialized() {
|
|
1185
1360
|
if (!this.initialized) {
|
|
1186
1361
|
throw new Error(
|
|
@@ -1194,7 +1369,7 @@ function sleep2(ms) {
|
|
|
1194
1369
|
}
|
|
1195
1370
|
|
|
1196
1371
|
// src/claude.ts
|
|
1197
|
-
import { EventEmitter as
|
|
1372
|
+
import { EventEmitter as EventEmitter3 } from "events";
|
|
1198
1373
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
1199
1374
|
Symbol.asyncDispose ??= /* @__PURE__ */ Symbol("Symbol.asyncDispose");
|
|
1200
1375
|
function buildEnv(opts) {
|
|
@@ -1267,7 +1442,7 @@ function waitForReady(controller, agentName, timeoutMs = 15e3) {
|
|
|
1267
1442
|
controller.on("agent:exited", onExit);
|
|
1268
1443
|
});
|
|
1269
1444
|
}
|
|
1270
|
-
var Agent = class _Agent extends
|
|
1445
|
+
var Agent = class _Agent extends EventEmitter3 {
|
|
1271
1446
|
controller;
|
|
1272
1447
|
handle;
|
|
1273
1448
|
ownsController;
|
|
@@ -1360,14 +1535,22 @@ var Agent = class _Agent extends EventEmitter2 {
|
|
|
1360
1535
|
this.ensureNotDisposed();
|
|
1361
1536
|
const timeout = opts?.timeout ?? 12e4;
|
|
1362
1537
|
const responsePromise = new Promise((resolve, reject) => {
|
|
1538
|
+
let gotMessage = false;
|
|
1363
1539
|
const timer = setTimeout(() => {
|
|
1364
1540
|
cleanup();
|
|
1365
1541
|
reject(new Error(`Timeout (${timeout}ms) waiting for response`));
|
|
1366
1542
|
}, timeout);
|
|
1367
1543
|
const onMsg = (text) => {
|
|
1544
|
+
gotMessage = true;
|
|
1368
1545
|
cleanup();
|
|
1369
1546
|
resolve(text);
|
|
1370
1547
|
};
|
|
1548
|
+
const onIdle = () => {
|
|
1549
|
+
if (!gotMessage) {
|
|
1550
|
+
cleanup();
|
|
1551
|
+
resolve("");
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1371
1554
|
const onExit = (code) => {
|
|
1372
1555
|
cleanup();
|
|
1373
1556
|
reject(new Error(`Agent exited (code=${code}) before responding`));
|
|
@@ -1375,9 +1558,11 @@ var Agent = class _Agent extends EventEmitter2 {
|
|
|
1375
1558
|
const cleanup = () => {
|
|
1376
1559
|
clearTimeout(timer);
|
|
1377
1560
|
this.removeListener("message", onMsg);
|
|
1561
|
+
this.removeListener("idle", onIdle);
|
|
1378
1562
|
this.removeListener("exit", onExit);
|
|
1379
1563
|
};
|
|
1380
1564
|
this.on("message", onMsg);
|
|
1565
|
+
this.on("idle", onIdle);
|
|
1381
1566
|
this.on("exit", onExit);
|
|
1382
1567
|
});
|
|
1383
1568
|
const wrapped = `${question}
|
|
@@ -1396,14 +1581,22 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
|
|
|
1396
1581
|
this.ensureNotDisposed();
|
|
1397
1582
|
const timeout = opts?.timeout ?? 12e4;
|
|
1398
1583
|
return new Promise((resolve, reject) => {
|
|
1584
|
+
let gotMessage = false;
|
|
1399
1585
|
const timer = setTimeout(() => {
|
|
1400
1586
|
cleanup();
|
|
1401
1587
|
reject(new Error(`Timeout (${timeout}ms) waiting for response`));
|
|
1402
1588
|
}, timeout);
|
|
1403
1589
|
const onMsg = (text) => {
|
|
1590
|
+
gotMessage = true;
|
|
1404
1591
|
cleanup();
|
|
1405
1592
|
resolve(text);
|
|
1406
1593
|
};
|
|
1594
|
+
const onIdle = () => {
|
|
1595
|
+
if (!gotMessage) {
|
|
1596
|
+
cleanup();
|
|
1597
|
+
resolve("");
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1407
1600
|
const onExit = (code) => {
|
|
1408
1601
|
cleanup();
|
|
1409
1602
|
reject(new Error(`Agent exited (code=${code}) before responding`));
|
|
@@ -1411,9 +1604,11 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
|
|
|
1411
1604
|
const cleanup = () => {
|
|
1412
1605
|
clearTimeout(timer);
|
|
1413
1606
|
this.removeListener("message", onMsg);
|
|
1607
|
+
this.removeListener("idle", onIdle);
|
|
1414
1608
|
this.removeListener("exit", onExit);
|
|
1415
1609
|
};
|
|
1416
1610
|
this.on("message", onMsg);
|
|
1611
|
+
this.on("idle", onIdle);
|
|
1417
1612
|
this.on("exit", onExit);
|
|
1418
1613
|
});
|
|
1419
1614
|
}
|
|
@@ -1447,6 +1642,9 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
|
|
|
1447
1642
|
const onIdle = (name, _details) => {
|
|
1448
1643
|
if (name === agentName) this.emit("idle");
|
|
1449
1644
|
};
|
|
1645
|
+
const onStatusLine = (name, data) => {
|
|
1646
|
+
if (name === agentName) this.emit("statusline", data);
|
|
1647
|
+
};
|
|
1450
1648
|
const onPermission = (name, parsed) => {
|
|
1451
1649
|
if (name !== agentName) return;
|
|
1452
1650
|
let handled = false;
|
|
@@ -1495,6 +1693,7 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
|
|
|
1495
1693
|
};
|
|
1496
1694
|
this.controller.on("message", onMessage);
|
|
1497
1695
|
this.controller.on("idle", onIdle);
|
|
1696
|
+
this.controller.on("agent:statusline", onStatusLine);
|
|
1498
1697
|
this.controller.on("permission:request", onPermission);
|
|
1499
1698
|
this.controller.on("plan:approval_request", onPlan);
|
|
1500
1699
|
this.controller.on("agent:exited", onExit);
|
|
@@ -1502,6 +1701,7 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
|
|
|
1502
1701
|
this.boundListeners = [
|
|
1503
1702
|
{ event: "message", fn: onMessage },
|
|
1504
1703
|
{ event: "idle", fn: onIdle },
|
|
1704
|
+
{ event: "agent:statusline", fn: onStatusLine },
|
|
1505
1705
|
{ event: "permission:request", fn: onPermission },
|
|
1506
1706
|
{ event: "plan:approval_request", fn: onPlan },
|
|
1507
1707
|
{ event: "agent:exited", fn: onExit },
|