@bulletproof-sh/ctrl-daemon 0.0.2 → 0.0.4
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 +74 -0
- package/bin/ctrl-daemon.js +1 -1
- package/dist/index.js +91 -14
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @bulletproof-sh/ctrl-daemon
|
|
2
|
+
|
|
3
|
+
WebSocket daemon for [Ctrl / Cubicles](https://bulletproof.sh) — watches Claude Code sessions on disk and broadcasts live agent state to connected web clients.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- [Bun](https://bun.sh) — required at runtime (`curl -fsSL https://bun.sh/install | bash`)
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
# Watch all Claude Code projects (default)
|
|
13
|
+
npx @bulletproof-sh/ctrl-daemon
|
|
14
|
+
|
|
15
|
+
# Watch a single project directory
|
|
16
|
+
npx @bulletproof-sh/ctrl-daemon --project-dir /path/to/your/project
|
|
17
|
+
|
|
18
|
+
# Custom port / host
|
|
19
|
+
npx @bulletproof-sh/ctrl-daemon --port 3001 --host 127.0.0.1
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Options
|
|
23
|
+
|
|
24
|
+
| Flag | Default | Description |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `--port <number>` | `3001` | Port to listen on |
|
|
27
|
+
| `--host <address>` | `0.0.0.0` | Host/address to bind to |
|
|
28
|
+
| `--project-dir <path>` | — | Watch a single project; omit to watch all projects |
|
|
29
|
+
| `--help`, `-h` | — | Print usage |
|
|
30
|
+
|
|
31
|
+
Without `--project-dir`, the daemon scans `~/.claude/projects/` and watches every session it finds there.
|
|
32
|
+
|
|
33
|
+
## WebSocket API
|
|
34
|
+
|
|
35
|
+
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.
|
|
36
|
+
|
|
37
|
+
The [Ctrl web app](https://ctrl.bulletproof.sh) connects here by default. You can change the URL in the app's Settings modal or by setting `VITE_DAEMON_WS_URL` at build time.
|
|
38
|
+
|
|
39
|
+
## Environment Variables
|
|
40
|
+
|
|
41
|
+
| Variable | Description |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `VITE_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics (optional) |
|
|
44
|
+
| `VITE_PUBLIC_POSTHOG_HOST` | PostHog host (default: `https://us.i.posthog.com`) |
|
|
45
|
+
| `CLAUDE_HOME` | Override the Claude home directory (default: `~/.claude`) |
|
|
46
|
+
|
|
47
|
+
Analytics are disabled when `VITE_PUBLIC_POSTHOG_KEY` is not set. Crash reports use PostHog Error Tracking (free feature).
|
|
48
|
+
|
|
49
|
+
## Auto-update
|
|
50
|
+
|
|
51
|
+
On startup the daemon checks npm for a newer version and prints a notice if one is available:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
[ctrl-daemon] Update available: v0.0.2 → v0.0.3
|
|
55
|
+
Run: npm install -g @bulletproof-sh/ctrl-daemon@latest
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The check is non-blocking and fails silently if the registry is unreachable.
|
|
59
|
+
|
|
60
|
+
> **Note:** If you're running via `npx`, you already get the latest version on every invocation — no manual update needed.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
bun install
|
|
66
|
+
bun run dev # watch mode
|
|
67
|
+
bun run build # compile to dist/
|
|
68
|
+
bun run check # biome lint + format
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
|
74
|
+
|
package/bin/ctrl-daemon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const entry = join(__dirname, "..", "dist", "index.js");
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,10 @@ import * as path3 from "path";
|
|
|
16
16
|
// src/analytics.ts
|
|
17
17
|
import os from "os";
|
|
18
18
|
|
|
19
|
+
// ../shared/src/constants.ts
|
|
20
|
+
var DEFAULT_POSTHOG_KEY = "phc_V3aPfuu7VEWm1u4CEYLIFr8Ksk8nR7qyZzGn8HotO8U";
|
|
21
|
+
var DEFAULT_POSTHOG_HOST = "https://a.bulletproof.sh";
|
|
22
|
+
|
|
19
23
|
// node_modules/posthog-node/dist/extensions/error-tracking/modifiers/module.node.mjs
|
|
20
24
|
import { dirname, posix, sep } from "path";
|
|
21
25
|
function createModulerModifier() {
|
|
@@ -4204,17 +4208,26 @@ class PostHog extends PostHogBackendClient {
|
|
|
4204
4208
|
}
|
|
4205
4209
|
|
|
4206
4210
|
// src/analytics.ts
|
|
4207
|
-
var
|
|
4208
|
-
|
|
4211
|
+
var systemInfo = {
|
|
4212
|
+
os_platform: os.platform(),
|
|
4213
|
+
os_arch: os.arch(),
|
|
4214
|
+
os_release: os.release(),
|
|
4215
|
+
node_version: process.version
|
|
4216
|
+
};
|
|
4217
|
+
var POSTHOG_API_KEY = process.env.VITE_PUBLIC_POSTHOG_KEY ?? DEFAULT_POSTHOG_KEY;
|
|
4218
|
+
var POSTHOG_HOST = process.env.VITE_PUBLIC_POSTHOG_HOST ?? DEFAULT_POSTHOG_HOST;
|
|
4219
|
+
var customKey = !!process.env.VITE_PUBLIC_POSTHOG_KEY;
|
|
4220
|
+
var customHost = !!process.env.VITE_PUBLIC_POSTHOG_HOST;
|
|
4209
4221
|
var client = null;
|
|
4210
4222
|
var distinctId = os.hostname();
|
|
4211
4223
|
function initAnalytics() {
|
|
4212
|
-
if (
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
}
|
|
4224
|
+
if (POSTHOG_API_KEY) {
|
|
4225
|
+
client = new PostHog(POSTHOG_API_KEY, { host: POSTHOG_HOST });
|
|
4226
|
+
client.on("error", (err) => {
|
|
4227
|
+
console.error("[ctrl-daemon] PostHog error:", err);
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
return { customKey, customHost };
|
|
4218
4231
|
}
|
|
4219
4232
|
function trackEvent(event, properties) {
|
|
4220
4233
|
if (!client)
|
|
@@ -4824,6 +4837,67 @@ function createServer({ port, host, agents }) {
|
|
|
4824
4837
|
};
|
|
4825
4838
|
}
|
|
4826
4839
|
|
|
4840
|
+
// src/updater.ts
|
|
4841
|
+
var PACKAGE_NAME = "@bulletproof-sh/ctrl-daemon";
|
|
4842
|
+
var NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
4843
|
+
var UPDATE_CHECK_TIMEOUT_MS = 5000;
|
|
4844
|
+
function parseSemver(version2) {
|
|
4845
|
+
const match = version2.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
4846
|
+
if (!match)
|
|
4847
|
+
return null;
|
|
4848
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
4849
|
+
}
|
|
4850
|
+
function isNewer(current, next) {
|
|
4851
|
+
const a = parseSemver(current);
|
|
4852
|
+
const b = parseSemver(next);
|
|
4853
|
+
if (!a || !b)
|
|
4854
|
+
return false;
|
|
4855
|
+
for (let i = 0;i < 3; i++) {
|
|
4856
|
+
if (b[i] > a[i])
|
|
4857
|
+
return true;
|
|
4858
|
+
if (b[i] < a[i])
|
|
4859
|
+
return false;
|
|
4860
|
+
}
|
|
4861
|
+
return false;
|
|
4862
|
+
}
|
|
4863
|
+
async function getCurrentVersion() {
|
|
4864
|
+
try {
|
|
4865
|
+
const pkgFile = Bun.file(new URL("../package.json", import.meta.url));
|
|
4866
|
+
const pkg = await pkgFile.json();
|
|
4867
|
+
return pkg.version ?? null;
|
|
4868
|
+
} catch {
|
|
4869
|
+
return null;
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
async function getLatestVersion() {
|
|
4873
|
+
try {
|
|
4874
|
+
const controller = new AbortController;
|
|
4875
|
+
const timeout = setTimeout(() => controller.abort(), UPDATE_CHECK_TIMEOUT_MS);
|
|
4876
|
+
const res = await fetch(NPM_REGISTRY_URL, { signal: controller.signal });
|
|
4877
|
+
clearTimeout(timeout);
|
|
4878
|
+
if (!res.ok)
|
|
4879
|
+
return null;
|
|
4880
|
+
const data = await res.json();
|
|
4881
|
+
return data.version ?? null;
|
|
4882
|
+
} catch {
|
|
4883
|
+
return null;
|
|
4884
|
+
}
|
|
4885
|
+
}
|
|
4886
|
+
async function checkForUpdate() {
|
|
4887
|
+
const [current, latest] = await Promise.all([
|
|
4888
|
+
getCurrentVersion(),
|
|
4889
|
+
getLatestVersion()
|
|
4890
|
+
]);
|
|
4891
|
+
if (!current || !latest)
|
|
4892
|
+
return;
|
|
4893
|
+
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
|
+
`);
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4827
4901
|
// src/index.ts
|
|
4828
4902
|
function printUsage() {
|
|
4829
4903
|
console.log(`Usage: ctrl-daemon [options]
|
|
@@ -4860,18 +4934,19 @@ function resolveProjectsRoot(claudeHome) {
|
|
|
4860
4934
|
const home = claudeHome || path3.join(process.env.HOME || "~", ".claude");
|
|
4861
4935
|
return path3.join(home, "projects");
|
|
4862
4936
|
}
|
|
4863
|
-
function main() {
|
|
4864
|
-
initAnalytics();
|
|
4937
|
+
async function main() {
|
|
4938
|
+
const analyticsConfig = initAnalytics();
|
|
4939
|
+
const [version2] = await Promise.all([getCurrentVersion(), checkForUpdate()]);
|
|
4865
4940
|
process.on("uncaughtException", async (err) => {
|
|
4866
4941
|
console.error("[ctrl-daemon] Uncaught exception:", err);
|
|
4867
|
-
trackException(err);
|
|
4942
|
+
trackException(err, { ...systemInfo, crash_type: "uncaughtException" });
|
|
4868
4943
|
await shutdownAnalytics();
|
|
4869
4944
|
process.exit(1);
|
|
4870
4945
|
});
|
|
4871
4946
|
process.on("unhandledRejection", async (reason) => {
|
|
4872
4947
|
const err = reason instanceof Error ? reason : new Error(String(reason));
|
|
4873
4948
|
console.error("[ctrl-daemon] Unhandled rejection:", err);
|
|
4874
|
-
trackException(err);
|
|
4949
|
+
trackException(err, { ...systemInfo, crash_type: "unhandledRejection" });
|
|
4875
4950
|
await shutdownAnalytics();
|
|
4876
4951
|
process.exit(1);
|
|
4877
4952
|
});
|
|
@@ -4895,10 +4970,12 @@ function main() {
|
|
|
4895
4970
|
const permissionTimers = new Map;
|
|
4896
4971
|
const server = createServer({ port, host, agents });
|
|
4897
4972
|
trackEvent("daemon_started", {
|
|
4898
|
-
|
|
4973
|
+
version: version2 ?? "unknown",
|
|
4899
4974
|
port,
|
|
4900
4975
|
host,
|
|
4901
|
-
mode: projectDir ? "single" : "all"
|
|
4976
|
+
mode: projectDir ? "single" : "all",
|
|
4977
|
+
...systemInfo,
|
|
4978
|
+
...analyticsConfig
|
|
4902
4979
|
});
|
|
4903
4980
|
const scanAll = !projectDir;
|
|
4904
4981
|
const scanner = startProjectScanner(scanDirs[0], scanAll, agents, fileWatchers, pollingTimers, waitingTimers, permissionTimers, server.broadcast);
|