@vibelet/cli 0.1.35 → 0.1.37

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 (323) hide show
  1. package/app.json +5 -0
  2. package/dist/advertised-hosts.d.ts +34 -0
  3. package/dist/advertised-hosts.d.ts.map +1 -0
  4. package/dist/advertised-hosts.js +176 -0
  5. package/dist/advertised-hosts.js.map +1 -0
  6. package/dist/advertised-hosts.test.d.ts +2 -0
  7. package/dist/advertised-hosts.test.d.ts.map +1 -0
  8. package/dist/advertised-hosts.test.js +96 -0
  9. package/dist/advertised-hosts.test.js.map +1 -0
  10. package/dist/audit.d.ts +30 -0
  11. package/dist/audit.d.ts.map +1 -0
  12. package/dist/audit.js +73 -0
  13. package/dist/audit.js.map +1 -0
  14. package/dist/audit.test.d.ts +2 -0
  15. package/dist/audit.test.d.ts.map +1 -0
  16. package/dist/audit.test.js +33 -0
  17. package/dist/audit.test.js.map +1 -0
  18. package/dist/auth.d.ts +6 -0
  19. package/dist/auth.d.ts.map +1 -0
  20. package/dist/auth.js +27 -0
  21. package/dist/auth.js.map +1 -0
  22. package/dist/claude-hooks.d.ts +58 -0
  23. package/dist/claude-hooks.d.ts.map +1 -0
  24. package/dist/claude-hooks.js +129 -0
  25. package/dist/claude-hooks.js.map +1 -0
  26. package/dist/cli-version.d.ts +3 -0
  27. package/dist/cli-version.d.ts.map +1 -0
  28. package/dist/cli-version.js +35 -0
  29. package/dist/cli-version.js.map +1 -0
  30. package/dist/cli-version.test.d.ts +2 -0
  31. package/dist/cli-version.test.d.ts.map +1 -0
  32. package/dist/cli-version.test.js +38 -0
  33. package/dist/cli-version.test.js.map +1 -0
  34. package/dist/config.d.ts +30 -0
  35. package/dist/config.d.ts.map +1 -0
  36. package/dist/config.js +327 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/config.test.d.ts +2 -0
  39. package/dist/config.test.d.ts.map +1 -0
  40. package/dist/config.test.js +184 -0
  41. package/dist/config.test.js.map +1 -0
  42. package/dist/dev-auth.test.d.ts +2 -0
  43. package/dist/dev-auth.test.d.ts.map +1 -0
  44. package/dist/dev-auth.test.js +154 -0
  45. package/dist/dev-auth.test.js.map +1 -0
  46. package/dist/dev-script.test.d.ts +2 -0
  47. package/dist/dev-script.test.d.ts.map +1 -0
  48. package/dist/dev-script.test.js +412 -0
  49. package/dist/dev-script.test.js.map +1 -0
  50. package/dist/drivers/claude.d.ts +34 -0
  51. package/dist/drivers/claude.d.ts.map +1 -0
  52. package/dist/drivers/claude.js +413 -0
  53. package/dist/drivers/claude.js.map +1 -0
  54. package/dist/drivers/claude.test.d.ts +2 -0
  55. package/dist/drivers/claude.test.d.ts.map +1 -0
  56. package/dist/drivers/claude.test.js +951 -0
  57. package/dist/drivers/claude.test.js.map +1 -0
  58. package/dist/drivers/codex.d.ts +38 -0
  59. package/dist/drivers/codex.d.ts.map +1 -0
  60. package/dist/drivers/codex.js +771 -0
  61. package/dist/drivers/codex.js.map +1 -0
  62. package/dist/drivers/codex.test.d.ts +2 -0
  63. package/dist/drivers/codex.test.d.ts.map +1 -0
  64. package/dist/drivers/codex.test.js +939 -0
  65. package/dist/drivers/codex.test.js.map +1 -0
  66. package/dist/drivers/types.d.ts +14 -0
  67. package/dist/drivers/types.d.ts.map +1 -0
  68. package/dist/drivers/types.js +2 -0
  69. package/dist/drivers/types.js.map +1 -0
  70. package/dist/e2e.test.d.ts +2 -0
  71. package/dist/e2e.test.d.ts.map +1 -0
  72. package/dist/e2e.test.js +111 -0
  73. package/dist/e2e.test.js.map +1 -0
  74. package/dist/identity.d.ts +10 -0
  75. package/dist/identity.d.ts.map +1 -0
  76. package/dist/identity.js +66 -0
  77. package/dist/identity.js.map +1 -0
  78. package/dist/identity.test.d.ts +2 -0
  79. package/dist/identity.test.d.ts.map +1 -0
  80. package/dist/identity.test.js +25 -0
  81. package/dist/identity.test.js.map +1 -0
  82. package/dist/index-entry.test.d.ts +2 -0
  83. package/dist/index-entry.test.d.ts.map +1 -0
  84. package/dist/index-entry.test.js +272 -0
  85. package/dist/index-entry.test.js.map +1 -0
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.d.ts.map +1 -0
  88. package/dist/index.js +707 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/logger.d.ts +31 -0
  91. package/dist/logger.d.ts.map +1 -0
  92. package/dist/logger.js +75 -0
  93. package/dist/logger.js.map +1 -0
  94. package/dist/metrics.d.ts +52 -0
  95. package/dist/metrics.d.ts.map +1 -0
  96. package/dist/metrics.js +89 -0
  97. package/dist/metrics.js.map +1 -0
  98. package/dist/pairing-store.d.ts +29 -0
  99. package/dist/pairing-store.d.ts.map +1 -0
  100. package/dist/pairing-store.js +131 -0
  101. package/dist/pairing-store.js.map +1 -0
  102. package/dist/pairing-store.test.d.ts +2 -0
  103. package/dist/pairing-store.test.d.ts.map +1 -0
  104. package/dist/pairing-store.test.js +47 -0
  105. package/dist/pairing-store.test.js.map +1 -0
  106. package/dist/paths.d.ts +16 -0
  107. package/dist/paths.d.ts.map +1 -0
  108. package/dist/paths.js +18 -0
  109. package/dist/paths.js.map +1 -0
  110. package/dist/perf-compare.d.ts +13 -0
  111. package/dist/perf-compare.d.ts.map +1 -0
  112. package/dist/perf-compare.js +125 -0
  113. package/dist/perf-compare.js.map +1 -0
  114. package/dist/port-conflict.d.ts +9 -0
  115. package/dist/port-conflict.d.ts.map +1 -0
  116. package/dist/port-conflict.js +33 -0
  117. package/dist/port-conflict.js.map +1 -0
  118. package/dist/port-conflict.test.d.ts +2 -0
  119. package/dist/port-conflict.test.d.ts.map +1 -0
  120. package/dist/port-conflict.test.js +38 -0
  121. package/dist/port-conflict.test.js.map +1 -0
  122. package/dist/process-scanner.d.ts +43 -0
  123. package/dist/process-scanner.d.ts.map +1 -0
  124. package/dist/process-scanner.js +453 -0
  125. package/dist/process-scanner.js.map +1 -0
  126. package/dist/process-scanner.perf.test.d.ts +2 -0
  127. package/dist/process-scanner.perf.test.d.ts.map +1 -0
  128. package/dist/process-scanner.perf.test.js +186 -0
  129. package/dist/process-scanner.perf.test.js.map +1 -0
  130. package/dist/process-scanner.test.d.ts +2 -0
  131. package/dist/process-scanner.test.d.ts.map +1 -0
  132. package/dist/process-scanner.test.js +399 -0
  133. package/dist/process-scanner.test.js.map +1 -0
  134. package/dist/push-protocol.d.ts +15 -0
  135. package/dist/push-protocol.d.ts.map +1 -0
  136. package/dist/push-protocol.js +23 -0
  137. package/dist/push-protocol.js.map +1 -0
  138. package/dist/push-protocol.test.d.ts +2 -0
  139. package/dist/push-protocol.test.d.ts.map +1 -0
  140. package/dist/push-protocol.test.js +57 -0
  141. package/dist/push-protocol.test.js.map +1 -0
  142. package/dist/push-store.d.ts +22 -0
  143. package/dist/push-store.d.ts.map +1 -0
  144. package/dist/push-store.js +103 -0
  145. package/dist/push-store.js.map +1 -0
  146. package/dist/push-store.test.d.ts +2 -0
  147. package/dist/push-store.test.d.ts.map +1 -0
  148. package/dist/push-store.test.js +79 -0
  149. package/dist/push-store.test.js.map +1 -0
  150. package/dist/push.d.ts +65 -0
  151. package/dist/push.d.ts.map +1 -0
  152. package/dist/push.js +202 -0
  153. package/dist/push.js.map +1 -0
  154. package/dist/push.test.d.ts +2 -0
  155. package/dist/push.test.d.ts.map +1 -0
  156. package/dist/push.test.js +199 -0
  157. package/dist/push.test.js.map +1 -0
  158. package/dist/safe-stdio.d.ts +3 -0
  159. package/dist/safe-stdio.d.ts.map +1 -0
  160. package/dist/safe-stdio.js +46 -0
  161. package/dist/safe-stdio.js.map +1 -0
  162. package/dist/scanner.d.ts +30 -0
  163. package/dist/scanner.d.ts.map +1 -0
  164. package/dist/scanner.js +859 -0
  165. package/dist/scanner.js.map +1 -0
  166. package/dist/scanner.perf.test.d.ts +2 -0
  167. package/dist/scanner.perf.test.d.ts.map +1 -0
  168. package/dist/scanner.perf.test.js +320 -0
  169. package/dist/scanner.perf.test.js.map +1 -0
  170. package/dist/scanner.test.d.ts +2 -0
  171. package/dist/scanner.test.d.ts.map +1 -0
  172. package/dist/scanner.test.js +948 -0
  173. package/dist/scanner.test.js.map +1 -0
  174. package/dist/session-inventory.d.ts +63 -0
  175. package/dist/session-inventory.d.ts.map +1 -0
  176. package/dist/session-inventory.js +525 -0
  177. package/dist/session-inventory.js.map +1 -0
  178. package/dist/session-inventory.perf.test.d.ts +2 -0
  179. package/dist/session-inventory.perf.test.d.ts.map +1 -0
  180. package/dist/session-inventory.perf.test.js +220 -0
  181. package/dist/session-inventory.perf.test.js.map +1 -0
  182. package/dist/session-inventory.test.d.ts +2 -0
  183. package/dist/session-inventory.test.d.ts.map +1 -0
  184. package/dist/session-inventory.test.js +712 -0
  185. package/dist/session-inventory.test.js.map +1 -0
  186. package/dist/session-manager.d.ts +75 -0
  187. package/dist/session-manager.d.ts.map +1 -0
  188. package/dist/session-manager.js +1515 -0
  189. package/dist/session-manager.js.map +1 -0
  190. package/dist/session-manager.test.d.ts +2 -0
  191. package/dist/session-manager.test.d.ts.map +1 -0
  192. package/dist/session-manager.test.js +2861 -0
  193. package/dist/session-manager.test.js.map +1 -0
  194. package/dist/session-store.d.ts +42 -0
  195. package/dist/session-store.d.ts.map +1 -0
  196. package/dist/session-store.js +163 -0
  197. package/dist/session-store.js.map +1 -0
  198. package/dist/session-store.test.d.ts +2 -0
  199. package/dist/session-store.test.d.ts.map +1 -0
  200. package/dist/session-store.test.js +236 -0
  201. package/dist/session-store.test.js.map +1 -0
  202. package/dist/session-title.d.ts +6 -0
  203. package/dist/session-title.d.ts.map +1 -0
  204. package/dist/session-title.js +105 -0
  205. package/dist/session-title.js.map +1 -0
  206. package/dist/session-title.perf.test.d.ts +2 -0
  207. package/dist/session-title.perf.test.d.ts.map +1 -0
  208. package/dist/session-title.perf.test.js +99 -0
  209. package/dist/session-title.perf.test.js.map +1 -0
  210. package/dist/session-title.test.d.ts +2 -0
  211. package/dist/session-title.test.d.ts.map +1 -0
  212. package/dist/session-title.test.js +199 -0
  213. package/dist/session-title.test.js.map +1 -0
  214. package/dist/shutdown-endpoint.test.d.ts +2 -0
  215. package/dist/shutdown-endpoint.test.d.ts.map +1 -0
  216. package/dist/shutdown-endpoint.test.js +93 -0
  217. package/dist/shutdown-endpoint.test.js.map +1 -0
  218. package/dist/storage-housekeeping.d.ts +28 -0
  219. package/dist/storage-housekeeping.d.ts.map +1 -0
  220. package/dist/storage-housekeeping.js +76 -0
  221. package/dist/storage-housekeeping.js.map +1 -0
  222. package/dist/storage-housekeeping.test.d.ts +2 -0
  223. package/dist/storage-housekeeping.test.d.ts.map +1 -0
  224. package/dist/storage-housekeeping.test.js +65 -0
  225. package/dist/storage-housekeeping.test.js.map +1 -0
  226. package/dist/test-daemon-harness.d.ts +31 -0
  227. package/dist/test-daemon-harness.d.ts.map +1 -0
  228. package/dist/test-daemon-harness.js +337 -0
  229. package/dist/test-daemon-harness.js.map +1 -0
  230. package/dist/token-auth.test.d.ts +2 -0
  231. package/dist/token-auth.test.d.ts.map +1 -0
  232. package/dist/token-auth.test.js +52 -0
  233. package/dist/token-auth.test.js.map +1 -0
  234. package/dist/utils.d.ts +4 -0
  235. package/dist/utils.d.ts.map +1 -0
  236. package/dist/utils.js +40 -0
  237. package/dist/utils.js.map +1 -0
  238. package/dist/utils.test.d.ts +2 -0
  239. package/dist/utils.test.d.ts.map +1 -0
  240. package/dist/utils.test.js +54 -0
  241. package/dist/utils.test.js.map +1 -0
  242. package/dist/ws-data.d.ts +4 -0
  243. package/dist/ws-data.d.ts.map +1 -0
  244. package/dist/ws-data.js +20 -0
  245. package/dist/ws-data.js.map +1 -0
  246. package/dist/ws-data.test.d.ts +2 -0
  247. package/dist/ws-data.test.d.ts.map +1 -0
  248. package/dist/ws-data.test.js +17 -0
  249. package/dist/ws-data.test.js.map +1 -0
  250. package/package.json +24 -24
  251. package/perf-reporter.mjs +138 -0
  252. package/scripts/build-release.mjs +41 -0
  253. package/scripts/dev.mjs +537 -0
  254. package/src/advertised-hosts.test.ts +125 -0
  255. package/src/advertised-hosts.ts +225 -0
  256. package/src/audit.test.ts +38 -0
  257. package/src/audit.ts +117 -0
  258. package/src/auth.ts +31 -0
  259. package/src/claude-hooks.ts +195 -0
  260. package/src/cli-version.test.ts +36 -0
  261. package/src/cli-version.ts +46 -0
  262. package/src/config.test.ts +254 -0
  263. package/src/config.ts +324 -0
  264. package/src/dev-auth.test.ts +183 -0
  265. package/src/dev-script.test.ts +511 -0
  266. package/src/drivers/claude.test.ts +1186 -0
  267. package/src/drivers/claude.ts +443 -0
  268. package/src/drivers/codex.test.ts +1096 -0
  269. package/src/drivers/codex.ts +879 -0
  270. package/src/drivers/types.ts +15 -0
  271. package/src/e2e.test.ts +139 -0
  272. package/src/identity.test.ts +26 -0
  273. package/src/identity.ts +82 -0
  274. package/src/index-entry.test.ts +336 -0
  275. package/src/index.ts +781 -0
  276. package/src/logger.ts +112 -0
  277. package/src/metrics.ts +117 -0
  278. package/src/pairing-store.test.ts +53 -0
  279. package/src/pairing-store.ts +154 -0
  280. package/src/paths.ts +19 -0
  281. package/src/perf-compare.ts +164 -0
  282. package/src/port-conflict.test.ts +45 -0
  283. package/src/port-conflict.ts +44 -0
  284. package/src/process-scanner.perf.test.ts +222 -0
  285. package/src/process-scanner.test.ts +575 -0
  286. package/src/process-scanner.ts +514 -0
  287. package/src/push-protocol.test.ts +74 -0
  288. package/src/push-protocol.ts +36 -0
  289. package/src/push-store.test.ts +89 -0
  290. package/src/push-store.ts +126 -0
  291. package/src/push.test.ts +234 -0
  292. package/src/push.ts +318 -0
  293. package/src/safe-stdio.ts +51 -0
  294. package/src/scanner.perf.test.ts +359 -0
  295. package/src/scanner.test.ts +1045 -0
  296. package/src/scanner.ts +924 -0
  297. package/src/session-inventory.perf.test.ts +250 -0
  298. package/src/session-inventory.test.ts +1002 -0
  299. package/src/session-inventory.ts +721 -0
  300. package/src/session-manager.test.ts +3430 -0
  301. package/src/session-manager.ts +1775 -0
  302. package/src/session-store.test.ts +276 -0
  303. package/src/session-store.ts +202 -0
  304. package/src/session-title.perf.test.ts +118 -0
  305. package/src/session-title.test.ts +286 -0
  306. package/src/session-title.ts +108 -0
  307. package/src/shutdown-endpoint.test.ts +95 -0
  308. package/src/storage-housekeeping.test.ts +78 -0
  309. package/src/storage-housekeeping.ts +111 -0
  310. package/src/test-daemon-harness.ts +410 -0
  311. package/src/token-auth.test.ts +67 -0
  312. package/src/utils.test.ts +65 -0
  313. package/src/utils.ts +47 -0
  314. package/src/ws-data.test.ts +20 -0
  315. package/src/ws-data.ts +26 -0
  316. package/tsconfig.json +12 -0
  317. package/README.md +0 -80
  318. package/bin/cloudflared-quick-tunnel.mjs +0 -11
  319. package/bin/cloudflared-resolver.mjs +0 -68
  320. package/bin/vibelet-runtime-policy.mjs +0 -36
  321. package/bin/vibelet.cjs +0 -12
  322. package/bin/vibelet.mjs +0 -1035
  323. package/dist/index.cjs +0 -125
