@bulletproof-sh/ctrl-daemon 0.0.17 → 0.0.18

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,26 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import { execFileSync } from "node:child_process";
3
- import { dirname, join } from "node:path";
4
- import { fileURLToPath } from "node:url";
2
+ import { execFileSync } from 'node:child_process';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
5
 
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const entry = join(__dirname, "..", "dist", "index.js");
7
+ const entry = join(__dirname, '..', 'dist', 'index.js');
8
8
 
9
9
  // Check for bun in PATH
10
10
  try {
11
- execFileSync("bun", ["--version"], { stdio: "ignore" });
11
+ execFileSync('bun', ['--version'], { stdio: 'ignore' });
12
12
  } catch {
13
13
  console.error(
14
- "ctrl-daemon requires Bun to run.\n\nInstall Bun: https://bun.sh\n curl -fsSL https://bun.sh/install | bash",
14
+ 'ctrl-daemon requires Bun to run.\n\nInstall Bun: https://bun.sh\n curl -fsSL https://bun.sh/install | bash',
15
15
  );
16
16
  process.exit(1);
17
17
  }
18
18
 
19
19
  // Forward all CLI args to the daemon entry point
20
- const args = ["run", entry, ...process.argv.slice(2)];
20
+ const args = ['run', entry, ...process.argv.slice(2)];
21
21
 
22
22
  try {
23
- execFileSync("bun", args, { stdio: "inherit" });
23
+ execFileSync('bun', args, { stdio: 'inherit' });
24
24
  } catch (err) {
25
25
  // execFileSync throws on non-zero exit — just propagate the code
26
26
  process.exit(err.status ?? 1);
package/dist/index.js CHANGED
@@ -11,8 +11,9 @@ var __export = (target, all) => {
11
11
  };
12
12
 
13
13
  // src/index.ts
14
- import * as fs3 from "fs";
15
- import * as path3 from "path";
14
+ import { spawn } from "child_process";
15
+ import * as fs4 from "fs";
16
+ import * as path4 from "path";
16
17
 
17
18
  // src/analytics.ts
18
19
  import os from "os";
@@ -4231,18 +4232,21 @@ function initAnalytics() {
4231
4232
  return { customKey, customHost };
4232
4233
  }
4233
4234
  function trackEvent(event, properties) {
4234
- if (!client)
4235
+ if (!client) {
4235
4236
  return;
4237
+ }
4236
4238
  client.capture({ distinctId, event, properties });
4237
4239
  }
4238
4240
  function trackException(error, properties) {
4239
- if (!client)
4241
+ if (!client) {
4240
4242
  return;
4243
+ }
4241
4244
  client.captureException(error, distinctId, properties);
4242
4245
  }
4243
4246
  async function shutdownAnalytics() {
4244
- if (!client)
4247
+ if (!client) {
4245
4248
  return;
4249
+ }
4246
4250
  await client.shutdown();
4247
4251
  }
4248
4252
 
@@ -4383,8 +4387,9 @@ function startSpinner(text) {
4383
4387
  };
4384
4388
  }
4385
4389
  function buildWebUrl(port, host) {
4386
- if (port === DEFAULT_WS_PORT)
4390
+ if (port === DEFAULT_WS_PORT) {
4387
4391
  return WEB_APP_BASE_URL;
4392
+ }
4388
4393
  const clientHost = host === "0.0.0.0" ? "localhost" : host;
4389
4394
  return `${WEB_APP_BASE_URL}?daemon=ws://${clientHost}:${port}/ws`;
4390
4395
  }
@@ -4407,9 +4412,43 @@ ${YL}${updateMsg}${RS}
4407
4412
  console.log("");
4408
4413
  }
4409
4414
 
