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/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { L as Logger, I as InboxMessage, S as StructuredMessage, a as LogLevel } from './claude-CSXlMCvP.js';
2
- export { A as Agent, b as AgentEvents, c as AgentHandle, d as AgentType, e as AskOptions, C as ClaudeCodeController, f as ClaudeOptions, g as ControllerEvents, h as ControllerOptions, i as IdleNotificationMessage, P as PermissionMode, j as PermissionPreset, k as PermissionRequestInfo, l as PermissionRequestMessage, m as PermissionResponseMessage, n as PlainTextMessage, o as PlanApprovalRequestMessage, p as PlanApprovalResponseMessage, q as PlanRequestInfo, R as ReceiveOptions, r as SandboxPermissionRequestMessage, s as SandboxPermissionResponseMessage, t as Session, u as SessionAgentOptions, v as SessionOptions, w as ShutdownApprovedMessage, x as ShutdownRequestMessage, y as SpawnAgentOptions, T as TaskAssignmentMessage, z as TaskCompletedMessage, B as TaskFile, D as TaskManager, E as TaskStatus, F as TeamConfig, G as TeamManager, H as TeamMember, J as claude } from './claude-CSXlMCvP.js';
1
+ import { L as Logger, I as InboxMessage, S as StructuredMessage, a as StatusLineData, b as LogLevel } from './claude-B7-oBjuE.js';
2
+ export { A as Agent, c as AgentEvents, d as AgentHandle, e as AgentType, f as AskOptions, C as ClaudeCodeController, g as ClaudeOptions, h as ControllerEvents, i as ControllerOptions, j as IdleNotificationMessage, P as PermissionMode, k as PermissionPreset, l as PermissionRequestInfo, m as PermissionRequestMessage, n as PermissionResponseMessage, o as PlainTextMessage, p as PlanApprovalRequestMessage, q as PlanApprovalResponseMessage, r as PlanRequestInfo, R as ReceiveOptions, s as SandboxPermissionRequestMessage, t as SandboxPermissionResponseMessage, u as Session, v as SessionAgentOptions, w as SessionOptions, x as ShutdownApprovedMessage, y as ShutdownRequestMessage, z as SpawnAgentOptions, T as TaskAssignmentMessage, B as TaskCompletedMessage, D as TaskFile, E as TaskManager, F as TaskStatus, G as TeamConfig, H as TeamManager, J as TeamMember, K as claude } from './claude-B7-oBjuE.js';
3
3
  import { ChildProcess } from 'node:child_process';
4
- import 'node:events';
4
+ import { EventEmitter } from 'node:events';
5
5
 
