@bulletproof-sh/ctrl-daemon 0.0.8 → 0.0.10
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/README.md +18 -6
- package/dist/index.js +819 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @bulletproof-sh/ctrl-daemon
|
|
2
2
|
|
|
3
|
-
WebSocket daemon for [Ctrl / Cubicles](https://ctrl.bulletproof.sh) — watches Claude Code sessions on disk and broadcasts live agent state to connected web clients.
|
|
3
|
+
WebSocket daemon for [Ctrl / Cubicles](https://ctrl.bulletproof.sh) — watches Claude Code sessions on disk and broadcasts live agent state to connected web clients. Features a full-screen animated Matrix rain TUI with a centered log panel showing daemon events in real time.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
@@ -17,20 +17,32 @@ npx @bulletproof-sh/ctrl-daemon --project-dir /path/to/your/project
|
|
|
17
17
|
|
|
18
18
|
# Custom port / host
|
|
19
19
|
npx @bulletproof-sh/ctrl-daemon --port 3001 --host 127.0.0.1
|
|
20
|
+
|
|
21
|
+
# Disable the Matrix rain TUI (plain text output)
|
|
22
|
+
npx @bulletproof-sh/ctrl-daemon --no-tui
|
|
20
23
|
```
|
|
21
24
|
|
|
22
25
|
### Options
|
|
23
26
|
|
|
24
27
|
| Flag | Default | Description |
|
|
25
28
|
|---|---|---|
|
|
26
|
-
| `--port <number>` | `3001` | Port to listen on |
|
|
29
|
+
| `--port <number>` | `3001` | Port to listen on (auto-increments if in use) |
|
|
27
30
|
| `--host <address>` | `0.0.0.0` | Host/address to bind to |
|
|
28
31
|
| `--project-dir <path>` | — | Watch a single project; omit to watch all projects |
|
|
29
32
|
| `--idle-timeout <minutes>` | `15` | Agent idle timeout in minutes |
|
|
33
|
+
| `--no-tui` | — | Disable Matrix rain TUI (also auto-disabled when not a TTY) |
|
|
30
34
|
| `--help`, `-h` | — | Print usage |
|
|
31
35
|
|
|
32
36
|
Without `--project-dir`, the daemon scans `~/.claude/projects/` and watches every session it finds there.
|
|
33
37
|
|
|
38
|
+
If the requested port is already in use, the daemon automatically tries the next port (up to 10 attempts).
|
|
39
|
+
|
|
40
|
+
## Terminal UI
|
|
41
|
+
|
|
42
|
+
When running in a TTY, the daemon displays an animated Matrix rain effect with a centered panel showing the CTRL logo, live agent/client counts, and scrolling daemon event logs. The TUI handles terminal resize and restores the terminal cleanly on exit.
|
|
43
|
+
|
|
44
|
+
Pass `--no-tui` to disable the TUI and use plain text output instead. The TUI is also automatically disabled when stdout is not a TTY (e.g. when piping output or running in CI).
|
|
45
|
+
|
|
34
46
|
## Sharing your office
|
|
35
47
|
|
|
36
48
|
Run the daemon and an [ngrok](https://ngrok.com) tunnel in parallel to share a live view with anyone:
|
|
@@ -63,11 +75,11 @@ Crash reports use PostHog analytics.
|
|
|
63
75
|
|
|
64
76
|
## Auto-update
|
|
65
77
|
|
|
66
|
-
On startup the daemon checks npm for a newer version
|
|
78
|
+
On startup — and every 5 minutes in the background — the daemon checks npm for a newer version. If one is found, a notice is printed (or logged to the TUI panel):
|
|
67
79
|
|
|
68
80
|
```
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
⬆ Update available: v0.0.8 → v0.0.9
|
|
82
|
+
Run: npx @bulletproof-sh/ctrl-daemon@latest
|
|
71
83
|
```
|
|
72
84
|
|
|
73
85
|
The check is non-blocking and fails silently if the registry is unreachable.
|
|
@@ -85,5 +97,5 @@ bun run check # biome lint + format
|
|
|
85
97
|
|
|
86
98
|
## License
|
|
87
99
|
|
|
88
|
-
|
|
100
|
+
BUSL-1.1
|
|
89
101
|
|
package/dist/index.js
CHANGED
|
@@ -4245,12 +4245,110 @@ async function shutdownAnalytics() {
|
|
|
4245
4245
|
await client.shutdown();
|
|
4246
4246
|
}
|
|
4247
4247
|
|
|
4248
|
+
// src/tui/constants.ts
|
|
4249
|
+
var KATAKANA_START = 65382;
|
|
4250
|
+
var KATAKANA_END = 65437;
|
|
4251
|
+
var KATAKANA_CHARS = [];
|
|
4252
|
+
for (let i = KATAKANA_START;i <= KATAKANA_END; i++) {
|
|
4253
|
+
KATAKANA_CHARS.push(String.fromCharCode(i));
|
|
4254
|
+
}
|
|
4255
|
+
var EXTRA_CHARS = "0123456789$+-*/%=#@&<>~^".split("");
|
|
4256
|
+
var RAIN_CHARS = [...KATAKANA_CHARS, ...EXTRA_CHARS];
|
|
4257
|
+
var TRAIL_GRADIENT = [
|
|
4258
|
+
231,
|
|
4259
|
+
159,
|
|
4260
|
+
123,
|
|
4261
|
+
49,
|
|
4262
|
+
48,
|
|
4263
|
+
46,
|
|
4264
|
+
40,
|
|
4265
|
+
34,
|
|
4266
|
+
28,
|
|
4267
|
+
22,
|
|
4268
|
+
23,
|
|
4269
|
+
29,
|
|
4270
|
+
24,
|
|
4271
|
+
18,
|
|
4272
|
+
17
|
|
4273
|
+
];
|
|
4274
|
+
var LAYERS = {
|
|
4275
|
+
back: {
|
|
4276
|
+
speedMin: 0.15,
|
|
4277
|
+
speedMax: 0.3,
|
|
4278
|
+
trailMin: 8,
|
|
4279
|
+
trailMax: 16,
|
|
4280
|
+
spawnRate: 0.01,
|
|
4281
|
+
mutationInterval: 12,
|
|
4282
|
+
brightnessOffset: 4
|
|
4283
|
+
},
|
|
4284
|
+
mid: {
|
|
4285
|
+
speedMin: 0.3,
|
|
4286
|
+
speedMax: 0.6,
|
|
4287
|
+
trailMin: 10,
|
|
4288
|
+
trailMax: 25,
|
|
4289
|
+
spawnRate: 0.02,
|
|
4290
|
+
mutationInterval: 8,
|
|
4291
|
+
brightnessOffset: 2
|
|
4292
|
+
},
|
|
4293
|
+
front: {
|
|
4294
|
+
speedMin: 0.6,
|
|
4295
|
+
speedMax: 1.2,
|
|
4296
|
+
trailMin: 6,
|
|
4297
|
+
trailMax: 14,
|
|
4298
|
+
spawnRate: 0.015,
|
|
4299
|
+
mutationInterval: 5,
|
|
4300
|
+
brightnessOffset: 0
|
|
4301
|
+
}
|
|
4302
|
+
};
|
|
4303
|
+
var LIGHTNING_CHANCE = 0.002;
|
|
4304
|
+
var LIGHTNING_SPEED_MIN = 2;
|
|
4305
|
+
var LIGHTNING_SPEED_MAX = 4;
|
|
4306
|
+
var LIGHTNING_TRAIL_LEN = 4;
|
|
4307
|
+
var TARGET_FPS = 8;
|
|
4308
|
+
var FRAME_INTERVAL_MS = Math.round(1000 / TARGET_FPS);
|
|
4309
|
+
var UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
|
|
4310
|
+
var WEB_APP_BASE_URL = "https://ctrl.bulletproof.sh";
|
|
4311
|
+
var DEFAULT_WS_PORT = 3001;
|
|
4312
|
+
var PANEL_MAX_WIDTH = 80;
|
|
4313
|
+
var PANEL_MAX_HEIGHT = 24;
|
|
4314
|
+
var PANEL_MARGIN_X = 5;
|
|
4315
|
+
var PANEL_MARGIN_Y = 3;
|
|
4316
|
+
var PANEL_MIN_WIDTH = 40;
|
|
4317
|
+
var PANEL_MIN_HEIGHT = 12;
|
|
4318
|
+
var PANEL_HEADER_HEIGHT = 8;
|
|
4319
|
+
var LOG_RING_SIZE = 200;
|
|
4320
|
+
var BORDER_GLOW_SPEED = 0.05;
|
|
4321
|
+
var BOX = {
|
|
4322
|
+
topLeft: "\u2554",
|
|
4323
|
+
topRight: "\u2557",
|
|
4324
|
+
bottomLeft: "\u255A",
|
|
4325
|
+
bottomRight: "\u255D",
|
|
4326
|
+
horizontal: "\u2550",
|
|
4327
|
+
vertical: "\u2551",
|
|
4328
|
+
teeLeft: "\u2560",
|
|
4329
|
+
teeRight: "\u2563"
|
|
4330
|
+
};
|
|
4331
|
+
var FG256_TABLE = [];
|
|
4332
|
+
for (let i = 0;i < 256; i++) {
|
|
4333
|
+
FG256_TABLE.push(`\x1B[38;5;${i}m`);
|
|
4334
|
+
}
|
|
4335
|
+
var fg256 = (n) => FG256_TABLE[n];
|
|
4336
|
+
var BOLD = "\x1B[1m";
|
|
4337
|
+
var DIM = "\x1B[2m";
|
|
4338
|
+
var RESET = "\x1B[0m";
|
|
4339
|
+
var BRIGHT_GREEN_FG = "\x1B[92m";
|
|
4340
|
+
var CYAN_FG = "\x1B[36m";
|
|
4341
|
+
var YELLOW_FG = "\x1B[33m";
|
|
4342
|
+
var RED_FG = "\x1B[31m";
|
|
4343
|
+
var ORANGE_FG = "\x1B[38;5;208m";
|
|
4344
|
+
|
|
4248
4345
|
// src/banner.ts
|
|
4249
4346
|
var BG = "\x1B[92m";
|
|
4250
4347
|
var CY = "\x1B[36m";
|
|
4251
4348
|
var YL = "\x1B[33m";
|
|
4252
4349
|
var DM = "\x1B[2m";
|
|
4253
4350
|
var BD = "\x1B[1m";
|
|
4351
|
+
var UL = "\x1B[4m";
|
|
4254
4352
|
var RS = "\x1B[0m";
|
|
4255
4353
|
var LOGO_LINES = [
|
|
4256
4354
|
" \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 ",
|
|
@@ -4270,18 +4368,7 @@ function printBanner() {
|
|
|
4270
4368
|
process.stdout.write(`
|
|
4271
4369
|
`);
|
|
4272
4370
|
}
|
|
4273
|
-
var SPINNER_FRAMES = [
|
|
4274
|
-
"\u280B",
|
|
4275
|
-
"\u2819",
|
|
4276
|
-
"\u2839",
|
|
4277
|
-
"\u2838",
|
|
4278
|
-
"\u283C",
|
|
4279
|
-
"\u2834",
|
|
4280
|
-
"\u2826",
|
|
4281
|
-
"\u2827",
|
|
4282
|
-
"\u2807",
|
|
4283
|
-
"\u280F"
|
|
4284
|
-
];
|
|
4371
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4285
4372
|
function startSpinner(text) {
|
|
4286
4373
|
let i = 0;
|
|
4287
4374
|
const clearWidth = text.length + 4;
|
|
@@ -4293,8 +4380,15 @@ function startSpinner(text) {
|
|
|
4293
4380
|
process.stdout.write(`\r${" ".repeat(clearWidth)}\r`);
|
|
4294
4381
|
};
|
|
4295
4382
|
}
|
|
4296
|
-
function
|
|
4383
|
+
function buildWebUrl(port, host) {
|
|
4384
|
+
if (port === DEFAULT_WS_PORT)
|
|
4385
|
+
return WEB_APP_BASE_URL;
|
|
4386
|
+
const wsUrl = `ws://${host}:${port}/ws`;
|
|
4387
|
+
return `${WEB_APP_BASE_URL}?daemon=${encodeURIComponent(wsUrl)}`;
|
|
4388
|
+
}
|
|
4389
|
+
function printReady(port, host, version2, updateMsg) {
|
|
4297
4390
|
const versionStr = version2 ? ` ${DM}v${version2}${RS}` : "";
|
|
4391
|
+
const webUrl = buildWebUrl(port, host);
|
|
4298
4392
|
if (updateMsg) {
|
|
4299
4393
|
process.stdout.write(`
|
|
4300
4394
|
${YL}${updateMsg}${RS}
|
|
@@ -4304,6 +4398,8 @@ ${YL}${updateMsg}${RS}
|
|
|
4304
4398
|
console.log(` ${BG}\u2713${RS} WebSocket server on :${port}${versionStr}`);
|
|
4305
4399
|
console.log(` ${BG}\u2713${RS} Watching Claude sessions`);
|
|
4306
4400
|
console.log("");
|
|
4401
|
+
console.log(` ${CY}Open: ${UL}${webUrl}${RS}`);
|
|
4402
|
+
console.log("");
|
|
4307
4403
|
console.log(` ${DM}Share: run \`ngrok http ${port}\`, then open Settings \u2192 Share${RS}`);
|
|
4308
4404
|
console.log(` ${DM}Issues: https://github.com/bulletproof-sh/ctrl${RS}`);
|
|
4309
4405
|
console.log("");
|
|
@@ -4660,6 +4756,91 @@ function processProgressRecord(agentId, record, agents, waitingTimers, permissio
|
|
|
4660
4756
|
}
|
|
4661
4757
|
}
|
|
4662
4758
|
}
|
|
4759
|
+
// src/tui/logSink.ts
|
|
4760
|
+
var logBuffer = [];
|
|
4761
|
+
var tuiActive = false;
|
|
4762
|
+
var originalConsoleLog = console.log;
|
|
4763
|
+
var originalConsoleError = console.error;
|
|
4764
|
+
function timestamp() {
|
|
4765
|
+
const d = new Date;
|
|
4766
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
|
|
4767
|
+
}
|
|
4768
|
+
function pushEntry(message, color) {
|
|
4769
|
+
logBuffer.push({ timestamp: timestamp(), message, color });
|
|
4770
|
+
if (logBuffer.length > LOG_RING_SIZE) {
|
|
4771
|
+
logBuffer.shift();
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
function getLogEntries() {
|
|
4775
|
+
return logBuffer;
|
|
4776
|
+
}
|
|
4777
|
+
function daemonLog(message, color) {
|
|
4778
|
+
if (tuiActive) {
|
|
4779
|
+
pushEntry(message, color || fg256(250));
|
|
4780
|
+
} else {
|
|
4781
|
+
originalConsoleError(`[ctrl-daemon] ${message}`);
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
function onBroadcast(msg) {
|
|
4785
|
+
if (!tuiActive)
|
|
4786
|
+
return;
|
|
4787
|
+
if (typeof msg !== "object" || msg === null)
|
|
4788
|
+
return;
|
|
4789
|
+
const m = msg;
|
|
4790
|
+
const type = m.type;
|
|
4791
|
+
const id = m.id;
|
|
4792
|
+
switch (type) {
|
|
4793
|
+
case "agentCreated":
|
|
4794
|
+
pushEntry(`Agent ${id}: session started`, BRIGHT_GREEN_FG);
|
|
4795
|
+
break;
|
|
4796
|
+
case "agentClosed":
|
|
4797
|
+
pushEntry(`Agent ${id}: session closed`, fg256(240));
|
|
4798
|
+
break;
|
|
4799
|
+
case "agentToolStart": {
|
|
4800
|
+
const toolName = m.toolName || "tool";
|
|
4801
|
+
pushEntry(`Agent ${id}: ${toolName}`, CYAN_FG);
|
|
4802
|
+
break;
|
|
4803
|
+
}
|
|
4804
|
+
case "agentToolDone": {
|
|
4805
|
+
const toolName = m.toolName || "tool";
|
|
4806
|
+
pushEntry(`Agent ${id}: ${toolName} done`, fg256(34));
|
|
4807
|
+
break;
|
|
4808
|
+
}
|
|
4809
|
+
case "agentStatus": {
|
|
4810
|
+
const status = m.status;
|
|
4811
|
+
if (status === "waiting") {
|
|
4812
|
+
pushEntry(`Agent ${id}: turn complete`, YELLOW_FG);
|
|
4813
|
+
} else if (status === "permission") {
|
|
4814
|
+
pushEntry(`Agent ${id}: waiting for permission`, ORANGE_FG);
|
|
4815
|
+
}
|
|
4816
|
+
break;
|
|
4817
|
+
}
|
|
4818
|
+
case "agentToolPermissionClear":
|
|
4819
|
+
break;
|
|
4820
|
+
default:
|
|
4821
|
+
pushEntry(`[broadcast] ${type}`, fg256(240));
|
|
4822
|
+
break;
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
function captureConsole() {
|
|
4826
|
+
tuiActive = true;
|
|
4827
|
+
originalConsoleLog = console.log;
|
|
4828
|
+
originalConsoleError = console.error;
|
|
4829
|
+
console.log = (...args) => {
|
|
4830
|
+
const msg = args.map((a) => String(a)).join(" ");
|
|
4831
|
+
pushEntry(msg, fg256(250));
|
|
4832
|
+
};
|
|
4833
|
+
console.error = (...args) => {
|
|
4834
|
+
const msg = args.map((a) => String(a)).join(" ");
|
|
4835
|
+
pushEntry(msg, RED_FG);
|
|
4836
|
+
};
|
|
4837
|
+
}
|
|
4838
|
+
function restoreConsole() {
|
|
4839
|
+
tuiActive = false;
|
|
4840
|
+
console.log = originalConsoleLog;
|
|
4841
|
+
console.error = originalConsoleError;
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4663
4844
|
// src/sessionWatcher.ts
|
|
4664
4845
|
function startSessionWatcher(agentId, filePath, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast) {
|
|
4665
4846
|
try {
|
|
@@ -4668,7 +4849,7 @@ function startSessionWatcher(agentId, filePath, agents, fileWatchers, pollingTim
|
|
|
4668
4849
|
});
|
|
4669
4850
|
fileWatchers.set(agentId, watcher);
|
|
4670
4851
|
} catch (e) {
|
|
4671
|
-
|
|
4852
|
+
daemonLog(`fs.watch failed for agent ${agentId}: ${e}`);
|
|
4672
4853
|
}
|
|
4673
4854
|
const interval = setInterval(() => {
|
|
4674
4855
|
if (!agents.has(agentId)) {
|
|
@@ -4712,7 +4893,7 @@ function readNewLines(agentId, agents, waitingTimers, permissionTimers, broadcas
|
|
|
4712
4893
|
processTranscriptLine(agentId, line, agents, waitingTimers, permissionTimers, broadcast);
|
|
4713
4894
|
}
|
|
4714
4895
|
} catch (e) {
|
|
4715
|
-
|
|
4896
|
+
daemonLog(`Read error for agent ${agentId}: ${e}`);
|
|
4716
4897
|
}
|
|
4717
4898
|
}
|
|
4718
4899
|
function stopSessionWatcher(agentId, fileWatchers, pollingTimers, waitingTimers, permissionTimers) {
|
|
@@ -4810,7 +4991,7 @@ function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTime
|
|
|
4810
4991
|
lastActivityAt: mtimeMs
|
|
4811
4992
|
};
|
|
4812
4993
|
agents.set(id, agent);
|
|
4813
|
-
|
|
4994
|
+
daemonLog(`Agent ${id}: watching ${path2.basename(filePath)}`);
|
|
4814
4995
|
broadcast({ type: "agentCreated", id });
|
|
4815
4996
|
startSessionWatcher(id, filePath, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast);
|
|
4816
4997
|
readNewLines(id, agents, waitingTimers, permissionTimers, broadcast);
|
|
@@ -4821,7 +5002,7 @@ function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTime
|
|
|
4821
5002
|
const removed = !fs2.existsSync(agent.jsonlFile);
|
|
4822
5003
|
if (removed || idle) {
|
|
4823
5004
|
const reason = removed ? "JSONL removed" : "idle timeout";
|
|
4824
|
-
|
|
5005
|
+
daemonLog(`Agent ${agentId}: ${reason}, closing`);
|
|
4825
5006
|
stopSessionWatcher(agentId, fileWatchers, pollingTimers, waitingTimers, permissionTimers);
|
|
4826
5007
|
agents.delete(agentId);
|
|
4827
5008
|
knownJsonlFiles.delete(agent.jsonlFile);
|
|
@@ -4837,6 +5018,7 @@ function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTime
|
|
|
4837
5018
|
}
|
|
4838
5019
|
|
|
4839
5020
|
// src/server.ts
|
|
5021
|
+
var PORT_RETRY_LIMIT = 10;
|
|
4840
5022
|
function createServer({ port, host, agents }) {
|
|
4841
5023
|
const clients = new Set;
|
|
4842
5024
|
function broadcast(msg) {
|
|
@@ -4847,60 +5029,602 @@ function createServer({ port, host, agents }) {
|
|
|
4847
5029
|
} catch {}
|
|
4848
5030
|
}
|
|
4849
5031
|
}
|
|
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
|
|
5032
|
+
function makeServerConfig(listenPort) {
|
|
5033
|
+
return {
|
|
5034
|
+
port: listenPort,
|
|
5035
|
+
hostname: host,
|
|
5036
|
+
fetch(req, server2) {
|
|
5037
|
+
const url = new URL(req.url);
|
|
5038
|
+
if (url.pathname === "/ws") {
|
|
5039
|
+
if (server2.upgrade(req))
|
|
5040
|
+
return;
|
|
5041
|
+
return new Response("WebSocket upgrade failed", { status: 400 });
|
|
5042
|
+
}
|
|
5043
|
+
if (url.pathname === "/health") {
|
|
5044
|
+
return Response.json({
|
|
5045
|
+
status: "ok",
|
|
5046
|
+
agents: agents.size
|
|
4886
5047
|
});
|
|
4887
5048
|
}
|
|
4888
|
-
|
|
5049
|
+
return new Response("Not found", { status: 404 });
|
|
4889
5050
|
},
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
5051
|
+
websocket: {
|
|
5052
|
+
open(ws) {
|
|
5053
|
+
clients.add(ws);
|
|
5054
|
+
daemonLog(`Client connected (${clients.size} total)`);
|
|
5055
|
+
const existingAgents = [];
|
|
5056
|
+
for (const [, agent] of agents) {
|
|
5057
|
+
const activeTools = [];
|
|
5058
|
+
for (const toolId of agent.activeToolIds) {
|
|
5059
|
+
activeTools.push({
|
|
5060
|
+
toolId,
|
|
5061
|
+
status: agent.activeToolStatuses.get(toolId) || ""
|
|
5062
|
+
});
|
|
5063
|
+
}
|
|
5064
|
+
existingAgents.push({
|
|
5065
|
+
id: agent.id,
|
|
5066
|
+
isWaiting: agent.isWaiting,
|
|
5067
|
+
permissionSent: agent.permissionSent,
|
|
5068
|
+
activeTools
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
ws.send(JSON.stringify({
|
|
5072
|
+
type: "existingAgents",
|
|
5073
|
+
agents: existingAgents
|
|
5074
|
+
}));
|
|
5075
|
+
},
|
|
5076
|
+
message(_ws, _message) {},
|
|
5077
|
+
close(ws) {
|
|
5078
|
+
clients.delete(ws);
|
|
5079
|
+
daemonLog(`Client disconnected (${clients.size} total)`);
|
|
5080
|
+
}
|
|
5081
|
+
}
|
|
5082
|
+
};
|
|
5083
|
+
}
|
|
5084
|
+
function tryServe(startPort) {
|
|
5085
|
+
let boundPort = startPort;
|
|
5086
|
+
for (let attempt = 0;attempt < PORT_RETRY_LIMIT; attempt++) {
|
|
5087
|
+
try {
|
|
5088
|
+
return Bun.serve(makeServerConfig(boundPort));
|
|
5089
|
+
} catch (err) {
|
|
5090
|
+
const isAddrInUse = err instanceof Error && (err.message.includes("EADDRINUSE") || err.message.includes("address already in use"));
|
|
5091
|
+
if (!isAddrInUse || attempt === PORT_RETRY_LIMIT - 1)
|
|
5092
|
+
throw err;
|
|
5093
|
+
boundPort++;
|
|
4894
5094
|
}
|
|
4895
5095
|
}
|
|
4896
|
-
|
|
4897
|
-
|
|
5096
|
+
throw new Error(`No available port after ${PORT_RETRY_LIMIT} attempts`);
|
|
5097
|
+
}
|
|
5098
|
+
const server = tryServe(port);
|
|
5099
|
+
if (server.port !== port) {
|
|
5100
|
+
daemonLog(`Port ${port} in use, using ${server.port} instead`);
|
|
5101
|
+
}
|
|
5102
|
+
daemonLog(`Listening on ws://${server.hostname}:${server.port}/ws`);
|
|
4898
5103
|
return {
|
|
4899
5104
|
broadcast,
|
|
5105
|
+
clientCount: () => clients.size,
|
|
5106
|
+
port: server.port,
|
|
4900
5107
|
stop: () => server.stop()
|
|
4901
5108
|
};
|
|
4902
5109
|
}
|
|
4903
5110
|
|
|
5111
|
+
// src/tui/terminal.ts
|
|
5112
|
+
var ALT_SCREEN_ON = "\x1B[?1049h";
|
|
5113
|
+
var ALT_SCREEN_OFF = "\x1B[?1049l";
|
|
5114
|
+
var CURSOR_HIDE = "\x1B[?25l";
|
|
5115
|
+
var CURSOR_SHOW = "\x1B[?25h";
|
|
5116
|
+
var CLEAR_SCREEN = "\x1B[2J";
|
|
5117
|
+
var RESET_ATTRS = "\x1B[0m";
|
|
5118
|
+
var resizeCallback = null;
|
|
5119
|
+
function onSigwinch() {
|
|
5120
|
+
resizeCallback?.();
|
|
5121
|
+
}
|
|
5122
|
+
function getTerminalSize() {
|
|
5123
|
+
return {
|
|
5124
|
+
rows: process.stdout.rows || 24,
|
|
5125
|
+
cols: process.stdout.columns || 80
|
|
5126
|
+
};
|
|
5127
|
+
}
|
|
5128
|
+
function enterAltScreen() {
|
|
5129
|
+
process.stdout.write(ALT_SCREEN_ON + CURSOR_HIDE + CLEAR_SCREEN);
|
|
5130
|
+
}
|
|
5131
|
+
function clearScreen() {
|
|
5132
|
+
process.stdout.write(CLEAR_SCREEN);
|
|
5133
|
+
}
|
|
5134
|
+
function exitAltScreen() {
|
|
5135
|
+
process.stdout.write(RESET_ATTRS + CURSOR_SHOW + ALT_SCREEN_OFF);
|
|
5136
|
+
}
|
|
5137
|
+
function onResize(callback) {
|
|
5138
|
+
resizeCallback = callback;
|
|
5139
|
+
process.on("SIGWINCH", onSigwinch);
|
|
5140
|
+
}
|
|
5141
|
+
function offResize() {
|
|
5142
|
+
resizeCallback = null;
|
|
5143
|
+
process.removeListener("SIGWINCH", onSigwinch);
|
|
5144
|
+
}
|
|
5145
|
+
function moveTo(row, col) {
|
|
5146
|
+
return `\x1B[${row};${col}H`;
|
|
5147
|
+
}
|
|
5148
|
+
|
|
5149
|
+
// src/tui/renderer.ts
|
|
5150
|
+
function createFrameBuffer(rows, cols) {
|
|
5151
|
+
const cells = [];
|
|
5152
|
+
for (let r = 0;r < rows; r++) {
|
|
5153
|
+
const row = [];
|
|
5154
|
+
for (let c = 0;c < cols; c++) {
|
|
5155
|
+
row.push({ char: " ", fg: "", bold: false, dim: false });
|
|
5156
|
+
}
|
|
5157
|
+
cells.push(row);
|
|
5158
|
+
}
|
|
5159
|
+
return { cells, rows, cols };
|
|
5160
|
+
}
|
|
5161
|
+
function clearBuffer(buf) {
|
|
5162
|
+
for (let r = 0;r < buf.rows; r++) {
|
|
5163
|
+
const row = buf.cells[r];
|
|
5164
|
+
for (let c = 0;c < buf.cols; c++) {
|
|
5165
|
+
const cell = row[c];
|
|
5166
|
+
cell.char = " ";
|
|
5167
|
+
cell.fg = "";
|
|
5168
|
+
cell.bold = false;
|
|
5169
|
+
cell.dim = false;
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
}
|
|
5173
|
+
function setCell(buf, row, col, char, fg, bold, dim) {
|
|
5174
|
+
if (row >= 0 && row < buf.rows && col >= 0 && col < buf.cols) {
|
|
5175
|
+
const cell = buf.cells[row][col];
|
|
5176
|
+
cell.char = char;
|
|
5177
|
+
cell.fg = fg;
|
|
5178
|
+
cell.bold = bold;
|
|
5179
|
+
cell.dim = dim;
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
function flushDiff(current, previous) {
|
|
5183
|
+
let out = "";
|
|
5184
|
+
let lastFg = "\x00";
|
|
5185
|
+
let lastBold = false;
|
|
5186
|
+
let lastDim = false;
|
|
5187
|
+
for (let r = 0;r < current.rows; r++) {
|
|
5188
|
+
const curRow = current.cells[r];
|
|
5189
|
+
const prevRow = previous?.cells[r];
|
|
5190
|
+
let runStart = -1;
|
|
5191
|
+
for (let c = 0;c <= current.cols; c++) {
|
|
5192
|
+
let changed = false;
|
|
5193
|
+
if (c < current.cols) {
|
|
5194
|
+
const cur = curRow[c];
|
|
5195
|
+
if (prevRow) {
|
|
5196
|
+
const prev = prevRow[c];
|
|
5197
|
+
if (cur.char === " " && cur.fg === "" && prev.char === " " && prev.fg === "") {
|
|
5198
|
+
if (runStart !== -1) {
|
|
5199
|
+
out += flushRun(curRow, r, runStart, c, lastFg, lastBold, lastDim);
|
|
5200
|
+
const last = curRow[c - 1];
|
|
5201
|
+
lastFg = last.fg;
|
|
5202
|
+
lastBold = last.bold;
|
|
5203
|
+
lastDim = last.dim;
|
|
5204
|
+
runStart = -1;
|
|
5205
|
+
}
|
|
5206
|
+
continue;
|
|
5207
|
+
}
|
|
5208
|
+
changed = cur.char !== prev.char || cur.fg !== prev.fg || cur.bold !== prev.bold || cur.dim !== prev.dim;
|
|
5209
|
+
} else {
|
|
5210
|
+
changed = cur.char !== " " || cur.fg !== "";
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
if (changed) {
|
|
5214
|
+
if (runStart === -1)
|
|
5215
|
+
runStart = c;
|
|
5216
|
+
} else if (runStart !== -1) {
|
|
5217
|
+
out += flushRun(curRow, r, runStart, c, lastFg, lastBold, lastDim);
|
|
5218
|
+
const last = curRow[c - 1];
|
|
5219
|
+
lastFg = last.fg;
|
|
5220
|
+
lastBold = last.bold;
|
|
5221
|
+
lastDim = last.dim;
|
|
5222
|
+
runStart = -1;
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
if (out.length > 0) {
|
|
5227
|
+
out += RESET;
|
|
5228
|
+
process.stdout.write(out);
|
|
5229
|
+
}
|
|
5230
|
+
}
|
|
5231
|
+
function flushRun(row, r, start, end, lastFg, lastBold, lastDim) {
|
|
5232
|
+
let s = moveTo(r + 1, start + 1);
|
|
5233
|
+
let fg = lastFg;
|
|
5234
|
+
let bold = lastBold;
|
|
5235
|
+
let dim = lastDim;
|
|
5236
|
+
for (let i = start;i < end; i++) {
|
|
5237
|
+
const cell = row[i];
|
|
5238
|
+
if (cell.fg !== fg || cell.bold !== bold || cell.dim !== dim) {
|
|
5239
|
+
s += RESET;
|
|
5240
|
+
if (cell.fg)
|
|
5241
|
+
s += cell.fg;
|
|
5242
|
+
if (cell.bold)
|
|
5243
|
+
s += BOLD;
|
|
5244
|
+
if (cell.dim)
|
|
5245
|
+
s += DIM;
|
|
5246
|
+
fg = cell.fg;
|
|
5247
|
+
bold = cell.bold;
|
|
5248
|
+
dim = cell.dim;
|
|
5249
|
+
}
|
|
5250
|
+
s += cell.char;
|
|
5251
|
+
}
|
|
5252
|
+
return s;
|
|
5253
|
+
}
|
|
5254
|
+
|
|
5255
|
+
// src/tui/panel.ts
|
|
5256
|
+
var glowPosition = 0;
|
|
5257
|
+
var GLOW_BRIGHT = fg256(46);
|
|
5258
|
+
var GLOW_MEDIUM = fg256(34);
|
|
5259
|
+
var GLOW_DIM = fg256(22);
|
|
5260
|
+
function computePanelGeometry(termRows, termCols) {
|
|
5261
|
+
const maxW = Math.min(PANEL_MAX_WIDTH, termCols - PANEL_MARGIN_X * 2);
|
|
5262
|
+
const maxH = Math.min(PANEL_MAX_HEIGHT, termRows - PANEL_MARGIN_Y * 2);
|
|
5263
|
+
if (maxW < PANEL_MIN_WIDTH || maxH < PANEL_MIN_HEIGHT) {
|
|
5264
|
+
return { x: 0, y: 0, width: 0, height: 0, logLines: 0, visible: false };
|
|
5265
|
+
}
|
|
5266
|
+
const width = maxW;
|
|
5267
|
+
const height = maxH;
|
|
5268
|
+
const x = Math.floor((termCols - width) / 2);
|
|
5269
|
+
const y = Math.floor((termRows - height) / 2);
|
|
5270
|
+
const logLines = height - PANEL_HEADER_HEIGHT - 1;
|
|
5271
|
+
return {
|
|
5272
|
+
x,
|
|
5273
|
+
y,
|
|
5274
|
+
width,
|
|
5275
|
+
height,
|
|
5276
|
+
logLines: Math.max(0, logLines),
|
|
5277
|
+
visible: true
|
|
5278
|
+
};
|
|
5279
|
+
}
|
|
5280
|
+
function borderColor(borderIdx, perimeterLen) {
|
|
5281
|
+
const dist = Math.abs(borderIdx - glowPosition);
|
|
5282
|
+
const wrappedDist = Math.min(dist, perimeterLen - dist);
|
|
5283
|
+
if (wrappedDist < 3)
|
|
5284
|
+
return GLOW_BRIGHT;
|
|
5285
|
+
if (wrappedDist < 6)
|
|
5286
|
+
return GLOW_MEDIUM;
|
|
5287
|
+
return GLOW_DIM;
|
|
5288
|
+
}
|
|
5289
|
+
function writeString(buf, row, col, str, fg, bold, dim) {
|
|
5290
|
+
for (let i = 0;i < str.length; i++) {
|
|
5291
|
+
setCell(buf, row, col + i, str[i], fg, bold, dim);
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
function renderPanel(buf, panel, logs, agentCount, clientCount, version2, webUrl) {
|
|
5295
|
+
if (!panel.visible)
|
|
5296
|
+
return;
|
|
5297
|
+
const { x, y, width, height } = panel;
|
|
5298
|
+
const innerWidth = width - 2;
|
|
5299
|
+
const perimeter = 2 * (width + height) - 4;
|
|
5300
|
+
glowPosition = (glowPosition + BORDER_GLOW_SPEED) % perimeter;
|
|
5301
|
+
let borderIdx = 0;
|
|
5302
|
+
setCell(buf, y, x, BOX.topLeft, borderColor(borderIdx++, perimeter), false, false);
|
|
5303
|
+
for (let c = 1;c < width - 1; c++) {
|
|
5304
|
+
setCell(buf, y, x + c, BOX.horizontal, borderColor(borderIdx++, perimeter), false, false);
|
|
5305
|
+
}
|
|
5306
|
+
setCell(buf, y, x + width - 1, BOX.topRight, borderColor(borderIdx++, perimeter), false, false);
|
|
5307
|
+
for (let r = 1;r < height - 1; r++) {
|
|
5308
|
+
setCell(buf, y + r, x + width - 1, BOX.vertical, borderColor(borderIdx++, perimeter), false, false);
|
|
5309
|
+
}
|
|
5310
|
+
setCell(buf, y + height - 1, x + width - 1, BOX.bottomRight, borderColor(borderIdx++, perimeter), false, false);
|
|
5311
|
+
for (let c = width - 2;c > 0; c--) {
|
|
5312
|
+
setCell(buf, y + height - 1, x + c, BOX.horizontal, borderColor(borderIdx++, perimeter), false, false);
|
|
5313
|
+
}
|
|
5314
|
+
setCell(buf, y + height - 1, x, BOX.bottomLeft, borderColor(borderIdx++, perimeter), false, false);
|
|
5315
|
+
for (let r = height - 2;r > 0; r--) {
|
|
5316
|
+
setCell(buf, y + r, x, BOX.vertical, borderColor(borderIdx++, perimeter), false, false);
|
|
5317
|
+
}
|
|
5318
|
+
for (let r = 1;r < height - 1; r++) {
|
|
5319
|
+
for (let c = 1;c < width - 1; c++) {
|
|
5320
|
+
setCell(buf, y + r, x + c, " ", "", false, false);
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
const logoStartRow = y + 1;
|
|
5324
|
+
for (let i = 0;i < LOGO_LINES.length && i < height - 2; i++) {
|
|
5325
|
+
const line = LOGO_LINES[i];
|
|
5326
|
+
const maxChars = Math.min(line.length, innerWidth - 1);
|
|
5327
|
+
for (let c = 0;c < maxChars; c++) {
|
|
5328
|
+
setCell(buf, logoStartRow + i, x + 2 + c, line[c], BRIGHT_GREEN_FG, true, false);
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
if (version2 && LOGO_LINES.length >= 3) {
|
|
5332
|
+
const vStr = `v${version2}`;
|
|
5333
|
+
const maxLen = innerWidth - (LOGO_LINES[2]?.length || 0) - 3;
|
|
5334
|
+
if (maxLen > 4) {
|
|
5335
|
+
const trimmed = vStr.slice(0, maxLen);
|
|
5336
|
+
const col = x + width - 2 - trimmed.length;
|
|
5337
|
+
writeString(buf, logoStartRow + 2, col, trimmed, CYAN_FG, false, true);
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
if (LOGO_LINES.length >= 4) {
|
|
5341
|
+
const maxLen = innerWidth - (LOGO_LINES[3]?.length || 0) - 3;
|
|
5342
|
+
if (maxLen > 10) {
|
|
5343
|
+
const trimmed = webUrl.slice(0, maxLen);
|
|
5344
|
+
const col = x + width - 2 - trimmed.length;
|
|
5345
|
+
writeString(buf, logoStartRow + 3, col, trimmed, CYAN_FG, false, true);
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
if (LOGO_LINES.length >= 5) {
|
|
5349
|
+
const countStr = `${agentCount} agent${agentCount !== 1 ? "s" : ""} \xB7 ${clientCount} client${clientCount !== 1 ? "s" : ""}`;
|
|
5350
|
+
const statusStr = `\u2713 ${countStr}`;
|
|
5351
|
+
const maxCountLen = innerWidth - (LOGO_LINES[4]?.length || 0) - 3;
|
|
5352
|
+
if (maxCountLen > 10) {
|
|
5353
|
+
const trimmed = statusStr.slice(0, maxCountLen);
|
|
5354
|
+
const countCol = x + width - 2 - trimmed.length;
|
|
5355
|
+
writeString(buf, logoStartRow + 4, countCol, trimmed, BRIGHT_GREEN_FG, false, false);
|
|
5356
|
+
}
|
|
5357
|
+
}
|
|
5358
|
+
const sepRow = y + LOGO_LINES.length + 1;
|
|
5359
|
+
if (sepRow < y + height - 1) {
|
|
5360
|
+
const sepBorderIdx = Math.floor(glowPosition) % perimeter;
|
|
5361
|
+
setCell(buf, sepRow, x, BOX.teeLeft, borderColor(sepBorderIdx, perimeter), false, false);
|
|
5362
|
+
for (let c = 1;c < width - 1; c++) {
|
|
5363
|
+
setCell(buf, sepRow, x + c, BOX.horizontal, borderColor((sepBorderIdx + c) % perimeter, perimeter), false, false);
|
|
5364
|
+
}
|
|
5365
|
+
setCell(buf, sepRow, x + width - 1, BOX.teeRight, borderColor((sepBorderIdx + width - 1) % perimeter, perimeter), false, false);
|
|
5366
|
+
}
|
|
5367
|
+
const logStartRow = sepRow + 1;
|
|
5368
|
+
const visibleLogs = logs.slice(-panel.logLines);
|
|
5369
|
+
for (let i = 0;i < visibleLogs.length; i++) {
|
|
5370
|
+
const row = logStartRow + i;
|
|
5371
|
+
if (row >= y + height - 1)
|
|
5372
|
+
break;
|
|
5373
|
+
const entry = visibleLogs[i];
|
|
5374
|
+
writeString(buf, row, x + 2, entry.timestamp, CYAN_FG, false, true);
|
|
5375
|
+
const msgCol = x + 2 + entry.timestamp.length + 1;
|
|
5376
|
+
const maxMsgLen = innerWidth - entry.timestamp.length - 3;
|
|
5377
|
+
const msg = entry.message.slice(0, Math.max(0, maxMsgLen));
|
|
5378
|
+
writeString(buf, row, msgCol, msg, entry.color, false, false);
|
|
5379
|
+
}
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5382
|
+
// src/tui/rain.ts
|
|
5383
|
+
var ACTIVE_LAYERS = ["mid", "front"];
|
|
5384
|
+
var MAX_DROPS_PER_COLUMN = 3;
|
|
5385
|
+
var RAIN_CHAR_COUNT = RAIN_CHARS.length;
|
|
5386
|
+
function randomChar() {
|
|
5387
|
+
return RAIN_CHARS[Math.random() * RAIN_CHAR_COUNT | 0];
|
|
5388
|
+
}
|
|
5389
|
+
function createRainLayers(cols) {
|
|
5390
|
+
return ACTIVE_LAYERS.map((name) => {
|
|
5391
|
+
const columns = [];
|
|
5392
|
+
for (let c = 0;c < cols; c++) {
|
|
5393
|
+
columns.push({ drops: [] });
|
|
5394
|
+
}
|
|
5395
|
+
return { name, columns };
|
|
5396
|
+
});
|
|
5397
|
+
}
|
|
5398
|
+
function resizeRainLayers(layers, cols) {
|
|
5399
|
+
for (const layer of layers) {
|
|
5400
|
+
while (layer.columns.length < cols) {
|
|
5401
|
+
layer.columns.push({ drops: [] });
|
|
5402
|
+
}
|
|
5403
|
+
if (layer.columns.length > cols) {
|
|
5404
|
+
layer.columns.length = cols;
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
function spawnDrop(layerName, rows) {
|
|
5409
|
+
const cfg = LAYERS[layerName];
|
|
5410
|
+
const trailLen = cfg.trailMin + (Math.random() * (cfg.trailMax - cfg.trailMin + 1) | 0);
|
|
5411
|
+
const chars = [];
|
|
5412
|
+
for (let i = 0;i < trailLen; i++) {
|
|
5413
|
+
chars.push(randomChar());
|
|
5414
|
+
}
|
|
5415
|
+
return {
|
|
5416
|
+
y: -(Math.random() * rows * 0.3 | 0),
|
|
5417
|
+
speed: cfg.speedMin + Math.random() * (cfg.speedMax - cfg.speedMin),
|
|
5418
|
+
trailLen,
|
|
5419
|
+
chars,
|
|
5420
|
+
frameCount: 0,
|
|
5421
|
+
isLightning: false
|
|
5422
|
+
};
|
|
5423
|
+
}
|
|
5424
|
+
function spawnLightning() {
|
|
5425
|
+
const chars = [];
|
|
5426
|
+
for (let i = 0;i < LIGHTNING_TRAIL_LEN; i++) {
|
|
5427
|
+
chars.push(randomChar());
|
|
5428
|
+
}
|
|
5429
|
+
return {
|
|
5430
|
+
y: -(Math.random() * 3 | 0),
|
|
5431
|
+
speed: LIGHTNING_SPEED_MIN + Math.random() * (LIGHTNING_SPEED_MAX - LIGHTNING_SPEED_MIN),
|
|
5432
|
+
trailLen: LIGHTNING_TRAIL_LEN,
|
|
5433
|
+
chars,
|
|
5434
|
+
frameCount: 0,
|
|
5435
|
+
isLightning: true
|
|
5436
|
+
};
|
|
5437
|
+
}
|
|
5438
|
+
function tickRain(layers, rows) {
|
|
5439
|
+
for (const layer of layers) {
|
|
5440
|
+
const cfg = LAYERS[layer.name];
|
|
5441
|
+
const isFront = layer.name === "front";
|
|
5442
|
+
for (let col = 0;col < layer.columns.length; col++) {
|
|
5443
|
+
const column = layer.columns[col];
|
|
5444
|
+
const drops = column.drops;
|
|
5445
|
+
if (drops.length < MAX_DROPS_PER_COLUMN && Math.random() < cfg.spawnRate) {
|
|
5446
|
+
drops.push(spawnDrop(layer.name, rows));
|
|
5447
|
+
}
|
|
5448
|
+
if (isFront && drops.length < MAX_DROPS_PER_COLUMN && Math.random() < LIGHTNING_CHANCE) {
|
|
5449
|
+
drops.push(spawnLightning());
|
|
5450
|
+
}
|
|
5451
|
+
let write = 0;
|
|
5452
|
+
for (let d = 0;d < drops.length; d++) {
|
|
5453
|
+
const drop = drops[d];
|
|
5454
|
+
drop.y += drop.speed;
|
|
5455
|
+
drop.frameCount++;
|
|
5456
|
+
if (drop.y - drop.trailLen > rows)
|
|
5457
|
+
continue;
|
|
5458
|
+
const mutInterval = drop.isLightning ? 3 : cfg.mutationInterval;
|
|
5459
|
+
if (drop.frameCount % mutInterval === 0 && drop.chars.length > 1) {
|
|
5460
|
+
const idx = 1 + (Math.random() * (drop.chars.length - 1) | 0);
|
|
5461
|
+
drop.chars[idx] = randomChar();
|
|
5462
|
+
}
|
|
5463
|
+
drop.chars[0] = randomChar();
|
|
5464
|
+
drops[write++] = drop;
|
|
5465
|
+
}
|
|
5466
|
+
drops.length = write;
|
|
5467
|
+
}
|
|
5468
|
+
}
|
|
5469
|
+
}
|
|
5470
|
+
var GRADIENT_STRINGS = [];
|
|
5471
|
+
for (let offset = 0;offset <= 4; offset++) {
|
|
5472
|
+
const arr = [];
|
|
5473
|
+
for (let g = 0;g < TRAIL_GRADIENT.length; g++) {
|
|
5474
|
+
const idx = Math.min(TRAIL_GRADIENT.length - 1, Math.max(0, g + offset));
|
|
5475
|
+
arr.push(fg256(TRAIL_GRADIENT[idx]));
|
|
5476
|
+
}
|
|
5477
|
+
GRADIENT_STRINGS.push(arr);
|
|
5478
|
+
}
|
|
5479
|
+
var LIGHTNING_HEAD = fg256(231);
|
|
5480
|
+
var LIGHTNING_NEAR = fg256(159);
|
|
5481
|
+
var LIGHTNING_TAIL = fg256(49);
|
|
5482
|
+
function renderRain(layers, buf, panel) {
|
|
5483
|
+
const bufRows = buf.rows;
|
|
5484
|
+
const bufCols = buf.cols;
|
|
5485
|
+
const panelVisible = panel?.visible ?? false;
|
|
5486
|
+
const panelX = panel?.x ?? 0;
|
|
5487
|
+
const panelY = panel?.y ?? 0;
|
|
5488
|
+
const panelX2 = panelX + (panel?.width ?? 0);
|
|
5489
|
+
const panelY2 = panelY + (panel?.height ?? 0);
|
|
5490
|
+
const gradLen = TRAIL_GRADIENT.length - 1;
|
|
5491
|
+
for (const layer of layers) {
|
|
5492
|
+
const brightnessOffset = LAYERS[layer.name].brightnessOffset;
|
|
5493
|
+
const gradStrs = GRADIENT_STRINGS[brightnessOffset];
|
|
5494
|
+
const colCount = Math.min(layer.columns.length, bufCols);
|
|
5495
|
+
for (let col = 0;col < colCount; col++) {
|
|
5496
|
+
if (panelVisible && col >= panelX && col < panelX2) {
|
|
5497
|
+
const drops = layer.columns[col].drops;
|
|
5498
|
+
for (let d = 0;d < drops.length; d++) {
|
|
5499
|
+
const drop = drops[d];
|
|
5500
|
+
const headRow = drop.y | 0;
|
|
5501
|
+
const trailLen = drop.trailLen;
|
|
5502
|
+
for (let i = 0;i < trailLen; i++) {
|
|
5503
|
+
const row = headRow - i;
|
|
5504
|
+
if (row < 0 || row >= bufRows)
|
|
5505
|
+
continue;
|
|
5506
|
+
if (row >= panelY && row < panelY2)
|
|
5507
|
+
continue;
|
|
5508
|
+
let color;
|
|
5509
|
+
if (drop.isLightning) {
|
|
5510
|
+
color = i === 0 ? LIGHTNING_HEAD : i === 1 ? LIGHTNING_NEAR : LIGHTNING_TAIL;
|
|
5511
|
+
} else {
|
|
5512
|
+
const gradIdx = trailLen > 1 ? i * gradLen / (trailLen - 1) | 0 : 0;
|
|
5513
|
+
color = gradStrs[Math.min(gradIdx, gradLen)];
|
|
5514
|
+
}
|
|
5515
|
+
const bold = i === 0 || drop.isLightning;
|
|
5516
|
+
const dim = !drop.isLightning && i > trailLen * 0.7;
|
|
5517
|
+
setCell(buf, row, col, drop.chars[i], color, bold, dim);
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
} else {
|
|
5521
|
+
const drops = layer.columns[col].drops;
|
|
5522
|
+
for (let d = 0;d < drops.length; d++) {
|
|
5523
|
+
const drop = drops[d];
|
|
5524
|
+
const headRow = drop.y | 0;
|
|
5525
|
+
const trailLen = drop.trailLen;
|
|
5526
|
+
for (let i = 0;i < trailLen; i++) {
|
|
5527
|
+
const row = headRow - i;
|
|
5528
|
+
if (row < 0 || row >= bufRows)
|
|
5529
|
+
continue;
|
|
5530
|
+
let color;
|
|
5531
|
+
if (drop.isLightning) {
|
|
5532
|
+
color = i === 0 ? LIGHTNING_HEAD : i === 1 ? LIGHTNING_NEAR : LIGHTNING_TAIL;
|
|
5533
|
+
} else {
|
|
5534
|
+
const gradIdx = trailLen > 1 ? i * gradLen / (trailLen - 1) | 0 : 0;
|
|
5535
|
+
color = gradStrs[Math.min(gradIdx, gradLen)];
|
|
5536
|
+
}
|
|
5537
|
+
const bold = i === 0 || drop.isLightning;
|
|
5538
|
+
const dim = !drop.isLightning && i > trailLen * 0.7;
|
|
5539
|
+
setCell(buf, row, col, drop.chars[i], color, bold, dim);
|
|
5540
|
+
}
|
|
5541
|
+
}
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
|
|
5547
|
+
// src/tui/index.ts
|
|
5548
|
+
var running = false;
|
|
5549
|
+
var renderTimer = null;
|
|
5550
|
+
var bufA = null;
|
|
5551
|
+
var bufB = null;
|
|
5552
|
+
var currentIsA = true;
|
|
5553
|
+
var hasPrevious = false;
|
|
5554
|
+
var rainLayers = [];
|
|
5555
|
+
var panel = {
|
|
5556
|
+
x: 0,
|
|
5557
|
+
y: 0,
|
|
5558
|
+
width: 0,
|
|
5559
|
+
height: 0,
|
|
5560
|
+
logLines: 0,
|
|
5561
|
+
visible: false
|
|
5562
|
+
};
|
|
5563
|
+
var tuiOptions = null;
|
|
5564
|
+
function handleResize() {
|
|
5565
|
+
const { rows, cols } = getTerminalSize();
|
|
5566
|
+
clearScreen();
|
|
5567
|
+
bufA = createFrameBuffer(rows, cols);
|
|
5568
|
+
bufB = createFrameBuffer(rows, cols);
|
|
5569
|
+
currentIsA = true;
|
|
5570
|
+
hasPrevious = false;
|
|
5571
|
+
resizeRainLayers(rainLayers, cols);
|
|
5572
|
+
panel = computePanelGeometry(rows, cols);
|
|
5573
|
+
}
|
|
5574
|
+
function scheduleFrame() {
|
|
5575
|
+
if (!running)
|
|
5576
|
+
return;
|
|
5577
|
+
renderTimer = setTimeout(renderFrame, FRAME_INTERVAL_MS);
|
|
5578
|
+
}
|
|
5579
|
+
function renderFrame() {
|
|
5580
|
+
if (!running || !bufA || !bufB || !tuiOptions)
|
|
5581
|
+
return;
|
|
5582
|
+
const current = currentIsA ? bufA : bufB;
|
|
5583
|
+
const previous = currentIsA ? bufB : bufA;
|
|
5584
|
+
tickRain(rainLayers, current.rows);
|
|
5585
|
+
clearBuffer(current);
|
|
5586
|
+
renderRain(rainLayers, current, panel);
|
|
5587
|
+
renderPanel(current, panel, getLogEntries(), tuiOptions.agentCount(), tuiOptions.clientCount(), tuiOptions.version, tuiOptions.webUrl);
|
|
5588
|
+
flushDiff(current, hasPrevious ? previous : null);
|
|
5589
|
+
currentIsA = !currentIsA;
|
|
5590
|
+
hasPrevious = true;
|
|
5591
|
+
scheduleFrame();
|
|
5592
|
+
}
|
|
5593
|
+
function startTui(options) {
|
|
5594
|
+
if (running)
|
|
5595
|
+
return;
|
|
5596
|
+
running = true;
|
|
5597
|
+
tuiOptions = options;
|
|
5598
|
+
enterAltScreen();
|
|
5599
|
+
const { rows, cols } = getTerminalSize();
|
|
5600
|
+
bufA = createFrameBuffer(rows, cols);
|
|
5601
|
+
bufB = createFrameBuffer(rows, cols);
|
|
5602
|
+
currentIsA = true;
|
|
5603
|
+
hasPrevious = false;
|
|
5604
|
+
rainLayers = createRainLayers(cols);
|
|
5605
|
+
panel = computePanelGeometry(rows, cols);
|
|
5606
|
+
captureConsole();
|
|
5607
|
+
onResize(handleResize);
|
|
5608
|
+
scheduleFrame();
|
|
5609
|
+
daemonLog("TUI started");
|
|
5610
|
+
}
|
|
5611
|
+
function stopTui() {
|
|
5612
|
+
if (!running)
|
|
5613
|
+
return;
|
|
5614
|
+
running = false;
|
|
5615
|
+
if (renderTimer) {
|
|
5616
|
+
clearTimeout(renderTimer);
|
|
5617
|
+
renderTimer = null;
|
|
5618
|
+
}
|
|
5619
|
+
offResize();
|
|
5620
|
+
restoreConsole();
|
|
5621
|
+
exitAltScreen();
|
|
5622
|
+
bufA = null;
|
|
5623
|
+
bufB = null;
|
|
5624
|
+
hasPrevious = false;
|
|
5625
|
+
tuiOptions = null;
|
|
5626
|
+
}
|
|
5627
|
+
|
|
4904
5628
|
// src/updater.ts
|
|
4905
5629
|
var PACKAGE_NAME = "@bulletproof-sh/ctrl-daemon";
|
|
4906
5630
|
var NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
@@ -4969,6 +5693,7 @@ Options:
|
|
|
4969
5693
|
--host <address> Host/address to bind to (default: 0.0.0.0)
|
|
4970
5694
|
--project-dir <path> Watch a single project directory
|
|
4971
5695
|
--idle-timeout <minutes> Agent idle timeout in minutes (default: 15)
|
|
5696
|
+
--no-tui Disable Matrix rain TUI
|
|
4972
5697
|
--help, -h Show this help message
|
|
4973
5698
|
|
|
4974
5699
|
Without --project-dir, watches ALL projects in ~/.claude/projects/.
|
|
@@ -4980,6 +5705,7 @@ function parseArgs() {
|
|
|
4980
5705
|
let port = 3001;
|
|
4981
5706
|
let host = "0.0.0.0";
|
|
4982
5707
|
let idleTimeoutMs = 15 * 60 * 1000;
|
|
5708
|
+
let noTui = false;
|
|
4983
5709
|
for (let i = 0;i < args.length; i++) {
|
|
4984
5710
|
if (args[i] === "--help" || args[i] === "-h") {
|
|
4985
5711
|
printUsage();
|
|
@@ -4992,9 +5718,11 @@ function parseArgs() {
|
|
|
4992
5718
|
host = args[++i];
|
|
4993
5719
|
} else if (args[i] === "--idle-timeout" && args[i + 1]) {
|
|
4994
5720
|
idleTimeoutMs = Number.parseInt(args[++i], 10) * 60 * 1000;
|
|
5721
|
+
} else if (args[i] === "--no-tui") {
|
|
5722
|
+
noTui = true;
|
|
4995
5723
|
}
|
|
4996
5724
|
}
|
|
4997
|
-
return { projectDir, port, host, idleTimeoutMs };
|
|
5725
|
+
return { projectDir, port, host, idleTimeoutMs, noTui };
|
|
4998
5726
|
}
|
|
4999
5727
|
function resolveProjectsRoot(claudeHome) {
|
|
5000
5728
|
const home = claudeHome || path3.join(process.env.HOME || "~", ".claude");
|
|
@@ -5010,19 +5738,22 @@ async function main() {
|
|
|
5010
5738
|
]);
|
|
5011
5739
|
stopSpinner();
|
|
5012
5740
|
process.on("uncaughtException", async (err) => {
|
|
5741
|
+
stopTui();
|
|
5013
5742
|
console.error("[ctrl-daemon] Uncaught exception:", err);
|
|
5014
5743
|
trackException(err, { ...systemInfo, crash_type: "uncaughtException" });
|
|
5015
5744
|
await shutdownAnalytics();
|
|
5016
5745
|
process.exit(1);
|
|
5017
5746
|
});
|
|
5018
5747
|
process.on("unhandledRejection", async (reason) => {
|
|
5748
|
+
stopTui();
|
|
5019
5749
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
5020
5750
|
console.error("[ctrl-daemon] Unhandled rejection:", err);
|
|
5021
5751
|
trackException(err, { ...systemInfo, crash_type: "unhandledRejection" });
|
|
5022
5752
|
await shutdownAnalytics();
|
|
5023
5753
|
process.exit(1);
|
|
5024
5754
|
});
|
|
5025
|
-
const { projectDir, port, host, idleTimeoutMs } = parseArgs();
|
|
5755
|
+
const { projectDir, port, host, idleTimeoutMs, noTui } = parseArgs();
|
|
5756
|
+
const useTui = !noTui && process.stdout.isTTY;
|
|
5026
5757
|
const claudeHome = process.env.CLAUDE_HOME;
|
|
5027
5758
|
let scanDirs;
|
|
5028
5759
|
if (projectDir) {
|
|
@@ -5038,18 +5769,41 @@ async function main() {
|
|
|
5038
5769
|
const waitingTimers = new Map;
|
|
5039
5770
|
const permissionTimers = new Map;
|
|
5040
5771
|
const server = createServer({ port, host, agents });
|
|
5041
|
-
|
|
5772
|
+
const boundPort = server.port;
|
|
5773
|
+
printReady(boundPort, host, version2, updateMsg);
|
|
5774
|
+
const rawBroadcast = server.broadcast;
|
|
5775
|
+
const broadcast = (msg) => {
|
|
5776
|
+
rawBroadcast(msg);
|
|
5777
|
+
onBroadcast(msg);
|
|
5778
|
+
};
|
|
5779
|
+
if (useTui) {
|
|
5780
|
+
startTui({
|
|
5781
|
+
version: version2,
|
|
5782
|
+
port: boundPort,
|
|
5783
|
+
webUrl: buildWebUrl(boundPort, host),
|
|
5784
|
+
agentCount: () => agents.size,
|
|
5785
|
+
clientCount: () => server.clientCount()
|
|
5786
|
+
});
|
|
5787
|
+
}
|
|
5042
5788
|
trackEvent("daemon_started", {
|
|
5043
5789
|
version: version2 ?? "unknown",
|
|
5044
|
-
port,
|
|
5790
|
+
port: boundPort,
|
|
5045
5791
|
host,
|
|
5046
5792
|
mode: projectDir ? "single" : "all",
|
|
5047
5793
|
...systemInfo,
|
|
5048
5794
|
...analyticsConfig
|
|
5049
5795
|
});
|
|
5050
5796
|
const scanAll = !projectDir;
|
|
5051
|
-
const scanner = startProjectScanner(scanDirs[0], scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers,
|
|
5797
|
+
const scanner = startProjectScanner(scanDirs[0], scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast, idleTimeoutMs);
|
|
5798
|
+
const updateCheckTimer = setInterval(async () => {
|
|
5799
|
+
const msg = await checkForUpdate();
|
|
5800
|
+
if (msg) {
|
|
5801
|
+
daemonLog(`Update available! ${msg.replace(/\n\s*/g, " ")}`, "\x1B[33m");
|
|
5802
|
+
}
|
|
5803
|
+
}, UPDATE_CHECK_INTERVAL_MS);
|
|
5052
5804
|
async function shutdown() {
|
|
5805
|
+
stopTui();
|
|
5806
|
+
clearInterval(updateCheckTimer);
|
|
5053
5807
|
console.log(`
|
|
5054
5808
|
[ctrl-daemon] Shutting down...`);
|
|
5055
5809
|
scanner.stop();
|