@bulletproof-sh/ctrl-daemon 0.0.7 → 0.0.9

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +811 -61
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -26,6 +26,7 @@ npx @bulletproof-sh/ctrl-daemon --port 3001 --host 127.0.0.1
26
26
  | `--port <number>` | `3001` | Port to listen on |
27
27
  | `--host <address>` | `0.0.0.0` | Host/address to bind to |
28
28
  | `--project-dir <path>` | — | Watch a single project; omit to watch all projects |
29
+ | `--idle-timeout <minutes>` | `15` | Agent idle timeout in minutes |
29
30
  | `--help`, `-h` | — | Print usage |
30
31
 
31
32
  Without `--project-dir`, the daemon scans `~/.claude/projects/` and watches every session it finds there.
package/dist/index.js CHANGED
@@ -4319,7 +4319,7 @@ var PROJECT_SCAN_INTERVAL_MS = 1000;
4319
4319
  var TOOL_DONE_DELAY_MS = 300;
4320
4320
  var PERMISSION_TIMER_DELAY_MS = 7000;
4321
4321
  var TEXT_IDLE_DELAY_MS = 5000;
4322
- var AGENT_IDLE_TIMEOUT_MS = 5 * 60 * 1000;
4322
+ var AGENT_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
4323
4323
  var BASH_COMMAND_DISPLAY_MAX_LENGTH = 30;
4324
4324
  var TASK_DESCRIPTION_DISPLAY_MAX_LENGTH = 40;
4325
4325
 
@@ -4660,6 +4660,186 @@ function processProgressRecord(agentId, record, agents, waitingTimers, permissio
4660
4660
  }
4661
4661
  }
4662
4662
  }
4663
+ // src/tui/constants.ts
4664
+ var KATAKANA_START = 65382;
4665
+ var KATAKANA_END = 65437;
4666
+ var KATAKANA_CHARS = [];
4667
+ for (let i = KATAKANA_START;i <= KATAKANA_END; i++) {
4668
+ KATAKANA_CHARS.push(String.fromCharCode(i));
4669
+ }
4670
+ var EXTRA_CHARS = "0123456789$+-*/%=#@&<>~^".split("");
4671
+ var RAIN_CHARS = [...KATAKANA_CHARS, ...EXTRA_CHARS];
4672
+ var TRAIL_GRADIENT = [
4673
+ 231,
4674
+ 159,
4675
+ 123,
4676
+ 49,
4677
+ 48,
4678
+ 46,
4679
+ 40,
4680
+ 34,
4681
+ 28,
4682
+ 22,
4683
+ 23,
4684
+ 29,
4685
+ 24,
4686
+ 18,
4687
+ 17
4688
+ ];
4689
+ var LAYERS = {
4690
+ back: {
4691
+ speedMin: 0.15,
4692
+ speedMax: 0.3,
4693
+ trailMin: 8,
4694
+ trailMax: 16,
4695
+ spawnRate: 0.01,
4696
+ mutationInterval: 12,
4697
+ brightnessOffset: 4
4698
+ },
4699
+ mid: {
4700
+ speedMin: 0.3,
4701
+ speedMax: 0.6,
4702
+ trailMin: 10,
4703
+ trailMax: 25,
4704
+ spawnRate: 0.02,
4705
+ mutationInterval: 8,
4706
+ brightnessOffset: 2
4707
+ },
4708
+ front: {
4709
+ speedMin: 0.6,
4710
+ speedMax: 1.2,
4711
+ trailMin: 6,
4712
+ trailMax: 14,
4713
+ spawnRate: 0.015,
4714
+ mutationInterval: 5,
4715
+ brightnessOffset: 0
4716
+ }
4717
+ };
4718
+ var LIGHTNING_CHANCE = 0.002;
4719
+ var LIGHTNING_SPEED_MIN = 2;
4720
+ var LIGHTNING_SPEED_MAX = 4;
4721
+ var LIGHTNING_TRAIL_LEN = 4;
4722
+ var TARGET_FPS = 8;
4723
+ var FRAME_INTERVAL_MS = Math.round(1000 / TARGET_FPS);
4724
+ var UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
4725
+ var PANEL_MAX_WIDTH = 80;
4726
+ var PANEL_MAX_HEIGHT = 24;
4727
+ var PANEL_MARGIN_X = 5;
4728
+ var PANEL_MARGIN_Y = 3;
4729
+ var PANEL_MIN_WIDTH = 40;
4730
+ var PANEL_MIN_HEIGHT = 12;
4731
+ var PANEL_HEADER_HEIGHT = 8;
4732
+ var LOG_RING_SIZE = 200;
4733
+ var BORDER_GLOW_SPEED = 0.05;
4734
+ var BOX = {
4735
+ topLeft: "\u2554",
4736
+ topRight: "\u2557",
4737
+ bottomLeft: "\u255A",
4738
+ bottomRight: "\u255D",
4739
+ horizontal: "\u2550",
4740
+ vertical: "\u2551",
4741
+ teeLeft: "\u2560",
4742
+ teeRight: "\u2563"
4743
+ };
4744
+ var FG256_TABLE = [];
4745
+ for (let i = 0;i < 256; i++) {
4746
+ FG256_TABLE.push(`\x1B[38;5;${i}m`);
4747
+ }
4748
+ var fg256 = (n) => FG256_TABLE[n];
4749
+ var BOLD = "\x1B[1m";
4750
+ var DIM = "\x1B[2m";
4751
+ var RESET = "\x1B[0m";
4752
+ var BRIGHT_GREEN_FG = "\x1B[92m";
4753
+ var CYAN_FG = "\x1B[36m";
4754
+ var YELLOW_FG = "\x1B[33m";
4755
+ var RED_FG = "\x1B[31m";
4756
+ var ORANGE_FG = "\x1B[38;5;208m";
4757
+
4758
+ // src/tui/logSink.ts
4759
+ var logBuffer = [];
4760
+ var tuiActive = false;
4761
+ var originalConsoleLog = console.log;
4762
+ var originalConsoleError = console.error;
4763
+ function timestamp() {
4764
+ const d = new Date;
4765
+ return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
4766
+ }
4767
+ function pushEntry(message, color) {
4768
+ logBuffer.push({ timestamp: timestamp(), message, color });
4769
+ if (logBuffer.length > LOG_RING_SIZE) {
4770
+ logBuffer.shift();
4771
+ }
4772
+ }
4773
+ function getLogEntries() {
4774
+ return logBuffer;
4775
+ }
4776
+ function daemonLog(message, color) {
4777
+ if (tuiActive) {
4778
+ pushEntry(message, color || fg256(250));
4779
+ } else {
4780
+ originalConsoleError(`[ctrl-daemon] ${message}`);
4781
+ }
4782
+ }
4783
+ function onBroadcast(msg) {
4784
+ if (!tuiActive)
4785
+ return;
4786
+ if (typeof msg !== "object" || msg === null)
4787
+ return;
4788
+ const m = msg;
4789
+ const type = m.type;
4790
+ const id = m.id;
4791
+ switch (type) {
4792
+ case "agentCreated":
4793
+ pushEntry(`Agent ${id}: session started`, BRIGHT_GREEN_FG);
4794
+ break;
4795
+ case "agentClosed":
4796
+ pushEntry(`Agent ${id}: session closed`, fg256(240));
4797
+ break;
4798
+ case "agentToolStart": {
4799
+ const toolName = m.toolName || "tool";
4800
+ pushEntry(`Agent ${id}: ${toolName}`, CYAN_FG);
4801
+ break;
4802
+ }
4803
+ case "agentToolDone": {
4804
+ const toolName = m.toolName || "tool";
4805
+ pushEntry(`Agent ${id}: ${toolName} done`, fg256(34));
4806
+ break;
4807
+ }
4808
+ case "agentStatus": {
4809
+ const status = m.status;
4810
+ if (status === "waiting") {
4811
+ pushEntry(`Agent ${id}: turn complete`, YELLOW_FG);
4812
+ } else if (status === "permission") {
4813
+ pushEntry(`Agent ${id}: waiting for permission`, ORANGE_FG);
4814
+ }
4815
+ break;
4816
+ }
4817
+ case "agentToolPermissionClear":
4818
+ break;
4819
+ default:
4820
+ pushEntry(`[broadcast] ${type}`, fg256(240));
4821
+ break;
4822
+ }
4823
+ }
4824
+ function captureConsole() {
4825
+ tuiActive = true;
4826
+ originalConsoleLog = console.log;
4827
+ originalConsoleError = console.error;
4828
+ console.log = (...args) => {
4829
+ const msg = args.map((a) => String(a)).join(" ");
4830
+ pushEntry(msg, fg256(250));
4831
+ };
4832
+ console.error = (...args) => {
4833
+ const msg = args.map((a) => String(a)).join(" ");
4834
+ pushEntry(msg, RED_FG);
4835
+ };
4836
+ }
4837
+ function restoreConsole() {
4838
+ tuiActive = false;
4839
+ console.log = originalConsoleLog;
4840
+ console.error = originalConsoleError;
4841
+ }
4842
+
4663
4843
  // src/sessionWatcher.ts