6
6
  interface SpawnOptions {
7
7
  teamName: string;
@@ -117,6 +117,62 @@ declare function readUnread(teamName: string, agentName: string): Promise<InboxM
117
117
  */
118
118
  declare function parseMessage(msg: InboxMessage): StructuredMessage;
119
119
 
120
+ /**
121
+ * Directory where statusLine JSON logs are written per agent.
122
+ */
123
+ declare function statusLineDir(teamName: string): string;
124
+ declare function statusLineLogPath(teamName: string, agentName: string): string;
125
+ interface StatusLineEvent {
126
+ agentName: string;
127
+ data: StatusLineData;
128
+ timestamp: string;
129
+ }
130
+ interface StatusLineCaptureEvents {
131
+ update: [event: StatusLineEvent];
132
+ error: [error: Error];
133
+ }
134
+ /**
135
+ * Generates the statusLine capture script.
136
+ * This script receives JSON from Claude Code via stdin and appends it to a log file.
137
+ * It also outputs a minimal display for the TUI statusline bar.
138
+ */
139
+ declare function buildStatusLineCommand(logFilePath: string): string;
140
+ /**
141
+ * Generates the settings.local.json content with statusLine configuration.
142
+ */
143
+ declare function buildStatusLineSettings(logFilePath: string): Record<string, unknown>;
144
+ /**
145
+ * Watches statusLine log files for a team and emits parsed events.
146
+ */
147
+ declare class StatusLineWatcher extends EventEmitter<StatusLineCaptureEvents> {
148
+ private teamName;
149
+ private log;
150
+ private watchers;
151
+ private fileOffsets;
152
+ private stopped;
153
+ constructor(teamName: string, logger: Logger);
154
+ /**
155
+ * Ensure the statusline directory exists.
156
+ */
157
+ ensureDir(): void;
158
+ /**
159
+ * Start watching a specific agent's statusLine log file.
160
+ */
161
+ watchAgent(agentName: string): void;
162
+ /**
163
+ * Stop watching a specific agent.
164
+ */
165
+ unwatchAgent(agentName: string): void;
166
+ /**
167
+ * Stop all watchers.
168
+ */
169
+ stop(): void;
170
+ /**
171
+ * Read new lines appended to the log file since last read.
172
+ */
173
+ private readNewLines;
174
+ }
175
+
120
176
  declare function teamsDir(): string;
121
177
  declare function teamDir(teamName: string): string;
122
178
  declare function teamConfigPath(teamName: string): string;
@@ -129,4 +185,4 @@ declare function taskPath(teamName: string, taskId: string): string;
129
185
  declare function createLogger(level?: LogLevel): Logger;
130
186
  declare const silentLogger: Logger;
131
187
 
132
- export { InboxMessage, InboxPoller, LogLevel, Logger, ProcessManager, StructuredMessage, createLogger, inboxPath, inboxesDir, parseMessage, readInbox, readUnread, silentLogger, taskPath, tasksBaseDir, tasksDir, teamConfigPath, teamDir, teamsDir, writeInbox };
188
+ export { InboxMessage, InboxPoller, LogLevel, Logger, ProcessManager, type StatusLineCaptureEvents, StatusLineData, type StatusLineEvent, StatusLineWatcher, StructuredMessage, buildStatusLineCommand, buildStatusLineSettings, createLogger, inboxPath, inboxesDir, parseMessage, readInbox, readUnread, silentLogger, statusLineDir, statusLineLogPath, taskPath, tasksBaseDir, tasksDir, teamConfigPath, teamDir, teamsDir, writeInbox };
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
1
  // src/claude.ts
2
- import { EventEmitter as EventEmitter2 } from "events";
2
+ import { EventEmitter as EventEmitter3 } from "events";
3
3
  import { randomUUID as randomUUID3 } from "crypto";
4
4
 
5
5
  // src/controller.ts
6
- import { EventEmitter } from "events";
6
+ import { EventEmitter as EventEmitter2 } from "events";
7
7
  import { execSync as execSync2 } from "child_process";
8
8
  import { randomUUID as randomUUID2 } from "crypto";
9
+ import { mkdirSync as mkdirSync2, existsSync as existsSync5, writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
10
+ import { join as join3 } from "path";
9
11
 
10
12
  // src/team-manager.ts
11
13
  import { readFile, writeFile, mkdir, rm } from "fs/promises";
@@ -338,6 +340,8 @@ if pid == 0:
338
340
  else:
339
341
  signal.signal(signal.SIGTERM, lambda *a: (os.kill(pid, signal.SIGTERM), sys.exit(0)))
340
342
  signal.signal(signal.SIGINT, lambda *a: (os.kill(pid, signal.SIGTERM), sys.exit(0)))
343
+ buf = b""
344
+ trust_sent = False
341
345
  try:
342
346
  while True:
343
347
  r, _, _ = select.select([fd, 0], [], [], 1.0)
@@ -347,6 +351,12 @@ else:
347
351
  if not data:
348
352
  break
349
353
  os.write(1, data)
354
+ if not trust_sent:
355
+ buf += data
356
+ if b"Yes" in buf and b"trust" in buf:
357
+ os.write(fd, b"\\r")
358
+ trust_sent = True
359
+ buf = b""
350
360
  except OSError:
351
361
  break
352
362
  if 0 in r:
@@ -702,6 +712,135 @@ var silentLogger = {
702
712
  }
703
713
  };
704
714
 
715
+ // src/statusline-capture.ts
716
+ import { EventEmitter } from "events";
717
+ import { watch, existsSync as existsSync4, mkdirSync, readFileSync, writeFileSync } from "fs";
718
+ import { join as join2 } from "path";
719
+ function statusLineDir(teamName) {
720
+ return join2(teamDir(teamName), "statusline");
721
+ }
722
+ function statusLineLogPath(teamName, agentName) {
723
+ return join2(statusLineDir(teamName), `${agentName}.jsonl`);
724
+ }
725
+ function buildStatusLineCommand(logFilePath) {
726
+ const escapedPath = logFilePath.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
727
+ return [
728
+ "python3 -c",
729
+ `'import sys,json;`,
730
+ `d=json.load(sys.stdin);`,
731
+ `open("${escapedPath}","a").write(json.dumps(d)+"\\n");`,
732
+ `print(d.get("model",{}).get("display_name",""))'`
733
+ ].join(" ");
734
+ }
735
+ function buildStatusLineSettings(logFilePath) {
736
+ return {
737
+ statusLine: {
738
+ type: "command",
739
+ command: buildStatusLineCommand(logFilePath)
740
+ }
741
+ };
742
+ }
743
+ var StatusLineWatcher = class extends EventEmitter {
744
+ teamName;
745
+ log;
746
+ watchers = /* @__PURE__ */ new Map();
747
+ fileOffsets = /* @__PURE__ */ new Map();
748
+ stopped = false;
749
+ constructor(teamName, logger) {
750
+ super();
751
+ this.teamName = teamName;
752
+ this.log = logger;
753
+ }
754
+ /**
755
+ * Ensure the statusline directory exists.
756
+ */
757
+ ensureDir() {
758
+ const dir = statusLineDir(this.teamName);
759
+ if (!existsSync4(dir)) {
760
+ mkdirSync(dir, { recursive: true });
761
+ }
762
+ }
763
+ /**
764
+ * Start watching a specific agent's statusLine log file.
765
+ */
766
+ watchAgent(agentName) {
767
+ if (this.stopped) return;
768
+ const filePath = statusLineLogPath(this.teamName, agentName);
769
+ if (!existsSync4(filePath)) {
770
+ writeFileSync(filePath, "");
771
+ }
772
+ try {
773
+ const stats = readFileSync(filePath);
774
+ this.fileOffsets.set(filePath, stats.length);
775
+ } catch {
776
+ this.fileOffsets.set(filePath, 0);
777
+ }
778
+ try {
779
+ const watcher = watch(filePath, (eventType) => {
780
+ if (eventType === "change") {
781
+ this.readNewLines(agentName, filePath);
782
+ }
783
+ });
784
+ this.watchers.set(agentName, watcher);
785
+ this.log.debug(`Watching statusLine for agent "${agentName}" at ${filePath}`);
786
+ } catch (err) {
787
+ this.log.error(`Failed to watch statusLine for "${agentName}": ${err}`);
788
+ }
789
+ }
790
+ /**
791
+ * Stop watching a specific agent.
792
+ */
793
+ unwatchAgent(agentName) {
794
+ const watcher = this.watchers.get(agentName);
795
+ if (watcher) {
796
+ watcher.close();
797
+ this.watchers.delete(agentName);
798
+ }
799
+ const filePath = statusLineLogPath(this.teamName, agentName);
800
+ this.fileOffsets.delete(filePath);
801
+ }
802
+ /**
803
+ * Stop all watchers.
804
+ */
805
+ stop() {
806
+ this.stopped = true;
807
+ for (const [, watcher] of this.watchers) {
808
+ watcher.close();
809
+ }
810
+ this.watchers.clear();
811
+ this.fileOffsets.clear();
812
+ }
813
+ /**
814
+ * Read new lines appended to the log file since last read.
815
+ */
816
+ readNewLines(agentName, filePath) {
817
+ try {
818
+ const content = readFileSync(filePath, "utf-8");
819
+ const offset = this.fileOffsets.get(filePath) ?? 0;
820
+ const newContent = content.slice(offset);
821
+ this.fileOffsets.set(filePath, content.length);
822
+ if (!newContent.trim()) return;
823
+ const lines = newContent.trim().split("\n");
824
+ for (const line of lines) {
825
+ if (!line.trim()) continue;
826
+ try {
827
+ const data = JSON.parse(line);
828
+ const event = {
829
+ agentName,
830
+ data,
831
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
832
+ };
833
+ this.emit("update", event);
834
+ } catch (parseErr) {
835
+ this.log.debug(`Failed to parse statusLine JSON: ${line}`);
836
+ }
837
+ }
838
+ } catch (err) {
839
+ this.log.debug(`Error reading statusLine file: ${err}`);
840
+ }
841
+ }
842
+ };
843
+
705
844
  // src/controller.ts
706
845
  var PROTOCOL_ONLY_TYPES = /* @__PURE__ */ new Set([
707
846
  "shutdown_approved",
@@ -721,12 +860,13 @@ var AGENT_COLORS = [
721
860
  "#FF69B4",
722
861
  "#7B68EE"
723
862
  ];
724
- var ClaudeCodeController = class extends EventEmitter {
863
+ var ClaudeCodeController = class extends EventEmitter2 {
725
864
  teamName;
726
865
  team;
727
866
  tasks;
728
867
  processes;
729
868
  poller;
869
+ statusLineWatcher;
730
870
  log;
731
871
  cwd;
732
872
  claudeBinary;
@@ -748,7 +888,11 @@ var ClaudeCodeController = class extends EventEmitter {
748
888
  "controller",
749
889
  this.log
750
890
  );
891
+ this.statusLineWatcher = new StatusLineWatcher(this.teamName, this.log);
751
892
  this.poller.onMessages((events) => this.handlePollEvents(events));
893
+ this.statusLineWatcher.on("update", (event) => {
894
+ this.emit("agent:statusline", event.agentName, event.data);
895
+ });
752
896
  }
753
897
  // ─── Lifecycle ───────────────────────────────────────────────────────
754
898
  /**
@@ -759,6 +903,7 @@ var ClaudeCodeController = class extends EventEmitter {
759
903
  if (this.initialized) return this;
760
904
  await this.team.create({ cwd: this.cwd });
761
905
  await this.tasks.init();
906
+ this.statusLineWatcher.ensureDir();
762
907
  this.poller.start();
763
908
  this.initialized = true;
764
909
  this.log.info(
@@ -799,6 +944,7 @@ var ClaudeCodeController = class extends EventEmitter {
799
944
  }
800
945
  await this.processes.killAll();
801
946
  this.poller.stop();
947
+ this.statusLineWatcher.stop();
802
948
  await this.team.destroy();
803
949
  this.initialized = false;
804
950
  this.log.info("Controller shut down");
@@ -827,6 +973,8 @@ var ClaudeCodeController = class extends EventEmitter {
827
973
  subscriptions: []
828
974
  };
829
975
  await this.team.addMember(member);
976
+ this.ensureWorkspaceTrusted(cwd, opts.name);
977
+ this.statusLineWatcher.watchAgent(opts.name);
830
978
  const env = Object.keys(this.defaultEnv).length > 0 || opts.env ? { ...this.defaultEnv, ...opts.env } : void 0;
831
979
  const proc = this.processes.spawn({
832
980
  teamName: this.teamName,
@@ -1033,6 +1181,7 @@ var ClaudeCodeController = class extends EventEmitter {
1033
1181
  * Kill a specific agent.
1034
1182
  */
1035
1183
  async killAgent(name) {
1184
+ this.statusLineWatcher.unwatchAgent(name);
1036
1185
  await this.processes.kill(name);
1037
1186
  await this.team.removeMember(name);
1038
1187
  }
@@ -1094,6 +1243,32 @@ var ClaudeCodeController = class extends EventEmitter {
1094
1243
  }
1095
1244
  }
1096
1245
  }
1246
+ /**
1247
+ * Ensure the agent's cwd has a .claude/settings.local.json so the
1248
+ * CLI skips the interactive workspace trust prompt.
1249
+ * Also injects statusLine capture configuration.
1250
+ */
1251
+ ensureWorkspaceTrusted(cwd, agentName) {
1252
+ const claudeDir = join3(cwd, ".claude");
1253
+ const settingsPath = join3(claudeDir, "settings.local.json");
1254
+ mkdirSync2(claudeDir, { recursive: true });
1255
+ let settings = {};
1256
+ if (existsSync5(settingsPath)) {
1257
+ try {
1258
+ settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
1259
+ } catch {
1260
+ settings = {};
1261
+ }
1262
+ }
1263
+ if (agentName && !settings.statusLine) {
1264
+ const logPath = statusLineLogPath(this.teamName, agentName);
1265
+ const statusLineSettings = buildStatusLineSettings(logPath);
1266
+ settings = { ...settings, ...statusLineSettings };
1267
+ this.log.debug(`Injected statusLine capture for "${agentName}"`);
1268
+ }
1269
+ writeFileSync2(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1270
+ this.log.debug(`Updated ${settingsPath}`);
1271
+ }
1097
1272
  ensureInitialized() {
1098
1273
  if (!this.initialized) {
1099
1274
  throw new Error(
@@ -1178,7 +1353,7 @@ function waitForReady(controller, agentName, timeoutMs = 15e3) {
1178
1353
  controller.on("agent:exited", onExit);
1179
1354
  });
1180
1355
  }
1181
- var Agent = class _Agent extends EventEmitter2 {
1356
+ var Agent = class _Agent extends EventEmitter3 {
1182
1357
  controller;
1183
1358
  handle;
1184
1359
  ownsController;
@@ -1271,14 +1446,22 @@ var Agent = class _Agent extends EventEmitter2 {
1271
1446
  this.ensureNotDisposed();
1272
1447
  const timeout = opts?.timeout ?? 12e4;
1273
1448
  const responsePromise = new Promise((resolve, reject) => {
1449
+ let gotMessage = false;
1274
1450
  const timer = setTimeout(() => {
1275
1451
  cleanup();
1276
1452
  reject(new Error(`Timeout (${timeout}ms) waiting for response`));
1277
1453
  }, timeout);
1278
1454
  const onMsg = (text) => {
1455
+ gotMessage = true;
1279
1456
  cleanup();
1280
1457
  resolve(text);
1281
1458
  };
1459
+ const onIdle = () => {
1460
+ if (!gotMessage) {
1461
+ cleanup();
1462
+ resolve("");
1463
+ }
1464
+ };
1282
1465
  const onExit = (code) => {
1283
1466
  cleanup();
1284
1467
  reject(new Error(`Agent exited (code=${code}) before responding`));
@@ -1286,9 +1469,11 @@ var Agent = class _Agent extends EventEmitter2 {
1286
1469
  const cleanup = () => {
1287
1470
  clearTimeout(timer);
1288
1471
  this.removeListener("message", onMsg);
1472
+ this.removeListener("idle", onIdle);
1289
1473
  this.removeListener("exit", onExit);
1290
1474
  };
1291
1475
  this.on("message", onMsg);
1476
+ this.on("idle", onIdle);
1292
1477
  this.on("exit", onExit);
1293
1478
  });
1294
1479
  const wrapped = `${question}
@@ -1307,14 +1492,22 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
1307
1492
  this.ensureNotDisposed();
1308
1493
  const timeout = opts?.timeout ?? 12e4;
1309
1494
  return new Promise((resolve, reject) => {
1495
+ let gotMessage = false;
1310
1496
  const timer = setTimeout(() => {
1311
1497
  cleanup();
1312
1498
  reject(new Error(`Timeout (${timeout}ms) waiting for response`));
1313
1499
  }, timeout);
1314
1500
  const onMsg = (text) => {
1501
+ gotMessage = true;
1315
1502
  cleanup();
1316
1503
  resolve(text);
1317
1504
  };
1505
+ const onIdle = () => {
1506
+ if (!gotMessage) {
1507
+ cleanup();
1508
+ resolve("");
1509
+ }
1510
+ };
1318
1511
  const onExit = (code) => {
1319
1512
  cleanup();
1320
1513
  reject(new Error(`Agent exited (code=${code}) before responding`));
@@ -1322,9 +1515,11 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
1322
1515
  const cleanup = () => {
1323
1516
  clearTimeout(timer);
1324
1517
  this.removeListener("message", onMsg);
1518
+ this.removeListener("idle", onIdle);
1325
1519
  this.removeListener("exit", onExit);
1326
1520
  };
1327
1521
  this.on("message", onMsg);
1522
+ this.on("idle", onIdle);
1328
1523
  this.on("exit", onExit);
1329
1524
  });
1330
1525
  }
@@ -1358,6 +1553,9 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
1358
1553
  const onIdle = (name, _details) => {
1359
1554
  if (name === agentName) this.emit("idle");
1360
1555
  };
1556
+ const onStatusLine = (name, data) => {
1557
+ if (name === agentName) this.emit("statusline", data);
1558
+ };
1361
1559
  const onPermission = (name, parsed) => {
1362
1560
  if (name !== agentName) return;
1363
1561
  let handled = false;
@@ -1406,6 +1604,7 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
1406
1604
  };
1407
1605
  this.controller.on("message", onMessage);
1408
1606
  this.controller.on("idle", onIdle);
1607
+ this.controller.on("agent:statusline", onStatusLine);
1409
1608
  this.controller.on("permission:request", onPermission);
1410
1609
  this.controller.on("plan:approval_request", onPlan);
1411
1610
  this.controller.on("agent:exited", onExit);
@@ -1413,6 +1612,7 @@ IMPORTANT: You MUST send your complete answer back using the SendMessage tool. D
1413
1612
  this.boundListeners = [
1414
1613
  { event: "message", fn: onMessage },
1415
1614
  { event: "idle", fn: onIdle },
1615
+ { event: "agent:statusline", fn: onStatusLine },
1416
1616
  { event: "permission:request", fn: onPermission },
1417
1617
  { event: "plan:approval_request", fn: onPlan },
1418
1618
  { event: "agent:exited", fn: onExit },
@@ -1558,8 +1758,11 @@ export {
1558
1758
  InboxPoller,
1559
1759
  ProcessManager,
1560
1760
  Session,
1761
+ StatusLineWatcher,
1561
1762
  TaskManager,
1562
1763
  TeamManager,
1764
+ buildStatusLineCommand,
1765
+ buildStatusLineSettings,
1563
1766
  claude,
1564
1767
  createLogger,
1565
1768
  inboxPath,
@@ -1568,6 +1771,8 @@ export {
1568
1771
  readInbox,
1569
1772
  readUnread,
1570
1773
  silentLogger,
1774
+ statusLineDir,
1775
+ statusLineLogPath,
1571
1776
  taskPath,
1572
1777
  tasksBaseDir,
1573
1778
  tasksDir,