ralph-dashboard 2.3.8

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,1419 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // server/server.ts
5
+ import { join as join5, dirname } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
8
+
9
+ // server/services/log-parser.ts
10
+ import { homedir as homedir2 } from "os";
11
+ import { join as join2 } from "path";
12
+ import {
13
+ readFileSync as readFileSync2,
14
+ existsSync as existsSync3,
15
+ writeFileSync,
16
+ renameSync,
17
+ unlinkSync
18
+ } from "fs";
19
+
20
+ // server/services/checklist-service.ts
21
+ import { readFileSync, existsSync as existsSync2 } from "fs";
22
+
23
+ // server/services/file-finder.ts
24
+ import { existsSync, readdirSync } from "fs";
25
+ import { join } from "path";
26
+ import { homedir } from "os";
27
+ var RALPH_BASE_DIR = join(homedir(), ".claude", "ralph-wiggum-pro");
28
+ var TRANSCRIPTS_DIR = join(RALPH_BASE_DIR, "transcripts");
29
+ var OLD_TRANSCRIPTS_DIR = join(homedir(), ".claude", "ralph-wiggum-pro-logs", "transcripts");
30
+ function getTranscriptsDir() {
31
+ return TRANSCRIPTS_DIR;
32
+ }
33
+ function findFileByLoopId(loopId, suffix) {
34
+ if (existsSync(TRANSCRIPTS_DIR)) {
35
+ const files = readdirSync(TRANSCRIPTS_DIR);
36
+ const match = files.find((f) => f.endsWith(`-${loopId}-${suffix}`) || f === `${loopId}-${suffix}`);
37
+ if (match) {
38
+ return join(TRANSCRIPTS_DIR, match);
39
+ }
40
+ }
41
+ if (existsSync(OLD_TRANSCRIPTS_DIR)) {
42
+ const files = readdirSync(OLD_TRANSCRIPTS_DIR);
43
+ const match = files.find((f) => f.endsWith(`-${loopId}-${suffix}`) || f === `${loopId}-${suffix}`);
44
+ if (match) {
45
+ return join(OLD_TRANSCRIPTS_DIR, match);
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+ function fileExistsForLoopId(loopId, suffix) {
51
+ return findFileByLoopId(loopId, suffix) !== null;
52
+ }
53
+
54
+ // server/services/checklist-service.ts
55
+ function validateLoopId(loopId) {
56
+ const safePattern = /^[a-zA-Z0-9._-]{1,256}$/;
57
+ if (!safePattern.test(loopId)) {
58
+ return false;
59
+ }
60
+ if (loopId.includes("..")) {
61
+ return false;
62
+ }
63
+ return true;
64
+ }
65
+ function getChecklistPath(loopId) {
66
+ if (!validateLoopId(loopId)) {
67
+ throw new Error(`Invalid loop_id format: ${loopId}`);
68
+ }
69
+ return findFileByLoopId(loopId, "checklist.json");
70
+ }
71
+ function getChecklist(loopId) {
72
+ try {
73
+ const path = getChecklistPath(loopId);
74
+ if (!path || !existsSync2(path)) {
75
+ return null;
76
+ }
77
+ const content = readFileSync(path, "utf-8");
78
+ const checklist = JSON.parse(content);
79
+ if (!checklist.loop_id || !checklist.completion_criteria) {
80
+ return null;
81
+ }
82
+ return checklist;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ function getChecklistProgress(checklist) {
88
+ const criteriaTotal = checklist.completion_criteria.length;
89
+ const criteriaCompleted = checklist.completion_criteria.filter((item) => item.status === "completed").length;
90
+ return {
91
+ criteria: `${criteriaCompleted}/${criteriaTotal} criteria`,
92
+ criteriaCompleted,
93
+ criteriaTotal
94
+ };
95
+ }
96
+ function getChecklistWithProgress(loopId) {
97
+ const checklist = getChecklist(loopId);
98
+ if (!checklist) {
99
+ return { checklist: null, progress: null };
100
+ }
101
+ const progress = getChecklistProgress(checklist);
102
+ return { checklist, progress };
103
+ }
104
+
105
+ // server/services/log-parser.ts
106
+ var RALPH_BASE_DIR2 = process.env.RALPH_TEST_BASE_DIR || join2(homedir2(), ".claude", "ralph-wiggum-pro");
107
+ var LOGS_DIR = join2(RALPH_BASE_DIR2, "logs");
108
+ var LOG_FILE = join2(LOGS_DIR, "sessions.jsonl");
109
+ var OLD_LOG_FILE = join2(homedir2(), ".claude", "ralph-wiggum-pro-logs", "sessions.jsonl");
110
+ function extractCompletionPromiseFromTask(task) {
111
+ if (!task) {
112
+ return { task, completionPromise: null };
113
+ }
114
+ const match = task.match(/--completion-promise=["']?([^"'\s]+)["']?/);
115
+ if (!match) {
116
+ return { task, completionPromise: null };
117
+ }
118
+ const completionPromise = match[1];
119
+ const cleanedTask = task.replace(/\s*--completion-promise=["']?[^"'\s]+["']?\s*/g, " ").trim();
120
+ return { task: cleanedTask, completionPromise };
121
+ }
122
+ function getLogFilePath() {
123
+ return LOG_FILE;
124
+ }
125
+ function parseIterationFromContent(content) {
126
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
127
+ if (!frontmatterMatch) {
128
+ return null;
129
+ }
130
+ const frontmatter = frontmatterMatch[1];
131
+ if (!frontmatter.includes("session_id:")) {
132
+ return null;
133
+ }
134
+ const iterationMatch = frontmatter.match(/^iteration:\s*(\d+)/m);
135
+ if (!iterationMatch) {
136
+ return null;
137
+ }
138
+ return parseInt(iterationMatch[1], 10);
139
+ }
140
+ function readIterationFromStateFile(stateFilePath, maxRetries = 2) {
141
+ for (let attempt = 0;attempt < maxRetries; attempt++) {
142
+ try {
143
+ if (!existsSync3(stateFilePath)) {
144
+ return null;
145
+ }
146
+ const content = readFileSync2(stateFilePath, "utf-8");
147
+ const iteration = parseIterationFromContent(content);
148
+ if (iteration !== null) {
149
+ return iteration;
150
+ }
151
+ if (attempt < maxRetries - 1) {
152
+ const start = Date.now();
153
+ while (Date.now() - start < 10) {}
154
+ }
155
+ } catch {
156
+ if (attempt < maxRetries - 1) {
157
+ continue;
158
+ }
159
+ return null;
160
+ }
161
+ }
162
+ return null;
163
+ }
164
+ function parseEntriesFromFile(filePath) {
165
+ if (!existsSync3(filePath)) {
166
+ return [];
167
+ }
168
+ const content = readFileSync2(filePath, "utf-8");
169
+ const lines = content.split(`
170
+ `).filter((line) => line.trim());
171
+ const entries = [];
172
+ for (const line of lines) {
173
+ try {
174
+ const entry = JSON.parse(line);
175
+ entries.push(entry);
176
+ } catch {
177
+ console.warn("Skipping malformed log entry:", line.slice(0, 50));
178
+ }
179
+ }
180
+ return entries;
181
+ }
182
+ function parseLogFile() {
183
+ const newEntries = parseEntriesFromFile(LOG_FILE);
184
+ const oldEntries = parseEntriesFromFile(OLD_LOG_FILE);
185
+ return [...oldEntries, ...newEntries];
186
+ }
187
+ function mergeSessions(entries) {
188
+ const loopMap = new Map;
189
+ for (const entry of entries) {
190
+ const loopId = entry.loop_id || entry.loop_id || entry.session_id;
191
+ const existing = loopMap.get(loopId) || {};
192
+ if (entry.status === "active") {
193
+ existing.start = entry;
194
+ } else if (entry.status === "completed") {
195
+ existing.completion = entry;
196
+ }
197
+ loopMap.set(loopId, existing);
198
+ }
199
+ const sessions = [];
200
+ for (const [loop_id, { start, completion }] of loopMap) {
201
+ if (!start && completion) {
202
+ sessions.push({
203
+ loop_id,
204
+ session_id: completion.session_id,
205
+ status: "orphaned",
206
+ outcome: completion.outcome,
207
+ project: "",
208
+ project_name: "(orphaned entry)",
209
+ state_file_path: undefined,
210
+ task: `Orphaned: ${completion.outcome}`,
211
+ started_at: completion.ended_at,
212
+ ended_at: completion.ended_at,
213
+ duration_seconds: completion.duration_seconds ?? 0,
214
+ iterations: completion.iterations ?? null,
215
+ max_iterations: 0,
216
+ completion_promise: null,
217
+ error_reason: completion.error_reason ?? null,
218
+ has_checklist: false,
219
+ checklist_progress: null
220
+ });
221
+ continue;
222
+ }
223
+ if (!start) {
224
+ continue;
225
+ }
226
+ const startTime = new Date(start.started_at);
227
+ const completionTime = completion?.ended_at ? new Date(completion.ended_at) : null;
228
+ const isActive = !completion || completionTime && startTime > completionTime;
229
+ const now = new Date;
230
+ const durationSeconds = isActive ? Math.floor((now.getTime() - startTime.getTime()) / 1000) : completion?.duration_seconds ?? 0;
231
+ let status;
232
+ if (isActive) {
233
+ status = "active";
234
+ } else if (completion) {
235
+ status = completion.outcome;
236
+ } else {
237
+ status = "active";
238
+ }
239
+ if (status === "active" && start.state_file_path) {
240
+ if (!existsSync3(start.state_file_path)) {
241
+ status = "orphaned";
242
+ }
243
+ }
244
+ let iterations = completion?.iterations ?? null;
245
+ if (isActive && start.state_file_path) {
246
+ iterations = readIterationFromStateFile(start.state_file_path);
247
+ }
248
+ const { task: cleanedTask, completionPromise: extractedPromise } = extractCompletionPromiseFromTask(start.task);
249
+ const completionPromise = start.completion_promise || extractedPromise;
250
+ const task = cleanedTask ?? start.task;
251
+ let hasChecklist = false;
252
+ let checklistProgress = null;
253
+ try {
254
+ const checklistResult = getChecklistWithProgress(loop_id);
255
+ if (checklistResult.checklist) {
256
+ hasChecklist = true;
257
+ checklistProgress = checklistResult.progress?.criteria ?? null;
258
+ }
259
+ } catch {}
260
+ sessions.push({
261
+ loop_id,
262
+ session_id: start.session_id,
263
+ status,
264
+ outcome: completion?.outcome,
265
+ project: start.project,
266
+ project_name: start.project_name,
267
+ state_file_path: start.state_file_path,
268
+ task,
269
+ started_at: start.started_at,
270
+ ended_at: completion?.ended_at ?? null,
271
+ duration_seconds: durationSeconds,
272
+ iterations,
273
+ max_iterations: start.max_iterations,
274
+ completion_promise: completionPromise,
275
+ error_reason: completion?.error_reason ?? null,
276
+ has_checklist: hasChecklist,
277
+ checklist_progress: checklistProgress
278
+ });
279
+ }
280
+ sessions.sort((a, b) => {
281
+ if (a.status === "active" && b.status !== "active")
282
+ return -1;
283
+ if (a.status !== "active" && b.status === "active")
284
+ return 1;
285
+ return new Date(b.started_at).getTime() - new Date(a.started_at).getTime();
286
+ });
287
+ return sessions;
288
+ }
289
+ function getSessions() {
290
+ const entries = parseLogFile();
291
+ return mergeSessions(entries);
292
+ }
293
+ function getSessionById(loopId) {
294
+ const sessions = getSessions();
295
+ return sessions.find((s) => s.loop_id === loopId) ?? null;
296
+ }
297
+ function deleteTranscriptFiles(loopId, sessionId) {
298
+ const suffixes = ["iterations.jsonl", "full.jsonl", "checklist.json"];
299
+ for (const suffix of suffixes) {
300
+ const filePath = findFileByLoopId(loopId, suffix);
301
+ if (filePath && existsSync3(filePath)) {
302
+ try {
303
+ unlinkSync(filePath);
304
+ } catch {}
305
+ }
306
+ }
307
+ if (sessionId && sessionId !== loopId) {
308
+ for (const suffix of suffixes) {
309
+ const filePath = findFileByLoopId(sessionId, suffix);
310
+ if (filePath && existsSync3(filePath)) {
311
+ try {
312
+ unlinkSync(filePath);
313
+ } catch {}
314
+ }
315
+ }
316
+ }
317
+ }
318
+ function deleteSession(loopId) {
319
+ const session = getSessionById(loopId);
320
+ if (session?.state_file_path && existsSync3(session.state_file_path)) {
321
+ try {
322
+ unlinkSync(session.state_file_path);
323
+ } catch {}
324
+ }
325
+ deleteTranscriptFiles(loopId, session?.session_id);
326
+ if (!existsSync3(LOG_FILE)) {
327
+ return false;
328
+ }
329
+ const content = readFileSync2(LOG_FILE, "utf-8");
330
+ const lines = content.split(`
331
+ `).filter((line) => line.trim());
332
+ const filteredLines = [];
333
+ let found = false;
334
+ for (const line of lines) {
335
+ try {
336
+ const entry = JSON.parse(line);
337
+ const entryLoopId = entry.loop_id || entry.loop_id || entry.session_id;
338
+ if (entryLoopId === loopId) {
339
+ found = true;
340
+ continue;
341
+ }
342
+ filteredLines.push(line);
343
+ } catch {
344
+ filteredLines.push(line);
345
+ }
346
+ }
347
+ if (!found) {
348
+ return false;
349
+ }
350
+ const tempFile = LOG_FILE + ".tmp." + Date.now();
351
+ writeFileSync(tempFile, filteredLines.join(`
352
+ `) + (filteredLines.length > 0 ? `
353
+ ` : ""));
354
+ renameSync(tempFile, LOG_FILE);
355
+ return true;
356
+ }
357
+ function deleteAllArchivedSessions() {
358
+ const sessions = getSessions();
359
+ const archivedSessions = sessions.filter((s) => s.status !== "active");
360
+ if (archivedSessions.length === 0) {
361
+ return 0;
362
+ }
363
+ const loopIdsToDelete = new Set(archivedSessions.map((s) => s.loop_id));
364
+ for (const session of archivedSessions) {
365
+ if (session.state_file_path && existsSync3(session.state_file_path)) {
366
+ try {
367
+ unlinkSync(session.state_file_path);
368
+ } catch {}
369
+ }
370
+ deleteTranscriptFiles(session.loop_id, session.session_id);
371
+ }
372
+ if (!existsSync3(LOG_FILE)) {
373
+ return archivedSessions.length;
374
+ }
375
+ const content = readFileSync2(LOG_FILE, "utf-8");
376
+ const lines = content.split(`
377
+ `).filter((line) => line.trim());
378
+ const filteredLines = lines.filter((line) => {
379
+ try {
380
+ const entry = JSON.parse(line);
381
+ const entryLoopId = entry.loop_id || entry.loop_id || entry.session_id;
382
+ return !loopIdsToDelete.has(entryLoopId);
383
+ } catch {
384
+ return true;
385
+ }
386
+ });
387
+ const tempFile = LOG_FILE + ".tmp." + Date.now();
388
+ writeFileSync(tempFile, filteredLines.join(`
389
+ `) + (filteredLines.length > 0 ? `
390
+ ` : ""));
391
+ renameSync(tempFile, LOG_FILE);
392
+ return archivedSessions.length;
393
+ }
394
+
395
+ // server/api/sessions.ts
396
+ function handleGetSessions() {
397
+ try {
398
+ const sessions = getSessions();
399
+ const activeCount = sessions.filter((s) => s.status === "active").length;
400
+ const response = {
401
+ sessions,
402
+ total: sessions.length,
403
+ active_count: activeCount
404
+ };
405
+ return Response.json(response);
406
+ } catch (error) {
407
+ const errorMessage = error instanceof Error ? error.message : String(error);
408
+ const response = {
409
+ error: "FETCH_ERROR",
410
+ message: `Failed to fetch sessions: ${errorMessage}`
411
+ };
412
+ return Response.json(response, { status: 500 });
413
+ }
414
+ }
415
+ function handleGetSession(loopId) {
416
+ try {
417
+ const session = getSessionById(loopId);
418
+ if (!session) {
419
+ const response = {
420
+ error: "NOT_FOUND",
421
+ message: `Loop not found: ${loopId}`
422
+ };
423
+ return Response.json(response, { status: 404 });
424
+ }
425
+ return Response.json(session);
426
+ } catch (error) {
427
+ const errorMessage = error instanceof Error ? error.message : String(error);
428
+ const response = {
429
+ error: "FETCH_ERROR",
430
+ message: `Failed to fetch session: ${errorMessage}`
431
+ };
432
+ return Response.json(response, { status: 500 });
433
+ }
434
+ }
435
+
436
+ // server/services/loop-manager.ts
437
+ import { existsSync as existsSync4, unlinkSync as unlinkSync2, appendFileSync } from "fs";
438
+ import { homedir as homedir3 } from "os";
439
+ import { join as join3, resolve } from "path";
440
+ var RALPH_BASE_DIR3 = join3(homedir3(), ".claude", "ralph-wiggum-pro");
441
+ var LOOPS_DIR = join3(RALPH_BASE_DIR3, "loops");
442
+ function validateStateFilePath(stateFilePath) {
443
+ try {
444
+ const resolvedPath = resolve(stateFilePath);
445
+ const expectedBase = resolve(LOOPS_DIR);
446
+ return resolvedPath.startsWith(expectedBase);
447
+ } catch {
448
+ return false;
449
+ }
450
+ }
451
+ function cancelLoop(session) {
452
+ if (session.status !== "active") {
453
+ return {
454
+ success: false,
455
+ message: `Loop ${session.loop_id} is not active (status: ${session.status})`
456
+ };
457
+ }
458
+ const stateFilePath = session.state_file_path;
459
+ if (!stateFilePath) {
460
+ return {
461
+ success: false,
462
+ message: `No state file found for loop ${session.loop_id}`
463
+ };
464
+ }
465
+ if (!validateStateFilePath(stateFilePath)) {
466
+ return {
467
+ success: false,
468
+ message: `Invalid state file path for loop ${session.loop_id}`
469
+ };
470
+ }
471
+ if (!existsSync4(stateFilePath)) {
472
+ return {
473
+ success: false,
474
+ message: `State file no longer exists for loop ${session.loop_id}`
475
+ };
476
+ }
477
+ try {
478
+ unlinkSync2(stateFilePath);
479
+ const logEntry = {
480
+ loop_id: session.loop_id,
481
+ session_id: session.session_id,
482
+ status: "completed",
483
+ outcome: "cancelled",
484
+ ended_at: new Date().toISOString(),
485
+ duration_seconds: Math.floor((Date.now() - new Date(session.started_at).getTime()) / 1000),
486
+ iterations: session.iterations ?? 0
487
+ };
488
+ appendFileSync(getLogFilePath(), JSON.stringify(logEntry) + `
489
+ `);
490
+ return {
491
+ success: true,
492
+ message: `Successfully cancelled loop ${session.loop_id}`
493
+ };
494
+ } catch (error) {
495
+ return {
496
+ success: false,
497
+ message: `Failed to cancel loop ${session.loop_id}`
498
+ };
499
+ }
500
+ }
501
+
502
+ // server/api/cancel.ts
503
+ function handleCancelSession(loopId) {
504
+ try {
505
+ const session = getSessionById(loopId);
506
+ if (!session) {
507
+ const response2 = {
508
+ error: "NOT_FOUND",
509
+ message: `Loop not found: ${loopId}`
510
+ };
511
+ return Response.json(response2, { status: 404 });
512
+ }
513
+ if (session.status !== "active") {
514
+ const response2 = {
515
+ error: "INVALID_STATE",
516
+ message: `Cannot cancel loop: status is '${session.status}', expected 'active'`
517
+ };
518
+ return Response.json(response2, { status: 400 });
519
+ }
520
+ const result = cancelLoop(session);
521
+ if (!result.success) {
522
+ const response2 = {
523
+ error: "CANCEL_FAILED",
524
+ message: result.message
525
+ };
526
+ return Response.json(response2, { status: 500 });
527
+ }
528
+ const response = {
529
+ success: true,
530
+ message: result.message,
531
+ loop_id: loopId
532
+ };
533
+ return Response.json(response);
534
+ } catch (error) {
535
+ const errorMessage = error instanceof Error ? error.message : String(error);
536
+ const response = {
537
+ error: "CANCEL_ERROR",
538
+ message: `Failed to cancel loop: ${errorMessage}`
539
+ };
540
+ return Response.json(response, { status: 500 });
541
+ }
542
+ }
543
+
544
+ // server/api/delete.ts
545
+ function handleDeleteSession(loopId) {
546
+ try {
547
+ const session = getSessionById(loopId);
548
+ if (!session) {
549
+ const response2 = {
550
+ error: "NOT_FOUND",
551
+ message: `Loop not found: ${loopId}`
552
+ };
553
+ return Response.json(response2, { status: 404 });
554
+ }
555
+ if (session.status === "active") {
556
+ const response2 = {
557
+ error: "INVALID_STATE",
558
+ message: `Cannot delete active loop. Cancel it first.`
559
+ };
560
+ return Response.json(response2, { status: 400 });
561
+ }
562
+ const deleted = deleteSession(loopId);
563
+ if (!deleted) {
564
+ const response2 = {
565
+ error: "DELETE_FAILED",
566
+ message: `Failed to delete loop from log file`
567
+ };
568
+ return Response.json(response2, { status: 500 });
569
+ }
570
+ const response = {
571
+ success: true,
572
+ message: `Loop permanently deleted from history`,
573
+ loop_id: loopId
574
+ };
575
+ return Response.json(response);
576
+ } catch (error) {
577
+ const errorMessage = error instanceof Error ? error.message : String(error);
578
+ const response = {
579
+ error: "DELETE_ERROR",
580
+ message: `Failed to delete loop: ${errorMessage}`
581
+ };
582
+ return Response.json(response, { status: 500 });
583
+ }
584
+ }
585
+ function handleDeleteAllArchivedSessions() {
586
+ try {
587
+ const deletedCount = deleteAllArchivedSessions();
588
+ const response = {
589
+ success: true,
590
+ deleted_count: deletedCount,
591
+ message: `Permanently deleted ${deletedCount} archived session(s) and their transcripts`
592
+ };
593
+ return Response.json(response);
594
+ } catch (error) {
595
+ const errorMessage = error instanceof Error ? error.message : String(error);
596
+ const response = {
597
+ error: "DELETE_ERROR",
598
+ message: `Failed to delete archived sessions: ${errorMessage}`
599
+ };
600
+ return Response.json(response, { status: 500 });
601
+ }
602
+ }
603
+
604
+ // server/api/archive.ts
605
+ import { appendFileSync as appendFileSync2 } from "fs";
606
+ function handleArchiveSession(loopId) {
607
+ try {
608
+ const session = getSessionById(loopId);
609
+ if (!session) {
610
+ const response2 = {
611
+ error: "NOT_FOUND",
612
+ message: `Loop not found: ${loopId}`
613
+ };
614
+ return Response.json(response2, { status: 404 });
615
+ }
616
+ if (session.status !== "orphaned") {
617
+ const response2 = {
618
+ error: "INVALID_STATE",
619
+ message: `Cannot archive loop: status is '${session.status}', expected 'orphaned'`
620
+ };
621
+ return Response.json(response2, { status: 400 });
622
+ }
623
+ const logEntry = {
624
+ loop_id: session.loop_id,
625
+ session_id: session.session_id,
626
+ status: "completed",
627
+ outcome: "archived",
628
+ ended_at: new Date().toISOString(),
629
+ duration_seconds: Math.floor((Date.now() - new Date(session.started_at).getTime()) / 1000),
630
+ iterations: session.iterations ?? null
631
+ };
632
+ appendFileSync2(getLogFilePath(), JSON.stringify(logEntry) + `
633
+ `);
634
+ const response = {
635
+ success: true,
636
+ message: `Successfully archived orphaned loop ${loopId}`,
637
+ loop_id: loopId
638
+ };
639
+ return Response.json(response);
640
+ } catch (error) {
641
+ const errorMessage = error instanceof Error ? error.message : String(error);
642
+ const response = {
643
+ error: "ARCHIVE_ERROR",
644
+ message: `Failed to archive loop: ${errorMessage}`
645
+ };
646
+ return Response.json(response, { status: 500 });
647
+ }
648
+ }
649
+
650
+ // server/services/transcript-service.ts
651
+ import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
652
+ function getIterationsFilePath(loopId) {
653
+ return findFileByLoopId(loopId, "iterations.jsonl");
654
+ }
655
+ function getFullTranscriptFilePath(loopId) {
656
+ return findFileByLoopId(loopId, "full.jsonl");
657
+ }
658
+ function hasIterations(loopId) {
659
+ return fileExistsForLoopId(loopId, "iterations.jsonl");
660
+ }
661
+ function hasFullTranscript(loopId) {
662
+ return getFullTranscriptFilePath(loopId) !== null;
663
+ }
664
+ function getIterations(loopId) {
665
+ const filePath = getIterationsFilePath(loopId);
666
+ if (!filePath || !existsSync5(filePath)) {
667
+ return null;
668
+ }
669
+ try {
670
+ const content = readFileSync3(filePath, "utf-8");
671
+ const lines = content.split(`
672
+ `).filter((line) => line.trim());
673
+ const entries = [];
674
+ for (const line of lines) {
675
+ try {
676
+ const entry = JSON.parse(line);
677
+ entries.push(entry);
678
+ } catch {
679
+ console.warn("Skipping malformed iteration entry:", line.slice(0, 50));
680
+ }
681
+ }
682
+ return entries;
683
+ } catch (error) {
684
+ console.error("Failed to read iterations for loop:", loopId, error);
685
+ return null;
686
+ }
687
+ }
688
+ function getFullTranscript(loopId) {
689
+ const filePath = getFullTranscriptFilePath(loopId);
690
+ if (!filePath || !existsSync5(filePath)) {
691
+ return null;
692
+ }
693
+ try {
694
+ const content = readFileSync3(filePath, "utf-8");
695
+ const lines = content.split(`
696
+ `).filter((line) => line.trim());
697
+ const messages = [];
698
+ for (const line of lines) {
699
+ try {
700
+ const entry = JSON.parse(line);
701
+ if (entry.message?.role && entry.message?.content) {
702
+ const role = entry.message.role;
703
+ const textContent = entry.message.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join(`
704
+ `);
705
+ if (textContent) {
706
+ messages.push({ role, content: textContent });
707
+ }
708
+ }
709
+ } catch {
710
+ console.warn("Skipping malformed transcript entry:", line.slice(0, 50));
711
+ }
712
+ }
713
+ return messages;
714
+ } catch (error) {
715
+ console.error("Failed to read full transcript for loop:", loopId, error);
716
+ return null;
717
+ }
718
+ }
719
+
720
+ // server/api/transcript.ts
721
+ function handleGetIterations(loopId) {
722
+ try {
723
+ const iterations = getIterations(loopId);
724
+ if (iterations === null) {
725
+ const response2 = {
726
+ error: "NOT_FOUND",
727
+ message: `No transcript iterations found for loop: ${loopId}`
728
+ };
729
+ return Response.json(response2, { status: 404 });
730
+ }
731
+ const response = { iterations };
732
+ return Response.json(response);
733
+ } catch (error) {
734
+ const errorMessage = error instanceof Error ? error.message : String(error);
735
+ const response = {
736
+ error: "FETCH_ERROR",
737
+ message: `Failed to fetch iterations: ${errorMessage}`
738
+ };
739
+ return Response.json(response, { status: 500 });
740
+ }
741
+ }
742
+ function handleGetFullTranscript(loopId) {
743
+ try {
744
+ const messages = getFullTranscript(loopId);
745
+ if (messages === null) {
746
+ const response2 = {
747
+ error: "NOT_FOUND",
748
+ message: `No full transcript found for loop: ${loopId}`
749
+ };
750
+ return Response.json(response2, { status: 404 });
751
+ }
752
+ const response = { messages };
753
+ return Response.json(response);
754
+ } catch (error) {
755
+ const errorMessage = error instanceof Error ? error.message : String(error);
756
+ const response = {
757
+ error: "FETCH_ERROR",
758
+ message: `Failed to fetch full transcript: ${errorMessage}`
759
+ };
760
+ return Response.json(response, { status: 500 });
761
+ }
762
+ }
763
+ function handleCheckTranscriptAvailability(loopId) {
764
+ try {
765
+ const response = {
766
+ hasIterations: hasIterations(loopId),
767
+ hasFullTranscript: hasFullTranscript(loopId)
768
+ };
769
+ return Response.json(response);
770
+ } catch (error) {
771
+ const errorMessage = error instanceof Error ? error.message : String(error);
772
+ const response = {
773
+ error: "CHECK_ERROR",
774
+ message: `Failed to check transcript availability: ${errorMessage}`
775
+ };
776
+ return Response.json(response, { status: 500 });
777
+ }
778
+ }
779
+
780
+ // server/api/checklist.ts
781
+ function handleGetChecklist(loopId) {
782
+ try {
783
+ const result = getChecklistWithProgress(loopId);
784
+ if (!result.checklist) {
785
+ const response2 = {
786
+ error: "NOT_FOUND",
787
+ message: `Checklist not found for loop_id: ${loopId}`
788
+ };
789
+ return Response.json(response2, { status: 404 });
790
+ }
791
+ const response = {
792
+ checklist: result.checklist,
793
+ progress: result.progress
794
+ };
795
+ return Response.json(response);
796
+ } catch (error) {
797
+ const errorMessage = error instanceof Error ? error.message : String(error);
798
+ if (errorMessage.includes("Invalid loop_id")) {
799
+ const response2 = {
800
+ error: "INVALID_LOOP_ID",
801
+ message: errorMessage
802
+ };
803
+ return Response.json(response2, { status: 400 });
804
+ }
805
+ const response = {
806
+ error: "FETCH_ERROR",
807
+ message: `Failed to fetch checklist: ${errorMessage}`
808
+ };
809
+ return Response.json(response, { status: 500 });
810
+ }
811
+ }
812
+
813
+ // server/services/websocket-service.ts
814
+ import { watch } from "fs";
815
+ import {
816
+ openSync,
817
+ readSync,
818
+ fstatSync,
819
+ closeSync,
820
+ existsSync as existsSync6,
821
+ mkdirSync,
822
+ statSync
823
+ } from "fs";
824
+ import { join as join4 } from "path";
825
+ var watchers = new Map;
826
+ var DEBOUNCE_MS = 100;
827
+ var MAX_SUBSCRIPTIONS_PER_CLIENT = 10;
828
+ function findIterationsFilePath(loopId) {
829
+ return findFileByLoopId(loopId, "iterations.jsonl");
830
+ }
831
+ function getIterationsWatchTarget(loopId) {
832
+ return {
833
+ dir: getTranscriptsDir(),
834
+ suffix: `-${loopId}-iterations.jsonl`
835
+ };
836
+ }
837
+ function readNewIterations(filePath, state) {
838
+ if (!existsSync6(filePath)) {
839
+ return [];
840
+ }
841
+ let fd = null;
842
+ try {
843
+ fd = openSync(filePath, "r");
844
+ const stats = fstatSync(fd);
845
+ if (stats.size <= state.lastSize) {
846
+ return [];
847
+ }
848
+ const newBytesCount = stats.size - state.lastSize;
849
+ const buffer = Buffer.alloc(newBytesCount);
850
+ readSync(fd, buffer, 0, newBytesCount, state.lastSize);
851
+ state.lastSize = stats.size;
852
+ const newContent = buffer.toString("utf-8");
853
+ const lines = newContent.split(`
854
+ `).filter((line) => line.trim());
855
+ const newEntries = [];
856
+ for (const line of lines) {
857
+ try {
858
+ const entry = JSON.parse(line);
859
+ newEntries.push(entry);
860
+ } catch {}
861
+ }
862
+ return newEntries;
863
+ } catch (error) {
864
+ console.error("Error reading new iterations:", error);
865
+ return [];
866
+ } finally {
867
+ if (fd !== null) {
868
+ try {
869
+ closeSync(fd);
870
+ } catch {}
871
+ }
872
+ }
873
+ }
874
+ function broadcastIterations(loopId, iterations) {
875
+ const state = watchers.get(loopId);
876
+ if (!state || iterations.length === 0)
877
+ return;
878
+ const message = JSON.stringify({
879
+ type: "iterations",
880
+ loopId,
881
+ iterations
882
+ });
883
+ for (const client of state.clients) {
884
+ if (client.readyState !== 1) {
885
+ state.clients.delete(client);
886
+ continue;
887
+ }
888
+ try {
889
+ client.send(message);
890
+ } catch (error) {
891
+ console.error("Error sending to WebSocket client:", error);
892
+ state.clients.delete(client);
893
+ }
894
+ }
895
+ }
896
+ function findChecklistFilePath(loopId) {
897
+ return findFileByLoopId(loopId, "checklist.json");
898
+ }
899
+ function broadcastChecklist(loopId) {
900
+ const state = watchers.get(loopId);
901
+ if (!state)
902
+ return;
903
+ const checklistData = getChecklistWithProgress(loopId);
904
+ if (!checklistData.checklist)
905
+ return;
906
+ const message = JSON.stringify({
907
+ type: "checklist",
908
+ loopId,
909
+ checklist: checklistData.checklist,
910
+ progress: checklistData.progress
911
+ });
912
+ for (const client of state.clients) {
913
+ if (client.readyState !== 1) {
914
+ state.clients.delete(client);
915
+ continue;
916
+ }
917
+ try {
918
+ client.send(message);
919
+ } catch (error) {
920
+ console.error("Error sending checklist to WebSocket client:", error);
921
+ state.clients.delete(client);
922
+ }
923
+ }
924
+ }
925
+ function handleChecklistChange(loopId) {
926
+ const state = watchers.get(loopId);
927
+ if (!state)
928
+ return;
929
+ if (state.checklistDebounceTimer) {
930
+ clearTimeout(state.checklistDebounceTimer);
931
+ }
932
+ state.checklistDebounceTimer = setTimeout(() => {
933
+ broadcastChecklist(loopId);
934
+ state.checklistDebounceTimer = null;
935
+ }, DEBOUNCE_MS);
936
+ }
937
+ function setupChecklistWatcher(loopId, state) {
938
+ if (state.checklistWatcher) {
939
+ state.checklistWatcher.close();
940
+ state.checklistWatcher = null;
941
+ }
942
+ const checklistPath = findChecklistFilePath(loopId);
943
+ if (checklistPath && existsSync6(checklistPath)) {
944
+ try {
945
+ const stats = statSync(checklistPath);
946
+ state.checklistLastModified = stats.mtimeMs;
947
+ } catch (error) {
948
+ console.debug(`Could not stat checklist file ${checklistPath}:`, error);
949
+ state.checklistLastModified = 0;
950
+ }
951
+ state.checklistFilePath = checklistPath;
952
+ state.checklistWatcher = watch(checklistPath, { persistent: false }, (eventType) => {
953
+ if (eventType === "change") {
954
+ handleChecklistChange(loopId);
955
+ }
956
+ });
957
+ state.checklistWatcher.on("error", (error) => {
958
+ console.error(`Checklist watcher error for ${loopId}:`, error);
959
+ });
960
+ console.log(`Started checklist watcher for loop: ${loopId}`);
961
+ } else {
962
+ state.checklistFilePath = null;
963
+ }
964
+ }
965
+ function handleDirectoryChangeForChecklist(loopId, filename) {
966
+ const state = watchers.get(loopId);
967
+ if (!state)
968
+ return;
969
+ if (filename && filename.endsWith("-checklist.json") && filename.includes(loopId)) {
970
+ setupChecklistWatcher(loopId, state);
971
+ handleChecklistChange(loopId);
972
+ }
973
+ }
974
+ function handleFileChange(loopId, filePath) {
975
+ const state = watchers.get(loopId);
976
+ if (!state)
977
+ return;
978
+ if (state.debounceTimer) {
979
+ clearTimeout(state.debounceTimer);
980
+ }
981
+ state.debounceTimer = setTimeout(() => {
982
+ const newIterations = readNewIterations(filePath, state);
983
+ if (newIterations.length > 0) {
984
+ broadcastIterations(loopId, newIterations);
985
+ }
986
+ state.debounceTimer = null;
987
+ }, DEBOUNCE_MS);
988
+ }
989
+ function handleDirectoryChange(loopId, targetSuffix, filename) {
990
+ const state = watchers.get(loopId);
991
+ if (!state)
992
+ return;
993
+ handleDirectoryChangeForChecklist(loopId, filename);
994
+ if (!state.watchingDirectory)
995
+ return;
996
+ if (filename && filename.endsWith(targetSuffix)) {
997
+ const filePath = join4(getTranscriptsDir(), filename);
998
+ if (state.watcher) {
999
+ state.watcher.close();
1000
+ }
1001
+ state.watchingDirectory = false;
1002
+ state.filePath = filePath;
1003
+ state.watcher = watch(filePath, { persistent: false }, (eventType) => {
1004
+ if (eventType === "change") {
1005
+ handleFileChange(loopId, filePath);
1006
+ }
1007
+ });
1008
+ state.watcher.on("error", (error) => {
1009
+ console.error(`Watcher error for ${loopId}:`, error);
1010
+ });
1011
+ console.log(`Switched to file watcher for loop: ${loopId}`);
1012
+ handleFileChange(loopId, filePath);
1013
+ }
1014
+ }
1015
+ function countClientSubscriptions(client) {
1016
+ let count = 0;
1017
+ for (const state of watchers.values()) {
1018
+ if (state.clients.has(client)) {
1019
+ count++;
1020
+ }
1021
+ }
1022
+ return count;
1023
+ }
1024
+ function canClientSubscribe(client) {
1025
+ return countClientSubscriptions(client) < MAX_SUBSCRIPTIONS_PER_CLIENT;
1026
+ }
1027
+ function subscribeToLoop(loopId, client) {
1028
+ if (!canClientSubscribe(client)) {
1029
+ return {
1030
+ success: false,
1031
+ message: `Maximum subscriptions (${MAX_SUBSCRIPTIONS_PER_CLIENT}) reached`
1032
+ };
1033
+ }
1034
+ let state = watchers.get(loopId);
1035
+ if (!state) {
1036
+ let lastSize = 0;
1037
+ let watcher = null;
1038
+ let watchingDirectory = false;
1039
+ let filePath = null;
1040
+ filePath = findIterationsFilePath(loopId);
1041
+ if (filePath && existsSync6(filePath)) {
1042
+ try {
1043
+ const fd = openSync(filePath, "r");
1044
+ const stats = fstatSync(fd);
1045
+ lastSize = stats.size;
1046
+ closeSync(fd);
1047
+ } catch {}
1048
+ watcher = watch(filePath, { persistent: false }, (eventType) => {
1049
+ if (eventType === "change") {
1050
+ handleFileChange(loopId, filePath);
1051
+ }
1052
+ });
1053
+ watcher.on("error", (error) => {
1054
+ console.error(`Watcher error for ${loopId}:`, error);
1055
+ });
1056
+ console.log(`Started file watcher for loop: ${loopId}`);
1057
+ } else {
1058
+ const { dir, suffix } = getIterationsWatchTarget(loopId);
1059
+ if (!existsSync6(dir)) {
1060
+ try {
1061
+ mkdirSync(dir, { recursive: true });
1062
+ } catch {}
1063
+ }
1064
+ if (existsSync6(dir)) {
1065
+ watchingDirectory = true;
1066
+ watcher = watch(dir, { persistent: false }, (eventType, filename) => {
1067
+ if (eventType === "rename") {
1068
+ handleDirectoryChange(loopId, suffix, filename);
1069
+ }
1070
+ });
1071
+ watcher.on("error", (error) => {
1072
+ console.error(`Directory watcher error for ${loopId}:`, error);
1073
+ });
1074
+ console.log(`Started directory watcher for loop: ${loopId} (file not yet created)`);
1075
+ } else {
1076
+ console.log(`Cannot watch loop ${loopId}: transcript directory does not exist`);
1077
+ }
1078
+ }
1079
+ state = {
1080
+ watcher,
1081
+ lastSize,
1082
+ debounceTimer: null,
1083
+ clients: new Set,
1084
+ watchingDirectory,
1085
+ filePath,
1086
+ checklistWatcher: null,
1087
+ checklistLastModified: 0,
1088
+ checklistDebounceTimer: null,
1089
+ checklistFilePath: null
1090
+ };
1091
+ watchers.set(loopId, state);
1092
+ setupChecklistWatcher(loopId, state);
1093
+ }
1094
+ state.clients.add(client);
1095
+ console.log(`Client subscribed to loop ${loopId} (${state.clients.size} clients)`);
1096
+ return { success: true };
1097
+ }
1098
+ function unsubscribeFromLoop(loopId, client) {
1099
+ const state = watchers.get(loopId);
1100
+ if (!state)
1101
+ return;
1102
+ state.clients.delete(client);
1103
+ console.log(`Client unsubscribed from loop ${loopId} (${state.clients.size} clients)`);
1104
+ if (state.clients.size === 0) {
1105
+ if (state.debounceTimer) {
1106
+ clearTimeout(state.debounceTimer);
1107
+ }
1108
+ if (state.watcher) {
1109
+ state.watcher.close();
1110
+ }
1111
+ if (state.checklistDebounceTimer) {
1112
+ clearTimeout(state.checklistDebounceTimer);
1113
+ }
1114
+ if (state.checklistWatcher) {
1115
+ state.checklistWatcher.close();
1116
+ }
1117
+ watchers.delete(loopId);
1118
+ console.log(`Stopped watching transcript and checklist for loop: ${loopId}`);
1119
+ }
1120
+ }
1121
+ function unsubscribeFromAll(client) {
1122
+ for (const [loopId, state] of watchers) {
1123
+ if (state.clients.has(client)) {
1124
+ unsubscribeFromLoop(loopId, client);
1125
+ }
1126
+ }
1127
+ }
1128
+ function cleanupAllWatchers() {
1129
+ for (const state of watchers.values()) {
1130
+ if (state.debounceTimer) {
1131
+ clearTimeout(state.debounceTimer);
1132
+ }
1133
+ if (state.watcher) {
1134
+ state.watcher.close();
1135
+ }
1136
+ if (state.checklistDebounceTimer) {
1137
+ clearTimeout(state.checklistDebounceTimer);
1138
+ }
1139
+ if (state.checklistWatcher) {
1140
+ state.checklistWatcher.close();
1141
+ }
1142
+ }
1143
+ watchers.clear();
1144
+ console.log("All transcript and checklist watchers cleaned up");
1145
+ }
1146
+
1147
+ // server/server.ts
1148
+ var __filename2 = fileURLToPath(import.meta.url);
1149
+ var __dirname2 = dirname(__filename2);
1150
+ function resolveDist() {
1151
+ const prodPath = join5(__dirname2, "..");
1152
+ if (existsSync7(join5(prodPath, "index.html"))) {
1153
+ return prodPath;
1154
+ }
1155
+ return join5(__dirname2, "..", "dist");
1156
+ }
1157
+ var DIST_DIR = resolveDist();
1158
+ function validateLoopId2(loopId) {
1159
+ const validPattern = /^[a-zA-Z0-9._-]+$/;
1160
+ const noPathTraversal = !loopId.includes("..");
1161
+ return validPattern.test(loopId) && noPathTraversal;
1162
+ }
1163
+ function invalidLoopIdResponse() {
1164
+ return Response.json({
1165
+ error: "INVALID_LOOP_ID",
1166
+ message: "Invalid loop ID format. Loop ID must contain only alphanumeric characters, hyphens, underscores, and dots."
1167
+ }, { status: 400 });
1168
+ }
1169
+ function getMimeType(path) {
1170
+ const ext = path.split(".").pop()?.toLowerCase();
1171
+ const mimeTypes = {
1172
+ html: "text/html",
1173
+ css: "text/css",
1174
+ js: "application/javascript",
1175
+ json: "application/json",
1176
+ png: "image/png",
1177
+ jpg: "image/jpeg",
1178
+ jpeg: "image/jpeg",
1179
+ svg: "image/svg+xml",
1180
+ ico: "image/x-icon"
1181
+ };
1182
+ return mimeTypes[ext ?? ""] ?? "application/octet-stream";
1183
+ }
1184
+ function serveStaticFile(path) {
1185
+ const filePath = join5(DIST_DIR, path);
1186
+ if (!existsSync7(filePath)) {
1187
+ return null;
1188
+ }
1189
+ try {
1190
+ const content = readFileSync4(filePath);
1191
+ return new Response(content, {
1192
+ headers: { "Content-Type": getMimeType(path) }
1193
+ });
1194
+ } catch {
1195
+ return null;
1196
+ }
1197
+ }
1198
+ function createServer(options) {
1199
+ const { port, host } = options;
1200
+ const server = Bun.serve({
1201
+ port,
1202
+ hostname: host,
1203
+ fetch(req, server2) {
1204
+ const url = new URL(req.url);
1205
+ const path = url.pathname;
1206
+ if (path === "/ws" && req.headers.get("upgrade") === "websocket") {
1207
+ const loopId = url.searchParams.get("loopId");
1208
+ if (!loopId || !validateLoopId2(loopId)) {
1209
+ return new Response("Invalid loopId", { status: 400 });
1210
+ }
1211
+ const success = server2.upgrade(req, { data: { loopId } });
1212
+ if (success) {
1213
+ return;
1214
+ }
1215
+ return new Response("WebSocket upgrade failed", { status: 500 });
1216
+ }
1217
+ if (path.startsWith("/api/")) {
1218
+ const securityHeaders = {
1219
+ "Access-Control-Allow-Origin": "*",
1220
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
1221
+ "Access-Control-Allow-Headers": "Content-Type",
1222
+ "Content-Security-Policy": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
1223
+ "X-Content-Type-Options": "nosniff",
1224
+ "X-Frame-Options": "DENY",
1225
+ "X-XSS-Protection": "1; mode=block",
1226
+ "Referrer-Policy": "strict-origin-when-cross-origin",
1227
+ "Strict-Transport-Security": "max-age=31536000; includeSubDomains"
1228
+ };
1229
+ if (req.method === "OPTIONS") {
1230
+ return new Response(null, { headers: securityHeaders });
1231
+ }
1232
+ let response;
1233
+ if (path === "/api/sessions" && req.method === "GET") {
1234
+ response = handleGetSessions();
1235
+ } else if (path.match(/^\/api\/sessions\/[^/]+$/) && req.method === "GET") {
1236
+ const loopId = path.split("/").pop();
1237
+ if (!validateLoopId2(loopId)) {
1238
+ response = invalidLoopIdResponse();
1239
+ } else {
1240
+ response = handleGetSession(loopId);
1241
+ }
1242
+ } else if (path.match(/^\/api\/sessions\/[^/]+\/cancel$/) && req.method === "POST") {
1243
+ const parts = path.split("/");
1244
+ const loopId = parts[parts.length - 2];
1245
+ if (!validateLoopId2(loopId)) {
1246
+ response = invalidLoopIdResponse();
1247
+ } else {
1248
+ response = handleCancelSession(loopId);
1249
+ }
1250
+ } else if (path.match(/^\/api\/sessions\/[^/]+\/archive$/) && req.method === "POST") {
1251
+ const parts = path.split("/");
1252
+ const loopId = parts[parts.length - 2];
1253
+ if (!validateLoopId2(loopId)) {
1254
+ response = invalidLoopIdResponse();
1255
+ } else {
1256
+ response = handleArchiveSession(loopId);
1257
+ }
1258
+ } else if (path === "/api/sessions" && req.method === "DELETE") {
1259
+ response = handleDeleteAllArchivedSessions();
1260
+ } else if (path.match(/^\/api\/sessions\/[^/]+$/) && req.method === "DELETE") {
1261
+ const loopId = path.split("/").pop();
1262
+ if (!validateLoopId2(loopId)) {
1263
+ response = invalidLoopIdResponse();
1264
+ } else {
1265
+ response = handleDeleteSession(loopId);
1266
+ }
1267
+ } else if (path.match(/^\/api\/transcript\/[^/]+\/iterations$/) && req.method === "GET") {
1268
+ const parts = path.split("/");
1269
+ const loopId = parts[parts.length - 2];
1270
+ if (!validateLoopId2(loopId)) {
1271
+ response = invalidLoopIdResponse();
1272
+ } else {
1273
+ response = handleGetIterations(loopId);
1274
+ }
1275
+ } else if (path.match(/^\/api\/transcript\/[^/]+\/full$/) && req.method === "GET") {
1276
+ const parts = path.split("/");
1277
+ const loopId = parts[parts.length - 2];
1278
+ if (!validateLoopId2(loopId)) {
1279
+ response = invalidLoopIdResponse();
1280
+ } else {
1281
+ response = handleGetFullTranscript(loopId);
1282
+ }
1283
+ } else if (path.match(/^\/api\/transcript\/[^/]+$/) && req.method === "GET") {
1284
+ const loopId = path.split("/").pop();
1285
+ if (!validateLoopId2(loopId)) {
1286
+ response = invalidLoopIdResponse();
1287
+ } else {
1288
+ response = handleCheckTranscriptAvailability(loopId);
1289
+ }
1290
+ } else if (path.match(/^\/api\/checklist\/[^/]+$/) && req.method === "GET") {
1291
+ const loopId = path.split("/").pop();
1292
+ if (!validateLoopId2(loopId)) {
1293
+ response = invalidLoopIdResponse();
1294
+ } else {
1295
+ response = handleGetChecklist(loopId);
1296
+ }
1297
+ } else {
1298
+ response = Response.json({ error: "NOT_FOUND", message: "API endpoint not found" }, { status: 404 });
1299
+ }
1300
+ const headers = new Headers(response.headers);
1301
+ Object.entries(securityHeaders).forEach(([key, value]) => {
1302
+ headers.set(key, value);
1303
+ });
1304
+ return new Response(response.body, {
1305
+ status: response.status,
1306
+ headers
1307
+ });
1308
+ }
1309
+ if (existsSync7(DIST_DIR)) {
1310
+ const staticResponse = serveStaticFile(path === "/" ? "index.html" : path.slice(1));
1311
+ if (staticResponse) {
1312
+ return staticResponse;
1313
+ }
1314
+ if (!path.includes(".")) {
1315
+ const indexResponse = serveStaticFile("index.html");
1316
+ if (indexResponse) {
1317
+ return indexResponse;
1318
+ }
1319
+ }
1320
+ }
1321
+ return new Response("Not Found", { status: 404 });
1322
+ },
1323
+ websocket: {
1324
+ open(ws) {
1325
+ const { loopId } = ws.data;
1326
+ console.log(`WebSocket connected for loop: ${loopId}`);
1327
+ const result = subscribeToLoop(loopId, ws);
1328
+ if (!result.success) {
1329
+ ws.send(JSON.stringify({ type: "error", message: result.message }));
1330
+ }
1331
+ },
1332
+ close(ws) {
1333
+ console.log(`WebSocket disconnected for loop: ${ws.data.loopId}`);
1334
+ unsubscribeFromAll(ws);
1335
+ },
1336
+ message(ws, message) {
1337
+ try {
1338
+ const data = JSON.parse(message.toString());
1339
+ if (data.type === "subscribe" && data.loopId && validateLoopId2(data.loopId)) {
1340
+ const result = subscribeToLoop(data.loopId, ws);
1341
+ if (!result.success) {
1342
+ ws.send(JSON.stringify({ type: "error", message: result.message }));
1343
+ }
1344
+ }
1345
+ } catch {}
1346
+ }
1347
+ }
1348
+ });
1349
+ process.once("SIGINT", () => {
1350
+ cleanupAllWatchers();
1351
+ server.stop();
1352
+ process.exit(0);
1353
+ });
1354
+ process.once("SIGTERM", () => {
1355
+ cleanupAllWatchers();
1356
+ server.stop();
1357
+ process.exit(0);
1358
+ });
1359
+ return server;
1360
+ }
1361
+
1362
+ // server/index.ts
1363
+ function parseArgs() {
1364
+ const args = process.argv.slice(2);
1365
+ let port = 3847;
1366
+ let host = "localhost";
1367
+ for (let i = 0;i < args.length; i++) {
1368
+ const arg = args[i];
1369
+ if (arg === "--port" || arg === "-p") {
1370
+ const value = args[++i];
1371
+ const parsed = parseInt(value, 10);
1372
+ if (isNaN(parsed) || parsed < 1 || parsed > 65535) {
1373
+ console.error(`Invalid port: ${value}`);
1374
+ process.exit(1);
1375
+ }
1376
+ port = parsed;
1377
+ } else if (arg === "--host" || arg === "-h") {
1378
+ host = args[++i] || "localhost";
1379
+ } else if (arg === "--help") {
1380
+ console.log(`
1381
+ Ralph Dashboard - Web UI for Ralph Wiggum loops
1382
+
1383
+ USAGE:
1384
+ ralph-dashboard [OPTIONS]
1385
+
1386
+ OPTIONS:
1387
+ --port, -p <PORT> Port to listen on (default: 3847)
1388
+ --host, -h <HOST> Host to bind to (default: localhost)
1389
+ Use 0.0.0.0 for public access
1390
+ --help Show this help message
1391
+
1392
+ EXAMPLES:
1393
+ ralph-dashboard # Start on localhost:3847
1394
+ ralph-dashboard --port 8080 # Start on localhost:8080
1395
+ ralph-dashboard --host 0.0.0.0 # Allow remote access
1396
+ ralph-dashboard -p 8080 -h 0.0.0.0 # Both options
1397
+ `);
1398
+ process.exit(0);
1399
+ }
1400
+ }
1401
+ return { port, host };
1402
+ }
1403
+ function main() {
1404
+ const { port, host } = parseArgs();
1405
+ console.log(`
1406
+ \uD83D\uDD04 Ralph Dashboard
1407
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
1408
+ `);
1409
+ createServer({ port, host });
1410
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
1411
+ const publicNote = host === "0.0.0.0" ? " (public access enabled)" : "";
1412
+ console.log(` \uD83C\uDF10 Server running at: http://${displayHost}:${port}${publicNote}`);
1413
+ console.log(` \uD83D\uDCCA View your Ralph loops in the browser`);
1414
+ console.log(` \u23F9 Press Ctrl+C to stop`);
1415
+ console.log(`
1416
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
1417
+ `);
1418
+ }
1419
+ main();