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.
@@ -1,5 +1,5 @@
1
1
  import { Hono } from 'hono';
2
- import { C as ClaudeCodeController, j as PermissionPreset, E as TaskStatus, a as LogLevel, d as AgentType } from '../claude-CSXlMCvP.cjs';
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 {
@@ -1,5 +1,5 @@
1
1
  import { Hono } from 'hono';
2
- import { C as ClaudeCodeController, j as PermissionPreset, E as TaskStatus, a as LogLevel, d as AgentType } from '../claude-CSXlMCvP.js';
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 EventEmitter {
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 EventEmitter2 } from "events";
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 EventEmitter2 {
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 },