heyhank 0.1.0

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 (199) hide show
  1. package/README.md +40 -0
  2. package/bin/cli.ts +168 -0
  3. package/bin/ctl.ts +528 -0
  4. package/bin/generate-token.ts +28 -0
  5. package/dist/apple-touch-icon.png +0 -0
  6. package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
  7. package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
  8. package/dist/assets/CronManager-DDbz-yiT.js +1 -0
  9. package/dist/assets/HelpPage-DMfkzERp.js +1 -0
  10. package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
  11. package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
  12. package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
  13. package/dist/assets/Playground-Fc5cdc5p.js +109 -0
  14. package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
  15. package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
  16. package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
  17. package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
  18. package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
  19. package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
  20. package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
  21. package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
  22. package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
  23. package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
  24. package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
  25. package/dist/assets/index-C8M_PUmX.css +32 -0
  26. package/dist/assets/index-CEqZnThB.js +204 -0
  27. package/dist/assets/sw-register-LSSpj6RU.js +1 -0
  28. package/dist/assets/time-ago-B6r_l9u1.js +1 -0
  29. package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
  30. package/dist/favicon-32-original.png +0 -0
  31. package/dist/favicon-32.png +0 -0
  32. package/dist/favicon.ico +0 -0
  33. package/dist/favicon.svg +8 -0
  34. package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
  35. package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
  36. package/dist/heyhank-mascot-poster.png +0 -0
  37. package/dist/heyhank-mascot.mp4 +0 -0
  38. package/dist/heyhank-mascot.webm +0 -0
  39. package/dist/icon-192-original.png +0 -0
  40. package/dist/icon-192.png +0 -0
  41. package/dist/icon-512-original.png +0 -0
  42. package/dist/icon-512.png +0 -0
  43. package/dist/index.html +21 -0
  44. package/dist/logo-192.png +0 -0
  45. package/dist/logo-512.png +0 -0
  46. package/dist/logo-codex.svg +14 -0
  47. package/dist/logo-docker.svg +4 -0
  48. package/dist/logo-original.png +0 -0
  49. package/dist/logo.png +0 -0
  50. package/dist/logo.svg +14 -0
  51. package/dist/manifest.json +24 -0
  52. package/dist/push-sw.js +34 -0
  53. package/dist/sw.js +1 -0
  54. package/dist/workbox-d2a0910a.js +1 -0
  55. package/package.json +109 -0
  56. package/server/agent-cron-migrator.ts +85 -0
  57. package/server/agent-executor.ts +357 -0
  58. package/server/agent-store.ts +185 -0
  59. package/server/agent-timeout.ts +107 -0
  60. package/server/agent-types.ts +122 -0
  61. package/server/ai-validation-settings.ts +37 -0
  62. package/server/ai-validator.ts +181 -0
  63. package/server/anthropic-provider-migration.ts +48 -0
  64. package/server/assistant-store.ts +272 -0
  65. package/server/auth-manager.ts +150 -0
  66. package/server/auto-approve.ts +153 -0
  67. package/server/auto-namer.ts +36 -0
  68. package/server/backend-adapter.ts +54 -0
  69. package/server/cache-headers.ts +61 -0
  70. package/server/calendar-service.ts +434 -0
  71. package/server/claude-adapter.ts +889 -0
  72. package/server/claude-container-auth.ts +30 -0
  73. package/server/claude-session-discovery.ts +157 -0
  74. package/server/claude-session-history.ts +410 -0
  75. package/server/cli-launcher.ts +1303 -0
  76. package/server/codex-adapter.ts +3027 -0
  77. package/server/codex-container-auth.ts +24 -0
  78. package/server/codex-home.ts +27 -0
  79. package/server/codex-ws-proxy.cjs +226 -0
  80. package/server/commands-discovery.ts +81 -0
  81. package/server/constants.ts +7 -0
  82. package/server/container-manager.ts +1053 -0
  83. package/server/cost-tracker.ts +222 -0
  84. package/server/cron-scheduler.ts +243 -0
  85. package/server/cron-store.ts +148 -0
  86. package/server/cron-types.ts +63 -0
  87. package/server/email-service.ts +354 -0
  88. package/server/env-manager.ts +161 -0
  89. package/server/event-bus-types.ts +75 -0
  90. package/server/event-bus.ts +124 -0
  91. package/server/execution-store.ts +170 -0
  92. package/server/federation/node-connection.ts +190 -0
  93. package/server/federation/node-manager.ts +366 -0
  94. package/server/federation/node-store.ts +86 -0
  95. package/server/federation/node-types.ts +121 -0
  96. package/server/fs-utils.ts +15 -0
  97. package/server/git-utils.ts +421 -0
  98. package/server/github-pr.ts +379 -0
  99. package/server/google-media.ts +342 -0
  100. package/server/image-pull-manager.ts +279 -0
  101. package/server/index.ts +491 -0
  102. package/server/internal-ai.ts +237 -0
  103. package/server/kill-switch.ts +99 -0
  104. package/server/llm-providers.ts +342 -0
  105. package/server/logger.ts +259 -0
  106. package/server/mcp-registry.ts +401 -0
  107. package/server/message-bus.ts +271 -0
  108. package/server/message-delivery.ts +128 -0
  109. package/server/metrics-collector.ts +350 -0
  110. package/server/metrics-types.ts +108 -0
  111. package/server/middleware/managed-auth.ts +195 -0
  112. package/server/novnc-proxy.ts +99 -0
  113. package/server/path-resolver.ts +186 -0
  114. package/server/paths.ts +13 -0
  115. package/server/pr-poller.ts +162 -0
  116. package/server/prompt-manager.ts +211 -0
  117. package/server/protocol/claude-upstream/README.md +19 -0
  118. package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
  119. package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
  120. package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
  121. package/server/protocol/codex-upstream/README.md +18 -0
  122. package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
  123. package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
  124. package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
  125. package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
  126. package/server/protocol-monitor.ts +50 -0
  127. package/server/provider-manager.ts +111 -0
  128. package/server/provider-registry.ts +393 -0
  129. package/server/push-notifications.ts +221 -0
  130. package/server/recorder.ts +374 -0
  131. package/server/recording-hub/compat-validator.ts +284 -0
  132. package/server/recording-hub/diagnostics.ts +299 -0
  133. package/server/recording-hub/hub-config.ts +19 -0
  134. package/server/recording-hub/hub-routes.ts +236 -0
  135. package/server/recording-hub/hub-store.ts +265 -0
  136. package/server/recording-hub/replay-adapter.ts +207 -0
  137. package/server/relay-client.ts +320 -0
  138. package/server/reminder-scheduler.ts +38 -0
  139. package/server/replay.ts +78 -0
  140. package/server/routes/agent-routes.ts +264 -0
  141. package/server/routes/assistant-routes.ts +90 -0
  142. package/server/routes/cron-routes.ts +103 -0
  143. package/server/routes/env-routes.ts +95 -0
  144. package/server/routes/federation-routes.ts +76 -0
  145. package/server/routes/fs-routes.ts +622 -0
  146. package/server/routes/git-routes.ts +97 -0
  147. package/server/routes/llm-routes.ts +166 -0
  148. package/server/routes/media-routes.ts +135 -0
  149. package/server/routes/metrics-routes.ts +13 -0
  150. package/server/routes/platform-routes.ts +1379 -0
  151. package/server/routes/prompt-routes.ts +67 -0
  152. package/server/routes/provider-routes.ts +109 -0
  153. package/server/routes/sandbox-routes.ts +127 -0
  154. package/server/routes/settings-routes.ts +285 -0
  155. package/server/routes/skills-routes.ts +100 -0
  156. package/server/routes/socialmedia-routes.ts +208 -0
  157. package/server/routes/system-routes.ts +228 -0
  158. package/server/routes/tailscale-routes.ts +22 -0
  159. package/server/routes/telephony-routes.ts +259 -0
  160. package/server/routes.ts +1379 -0
  161. package/server/sandbox-manager.ts +168 -0
  162. package/server/service.ts +718 -0
  163. package/server/session-creation-service.ts +457 -0
  164. package/server/session-git-info.ts +104 -0
  165. package/server/session-names.ts +67 -0
  166. package/server/session-orchestrator.ts +824 -0
  167. package/server/session-state-machine.ts +207 -0
  168. package/server/session-store.ts +146 -0
  169. package/server/session-types.ts +511 -0
  170. package/server/settings-manager.ts +149 -0
  171. package/server/shared-context.ts +157 -0
  172. package/server/socialmedia/adapter.ts +15 -0
  173. package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
  174. package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
  175. package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
  176. package/server/socialmedia/manager.ts +227 -0
  177. package/server/socialmedia/store.ts +98 -0
  178. package/server/socialmedia/types.ts +89 -0
  179. package/server/tailscale-manager.ts +451 -0
  180. package/server/telephony/audio-bridge.ts +331 -0
  181. package/server/telephony/call-manager.ts +457 -0
  182. package/server/telephony/call-types.ts +108 -0
  183. package/server/telephony/telephony-store.ts +119 -0
  184. package/server/terminal-manager.ts +240 -0
  185. package/server/update-checker.ts +192 -0
  186. package/server/usage-limits.ts +225 -0
  187. package/server/web-push.d.ts +51 -0
  188. package/server/worktree-tracker.ts +84 -0
  189. package/server/ws-auth.ts +41 -0
  190. package/server/ws-bridge-browser-ingest.ts +72 -0
  191. package/server/ws-bridge-browser.ts +112 -0
  192. package/server/ws-bridge-cli-ingest.ts +81 -0
  193. package/server/ws-bridge-codex.ts +266 -0
  194. package/server/ws-bridge-controls.ts +20 -0
  195. package/server/ws-bridge-persist.ts +66 -0
  196. package/server/ws-bridge-publish.ts +79 -0
  197. package/server/ws-bridge-replay.ts +61 -0
  198. package/server/ws-bridge-types.ts +121 -0
  199. package/server/ws-bridge.ts +1240 -0