4664
4844
  function startSessionWatcher(agentId, filePath, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast) {
4665
4845
  try {
@@ -4668,7 +4848,7 @@ function startSessionWatcher(agentId, filePath, agents, fileWatchers, pollingTim
4668
4848
  });
4669
4849
  fileWatchers.set(agentId, watcher);
4670
4850
  } catch (e) {
4671
- console.log(`[ctrl-daemon] fs.watch failed for agent ${agentId}: ${e}`);
4851
+ daemonLog(`fs.watch failed for agent ${agentId}: ${e}`);
4672
4852
  }
4673
4853
  const interval = setInterval(() => {
4674
4854
  if (!agents.has(agentId)) {
@@ -4712,7 +4892,7 @@ function readNewLines(agentId, agents, waitingTimers, permissionTimers, broadcas
4712
4892
  processTranscriptLine(agentId, line, agents, waitingTimers, permissionTimers, broadcast);
4713
4893
  }
4714
4894
  } catch (e) {
4715
- console.log(`[ctrl-daemon] Read error for agent ${agentId}: ${e}`);
4895
+ daemonLog(`Read error for agent ${agentId}: ${e}`);
4716
4896
  }
4717
4897
  }
4718
4898
  function stopSessionWatcher(agentId, fileWatchers, pollingTimers, waitingTimers, permissionTimers) {
@@ -4779,7 +4959,7 @@ function collectAllJsonlFiles(projectsRoot) {
4779
4959
  }
4780
4960
  return files;
4781
4961
  }
4782
- function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast) {
4962
+ function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast, idleTimeoutMs) {
4783
4963
  const knownJsonlFiles = new Set;
4784
4964
  let nextAgentId = 1;
4785
4965
  function scan() {
@@ -4788,9 +4968,9 @@ function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTime
4788
4968
  for (const { filePath, mtimeMs } of fileInfos) {
4789
4969
  if (knownJsonlFiles.has(filePath))
4790
4970
  continue;
4791
- knownJsonlFiles.add(filePath);
4792
- if (now - mtimeMs > AGENT_IDLE_TIMEOUT_MS)
4971
+ if (now - mtimeMs > idleTimeoutMs)
4793
4972
  continue;
4973
+ knownJsonlFiles.add(filePath);
4794
4974
  const id = nextAgentId++;
4795
4975
  const agentProjectDir = path2.dirname(filePath);
4796
4976
  const agent = {
@@ -4810,18 +4990,18 @@ function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTime
4810
4990
  lastActivityAt: mtimeMs
4811
4991
  };
4812
4992
  agents.set(id, agent);
4813
- console.log(`[ctrl-daemon] Agent ${id}: watching ${path2.basename(filePath)}`);
4993
+ daemonLog(`Agent ${id}: watching ${path2.basename(filePath)}`);
4814
4994
  broadcast({ type: "agentCreated", id });
4815
4995
  startSessionWatcher(id, filePath, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast);
4816
4996
  readNewLines(id, agents, waitingTimers, permissionTimers, broadcast);
4817
4997
  }
4818
4998
  for (const [agentId, agent] of agents) {
4819
4999
  const lastActivity = agent.lastActivityAt || 0;
4820
- const idle = now - lastActivity > AGENT_IDLE_TIMEOUT_MS;
5000
+ const idle = now - lastActivity > idleTimeoutMs;
4821
5001
  const removed = !fs2.existsSync(agent.jsonlFile);
4822
5002
  if (removed || idle) {
4823
5003
  const reason = removed ? "JSONL removed" : "idle timeout";
4824
- console.log(`[ctrl-daemon] Agent ${agentId}: ${reason}, closing`);
5004
+ daemonLog(`Agent ${agentId}: ${reason}, closing`);
4825
5005
  stopSessionWatcher(agentId, fileWatchers, pollingTimers, waitingTimers, permissionTimers);
4826
5006
  agents.delete(agentId);
4827
5007
  knownJsonlFiles.delete(agent.jsonlFile);
@@ -4837,6 +5017,7 @@ function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTime
4837
5017
  }
4838
5018
 
4839
5019
  // src/server.ts
5020
+ var PORT_RETRY_LIMIT = 10;
4840
5021
  function createServer({ port, host, agents }) {
4841
5022
  const clients = new Set;
4842
5023
  function broadcast(msg) {
@@ -4847,60 +5028,595 @@ function createServer({ port, host, agents }) {
4847
5028
  } catch {}
4848
5029
  }
4849
5030
  }
4850
- const server = Bun.serve({
4851
- port,
4852
- hostname: host,
4853
- fetch(req, server2) {
4854
- const url = new URL(req.url);
4855
- if (url.pathname === "/ws") {
4856
- if (server2.upgrade(req))
4857
- return;
4858
- return new Response("WebSocket upgrade failed", { status: 400 });
4859
- }
4860
- if (url.pathname === "/health") {
4861
- return Response.json({
4862
- status: "ok",
4863
- agents: agents.size
4864
- });
4865
- }
4866
- return new Response("Not found", { status: 404 });
4867
- },
4868
- websocket: {
4869
- open(ws) {
4870
- clients.add(ws);
4871
- console.log(`[ctrl-daemon] WebSocket client connected (${clients.size} total)`);
4872
- const existingAgents = [];
4873
- for (const [, agent] of agents) {
4874
- const activeTools = [];
4875
- for (const toolId of agent.activeToolIds) {
4876
- activeTools.push({
4877
- toolId,
4878
- status: agent.activeToolStatuses.get(toolId) || ""
4879
- });
4880
- }
4881
- existingAgents.push({
4882
- id: agent.id,
4883
- isWaiting: agent.isWaiting,
4884
- permissionSent: agent.permissionSent,
4885
- activeTools
5031
+ function makeServerConfig(listenPort) {
5032
+ return {
5033
+ port: listenPort,
5034
+ hostname: host,
5035
+ fetch(req, server2) {
5036
+ const url = new URL(req.url);
5037
+ if (url.pathname === "/ws") {
5038
+ if (server2.upgrade(req))
5039
+ return;
5040
+ return new Response("WebSocket upgrade failed", { status: 400 });
5041
+ }
5042
+ if (url.pathname === "/health") {
5043
+ return Response.json({
5044
+ status: "ok",
5045
+ agents: agents.size
4886
5046
  });
4887
5047
  }
4888
- ws.send(JSON.stringify({ type: "existingAgents", agents: existingAgents }));
5048
+ return new Response("Not found", { status: 404 });
4889
5049
  },
4890
- message(_ws, _message) {},
4891
- close(ws) {
4892
- clients.delete(ws);
4893
- console.log(`[ctrl-daemon] WebSocket client disconnected (${clients.size} total)`);
5050
+ websocket: {
5051
+ open(ws) {
5052
+ clients.add(ws);
5053
+ daemonLog(`Client connected (${clients.size} total)`);
5054
+ const existingAgents = [];
5055
+ for (const [, agent] of agents) {
5056
+ const activeTools = [];
5057
+ for (const toolId of agent.activeToolIds) {
5058
+ activeTools.push({
5059
+ toolId,
5060
+ status: agent.activeToolStatuses.get(toolId) || ""
5061
+ });
5062
+ }
5063
+ existingAgents.push({
5064
+ id: agent.id,
5065
+ isWaiting: agent.isWaiting,
5066
+ permissionSent: agent.permissionSent,
5067
+ activeTools
5068
+ });
5069
+ }
5070
+ ws.send(JSON.stringify({
5071
+ type: "existingAgents",
5072
+ agents: existingAgents
5073
+ }));
5074
+ },
5075
+ message(_ws, _message) {},
5076
+ close(ws) {
5077
+ clients.delete(ws);
5078
+ daemonLog(`Client disconnected (${clients.size} total)`);
5079
+ }
5080
+ }
5081
+ };
5082
+ }
5083
+ function tryServe(startPort) {
5084
+ let boundPort = startPort;
5085
+ for (let attempt = 0;attempt < PORT_RETRY_LIMIT; attempt++) {
5086
+ try {
5087
+ return Bun.serve(makeServerConfig(boundPort));
5088
+ } catch (err) {
5089
+ const isAddrInUse = err instanceof Error && (err.message.includes("EADDRINUSE") || err.message.includes("address already in use"));
5090
+ if (!isAddrInUse || attempt === PORT_RETRY_LIMIT - 1)
5091
+ throw err;
5092
+ boundPort++;
4894
5093
  }
4895
5094
  }
4896
- });
4897
- console.log(`[ctrl-daemon] Listening on ws://${server.hostname}:${server.port}/ws`);
5095
+ throw new Error(`No available port after ${PORT_RETRY_LIMIT} attempts`);
5096
+ }
5097
+ const server = tryServe(port);
5098
+ if (server.port !== port) {
5099
+ daemonLog(`Port ${port} in use, using ${server.port} instead`);
5100
+ }
5101
+ daemonLog(`Listening on ws://${server.hostname}:${server.port}/ws`);
4898
5102
  return {
4899
5103
  broadcast,
5104
+ clientCount: () => clients.size,
5105
+ port: server.port,
4900
5106
  stop: () => server.stop()
4901
5107
  };
4902
5108
  }
4903
5109
 
5110
+ // src/tui/terminal.ts
5111
+ var ALT_SCREEN_ON = "\x1B[?1049h";
5112
+ var ALT_SCREEN_OFF = "\x1B[?1049l";
5113
+ var CURSOR_HIDE = "\x1B[?25l";
5114
+ var CURSOR_SHOW = "\x1B[?25h";
5115
+ var CLEAR_SCREEN = "\x1B[2J";
5116
+ var RESET_ATTRS = "\x1B[0m";
5117
+ var resizeCallback = null;
5118
+ function onSigwinch() {
5119
+ resizeCallback?.();
5120
+ }
5121
+ function getTerminalSize() {
5122
+ return {
5123
+ rows: process.stdout.rows || 24,
5124
+ cols: process.stdout.columns || 80
5125
+ };
5126
+ }
5127
+ function enterAltScreen() {
5128
+ process.stdout.write(ALT_SCREEN_ON + CURSOR_HIDE + CLEAR_SCREEN);
5129
+ }
5130
+ function clearScreen() {
5131
+ process.stdout.write(CLEAR_SCREEN);
5132
+ }
5133
+ function exitAltScreen() {
5134
+ process.stdout.write(RESET_ATTRS + CURSOR_SHOW + ALT_SCREEN_OFF);
5135
+ }
5136
+ function onResize(callback) {
5137
+ resizeCallback = callback;
5138
+ process.on("SIGWINCH", onSigwinch);
5139
+ }
5140
+ function offResize() {
5141
+ resizeCallback = null;
5142
+ process.removeListener("SIGWINCH", onSigwinch);
5143
+ }
5144
+ function moveTo(row, col) {
5145
+ return `\x1B[${row};${col}H`;
5146
+ }
5147
+
5148
+ // src/tui/renderer.ts
5149
+ function createFrameBuffer(rows, cols) {
5150
+ const cells = [];
5151
+ for (let r = 0;r < rows; r++) {
5152
+ const row = [];
5153
+ for (let c = 0;c < cols; c++) {
5154
+ row.push({ char: " ", fg: "", bold: false, dim: false });
5155
+ }
5156
+ cells.push(row);
5157
+ }
5158
+ return { cells, rows, cols };
5159
+ }
5160
+ function clearBuffer(buf) {
5161
+ for (let r = 0;r < buf.rows; r++) {
5162
+ const row = buf.cells[r];
5163
+ for (let c = 0;c < buf.cols; c++) {
5164
+ const cell = row[c];
5165
+ cell.char = " ";
5166
+ cell.fg = "";
5167
+ cell.bold = false;
5168
+ cell.dim = false;
5169
+ }
5170
+ }
5171
+ }
5172
+ function setCell(buf, row, col, char, fg, bold, dim) {
5173
+ if (row >= 0 && row < buf.rows && col >= 0 && col < buf.cols) {
5174
+ const cell = buf.cells[row][col];
5175
+ cell.char = char;
5176
+ cell.fg = fg;
5177
+ cell.bold = bold;
5178
+ cell.dim = dim;
5179
+ }
5180
+ }
5181
+ function flushDiff(current, previous) {
5182
+ let out = "";
5183
+ let lastFg = "\x00";
5184
+ let lastBold = false;
5185
+ let lastDim = false;
5186
+ for (let r = 0;r < current.rows; r++) {
5187
+ const curRow = current.cells[r];
5188
+ const prevRow = previous?.cells[r];
5189
+ let runStart = -1;
5190
+ for (let c = 0;c <= current.cols; c++) {
5191
+ let changed = false;
5192
+ if (c < current.cols) {
5193
+ const cur = curRow[c];
5194
+ if (prevRow) {
5195
+ const prev = prevRow[c];
5196
+ if (cur.char === " " && cur.fg === "" && prev.char === " " && prev.fg === "") {
5197
+ if (runStart !== -1) {
5198
+ out += flushRun(curRow, r, runStart, c, lastFg, lastBold, lastDim);
5199
+ const last = curRow[c - 1];
5200
+ lastFg = last.fg;
5201
+ lastBold = last.bold;
5202
+ lastDim = last.dim;
5203
+ runStart = -1;
5204
+ }
5205
+ continue;
5206
+ }
5207
+ changed = cur.char !== prev.char || cur.fg !== prev.fg || cur.bold !== prev.bold || cur.dim !== prev.dim;
5208
+ } else {
5209
+ changed = cur.char !== " " || cur.fg !== "";
5210
+ }
5211
+ }
5212
+ if (changed) {
5213
+ if (runStart === -1)
5214
+ runStart = c;
5215
+ } else if (runStart !== -1) {
5216
+ out += flushRun(curRow, r, runStart, c, lastFg, lastBold, lastDim);
5217
+ const last = curRow[c - 1];
5218
+ lastFg = last.fg;
5219
+ lastBold = last.bold;
5220
+ lastDim = last.dim;
5221
+ runStart = -1;
5222
+ }
5223
+ }
5224
+ }
5225
+ if (out.length > 0) {
5226
+ out += RESET;
5227
+ process.stdout.write(out);
5228
+ }
5229
+ }
5230
+ function flushRun(row, r, start, end, lastFg, lastBold, lastDim) {
5231
+ let s = moveTo(r + 1, start + 1);
5232
+ let fg = lastFg;
5233
+ let bold = lastBold;
5234
+ let dim = lastDim;
5235
+ for (let i = start;i < end; i++) {
5236
+ const cell = row[i];
5237
+ if (cell.fg !== fg || cell.bold !== bold || cell.dim !== dim) {
5238
+ s += RESET;
5239
+ if (cell.fg)
5240
+ s += cell.fg;
5241
+ if (cell.bold)
5242
+ s += BOLD;
5243
+ if (cell.dim)
5244
+ s += DIM;
5245
+ fg = cell.fg;
5246
+ bold = cell.bold;
5247
+ dim = cell.dim;
5248
+ }
5249
+ s += cell.char;
5250
+ }
5251
+ return s;
5252
+ }
5253
+
5254
+ // src/tui/panel.ts
5255
+ var glowPosition = 0;
5256
+ var GLOW_BRIGHT = fg256(46);
5257
+ var GLOW_MEDIUM = fg256(34);
5258
+ var GLOW_DIM = fg256(22);
5259
+ function computePanelGeometry(termRows, termCols) {
5260
+ const maxW = Math.min(PANEL_MAX_WIDTH, termCols - PANEL_MARGIN_X * 2);
5261
+ const maxH = Math.min(PANEL_MAX_HEIGHT, termRows - PANEL_MARGIN_Y * 2);
5262
+ if (maxW < PANEL_MIN_WIDTH || maxH < PANEL_MIN_HEIGHT) {
5263
+ return { x: 0, y: 0, width: 0, height: 0, logLines: 0, visible: false };
5264
+ }
5265
+ const width = maxW;
5266
+ const height = maxH;
5267
+ const x = Math.floor((termCols - width) / 2);
5268
+ const y = Math.floor((termRows - height) / 2);
5269
+ const logLines = height - PANEL_HEADER_HEIGHT - 1;
5270
+ return {
5271
+ x,
5272
+ y,
5273
+ width,
5274
+ height,
5275
+ logLines: Math.max(0, logLines),
5276
+ visible: true
5277
+ };
5278
+ }
5279
+ function borderColor(borderIdx, perimeterLen) {
5280
+ const dist = Math.abs(borderIdx - glowPosition);
5281
+ const wrappedDist = Math.min(dist, perimeterLen - dist);
5282
+ if (wrappedDist < 3)
5283
+ return GLOW_BRIGHT;
5284
+ if (wrappedDist < 6)
5285
+ return GLOW_MEDIUM;
5286
+ return GLOW_DIM;
5287
+ }
5288
+ function writeString(buf, row, col, str, fg, bold, dim) {
5289
+ for (let i = 0;i < str.length; i++) {
5290
+ setCell(buf, row, col + i, str[i], fg, bold, dim);
5291
+ }
5292
+ }
5293
+ function renderPanel(buf, panel, logs, agentCount, clientCount, version2, wsUrl) {
5294
+ if (!panel.visible)
5295
+ return;
5296
+ const { x, y, width, height } = panel;
5297
+ const innerWidth = width - 2;
5298
+ const perimeter = 2 * (width + height) - 4;
5299
+ glowPosition = (glowPosition + BORDER_GLOW_SPEED) % perimeter;
5300
+ let borderIdx = 0;
5301
+ setCell(buf, y, x, BOX.topLeft, borderColor(borderIdx++, perimeter), false, false);
5302
+ for (let c = 1;c < width - 1; c++) {
5303
+ setCell(buf, y, x + c, BOX.horizontal, borderColor(borderIdx++, perimeter), false, false);
5304
+ }
5305
+ setCell(buf, y, x + width - 1, BOX.topRight, borderColor(borderIdx++, perimeter), false, false);
5306
+ for (let r = 1;r < height - 1; r++) {
5307
+ setCell(buf, y + r, x + width - 1, BOX.vertical, borderColor(borderIdx++, perimeter), false, false);
5308
+ }
5309
+ setCell(buf, y + height - 1, x + width - 1, BOX.bottomRight, borderColor(borderIdx++, perimeter), false, false);
5310
+ for (let c = width - 2;c > 0; c--) {
5311
+ setCell(buf, y + height - 1, x + c, BOX.horizontal, borderColor(borderIdx++, perimeter), false, false);
5312
+ }
5313
+ setCell(buf, y + height - 1, x, BOX.bottomLeft, borderColor(borderIdx++, perimeter), false, false);
5314
+ for (let r = height - 2;r > 0; r--) {
5315
+ setCell(buf, y + r, x, BOX.vertical, borderColor(borderIdx++, perimeter), false, false);
5316
+ }
5317
+ for (let r = 1;r < height - 1; r++) {
5318
+ for (let c = 1;c < width - 1; c++) {
5319
+ setCell(buf, y + r, x + c, " ", "", false, false);
5320
+ }
5321
+ }
5322
+ const logoStartRow = y + 1;
5323
+ for (let i = 0;i < LOGO_LINES.length && i < height - 2; i++) {
5324
+ const line = LOGO_LINES[i];
5325
+ const maxChars = Math.min(line.length, innerWidth - 1);
5326
+ for (let c = 0;c < maxChars; c++) {
5327
+ setCell(buf, logoStartRow + i, x + 2 + c, line[c], BRIGHT_GREEN_FG, true, false);
5328
+ }
5329
+ }
5330
+ if (LOGO_LINES.length >= 3) {
5331
+ const infoStr = `${version2 ? `v${version2}` : ""} \xB7 ${wsUrl}`;
5332
+ const maxInfoLen = innerWidth - (LOGO_LINES[2]?.length || 0) - 3;
5333
+ if (maxInfoLen > 10) {
5334
+ const trimmed = infoStr.slice(0, maxInfoLen);
5335
+ const infoCol = x + width - 2 - trimmed.length;
5336
+ writeString(buf, logoStartRow + 2, infoCol, trimmed, CYAN_FG, false, true);
5337
+ }
5338
+ }
5339
+ if (LOGO_LINES.length >= 5) {
5340
+ const countStr = `${agentCount} agent${agentCount !== 1 ? "s" : ""} \xB7 ${clientCount} client${clientCount !== 1 ? "s" : ""}`;
5341
+ const statusStr = `\u2713 ${countStr}`;
5342
+ const maxCountLen = innerWidth - (LOGO_LINES[4]?.length || 0) - 3;
5343
+ if (maxCountLen > 10) {
5344
+ const trimmed = statusStr.slice(0, maxCountLen);
5345
+ const countCol = x + width - 2 - trimmed.length;
5346
+ writeString(buf, logoStartRow + 4, countCol, trimmed, BRIGHT_GREEN_FG, false, false);
5347
+ }
5348
+ }
5349
+ const sepRow = y + LOGO_LINES.length + 1;
5350
+ if (sepRow < y + height - 1) {
5351
+ const sepBorderIdx = Math.floor(glowPosition) % perimeter;
5352
+ setCell(buf, sepRow, x, BOX.teeLeft, borderColor(sepBorderIdx, perimeter), false, false);
5353
+ for (let c = 1;c < width - 1; c++) {
5354
+ setCell(buf, sepRow, x + c, BOX.horizontal, borderColor((sepBorderIdx + c) % perimeter, perimeter), false, false);
5355
+ }
5356
+ setCell(buf, sepRow, x + width - 1, BOX.teeRight, borderColor((sepBorderIdx + width - 1) % perimeter, perimeter), false, false);
5357
+ }
5358
+ const logStartRow = sepRow + 1;
5359
+ const visibleLogs = logs.slice(-panel.logLines);
5360
+ for (let i = 0;i < visibleLogs.length; i++) {
5361
+ const row = logStartRow + i;
5362
+ if (row >= y + height - 1)
5363
+ break;
5364
+ const entry = visibleLogs[i];
5365
+ writeString(buf, row, x + 2, entry.timestamp, CYAN_FG, false, true);
5366
+ const msgCol = x + 2 + entry.timestamp.length + 1;
5367
+ const maxMsgLen = innerWidth - entry.timestamp.length - 3;
5368
+ const msg = entry.message.slice(0, Math.max(0, maxMsgLen));
5369
+ writeString(buf, row, msgCol, msg, entry.color, false, false);
5370
+ }
5371
+ }
5372
+
5373
+ // src/tui/rain.ts
5374
+ var ACTIVE_LAYERS = ["mid", "front"];
5375
+ var MAX_DROPS_PER_COLUMN = 3;
5376
+ var RAIN_CHAR_COUNT = RAIN_CHARS.length;
5377
+ function randomChar() {
5378
+ return RAIN_CHARS[Math.random() * RAIN_CHAR_COUNT | 0];
5379
+ }
5380
+ function createRainLayers(cols) {
5381
+ return ACTIVE_LAYERS.map((name) => {
5382
+ const columns = [];
5383
+ for (let c = 0;c < cols; c++) {
5384
+ columns.push({ drops: [] });
5385
+ }
5386
+ return { name, columns };
5387
+ });
5388
+ }
5389
+ function resizeRainLayers(layers, cols) {
5390
+ for (const layer of layers) {
5391
+ while (layer.columns.length < cols) {
5392
+ layer.columns.push({ drops: [] });
5393
+ }
5394
+ if (layer.columns.length > cols) {
5395
+ layer.columns.length = cols;
5396
+ }
5397
+ }
5398
+ }
5399
+ function spawnDrop(layerName, rows) {
5400
+ const cfg = LAYERS[layerName];
5401
+ const trailLen = cfg.trailMin + (Math.random() * (cfg.trailMax - cfg.trailMin + 1) | 0);
5402
+ const chars = [];
5403
+ for (let i = 0;i < trailLen; i++) {
5404
+ chars.push(randomChar());
5405
+ }
5406
+ return {
5407
+ y: -(Math.random() * rows * 0.3 | 0),
5408
+ speed: cfg.speedMin + Math.random() * (cfg.speedMax - cfg.speedMin),
5409
+ trailLen,
5410
+ chars,
5411
+ frameCount: 0,
5412
+ isLightning: false
5413
+ };
5414
+ }
5415
+ function spawnLightning() {
5416
+ const chars = [];
5417
+ for (let i = 0;i < LIGHTNING_TRAIL_LEN; i++) {
5418
+ chars.push(randomChar());
5419
+ }
5420
+ return {
5421
+ y: -(Math.random() * 3 | 0),
5422
+ speed: LIGHTNING_SPEED_MIN + Math.random() * (LIGHTNING_SPEED_MAX - LIGHTNING_SPEED_MIN),
5423
+ trailLen: LIGHTNING_TRAIL_LEN,
5424
+ chars,
5425
+ frameCount: 0,
5426
+ isLightning: true
5427
+ };
5428
+ }
5429
+ function tickRain(layers, rows) {
5430
+ for (const layer of layers) {
5431
+ const cfg = LAYERS[layer.name];
5432
+ const isFront = layer.name === "front";
5433
+ for (let col = 0;col < layer.columns.length; col++) {
5434
+ const column = layer.columns[col];
5435
+ const drops = column.drops;
5436
+ if (drops.length < MAX_DROPS_PER_COLUMN && Math.random() < cfg.spawnRate) {
5437
+ drops.push(spawnDrop(layer.name, rows));
5438
+ }
5439
+ if (isFront && drops.length < MAX_DROPS_PER_COLUMN && Math.random() < LIGHTNING_CHANCE) {
5440
+ drops.push(spawnLightning());
5441
+ }
5442
+ let write = 0;
5443
+ for (let d = 0;d < drops.length; d++) {
5444
+ const drop = drops[d];
5445
+ drop.y += drop.speed;
5446
+ drop.frameCount++;
5447
+ if (drop.y - drop.trailLen > rows)
5448
+ continue;
5449
+ const mutInterval = drop.isLightning ? 3 : cfg.mutationInterval;
5450
+ if (drop.frameCount % mutInterval === 0 && drop.chars.length > 1) {
5451
+ const idx = 1 + (Math.random() * (drop.chars.length - 1) | 0);
5452
+ drop.chars[idx] = randomChar();
5453
+ }
5454
+ drop.chars[0] = randomChar();
5455
+ drops[write++] = drop;
5456
+ }
5457
+ drops.length = write;
5458
+ }
5459
+ }
5460
+ }
5461
+ var GRADIENT_STRINGS = [];
5462
+ for (let offset = 0;offset <= 4; offset++) {
5463
+ const arr = [];
5464
+ for (let g = 0;g < TRAIL_GRADIENT.length; g++) {
5465
+ const idx = Math.min(TRAIL_GRADIENT.length - 1, Math.max(0, g + offset));
5466
+ arr.push(fg256(TRAIL_GRADIENT[idx]));
5467
+ }
5468
+ GRADIENT_STRINGS.push(arr);
5469
+ }
5470
+ var LIGHTNING_HEAD = fg256(231);
5471
+ var LIGHTNING_NEAR = fg256(159);
5472
+ var LIGHTNING_TAIL = fg256(49);
5473
+ function renderRain(layers, buf, panel) {
5474
+ const bufRows = buf.rows;
5475
+ const bufCols = buf.cols;
5476
+ const panelVisible = panel?.visible ?? false;
5477
+ const panelX = panel?.x ?? 0;
5478
+ const panelY = panel?.y ?? 0;
5479
+ const panelX2 = panelX + (panel?.width ?? 0);
5480
+ const panelY2 = panelY + (panel?.height ?? 0);
5481
+ const gradLen = TRAIL_GRADIENT.length - 1;
5482
+ for (const layer of layers) {
5483
+ const brightnessOffset = LAYERS[layer.name].brightnessOffset;
5484
+ const gradStrs = GRADIENT_STRINGS[brightnessOffset];
5485
+ const colCount = Math.min(layer.columns.length, bufCols);
5486
+ for (let col = 0;col < colCount; col++) {
5487
+ if (panelVisible && col >= panelX && col < panelX2) {
5488
+ const drops = layer.columns[col].drops;
5489
+ for (let d = 0;d < drops.length; d++) {
5490
+ const drop = drops[d];
5491
+ const headRow = drop.y | 0;
5492
+ const trailLen = drop.trailLen;
5493
+ for (let i = 0;i < trailLen; i++) {
5494
+ const row = headRow - i;
5495
+ if (row < 0 || row >= bufRows)
5496
+ continue;
5497
+ if (row >= panelY && row < panelY2)
5498
+ continue;
5499
+ let color;
5500
+ if (drop.isLightning) {
5501
+ color = i === 0 ? LIGHTNING_HEAD : i === 1 ? LIGHTNING_NEAR : LIGHTNING_TAIL;
5502
+ } else {
5503
+ const gradIdx = trailLen > 1 ? i * gradLen / (trailLen - 1) | 0 : 0;
5504
+ color = gradStrs[Math.min(gradIdx, gradLen)];
5505
+ }
5506
+ const bold = i === 0 || drop.isLightning;
5507
+ const dim = !drop.isLightning && i > trailLen * 0.7;
5508
+ setCell(buf, row, col, drop.chars[i], color, bold, dim);
5509
+ }
5510
+ }
5511
+ } else {
5512
+ const drops = layer.columns[col].drops;
5513
+ for (let d = 0;d < drops.length; d++) {
5514
+ const drop = drops[d];
5515
+ const headRow = drop.y | 0;
5516
+ const trailLen = drop.trailLen;
5517
+ for (let i = 0;i < trailLen; i++) {
5518
+ const row = headRow - i;
5519
+ if (row < 0 || row >= bufRows)
5520
+ continue;
5521
+ let color;
5522
+ if (drop.isLightning) {
5523
+ color = i === 0 ? LIGHTNING_HEAD : i === 1 ? LIGHTNING_NEAR : LIGHTNING_TAIL;
5524
+ } else {
5525
+ const gradIdx = trailLen > 1 ? i * gradLen / (trailLen - 1) | 0 : 0;
5526
+ color = gradStrs[Math.min(gradIdx, gradLen)];
5527
+ }
5528
+ const bold = i === 0 || drop.isLightning;
5529
+ const dim = !drop.isLightning && i > trailLen * 0.7;
5530
+ setCell(buf, row, col, drop.chars[i], color, bold, dim);
5531
+ }
5532
+ }
5533
+ }
5534
+ }
5535
+ }
5536
+ }
5537
+
5538
+ // src/tui/index.ts
5539
+ var running = false;
5540
+ var renderTimer = null;
5541
+ var bufA = null;
5542
+ var bufB = null;
5543
+ var currentIsA = true;
5544
+ var hasPrevious = false;
5545
+ var rainLayers = [];
5546
+ var panel = {
5547
+ x: 0,
5548
+ y: 0,
5549
+ width: 0,
5550
+ height: 0,
5551
+ logLines: 0,
5552
+ visible: false
5553
+ };
5554
+ var tuiOptions = null;
5555
+ function handleResize() {
5556
+ const { rows, cols } = getTerminalSize();
5557
+ clearScreen();
5558
+ bufA = createFrameBuffer(rows, cols);
5559
+ bufB = createFrameBuffer(rows, cols);
5560
+ currentIsA = true;
5561
+ hasPrevious = false;
5562
+ resizeRainLayers(rainLayers, cols);
5563
+ panel = computePanelGeometry(rows, cols);
5564
+ }
5565
+ function scheduleFrame() {
5566
+ if (!running)
5567
+ return;
5568
+ renderTimer = setTimeout(renderFrame, FRAME_INTERVAL_MS);
5569
+ }
5570
+ function renderFrame() {
5571
+ if (!running || !bufA || !bufB || !tuiOptions)
5572
+ return;
5573
+ const current = currentIsA ? bufA : bufB;
5574
+ const previous = currentIsA ? bufB : bufA;
5575
+ tickRain(rainLayers, current.rows);
5576
+ clearBuffer(current);
5577
+ renderRain(rainLayers, current, panel);
5578
+ const wsUrl = `ws://${tuiOptions.host}:${tuiOptions.port}`;
5579
+ renderPanel(current, panel, getLogEntries(), tuiOptions.agentCount(), tuiOptions.clientCount(), tuiOptions.version, wsUrl);
5580
+ flushDiff(current, hasPrevious ? previous : null);
5581
+ currentIsA = !currentIsA;
5582
+ hasPrevious = true;
5583
+ scheduleFrame();
5584
+ }
5585
+ function startTui(options) {
5586
+ if (running)
5587
+ return;
5588
+ running = true;
5589
+ tuiOptions = options;
5590
+ enterAltScreen();
5591
+ const { rows, cols } = getTerminalSize();
5592
+ bufA = createFrameBuffer(rows, cols);
5593
+ bufB = createFrameBuffer(rows, cols);
5594
+ currentIsA = true;
5595
+ hasPrevious = false;
5596
+ rainLayers = createRainLayers(cols);
5597
+ panel = computePanelGeometry(rows, cols);
5598
+ captureConsole();
5599
+ onResize(handleResize);
5600
+ scheduleFrame();
5601
+ daemonLog("TUI started");
5602
+ }
5603
+ function stopTui() {
5604
+ if (!running)
5605
+ return;
5606
+ running = false;
5607
+ if (renderTimer) {
5608
+ clearTimeout(renderTimer);
5609
+ renderTimer = null;
5610
+ }
5611
+ offResize();
5612
+ restoreConsole();
5613
+ exitAltScreen();
5614
+ bufA = null;
5615
+ bufB = null;
5616
+ hasPrevious = false;
5617
+ tuiOptions = null;
5618
+ }
5619
+
4904
5620
  // src/updater.ts
4905
5621
  var PACKAGE_NAME = "@bulletproof-sh/ctrl-daemon";
4906
5622
  var NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
@@ -4965,10 +5681,12 @@ function printUsage() {
4965
5681
  console.log(`Usage: ctrl-daemon [options]
4966
5682
 
4967
5683
  Options:
4968
- --port <number> Port to listen on (default: 3001)
4969
- --host <address> Host/address to bind to (default: 0.0.0.0)
4970
- --project-dir <path> Watch a single project directory
4971
- --help, -h Show this help message
5684
+ --port <number> Port to listen on (default: 3001)
5685
+ --host <address> Host/address to bind to (default: 0.0.0.0)
5686
+ --project-dir <path> Watch a single project directory
5687
+ --idle-timeout <minutes> Agent idle timeout in minutes (default: 15)
5688
+ --no-tui Disable Matrix rain TUI
5689
+ --help, -h Show this help message
4972
5690
 
4973
5691
  Without --project-dir, watches ALL projects in ~/.claude/projects/.
4974
5692
  With --project-dir, watches only that specific project.`);
@@ -4978,6 +5696,8 @@ function parseArgs() {
4978
5696
  let projectDir;
4979
5697
  let port = 3001;
4980
5698
  let host = "0.0.0.0";
5699
+ let idleTimeoutMs = 15 * 60 * 1000;
5700
+ let noTui = false;
4981
5701
  for (let i = 0;i < args.length; i++) {
4982
5702
  if (args[i] === "--help" || args[i] === "-h") {
4983
5703
  printUsage();
@@ -4988,9 +5708,13 @@ function parseArgs() {
4988
5708
  port = Number.parseInt(args[++i], 10);
4989
5709
  } else if (args[i] === "--host" && args[i + 1]) {
4990
5710
  host = args[++i];
5711
+ } else if (args[i] === "--idle-timeout" && args[i + 1]) {
5712
+ idleTimeoutMs = Number.parseInt(args[++i], 10) * 60 * 1000;
5713
+ } else if (args[i] === "--no-tui") {
5714
+ noTui = true;
4991
5715
  }
4992
5716
  }
4993
- return { projectDir, port, host };
5717
+ return { projectDir, port, host, idleTimeoutMs, noTui };
4994
5718
  }
4995
5719
  function resolveProjectsRoot(claudeHome) {
4996
5720
  const home = claudeHome || path3.join(process.env.HOME || "~", ".claude");
@@ -5006,19 +5730,22 @@ async function main() {
5006
5730
  ]);
5007
5731
  stopSpinner();
5008
5732
  process.on("uncaughtException", async (err) => {
5733
+ stopTui();
5009
5734
  console.error("[ctrl-daemon] Uncaught exception:", err);
5010
5735
  trackException(err, { ...systemInfo, crash_type: "uncaughtException" });
5011
5736
  await shutdownAnalytics();
5012
5737
  process.exit(1);
5013
5738
  });
5014
5739
  process.on("unhandledRejection", async (reason) => {
5740
+ stopTui();
5015
5741
  const err = reason instanceof Error ? reason : new Error(String(reason));
5016
5742
  console.error("[ctrl-daemon] Unhandled rejection:", err);
5017
5743
  trackException(err, { ...systemInfo, crash_type: "unhandledRejection" });
5018
5744
  await shutdownAnalytics();
5019
5745
  process.exit(1);
5020
5746
  });
5021
- const { projectDir, port, host } = parseArgs();
5747
+ const { projectDir, port, host, idleTimeoutMs, noTui } = parseArgs();
5748
+ const useTui = !noTui && process.stdout.isTTY;
5022
5749
  const claudeHome = process.env.CLAUDE_HOME;
5023
5750
  let scanDirs;
5024
5751
  if (projectDir) {
@@ -5034,18 +5761,41 @@ async function main() {
5034
5761
  const waitingTimers = new Map;
5035
5762
  const permissionTimers = new Map;
5036
5763
  const server = createServer({ port, host, agents });
5037
- printReady(port, version2, updateMsg);
5764
+ const boundPort = server.port;
5765
+ printReady(boundPort, version2, updateMsg);
5766
+ const rawBroadcast = server.broadcast;
5767
+ const broadcast = (msg) => {
5768
+ rawBroadcast(msg);
5769
+ onBroadcast(msg);
5770
+ };
5771
+ if (useTui) {
5772
+ startTui({
5773
+ version: version2,
5774
+ port: boundPort,
5775
+ host,
5776
+ agentCount: () => agents.size,
5777
+ clientCount: () => server.clientCount()
5778
+ });
5779
+ }
5038
5780
  trackEvent("daemon_started", {
5039
5781
  version: version2 ?? "unknown",
5040
- port,
5782
+ port: boundPort,
5041
5783
  host,
5042
5784
  mode: projectDir ? "single" : "all",
5043
5785
  ...systemInfo,
5044
5786
  ...analyticsConfig
5045
5787
  });
5046
5788
  const scanAll = !projectDir;
5047
- const scanner = startProjectScanner(scanDirs[0], scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, server.broadcast);
5789
+ const scanner = startProjectScanner(scanDirs[0], scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast, idleTimeoutMs);
5790
+ const updateCheckTimer = setInterval(async () => {
5791
+ const msg = await checkForUpdate();
5792
+ if (msg) {
5793
+ daemonLog(`Update available! ${msg.replace(/\n\s*/g, " ")}`, "\x1B[33m");
5794
+ }
5795
+ }, UPDATE_CHECK_INTERVAL_MS);
5048
5796
  async function shutdown() {
5797
+ stopTui();
5798
+ clearInterval(updateCheckTimer);
5049
5799
  console.log(`
5050
5800
  [ctrl-daemon] Shutting down...`);
5051
5801
  scanner.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bulletproof-sh/ctrl-daemon",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "WebSocket daemon for ctrl — watches Claude Code sessions and broadcasts agent state",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",