auq-mcp-server 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 (66) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +176 -0
  3. package/dist/__tests__/schema-validation.test.js +137 -0
  4. package/dist/__tests__/server.integration.test.js +263 -0
  5. package/dist/add.js +1 -0
  6. package/dist/add.test.js +5 -0
  7. package/dist/bin/auq.js +245 -0
  8. package/dist/bin/test-session-menu.js +28 -0
  9. package/dist/bin/test-tabbar.js +42 -0
  10. package/dist/file-utils.js +59 -0
  11. package/dist/format/ResponseFormatter.js +206 -0
  12. package/dist/format/__tests__/ResponseFormatter.test.js +380 -0
  13. package/dist/package.json +74 -0
  14. package/dist/server.js +107 -0
  15. package/dist/session/ResponseFormatter.js +130 -0
  16. package/dist/session/SessionManager.js +474 -0
  17. package/dist/session/__tests__/ResponseFormatter.test.js +417 -0
  18. package/dist/session/__tests__/SessionManager.test.js +553 -0
  19. package/dist/session/__tests__/atomic-operations.test.js +345 -0
  20. package/dist/session/__tests__/file-watcher.test.js +311 -0
  21. package/dist/session/__tests__/workflow.integration.test.js +334 -0
  22. package/dist/session/atomic-operations.js +307 -0
  23. package/dist/session/file-watcher.js +218 -0
  24. package/dist/session/index.js +7 -0
  25. package/dist/session/types.js +20 -0
  26. package/dist/session/utils.js +125 -0
  27. package/dist/session-manager.js +171 -0
  28. package/dist/session-watcher.js +110 -0
  29. package/dist/src/__tests__/schema-validation.test.js +170 -0
  30. package/dist/src/__tests__/server.integration.test.js +274 -0
  31. package/dist/src/add.js +1 -0
  32. package/dist/src/add.test.js +5 -0
  33. package/dist/src/server.js +163 -0
  34. package/dist/src/session/ResponseFormatter.js +163 -0
  35. package/dist/src/session/SessionManager.js +572 -0
  36. package/dist/src/session/__tests__/ResponseFormatter.test.js +741 -0
  37. package/dist/src/session/__tests__/SessionManager.test.js +593 -0
  38. package/dist/src/session/__tests__/atomic-operations.test.js +346 -0
  39. package/dist/src/session/__tests__/file-watcher.test.js +311 -0
  40. package/dist/src/session/atomic-operations.js +307 -0
  41. package/dist/src/session/file-watcher.js +227 -0
  42. package/dist/src/session/index.js +7 -0
  43. package/dist/src/session/types.js +20 -0
  44. package/dist/src/session/utils.js +180 -0
  45. package/dist/src/tui/__tests__/session-watcher.test.js +368 -0
  46. package/dist/src/tui/components/AnimatedGradient.js +45 -0
  47. package/dist/src/tui/components/ConfirmationDialog.js +89 -0
  48. package/dist/src/tui/components/CustomInput.js +14 -0
  49. package/dist/src/tui/components/Footer.js +55 -0
  50. package/dist/src/tui/components/Header.js +35 -0
  51. package/dist/src/tui/components/MultiLineTextInput.js +65 -0
  52. package/dist/src/tui/components/OptionsList.js +115 -0
  53. package/dist/src/tui/components/QuestionDisplay.js +36 -0
  54. package/dist/src/tui/components/ReviewScreen.js +57 -0
  55. package/dist/src/tui/components/SessionSelectionMenu.js +151 -0
  56. package/dist/src/tui/components/StepperView.js +166 -0
  57. package/dist/src/tui/components/TabBar.js +42 -0
  58. package/dist/src/tui/components/Toast.js +19 -0
  59. package/dist/src/tui/components/WaitingScreen.js +20 -0
  60. package/dist/src/tui/session-watcher.js +195 -0
  61. package/dist/src/tui/theme.js +114 -0
  62. package/dist/src/tui/utils/gradientText.js +24 -0
  63. package/dist/tui/__tests__/session-watcher.test.js +368 -0
  64. package/dist/tui/session-watcher.js +183 -0
  65. package/package.json +78 -0
  66. package/scripts/postinstall.cjs +51 -0
