claude-code-controller 0.5.1 → 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,11 +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_fs4 = require("fs");
138
- var import_node_path3 = require("path");
137
+ var import_node_fs5 = require("fs");
138
+ var import_node_path4 = require("path");
139
139
 
140
140
  // src/team-manager.ts
141
141
  var import_promises = require("fs/promises");
@@ -824,6 +824,135 @@ function createLogger(level = "info") {
824
824
  };
825
825
  }
826
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
+
827
956
  // src/controller.ts
828
957
  var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
829
958
  "shutdown_approved",
@@ -843,12 +972,13 @@ var AGENT_COLORS = [
843
972
  "#FF69B4",
844
973
  "#7B68EE"
845
974
  ];
846
- var ClaudeCodeController = class extends import_node_events.EventEmitter {
975
+ var ClaudeCodeController = class extends import_node_events2.EventEmitter {
847
976
  teamName;
848
977
  team;
849
978
  tasks;
850
979
  processes;
851
980
  poller;
981
+ statusLineWatcher;
852
982
  log;
853
983
  cwd;
854
984
  claudeBinary;
@@ -870,7 +1000,11 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
870
1000
  "controller",
871
1001
  this.log
872
1002
  );
1003
+ this.statusLineWatcher = new StatusLineWatcher(this.teamName, this.log);
873
1004
  this.poller.onMessages((events) => this.handlePollEvents(events));
1005
+ this.statusLineWatcher.on("update", (event) => {
1006
+ this.emit("agent:statusline", event.agentName, event.data);
1007
+ });
874
1008
  }
875
1009
  // ─── Lifecycle ───────────────────────────────────────────────────────
876
1010
  /**
@@ -881,6 +1015,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
881
1015
  if (this.initialized) return this;
882
1016
  await this.team.create({ cwd: this.cwd });
883
1017
  await this.tasks.init();
1018
+ this.statusLineWatcher.ensureDir();
884
1019
  this.poller.start();
885
1020
  this.initialized = true;
886
1021
  this.log.info(
@@ -921,6 +1056,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
921
1056
  }
922
1057
  await this.processes.killAll();
923
1058
  this.poller.stop();
1059
+ this.statusLineWatcher.stop();
924
1060
  await this.team.destroy();
925
1061
  this.initialized = false;
926
1062
  this.log.info("Controller shut down");
@@ -949,7 +1085,8 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
949
1085
  subscriptions: []
950
1086
  };
951
1087
  await this.team.addMember(member);
952
- this.ensureWorkspaceTrusted(cwd);
1088
+ this.ensureWorkspaceTrusted(cwd, opts.name);
1089
+ this.statusLineWatcher.watchAgent(opts.name);
953
1090
  const env = Object.keys(this.defaultEnv).length > 0 || opts.env ? { ...this.defaultEnv, ...opts.env } : void 0;
954
1091
  const proc = this.processes.spawn({
955
1092
  teamName: this.teamName,
@@ -1156,6 +1293,7 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
1156
1293
  * Kill a specific agent.
1157
1294
  */
1158
1295
  async killAgent(name) {
1296
+ this.statusLineWatcher.unwatchAgent(name);
1159
1297
  await this.processes.kill(name);
1160
1298
  await this.team.removeMember(name);
1161
1299
  }
@@ -1220,15 +1358,28 @@ var ClaudeCodeController = class extends import_node_events.EventEmitter {
1220
1358
  /**
1221
1359
  * Ensure the agent's cwd has a .claude/settings.local.json so the
1222
1360
  * CLI skips the interactive workspace trust prompt.
1361
+ * Also injects statusLine capture configuration.
1223
1362
  */
1224
- ensureWorkspaceTrusted(cwd) {
1225
- const claudeDir = (0, import_node_path3.join)(cwd, ".claude");
1226
- const settingsPath = (0, import_node_path3.join)(claudeDir, "settings.local.json");
1227
- if (!(0, import_node_fs4.existsSync)(settingsPath)) {
1228
- (0, import_node_fs4.mkdirSync)(claudeDir, { recursive: true });
1229
- (0, import_node_fs4.writeFileSync)(settingsPath, "{}\n");
1230
- this.log.debug(`Created ${settingsPath} for workspace trust`);
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}"`);
1231
1380
  }
1381
+ (0, import_node_fs5.writeFileSync)(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1382
+ this.log.debug(`Updated ${settingsPath}`);
1232
1383
  }
1233
1384
  ensureInitialized() {
1234
1385
  if (!this.initialized) {
@@ -1243,7 +1394,7 @@ function sleep2(ms) {
1243
1394
  }
1244
1395
 
1245
1396
  // src/claude.ts
1246
- var import_node_events2 = require("events");
1397
+ var import_node_events3 = require("events");
1247
1398
  var import_node_crypto3 = require("crypto");
1248
1399
  Symbol.asyncDispose ??= /* @__PURE__ */ Symbol("Symbol.asyncDispose");
1249
1400
  function buildEnv(opts) {
@@ -1316,7 +1467,7 @@ function waitForReady(controller, agentName, timeoutMs = 15e3) {
1316
1467
  controller.on("agent:exited", onExit);
1317
1468
  });
1318
1469
  }
1319
- var Agent = class _Agent extends import_node_events2.EventEmitter {
1470
+ var Agent = class _Agent extends import_node_events3.EventEmitter {
1320
1471
  controller;
1321
1472
  handle;
1322
1473
  ownsController;
@@ -1516,6 +1667,9 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
1516
1667
  const onIdle = (name, _details) => {
1517
1668
  if (name === agentName) this.emit("idle");
1518
1669
  };
1670
+ const onStatusLine = (name, data) => {
1671
+ if (name === agentName) this.emit("statusline", data);
1672
+ };
1519
1673
  const onPermission = (name, parsed) => {
1520
1674
  if (name !== agentName) return;
1521
1675
  let handled = false;
@@ -1564,6 +1718,7 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
1564
1718
  };
1565
1719
  this.controller.on("message", onMessage);
1566
1720
  this.controller.on("idle", onIdle);
1721
+ this.controller.on("agent:statusline", onStatusLine);
1567
1722
  this.controller.on("permission:request", onPermission);
1568
1723
  this.controller.on("plan:approval_request", onPlan);
1569
1724
  this.controller.on("agent:exited", onExit);
@@ -1571,6 +1726,7 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
1571
1726
  this.boundListeners = [
1572
1727
  { event: "message", fn: onMessage },
1573
1728
  { event: "idle", fn: onIdle },
1729
+ { event: "agent:statusline", fn: onStatusLine },
1574
1730
  { event: "permission:request", fn: onPermission },
1575
1731
  { event: "plan:approval_request", fn: onPlan },
1576
1732
  { event: "agent:exited", fn: onExit },