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.cjs
CHANGED
|
@@ -131,9 +131,11 @@ var ActionTracker = class {
|
|
|
131
131
|
var import_hono = require("hono");
|
|
132
132
|
|
|
133
133
|
// src/controller.ts
|
|
134
|
-
var
|
|
134
|
+
var import_node_events2 = require("events");
|
|
135
135
|
var import_node_child_process3 = require("child_process");
|
|
136
136
|
var import_node_crypto2 = require("crypto");
|
|
137
|
+
var import_node_fs5 = require("fs");
|
|
138
|
+
var import_node_path4 = require("path");
|
|
137
139
|
|
|
138
140
|
// src/team-manager.ts
|
|
139
141
|
var import_promises = require("fs/promises");
|
|
@@ -466,6 +468,8 @@ if pid == 0:
|
|
|
466
468
|
else:
|
|
467
469
|
signal.signal(signal.SIGTERM, lambda *a: (os.kill(pid, signal.SIGTERM), sys.exit(0)))
|
|
468
470
|
signal.signal(signal.SIGINT, lambda *a: (os.kill(pid, signal.SIGTERM), sys.exit(0)))
|
|
471
|
+
buf = b""
|
|
472
|
+
trust_sent = False
|
|
469
473
|
try:
|
|
470
474
|
while True:
|
|
471
475
|
r, _, _ = select.select([fd, 0], [], [], 1.0)
|
|
@@ -475,6 +479,12 @@ else:
|
|
|
475
479
|
if not data:
|
|
476
480
|
break
|
|
477
481
|
os.write(1, data)
|
|
482
|
+
if not trust_sent:
|
|
483
|
+
buf += data
|
|
484
|
+
if b"Yes" in buf and b"trust" in buf:
|
|
485
|
+
os.write(fd, b"\\r")
|
|
486
|
+
trust_sent = True
|
|
487
|
+
buf = b""
|
|
478
488
|
except OSError:
|
|
479
489
|
break
|
|
480
490
|
if 0 in r:
|
|
@@ -814,6 +824,135 @@ function createLogger(level = "info") {
|
|
|
814
824
|
};
|
|
815
825
|
}
|
|
816
826
|
|
|
827
|
+
// src/statusline-capture.ts
|
|
828
|
+
var import_node_events = require("events");
|
|
829
|
+
var import_node_fs4 = require("fs");
|
|
830
|
+
var import_node_path3 = require("path");
|
|
831
|
+
function statusLineDir(teamName) {
|
|
832
|
+
return (0, import_node_path3.join)(teamDir(teamName), "statusline");
|
|
833
|
+
}
|
|
834
|
+
function statusLineLogPath(teamName, agentName) {
|
|
835
|
+
return (0, import_node_path3.join)(statusLineDir(teamName), `${agentName}.jsonl`);
|
|
836
|
+
}
|
|
837
|
+
function buildStatusLineCommand(logFilePath) {
|
|
838
|
+
const escapedPath = logFilePath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
839
|
+
return [
|
|
840
|
+
"python3 -c",
|
|
841
|
+
`'import sys,json;`,
|
|
842
|
+
`d=json.load(sys.stdin);`,
|
|
843
|
+
`open("${escapedPath}","a").write(json.dumps(d)+"\\n");`,
|
|
844
|
+
`print(d.get("model",{}).get("display_name",""))'`
|
|
845
|
+
].join(" ");
|
|
846
|
+
}
|
|
847
|
+
function buildStatusLineSettings(logFilePath) {
|
|
848
|
+
return {
|
|
849
|
+
statusLine: {
|
|
850
|
+
type: "command",
|
|
851
|
+
command: buildStatusLineCommand(logFilePath)
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
var StatusLineWatcher = class extends import_node_events.EventEmitter {
|
|
856
|
+
teamName;
|
|
857
|
+
log;
|
|
858
|
+
watchers = /* @__PURE__ */ new Map();
|
|
859
|
+
fileOffsets = /* @__PURE__ */ new Map();
|
|
860
|
+
stopped = false;
|
|
861
|
+
constructor(teamName, logger) {
|
|
862
|
+
super();
|
|
863
|
+
this.teamName = teamName;
|
|
864
|
+
this.log = logger;
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Ensure the statusline directory exists.
|
|
868
|
+
*/
|
|
869
|
+
ensureDir() {
|
|
870
|
+
const dir = statusLineDir(this.teamName);
|
|
871
|
+
if (!(0, import_node_fs4.existsSync)(dir)) {
|
|
872
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Start watching a specific agent's statusLine log file.
|
|
877
|
+
*/
|
|
878
|
+
watchAgent(agentName) {
|
|
879
|
+
if (this.stopped) return;
|
|
880
|
+
const filePath = statusLineLogPath(this.teamName, agentName);
|
|
881
|
+
if (!(0, import_node_fs4.existsSync)(filePath)) {
|
|
882
|
+
(0, import_node_fs4.writeFileSync)(filePath, "");
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
const stats = (0, import_node_fs4.readFileSync)(filePath);
|
|
886
|
+
this.fileOffsets.set(filePath, stats.length);
|
|
887
|
+
} catch {
|
|
888
|
+
this.fileOffsets.set(filePath, 0);
|
|
889
|
+
}
|
|
890
|
+
try {
|
|
891
|
+
const watcher = (0, import_node_fs4.watch)(filePath, (eventType) => {
|
|
892
|
+
if (eventType === "change") {
|
|
893
|
+
this.readNewLines(agentName, filePath);
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
this.watchers.set(agentName, watcher);
|
|
897
|
+
this.log.debug(`Watching statusLine for agent "${agentName}" at ${filePath}`);
|
|
898
|
+
} catch (err) {
|
|
899
|
+
this.log.error(`Failed to watch statusLine for "${agentName}": ${err}`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Stop watching a specific agent.
|
|
904
|
+
*/
|
|
905
|
+
unwatchAgent(agentName) {
|
|
906
|
+
const watcher = this.watchers.get(agentName);
|
|
907
|
+
if (watcher) {
|
|
908
|
+
watcher.close();
|
|
909
|
+
this.watchers.delete(agentName);
|
|
910
|
+
}
|
|
911
|
+
const filePath = statusLineLogPath(this.teamName, agentName);
|
|
912
|
+
this.fileOffsets.delete(filePath);
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Stop all watchers.
|
|
916
|
+
*/
|
|
917
|
+
stop() {
|
|
918
|
+
this.stopped = true;
|
|
919
|
+
for (const [, watcher] of this.watchers) {
|
|
920
|
+
watcher.close();
|
|
921
|
+
}
|
|
922
|
+
this.watchers.clear();
|
|
923
|
+
this.fileOffsets.clear();
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Read new lines appended to the log file since last read.
|
|
927
|
+
*/
|
|
928
|
+
readNewLines(agentName, filePath) {
|
|
929
|
+
try {
|
|
930
|
+
const content = (0, import_node_fs4.readFileSync)(filePath, "utf-8");
|
|
931
|
+
const offset = this.fileOffsets.get(filePath) ?? 0;
|
|
932
|
+
const newContent = content.slice(offset);
|
|
933
|
+
this.fileOffsets.set(filePath, content.length);
|
|
934
|
+
if (!newContent.trim()) return;
|
|
935
|
+
const lines = newContent.trim().split("\n");
|
|
936
|
+
for (const line of lines) {
|
|
937
|
+
if (!line.trim()) continue;
|
|
938
|
+
try {
|
|
939
|
+
const data = JSON.parse(line);
|
|
940
|
+
const event = {
|
|
941
|
+
agentName,
|
|
942
|
+
data,
|
|
943
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
944
|
+
};
|
|
945
|
+
this.emit("update", event);
|
|
946
|
+
} catch (parseErr) {
|
|
947
|
+
this.log.debug(`Failed to parse statusLine JSON: ${line}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
} catch (err) {
|
|
951
|
+
this.log.debug(`Error reading statusLine file: ${err}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
};
|
|
955
|
+
|
|
817
956
|
// src/controller.ts
|
|
818
957
|
var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
|
|
819
958
|
"shutdown_approved",
|
|
@@ -833,12 +972,13 @@ var AGENT_COLORS = [
|
|
|
833
972
|
"#FF69B4",
|
|
834
973
|
"#7B68EE"
|
|
835
974
|
];
|
|
836
|
-
var ClaudeCodeController = class extends
|
|
975
|
+
var ClaudeCodeController = class extends import_node_events2.EventEmitter {
|
|
837
976
|
teamName;
|
|
838
977
|
team;
|
|
839
978
|
tasks;
|
|
840
979
|
processes;
|
|
841
980
|
poller;
|
|
981
|
+
statusLineWatcher;
|
|
842
982
|
log;
|
|
843
983
|
cwd;
|
|
844
984
|
claudeBinary;
|
|
@@ -860,7 +1000,11 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
860
1000
|
"controller",
|
|
861
1001
|
this.log
|
|
862
1002
|
);
|
|
1003
|
+
this.statusLineWatcher = new StatusLineWatcher(this.teamName, this.log);
|
|
863
1004
|
this.poller.onMessages((events) => this.handlePollEvents(events));
|
|
1005
|
+
this.statusLineWatcher.on("update", (event) => {
|
|
1006
|
+
this.emit("agent:statusline", event.agentName, event.data);
|
|
1007
|
+
});
|
|
864
1008
|
}
|
|
865
1009
|
// ─── Lifecycle ───────────────────────────────────────────────────────
|
|
866
1010
|
/**
|
|
@@ -871,6 +1015,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
871
1015
|
if (this.initialized) return this;
|
|
872
1016
|
await this.team.create({ cwd: this.cwd });
|
|
873
1017
|
await this.tasks.init();
|
|
1018
|
+
this.statusLineWatcher.ensureDir();
|
|
874
1019
|
this.poller.start();
|
|
875
1020
|
this.initialized = true;
|
|
876
1021
|
this.log.info(
|
|
@@ -911,6 +1056,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
911
1056
|
}
|
|
912
1057
|
await this.processes.killAll();
|
|
913
1058
|
this.poller.stop();
|
|
1059
|
+
this.statusLineWatcher.stop();
|
|
914
1060
|
await this.team.destroy();
|
|
915
1061
|
this.initialized = false;
|
|
916
1062
|
this.log.info("Controller shut down");
|
|
@@ -939,6 +1085,8 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
939
1085
|
subscriptions: []
|
|
940
1086
|
};
|
|
941
1087
|
await this.team.addMember(member);
|
|
1088
|
+
this.ensureWorkspaceTrusted(cwd, opts.name);
|
|
1089
|
+
this.statusLineWatcher.watchAgent(opts.name);
|
|
942
1090
|
const env = Object.keys(this.defaultEnv).length > 0 || opts.env ? { ...this.defaultEnv, ...opts.env } : void 0;
|
|
943
1091
|
const proc = this.processes.spawn({
|
|
944
1092
|
teamName: this.teamName,
|
|
@@ -1145,6 +1293,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
1145
1293
|
* Kill a specific agent.
|
|
1146
1294
|
*/
|
|
1147
1295
|
async killAgent(name) {
|
|
1296
|
+
this.statusLineWatcher.unwatchAgent(name);
|
|
1148
1297
|
await this.processes.kill(name);
|
|
1149
1298
|
await this.team.removeMember(name);
|
|
1150
1299
|
}
|
|
@@ -1206,6 +1355,32 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
|
|
|
1206
1355
|
}
|
|
1207
1356
|
}
|
|
1208
1357
|
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Ensure the agent's cwd has a .claude/settings.local.json so the
|
|
1360
|
+
* CLI skips the interactive workspace trust prompt.
|
|
1361
|
+
* Also injects statusLine capture configuration.
|
|
1362
|
+
*/
|
|
1363
|
+
ensureWorkspaceTrusted(cwd, agentName) {
|
|
1364
|
+
const claudeDir = (0, import_node_path4.join)(cwd, ".claude");
|
|
1365
|
+
const settingsPath = (0, import_node_path4.join)(claudeDir, "settings.local.json");
|
|
1366
|
+
(0, import_node_fs5.mkdirSync)(claudeDir, { recursive: true });
|
|
1367
|
+
let settings = {};
|
|
1368
|
+
if ((0, import_node_fs5.existsSync)(settingsPath)) {
|
|
1369
|
+
try {
|
|
1370
|
+
settings = JSON.parse((0, import_node_fs5.readFileSync)(settingsPath, "utf-8"));
|
|
1371
|
+
} catch {
|
|
1372
|
+
settings = {};
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
if (agentName && !settings.statusLine) {
|
|
1376
|
+
const logPath = statusLineLogPath(this.teamName, agentName);
|
|
1377
|
+
const statusLineSettings = buildStatusLineSettings(logPath);
|
|
1378
|
+
settings = { ...settings, ...statusLineSettings };
|
|
1379
|
+
this.log.debug(`Injected statusLine capture for "${agentName}"`);
|
|
1380
|
+
}
|
|
1381
|
+
(0, import_node_fs5.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1382
|
+
this.log.debug(`Updated ${settingsPath}`);
|
|
1383
|
+
}
|
|
1209
1384
|
ensureInitialized() {
|
|
1210
1385
|
if (!this.initialized) {
|
|
1211
1386
|
throw new Error(
|
|
@@ -1219,7 +1394,7 @@ function sleep2(ms) {
|
|
|
1219
1394
|
}
|
|
1220
1395
|
|
|
1221
1396
|
// src/claude.ts
|
|
1222
|
-
var
|
|
1397
|
+
var import_node_events3 = require("events");
|
|
1223
1398
|
var import_node_crypto3 = require("crypto");
|
|
1224
1399
|
Symbol.asyncDispose ??= /* @__PURE__ */ Symbol("Symbol.asyncDispose");
|
|
1225
1400
|
function buildEnv(opts) {
|
|
@@ -1292,7 +1467,7 @@ function waitForReady(controller, agentName, timeoutMs = 15e3) {
|
|
|
1292
1467
|
controller.on("agent:exited", onExit);
|
|
1293
1468
|
});
|
|
1294
1469
|
}
|
|
1295
|
-
var Agent = class _Agent extends
|
|
1470
|
+
var Agent = class _Agent extends import_node_events3.EventEmitter {
|
|
1296
1471
|
controller;
|
|
1297
1472
|
handle;
|
|
1298
1473
|
ownsController;
|
|
@@ -1385,14 +1560,22 @@ var Agent = class _Agent extends import_node_events2.EventEmitter {
|
|
|
1385
1560
|
this.ensureNotDisposed();
|
|
1386
1561
|
const timeout = opts?.timeout ?? 12e4;
|
|
1387
1562
|
const responsePromise = new Promise((resolve, reject) => {
|
|
1563
|
+
let gotMessage = false;
|
|
1388
1564
|
const timer = setTimeout(() => {
|
|
1389
1565
|
cleanup();
|
|
1390
1566
|
reject(new Error(`Timeout (${timeout}ms) waiting for response`));
|
|
1391
1567
|
}, timeout);
|
|
1392
1568
|
const onMsg = (text) => {
|
|
1569
|
+
gotMessage = true;
|
|
1393
1570
|
cleanup();
|
|
1394
1571
|
resolve(text);
|
|
1395
1572
|
};
|
|
1573
|
+
const onIdle = () => {
|
|
1574
|
+
if (!gotMessage) {
|
|
1575
|
+
cleanup();
|
|
1576
|
+
resolve("");
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1396
1579
|
const onExit = (code) => {
|
|
1397
1580
|
cleanup();
|
|
1398
1581
|
reject(new Error(`Agent exited (code=${code}) before responding`));
|
|
@@ -1400,9 +1583,11 @@ var Agent = class _Agent extends import_node_events2.EventEmitter {
|
|
|
1400
1583
|
const cleanup = () => {
|
|
1401
1584
|
clearTimeout(timer);
|
|
1402
1585
|
this.removeListener("message", onMsg);
|
|
1586
|
+
this.removeListener("idle", onIdle);
|
|
1403
1587
|
this.removeListener("exit", onExit);
|
|
1404
1588
|
};
|
|
1405
1589
|
this.on("message", onMsg);
|
|
1590
|
+
this.on("idle", onIdle);
|
|
1406
1591
|
this.on("exit", onExit);
|
|
1407
1592
|
});
|
|
1408
1593
|
const wrapped = `${question}
|
|
@@ -1421,14 +1606,22 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
|
|
|
1421
1606
|
this.ensureNotDisposed();
|
|
1422
1607
|
const timeout = opts?.timeout ?? 12e4;
|
|
1423
1608
|
return new Promise((resolve, reject) => {
|
|
1609
|
+
let gotMessage = false;
|
|
1424
1610
|
const timer = setTimeout(() => {
|
|
1425
1611
|
cleanup();
|
|
1426
1612
|
reject(new Error(`Timeout (${timeout}ms) waiting for response`));
|
|
1427
1613
|
}, timeout);
|
|
1428
1614
|
const onMsg = (text) => {
|
|
1615
|
+
gotMessage = true;
|
|
1429
1616
|
cleanup();
|
|
1430
1617
|
resolve(text);
|
|
1431
1618
|
};
|
|
1619
|
+
const onIdle = () => {
|
|
1620
|
+
if (!gotMessage) {
|
|
1621
|
+
cleanup();
|
|
1622
|
+
resolve("");
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1432
1625
|
const onExit = (code) => {
|
|
1433
1626
|
cleanup();
|
|
1434
1627
|
reject(new Error(`Agent exited (code=${code}) before responding`));
|
|
@@ -1436,9 +1629,11 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
|
|
|
1436
1629
|
const cleanup = () => {
|
|
1437
1630
|
clearTimeout(timer);
|
|
1438
1631
|
this.removeListener("message", onMsg);
|
|
1632
|
+
this.removeListener("idle", onIdle);
|
|
1439
1633
|
this.removeListener("exit", onExit);
|
|
1440
1634
|
};
|
|
1441
1635
|
this.on("message", onMsg);
|
|
1636
|
+
this.on("idle", onIdle);
|
|
1442
1637
|
this.on("exit", onExit);
|
|
1443
1638
|
});
|
|
1444
1639
|
}
|
|
@@ -1472,6 +1667,9 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
|
|
|
1472
1667
|
const onIdle = (name, _details) => {
|
|
1473
1668
|
if (name === agentName) this.emit("idle");
|
|
1474
1669
|
};
|
|
1670
|
+
const onStatusLine = (name, data) => {
|
|
1671
|
+
if (name === agentName) this.emit("statusline", data);
|
|
1672
|
+
};
|
|
1475
1673
|
const onPermission = (name, parsed) => {
|
|
1476
1674
|
if (name !== agentName) return;
|
|
1477
1675
|
let handled = false;
|
|
@@ -1520,6 +1718,7 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
|
|
|
1520
1718
|
};
|
|
1521
1719
|
this.controller.on("message", onMessage);
|
|
1522
1720
|
this.controller.on("idle", onIdle);
|
|
1721
|
+
this.controller.on("agent:statusline", onStatusLine);
|
|
1523
1722
|
this.controller.on("permission:request", onPermission);
|
|
1524
1723
|
this.controller.on("plan:approval_request", onPlan);
|
|
1525
1724
|
this.controller.on("agent:exited", onExit);
|
|
@@ -1527,6 +1726,7 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
|
|
|
1527
1726
|
this.boundListeners = [
|
|
1528
1727
|
{ event: "message", fn: onMessage },
|
|
1529
1728
|
{ event: "idle", fn: onIdle },
|
|
1729
|
+
{ event: "agent:statusline", fn: onStatusLine },
|
|
1530
1730
|
{ event: "permission:request", fn: onPermission },
|
|
1531
1731
|
{ event: "plan:approval_request", fn: onPlan },
|
|
1532
1732
|
{ event: "agent:exited", fn: onExit },
|