@@ -0,0 +1,572 @@
1
+ /**
2
+ * SessionManager - Core session management for AskUserQuestions MCP server
3
+ */
4
+ import { promises as fs } from "fs";
5
+ import { join } from "path";
6
+ import { v4 as uuidv4 } from "uuid";
7
+ import { atomicDeleteFile, AtomicReadError, atomicReadFile, AtomicWriteError, atomicWriteFile, } from "./atomic-operations.js";
8
+ // import { PromiseFileWatcher } from "./file-watcher.js";
9
+ import { ResponseFormatter } from "./ResponseFormatter.js";
10
+ import { DEFAULT_SESSION_CONFIG, SESSION_FILES } from "./types.js";
11
+ import { createSafeFilename, ensureDirectoryExists, fileExists, getCurrentTimestamp, isTimestampExpired, resolveSessionDirectory, sanitizeSessionId, validateSessionDirectory, } from "./utils.js";
12
+ export class SessionManager {
13
+ baseDir;
14
+ config;
15
+ sessionsDir;
16
+ constructor(config = {}) {
17
+ this.config = {
18
+ ...DEFAULT_SESSION_CONFIG,
19
+ ...config,
20
+ };
21
+ // Resolve the directory path using XDG-compliant resolution
22
+ this.baseDir = resolveSessionDirectory(this.config.baseDir);
23
+ this.sessionsDir = this.baseDir;
24
+ }
25
+ /**
26
+ * Clean up old sessions that have exceeded the retention period (garbage collection)
27
+ * This is different from session timeout - retention period determines when old
28
+ * sessions are permanently deleted, regardless of their completion status.
29
+ */
30
+ async cleanupExpiredSessions() {
31
+ try {
32
+ const sessionIds = await this.getAllSessionIds();
33
+ let cleanedCount = 0;
34
+ for (const sessionId of sessionIds) {
35
+ const status = await this.getSessionStatus(sessionId);
36
+ if (status && this.isSessionRetentionExpired(status)) {
37
+ await this.deleteSession(sessionId);
38
+ cleanedCount++;
39
+ }
40
+ }
41
+ return cleanedCount;
42
+ }
43
+ catch (error) {
44
+ console.warn("Failed to cleanup expired sessions:", error);
45
+ return 0;
46
+ }
47
+ }
48
+ /**
49
+ * Create a new session with unique ID
50
+ */
51
+ async createSession(questions) {
52
+ if (!questions || questions.length === 0) {
53
+ throw new Error("At least one question is required to create a session");
54
+ }
55
+ const sessionId = uuidv4();
56
+ const sessionDir = this.getSessionDir(sessionId);
57
+ // Create session directory with secure permissions
58
+ await ensureDirectoryExists(sessionDir);
59
+ const timestamp = getCurrentTimestamp();
60
+ // Create session request
61
+ const sessionRequest = {
62
+ questions,
63
+ sessionId,
64
+ status: "pending",
65
+ timestamp,
66
+ };
67
+ // Create session status
68
+ const sessionStatus = {
69
+ createdAt: timestamp,
70
+ lastModified: timestamp,
71
+ sessionId,
72
+ status: "pending",
73
+ totalQuestions: questions.length,
74
+ };
75
+ // Write session files
76
+ await Promise.all([
77
+ this.writeSessionFile(sessionId, SESSION_FILES.REQUEST, sessionRequest),
78
+ this.writeSessionFile(sessionId, SESSION_FILES.STATUS, sessionStatus),
79
+ ]);
80
+ return sessionId;
81
+ }
82
+ /**
83
+ * Delete a session directory and all files using atomic operations
84
+ */
85
+ async deleteSession(sessionId) {
86
+ if (!this.isValidSessionId(sessionId)) {
87
+ throw new Error(`Invalid session ID format: ${sessionId}`);
88
+ }
89
+ const sessionDir = this.getSessionDir(sessionId);
90
+ try {
91
+ // Delete session files atomically
92
+ const filesToDelete = [
93
+ SESSION_FILES.REQUEST,
94
+ SESSION_FILES.STATUS,
95
+ SESSION_FILES.ANSWERS,
96
+ ];
97
+ for (const filename of filesToDelete) {
98
+ const filePath = join(sessionDir, createSafeFilename(sessionId, filename));
99
+ try {
100
+ await atomicDeleteFile(filePath);
101
+ }
102
+ catch (error) {
103
+ // Ignore file not found errors during cleanup
104
+ if (!error?.cause?.message.includes("ENOENT")) {
105
+ console.warn(`Warning: Failed to delete session file ${filename}: ${error}`);
106
+ }
107
+ }
108
+ }
109
+ // Delete session directory
110
+ await fs.rm(sessionDir, { force: true, recursive: true });
111
+ }
112
+ catch (error) {
113
+ throw new Error(`Failed to delete session ${sessionId}: ${error}`);
114
+ }
115
+ }
116
+ /**
117
+ * Get all session IDs
118
+ */
119
+ async getAllSessionIds() {
120
+ try {
121
+ return await fs.readdir(this.sessionsDir);
122
+ }
123
+ catch (error) {
124
+ if (error.code === "ENOENT") {
125
+ return [];
126
+ }
127
+ throw error;
128
+ }
129
+ }
130
+ /**
131
+ * Get configuration
132
+ */
133
+ getConfig() {
134
+ return { ...this.config };
135
+ }
136
+ /**
137
+ * Get session answers
138
+ */
139
+ async getSessionAnswers(sessionId) {
140
+ return this.readSessionFile(sessionId, SESSION_FILES.ANSWERS);
141
+ }
142
+ /**
143
+ * Get session count
144
+ */
145
+ async getSessionCount() {
146
+ const sessionIds = await this.getAllSessionIds();
147
+ return sessionIds.length;
148
+ }
149
+ /**
150
+ * Get session request (questions)
151
+ */
152
+ async getSessionRequest(sessionId) {
153
+ return this.readSessionFile(sessionId, SESSION_FILES.REQUEST);
154
+ }
155
+ /**
156
+ * Get session status
157
+ */
158
+ async getSessionStatus(sessionId) {
159
+ return this.readSessionFile(sessionId, SESSION_FILES.STATUS);
160
+ }
161
+ /**
162
+ * Initialize the session manager - create base directories
163
+ */
164
+ async initialize() {
165
+ try {
166
+ await ensureDirectoryExists(this.sessionsDir);
167
+ // Validate the directory was created and is accessible
168
+ const isValid = await validateSessionDirectory(this.sessionsDir);
169
+ if (!isValid) {
170
+ throw new Error(`Failed to create or access session directory: ${this.sessionsDir}`);
171
+ }
172
+ }
173
+ catch (error) {
174
+ throw new Error(`Failed to initialize session directories: ${error}`);
175
+ }
176
+ }
177
+ /**
178
+ * Check if maximum session limit is reached
179
+ */
180
+ async isSessionLimitReached() {
181
+ const maxSessions = this.config.maxSessions || 100;
182
+ const currentCount = await this.getSessionCount();
183
+ return currentCount >= maxSessions;
184
+ }
185
+ /**
186
+ * Save session answers
187
+ */
188
+ async saveSessionAnswers(sessionId, answers) {
189
+ const exists = await this.sessionExists(sessionId);
190
+ if (!exists) {
191
+ throw new Error(`Session not found: ${sessionId}`);
192
+ }
193
+ await this.writeSessionFile(sessionId, SESSION_FILES.ANSWERS, answers);
194
+ // Update session status to completed
195
+ await this.updateSessionStatus(sessionId, "completed");
196
+ }
197
+ /**
198
+ * Reject a session - mark as rejected by user
199
+ * This allows users to skip unwanted question sets
200
+ * @param sessionId - The session ID to reject
201
+ * @param reason - Optional reason for rejection (null if user skipped providing reason)
202
+ */
203
+ async rejectSession(sessionId, reason) {
204
+ const exists = await this.sessionExists(sessionId);
205
+ if (!exists) {
206
+ throw new Error(`Session not found: ${sessionId}`);
207
+ }
208
+ // Update status to rejected with reason
209
+ const statusUpdate = {
210
+ rejectionReason: reason || null,
211
+ };
212
+ await this.updateSessionStatus(sessionId, "rejected", statusUpdate);
213
+ }
214
+ /**
215
+ * Check if session exists
216
+ */
217
+ async sessionExists(sessionId) {
218
+ // First validate session ID format
219
+ if (!this.isValidSessionId(sessionId)) {
220
+ return false;
221
+ }
222
+ const sessionDir = this.getSessionDir(sessionId);
223
+ return await fileExists(sessionDir);
224
+ }
225
+ /**
226
+ * Start a complete session lifecycle from creation to formatted response
227
+ *
228
+ * This is the main orchestration method that:
229
+ * 1. Creates a new session with the provided questions
230
+ * 2. Waits for user to submit answers (with optional timeout)
231
+ * 3. Reads and validates the answers
232
+ * 4. Formats the response according to PRD specification
233
+ * 5. Updates session status to completed
234
+ * 6. Returns the formatted response for the AI model
235
+ *
236
+ * @param questions - Array of questions to ask the user
237
+ * @returns Object containing sessionId and formatted response text
238
+ * @throws Error if timeout occurs, validation fails, or file operations fail
239
+ */
240
+ async startSession(questions, callId) {
241
+ // Step 1: Create the session
242
+ const sessionId = await this.createSession(questions);
243
+ // Optionally attach callId metadata to request and status
244
+ if (callId) {
245
+ try {
246
+ const req = await this.getSessionRequest(sessionId);
247
+ if (req) {
248
+ await this.writeSessionFile(sessionId, SESSION_FILES.REQUEST, {
249
+ ...req,
250
+ callId,
251
+ });
252
+ }
253
+ const stat = await this.getSessionStatus(sessionId);
254
+ if (stat) {
255
+ await this.writeSessionFile(sessionId, SESSION_FILES.STATUS, {
256
+ ...stat,
257
+ callId,
258
+ });
259
+ }
260
+ }
261
+ catch (e) {
262
+ console.warn("Failed to write callId metadata:", e);
263
+ }
264
+ }
265
+ try {
266
+ // Step 2: Calculate timeouts
267
+ const sessionTimeout = this.config.sessionTimeout ?? 0; // 0 = infinite
268
+ const watcherTimeout = sessionTimeout > 0
269
+ ? Math.floor(sessionTimeout * 0.9) // 90% of session timeout
270
+ : 0; // Also infinite if session is infinite
271
+ // Step 3: Wait for answers with timeout
272
+ try {
273
+ await this.waitForAnswers(sessionId, watcherTimeout, callId);
274
+ }
275
+ catch (error) {
276
+ // Check if session was rejected by user
277
+ if (error instanceof Error && error.message === "SESSION_REJECTED") {
278
+ // Get session status to retrieve rejection reason
279
+ const status = await this.getSessionStatus(sessionId);
280
+ const reason = status?.rejectionReason;
281
+ // Format rejection message with reason for MCP caller
282
+ let formattedResponse = "User rejected this question set.\n\n";
283
+ if (reason) {
284
+ formattedResponse += `Rejection reason: "${reason}"\n\n`;
285
+ }
286
+ else {
287
+ formattedResponse += "No reason provided.\n\n";
288
+ }
289
+ // formattedResponse += "The user chose not to answer these questions at this time.";
290
+ return {
291
+ formattedResponse,
292
+ sessionId,
293
+ };
294
+ }
295
+ // Watcher timeout occurred
296
+ await this.updateSessionStatus(sessionId, "timed_out");
297
+ throw new Error(`Session ${sessionId} timed out waiting for user response: ${error instanceof Error ? error.message : String(error)}`);
298
+ }
299
+ // Step 4: Read and validate answers
300
+ let answers;
301
+ try {
302
+ answers = await this.getSessionAnswers(sessionId);
303
+ }
304
+ catch (error) {
305
+ // Handle JSON parse errors or other read failures
306
+ await this.updateSessionStatus(sessionId, "abandoned");
307
+ throw error;
308
+ }
309
+ if (!answers) {
310
+ await this.updateSessionStatus(sessionId, "abandoned");
311
+ throw new Error(`Answers file was created but is invalid for session ${sessionId}`);
312
+ }
313
+ const request = await this.getSessionRequest(sessionId);
314
+ if (!request) {
315
+ await this.updateSessionStatus(sessionId, "abandoned");
316
+ throw new Error(`Session request not found: ${sessionId}`);
317
+ }
318
+ // Step 5: Validate answers match questions
319
+ try {
320
+ ResponseFormatter.validateAnswers(answers, request.questions);
321
+ }
322
+ catch (error) {
323
+ await this.updateSessionStatus(sessionId, "abandoned");
324
+ throw new Error(`Answer validation failed for session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
325
+ }
326
+ // Step 6: Format the response according to PRD specification
327
+ const formattedResponse = ResponseFormatter.formatUserResponse(answers, request.questions);
328
+ // Step 7: Update final status
329
+ await this.updateSessionStatus(sessionId, "completed");
330
+ // Step 8: Return results
331
+ return {
332
+ formattedResponse,
333
+ sessionId,
334
+ };
335
+ }
336
+ catch (error) {
337
+ // Ensure any errors are properly propagated with session context
338
+ if (error instanceof Error) {
339
+ throw error;
340
+ }
341
+ throw new Error(`Session ${sessionId} failed: ${String(error)}`);
342
+ }
343
+ }
344
+ /**
345
+ * Update session status
346
+ */
347
+ async updateSessionStatus(sessionId, status, additionalData) {
348
+ // First validate session ID format
349
+ if (!this.isValidSessionId(sessionId)) {
350
+ throw new Error(`Session not found: ${sessionId}`);
351
+ }
352
+ const currentStatus = await this.getSessionStatus(sessionId);
353
+ if (!currentStatus) {
354
+ throw new Error(`Session not found: ${sessionId}`);
355
+ }
356
+ const updatedStatus = {
357
+ ...currentStatus,
358
+ lastModified: getCurrentTimestamp(),
359
+ status,
360
+ ...additionalData,
361
+ };
362
+ await this.writeSessionFile(sessionId, SESSION_FILES.STATUS, updatedStatus);
363
+ }
364
+ /**
365
+ * Validate session data integrity
366
+ */
367
+ async validateSession(sessionId) {
368
+ const issues = [];
369
+ // Check if session exists
370
+ if (!(await this.sessionExists(sessionId))) {
371
+ issues.push("Session directory does not exist");
372
+ return { issues, isValid: false };
373
+ }
374
+ // Check required files
375
+ const requiredFiles = [SESSION_FILES.REQUEST, SESSION_FILES.STATUS];
376
+ for (const filename of requiredFiles) {
377
+ const filePath = join(this.getSessionDir(sessionId), createSafeFilename(sessionId, filename));
378
+ if (!(await fileExists(filePath))) {
379
+ issues.push(`Required file missing: ${filename}`);
380
+ }
381
+ }
382
+ // If any required files are missing, don't try to read them
383
+ if (issues.length > 0) {
384
+ return {
385
+ issues,
386
+ isValid: false,
387
+ };
388
+ }
389
+ // Validate session status consistency
390
+ const status = await this.getSessionStatus(sessionId);
391
+ const request = await this.getSessionRequest(sessionId);
392
+ if (status && request) {
393
+ if (status.sessionId !== sessionId) {
394
+ issues.push("Session ID mismatch in status file");
395
+ }
396
+ if (request.sessionId !== sessionId) {
397
+ issues.push("Session ID mismatch in request file");
398
+ }
399
+ if (status.totalQuestions !== request.questions.length) {
400
+ issues.push("Question count mismatch between status and request");
401
+ }
402
+ }
403
+ else {
404
+ issues.push("Could not read session status or request");
405
+ }
406
+ return {
407
+ issues,
408
+ isValid: issues.length === 0,
409
+ };
410
+ }
411
+ /**
412
+ * Wait for user answers to be submitted for a specific session
413
+ * Returns the session ID when answers are detected, or rejects on timeout
414
+ */
415
+ async waitForAnswers(sessionId, timeoutMs, expectedCallId) {
416
+ const sessionDir = this.getSessionDir(sessionId);
417
+ const answersPath = join(sessionDir, SESSION_FILES.ANSWERS);
418
+ const startTime = Date.now();
419
+ const pollInterval = 200; // ms
420
+ // Poll for answers.json existence, guard against rejection and timeout
421
+ // This avoids race conditions inherent in fs.watch when files are created before watch attaches
422
+ // and isolates each MCP call purely by sessionId
423
+ while (true) {
424
+ // Check for answers
425
+ if (await fileExists(answersPath)) {
426
+ if (expectedCallId) {
427
+ // Verify callId matches before resolving (defensive)
428
+ try {
429
+ const ans = await this.getSessionAnswers(sessionId);
430
+ if (ans && (!ans.callId || ans.callId === expectedCallId)) {
431
+ return sessionId;
432
+ }
433
+ }
434
+ catch {
435
+ // If read fails transiently, continue polling
436
+ }
437
+ }
438
+ else {
439
+ return sessionId;
440
+ }
441
+ }
442
+ // Check for rejection
443
+ const status = await this.getSessionStatus(sessionId);
444
+ if (status && status.status === "rejected") {
445
+ throw new Error("SESSION_REJECTED");
446
+ }
447
+ // Check for timeout
448
+ if (timeoutMs && timeoutMs > 0 && Date.now() - startTime > timeoutMs) {
449
+ throw new Error("Timeout waiting for user response");
450
+ }
451
+ // Sleep before next poll
452
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
453
+ }
454
+ }
455
+ /**
456
+ * Poll status.json to detect if session was rejected
457
+ * Throws error if status becomes "rejected"
458
+ */
459
+ async pollForRejection(sessionId, timeoutMs) {
460
+ const startTime = Date.now();
461
+ const pollInterval = 500; // Poll every 500ms
462
+ while (true) {
463
+ // Check for timeout
464
+ if (timeoutMs > 0 && Date.now() - startTime > timeoutMs) {
465
+ throw new Error("Timeout waiting for user response");
466
+ }
467
+ try {
468
+ const status = await this.getSessionStatus(sessionId);
469
+ if (status && status.status === "rejected") {
470
+ throw new Error("SESSION_REJECTED");
471
+ }
472
+ }
473
+ catch (error) {
474
+ // If we can't read status, ignore and continue polling
475
+ if (error instanceof Error && error.message === "SESSION_REJECTED") {
476
+ throw error; // Re-throw rejection
477
+ }
478
+ }
479
+ // Wait before next poll
480
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
481
+ }
482
+ }
483
+ /**
484
+ * Get session directory path for a given session ID
485
+ */
486
+ getSessionDir(sessionId) {
487
+ return join(this.sessionsDir, sessionId);
488
+ }
489
+ /**
490
+ * Check if a session is expired based on timeout
491
+ */
492
+ isSessionExpired(status) {
493
+ const timeout = this.config.sessionTimeout || 0;
494
+ if (timeout <= 0)
495
+ return false;
496
+ return isTimestampExpired(status.lastModified, timeout);
497
+ }
498
+ /**
499
+ * Check if a session has exceeded the retention period and should be garbage collected
500
+ */
501
+ isSessionRetentionExpired(status) {
502
+ const retentionPeriod = this.config.retentionPeriod ?? 604800000; // Default 7 days
503
+ if (retentionPeriod <= 0)
504
+ return false;
505
+ // Check the more recent timestamp (lastModified or createdAt)
506
+ // to determine if session is old enough for cleanup
507
+ const recentTimestamp = status.lastModified > status.createdAt
508
+ ? status.lastModified
509
+ : status.createdAt;
510
+ return isTimestampExpired(recentTimestamp, retentionPeriod);
511
+ }
512
+ /**
513
+ * Validate session ID format
514
+ */
515
+ isValidSessionId(sessionId) {
516
+ return sanitizeSessionId(sessionId);
517
+ }
518
+ /**
519
+ * Read data from a session file using atomic operations
520
+ */
521
+ async readSessionFile(sessionId, filename, fallback = null) {
522
+ // First validate session ID format
523
+ if (!this.isValidSessionId(sessionId)) {
524
+ return fallback;
525
+ }
526
+ const safeFilename = createSafeFilename(sessionId, filename);
527
+ const filePath = join(this.getSessionDir(sessionId), safeFilename);
528
+ try {
529
+ const content = await atomicReadFile(filePath, {
530
+ encoding: "utf8",
531
+ maxRetries: 3,
532
+ retryDelay: 100,
533
+ });
534
+ try {
535
+ return JSON.parse(content);
536
+ }
537
+ catch (parseError) {
538
+ throw new Error(`Failed to parse JSON from session file ${filename} for session ${sessionId}: ${parseError}`);
539
+ }
540
+ }
541
+ catch (error) {
542
+ if (error instanceof AtomicReadError) {
543
+ // Check if the error is just that the file doesn't exist
544
+ if (error.cause?.message.includes("File does not exist") ||
545
+ error.message.includes("File does not exist")) {
546
+ return fallback;
547
+ }
548
+ throw new Error(`Failed to read session file ${filename} for session ${sessionId}: ${error.message}`);
549
+ }
550
+ throw error;
551
+ }
552
+ }
553
+ /**
554
+ * Write data to a session file using atomic operations
555
+ */
556
+ async writeSessionFile(sessionId, filename, data) {
557
+ const safeFilename = createSafeFilename(sessionId, filename);
558
+ const filePath = join(this.getSessionDir(sessionId), safeFilename);
559
+ try {
560
+ await atomicWriteFile(filePath, JSON.stringify(data, null, 2), {
561
+ encoding: "utf8",
562
+ mode: 0o600,
563
+ });
564
+ }
565
+ catch (error) {
566
+ if (error instanceof AtomicWriteError) {
567
+ throw new Error(`Failed to write session file ${filename} for session ${sessionId}: ${error.message}`);
568
+ }
569
+ throw error;
570
+ }
571
+ }
572
+ }