4415
+ // src/lockfile.ts
4416
+ import * as fs from "fs";
4417
+ import * as path from "path";
4418
+ var LOCK_DIR = path.join(process.env.HOME || "~", ".ctrl");
4419
+ var LOCK_FILE = path.join(LOCK_DIR, "daemon.pid");
4420
+ function isProcessRunning(pid) {
4421
+ try {
4422
+ process.kill(pid, 0);
4423
+ return true;
4424
+ } catch {
4425
+ return false;
4426
+ }
4427
+ }
4428
+ function acquireLock() {
4429
+ fs.mkdirSync(LOCK_DIR, { recursive: true });
4430
+ try {
4431
+ const content = fs.readFileSync(LOCK_FILE, "utf-8").trim();
4432
+ const pid = Number.parseInt(content, 10);
4433
+ if (!Number.isNaN(pid) && isProcessRunning(pid)) {
4434
+ return { acquired: false, existingPid: pid };
4435
+ }
4436
+ } catch {}
4437
+ fs.writeFileSync(LOCK_FILE, String(process.pid), "utf-8");
4438
+ return { acquired: true };
4439
+ }
4440
+ function releaseLock() {
4441
+ try {
4442
+ const content = fs.readFileSync(LOCK_FILE, "utf-8").trim();
4443
+ if (content === String(process.pid)) {
4444
+ fs.unlinkSync(LOCK_FILE);
4445
+ }
4446
+ } catch {}
4447
+ }
4448
+
4410
4449
  // src/projectScanner.ts
4411
- import * as fs2 from "fs";
4412
- import * as path2 from "path";
4450
+ import * as fs3 from "fs";
4451
+ import * as path3 from "path";
4413
4452
 
4414
4453
  // ../shared/src/transcript/constants.ts
4415
4454
  var FILE_WATCHER_POLL_INTERVAL_MS = 2000;
@@ -4422,7 +4461,7 @@ var BASH_COMMAND_DISPLAY_MAX_LENGTH = 30;
4422
4461
  var TASK_DESCRIPTION_DISPLAY_MAX_LENGTH = 40;
4423
4462
 
4424
4463
  // src/sessionWatcher.ts
4425
- import * as fs from "fs";
4464
+ import * as fs2 from "fs";
4426
4465
  // ../shared/src/transcript/timerManager.ts
