@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.
- package/bin/ctrl-daemon.js +8 -8
- package/dist/index.js +171 -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
|
|
|
@@ -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
|
|
4412
|
-
import * as
|
|
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
|
|
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
|
|
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" ?
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 ||
|
|
4938
|
-
const projectsDir =
|
|
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 =
|
|
4941
|
-
if (
|
|
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 =
|
|
4945
|
-
if (
|
|
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
|
|
4953
|
-
if (!f.endsWith(".jsonl"))
|
|
5000
|
+
for (const f of fs3.readdirSync(dir)) {
|
|
5001
|
+
if (!f.endsWith(".jsonl")) {
|
|
4954
5002
|
continue;
|
|
4955
|
-
|
|
5003
|
+
}
|
|
5004
|
+
const filePath = path3.join(dir, f);
|
|
4956
5005
|
try {
|
|
4957
|
-
const stat =
|
|
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 =
|
|
5019
|
+
subdirs = fs3.readdirSync(projectsRoot);
|
|
4971
5020
|
} catch {
|
|
4972
5021
|
return [];
|
|
4973
5022
|
}
|
|
4974
5023
|
for (const subdir of subdirs) {
|
|
4975
|
-
const fullPath =
|
|
5024
|
+
const fullPath = path3.join(projectsRoot, subdir);
|
|
4976
5025
|
try {
|
|
4977
|
-
const stat =
|
|
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
|
-
|
|
5043
|
+
}
|
|
5044
|
+
if (now - mtimeMs > idleTimeoutMs) {
|
|
4995
5045
|
continue;
|
|
5046
|
+
}
|
|
4996
5047
|
knownJsonlFiles.add(filePath);
|
|
4997
5048
|
const id = nextAgentId++;
|
|
4998
|
-
const agentProjectDir =
|
|
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 ${
|
|
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
|
-
|
|
5333
|
+
}
|
|
5334
|
+
if (cell.bold) {
|
|
5280
5335
|
s += BOLD;
|
|
5281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
5725
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5900
|
+
}
|
|
5901
|
+
for (const timer of pollingTimers.values()) {
|
|
5817
5902
|
clearInterval(timer);
|
|
5818
|
-
|
|
5903
|
+
}
|
|
5904
|
+
for (const timer of waitingTimers.values()) {
|
|
5819
5905
|
clearTimeout(timer);
|
|
5820
|
-
|
|
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.
|
|
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
|
},
|