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,218 @@
1
+ /**
2
+ * File System Watching Module
3
+ *
4
+ * Provides cross-platform file system watching capabilities for both
5
+ * MCP server and TUI coordination with proper debouncing and error handling.
6
+ */
7
+ import { EventEmitter } from "events";
8
+ import { watch } from "fs";
9
+ import { join } from "path";
10
+ import { resolveSessionDirectory } from "./utils.js";
11
+ /**
12
+ * Promise-based file watcher for specific file patterns
13
+ */
14
+ export class PromiseFileWatcher extends EventEmitter {
15
+ config;
16
+ debounceTimers = new Map();
17
+ isWatching = false;
18
+ watchers = new Map();
19
+ constructor(config = {}) {
20
+ super();
21
+ this.config = {
22
+ debounceMs: config.debounceMs ?? 100,
23
+ ignoreInitial: config.ignoreInitial ?? true,
24
+ timeoutMs: config.timeoutMs ?? 30000, // 30 seconds default
25
+ };
26
+ }
27
+ /**
28
+ * Check if currently watching
29
+ */
30
+ active() {
31
+ return this.isWatching;
32
+ }
33
+ /**
34
+ * Clean up all watchers and timers
35
+ */
36
+ cleanup() {
37
+ // Clear all debounce timers
38
+ for (const timer of this.debounceTimers.values()) {
39
+ clearTimeout(timer);
40
+ }
41
+ this.debounceTimers.clear();
42
+ // Close all file watchers
43
+ for (const watcher of this.watchers.values()) {
44
+ watcher.close();
45
+ }
46
+ this.watchers.clear();
47
+ this.isWatching = false;
48
+ this.removeAllListeners();
49
+ }
50
+ /**
51
+ * Watch for a specific file to be created or modified
52
+ * Returns a promise that resolves when the file is detected
53
+ */
54
+ async waitForFile(watchPath, fileName) {
55
+ return new Promise((resolve, reject) => {
56
+ const fullPath = join(watchPath, fileName);
57
+ const timeoutId = this.config.timeoutMs > 0
58
+ ? setTimeout(() => {
59
+ this.cleanup();
60
+ reject(new Error(`Timeout waiting for file: ${fullPath}`));
61
+ }, this.config.timeoutMs)
62
+ : undefined;
63
+ try {
64
+ // Set up file watcher
65
+ const watcher = watch(watchPath, { persistent: false }, (eventType, filename) => {
66
+ if (!filename)
67
+ return;
68
+ const eventPath = join(watchPath, filename);
69
+ // Check if this is the file we're waiting for
70
+ if (filename === fileName || eventPath === fullPath) {
71
+ this.handleFileEvent(eventType, eventPath);
72
+ // Verify file exists and is accessible
73
+ if (eventType === "rename") {
74
+ // File was created - resolve the promise
75
+ if (timeoutId)
76
+ clearTimeout(timeoutId);
77
+ this.cleanup();
78
+ resolve(fullPath);
79
+ }
80
+ }
81
+ });
82
+ this.watchers.set(watchPath, watcher);
83
+ // Handle watcher errors
84
+ watcher.on("error", (error) => {
85
+ if (timeoutId)
86
+ clearTimeout(timeoutId);
87
+ this.cleanup();
88
+ reject(new Error(`File watcher error: ${error.message}`));
89
+ });
90
+ this.isWatching = true;
91
+ }
92
+ catch (error) {
93
+ if (timeoutId)
94
+ clearTimeout(timeoutId);
95
+ reject(new Error(`File watcher setup error: ${error}`));
96
+ }
97
+ });
98
+ }
99
+ /**
100
+ * Watch a directory for new session directories
101
+ * Emits events when new directories are created
102
+ */
103
+ watchForSessions(sessionDirPath, onSessionCreated) {
104
+ try {
105
+ const watcher = watch(sessionDirPath, { persistent: false }, (eventType, filename) => {
106
+ if (!filename || eventType !== "rename")
107
+ return;
108
+ const fullPath = join(sessionDirPath, filename);
109
+ // Debounce rapid events
110
+ this.debounceEvent(fullPath, () => {
111
+ // Check if this is a new directory (potential session)
112
+ this.handleSessionEvent(fullPath, onSessionCreated);
113
+ });
114
+ });
115
+ this.watchers.set(sessionDirPath, watcher);
116
+ watcher.on("error", (error) => {
117
+ this.emit("error", new Error(`Session watcher error: ${error.message}`));
118
+ });
119
+ this.isWatching = true;
120
+ }
121
+ catch (error) {
122
+ this.emit("error", new Error(`Session watcher setup error: ${error}`));
123
+ }
124
+ }
125
+ /**
126
+ * Debounce file system events to prevent duplicates
127
+ */
128
+ debounceEvent(eventKey, callback) {
129
+ // Clear existing timer for this event
130
+ const existingTimer = this.debounceTimers.get(eventKey);
131
+ if (existingTimer) {
132
+ clearTimeout(existingTimer);
133
+ }
134
+ // Set new timer
135
+ const timer = setTimeout(() => {
136
+ this.debounceTimers.delete(eventKey);
137
+ callback();
138
+ }, this.config.debounceMs);
139
+ this.debounceTimers.set(eventKey, timer);
140
+ }
141
+ /**
142
+ * Handle file system events with debouncing
143
+ */
144
+ handleFileEvent(eventType, filePath) {
145
+ const event = {
146
+ eventType: eventType,
147
+ filePath,
148
+ timestamp: Date.now(),
149
+ };
150
+ this.emit("fileEvent", event);
151
+ }
152
+ /**
153
+ * Handle new session directory creation
154
+ */
155
+ async handleSessionEvent(sessionPath, onSessionCreated) {
156
+ try {
157
+ // Check if this is actually a directory and has session files
158
+ const stats = await import("fs").then((fs) => fs.promises.stat(sessionPath));
159
+ if (!stats.isDirectory())
160
+ return;
161
+ // Extract session ID from directory name
162
+ const sessionId = sessionPath.split("/").pop() ?? "";
163
+ if (!sessionId)
164
+ return;
165
+ // Verify it's a valid session (has request.json)
166
+ const requestFile = join(sessionPath, "request.json");
167
+ try {
168
+ await import("fs").then((fs) => fs.promises.access(requestFile));
169
+ onSessionCreated(sessionId, sessionPath);
170
+ }
171
+ catch {
172
+ // Not a valid session directory
173
+ return;
174
+ }
175
+ }
176
+ catch {
177
+ // Error accessing directory - ignore
178
+ return;
179
+ }
180
+ }
181
+ }
182
+ /**
183
+ * TUI Session Watcher -专门用于 TUI 检测新会话
184
+ */
185
+ export class TUISessionWatcher {
186
+ /**
187
+ * Get the session directory path being watched
188
+ */
189
+ get watchedPath() {
190
+ return this.sessionDirPath;
191
+ }
192
+ fileWatcher;
193
+ sessionDirPath;
194
+ constructor(config) {
195
+ // Resolve session directory using XDG-compliant path
196
+ const sessionConfig = {
197
+ baseDir: config?.baseDir ?? "~/.local/share/auq/sessions",
198
+ };
199
+ this.sessionDirPath = resolveSessionDirectory(sessionConfig.baseDir);
200
+ this.fileWatcher = new PromiseFileWatcher({
201
+ debounceMs: config?.debounceMs ?? 200,
202
+ ignoreInitial: config?.ignoreInitial ?? true,
203
+ timeoutMs: config?.timeoutMs ?? 60000, // 1 minute for TUI
204
+ });
205
+ }
206
+ /**
207
+ * Start watching for new sessions
208
+ */
209
+ startWatching(onNewSession) {
210
+ this.fileWatcher.watchForSessions(this.sessionDirPath, onNewSession);
211
+ }
212
+ /**
213
+ * Stop watching and clean up
214
+ */
215
+ stop() {
216
+ this.fileWatcher.cleanup();
217
+ }
218
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Session module exports for AskUserQuery MCP server
3
+ */
4
+ export * from "./atomic-operations.js";
5
+ export { SessionManager } from "./SessionManager.js";
6
+ export * from "./types.js";
7
+ export * from "./utils.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Session-related TypeScript interfaces and types for AskUserQuery MCP server
3
+ */
4
+ /**
5
+ * File names for session storage
6
+ */
7
+ export const SESSION_FILES = {
8
+ ANSWERS: "answers.json",
9
+ REQUEST: "request.json",
10
+ STATUS: "status.json",
11
+ };
12
+ /**
13
+ * Default session configuration
14
+ */
15
+ export const DEFAULT_SESSION_CONFIG = {
16
+ baseDir: "~/.local/share/auq/sessions", // Will be resolved to actual path
17
+ maxSessions: 100,
18
+ retentionPeriod: 604800000, // 7 days in milliseconds
19
+ sessionTimeout: 0, // 0 = infinite timeout (wait indefinitely for user)
20
+ };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Utility functions for session management
3
+ */
4
+ import { constants } from "fs";
5
+ import { promises as fs } from "fs";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+ /**
9
+ * Create a safe filename from a session ID (basic validation)
10
+ */
11
+ export function createSafeFilename(sessionId, filename) {
12
+ if (!sanitizeSessionId(sessionId)) {
13
+ throw new Error(`Invalid session ID format: ${sessionId}`);
14
+ }
15
+ return filename;
16
+ }
17
+ /**
18
+ * Ensure a directory exists with proper permissions
19
+ */
20
+ export async function ensureDirectoryExists(dirPath) {
21
+ try {
22
+ await fs.access(dirPath, constants.W_OK);
23
+ }
24
+ catch {
25
+ await fs.mkdir(dirPath, { mode: 0o700, recursive: true });
26
+ }
27
+ }
28
+ /**
29
+ * Check if a file exists and is readable
30
+ */
31
+ export async function fileExists(filePath) {
32
+ try {
33
+ await fs.access(filePath, constants.R_OK);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ /**
41
+ * Get the current timestamp in ISO format
42
+ */
43
+ export function getCurrentTimestamp() {
44
+ return new Date().toISOString();
45
+ }
46
+ /**
47
+ * Check if a timestamp is older than the specified timeout in milliseconds
48
+ */
49
+ export function isTimestampExpired(timestamp, timeoutMs) {
50
+ const now = new Date().getTime();
51
+ const timestampTime = new Date(timestamp).getTime();
52
+ return now - timestampTime > timeoutMs;
53
+ }
54
+ /**
55
+ * Resolve session directory path using XDG Base Directory specification
56
+ * Falls back to user home directory if XDG is not available
57
+ */
58
+ export function resolveSessionDirectory(baseDir) {
59
+ if (baseDir) {
60
+ // If baseDir is provided, expand any ~ to home directory
61
+ if (baseDir.startsWith("~")) {
62
+ return join(homedir(), baseDir.slice(1));
63
+ }
64
+ return baseDir;
65
+ }
66
+ // Default XDG-compliant paths
67
+ const home = homedir();
68
+ const platform = process.platform;
69
+ if (platform === "darwin") {
70
+ // macOS: ~/Library/Application Support/
71
+ return join(home, "Library", "Application Support", "auq", "sessions");
72
+ }
73
+ else if (platform === "win32") {
74
+ // Windows: %APPDATA%/auq/sessions/
75
+ const appData = process.env.APPDATA;
76
+ if (appData) {
77
+ return join(appData, "auq", "sessions");
78
+ }
79
+ // Fallback to user profile
80
+ const userProfile = process.env.USERPROFILE || home;
81
+ return join(userProfile, "auq", "sessions");
82
+ }
83
+ else {
84
+ // Linux/Unix: ~/.local/share/ (XDG Base Directory)
85
+ // Check for XDG_DATA_HOME environment variable
86
+ const xdgDataHome = process.env.XDG_DATA_HOME;
87
+ if (xdgDataHome) {
88
+ return join(xdgDataHome, "auq", "sessions");
89
+ }
90
+ // Fallback to ~/.local/share/
91
+ return join(home, ".local", "share", "auq", "sessions");
92
+ }
93
+ }
94
+ /**
95
+ * Safely parse JSON with error handling
96
+ */
97
+ export function safeJsonParse(json, fallback) {
98
+ try {
99
+ return JSON.parse(json);
100
+ }
101
+ catch {
102
+ return fallback;
103
+ }
104
+ }
105
+ /**
106
+ * Validate that a session ID follows UUID v4 format
107
+ */
108
+ export function sanitizeSessionId(sessionId) {
109
+ // Basic validation - UUID v4 format
110
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
111
+ return uuidRegex.test(sessionId);
112
+ }
113
+ /**
114
+ * Validate that a session directory exists and is accessible
115
+ */
116
+ export async function validateSessionDirectory(baseDir) {
117
+ try {
118
+ await fs.access(baseDir, constants.R_OK | constants.W_OK);
119
+ const stat = await fs.stat(baseDir);
120
+ return stat.isDirectory();
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ }
@@ -0,0 +1,171 @@
1
+ import { promises as fs } from "fs";
2
+ import { join } from "path";
3
+ import { v4 as uuidv4 } from "uuid";
4
+ import { ensureDir, readJsonFile, writeJsonAtomically } from "./file-utils.js";
5
+ import { waitForAnswers } from "./session-watcher.js";
6
+ // Base directory for all sessions
7
+ const SESSION_BASE_DIR = "/tmp/auq/sessions";
8
+ /**
9
+ * Manages session directories and files for coordinating between MCP server and TUI
10
+ */
11
+ export class SessionManager {
12
+ /**
13
+ * Create a new session with the provided questions
14
+ */
15
+ async createSession(questions) {
16
+ const sessionId = uuidv4();
17
+ const sessionDir = this.getSessionDir(sessionId);
18
+ // Create session directory with secure permissions
19
+ await ensureDir(sessionDir, 0o700);
20
+ // Create session objects
21
+ const now = new Date().toISOString();
22
+ const request = { questions };
23
+ const status = {
24
+ createdAt: now,
25
+ state: "pending",
26
+ updatedAt: now,
27
+ };
28
+ const session = {
29
+ answers: {},
30
+ id: sessionId,
31
+ request,
32
+ status,
33
+ };
34
+ // Write session files atomically
35
+ await this.writeRequestFile(sessionId, request);
36
+ await this.writeStatusFile(sessionId, status);
37
+ return session;
38
+ }
39
+ /**
40
+ * Get all existing session IDs
41
+ */
42
+ async getAllSessionIds() {
43
+ try {
44
+ const entries = await fs.readdir(SESSION_BASE_DIR, {
45
+ withFileTypes: true,
46
+ });
47
+ return entries
48
+ .filter((entry) => entry.isDirectory())
49
+ .map((entry) => entry.name);
50
+ }
51
+ catch (error) {
52
+ // If the directory doesn't exist yet, return an empty array
53
+ if (error.code === "ENOENT") {
54
+ return [];
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+ /**
60
+ * Initialize the session manager, ensuring the base directory exists
61
+ */
62
+ async initialize() {
63
+ try {
64
+ await ensureDir(SESSION_BASE_DIR, 0o700);
65
+ }
66
+ catch (error) {
67
+ throw new Error(`Failed to create session directory: ${error}`);
68
+ }
69
+ }
70
+ /**
71
+ * Read the answers.json file for a session
72
+ */
73
+ async readAnswersFile(sessionId) {
74
+ const filePath = this.getSessionFilePath(sessionId, "answers.json");
75
+ try {
76
+ return (await readJsonFile(filePath));
77
+ }
78
+ catch (error) {
79
+ throw new Error(`Failed to read answers file for session ${sessionId}: ${error}`);
80
+ }
81
+ }
82
+ /**
83
+ * Read the request.json file for a session
84
+ */
85
+ async readRequestFile(sessionId) {
86
+ const filePath = this.getSessionFilePath(sessionId, "request.json");
87
+ try {
88
+ return (await readJsonFile(filePath));
89
+ }
90
+ catch (error) {
91
+ throw new Error(`Failed to read request file for session ${sessionId}: ${error}`);
92
+ }
93
+ }
94
+ /**
95
+ * Read the status.json file for a session
96
+ */
97
+ async readStatusFile(sessionId) {
98
+ const filePath = this.getSessionFilePath(sessionId, "status.json");
99
+ try {
100
+ return (await readJsonFile(filePath));
101
+ }
102
+ catch (error) {
103
+ throw new Error(`Failed to read status file for session ${sessionId}: ${error}`);
104
+ }
105
+ }
106
+ /**
107
+ * Check if a session exists
108
+ */
109
+ async sessionExists(sessionId) {
110
+ try {
111
+ const sessionDir = this.getSessionDir(sessionId);
112
+ const stats = await fs.stat(sessionDir);
113
+ return stats.isDirectory();
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ /**
120
+ * Start a new session and wait for user answers
121
+ */
122
+ async startSession(questions, timeoutMs = 300000 // 5 minutes default
123
+ ) {
124
+ const session = await this.createSession(questions);
125
+ // Wait for answers using the watcher
126
+ await waitForAnswers(session.id, this.getSessionDir(session.id), timeoutMs);
127
+ // Read the answers file
128
+ const answers = await this.readAnswersFile(session.id);
129
+ // Update session status to completed
130
+ await this.updateSessionStatus(session.id, "completed");
131
+ return { answers, session };
132
+ }
133
+ /**
134
+ * Update the status of a session
135
+ */
136
+ async updateSessionStatus(sessionId, state) {
137
+ const currentStatus = await this.readStatusFile(sessionId);
138
+ const updatedStatus = {
139
+ ...currentStatus,
140
+ state,
141
+ updatedAt: new Date().toISOString(),
142
+ };
143
+ await this.writeStatusFile(sessionId, updatedStatus);
144
+ }
145
+ /**
146
+ * Get the full path for a session directory
147
+ */
148
+ getSessionDir(sessionId) {
149
+ return join(SESSION_BASE_DIR, sessionId);
150
+ }
151
+ /**
152
+ * Get the path for a specific session file
153
+ */
154
+ getSessionFilePath(sessionId, filename) {
155
+ return join(this.getSessionDir(sessionId), filename);
156
+ }
157
+ /**
158
+ * Write the request.json file for a session
159
+ */
160
+ async writeRequestFile(sessionId, request) {
161
+ const filePath = this.getSessionFilePath(sessionId, "request.json");
162
+ await writeJsonAtomically(filePath, request, 0o600);
163
+ }
164
+ /**
165
+ * Write the status.json file for a session
166
+ */
167
+ async writeStatusFile(sessionId, status) {
168
+ const filePath = this.getSessionFilePath(sessionId, "status.json");
169
+ await writeJsonAtomically(filePath, status, 0o600);
170
+ }
171
+ }
@@ -0,0 +1,110 @@
1
+ import { EventEmitter } from "events";
2
+ import { watch } from "fs";
3
+ import { join } from "path";
4
+ /**
5
+ * Debounce utility to prevent multiple rapid calls
6
+ */
7
+ function debounce(func, delay) {
8
+ let timeoutId = null;
9
+ return (eventType, fullPath) => {
10
+ if (timeoutId) {
11
+ clearTimeout(timeoutId);
12
+ }
13
+ timeoutId = setTimeout(() => {
14
+ func(eventType, fullPath);
15
+ timeoutId = null;
16
+ }, delay);
17
+ };
18
+ }
19
+ /**
20
+ * Watches for session directory changes for TUI client
21
+ */
22
+ export class SessionWatcher extends EventEmitter {
23
+ baseDir;
24
+ debounceDelay;
25
+ /**
26
+ * Handle changes to session directories
27
+ */
28
+ handleSessionChange = debounce((eventType, fullPath) => {
29
+ const sessionId = fullPath.split("/").pop();
30
+ if (!sessionId)
31
+ return;
32
+ if (eventType === "rename") {
33
+ // Check if it's a new directory being created
34
+ this.emit("newSession", sessionId);
35
+ }
36
+ }, this.debounceDelay);
37
+ watchers = new Map();
38
+ constructor(baseDir, debounceDelay = 300) {
39
+ super();
40
+ this.baseDir = baseDir;
41
+ this.debounceDelay = debounceDelay;
42
+ }
43
+ /**
44
+ * Start watching for session directory changes
45
+ */
46
+ start() {
47
+ // Watch for base sessions directory for new subdirectories
48
+ const baseWatcher = watch(this.baseDir, { recursive: false }, (eventType, filename) => {
49
+ if (filename) {
50
+ const fullPath = join(this.baseDir, filename);
51
+ this.handleSessionChange(eventType, fullPath);
52
+ }
53
+ });
54
+ this.watchers.set("base", baseWatcher);
55
+ }
56
+ /**
57
+ * Stop all watching
58
+ */
59
+ stop() {
60
+ for (const [, watcher] of this.watchers) {
61
+ watcher.close();
62
+ }
63
+ this.watchers.clear();
64
+ }
65
+ /**
66
+ * Stop watching a specific session
67
+ */
68
+ stopWatchingSession(sessionId) {
69
+ const watcher = this.watchers.get(sessionId);
70
+ if (watcher) {
71
+ watcher.close();
72
+ this.watchers.delete(sessionId);
73
+ }
74
+ }
75
+ /**
76
+ * Watch a specific session for file changes
77
+ */
78
+ watchSession(sessionId) {
79
+ const sessionDir = join(this.baseDir, sessionId);
80
+ // Watch for answers.json file creation/modification
81
+ const watcher = watch(sessionDir, (eventType, filename) => {
82
+ if (filename === "answers.json") {
83
+ this.emit("sessionUpdated", sessionId, filename);
84
+ }
85
+ });
86
+ this.watchers.set(sessionId, watcher);
87
+ }
88
+ }
89
+ /**
90
+ * Watch for answers.json file creation for MCP server
91
+ */
92
+ export function waitForAnswers(sessionId, sessionDir, timeoutMs = 300000 // 5 minutes default
93
+ ) {
94
+ return new Promise((resolve, reject) => {
95
+ let timeoutId;
96
+ // Start watching for answers.json
97
+ const watcher = watch(sessionDir, (eventType, filename) => {
98
+ if (filename === "answers.json") {
99
+ clearTimeout(timeoutId);
100
+ watcher.close();
101
+ resolve();
102
+ }
103
+ });
104
+ // Set timeout
105
+ timeoutId = setTimeout(() => {
106
+ watcher.close();
107
+ reject(new Error(`Timeout waiting for answers for session ${sessionId}`));
108
+ }, timeoutMs);
109
+ });
110
+ }