@bulletproof-sh/ctrl-daemon 0.0.6 → 0.0.7

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.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/dist/index.js +76 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,6 +30,22 @@ npx @bulletproof-sh/ctrl-daemon --port 3001 --host 127.0.0.1
30
30
 
31
31
  Without `--project-dir`, the daemon scans `~/.claude/projects/` and watches every session it finds there.
32
32
 
33
+ ## Sharing your office
34
+
35
+ Run the daemon and an [ngrok](https://ngrok.com) tunnel in parallel to share a live view with anyone:
36
+
37
+ ```sh
38
+ # Terminal 1 — start the daemon
39
+ npx @bulletproof-sh/ctrl-daemon
40
+
41
+ # Terminal 2 — expose it publicly
42
+ ngrok http 3001
43
+ ```
44
+
45
+ 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.
46
+
47
+ > **Note:** Free ngrok URLs rotate each time you restart the tunnel. A paid ngrok plan gives you a stable domain.
48
+
33
49
  ## WebSocket API
34
50
 
35
51
  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";
@@ -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
- console.log(`
4896
- [ctrl-daemon] Update available: v${current} \u2192 v${latest}
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
@@ -4935,8 +4997,14 @@ function resolveProjectsRoot(claudeHome) {
4935
4997
  return path3.join(home, "projects");
4936
4998
  }
4937
4999
  async function main() {
5000
+ printBanner();
5001
+ const stopSpinner = startSpinner("Starting\u2026");
4938
5002
  const analyticsConfig = initAnalytics();
4939
- const [version2] = await Promise.all([getCurrentVersion(), checkForUpdate()]);
5003
+ const [version2, updateMsg] = await Promise.all([
5004
+ getCurrentVersion(),
5005
+ checkForUpdate()
5006
+ ]);
5007
+ stopSpinner();
4940
5008
  process.on("uncaughtException", async (err) => {
4941
5009
  console.error("[ctrl-daemon] Uncaught exception:", err);
4942
5010
  trackException(err, { ...systemInfo, crash_type: "uncaughtException" });
@@ -4952,16 +5020,12 @@ async function main() {
4952
5020
  });
4953
5021
  const { projectDir, port, host } = parseArgs();
4954
5022
  const claudeHome = process.env.CLAUDE_HOME;
4955
- console.log("[ctrl-daemon] Contribute or report issues: https://github.com/bulletproof-sh/ctrl");
4956
5023
  let scanDirs;
4957
5024
  if (projectDir) {
4958
5025
  const dir = resolveProjectDir(projectDir, claudeHome);
4959
5026
  scanDirs = [dir];
4960
- console.log(`[ctrl-daemon] Project dir: ${projectDir}`);
4961
- console.log(`[ctrl-daemon] Session dir: ${dir}`);
4962
5027
  } else {
4963
5028
  const projectsRoot = resolveProjectsRoot(claudeHome);
4964
- console.log(`[ctrl-daemon] Watching all projects in: ${projectsRoot}`);
4965
5029
  scanDirs = [projectsRoot];
4966
5030
  }
4967
5031
  const agents = new Map;
@@ -4970,6 +5034,7 @@ async function main() {
4970
5034
  const waitingTimers = new Map;
4971
5035
  const permissionTimers = new Map;
4972
5036
  const server = createServer({ port, host, agents });
5037
+ printReady(port, version2, updateMsg);
4973
5038
  trackEvent("daemon_started", {
4974
5039
  version: version2 ?? "unknown",
4975
5040
  port,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bulletproof-sh/ctrl-daemon",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "WebSocket daemon for ctrl — watches Claude Code sessions and broadcasts agent state",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",