@bulletproof-sh/ctrl-daemon 0.0.13 → 0.0.15

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 (2) hide show
  1. package/dist/index.js +104 -104
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ var __export = (target, all) => {
11
11
  };
12
12
 
13
13
  // src/index.ts
14
+ import * as fs3 from "fs";
14
15
  import * as path3 from "path";
15
16
 
16
17
  // src/analytics.ts
@@ -4315,7 +4316,7 @@ var PANEL_MARGIN_X = 5;
4315
4316
  var PANEL_MARGIN_Y = 3;
4316
4317
  var PANEL_MIN_WIDTH = 40;
4317
4318
  var PANEL_MIN_HEIGHT = 12;
4318
- var PANEL_HEADER_HEIGHT = 8;
4319
+ var PANEL_HEADER_HEIGHT = 9;
4319
4320
  var LOG_RING_SIZE = 200;
4320
4321
  var BORDER_GLOW_SPEED = 0.05;
4321
4322
  var BOX = {
@@ -4842,10 +4843,19 @@ function restoreConsole() {
4842
4843
  }
4843
4844
 
4844
4845
  // src/sessionWatcher.ts
4845
- function startSessionWatcher(agentId, filePath, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast) {
4846
+ var SHARED_BUF_SIZE = 64 * 1024;
4847
+ var sharedReadBuf = Buffer.allocUnsafe(SHARED_BUF_SIZE);
4848
+ function startSessionWatcher(agentId, filePath, agents, openFds, staleAgents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast) {
4849
+ try {
4850
+ const fd = fs.openSync(filePath, "r");
4851
+ openFds.set(agentId, fd);
4852
+ } catch (e) {
4853
+ daemonLog(`Failed to open fd for agent ${agentId}: ${e}`);
4854
+ return;
4855
+ }
4846
4856
  try {
4847
4857
  const watcher = fs.watch(filePath, () => {
4848
- readNewLines(agentId, agents, waitingTimers, permissionTimers, broadcast);
4858
+ readNewLines(agentId, agents, openFds, staleAgents, waitingTimers, permissionTimers, broadcast);
4849
4859
  });
4850
4860
  fileWatchers.set(agentId, watcher);
4851
4861
  } catch (e) {
@@ -4856,47 +4866,58 @@ function startSessionWatcher(agentId, filePath, agents, fileWatchers, pollingTim
4856
4866
  clearInterval(interval);
4857
4867
  return;
4858
4868
  }
4859
- readNewLines(agentId, agents, waitingTimers, permissionTimers, broadcast);
4869
+ readNewLines(agentId, agents, openFds, staleAgents, waitingTimers, permissionTimers, broadcast);
4860
4870
  }, FILE_WATCHER_POLL_INTERVAL_MS);
4861
4871
  pollingTimers.set(agentId, interval);
4862
4872
  }
4863
- function readNewLines(agentId, agents, waitingTimers, permissionTimers, broadcast) {
4873
+ function readNewLines(agentId, agents, openFds, staleAgents, waitingTimers, permissionTimers, broadcast) {
4864
4874
  const agent = agents.get(agentId);
4865
4875
  if (!agent)
4866
4876
  return;
4877
+ const fd = openFds.get(agentId);
4878
+ if (fd === undefined)
4879
+ return;
4867
4880
  try {
4868
- const stat = fs.statSync(agent.jsonlFile);
4881
+ const stat = fs.fstatSync(fd);
4869
4882
  if (stat.size <= agent.fileOffset)
4870
4883
  return;
4871
- const buf = Buffer.alloc(stat.size - agent.fileOffset);
4872
- const fd = fs.openSync(agent.jsonlFile, "r");
4873
- fs.readSync(fd, buf, 0, buf.length, agent.fileOffset);
4874
- fs.closeSync(fd);
4884
+ const readSize = stat.size - agent.fileOffset;
4885
+ const buf = readSize <= sharedReadBuf.length ? sharedReadBuf : Buffer.allocUnsafe(readSize);
4886
+ const bytesRead = fs.readSync(fd, buf, 0, readSize, agent.fileOffset);
4875
4887
  agent.fileOffset = stat.size;
4876
- const text = agent.lineBuffer + buf.toString("utf-8");
4888
+ const text = agent.lineBuffer + buf.toString("utf-8", 0, bytesRead);
4877
4889
  const lines = text.split(`
4878
4890
  `);
4879
4891
  agent.lineBuffer = lines.pop() || "";
4880
- const hasLines = lines.some((l) => l.trim());
4881
- if (hasLines) {
4882
- agent.lastActivityAt = Date.now();
4883
- cancelWaitingTimer(agentId, waitingTimers);
4884
- cancelPermissionTimer(agentId, permissionTimers);
4885
- if (agent.permissionSent) {
4886
- agent.permissionSent = false;
4887
- broadcast({ type: "agentToolPermissionClear", id: agentId });
4888
- }
4889
- }
4892
+ let activityUpdated = false;
4890
4893
  for (const line of lines) {
4891
4894
  if (!line.trim())
4892
4895
  continue;
4896
+ if (!activityUpdated) {
4897
+ agent.lastActivityAt = Date.now();
4898
+ cancelWaitingTimer(agentId, waitingTimers);
4899
+ cancelPermissionTimer(agentId, permissionTimers);
4900
+ if (agent.permissionSent) {
4901
+ agent.permissionSent = false;
4902
+ broadcast({ type: "agentToolPermissionClear", id: agentId });
4903
+ }
4904
+ activityUpdated = true;
4905
+ }
4893
4906
  processTranscriptLine(agentId, line, agents, waitingTimers, permissionTimers, broadcast);
4894
4907
  }
4895
4908
  } catch (e) {
4896
4909
  daemonLog(`Read error for agent ${agentId}: ${e}`);
4910
+ staleAgents.add(agentId);
4897
4911
  }
4898
4912
  }
4899
- function stopSessionWatcher(agentId, fileWatchers, pollingTimers, waitingTimers, permissionTimers) {
4913
+ function stopSessionWatcher(agentId, openFds, fileWatchers, pollingTimers, waitingTimers, permissionTimers) {
4914
+ const fd = openFds.get(agentId);
4915
+ if (fd !== undefined) {
4916
+ try {
4917
+ fs.closeSync(fd);
4918
+ } catch {}
4919
+ openFds.delete(agentId);
4920
+ }
4900
4921
  fileWatchers.get(agentId)?.close();
4901
4922
  fileWatchers.delete(agentId);
4902
4923
  const pt = pollingTimers.get(agentId);
@@ -4960,7 +4981,7 @@ function collectAllJsonlFiles(projectsRoot) {
4960
4981
  }
4961
4982
  return files;
4962
4983
  }
4963
- function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast, idleTimeoutMs) {
4984
+ function startProjectScanner(rootDir, scanAll, agents, openFds, staleAgents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast, idleTimeoutMs) {
4964
4985
  const knownJsonlFiles = new Set;
4965
4986
  let nextAgentId = 1;
4966
4987
  function scan() {
@@ -4993,17 +5014,18 @@ function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTime
4993
5014
  agents.set(id, agent);
4994
5015
  daemonLog(`Agent ${id}: watching ${path2.basename(filePath)}`);
4995
5016
  broadcast({ type: "agentCreated", id });
4996
- startSessionWatcher(id, filePath, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast);
4997
- readNewLines(id, agents, waitingTimers, permissionTimers, broadcast);
5017
+ startSessionWatcher(id, filePath, agents, openFds, staleAgents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast);
5018
+ readNewLines(id, agents, openFds, staleAgents, waitingTimers, permissionTimers, broadcast);
4998
5019
  }
4999
5020
  for (const [agentId, agent] of agents) {
5000
5021
  const lastActivity = agent.lastActivityAt || 0;
5001
5022
  const idle = now - lastActivity > idleTimeoutMs;
5002
- const removed = !fs2.existsSync(agent.jsonlFile);
5023
+ const removed = staleAgents.has(agentId);
5003
5024
  if (removed || idle) {
5004
5025
  const reason = removed ? "JSONL removed" : "idle timeout";
5005
5026
  daemonLog(`Agent ${agentId}: ${reason}, closing`);
5006
- stopSessionWatcher(agentId, fileWatchers, pollingTimers, waitingTimers, permissionTimers);
5027
+ staleAgents.delete(agentId);
5028
+ stopSessionWatcher(agentId, openFds, fileWatchers, pollingTimers, waitingTimers, permissionTimers);
5007
5029
  agents.delete(agentId);
5008
5030
  knownJsonlFiles.delete(agent.jsonlFile);
5009
5031
  broadcast({ type: "agentClosed", id: agentId });
@@ -5118,6 +5140,7 @@ var CURSOR_SHOW = "\x1B[?25h";
5118
5140
  var CLEAR_SCREEN = "\x1B[2J";
5119
5141
  var RESET_ATTRS = "\x1B[0m";
5120
5142
  var resizeCallback = null;
5143
+ var moveToTable = [];
5121
5144
  function onSigwinch() {
5122
5145
  resizeCallback?.();
5123
5146
  }
@@ -5144,8 +5167,19 @@ function offResize() {
5144
5167
  resizeCallback = null;
5145
5168
  process.removeListener("SIGWINCH", onSigwinch);
5146
5169
  }
5170
+ function buildMoveToTable(rows, cols) {
5171
+ const table = [];
5172
+ for (let r = 0;r <= rows; r++) {
5173
+ const row = [];
5174
+ for (let c = 0;c <= cols; c++) {
5175
+ row.push(`\x1B[${r};${c}H`);
5176
+ }
5177
+ table.push(row);
5178
+ }
5179
+ moveToTable = table;
5180
+ }
5147
5181
  function moveTo(row, col) {
5148
- return `\x1B[${row};${col}H`;
5182
+ return moveToTable[row]?.[col] ?? `\x1B[${row};${col}H`;
5149
5183
  }
5150
5184
 
5151
5185
  // src/tui/renderer.ts
@@ -5182,7 +5216,7 @@ function setCell(buf, row, col, char, fg, bold, dim) {
5182
5216
  }
5183
5217
  }
5184
5218
  function flushDiff(current, previous) {
5185
- let out = "";
5219
+ const parts = [];
5186
5220
  let lastFg = "\x00";
5187
5221
  let lastBold = false;
5188
5222
  let lastDim = false;
@@ -5198,7 +5232,7 @@ function flushDiff(current, previous) {
5198
5232
  const prev = prevRow[c];
5199
5233
  if (cur.char === " " && cur.fg === "" && prev.char === " " && prev.fg === "") {
5200
5234
  if (runStart !== -1) {
5201
- out += flushRun(curRow, r, runStart, c, lastFg, lastBold, lastDim);
5235
+ parts.push(flushRun(curRow, r, runStart, c, lastFg, lastBold, lastDim));
5202
5236
  const last = curRow[c - 1];
5203
5237
  lastFg = last.fg;
5204
5238
  lastBold = last.bold;
@@ -5216,7 +5250,7 @@ function flushDiff(current, previous) {
5216
5250
  if (runStart === -1)
5217
5251
  runStart = c;
5218
5252
  } else if (runStart !== -1) {
5219
- out += flushRun(curRow, r, runStart, c, lastFg, lastBold, lastDim);
5253
+ parts.push(flushRun(curRow, r, runStart, c, lastFg, lastBold, lastDim));
5220
5254
  const last = curRow[c - 1];
5221
5255
  lastFg = last.fg;
5222
5256
  lastBold = last.bold;
@@ -5225,9 +5259,9 @@ function flushDiff(current, previous) {
5225
5259
  }
5226
5260
  }
5227
5261
  }
5228
- if (out.length > 0) {
5229
- out += RESET;
5230
- process.stdout.write(out);
5262
+ if (parts.length > 0) {
5263
+ parts.push(RESET);
5264
+ process.stdout.write(parts.join(""));
5231
5265
  }
5232
5266
  }
5233
5267
  function flushRun(row, r, start, end, lastFg, lastBold, lastDim) {
@@ -5331,33 +5365,21 @@ function renderPanel(buf, panel, logs, agentCount, clientCount, version2, webUrl
5331
5365
  }
5332
5366
  }
5333
5367
  if (version2 && LOGO_LINES.length >= 3) {
5334
- const vStr = `v${version2}`;
5368
+ const infoStr = `v${version2} \xB7 ${agentCount} agent${agentCount !== 1 ? "s" : ""} \xB7 ${clientCount} client${clientCount !== 1 ? "s" : ""}`;
5335
5369
  const maxLen = innerWidth - (LOGO_LINES[2]?.length || 0) - 3;
5336
- if (maxLen > 4) {
5337
- const trimmed = vStr.slice(0, maxLen);
5338
- const col = x + width - 2 - trimmed.length;
5339
- writeString(buf, logoStartRow + 2, col, trimmed, CYAN_FG, false, true);
5340
- }
5341
- }
5342
- if (LOGO_LINES.length >= 4) {
5343
- const maxLen = innerWidth - (LOGO_LINES[3]?.length || 0) - 3;
5344
5370
  if (maxLen > 10) {
5345
- const trimmed = webUrl.slice(0, maxLen);
5371
+ const trimmed = infoStr.slice(0, maxLen);
5346
5372
  const col = x + width - 2 - trimmed.length;
5347
- writeString(buf, logoStartRow + 3, col, trimmed, CYAN_FG, false, true);
5373
+ writeString(buf, logoStartRow + 2, col, trimmed, CYAN_FG, false, true);
5348
5374
  }
5349
5375
  }
5350
- if (LOGO_LINES.length >= 5) {
5351
- const countStr = `${agentCount} agent${agentCount !== 1 ? "s" : ""} \xB7 ${clientCount} client${clientCount !== 1 ? "s" : ""}`;
5352
- const statusStr = `\u2713 ${countStr}`;
5353
- const maxCountLen = innerWidth - (LOGO_LINES[4]?.length || 0) - 3;
5354
- if (maxCountLen > 10) {
5355
- const trimmed = statusStr.slice(0, maxCountLen);
5356
- const countCol = x + width - 2 - trimmed.length;
5357
- writeString(buf, logoStartRow + 4, countCol, trimmed, BRIGHT_GREEN_FG, false, false);
5358
- }
5376
+ const urlRow = logoStartRow + LOGO_LINES.length;
5377
+ if (urlRow < y + height - 2) {
5378
+ const trimmed = webUrl.slice(0, innerWidth - 2);
5379
+ const col = x + 1 + Math.floor((innerWidth - trimmed.length) / 2);
5380
+ writeString(buf, urlRow, col, trimmed, CYAN_FG, false, false);
5359
5381
  }
5360
- const sepRow = y + LOGO_LINES.length + 1;
5382
+ const sepRow = urlRow + 1;
5361
5383
  if (sepRow < y + height - 1) {
5362
5384
  const sepBorderIdx = Math.floor(glowPosition) % perimeter;
5363
5385
  setCell(buf, sepRow, x, BOX.teeLeft, borderColor(sepBorderIdx, perimeter), false, false);
@@ -5478,9 +5500,7 @@ for (let offset = 0;offset <= 4; offset++) {
5478
5500
  }
5479
5501
  GRADIENT_STRINGS.push(arr);
5480
5502
  }
5481
- var LIGHTNING_HEAD = fg256(231);
5482
- var LIGHTNING_NEAR = fg256(159);
5483
- var LIGHTNING_TAIL = fg256(49);
5503
+ var LIGHTNING_COLORS = [fg256(231), fg256(159), fg256(49)];
5484
5504
  function renderRain(layers, buf, panel) {
5485
5505
  const bufRows = buf.rows;
5486
5506
  const bufCols = buf.cols;
@@ -5495,51 +5515,22 @@ function renderRain(layers, buf, panel) {
5495
5515
  const gradStrs = GRADIENT_STRINGS[brightnessOffset];
5496
5516
  const colCount = Math.min(layer.columns.length, bufCols);
5497
5517
  for (let col = 0;col < colCount; col++) {
5498
- if (panelVisible && col >= panelX && col < panelX2) {
5499
- const drops = layer.columns[col].drops;
5500
- for (let d = 0;d < drops.length; d++) {
5501
- const drop = drops[d];
5502
- const headRow = drop.y | 0;
5503
- const trailLen = drop.trailLen;
5504
- for (let i = 0;i < trailLen; i++) {
5505
- const row = headRow - i;
5506
- if (row < 0 || row >= bufRows)
5507
- continue;
5508
- if (row >= panelY && row < panelY2)
5509
- continue;
5510
- let color;
5511
- if (drop.isLightning) {
5512
- color = i === 0 ? LIGHTNING_HEAD : i === 1 ? LIGHTNING_NEAR : LIGHTNING_TAIL;
5513
- } else {
5514
- const gradIdx = trailLen > 1 ? i * gradLen / (trailLen - 1) | 0 : 0;
5515
- color = gradStrs[Math.min(gradIdx, gradLen)];
5516
- }
5517
- const bold = i === 0 || drop.isLightning;
5518
- const dim = !drop.isLightning && i > trailLen * 0.7;
5519
- setCell(buf, row, col, drop.chars[i], color, bold, dim);
5520
- }
5521
- }
5522
- } else {
5523
- const drops = layer.columns[col].drops;
5524
- for (let d = 0;d < drops.length; d++) {
5525
- const drop = drops[d];
5526
- const headRow = drop.y | 0;
5527
- const trailLen = drop.trailLen;
5528
- for (let i = 0;i < trailLen; i++) {
5529
- const row = headRow - i;
5530
- if (row < 0 || row >= bufRows)
5531
- continue;
5532
- let color;
5533
- if (drop.isLightning) {
5534
- color = i === 0 ? LIGHTNING_HEAD : i === 1 ? LIGHTNING_NEAR : LIGHTNING_TAIL;
5535
- } else {
5536
- const gradIdx = trailLen > 1 ? i * gradLen / (trailLen - 1) | 0 : 0;
5537
- color = gradStrs[Math.min(gradIdx, gradLen)];
5538
- }
5539
- const bold = i === 0 || drop.isLightning;
5540
- const dim = !drop.isLightning && i > trailLen * 0.7;
5541
- setCell(buf, row, col, drop.chars[i], color, bold, dim);
5542
- }
5518
+ const checkPanel = panelVisible && col >= panelX && col < panelX2;
5519
+ const drops = layer.columns[col].drops;
5520
+ for (let d = 0;d < drops.length; d++) {
5521
+ const drop = drops[d];
5522
+ const headRow = drop.y | 0;
5523
+ const trailLen = drop.trailLen;
5524
+ for (let i = 0;i < trailLen; i++) {
5525
+ const row = headRow - i;
5526
+ if (row < 0 || row >= bufRows)
5527
+ continue;
5528
+ if (checkPanel && row >= panelY && row < panelY2)
5529
+ continue;
5530
+ const color = drop.isLightning ? LIGHTNING_COLORS[Math.min(i, 2)] : gradStrs[Math.min(trailLen > 1 ? i * gradLen / (trailLen - 1) | 0 : 0, gradLen)];
5531
+ const bold = i === 0 || drop.isLightning;
5532
+ const dim = !drop.isLightning && i > trailLen * 0.7;
5533
+ setCell(buf, row, col, drop.chars[i], color, bold, dim);
5543
5534
  }
5544
5535
  }
5545
5536
  }
@@ -5566,6 +5557,7 @@ var tuiOptions = null;
5566
5557
  function handleResize() {
5567
5558
  const { rows, cols } = getTerminalSize();
5568
5559
  clearScreen();
5560
+ buildMoveToTable(rows, cols);
5569
5561
  bufA = createFrameBuffer(rows, cols);
5570
5562
  bufB = createFrameBuffer(rows, cols);
5571
5563
  currentIsA = true;
@@ -5599,6 +5591,7 @@ function startTui(options) {
5599
5591
  tuiOptions = options;
5600
5592
  enterAltScreen();
5601
5593
  const { rows, cols } = getTerminalSize();
5594
+ buildMoveToTable(rows, cols);
5602
5595
  bufA = createFrameBuffer(rows, cols);
5603
5596
  bufB = createFrameBuffer(rows, cols);
5604
5597
  currentIsA = true;
@@ -5766,6 +5759,8 @@ async function main() {
5766
5759
  scanDirs = [projectsRoot];
5767
5760
  }
5768
5761
  const agents = new Map;
5762
+ const openFds = new Map;
5763
+ const staleAgents = new Set;
5769
5764
  const fileWatchers = new Map;
5770
5765
  const pollingTimers = new Map;
5771
5766
  const waitingTimers = new Map;
@@ -5796,7 +5791,7 @@ async function main() {
5796
5791
  ...analyticsConfig
5797
5792
  });
5798
5793
  const scanAll = !projectDir;
5799
- const scanner = startProjectScanner(scanDirs[0], scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast, idleTimeoutMs);
5794
+ const scanner = startProjectScanner(scanDirs[0], scanAll, agents, openFds, staleAgents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast, idleTimeoutMs);
5800
5795
  const updateCheckTimer = setInterval(async () => {
5801
5796
  const msg = await checkForUpdate();
5802
5797
  if (msg) {
@@ -5810,6 +5805,11 @@ async function main() {
5810
5805
  [ctrl-daemon] Shutting down...`);
5811
5806
  scanner.stop();
5812
5807
  server.stop();
5808
+ for (const fd of openFds.values()) {
5809
+ try {
5810
+ fs3.closeSync(fd);
5811
+ } catch {}
5812
+ }
5813
5813
  for (const watcher of fileWatchers.values())
5814
5814
  watcher.close();
5815
5815
  for (const timer of pollingTimers.values())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bulletproof-sh/ctrl-daemon",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
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",