claude-synapse 1.0.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.
@@ -0,0 +1,679 @@
1
+ import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
+
3
+ // apps/collector/dist/index.js
4
+ import { existsSync } from "node:fs";
5
+ import { resolve, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import Fastify from "fastify";
8
+ import cors from "@fastify/cors";
9
+ import fastifyStatic from "@fastify/static";
10
+ import { Server as SocketIOServer } from "socket.io";
11
+
12
+ // apps/collector/dist/routes/hooks.js
13
+ import { stat } from "node:fs/promises";
14
+
15
+ // packages/protocol/dist/labels.js
16
+ var toolLabels = {
17
+ Bash: { active: "Running command", done: "Ran command", icon: ">" },
18
+ Write: { active: "Writing code", done: "Wrote file", icon: "\u270E" },
19
+ Edit: { active: "Editing code", done: "Edited file", icon: "\u270E" },
20
+ Read: { active: "Reading file", done: "Read file", icon: "\u25C9" },
21
+ Grep: { active: "Searching code", done: "Searched code", icon: "\u2315" },
22
+ Glob: { active: "Finding files", done: "Found files", icon: "\u2315" },
23
+ Agent: { active: "Spawning agent", done: "Agent finished", icon: "\u25CE" },
24
+ WebSearch: { active: "Searching the web", done: "Web search done", icon: "\u2295" },
25
+ WebFetch: { active: "Fetching page", done: "Fetched page", icon: "\u2295" },
26
+ NotebookEdit: { active: "Editing notebook", done: "Edited notebook", icon: "\u270E" },
27
+ TodoWrite: { active: "Updating tasks", done: "Updated tasks", icon: "\u2611" },
28
+ TaskCreate: { active: "Creating task", done: "Created task", icon: "\u2611" },
29
+ TaskUpdate: { active: "Updating task", done: "Updated task", icon: "\u2611" },
30
+ EnterPlanMode: { active: "Planning", done: "Plan ready", icon: "\u25C7" },
31
+ ExitPlanMode: { active: "Finishing plan", done: "Plan done", icon: "\u25C7" },
32
+ AskUserQuestion: { active: "Asking user", done: "Got answer", icon: "?" },
33
+ Skill: { active: "Running skill", done: "Skill complete", icon: "\u26A1" }
34
+ };
35
+ var agentTypeLabels = {
36
+ Explore: { active: "Exploring codebase", done: "Exploration done", icon: "\u2315" },
37
+ Plan: { active: "Planning approach", done: "Plan complete", icon: "\u25C7" },
38
+ "general-purpose": { active: "Working on task", done: "Task complete", icon: "\u25CE" },
39
+ root: { active: "Thinking", done: "Done", icon: "\u25CE" }
40
+ };
41
+ function getToolLabel(toolName, phase = "active") {
42
+ const entry = toolLabels[toolName];
43
+ if (!entry)
44
+ return phase === "active" ? `Using ${toolName}` : `Used ${toolName}`;
45
+ return entry[phase];
46
+ }
47
+ function getAgentLabel(agentType, phase = "active") {
48
+ const entry = agentTypeLabels[agentType];
49
+ if (!entry)
50
+ return phase === "active" ? agentType : `${agentType} done`;
51
+ return entry[phase];
52
+ }
53
+ function getToolDetail(toolName, toolInput) {
54
+ if (!toolInput)
55
+ return "";
56
+ switch (toolName) {
57
+ case "Bash":
58
+ return toolInput.description || toolInput.command?.slice(0, 60) || "";
59
+ case "Write":
60
+ case "Read":
61
+ return shortPath(toolInput.file_path);
62
+ case "Edit":
63
+ return shortPath(toolInput.file_path);
64
+ case "Grep":
65
+ return toolInput.pattern || "";
66
+ case "Glob":
67
+ return toolInput.pattern || "";
68
+ case "Agent":
69
+ return toolInput.description || toolInput.subagent_type || "";
70
+ case "WebSearch":
71
+ return toolInput.query?.slice(0, 50) || "";
72
+ case "WebFetch":
73
+ return shortUrl(toolInput.url);
74
+ default:
75
+ return "";
76
+ }
77
+ }
78
+ function shortPath(p) {
79
+ if (!p)
80
+ return "";
81
+ const parts = p.split("/");
82
+ return parts.length > 2 ? `.../${parts.slice(-2).join("/")}` : p;
83
+ }
84
+ function shortUrl(u) {
85
+ if (!u)
86
+ return "";
87
+ try {
88
+ return new URL(u).hostname;
89
+ } catch {
90
+ return u.slice(0, 40);
91
+ }
92
+ }
93
+
94
+ // packages/protocol/dist/transforms.js
95
+ var eventCounter = 0;
96
+ function generateId() {
97
+ return `evt_${Date.now()}_${++eventCounter}`;
98
+ }
99
+ var activeSessions = /* @__PURE__ */ new Set();
100
+ var knownSubagents = /* @__PURE__ */ new Map();
101
+ var pendingAgentModels = /* @__PURE__ */ new Map();
102
+ function transformClaudeHook(hookEvent, body) {
103
+ const events = [];
104
+ const now = Date.now();
105
+ const sessionId = body.session_id || "session_default";
106
+ const rootAgentId = `root_${sessionId}`;
107
+ const commonMeta = {};
108
+ if (body.permission_mode)
109
+ commonMeta.permissionMode = body.permission_mode;
110
+ if (body.cwd)
111
+ commonMeta.cwd = body.cwd;
112
+ if (body.transcript_path)
113
+ commonMeta.transcriptPath = body.transcript_path;
114
+ if (!activeSessions.has(sessionId)) {
115
+ activeSessions.add(sessionId);
116
+ events.push({
117
+ id: generateId(),
118
+ type: "session_start",
119
+ timestamp: now,
120
+ sessionId,
121
+ agentId: "system",
122
+ agentName: "Session",
123
+ agentType: "system",
124
+ parentAgentId: null,
125
+ payload: { metadata: commonMeta },
126
+ source: "claude_hooks"
127
+ });
128
+ events.push({
129
+ id: generateId(),
130
+ type: "agent_spawned",
131
+ timestamp: now,
132
+ sessionId,
133
+ agentId: rootAgentId,
134
+ agentName: "Claude",
135
+ agentType: "root",
136
+ parentAgentId: null,
137
+ payload: {},
138
+ source: "claude_hooks"
139
+ });
140
+ }
141
+ if (Object.keys(commonMeta).length > 0) {
142
+ events.push({
143
+ id: generateId(),
144
+ type: "session_meta",
145
+ timestamp: now,
146
+ sessionId,
147
+ agentId: "system",
148
+ agentName: "Session",
149
+ agentType: "system",
150
+ parentAgentId: null,
151
+ payload: { metadata: commonMeta },
152
+ source: "claude_hooks"
153
+ });
154
+ }
155
+ if (hookEvent === "SessionStart") {
156
+ const meta = { ...commonMeta };
157
+ if (body.model)
158
+ meta.model = body.model;
159
+ if (body.source)
160
+ meta.sessionSource = body.source;
161
+ events.push({
162
+ id: generateId(),
163
+ type: "session_meta",
164
+ timestamp: now,
165
+ sessionId,
166
+ agentId: "system",
167
+ agentName: "Session started",
168
+ agentType: "system",
169
+ parentAgentId: null,
170
+ payload: {
171
+ message: body.source === "compact" ? "Context compacted \u2014 session resumed" : body.source === "resume" ? "Session resumed" : body.model ? `Model: ${body.model}` : "Session started",
172
+ metadata: meta
173
+ },
174
+ source: "claude_hooks"
175
+ });
176
+ } else if (hookEvent === "PreCompact") {
177
+ events.push({
178
+ id: generateId(),
179
+ type: "context_compact",
180
+ timestamp: now,
181
+ sessionId,
182
+ agentId: "system",
183
+ agentName: "Context compaction",
184
+ agentType: "system",
185
+ parentAgentId: null,
186
+ payload: {
187
+ message: body.trigger === "manual" ? "Manual context compaction (/compact)" : "Auto context compaction (nearing limit)",
188
+ metadata: {
189
+ trigger: body.trigger,
190
+ customInstructions: body.custom_instructions
191
+ }
192
+ },
193
+ source: "claude_hooks"
194
+ });
195
+ } else if (hookEvent === "UserPromptSubmit") {
196
+ events.push({
197
+ id: generateId(),
198
+ type: "user_prompt",
199
+ timestamp: now,
200
+ sessionId,
201
+ agentId: "system",
202
+ agentName: "User prompt",
203
+ agentType: "system",
204
+ parentAgentId: null,
205
+ payload: {
206
+ message: body.prompt?.slice(0, 200)
207
+ },
208
+ source: "claude_hooks"
209
+ });
210
+ events.push({
211
+ id: generateId(),
212
+ type: "agent_thinking",
213
+ timestamp: now,
214
+ sessionId,
215
+ agentId: rootAgentId,
216
+ agentName: "Claude",
217
+ agentType: "root",
218
+ parentAgentId: null,
219
+ payload: { message: "Processing prompt..." },
220
+ source: "claude_hooks"
221
+ });
222
+ } else if (hookEvent === "Notification") {
223
+ events.push({
224
+ id: generateId(),
225
+ type: "notification",
226
+ timestamp: now,
227
+ sessionId,
228
+ agentId: "system",
229
+ agentName: body.title || "Notification",
230
+ agentType: "system",
231
+ parentAgentId: null,
232
+ payload: {
233
+ message: body.message,
234
+ metadata: {
235
+ notificationType: body.notification_type
236
+ }
237
+ },
238
+ source: "claude_hooks"
239
+ });
240
+ } else if (hookEvent === "SessionEnd") {
241
+ events.push({
242
+ id: generateId(),
243
+ type: "session_end",
244
+ timestamp: now,
245
+ sessionId,
246
+ agentId: "system",
247
+ agentName: "Session ended",
248
+ agentType: "system",
249
+ parentAgentId: null,
250
+ payload: {
251
+ message: `Session ended: ${body.reason || "unknown"}`,
252
+ metadata: { reason: body.reason }
253
+ },
254
+ source: "claude_hooks"
255
+ });
256
+ activeSessions.delete(sessionId);
257
+ for (const [id, sub] of knownSubagents) {
258
+ if (sub.sessionId === sessionId)
259
+ knownSubagents.delete(id);
260
+ }
261
+ } else if (hookEvent === "PermissionRequest") {
262
+ const toolName = body.tool_name || "unknown";
263
+ const detail = getToolDetail(toolName, body.tool_input);
264
+ events.push({
265
+ id: generateId(),
266
+ type: "notification",
267
+ timestamp: now,
268
+ sessionId,
269
+ agentId: "system",
270
+ agentName: "Permission request",
271
+ agentType: "system",
272
+ parentAgentId: null,
273
+ payload: {
274
+ message: `${getToolLabel(toolName, "active")}${detail ? ": " + detail : ""}`,
275
+ metadata: { notificationType: "permission_prompt", toolName }
276
+ },
277
+ source: "claude_hooks"
278
+ });
279
+ } else if (hookEvent === "SubagentStart") {
280
+ const agentId = body.agent_id || `sub_${generateId()}`;
281
+ const agentType = body.agent_type || "subagent";
282
+ const queue = pendingAgentModels.get(sessionId);
283
+ const agentModel = queue?.shift() || null;
284
+ if (queue && queue.length === 0)
285
+ pendingAgentModels.delete(sessionId);
286
+ knownSubagents.set(agentId, { sessionId, agentType });
287
+ events.push({
288
+ id: generateId(),
289
+ type: "agent_spawned",
290
+ timestamp: now,
291
+ sessionId,
292
+ agentId,
293
+ agentName: getAgentLabel(agentType, "active"),
294
+ agentType,
295
+ parentAgentId: rootAgentId,
296
+ payload: { metadata: agentModel ? { model: agentModel } : void 0 },
297
+ source: "claude_hooks"
298
+ });
299
+ } else if (hookEvent === "SubagentStop") {
300
+ const agentId = body.agent_id || "unknown_sub";
301
+ const sub = knownSubagents.get(agentId);
302
+ const agentType = sub?.agentType || body.agent_type || "subagent";
303
+ events.push({
304
+ id: generateId(),
305
+ type: "agent_completed",
306
+ timestamp: now,
307
+ sessionId,
308
+ agentId,
309
+ agentName: getAgentLabel(agentType, "done"),
310
+ agentType,
311
+ parentAgentId: rootAgentId,
312
+ payload: {
313
+ message: body.last_assistant_message?.slice(0, 200)
314
+ },
315
+ source: "claude_hooks"
316
+ });
317
+ } else if (hookEvent === "PreToolUse") {
318
+ const toolName = body.tool_name || "unknown";
319
+ const toolUseId = body.tool_use_id || `tool_${generateId()}`;
320
+ const detail = getToolDetail(toolName, body.tool_input);
321
+ if (toolName === "Agent" && body.tool_input?.model) {
322
+ const queue = pendingAgentModels.get(sessionId) || [];
323
+ queue.push(body.tool_input.model);
324
+ pendingAgentModels.set(sessionId, queue);
325
+ }
326
+ events.push({
327
+ id: generateId(),
328
+ type: "agent_spawned",
329
+ timestamp: now,
330
+ sessionId,
331
+ agentId: toolUseId,
332
+ agentName: getToolLabel(toolName, "active"),
333
+ agentType: toolName,
334
+ parentAgentId: rootAgentId,
335
+ payload: {
336
+ tool: { toolName, toolInput: body.tool_input },
337
+ message: detail
338
+ },
339
+ source: "claude_hooks"
340
+ });
341
+ events.push({
342
+ id: generateId(),
343
+ type: "agent_tool_start",
344
+ timestamp: now,
345
+ sessionId,
346
+ agentId: toolUseId,
347
+ agentName: getToolLabel(toolName, "active"),
348
+ agentType: toolName,
349
+ parentAgentId: rootAgentId,
350
+ payload: {
351
+ tool: { toolName, toolInput: body.tool_input },
352
+ message: detail
353
+ },
354
+ source: "claude_hooks"
355
+ });
356
+ events.push({
357
+ id: generateId(),
358
+ type: "agent_tool_start",
359
+ timestamp: now,
360
+ sessionId,
361
+ agentId: rootAgentId,
362
+ agentName: "Claude",
363
+ agentType: "root",
364
+ parentAgentId: null,
365
+ payload: {
366
+ tool: { toolName }
367
+ },
368
+ source: "claude_hooks"
369
+ });
370
+ } else if (hookEvent === "PostToolUse") {
371
+ const toolName = body.tool_name || "unknown";
372
+ const toolUseId = body.tool_use_id || "";
373
+ if (toolUseId) {
374
+ events.push({
375
+ id: generateId(),
376
+ type: "agent_completed",
377
+ timestamp: now,
378
+ sessionId,
379
+ agentId: toolUseId,
380
+ agentName: getToolLabel(toolName, "done"),
381
+ agentType: toolName,
382
+ parentAgentId: rootAgentId,
383
+ payload: {
384
+ tool: {
385
+ toolName,
386
+ toolResult: typeof body.tool_response === "string" ? body.tool_response.slice(0, 500) : body.tool_response ? JSON.stringify(body.tool_response).slice(0, 500) : void 0
387
+ }
388
+ },
389
+ source: "claude_hooks"
390
+ });
391
+ events.push({
392
+ id: generateId(),
393
+ type: "agent_tool_end",
394
+ timestamp: now,
395
+ sessionId,
396
+ agentId: rootAgentId,
397
+ agentName: "Claude",
398
+ agentType: "root",
399
+ parentAgentId: null,
400
+ payload: {
401
+ tool: { toolName }
402
+ },
403
+ source: "claude_hooks"
404
+ });
405
+ }
406
+ } else if (hookEvent === "PostToolUseFailure") {
407
+ const toolName = body.tool_name || "unknown";
408
+ const toolUseId = body.tool_use_id || "";
409
+ if (toolUseId) {
410
+ events.push({
411
+ id: generateId(),
412
+ type: "agent_error",
413
+ timestamp: now,
414
+ sessionId,
415
+ agentId: toolUseId,
416
+ agentName: getToolLabel(toolName, "done"),
417
+ agentType: toolName,
418
+ parentAgentId: rootAgentId,
419
+ payload: {
420
+ error: body.error,
421
+ tool: { toolName, error: body.error }
422
+ },
423
+ source: "claude_hooks"
424
+ });
425
+ events.push({
426
+ id: generateId(),
427
+ type: "agent_tool_end",
428
+ timestamp: now,
429
+ sessionId,
430
+ agentId: rootAgentId,
431
+ agentName: "Claude",
432
+ agentType: "root",
433
+ parentAgentId: null,
434
+ payload: {
435
+ tool: { toolName }
436
+ },
437
+ source: "claude_hooks"
438
+ });
439
+ }
440
+ } else if (hookEvent === "Stop") {
441
+ events.push({
442
+ id: generateId(),
443
+ type: "agent_completed",
444
+ timestamp: now,
445
+ sessionId,
446
+ agentId: rootAgentId,
447
+ agentName: "Claude",
448
+ agentType: "root",
449
+ parentAgentId: null,
450
+ payload: {
451
+ message: body.last_assistant_message?.slice(0, 200)
452
+ },
453
+ source: "claude_hooks"
454
+ });
455
+ activeSessions.delete(sessionId);
456
+ for (const [id, sub] of knownSubagents) {
457
+ if (sub.sessionId === sessionId)
458
+ knownSubagents.delete(id);
459
+ }
460
+ }
461
+ return events;
462
+ }
463
+
464
+ // apps/collector/dist/store/memory-store.js
465
+ var MemoryStore = class {
466
+ events = /* @__PURE__ */ new Map();
467
+ sessions = /* @__PURE__ */ new Map();
468
+ addEvent(event) {
469
+ const { sessionId } = event;
470
+ if (!this.sessions.has(sessionId)) {
471
+ this.sessions.set(sessionId, {
472
+ id: sessionId,
473
+ startedAt: event.timestamp,
474
+ endedAt: null,
475
+ eventCount: 0
476
+ });
477
+ }
478
+ const session = this.sessions.get(sessionId);
479
+ session.eventCount++;
480
+ if (event.type === "session_end") {
481
+ session.endedAt = event.timestamp;
482
+ }
483
+ if (!this.events.has(sessionId)) {
484
+ this.events.set(sessionId, []);
485
+ }
486
+ this.events.get(sessionId).push(event);
487
+ }
488
+ getSessionEvents(sessionId) {
489
+ return this.events.get(sessionId) || [];
490
+ }
491
+ getSessions() {
492
+ return Array.from(this.sessions.values()).sort((a, b) => b.startedAt - a.startedAt);
493
+ }
494
+ getLatestSession() {
495
+ const sessions = this.getSessions();
496
+ return sessions[0] || null;
497
+ }
498
+ getAllEvents() {
499
+ const all = [];
500
+ for (const events of this.events.values()) {
501
+ all.push(...events);
502
+ }
503
+ return all.sort((a, b) => a.timestamp - b.timestamp);
504
+ }
505
+ clear() {
506
+ this.events.clear();
507
+ this.sessions.clear();
508
+ }
509
+ };
510
+ var store = new MemoryStore();
511
+
512
+ // apps/collector/dist/ws/broadcaster.js
513
+ var io = null;
514
+ function initBroadcaster(socketServer) {
515
+ io = socketServer;
516
+ io.on("connection", (socket) => {
517
+ console.log(`[ws] Dashboard connected: ${socket.id}`);
518
+ socket.on("join_session", (sessionId) => {
519
+ socket.join(`session:${sessionId}`);
520
+ console.log(`[ws] ${socket.id} joined session: ${sessionId}`);
521
+ });
522
+ socket.on("leave_session", (sessionId) => {
523
+ socket.leave(`session:${sessionId}`);
524
+ });
525
+ socket.on("disconnect", () => {
526
+ console.log(`[ws] Dashboard disconnected: ${socket.id}`);
527
+ });
528
+ });
529
+ }
530
+ function broadcastEvent(event) {
531
+ if (!io)
532
+ return;
533
+ io.emit("agent_event", event);
534
+ io.to(`session:${event.sessionId}`).emit("session_event", event);
535
+ }
536
+ function broadcastSessionList(sessions) {
537
+ if (!io)
538
+ return;
539
+ io.emit("sessions_updated", sessions);
540
+ }
541
+
542
+ // apps/collector/dist/routes/hooks.js
543
+ var contextEventCounter = 0;
544
+ function formatBytes(bytes) {
545
+ if (bytes < 1024)
546
+ return `${bytes} B`;
547
+ if (bytes < 1024 * 1024)
548
+ return `${(bytes / 1024).toFixed(1)} KB`;
549
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
550
+ }
551
+ async function getTranscriptSize(transcriptPath, sessionId) {
552
+ try {
553
+ const stats = await stat(transcriptPath);
554
+ return {
555
+ id: `ctx_${Date.now()}_${++contextEventCounter}`,
556
+ type: "session_meta",
557
+ timestamp: Date.now(),
558
+ sessionId,
559
+ agentId: "system",
560
+ agentName: "Transcript",
561
+ agentType: "system",
562
+ parentAgentId: null,
563
+ payload: {
564
+ metadata: {
565
+ transcriptBytes: stats.size,
566
+ transcriptSize: formatBytes(stats.size)
567
+ }
568
+ },
569
+ source: "claude_hooks"
570
+ };
571
+ } catch {
572
+ return null;
573
+ }
574
+ }
575
+ async function hookRoutes(app) {
576
+ app.post("/api/hooks/:hookEvent", async (request, reply) => {
577
+ const { hookEvent } = request.params;
578
+ const body = request.body || {};
579
+ console.log(`[hook] ${hookEvent}`, JSON.stringify(body, null, 2));
580
+ try {
581
+ const events = transformClaudeHook(hookEvent, body);
582
+ for (const event of events) {
583
+ store.addEvent(event);
584
+ broadcastEvent(event);
585
+ }
586
+ const transcriptPath = body.transcript_path;
587
+ const sessionId = body.session_id;
588
+ if (transcriptPath && sessionId) {
589
+ const ctxEvent = await getTranscriptSize(transcriptPath, sessionId);
590
+ if (ctxEvent) {
591
+ store.addEvent(ctxEvent);
592
+ broadcastEvent(ctxEvent);
593
+ }
594
+ }
595
+ broadcastSessionList(store.getSessions());
596
+ return reply.status(200).send({ ok: true, eventsCreated: events.length });
597
+ } catch (err) {
598
+ console.error(`[hook] Error processing ${hookEvent}:`, err);
599
+ return reply.status(200).send({ ok: false, error: "processing_error" });
600
+ }
601
+ });
602
+ }
603
+
604
+ // apps/collector/dist/routes/events.js
605
+ async function eventRoutes(app) {
606
+ app.post("/api/events", async (request, reply) => {
607
+ const event = request.body;
608
+ if (!event || !event.type || !event.sessionId) {
609
+ return reply.status(400).send({ error: "Invalid event: requires type and sessionId" });
610
+ }
611
+ console.log(`[sdk] ${event.type} from ${event.agentName}`);
612
+ store.addEvent(event);
613
+ broadcastEvent(event);
614
+ broadcastSessionList(store.getSessions());
615
+ return reply.status(200).send({ ok: true });
616
+ });
617
+ app.post("/api/events/batch", async (request, reply) => {
618
+ const events = request.body;
619
+ if (!Array.isArray(events)) {
620
+ return reply.status(400).send({ error: "Expected array of events" });
621
+ }
622
+ for (const event of events) {
623
+ store.addEvent(event);
624
+ broadcastEvent(event);
625
+ }
626
+ broadcastSessionList(store.getSessions());
627
+ return reply.status(200).send({ ok: true, count: events.length });
628
+ });
629
+ }
630
+
631
+ // apps/collector/dist/routes/sessions.js
632
+ async function sessionRoutes(app) {
633
+ app.get("/api/sessions", async (_request, reply) => {
634
+ return reply.send(store.getSessions());
635
+ });
636
+ app.get("/api/sessions/:sessionId/events", async (request, reply) => {
637
+ const { sessionId } = request.params;
638
+ const events = store.getSessionEvents(sessionId);
639
+ return reply.send(events);
640
+ });
641
+ }
642
+
643
+ // apps/collector/dist/index.js
644
+ var __dirname = dirname(fileURLToPath(import.meta.url));
645
+ var PORT = Number(process.env.PORT) || 4800;
646
+ async function main() {
647
+ const app = Fastify({ logger: false });
648
+ await app.register(cors, { origin: true });
649
+ await app.register(hookRoutes);
650
+ await app.register(eventRoutes);
651
+ await app.register(sessionRoutes);
652
+ app.get("/health", async () => ({ status: "ok", uptime: process.uptime() }));
653
+ const dashboardDist = resolve(process.env.SYNAPSE_DASHBOARD_DIR || resolve(__dirname, "../../dashboard/dist"));
654
+ if (existsSync(dashboardDist)) {
655
+ await app.register(fastifyStatic, {
656
+ root: dashboardDist,
657
+ prefix: "/",
658
+ wildcard: false
659
+ });
660
+ app.setNotFoundHandler(async (request, reply) => {
661
+ if (request.url.startsWith("/api/") || request.url.startsWith("/socket.io/")) {
662
+ return reply.status(404).send({ error: "Not found" });
663
+ }
664
+ return reply.sendFile("index.html");
665
+ });
666
+ console.log(`[synapse] Dashboard available at http://localhost:${PORT}`);
667
+ }
668
+ await app.listen({ port: PORT, host: "0.0.0.0" });
669
+ const io2 = new SocketIOServer(app.server, {
670
+ cors: { origin: "*" }
671
+ });
672
+ initBroadcaster(io2);
673
+ console.log(`[synapse] Collector listening on http://localhost:${PORT}`);
674
+ console.log(`[synapse] WebSocket ready for dashboard connections`);
675
+ }
676
+ main().catch((err) => {
677
+ console.error("[synapse] Failed to start:", err);
678
+ process.exit(1);
679
+ });