@bulletproof-sh/ctrl-daemon 0.0.6 → 0.0.8
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 +17 -0
- package/dist/index.js +92 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,10 +26,27 @@ npx @bulletproof-sh/ctrl-daemon --port 3001 --host 127.0.0.1
|
|
|
26
26
|
| `--port <number>` | `3001` | Port to listen on |
|
|
27
27
|
| `--host <address>` | `0.0.0.0` | Host/address to bind to |
|
|
28
28
|
| `--project-dir <path>` | — | Watch a single project; omit to watch all projects |
|
|
29
|
+
| `--idle-timeout <minutes>` | `15` | Agent idle timeout in minutes |
|
|
29
30
|
| `--help`, `-h` | — | Print usage |
|
|
30
31
|
|
|
31
32
|
Without `--project-dir`, the daemon scans `~/.claude/projects/` and watches every session it finds there.
|
|
32
33
|
|
|
34
|
+
## Sharing your office
|
|
35
|
+
|
|
36
|
+
Run the daemon and an [ngrok](https://ngrok.com) tunnel in parallel to share a live view with anyone:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
# Terminal 1 — start the daemon
|
|
40
|
+
npx @bulletproof-sh/ctrl-daemon
|
|
41
|
+
|
|
42
|
+
# Terminal 2 — expose it publicly
|
|
43
|
+
ngrok http 3001
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then open [bulletproof.sh](https://bulletproof.sh), go to **Settings → Share Office** and copy the generated link. Anyone who opens it will see your agents in real time — no auth or setup required on their end.
|
|
47
|
+
|
|
48
|
+
> **Note:** Free ngrok URLs rotate each time you restart the tunnel. A paid ngrok plan gives you a stable domain.
|
|
49
|
+
|
|
33
50
|
## WebSocket API
|
|
34
51
|
|
|
35
52
|
Connect to `ws://localhost:3001/ws`. The daemon broadcasts JSON messages whenever agent state changes — the same message format used by the VS Code extension's internal webview protocol.
|
package/dist/index.js
CHANGED
|
@@ -4245,6 +4245,70 @@ async function shutdownAnalytics() {
|
|
|
4245
4245
|
await client.shutdown();
|
|
4246
4246
|
}
|
|
4247
4247
|
|
|
4248
|
+
// src/banner.ts
|
|
4249
|
+
var BG = "\x1B[92m";
|
|
4250
|
+
var CY = "\x1B[36m";
|
|
4251
|
+
var YL = "\x1B[33m";
|
|
4252
|
+
var DM = "\x1B[2m";
|
|
4253
|
+
var BD = "\x1B[1m";
|
|
4254
|
+
var RS = "\x1B[0m";
|
|
4255
|
+
var LOGO_LINES = [
|
|
4256
|
+
" \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 ",
|
|
4257
|
+
" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 ",
|
|
4258
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 ",
|
|
4259
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 ",
|
|
4260
|
+
" \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
|
|
4261
|
+
" \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
|
|
4262
|
+
];
|
|
4263
|
+
function printBanner() {
|
|
4264
|
+
process.stdout.write(`
|
|
4265
|
+
`);
|
|
4266
|
+
for (const line of LOGO_LINES) {
|
|
4267
|
+
process.stdout.write(`${BD}${BG}${line}${RS}
|
|
4268
|
+
`);
|
|
4269
|
+
}
|
|
4270
|
+
process.stdout.write(`
|
|
4271
|
+
`);
|
|
4272
|
+
}
|
|
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
|
+
];
|
|
4285
|
+
function startSpinner(text) {
|
|
4286
|
+
let i = 0;
|
|
4287
|
+
const clearWidth = text.length + 4;
|
|
4288
|
+
const timer = setInterval(() => {
|
|
4289
|
+
process.stdout.write(`\r${CY}${SPINNER_FRAMES[i++ % SPINNER_FRAMES.length]}${RS} ${text}`);
|
|
4290
|
+
}, 80);
|
|
4291
|
+
return () => {
|
|
4292
|
+
clearInterval(timer);
|
|
4293
|
+
process.stdout.write(`\r${" ".repeat(clearWidth)}\r`);
|
|
4294
|
+
};
|
|
4295
|
+
}
|
|
4296
|
+
function printReady(port, version2, updateMsg) {
|
|
4297
|
+
const versionStr = version2 ? ` ${DM}v${version2}${RS}` : "";
|
|
4298
|
+
if (updateMsg) {
|
|
4299
|
+
process.stdout.write(`
|
|
4300
|
+
${YL}${updateMsg}${RS}
|
|
4301
|
+
|
|
4302
|
+
`);
|
|
4303
|
+
}
|
|
4304
|
+
console.log(` ${BG}\u2713${RS} WebSocket server on :${port}${versionStr}`);
|
|
4305
|
+
console.log(` ${BG}\u2713${RS} Watching Claude sessions`);
|
|
4306
|
+
console.log("");
|
|
4307
|
+
console.log(` ${DM}Share: run \`ngrok http ${port}\`, then open Settings \u2192 Share${RS}`);
|
|
4308
|
+
console.log(` ${DM}Issues: https://github.com/bulletproof-sh/ctrl${RS}`);
|
|
4309
|
+
console.log("");
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4248
4312
|
// src/projectScanner.ts
|
|
4249
4313
|
import * as fs2 from "fs";
|
|
4250
4314
|
import * as path2 from "path";
|
|
@@ -4255,7 +4319,7 @@ var PROJECT_SCAN_INTERVAL_MS = 1000;
|
|
|
4255
4319
|
var TOOL_DONE_DELAY_MS = 300;
|
|
4256
4320
|
var PERMISSION_TIMER_DELAY_MS = 7000;
|
|
4257
4321
|
var TEXT_IDLE_DELAY_MS = 5000;
|
|
4258
|
-
var AGENT_IDLE_TIMEOUT_MS =
|
|
4322
|
+
var AGENT_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
|
|
4259
4323
|
var BASH_COMMAND_DISPLAY_MAX_LENGTH = 30;
|
|
4260
4324
|
var TASK_DESCRIPTION_DISPLAY_MAX_LENGTH = 40;
|
|
4261
4325
|
|
|
@@ -4715,7 +4779,7 @@ function collectAllJsonlFiles(projectsRoot) {
|
|
|
4715
4779
|
}
|
|
4716
4780
|
return files;
|
|
4717
4781
|
}
|
|
4718
|
-
function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast) {
|
|
4782
|
+
function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, broadcast, idleTimeoutMs) {
|
|
4719
4783
|
const knownJsonlFiles = new Set;
|
|
4720
4784
|
let nextAgentId = 1;
|
|
4721
4785
|
function scan() {
|
|
@@ -4724,9 +4788,9 @@ function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTime
|
|
|
4724
4788
|
for (const { filePath, mtimeMs } of fileInfos) {
|
|
4725
4789
|
if (knownJsonlFiles.has(filePath))
|
|
4726
4790
|
continue;
|
|
4727
|
-
|
|
4728
|
-
if (now - mtimeMs > AGENT_IDLE_TIMEOUT_MS)
|
|
4791
|
+
if (now - mtimeMs > idleTimeoutMs)
|
|
4729
4792
|
continue;
|
|
4793
|
+
knownJsonlFiles.add(filePath);
|
|
4730
4794
|
const id = nextAgentId++;
|
|
4731
4795
|
const agentProjectDir = path2.dirname(filePath);
|
|
4732
4796
|
const agent = {
|
|
@@ -4753,7 +4817,7 @@ function startProjectScanner(rootDir, scanAll, agents, fileWatchers, pollingTime
|
|
|
4753
4817
|
}
|
|
4754
4818
|
for (const [agentId, agent] of agents) {
|
|
4755
4819
|
const lastActivity = agent.lastActivityAt || 0;
|
|
4756
|
-
const idle = now - lastActivity >
|
|
4820
|
+
const idle = now - lastActivity > idleTimeoutMs;
|
|
4757
4821
|
const removed = !fs2.existsSync(agent.jsonlFile);
|
|
4758
4822
|
if (removed || idle) {
|
|
4759
4823
|
const reason = removed ? "JSONL removed" : "idle timeout";
|
|
@@ -4889,13 +4953,11 @@ async function checkForUpdate() {
|
|
|
4889
4953
|
getLatestVersion()
|
|
4890
4954
|
]);
|
|
4891
4955
|
if (!current || !latest)
|
|
4892
|
-
return;
|
|
4956
|
+
return null;
|
|
4893
4957
|
if (!isNewer(current, latest))
|
|
4894
|
-
return;
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
` + ` Run: npx ${PACKAGE_NAME}@latest
|
|
4898
|
-
`);
|
|
4958
|
+
return null;
|
|
4959
|
+
return ` \u2B06 Update available: v${current} \u2192 v${latest}
|
|
4960
|
+
` + ` Run: npx ${PACKAGE_NAME}@latest`;
|
|
4899
4961
|
}
|
|
4900
4962
|
|
|
4901
4963
|
// src/index.ts
|
|
@@ -4903,10 +4965,11 @@ function printUsage() {
|
|
|
4903
4965
|
console.log(`Usage: ctrl-daemon [options]
|
|
4904
4966
|
|
|
4905
4967
|
Options:
|
|
4906
|
-
--port <number>
|
|
4907
|
-
--host <address>
|
|
4908
|
-
--project-dir <path>
|
|
4909
|
-
--
|
|
4968
|
+
--port <number> Port to listen on (default: 3001)
|
|
4969
|
+
--host <address> Host/address to bind to (default: 0.0.0.0)
|
|
4970
|
+
--project-dir <path> Watch a single project directory
|
|
4971
|
+
--idle-timeout <minutes> Agent idle timeout in minutes (default: 15)
|
|
4972
|
+
--help, -h Show this help message
|
|
4910
4973
|
|
|
4911
4974
|
Without --project-dir, watches ALL projects in ~/.claude/projects/.
|
|
4912
4975
|
With --project-dir, watches only that specific project.`);
|
|
@@ -4916,6 +4979,7 @@ function parseArgs() {
|
|
|
4916
4979
|
let projectDir;
|
|
4917
4980
|
let port = 3001;
|
|
4918
4981
|
let host = "0.0.0.0";
|
|
4982
|
+
let idleTimeoutMs = 15 * 60 * 1000;
|
|
4919
4983
|
for (let i = 0;i < args.length; i++) {
|
|
4920
4984
|
if (args[i] === "--help" || args[i] === "-h") {
|
|
4921
4985
|
printUsage();
|
|
@@ -4926,17 +4990,25 @@ function parseArgs() {
|
|
|
4926
4990
|
port = Number.parseInt(args[++i], 10);
|
|
4927
4991
|
} else if (args[i] === "--host" && args[i + 1]) {
|
|
4928
4992
|
host = args[++i];
|
|
4993
|
+
} else if (args[i] === "--idle-timeout" && args[i + 1]) {
|
|
4994
|
+
idleTimeoutMs = Number.parseInt(args[++i], 10) * 60 * 1000;
|
|
4929
4995
|
}
|
|
4930
4996
|
}
|
|
4931
|
-
return { projectDir, port, host };
|
|
4997
|
+
return { projectDir, port, host, idleTimeoutMs };
|
|
4932
4998
|
}
|
|
4933
4999
|
function resolveProjectsRoot(claudeHome) {
|
|
4934
5000
|
const home = claudeHome || path3.join(process.env.HOME || "~", ".claude");
|
|
4935
5001
|
return path3.join(home, "projects");
|
|
4936
5002
|
}
|
|
4937
5003
|
async function main() {
|
|
5004
|
+
printBanner();
|
|
5005
|
+
const stopSpinner = startSpinner("Starting\u2026");
|
|
4938
5006
|
const analyticsConfig = initAnalytics();
|
|
4939
|
-
const [version2] = await Promise.all([
|
|
5007
|
+
const [version2, updateMsg] = await Promise.all([
|
|
5008
|
+
getCurrentVersion(),
|
|
5009
|
+
checkForUpdate()
|
|
5010
|
+
]);
|
|
5011
|
+
stopSpinner();
|
|
4940
5012
|
process.on("uncaughtException", async (err) => {
|
|
4941
5013
|
console.error("[ctrl-daemon] Uncaught exception:", err);
|
|
4942
5014
|
trackException(err, { ...systemInfo, crash_type: "uncaughtException" });
|
|
@@ -4950,18 +5022,14 @@ async function main() {
|
|
|
4950
5022
|
await shutdownAnalytics();
|
|
4951
5023
|
process.exit(1);
|
|
4952
5024
|
});
|
|
4953
|
-
const { projectDir, port, host } = parseArgs();
|
|
5025
|
+
const { projectDir, port, host, idleTimeoutMs } = parseArgs();
|
|
4954
5026
|
const claudeHome = process.env.CLAUDE_HOME;
|
|
4955
|
-
console.log("[ctrl-daemon] Contribute or report issues: https://github.com/bulletproof-sh/ctrl");
|
|
4956
5027
|
let scanDirs;
|
|
4957
5028
|
if (projectDir) {
|
|
4958
5029
|
const dir = resolveProjectDir(projectDir, claudeHome);
|
|
4959
5030
|
scanDirs = [dir];
|
|
4960
|
-
console.log(`[ctrl-daemon] Project dir: ${projectDir}`);
|
|
4961
|
-
console.log(`[ctrl-daemon] Session dir: ${dir}`);
|
|
4962
5031
|
} else {
|
|
4963
5032
|
const projectsRoot = resolveProjectsRoot(claudeHome);
|
|
4964
|
-
console.log(`[ctrl-daemon] Watching all projects in: ${projectsRoot}`);
|
|
4965
5033
|
scanDirs = [projectsRoot];
|
|
4966
5034
|
}
|
|
4967
5035
|
const agents = new Map;
|
|
@@ -4970,6 +5038,7 @@ async function main() {
|
|
|
4970
5038
|
const waitingTimers = new Map;
|
|
4971
5039
|
const permissionTimers = new Map;
|
|
4972
5040
|
const server = createServer({ port, host, agents });
|
|
5041
|
+
printReady(port, version2, updateMsg);
|
|
4973
5042
|
trackEvent("daemon_started", {
|
|
4974
5043
|
version: version2 ?? "unknown",
|
|
4975
5044
|
port,
|
|
@@ -4979,7 +5048,7 @@ async function main() {
|
|
|
4979
5048
|
...analyticsConfig
|
|
4980
5049
|
});
|
|
4981
5050
|
const scanAll = !projectDir;
|
|
4982
|
-
const scanner = startProjectScanner(scanDirs[0], scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, server.broadcast);
|
|
5051
|
+
const scanner = startProjectScanner(scanDirs[0], scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, server.broadcast, idleTimeoutMs);
|
|
4983
5052
|
async function shutdown() {
|
|
4984
5053
|
console.log(`
|
|
4985
5054
|
[ctrl-daemon] Shutting down...`);
|