@@ -0,0 +1,320 @@
1
+ // ─── Relay Client ────────────────────────────────────────────────────────────
2
+ // Connects HeyHank to a cloud relay worker via outbound WebSocket.
3
+ // This allows a HeyHank instance running behind a firewall (NAT, no public IP)
4
+ // to receive external webhooks relayed through a cloud worker.
5
+ //
6
+ // Flow:
7
+ // HeyHank --[outbound WS]--> Relay Worker <--[HTTPS webhooks]-- External platforms
8
+ // HeyHank receives webhook_request messages, processes them locally,
9
+ // and sends webhook_response messages back through the same WebSocket.
10
+
11
+ type WebhookHandler = (req: Request, opts?: { waitUntil?: (task: Promise<unknown>) => void }) => Promise<Response>;
12
+
13
+ /** Generic interface for webhook routing in the relay client */
14
+ export interface RelayWebhookRouter {
15
+ /** Global webhook handlers keyed by platform name */
16
+ webhooks: Record<string, WebhookHandler>;
17
+ /** Get a webhook handler for a specific agent + platform combination */
18
+ getWebhookHandler?: (agentId: string, platform: string) => WebhookHandler | null;
19
+ }
20
+
21
+ /** Inbound message from the relay worker containing a webhook to process */
22
+ interface WebhookRequestMessage {
23
+ type: "webhook_request";
24
+ requestId: string;
25
+ platform: string;
26
+ /** Optional agent ID for agent-scoped webhook routing */
27
+ agentId?: string;
28
+ method: string;
29
+ headers: Record<string, string>;
30
+ body: string;
31
+ }
32
+
33
+ /** Outbound message sent back to the relay worker with the webhook response */
34
+ interface WebhookResponseMessage {
35
+ type: "webhook_response";
36
+ requestId: string;
37
+ status: number;
38
+ headers: Record<string, string>;
39
+ body: string;
40
+ }
41
+
42
+ /** Any message that may arrive from the relay worker */
43
+ type RelayIncomingMessage = WebhookRequestMessage | { type: string; [key: string]: unknown };
44
+
45
+ const MIN_RECONNECT_DELAY_MS = 1_000;
46
+ const MAX_RECONNECT_DELAY_MS = 30_000;
47
+
48
+ export class RelayClient {
49
+ private relayUrl: string;
50
+ private relaySecret: string;
51
+ private router: RelayWebhookRouter;
52
+
53
+ private ws: WebSocket | null = null;
54
+ private reconnectDelay = MIN_RECONNECT_DELAY_MS;
55
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
56
+ private intentionalDisconnect = false;
57
+
58
+ constructor(relayUrl: string, relaySecret: string, router: RelayWebhookRouter) {
59
+ this.relayUrl = relayUrl;
60
+ this.relaySecret = relaySecret;
61
+ this.router = router;
62
+ }
63
+
64
+ /**
65
+ * Open a WebSocket connection to the relay worker.
66
+ * Automatically reconnects with exponential backoff on disconnection.
67
+ */
68
+ connect(): void {
69
+ this.intentionalDisconnect = false;
70
+ this.clearReconnectTimer();
71
+
72
+ // Convert http(s) URL to ws(s) URL
73
+ const wsUrl = this.buildWsUrl();
74
+ const displayUrl = wsUrl.replace(/([?&])(secret)=[^&]*/gi, "$1$2=***");
75
+ console.log(`[relay-client] Connecting to ${displayUrl}`);
76
+
77
+ try {
78
+ this.ws = new WebSocket(wsUrl);
79
+ } catch (err) {
80
+ console.log(`[relay-client] Failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`);
81
+ this.scheduleReconnect();
82
+ return;
83
+ }
84
+
85
+ this.ws.addEventListener("open", () => {
86
+ console.log("[relay-client] Connected to relay worker");
87
+ this.resetBackoff();
88
+ });
89
+
90
+ this.ws.addEventListener("message", (event: MessageEvent) => {
91
+ this.handleRawMessage(event.data);
92
+ });
93
+
94
+ this.ws.addEventListener("close", (event: CloseEvent) => {
95
+ console.log(`[relay-client] Connection closed (code=${event.code}, reason=${event.reason || "none"})`);
96
+ this.ws = null;
97
+
98
+ if (!this.intentionalDisconnect) {
99
+ this.scheduleReconnect();
100
+ }
101
+ });
102
+
103
+ this.ws.addEventListener("error", (event: Event) => {
104
+ // The error event does not carry much detail in browser-style WebSocket API;
105
+ // the subsequent close event will trigger reconnection.
106
+ console.log(`[relay-client] WebSocket error: ${String(event)}`);
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Gracefully close the connection and stop reconnection attempts.
112
+ */
113
+ disconnect(): void {
114
+ this.intentionalDisconnect = true;
115
+ this.clearReconnectTimer();
116
+
117
+ if (this.ws) {
118
+ console.log("[relay-client] Disconnecting from relay worker");
119
+ try {
120
+ this.ws.close(1000, "Client shutting down");
121
+ } catch {
122
+ // Ignore errors on close — socket may already be closed
123
+ }
124
+ this.ws = null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Handle a parsed webhook_request message by forwarding it to the webhook router
130
+ * and sending the response back to the relay worker.
131
+ */
132
+ async handleWebhookRequest(msg: WebhookRequestMessage): Promise<void> {
133
+ const { requestId, platform, agentId, method, headers, body } = msg;
134
+
135
+ // Route to agent-scoped handler if agentId is provided, otherwise use global handler
136
+ const webhookHandler = agentId
137
+ ? this.router.getWebhookHandler?.(agentId, platform) ?? null
138
+ : this.router.webhooks[platform];
139
+
140
+ if (!webhookHandler) {
141
+ const target = agentId ? `agent "${agentId}" / platform "${platform}"` : `platform "${platform}"`;
142
+ console.log(`[relay-client] No webhook handler for ${target}, returning 404`);
143
+ this.sendWebhookResponse({
144
+ type: "webhook_response",
145
+ requestId,
146
+ status: 404,
147
+ headers: {},
148
+ body: `No webhook handler for ${target}`,
149
+ });
150
+ return;
151
+ }
152
+
153
+ try {
154
+ // Construct a Request object from the relayed data
155
+ const hasBody = method !== "GET" && method !== "HEAD" && body;
156
+ const request = new Request(`https://relay-proxy/${platform}`, {
157
+ method,
158
+ headers: new Headers(headers),
159
+ body: hasBody ? body : undefined,
160
+ });
161
+
162
+ // Execute the webhook handler
163
+ const response = await webhookHandler(request, {
164
+ waitUntil: (task: Promise<unknown>) => {
165
+ task.catch((err) => console.error("[relay-client] Background task error:", err));
166
+ },
167
+ });
168
+
169
+ // Extract response details
170
+ const responseBody = await response.text();
171
+ const responseHeaders: Record<string, string> = {};
172
+ response.headers.forEach((value, key) => {
173
+ responseHeaders[key] = value;
174
+ });
175
+
176
+ this.sendWebhookResponse({
177
+ type: "webhook_response",
178
+ requestId,
179
+ status: response.status,
180
+ headers: responseHeaders,
181
+ body: responseBody,
182
+ });
183
+ } catch (err) {
184
+ console.log(`[relay-client] Error processing webhook for platform "${platform}": ${err instanceof Error ? err.message : String(err)}`);
185
+ this.sendWebhookResponse({
186
+ type: "webhook_response",
187
+ requestId,
188
+ status: 500,
189
+ headers: {},
190
+ body: "Internal error",
191
+ });
192
+ }
193
+ }
194
+
195
+ // ─── Private helpers ─────────────────────────────────────────────────────────
196
+
197
+ /**
198
+ * Build the WebSocket URL from the relay HTTP URL.
199
+ * Converts https:// to wss:// and http:// to ws://.
200
+ */
201
+ private buildWsUrl(): string {
202
+ let url = this.relayUrl;
203
+
204
+ if (url.startsWith("https://")) {
205
+ url = "wss://" + url.slice("https://".length);
206
+ } else if (url.startsWith("http://")) {
207
+ url = "ws://" + url.slice("http://".length);
208
+ }
209
+
210
+ // Remove trailing slash if present before appending path
211
+ url = url.replace(/\/+$/, "");
212
+
213
+ // NOTE: The secret is passed as a query param for simplicity. This means
214
+ // it may appear in relay-side HTTP access logs. For higher security, migrate
215
+ // to an auth-frame approach (send secret as the first WebSocket message after
216
+ // connection opens). Rotate the secret regularly if using this approach.
217
+ return `${url}/ws/relay?secret=${encodeURIComponent(this.relaySecret)}`;
218
+ }
219
+
220
+ /**
221
+ * Parse and route an incoming raw WebSocket message.
222
+ */
223
+ private handleRawMessage(data: unknown): void {
224
+ let parsed: RelayIncomingMessage;
225
+
226
+ try {
227
+ const text = typeof data === "string" ? data : String(data);
228
+ parsed = JSON.parse(text) as RelayIncomingMessage;
229
+ } catch (err) {
230
+ console.log(`[relay-client] Failed to parse message: ${err instanceof Error ? err.message : String(err)}`);
231
+ return;
232
+ }
233
+
234
+ if (!parsed || typeof parsed.type !== "string") {
235
+ console.log("[relay-client] Received message without a valid type field, ignoring");
236
+ return;
237
+ }
238
+
239
+ switch (parsed.type) {
240
+ case "webhook_request":
241
+ // Validate required fields before processing
242
+ if (!this.isValidWebhookRequest(parsed)) {
243
+ console.log("[relay-client] Received malformed webhook_request, ignoring");
244
+ return;
245
+ }
246
+ // Fire-and-forget: errors are caught inside handleWebhookRequest
247
+ void this.handleWebhookRequest(parsed as WebhookRequestMessage);
248
+ break;
249
+
250
+ default:
251
+ console.log(`[relay-client] Received unknown message type: ${parsed.type}`);
252
+ break;
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Validate that a parsed message has all required webhook_request fields.
258
+ */
259
+ private isValidWebhookRequest(msg: RelayIncomingMessage): msg is WebhookRequestMessage {
260
+ return (
261
+ msg.type === "webhook_request" &&
262
+ typeof (msg as WebhookRequestMessage).requestId === "string" &&
263
+ typeof (msg as WebhookRequestMessage).platform === "string" &&
264
+ typeof (msg as WebhookRequestMessage).method === "string" &&
265
+ typeof (msg as WebhookRequestMessage).headers === "object" &&
266
+ (msg as WebhookRequestMessage).headers !== null &&
267
+ typeof (msg as WebhookRequestMessage).body === "string"
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Send a webhook response message back through the WebSocket.
273
+ */
274
+ private sendWebhookResponse(response: WebhookResponseMessage): void {
275
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
276
+ console.log(`[relay-client] Cannot send response for ${response.requestId}: WebSocket not open`);
277
+ return;
278
+ }
279
+
280
+ try {
281
+ this.ws.send(JSON.stringify(response));
282
+ } catch (err) {
283
+ console.log(`[relay-client] Failed to send response for ${response.requestId}: ${err instanceof Error ? err.message : String(err)}`);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Schedule a reconnection attempt with exponential backoff.
289
+ */
290
+ private scheduleReconnect(): void {
291
+ if (this.intentionalDisconnect) return;
292
+
293
+ const delay = this.reconnectDelay;
294
+ console.log(`[relay-client] Reconnecting in ${delay}ms`);
295
+
296
+ this.reconnectTimer = setTimeout(() => {
297
+ this.reconnectTimer = null;
298
+ // Double the delay for next time, capped at max
299
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
300
+ this.connect();
301
+ }, delay);
302
+ }
303
+
304
+ /**
305
+ * Reset backoff delay to the minimum after a successful connection.
306
+ */
307
+ private resetBackoff(): void {
308
+ this.reconnectDelay = MIN_RECONNECT_DELAY_MS;
309
+ }
310
+
311
+ /**
312
+ * Clear any pending reconnection timer.
313
+ */
314
+ private clearReconnectTimer(): void {
315
+ if (this.reconnectTimer !== null) {
316
+ clearTimeout(this.reconnectTimer);
317
+ this.reconnectTimer = null;
318
+ }
319
+ }
320
+ }
@@ -0,0 +1,38 @@
1
+ // ─── Reminder Scheduler ──────────────────────────────────────────────────────
2
+ // Checks every 60 seconds for due reminders and sends push notifications.
3
+
4
+ import { getDueReminders, fireReminder } from "./assistant-store.js";
5
+ import { sendNotification } from "./push-notifications.js";
6
+
7
+ let intervalId: ReturnType<typeof setInterval> | null = null;
8
+
9
+ async function checkReminders(): Promise<void> {
10
+ const due = getDueReminders();
11
+ for (const reminder of due) {
12
+ try {
13
+ await sendNotification("Erinnerung", reminder.text, {
14
+ tag: `reminder-${reminder.id}`,
15
+ icon: "/icon-192.png",
16
+ });
17
+ console.log(`[reminder-scheduler] Fired: "${reminder.text}"`);
18
+ } catch (err) {
19
+ console.error(`[reminder-scheduler] Failed to send notification for "${reminder.text}":`, err);
20
+ }
21
+ fireReminder(reminder.id);
22
+ }
23
+ }
24
+
25
+ export function startReminderScheduler(): void {
26
+ if (intervalId) return;
27
+ // Check immediately on start, then every 60 seconds
28
+ checkReminders();
29
+ intervalId = setInterval(checkReminders, 60_000);
30
+ console.log("[reminder-scheduler] Started (checking every 60s)");
31
+ }
32
+
33
+ export function stopReminderScheduler(): void {
34
+ if (intervalId) {
35
+ clearInterval(intervalId);
36
+ intervalId = null;
37
+ }
38
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Replay utility for session recordings.
3
+ *
4
+ * Loads JSONL recording files and replays them through WsBridge or CodexAdapter
5
+ * to produce browser messages. Used in tests to validate that message processing
6
+ * produces the expected output from recorded real sessions.
7
+ */
8
+
9
+ import { readFileSync } from "node:fs";
10
+ import type { RecordingHeader, RecordingEntry } from "./recorder.js";
11
+
12
+ // ─── Types ───────────────────────────────────────────────────────────────────
13
+
14
+ export interface Recording {
15
+ header: RecordingHeader;
16
+ entries: RecordingEntry[];
17
+ }
18
+
19
+ // ─── Loading ─────────────────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Load a JSONL recording file. Returns the parsed header and all entries.
23
+ * Throws if the file is missing a valid header.
24
+ */
25
+ export function loadRecording(path: string): Recording {
26
+ const content = readFileSync(path, "utf-8");
27
+ const lines = content.split("\n").filter((l) => l.trim());
28
+
29
+ if (lines.length === 0) {
30
+ throw new Error("Recording file is empty");
31
+ }
32
+
33
+ const header = JSON.parse(lines[0]) as RecordingHeader;
34
+ if (!header._header || header.version !== 1) {
35
+ throw new Error("Invalid recording header: missing _header or version !== 1");
36
+ }
37
+
38
+ const entries: RecordingEntry[] = [];
39
+ for (let i = 1; i < lines.length; i++) {
40
+ try {
41
+ entries.push(JSON.parse(lines[i]) as RecordingEntry);
42
+ } catch {
43
+ // Skip malformed lines — recording might have been truncated
44
+ }
45
+ }
46
+
47
+ return { header, entries };
48
+ }
49
+
50
+ // ─── Replay helpers ──────────────────────────────────────────────────────────
51
+
52
+ /**
53
+ * Filter recording entries by direction and channel.
54
+ * Useful for extracting only incoming CLI messages for replay.
55
+ */
56
+ export function filterEntries(
57
+ entries: RecordingEntry[],
58
+ dir: "in" | "out",
59
+ channel: "cli" | "browser",
60
+ ): RecordingEntry[] {
61
+ return entries.filter((e) => e.dir === dir && e.ch === channel);
62
+ }
63
+
64
+ /**
65
+ * Get all outgoing browser messages from a recording.
66
+ * These represent what the server actually sent to browsers during the recorded session.
67
+ */
68
+ export function getExpectedBrowserMessages(entries: RecordingEntry[]): string[] {
69
+ return filterEntries(entries, "out", "browser").map((e) => e.raw);
70
+ }
71
+
72
+ /**
73
+ * Get all incoming CLI messages from a recording.
74
+ * These are the raw NDJSON/JSON-RPC lines received from the backend.
75
+ */
76
+ export function getIncomingCLIMessages(entries: RecordingEntry[]): string[] {
77
+ return filterEntries(entries, "in", "cli").map((e) => e.raw);
78
+ }