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