package/src/index.ts ADDED
@@ -0,0 +1,781 @@
1
+ import { createServer, type IncomingMessage, type ServerResponse } from 'http';
2
+ import { WebSocketServer, type WebSocket } from 'ws';
3
+ import { mkdir, readFile, readdir, stat, writeFile } from 'fs/promises';
4
+ import { randomBytes } from 'crypto';
5
+ import { extname, join } from 'path';
6
+ import { networkInterfaces } from 'os';
7
+ import type { Socket } from 'net';
8
+ import QRCode from 'qrcode';
9
+ import type { DirEntry, PairingQrPayload } from '@vibelet/shared';
10
+ import { config } from './config.js';
11
+ import { SessionManager } from './session-manager.js';
12
+ import { getExternalInventoryHealth } from './session-inventory.js';
13
+ import { expandPath, resolveFileRequestPath } from './utils.js';
14
+ import type { ClientMessage } from '@vibelet/shared';
15
+ import { logger as rootLogger } from './logger.js';
16
+ import { metrics } from './metrics.js';
17
+ import { audit } from './audit.js';
18
+ import { registerToken, sendPush, unregisterToken } from './push.js';
19
+ import { handlePushProtocolMessage } from './push-protocol.js';
20
+ import { collectPortOccupants } from './port-conflict.js';
21
+ import { loadOrCreateDaemonIdentity } from './identity.js';
22
+ import { PairingStore } from './pairing-store.js';
23
+ import { isAuthorizedToken, isLegacyToken, isLoopbackAddress, resolveRequestToken } from './auth.js';
24
+ import {
25
+ computeAdvertisedConnectionTarget,
26
+ computeAdvertisedHosts,
27
+ readConfiguredFallbackHosts,
28
+ readTailscaleHosts,
29
+ } from './advertised-hosts.js';
30
+ import { readRawDataAsText } from './ws-data.js';
31
+ import { runStorageHousekeepingSync } from './storage-housekeeping.js';
32
+ import { AUDIT_PATH, DAEMON_STDERR_LOG_PATH, DAEMON_STDOUT_LOG_PATH, PAIRING_QR_PNG_PATH, UPDATE_CHECK_PATH, VIBELET_DIR, VIBELET_UPLOADS_DIR } from './paths.js';
33
+ import { CLI_VERSION } from './cli-version.js';
34
+ import { CLAUDE_HOOK_SECRET_HEADER, type ClaudePermissionHookData, type ClaudeSessionHookData } from './claude-hooks.js';
35
+ import { writeStdoutSafe } from './safe-stdio.js';
36
+
37
+ const log = rootLogger.child({ module: 'daemon' });
38
+ const wsLog = rootLogger.child({ module: 'ws' });
39
+
40
+ function readLatestVersion(): string | undefined {
41
+ try {
42
+ const data = JSON.parse(require('fs').readFileSync(UPDATE_CHECK_PATH, 'utf8'));
43
+ return typeof data.latestVersion === 'string' ? data.latestVersion : undefined;
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ const identity = loadOrCreateDaemonIdentity(config.port);
50
+ const manager = new SessionManager((title, body, data) => sendPush(title, body, {
51
+ daemonId: identity.daemonId,
52
+ canonicalHost: getAdvertisedConnectionTarget().canonicalHost,
53
+ ...(data ?? {}),
54
+ }));
55
+ const pairingStore = new PairingStore();
56
+ const sockets = new Set<Socket>();
57
+ interface AuthenticatedClientContext {
58
+ authMode: 'query_token' | 'auth_hello';
59
+ deviceId?: string;
60
+ }
61
+
62
+ const authenticatedClients = new WeakMap<WebSocket, AuthenticatedClientContext>();
63
+ let shuttingDown = false;
64
+ const startTime = Date.now();
65
+ let storageHousekeepingInterval: ReturnType<typeof setInterval> | null = null;
66
+ let advertisedConnectionTargetCache:
67
+ | { value: { canonicalHost: string; fallbackHosts: string[]; port: number }; expiresAt: number }
68
+ | null = null;
69
+
70
+ const ADVERTISED_CONNECTION_TARGET_CACHE_TTL_MS = 1_000;
71
+
72
+ function getAdvertisedHosts(): { canonicalHost: string; fallbackHosts: string[] } {
73
+ const tailscaleHosts = readTailscaleHosts();
74
+ return computeAdvertisedHosts({
75
+ canonicalHost: identity.canonicalHost,
76
+ configuredCanonicalHost: config.canonicalHost,
77
+ configuredFallbackHosts: readConfiguredFallbackHosts(config.fallbackHosts),
78
+ tailscaleCanonicalHost: tailscaleHosts.canonicalHost,
79
+ tailscaleFallbackHosts: tailscaleHosts.fallbackHosts,
80
+ interfaces: networkInterfaces(),
81
+ });
82
+ }
83
+
84
+ function getAdvertisedConnectionTarget(): { canonicalHost: string; fallbackHosts: string[]; port: number } {
85
+ const now = Date.now();
86
+ if (advertisedConnectionTargetCache && advertisedConnectionTargetCache.expiresAt > now) {
87
+ return advertisedConnectionTargetCache.value;
88
+ }
89
+
90
+ const advertisedHosts = getAdvertisedHosts();
91
+ const connectionTarget = computeAdvertisedConnectionTarget({
92
+ canonicalHost: advertisedHosts.canonicalHost,
93
+ fallbackHosts: advertisedHosts.fallbackHosts,
94
+ port: identity.port,
95
+ relayUrl: config.relayUrl,
96
+ });
97
+
98
+ // Tailscale probing is synchronous and can take a few seconds when the socket
99
+ // is unavailable. Cache the computed target briefly so startup logging, health
100
+ // checks, and immediate pairing calls do not repeatedly block the event loop.
101
+ advertisedConnectionTargetCache = {
102
+ value: connectionTarget,
103
+ expiresAt: now + ADVERTISED_CONNECTION_TARGET_CACHE_TTL_MS,
104
+ };
105
+
106
+ return connectionTarget;
107
+ }
108
+
109
+ function createCompactPairingPayload(pairingPayload: PairingQrPayload): Record<string, unknown> {
110
+ const compactPayload: Record<string, unknown> = {
111
+ t: 'vp',
112
+ d: pairingPayload.daemonId,
113
+ n: pairingPayload.displayName,
114
+ h: pairingPayload.canonicalHost,
115
+ p: pairingPayload.port,
116
+ c: pairingPayload.pairNonce,
117
+ e: pairingPayload.expiresAt,
118
+ };
119
+ if (pairingPayload.fallbackHosts) compactPayload.f = pairingPayload.fallbackHosts;
120
+ return compactPayload;
121
+ }
122
+
123
+ async function writeStartupPairingQr(pairingPayload: PairingQrPayload): Promise<void> {
124
+ const payload = JSON.stringify(createCompactPairingPayload(pairingPayload));
125
+ await mkdir(VIBELET_DIR, { recursive: true });
126
+ await QRCode.toFile(PAIRING_QR_PNG_PATH, payload, {
127
+ type: 'png',
128
+ errorCorrectionLevel: 'M',
129
+ margin: 1,
130
+ scale: 8,
131
+ });
132
+ const qr = await QRCode.toString(payload, {
133
+ type: 'terminal',
134
+ small: true,
135
+ errorCorrectionLevel: 'M',
136
+ });
137
+ writeStdoutSafe(`\nScan this QR code with Vibelet app:\n\n${qr}\n`);
138
+ writeStdoutSafe(`If Vibelet app can't scan the terminal QR, open this PNG instead:\n${PAIRING_QR_PNG_PATH}\n`);
139
+ }
140
+
141
+ async function readJsonBody<T>(req: IncomingMessage): Promise<T> {
142
+ const chunks: Buffer[] = [];
143
+ for await (const chunk of req) {
144
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
145
+ }
146
+ const raw = Buffer.concat(chunks).toString('utf8');
147
+ return raw ? JSON.parse(raw) as T : {} as T;
148
+ }
149
+
150
+ function sendJson(res: ServerResponse, statusCode: number, body: unknown): void {
151
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
152
+ res.end(JSON.stringify(body, null, 2));
153
+ }
154
+
155
+ function getFileContentType(filePath: string): string {
156
+ const ext = extname(filePath).toLowerCase();
157
+ const mimeTypes: Record<string, string> = {
158
+ '.png': 'image/png',
159
+ '.jpg': 'image/jpeg',
160
+ '.jpeg': 'image/jpeg',
161
+ '.gif': 'image/gif',
162
+ '.svg': 'image/svg+xml',
163
+ '.webp': 'image/webp',
164
+ '.heic': 'image/heic',
165
+ '.heif': 'image/heif',
166
+ '.pdf': 'application/pdf',
167
+ '.md': 'text/markdown; charset=utf-8',
168
+ '.markdown': 'text/markdown; charset=utf-8',
169
+ '.txt': 'text/plain; charset=utf-8',
170
+ '.log': 'text/plain; charset=utf-8',
171
+ '.json': 'application/json; charset=utf-8',
172
+ '.js': 'text/plain; charset=utf-8',
173
+ '.jsx': 'text/plain; charset=utf-8',
174
+ '.ts': 'text/plain; charset=utf-8',
175
+ '.tsx': 'text/plain; charset=utf-8',
176
+ '.mjs': 'text/plain; charset=utf-8',
177
+ '.cjs': 'text/plain; charset=utf-8',
178
+ '.css': 'text/plain; charset=utf-8',
179
+ '.html': 'text/plain; charset=utf-8',
180
+ '.yml': 'text/plain; charset=utf-8',
181
+ '.yaml': 'text/plain; charset=utf-8',
182
+ };
183
+ return mimeTypes[ext] || 'application/octet-stream';
184
+ }
185
+
186
+ function buildPairingPayload(): PairingQrPayload {
187
+ const window = pairingStore.openWindow();
188
+ const connectionTarget = getAdvertisedConnectionTarget();
189
+
190
+ return {
191
+ type: 'vibelet-pair',
192
+ daemonId: identity.daemonId,
193
+ displayName: identity.displayName,
194
+ canonicalHost: connectionTarget.canonicalHost,
195
+ fallbackHosts: connectionTarget.fallbackHosts.length > 0 ? connectionTarget.fallbackHosts : undefined,
196
+ port: connectionTarget.port,
197
+ pairNonce: window.pairNonce,
198
+ expiresAt: window.expiresAt,
199
+ };
200
+ }
201
+
202
+ async function listDirs(path: string, cwd?: string): Promise<{ entries: DirEntry[] }> {
203
+ const resolvedPath = resolveFileRequestPath(path, cwd);
204
+ const entries = await readdir(resolvedPath);
205
+ const results: DirEntry[] = [];
206
+ for (const name of entries) {
207
+ if (name.startsWith('.')) continue;
208
+ const s = await stat(join(resolvedPath, name)).catch(() => null);
209
+ if (s) results.push({ name, isDirectory: s.isDirectory() });
210
+ }
211
+ // Sort: dirs first, then files
212
+ results.sort((a, b) => {
213
+ if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1;
214
+ return a.name.localeCompare(b.name);
215
+ });
216
+ return { entries: results };
217
+ }
218
+
219
+ // HTTP server for file serving (images) + WebSocket upgrade + health endpoint
220
+ const httpServer = createServer(async (req, res) => {
221
+ const url = new URL(req.url ?? '/', `http://localhost:${config.port}`);
222
+
223
+ // Health endpoint — no token required for basic health check
224
+ if (url.pathname === '/health') {
225
+ const connectionTarget = getAdvertisedConnectionTarget();
226
+ const health: Record<string, unknown> = {
227
+ status: 'ok',
228
+ version: CLI_VERSION,
229
+ daemonId: identity.daemonId,
230
+ displayName: identity.displayName,
231
+ canonicalHost: connectionTarget.canonicalHost,
232
+ uptimeSeconds: Math.round((Date.now() - startTime) / 1000),
233
+ activeSessions: manager.getActiveSessionCount(),
234
+ pairedDevices: pairingStore.pairedCount(),
235
+ wsClients: wss.clients.size,
236
+ drivers: manager.getDriverCounts(),
237
+ sessionInventory: getExternalInventoryHealth(),
238
+ metrics: metrics.snapshot(),
239
+ };
240
+ if (config.relayUrl) {
241
+ health.relayUrl = config.relayUrl;
242
+ }
243
+ const latestVersion = readLatestVersion();
244
+ if (latestVersion) {
245
+ health.latestVersion = latestVersion;
246
+ }
247
+ sendJson(res, 200, health);
248
+ return;
249
+ }
250
+
251
+ if (req.method === 'POST' && url.pathname === '/pair/open') {
252
+ if (!isLoopbackAddress(req.socket.remoteAddress)) {
253
+ sendJson(res, 403, { error: 'forbidden' });
254
+ return;
255
+ }
256
+ sendJson(res, 200, buildPairingPayload());
257
+ return;
258
+ }
259
+
260
+ if (req.method === 'POST' && url.pathname === '/pair/reset') {
261
+ if (!isLoopbackAddress(req.socket.remoteAddress)) {
262
+ sendJson(res, 403, { error: 'forbidden' });
263
+ return;
264
+ }
265
+ pairingStore.reset();
266
+ sendJson(res, 200, { ok: true });
267
+ return;
268
+ }
269
+
270
+ if (req.method === 'POST' && url.pathname === '/shutdown') {
271
+ if (!isLoopbackAddress(req.socket.remoteAddress)) {
272
+ sendJson(res, 403, { error: 'forbidden' });
273
+ return;
274
+ }
275
+ sendJson(res, 200, { status: 'stopping' });
276
+ setTimeout(() => shutdown('HTTP /shutdown request'), 50);
277
+ return;
278
+ }
279
+
280
+ if (req.method === 'POST' && url.pathname === '/hook/claude/session-start') {
281
+ if (!isLoopbackAddress(req.socket.remoteAddress)) {
282
+ sendJson(res, 403, { error: 'forbidden' });
283
+ return;
284
+ }
285
+ try {
286
+ const body = await readJsonBody<ClaudeSessionHookData>(req);
287
+ const secretHeader = req.headers[CLAUDE_HOOK_SECRET_HEADER];
288
+ const secret = Array.isArray(secretHeader) ? secretHeader[0] : secretHeader;
289
+ manager.handleClaudeSessionStartHook(secret, body);
290
+ sendJson(res, 200, { ok: true });
291
+ } catch (error) {
292
+ sendJson(res, 400, { error: String(error) });
293
+ }
294
+ return;
295
+ }
296
+
297
+ if (req.method === 'POST' && url.pathname === '/hook/claude/permission-request') {
298
+ if (!isLoopbackAddress(req.socket.remoteAddress)) {
299
+ sendJson(res, 403, { error: 'forbidden' });
300
+ return;
301
+ }
302
+ try {
303
+ const body = await readJsonBody<ClaudePermissionHookData>(req);
304
+ const secretHeader = req.headers[CLAUDE_HOOK_SECRET_HEADER];
305
+ const secret = Array.isArray(secretHeader) ? secretHeader[0] : secretHeader;
306
+ const response = await manager.handleClaudePermissionHook(secret, body);
307
+ sendJson(res, 200, response);
308
+ } catch (error) {
309
+ sendJson(res, 400, { error: String(error) });
310
+ }
311
+ return;
312
+ }
313
+
314
+ if (req.method === 'POST' && url.pathname === '/pair/create') {
315
+ try {
316
+ const body = await readJsonBody<{
317
+ daemonId?: string;
318
+ pairNonce?: string;
319
+ deviceId?: string;
320
+ deviceName?: string;
321
+ }>(req);
322
+ if (
323
+ body.daemonId !== identity.daemonId ||
324
+ typeof body.pairNonce !== 'string' ||
325
+ typeof body.deviceId !== 'string' ||
326
+ typeof body.deviceName !== 'string'
327
+ ) {
328
+ sendJson(res, 400, { error: 'invalid_pair_request' });
329
+ return;
330
+ }
331
+ const window = pairingStore.consumeWindow(body.pairNonce);
332
+ if (!window) {
333
+ sendJson(res, 401, { error: 'pair_window_expired' });
334
+ return;
335
+ }
336
+ const pairToken = pairingStore.issuePairToken(body.deviceId, body.deviceName);
337
+ const connectionTarget = getAdvertisedConnectionTarget();
338
+ sendJson(res, 200, {
339
+ daemonId: identity.daemonId,
340
+ displayName: identity.displayName,
341
+ canonicalHost: connectionTarget.canonicalHost,
342
+ fallbackHosts: connectionTarget.fallbackHosts,
343
+ port: connectionTarget.port,
344
+ deviceId: body.deviceId,
345
+ pairToken,
346
+ });
347
+ } catch (error) {
348
+ sendJson(res, 400, { error: String(error) });
349
+ }
350
+ return;
351
+ }
352
+
353
+ const token = resolveRequestToken(req.headers.authorization, url.searchParams.get('token'));
354
+ if (!isAuthorizedToken(token, config.legacyToken, (value, touch) => pairingStore.validateAnyPairToken(value, touch), false)) {
355
+ res.writeHead(401);
356
+ res.end('Unauthorized');
357
+ return;
358
+ }
359
+
360
+ // Upload image: POST /upload (raw binary body, Content-Type = image/*)
361
+ if (req.method === 'POST' && url.pathname === '/upload') {
362
+ const UPLOAD_MIME_TO_EXT: Record<string, string> = {
363
+ 'image/png': '.png',
364
+ 'image/jpeg': '.jpg',
365
+ 'image/gif': '.gif',
366
+ 'image/webp': '.webp',
367
+ 'image/heic': '.heic',
368
+ 'image/heif': '.heif',
369
+ };
370
+ const contentType = (req.headers['content-type'] ?? '').split(';')[0].trim();
371
+ const ext = UPLOAD_MIME_TO_EXT[contentType];
372
+ if (!ext) {
373
+ sendJson(res, 400, { error: 'Unsupported content type' });
374
+ return;
375
+ }
376
+
377
+ const MAX_UPLOAD_SIZE = 10 * 1024 * 1024; // 10MB
378
+ const chunks: Buffer[] = [];
379
+ let totalSize = 0;
380
+ for await (const chunk of req) {
381
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
382
+ totalSize += buf.length;
383
+ if (totalSize > MAX_UPLOAD_SIZE) {
384
+ res.writeHead(413);
385
+ res.end('File too large');
386
+ return;
387
+ }
388
+ chunks.push(buf);
389
+ }
390
+
391
+ try {
392
+ await mkdir(VIBELET_UPLOADS_DIR, { recursive: true });
393
+ const filename = `${Date.now()}_${randomBytes(8).toString('hex')}${ext}`;
394
+ const filePath = join(VIBELET_UPLOADS_DIR, filename);
395
+ await writeFile(filePath, Buffer.concat(chunks));
396
+ log.info({ filePath, size: totalSize }, 'file uploaded');
397
+ sendJson(res, 200, { path: filePath });
398
+ } catch (e) {
399
+ log.error({ error: String(e) }, 'upload failed');
400
+ sendJson(res, 500, { error: 'Upload failed' });
401
+ }
402
+ return;
403
+ }
404
+
405
+ // Serve files: GET /file?path=/path/to/file&cwd=/repo&token=xxx
406
+ if (url.pathname === '/file') {
407
+ const filePath = url.searchParams.get('path');
408
+ const cwd = url.searchParams.get('cwd') || undefined;
409
+ if (!filePath) {
410
+ res.writeHead(400);
411
+ res.end('Missing path parameter');
412
+ return;
413
+ }
414
+
415
+ try {
416
+ const resolvedPath = resolveFileRequestPath(filePath, cwd);
417
+ const fileStat = await stat(resolvedPath);
418
+ if (!fileStat.isFile()) {
419
+ res.writeHead(404);
420
+ res.end('Not a file');
421
+ return;
422
+ }
423
+
424
+ const contentType = getFileContentType(resolvedPath);
425
+ const data = await readFile(resolvedPath);
426
+ res.writeHead(200, { 'Content-Type': contentType, 'Content-Length': data.length });
427
+ res.end(data);
428
+ } catch (e) {
429
+ res.writeHead(404);
430
+ res.end('File not found');
431
+ }
432
+ return;
433
+ }
434
+
435
+ res.writeHead(200);
436
+ res.end(`Vibelet Daemon v${CLI_VERSION}`);
437
+ });
438
+
439
+ const wss = new WebSocketServer({ server: httpServer });
440
+
441
+ httpServer.on('connection', (socket) => {
442
+ sockets.add(socket);
443
+ socket.on('close', () => {
444
+ sockets.delete(socket);
445
+ });
446
+ });
447
+
448
+ wss.on('connection', (ws, req) => {
449
+ const url = new URL(req.url ?? '/', `http://localhost:${config.port}`);
450
+ const token = url.searchParams.get('token');
451
+ if (token && isLegacyToken(token, config.legacyToken)) {
452
+ authenticatedClients.set(ws, { authMode: 'query_token' });
453
+ metrics.increment('ws.auth.success', { mode: 'query_token' });
454
+ manager.addGlobalClient(ws);
455
+ manager.prewarmCaches();
456
+ } else if (token) {
457
+ const pairingRecord = pairingStore.validateAnyPairToken(token);
458
+ if (pairingRecord) {
459
+ authenticatedClients.set(ws, {
460
+ authMode: 'query_token',
461
+ deviceId: pairingRecord.deviceId,
462
+ });
463
+ metrics.increment('ws.auth.success', { mode: 'query_token' });
464
+ manager.addGlobalClient(ws);
465
+ manager.prewarmCaches();
466
+ } else {
467
+ metrics.increment('ws.auth.failure', { mode: 'query_token' });
468
+ ws.close(4001, 'Unauthorized');
469
+ return;
470
+ }
471
+ }
472
+
473
+ wsLog.info({ clients: wss.clients.size }, 'client connected');
474
+ metrics.increment('ws.connect');
475
+ metrics.gauge('ws.clients', wss.clients.size);
476
+ audit.emit('ws.connect', {
477
+ clients: wss.clients.size,
478
+ authMode: authenticatedClients.get(ws)?.authMode ?? 'pending',
479
+ deviceId: authenticatedClients.get(ws)?.deviceId,
480
+ });
481
+
482
+ ws.on('message', async (data) => {
483
+ try {
484
+ const parsed = JSON.parse(readRawDataAsText(data));
485
+
486
+ // Basic validation: action must exist and be a string
487
+ if (!parsed || typeof parsed.action !== 'string') {
488
+ ws.send(JSON.stringify({ type: 'error', sessionId: '', message: 'Invalid message: missing or non-string "action" field' }));
489
+ return;
490
+ }
491
+
492
+ // Respond to heartbeat pings before auth check (harmless, keeps connection alive)
493
+ if (parsed.action === 'ping') {
494
+ ws.send(JSON.stringify({ type: 'pong' }));
495
+ return;
496
+ }
497
+
498
+ const msg = parsed as ClientMessage;
499
+
500
+ if (!authenticatedClients.has(ws)) {
501
+ if (
502
+ msg.action === 'auth.hello' &&
503
+ msg.daemonId === identity.daemonId &&
504
+ pairingStore.validatePairToken(msg.deviceId, msg.pairToken)
505
+ ) {
506
+ authenticatedClients.set(ws, {
507
+ authMode: 'auth_hello',
508
+ deviceId: msg.deviceId,
509
+ });
510
+ metrics.increment('ws.auth.success', { mode: 'auth_hello' });
511
+ manager.addGlobalClient(ws);
512
+ manager.prewarmCaches();
513
+ ws.send(JSON.stringify({
514
+ type: 'response',
515
+ id: msg.id,
516
+ ok: true,
517
+ data: {
518
+ daemonId: identity.daemonId,
519
+ displayName: identity.displayName,
520
+ },
521
+ }));
522
+ return;
523
+ }
524
+
525
+ if (msg.action === 'auth.hello') {
526
+ metrics.increment('ws.auth.failure', { mode: 'auth_hello' });
527
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: 'auth_failed' }));
528
+ ws.close(4001, 'Unauthorized');
529
+ return;
530
+ }
531
+
532
+ metrics.increment('ws.auth.failure', { mode: 'missing' });
533
+ ws.send(JSON.stringify({ type: 'error', sessionId: '', message: 'Authentication required' }));
534
+ ws.close(4001, 'Unauthorized');
535
+ return;
536
+ }
537
+
538
+ // Handle log.report: app sends client-side logs to daemon audit trail
539
+ if (msg.action === 'log.report') {
540
+ for (const entry of msg.entries) {
541
+ audit.emitApp(entry.event, entry.level, entry.data ?? {}, entry.ts);
542
+ // Also log to daemon stdout for debugging
543
+ const logFn = entry.level === 'error' ? log.error : entry.level === 'warn' ? log.warn : log.info;
544
+ logFn.call(log, { event: entry.event, ...entry.data }, `[app] ${entry.event}`);
545
+ }
546
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true }));
547
+ return;
548
+ }
549
+
550
+ // Handle dirs.list directly in index.ts
551
+ if (msg.action === 'dirs.list') {
552
+ try {
553
+ const result = await listDirs(msg.path, msg.cwd);
554
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: true, data: result }));
555
+ } catch (e) {
556
+ ws.send(JSON.stringify({ type: 'response', id: msg.id, ok: false, error: String(e) }));
557
+ }
558
+ return;
559
+ }
560
+
561
+ if (parsed.action === 'push.register') {
562
+ const context = authenticatedClients.get(ws);
563
+ handlePushProtocolMessage(parsed, {
564
+ deviceId: context?.deviceId,
565
+ registerToken,
566
+ unregisterToken,
567
+ respond: (response) => ws.send(JSON.stringify(response)),
568
+ });
569
+ return;
570
+ }
571
+
572
+ if (parsed.action === 'push.unregister') {
573
+ const context = authenticatedClients.get(ws);
574
+ handlePushProtocolMessage(parsed, {
575
+ deviceId: context?.deviceId,
576
+ registerToken,
577
+ unregisterToken,
578
+ respond: (response) => ws.send(JSON.stringify(response)),
579
+ });
580
+ return;
581
+ }
582
+
583
+ await manager.handle(ws, msg);
584
+ } catch (e) {
585
+ wsLog.error({ error: String(e) }, 'message handling error');
586
+ ws.send(JSON.stringify({ type: 'error', sessionId: '', message: String(e) }));
587
+ }
588
+ });
589
+
590
+ ws.on('close', (code) => {
591
+ wsLog.info({ clients: wss.clients.size, code }, 'client disconnected');
592
+ metrics.increment('ws.disconnect');
593
+ metrics.increment('ws.disconnect.code', { code: String(code) });
594
+ metrics.gauge('ws.clients', wss.clients.size);
595
+ audit.emit('ws.disconnect', { clients: wss.clients.size, code });
596
+ manager.removeClient(ws);
597
+ });
598
+
599
+ ws.on('error', (err) => {
600
+ wsLog.error({ error: err.message }, 'websocket error');
601
+ });
602
+ });
603
+
604
+ function destroyAllSockets(): void {
605
+ for (const socket of sockets) {
606
+ try {
607
+ socket.destroy();
608
+ } catch {}
609
+ }
610
+ sockets.clear();
611
+ }
612
+
613
+ function runStorageHousekeeping(): void {
614
+ const summary = runStorageHousekeepingSync({
615
+ auditPath: AUDIT_PATH,
616
+ stdoutLogPath: DAEMON_STDOUT_LOG_PATH,
617
+ stderrLogPath: DAEMON_STDERR_LOG_PATH,
618
+ auditMaxBytes: config.auditMaxBytes,
619
+ logMaxBytes: config.daemonLogMaxBytes,
620
+ });
621
+ if (summary.trimmedFiles > 0) {
622
+ log.info({
623
+ trimmedFiles: summary.trimmedFiles,
624
+ reclaimedBytes: summary.reclaimedBytes,
625
+ files: summary.files
626
+ .filter(file => file.trimmed)
627
+ .map(file => ({
628
+ path: file.path,
629
+ beforeBytes: file.beforeBytes,
630
+ afterBytes: file.afterBytes,
631
+ })),
632
+ }, 'trimmed vibelet storage');
633
+ }
634
+ }
635
+
636
+ function startStorageHousekeeping(): void {
637
+ if (storageHousekeepingInterval) return;
638
+ storageHousekeepingInterval = setInterval(runStorageHousekeeping, config.storageHousekeepingIntervalMs);
639
+ storageHousekeepingInterval.unref();
640
+ }
641
+
642
+ function stopStorageHousekeeping(): void {
643
+ if (!storageHousekeepingInterval) return;
644
+ clearInterval(storageHousekeepingInterval);
645
+ storageHousekeepingInterval = null;
646
+ }
647
+
648
+ function shutdown(reason: string, exitCode = 0): void {
649
+ if (shuttingDown) return;
650
+ shuttingDown = true;
651
+ log.info({ reason, exitCode }, 'shutting down');
652
+ audit.emit('daemon.shutdown', { reason, exitCode, uptimeSeconds: Math.round((Date.now() - startTime) / 1000) });
653
+ metrics.stopPeriodicLog();
654
+ stopStorageHousekeeping();
655
+
656
+ manager.shutdown();
657
+
658
+ for (const client of wss.clients) {
659
+ try {
660
+ client.close(1001, 'Server shutting down');
661
+ client.terminate();
662
+ } catch {}
663
+ }
664
+
665
+ wss.close((err) => {
666
+ if (err) log.error({ error: String(err) }, 'websocket close error');
667
+ });
668
+
669
+ httpServer.close((err) => {
670
+ if (err) {
671
+ log.error({ error: String(err) }, 'http close error');
672
+ destroyAllSockets();
673
+ process.exit(1);
674
+ return;
675
+ }
676
+ destroyAllSockets();
677
+ process.exit(exitCode);
678
+ });
679
+
680
+ setTimeout(() => {
681
+ log.error('force exiting after shutdown timeout');
682
+ destroyAllSockets();
683
+ process.exit(exitCode || 1);
684
+ }, 3000).unref();
685
+ }
686
+
687
+ wss.on('error', (err: Error) => {
688
+ wsLog.error({ error: err.message }, 'wss error');
689
+ });
690
+
691
+ httpServer.on('error', (err: NodeJS.ErrnoException) => {
692
+ if (err.code === 'EADDRINUSE') {
693
+ const occupants = collectPortOccupants(config.port).filter((occupant) => occupant.pid !== process.pid);
694
+ log.error(
695
+ { port: config.port, occupants },
696
+ 'port is already in use. Stop the existing process or use VIBE_PORT=<port> to specify another port',
697
+ );
698
+ } else {
699
+ log.error({ error: String(err) }, 'server error');
700
+ }
701
+ shutdown(`httpServer error: ${err.code ?? err.message}`, 1);
702
+ });
703
+
704
+ process.once('SIGINT', () => shutdown('SIGINT'));
705
+ process.once('SIGTERM', () => shutdown('SIGTERM'));
706
+ process.once('SIGHUP', () => shutdown('SIGHUP'));
707
+ process.once('SIGTTIN', () => shutdown('SIGTTIN'));
708
+ process.once('SIGTSTP', () => shutdown('SIGTSTP'));
709
+
710
+ process.on('uncaughtException', (err) => {
711
+ log.error({ error: String(err), stack: err.stack }, 'uncaughtException');
712
+ audit.emit('daemon.error', { type: 'uncaughtException', error: String(err) });
713
+ });
714
+
715
+ process.on('unhandledRejection', (reason) => {
716
+ log.error({ reason: String(reason) }, 'unhandledRejection');
717
+ audit.emit('daemon.error', { type: 'unhandledRejection', reason: String(reason) });
718
+ });
719
+
720
+ httpServer.listen(config.port, async () => {
721
+ runStorageHousekeeping();
722
+ audit.emit('daemon.start', { port: config.port });
723
+ metrics.startPeriodicLog();
724
+ startStorageHousekeeping();
725
+ const startupConnectionTarget = getAdvertisedConnectionTarget();
726
+
727
+ log.info(
728
+ {
729
+ port: config.port,
730
+ pairingPort: startupConnectionTarget.port,
731
+ daemonId: identity.daemonId,
732
+ displayName: identity.displayName,
733
+ canonicalHost: startupConnectionTarget.canonicalHost,
734
+ fallbackHosts: startupConnectionTarget.fallbackHosts,
735
+ claudePath: config.claudePath,
736
+ codexPath: config.codexPath,
737
+ auditMaxBytes: config.auditMaxBytes,
738
+ daemonLogMaxBytes: config.daemonLogMaxBytes,
739
+ storageHousekeepingIntervalMs: config.storageHousekeepingIntervalMs,
740
+ turnStallTimeoutMs: config.turnStallTimeoutMs,
741
+ },
742
+ 'daemon started',
743
+ );
744
+
745
+ const window = pairingStore.openWindow();
746
+ const qrPayload: PairingQrPayload = {
747
+ type: 'vibelet-pair',
748
+ daemonId: identity.daemonId,
749
+ displayName: identity.displayName,
750
+ canonicalHost: startupConnectionTarget.canonicalHost,
751
+ fallbackHosts: startupConnectionTarget.fallbackHosts.length > 0 ? startupConnectionTarget.fallbackHosts : undefined,
752
+ port: startupConnectionTarget.port,
753
+ pairNonce: window.pairNonce,
754
+ expiresAt: window.expiresAt,
755
+ };
756
+
757
+ // Keep the human-friendly banner on stdout for interactive use
758
+ writeStdoutSafe(`
759
+ ╔══════════════════════════════════════╗
760
+ ║ Vibelet Daemon v${CLI_VERSION} ║
761
+ ╠══════════════════════════════════════╣
762
+ ║ Port: ${String(qrPayload.port).padEnd(28)}║
763
+ ║ ID: ${identity.daemonId.slice(0, 28).padEnd(28)}║
764
+ ║ Host: ${qrPayload.canonicalHost.slice(0, 28).padEnd(28)}║
765
+ ╚══════════════════════════════════════╝
766
+
767
+ Pair with: npx @vibelet/cli or npx vibelet
768
+ Host: ${qrPayload.canonicalHost}
769
+ Fallbacks: ${qrPayload.fallbackHosts?.join(', ') || '(none)'}
770
+
771
+ Claude: ${config.claudePath}
772
+ Codex: ${config.codexPath}
773
+ `);
774
+
775
+ // Print QR code for app to scan
776
+ try {
777
+ await writeStartupPairingQr(qrPayload);
778
+ } catch {
779
+ // QR generation is best-effort
780
+ }
781
+ });