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.
@@ -131,9 +131,11 @@ var ActionTracker = class {
131
131
  var import_hono = require("hono");
132
132
 
133
133
  // src/controller.ts
134
- var import_node_events = require("events");
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 import_node_events.EventEmitter {
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 import_node_events2 = require("events");
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 import_node_events2.EventEmitter {
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 },