@bulletproof-sh/ctrl-daemon 0.0.9 → 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 +128 -120
- 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,101 +4756,6 @@ function processProgressRecord(agentId, record, agents, waitingTimers, permissio
|
|
|
4660
4756
|
}
|
|
4661
4757
|
}
|
|
4662
4758
|
}
|
|
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
4759
|
// src/tui/logSink.ts
|
|
4759
4760
|
var logBuffer = [];
|
|
4760
4761
|
var tuiActive = false;
|
|
@@ -5290,7 +5291,7 @@ function writeString(buf, row, col, str, fg, bold, dim) {
|
|
|
5290
5291
|
setCell(buf, row, col + i, str[i], fg, bold, dim);
|
|
5291
5292
|
}
|
|
5292
5293
|
}
|
|
5293
|
-
function renderPanel(buf, panel, logs, agentCount, clientCount, version2,
|
|
5294
|
+
function renderPanel(buf, panel, logs, agentCount, clientCount, version2, webUrl) {
|
|
5294
5295
|
if (!panel.visible)
|
|
5295
5296
|
return;
|
|
5296
5297
|
const { x, y, width, height } = panel;
|
|
@@ -5327,13 +5328,21 @@ function renderPanel(buf, panel, logs, agentCount, clientCount, version2, wsUrl)
|
|
|
5327
5328
|
setCell(buf, logoStartRow + i, x + 2 + c, line[c], BRIGHT_GREEN_FG, true, false);
|
|
5328
5329
|
}
|
|
5329
5330
|
}
|
|
5330
|
-
if (LOGO_LINES.length >= 3) {
|
|
5331
|
-
const
|
|
5332
|
-
const
|
|
5333
|
-
if (
|
|
5334
|
-
const trimmed =
|
|
5335
|
-
const
|
|
5336
|
-
writeString(buf, logoStartRow + 2,
|
|
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);
|
|
5337
5346
|
}
|
|
5338
5347
|
}
|
|
5339
5348
|
if (LOGO_LINES.length >= 5) {
|
|
@@ -5575,8 +5584,7 @@ function renderFrame() {
|
|
|
5575
5584
|
tickRain(rainLayers, current.rows);
|
|
5576
5585
|
clearBuffer(current);
|
|
5577
5586
|
renderRain(rainLayers, current, panel);
|
|
5578
|
-
|
|
5579
|
-
renderPanel(current, panel, getLogEntries(), tuiOptions.agentCount(), tuiOptions.clientCount(), tuiOptions.version, wsUrl);
|
|
5587
|
+
renderPanel(current, panel, getLogEntries(), tuiOptions.agentCount(), tuiOptions.clientCount(), tuiOptions.version, tuiOptions.webUrl);
|
|
5580
5588
|
flushDiff(current, hasPrevious ? previous : null);
|
|
5581
5589
|
currentIsA = !currentIsA;
|
|
5582
5590
|
hasPrevious = true;
|
|
@@ -5762,7 +5770,7 @@ async function main() {
|
|
|
5762
5770
|
const permissionTimers = new Map;
|
|
5763
5771
|
const server = createServer({ port, host, agents });
|
|
5764
5772
|
const boundPort = server.port;
|
|
5765
|
-
printReady(boundPort, version2, updateMsg);
|
|
5773
|
+
printReady(boundPort, host, version2, updateMsg);
|
|
5766
5774
|
const rawBroadcast = server.broadcast;
|
|
5767
5775
|
const broadcast = (msg) => {
|
|
5768
5776
|
rawBroadcast(msg);
|
|
@@ -5772,7 +5780,7 @@ async function main() {
|
|
|
5772
5780
|
startTui({
|
|
5773
5781
|
version: version2,
|
|
5774
5782
|
port: boundPort,
|
|
5775
|
-
host,
|
|
5783
|
+
webUrl: buildWebUrl(boundPort, host),
|
|
5776
5784
|
agentCount: () => agents.size,
|
|
5777
5785
|
clientCount: () => server.clientCount()
|
|
5778
5786
|
});
|