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