4427
4466
  function clearAgentActivity(agent, agentId, permissionTimers, broadcast) {
4428
4467
  if (!agent)
@@ -4512,10 +4551,10 @@ function startPermissionTimer(agentId, agents, permissionTimers, permissionExemp
4512
4551
  permissionTimers.set(agentId, timer);
4513
4552
  }
4514
4553
  // ../shared/src/transcript/transcriptParser.ts
4515
- import * as path from "path";
4554
+ import * as path2 from "path";
4516
4555
  var PERMISSION_EXEMPT_TOOLS = new Set(["Task", "AskUserQuestion"]);
4517
4556
  function formatToolStatus(toolName, input) {
4518
- const base = (p) => typeof p === "string" ? path.basename(p) : "";
4557
+ const base = (p) => typeof p === "string" ? path2.basename(p) : "";
4519
4558
  switch (toolName) {
4520
4559
  case "Read":
4521
4560
  return `Reading ${base(input.file_path)}`;
@@ -4658,7 +4697,7 @@ function processTranscriptLine(agentId, line, agents, waitingTimers, permissionT
4658
4697
  }
4659
4698
  } catch {}
4660
4699
  }
4661
- function processProgressRecord(agentId, record, agents, waitingTimers, permissionTimers, broadcast) {
4700
+ function processProgressRecord(agentId, record, agents, _waitingTimers, permissionTimers, broadcast) {
4662
4701
  const agent = agents.get(agentId);
4663
4702
  if (!agent)
4664
4703
  return;
@@ -4784,10 +4823,12 @@ function daemonLog(message, color) {
4784
4823
  }
4785
4824
  }
4786
4825
  function onBroadcast(msg) {
4787
- if (!tuiActive)
4826
+ if (!tuiActive) {
4788
4827
  return;
4789
- if (typeof msg !== "object" || msg === null)
4828
+ }
4829
+ if (typeof msg !== "object" || msg === null) {
4790
4830
  return;
4831
+ }
4791
4832
  const m = msg;
4792
4833
  const type = m.type;
4793
4834
  const id = m.id;
@@ -4848,14 +4889,14 @@ var SHARED_BUF_SIZE = 64 * 1024;
4848
4889
  var sharedReadBuf = Buffer.allocUnsafe(SHARED_BUF_SIZE);
4849
4890
  function startSessionWatcher(agentId, filePath, agents, openFds, staleAgents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast) {
4850
4891
  try {
4851
- const fd = fs.openSync(filePath, "r");
4892
+ const fd = fs2.openSync(filePath, "r");
4852
4893
  openFds.set(agentId, fd);
4853
4894
  } catch (e) {
4854
4895
  daemonLog(`Failed to open fd for agent ${agentId}: ${e}`);
4855
4896
  return;
4856
4897
  }
4857
4898
  try {
4858
- const watcher = fs.watch(filePath, () => {
4899
+ const watcher = fs2.watch(filePath, () => {
4859
4900
  readNewLines(agentId, agents, openFds, staleAgents, waitingTimers, permissionTimers, broadcast);
4860
4901
  });
4861
4902
  fileWatchers.set(agentId, watcher);
@@ -4873,18 +4914,21 @@ function startSessionWatcher(agentId, filePath, agents, openFds, staleAgents, fi
4873
4914
  }
4874
4915
  function readNewLines(agentId, agents, openFds, staleAgents, waitingTimers, permissionTimers, broadcast) {
4875
4916
  const agent = agents.get(agentId);
4876
- if (!agent)
4917
+ if (!agent) {
4877
4918
  return;
4919
+ }
4878
4920
  const fd = openFds.get(agentId);
4879
- if (fd === undefined)
4921
+ if (fd === undefined) {
4880
4922
  return;
4923
+ }
4881
4924
  try {
4882
- const stat = fs.fstatSync(fd);
4883
- if (stat.size <= agent.fileOffset)
4925
+ const stat = fs2.fstatSync(fd);
4926
+ if (stat.size <= agent.fileOffset) {
4884
4927
  return;
4928
+ }
4885
4929
  const readSize = stat.size - agent.fileOffset;
4886
4930
  const buf = readSize <= sharedReadBuf.length ? sharedReadBuf : Buffer.allocUnsafe(readSize);
4887
- const bytesRead = fs.readSync(fd, buf, 0, readSize, agent.fileOffset);
4931
+ const bytesRead = fs2.readSync(fd, buf, 0, readSize, agent.fileOffset);
4888
4932
  agent.fileOffset = stat.size;
4889
4933
  const text = agent.lineBuffer + buf.toString("utf-8", 0, bytesRead);
4890
4934
  const lines = text.split(`
@@ -4892,8 +4936,9 @@ function readNewLines(agentId, agents, openFds, staleAgents, waitingTimers, perm
4892
4936
  agent.lineBuffer = lines.pop() || "";
4893
4937
  let activityUpdated = false;
4894
4938
  for (const line of lines) {
4895
- if (!line.trim())
4939
+ if (!line.trim()) {
4896
4940
  continue;
4941
+ }
4897
4942
  if (!activityUpdated) {
4898
4943
  agent.lastActivityAt = Date.now();
4899
4944
  cancelWaitingTimer(agentId, waitingTimers);
@@ -4915,15 +4960,16 @@ function stopSessionWatcher(agentId, openFds, fileWatchers, pollingTimers, waiti
4915
4960
  const fd = openFds.get(agentId);
4916
4961
  if (fd !== undefined) {
4917
4962
  try {
4918
- fs.closeSync(fd);
4963
+ fs2.closeSync(fd);
4919
4964
  } catch {}
4920
4965
  openFds.delete(agentId);
4921
4966
  }
4922
4967
  fileWatchers.get(agentId)?.close();
4923
4968
  fileWatchers.delete(agentId);
4924
4969
  const pt = pollingTimers.get(agentId);
4925
- if (pt)
4970
+ if (pt) {
4926
4971
  clearInterval(pt);
4972
+ }
4927
4973
  pollingTimers.delete(agentId);
4928
4974
  cancelWaitingTimer(agentId, waitingTimers);
4929
4975
  cancelPermissionTimer(agentId, permissionTimers);
@@ -4934,27 +4980,30 @@ function resolveProjectHash(projectDir) {
4934
4980
  return projectDir.replace(/[:\\/.]/g, "-");
4935
4981
  }
4936
4982
  function resolveProjectDir(projectDir, claudeHome) {
4937
- const home = claudeHome || path2.join(process.env.HOME || "~", ".claude");
4938
- const projectsDir = path2.join(home, "projects");
4983
+ const home = claudeHome || path3.join(process.env.HOME || "~", ".claude");
4984
+ const projectsDir = path3.join(home, "projects");
4939
4985
  const exactHash = resolveProjectHash(projectDir);
4940
- const exactPath = path2.join(projectsDir, exactHash);
4941
- if (fs2.existsSync(exactPath))
4986
+ const exactPath = path3.join(projectsDir, exactHash);
4987
+ if (fs3.existsSync(exactPath)) {
4942
4988
  return exactPath;
4989
+ }
4943
4990
  const macHash = projectDir.replace(/[:\\/._ ]/g, "-");
4944
- const macPath = path2.join(projectsDir, macHash);
4945
- if (fs2.existsSync(macPath))
4991
+ const macPath = path3.join(projectsDir, macHash);
4992
+ if (fs3.existsSync(macPath)) {
4946
4993
  return macPath;
4994
+ }
4947
4995
  return exactPath;
4948
4996
  }
4949
4997
  function collectJsonlFiles(dir) {
4950
4998
  try {
4951
4999
  const results = [];
4952
- for (const f of fs2.readdirSync(dir)) {
4953
- if (!f.endsWith(".jsonl"))
5000
+ for (const f of fs3.readdirSync(dir)) {
5001
+ if (!f.endsWith(".jsonl")) {
4954
5002
  continue;
4955
- const filePath = path2.join(dir, f);
5003
+ }
5004
+ const filePath = path3.join(dir, f);
4956
5005
  try {
4957
- const stat = fs2.statSync(filePath);
5006
+ const stat = fs3.statSync(filePath);
4958
5007
  results.push({ filePath, mtimeMs: stat.mtimeMs });
4959
5008
  } catch {}
4960
5009
  }
@@ -4967,14 +5016,14 @@ function collectAllJsonlFiles(projectsRoot) {
4967
5016
  const files = [];
4968
5017
  let subdirs;
4969
5018
  try {
4970
- subdirs = fs2.readdirSync(projectsRoot);
5019
+ subdirs = fs3.readdirSync(projectsRoot);
4971
5020
  } catch {
4972
5021
  return [];
4973
5022
  }
4974
5023
  for (const subdir of subdirs) {
4975
- const fullPath = path2.join(projectsRoot, subdir);
5024
+ const fullPath = path3.join(projectsRoot, subdir);
4976
5025
  try {
4977
- const stat = fs2.statSync(fullPath);
5026
+ const stat = fs3.statSync(fullPath);
4978
5027
  if (stat.isDirectory()) {
4979
5028
  files.push(...collectJsonlFiles(fullPath));
4980
5029
  }
@@ -4989,13 +5038,15 @@ function startProjectScanner(rootDir, scanAll, agents, openFds, staleAgents, fil
4989
5038
  const now = Date.now();
4990
5039
  const fileInfos = scanAll ? collectAllJsonlFiles(rootDir) : collectJsonlFiles(rootDir);
4991
5040
  for (const { filePath, mtimeMs } of fileInfos) {
4992
- if (knownJsonlFiles.has(filePath))
5041
+ if (knownJsonlFiles.has(filePath)) {
4993
5042
  continue;
4994
- if (now - mtimeMs > idleTimeoutMs)
5043
+ }
5044
+ if (now - mtimeMs > idleTimeoutMs) {
4995
5045
  continue;
5046
+ }
4996
5047
  knownJsonlFiles.add(filePath);
4997
5048
  const id = nextAgentId++;
4998
- const agentProjectDir = path2.dirname(filePath);
5049
+ const agentProjectDir = path3.dirname(filePath);
4999
5050
  const agent = {
5000
5051
  id,
5001
5052
  projectDir: agentProjectDir,
@@ -5013,7 +5064,7 @@ function startProjectScanner(rootDir, scanAll, agents, openFds, staleAgents, fil
5013
5064
  lastActivityAt: mtimeMs
5014
5065
  };
5015
5066
  agents.set(id, agent);
5016
- daemonLog(`Agent ${id}: watching ${path2.basename(filePath)}`);
5067
+ daemonLog(`Agent ${id}: watching ${path3.basename(filePath)}`);
5017
5068
  broadcast({ type: "agentCreated", id });
5018
5069
  startSessionWatcher(id, filePath, agents, openFds, staleAgents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast);
5019
5070
  readNewLines(id, agents, openFds, staleAgents, waitingTimers, permissionTimers, broadcast);
@@ -5059,8 +5110,9 @@ function createServer({ port, host, agents }) {
5059
5110
  fetch(req, server2) {
5060
5111
  const url = new URL(req.url);
5061
5112
  if (url.pathname === "/ws") {
5062
- if (server2.upgrade(req))
5113
+ if (server2.upgrade(req)) {
5063
5114
  return;
5115
+ }
5064
5116
  return new Response("WebSocket upgrade failed", { status: 400 });
5065
5117
  }
5066
5118
  if (url.pathname === "/health") {
@@ -5113,8 +5165,9 @@ function createServer({ port, host, agents }) {
5113
5165
  const errCode = err.code ?? "";
5114
5166
  const errMsg = err instanceof Error ? err.message : "";
5115
5167
  const isAddrInUse = errCode === "EADDRINUSE" || errMsg.includes("EADDRINUSE") || errMsg.includes("address already in use");
5116
- if (!isAddrInUse || attempt === PORT_RETRY_LIMIT - 1)
5168
+ if (!isAddrInUse || attempt === PORT_RETRY_LIMIT - 1) {
5117
5169
  throw err;
5170
+ }
5118
5171
  boundPort++;
5119
5172
  }
5120
5173
  }
@@ -5128,7 +5181,7 @@ function createServer({ port, host, agents }) {
5128
5181
  return {
5129
5182
  broadcast,
5130
5183
  clientCount: () => clients.size,
5131
- port: server.port,
5184
+ port: server.port ?? port,
5132
5185
  stop: () => server.stop()
5133
5186
  };
5134
5187
  }
@@ -5248,8 +5301,9 @@ function flushDiff(current, previous) {
5248
5301
  }
5249
5302
  }
5250
5303
  if (changed) {
5251
- if (runStart === -1)
5304
+ if (runStart === -1) {
5252
5305
  runStart = c;
5306
+ }
5253
5307
  } else if (runStart !== -1) {
5254
5308
  parts.push(flushRun(curRow, r, runStart, c, lastFg, lastBold, lastDim));
5255
5309
  const last = curRow[c - 1];
@@ -5274,12 +5328,15 @@ function flushRun(row, r, start, end, lastFg, lastBold, lastDim) {
5274
5328
  const cell = row[i];
5275
5329
  if (cell.fg !== fg || cell.bold !== bold || cell.dim !== dim) {
5276
5330
  s += RESET;
5277
- if (cell.fg)
5331
+ if (cell.fg) {
5278
5332
  s += cell.fg;
5279
- if (cell.bold)
5333
+ }
5334
+ if (cell.bold) {
5280
5335
  s += BOLD;
5281
- if (cell.dim)
5336
+ }
5337
+ if (cell.dim) {
5282
5338
  s += DIM;
5339
+ }
5283
5340
  fg = cell.fg;
5284
5341
  bold = cell.bold;
5285
5342
  dim = cell.dim;
@@ -5317,10 +5374,12 @@ function computePanelGeometry(termRows, termCols) {
5317
5374
  function borderColor(borderIdx, perimeterLen) {
5318
5375
  const dist = Math.abs(borderIdx - glowPosition);
5319
5376
  const wrappedDist = Math.min(dist, perimeterLen - dist);
5320
- if (wrappedDist < 3)
5377
+ if (wrappedDist < 3) {
5321
5378
  return GLOW_BRIGHT;
5322
- if (wrappedDist < 6)
5379
+ }
5380
+ if (wrappedDist < 6) {
5323
5381
  return GLOW_MEDIUM;
5382
+ }
5324
5383
  return GLOW_DIM;
5325
5384
  }
5326
5385
  function writeString(buf, row, col, str, fg, bold, dim) {
@@ -5329,8 +5388,9 @@ function writeString(buf, row, col, str, fg, bold, dim) {
5329
5388
  }
5330
5389
  }
5331
5390
  function renderPanel(buf, panel, logs, agentCount, clientCount, version2, webUrl) {
5332
- if (!panel.visible)
5391
+ if (!panel.visible) {
5333
5392
  return;
5393
+ }
5334
5394
  const { x, y, width, height } = panel;
5335
5395
  const innerWidth = width - 2;
5336
5396
  const perimeter = 2 * (width + height) - 4;
@@ -5393,8 +5453,9 @@ function renderPanel(buf, panel, logs, agentCount, clientCount, version2, webUrl
5393
5453
  const visibleLogs = logs.slice(-panel.logLines);
5394
5454
  for (let i = 0;i < visibleLogs.length; i++) {
5395
5455
  const row = logStartRow + i;
5396
- if (row >= y + height - 1)
5456
+ if (row >= y + height - 1) {
5397
5457
  break;
5458
+ }
5398
5459
  const entry = visibleLogs[i];
5399
5460
  writeString(buf, row, x + 2, entry.timestamp, CYAN_FG, false, true);
5400
5461
  const msgCol = x + 2 + entry.timestamp.length + 1;
@@ -5478,8 +5539,9 @@ function tickRain(layers, rows) {
5478
5539
  const drop = drops[d];
5479
5540
  drop.y += drop.speed;
5480
5541
  drop.frameCount++;
5481
- if (drop.y - drop.trailLen > rows)
5542
+ if (drop.y - drop.trailLen > rows) {
5482
5543
  continue;
5544
+ }
5483
5545
  const mutInterval = drop.isLightning ? 3 : cfg.mutationInterval;
5484
5546
  if (drop.frameCount % mutInterval === 0 && drop.chars.length > 1) {
5485
5547
  const idx = 1 + (Math.random() * (drop.chars.length - 1) | 0);
@@ -5524,10 +5586,12 @@ function renderRain(layers, buf, panel) {
5524
5586
  const trailLen = drop.trailLen;
5525
5587
  for (let i = 0;i < trailLen; i++) {
5526
5588
  const row = headRow - i;
5527
- if (row < 0 || row >= bufRows)
5589
+ if (row < 0 || row >= bufRows) {
5528
5590
  continue;
5529
- if (checkPanel && row >= panelY && row < panelY2)
5591
+ }
5592
+ if (checkPanel && row >= panelY && row < panelY2) {
5530
5593
  continue;
5594
+ }
5531
5595
  const color = drop.isLightning ? LIGHTNING_COLORS[Math.min(i, 2)] : gradStrs[Math.min(trailLen > 1 ? i * gradLen / (trailLen - 1) | 0 : 0, gradLen)];
5532
5596
  const bold = i === 0 || drop.isLightning;
5533
5597
  const dim = !drop.isLightning && i > trailLen * 0.7;
@@ -5567,13 +5631,15 @@ function handleResize() {
5567
5631
  panel = computePanelGeometry(rows, cols);
5568
5632
  }
5569
5633
  function scheduleFrame() {
5570
- if (!running)
5634
+ if (!running) {
5571
5635
  return;
5636
+ }
5572
5637
  renderTimer = setTimeout(renderFrame, FRAME_INTERVAL_MS);
5573
5638
  }
5574
5639
  function renderFrame() {
5575
- if (!running || !bufA || !bufB || !tuiOptions)
5640
+ if (!running || !bufA || !bufB || !tuiOptions) {
5576
5641
  return;
5642
+ }
5577
5643
  const current = currentIsA ? bufA : bufB;
5578
5644
  const previous = currentIsA ? bufB : bufA;
5579
5645
  tickRain(rainLayers, current.rows);
@@ -5586,8 +5652,9 @@ function renderFrame() {
5586
5652
  scheduleFrame();
5587
5653
  }
5588
5654
  function startTui(options) {
5589
- if (running)
5655
+ if (running) {
5590
5656
  return;
5657
+ }
5591
5658
  running = true;
5592
5659
  tuiOptions = options;
5593
5660
  enterAltScreen();
@@ -5605,8 +5672,9 @@ function startTui(options) {
5605
5672
  daemonLog("TUI started");
5606
5673
  }
5607
5674
  function stopTui() {
5608
- if (!running)
5675
+ if (!running) {
5609
5676
  return;
5677
+ }
5610
5678
  running = false;
5611
5679
  if (renderTimer) {
5612
5680
  clearTimeout(renderTimer);
@@ -5627,20 +5695,24 @@ var NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
5627
5695
  var UPDATE_CHECK_TIMEOUT_MS = 5000;
5628
5696
  function parseSemver(version2) {
5629
5697
  const match = version2.match(/^(\d+)\.(\d+)\.(\d+)/);
5630
- if (!match)
5698
+ if (!match) {
5631
5699
  return null;
5700
+ }
5632
5701
  return [Number(match[1]), Number(match[2]), Number(match[3])];
5633
5702
  }
5634
5703
  function isNewer(current, next) {
5635
5704
  const a = parseSemver(current);
5636
5705
  const b = parseSemver(next);
5637
- if (!a || !b)
5706
+ if (!a || !b) {
5638
5707
  return false;
5708
+ }
5639
5709
  for (let i = 0;i < 3; i++) {
5640
- if (b[i] > a[i])
5710
+ if (b[i] > a[i]) {
5641
5711
  return true;
5642
- if (b[i] < a[i])
5712
+ }
5713
+ if (b[i] < a[i]) {
5643
5714
  return false;
5715
+ }
5644
5716
  }
5645
5717
  return false;
5646
5718
  }
@@ -5659,8 +5731,9 @@ async function getLatestVersion() {
5659
5731
  const timeout = setTimeout(() => controller.abort(), UPDATE_CHECK_TIMEOUT_MS);
5660
5732
  const res = await fetch(NPM_REGISTRY_URL, { signal: controller.signal });
5661
5733
  clearTimeout(timeout);
5662
- if (!res.ok)
5734
+ if (!res.ok) {
5663
5735
  return null;
5736
+ }
5664
5737
  const data = await res.json();
5665
5738
  return data.version ?? null;
5666
5739
  } catch {
@@ -5668,14 +5741,13 @@ async function getLatestVersion() {
5668
5741
  }
5669
5742
  }
5670
5743
  async function checkForUpdate() {
5671
- const [current, latest] = await Promise.all([
5672
- getCurrentVersion(),
5673
- getLatestVersion()
5674
- ]);
5675
- if (!current || !latest)
5744
+ const [current, latest] = await Promise.all([getCurrentVersion(), getLatestVersion()]);
5745
+ if (!current || !latest) {
5676
5746
  return null;
5677
- if (!isNewer(current, latest))
5747
+ }
5748
+ if (!isNewer(current, latest)) {
5678
5749
  return null;
5750
+ }
5679
5751
  return ` \u2B06 Update available: v${current} \u2192 v${latest}
5680
5752
  ` + ` Run: npx ${PACKAGE_NAME}@latest`;
5681
5753
  }
@@ -5720,18 +5792,28 @@ function parseArgs() {
5720
5792
  }
5721
5793
  return { projectDir, port, host, idleTimeoutMs, noTui };
5722
5794
  }
5795
+ function openBrowser(url) {
5796
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
5797
+ try {
5798
+ const child = spawn(cmd, [url], { detached: true, stdio: "ignore" });
5799
+ child.unref();
5800
+ } catch {}
5801
+ }
5723
5802
  function resolveProjectsRoot(claudeHome) {
5724
- const home = claudeHome || path3.join(process.env.HOME || "~", ".claude");
5725
- return path3.join(home, "projects");
5803
+ const home = claudeHome || path4.join(process.env.HOME || "~", ".claude");
5804
+ return path4.join(home, "projects");
5726
5805
  }
5727
5806
  async function main() {
5728
5807
  printBanner();
5808
+ const lockResult = acquireLock();
5809
+ if (!lockResult.acquired) {
5810
+ console.error(`\x1B[31m[ctrl-daemon] Another instance is already running (pid ${lockResult.existingPid}).\x1B[0m`);
5811
+ console.error("Kill it first or use a different --port.");
5812
+ process.exit(1);
5813
+ }
5729
5814
  const stopSpinner = startSpinner("Starting\u2026");
5730
5815
  const analyticsConfig = initAnalytics();
5731
- const [version2, updateMsg] = await Promise.all([
5732
- getCurrentVersion(),
5733
- checkForUpdate()
5734
- ]);
5816
+ const [version2, updateMsg] = await Promise.all([getCurrentVersion(), checkForUpdate()]);
5735
5817
  stopSpinner();
5736
5818
  process.on("uncaughtException", async (err) => {
5737
5819
  stopTui();
@@ -5768,7 +5850,9 @@ async function main() {
5768
5850
  const permissionTimers = new Map;
5769
5851
  const server = createServer({ port, host, agents });
5770
5852
  const boundPort = server.port;
5853
+ const webUrl = buildWebUrl(boundPort, host);
5771
5854
  printReady(boundPort, host, version2, updateMsg);
5855
+ openBrowser(webUrl);
5772
5856
  const rawBroadcast = server.broadcast;
5773
5857
  const broadcast = (msg) => {
5774
5858
  rawBroadcast(msg);
@@ -5778,7 +5862,7 @@ async function main() {
5778
5862
  startTui({
5779
5863
  version: version2,
5780
5864
  port: boundPort,
5781
- webUrl: buildWebUrl(boundPort, host),
5865
+ webUrl,
5782
5866
  agentCount: () => agents.size,
5783
5867
  clientCount: () => server.clientCount()
5784
5868
  });
@@ -5808,17 +5892,22 @@ async function main() {
5808
5892
  server.stop();
5809
5893
  for (const fd of openFds.values()) {
5810
5894
  try {
5811
- fs3.closeSync(fd);
5895
+ fs4.closeSync(fd);
5812
5896
  } catch {}
5813
5897
  }
5814
- for (const watcher of fileWatchers.values())
5898
+ for (const watcher of fileWatchers.values()) {
5815
5899
  watcher.close();
5816
- for (const timer of pollingTimers.values())
5900
+ }
5901
+ for (const timer of pollingTimers.values()) {
5817
5902
  clearInterval(timer);
5818
- for (const timer of waitingTimers.values())
5903
+ }
5904
+ for (const timer of waitingTimers.values()) {
5819
5905
  clearTimeout(timer);
5820
- for (const timer of permissionTimers.values())
5906
+ }
5907
+ for (const timer of permissionTimers.values()) {
5821
5908
  clearTimeout(timer);
5909
+ }
5910
+ releaseLock();
5822
5911
  await shutdownAnalytics();
5823
5912
  process.exit(0);
5824
5913
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bulletproof-sh/ctrl-daemon",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
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",
@@ -16,10 +16,11 @@
16
16
  "start": "bun src/index.ts",
17
17
  "build": "bun build src/index.ts --outdir dist --target bun",
18
18
  "check": "biome check .",
19
+ "verify": "bunx tsc --noEmit && biome check .",
19
20
  "prepublishOnly": "bun run build"
20
21
  },
21
22
  "devDependencies": {
22
- "@biomejs/biome": "^1.9.4",
23
+ "@biomejs/biome": "^2.4.4",
23
24
  "@types/bun": "latest",
24
25
  "typescript": "~5.9.3"
25
26
  },