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.
- package/LICENSE +25 -0
- package/README.md +176 -0
- package/dist/__tests__/schema-validation.test.js +137 -0
- package/dist/__tests__/server.integration.test.js +263 -0
- package/dist/add.js +1 -0
- package/dist/add.test.js +5 -0
- package/dist/bin/auq.js +245 -0
- package/dist/bin/test-session-menu.js +28 -0
- package/dist/bin/test-tabbar.js +42 -0
- package/dist/file-utils.js +59 -0
- package/dist/format/ResponseFormatter.js +206 -0
- package/dist/format/__tests__/ResponseFormatter.test.js +380 -0
- package/dist/package.json +74 -0
- package/dist/server.js +107 -0
- package/dist/session/ResponseFormatter.js +130 -0
- package/dist/session/SessionManager.js +474 -0
- package/dist/session/__tests__/ResponseFormatter.test.js +417 -0
- package/dist/session/__tests__/SessionManager.test.js +553 -0
- package/dist/session/__tests__/atomic-operations.test.js +345 -0
- package/dist/session/__tests__/file-watcher.test.js +311 -0
- package/dist/session/__tests__/workflow.integration.test.js +334 -0
- package/dist/session/atomic-operations.js +307 -0
- package/dist/session/file-watcher.js +218 -0
- package/dist/session/index.js +7 -0
- package/dist/session/types.js +20 -0
- package/dist/session/utils.js +125 -0
- package/dist/session-manager.js +171 -0
- package/dist/session-watcher.js +110 -0
- package/dist/src/__tests__/schema-validation.test.js +170 -0
- package/dist/src/__tests__/server.integration.test.js +274 -0
- package/dist/src/add.js +1 -0
- package/dist/src/add.test.js +5 -0
- package/dist/src/server.js +163 -0
- package/dist/src/session/ResponseFormatter.js +163 -0
- package/dist/src/session/SessionManager.js +572 -0
- package/dist/src/session/__tests__/ResponseFormatter.test.js +741 -0
- package/dist/src/session/__tests__/SessionManager.test.js +593 -0
- package/dist/src/session/__tests__/atomic-operations.test.js +346 -0
- package/dist/src/session/__tests__/file-watcher.test.js +311 -0
- package/dist/src/session/atomic-operations.js +307 -0
- package/dist/src/session/file-watcher.js +227 -0
- package/dist/src/session/index.js +7 -0
- package/dist/src/session/types.js +20 -0
- package/dist/src/session/utils.js +180 -0
- package/dist/src/tui/__tests__/session-watcher.test.js +368 -0
- package/dist/src/tui/components/AnimatedGradient.js +45 -0
- package/dist/src/tui/components/ConfirmationDialog.js +89 -0
- package/dist/src/tui/components/CustomInput.js +14 -0
- package/dist/src/tui/components/Footer.js +55 -0
- package/dist/src/tui/components/Header.js +35 -0
- package/dist/src/tui/components/MultiLineTextInput.js +65 -0
- package/dist/src/tui/components/OptionsList.js +115 -0
- package/dist/src/tui/components/QuestionDisplay.js +36 -0
- package/dist/src/tui/components/ReviewScreen.js +57 -0
- package/dist/src/tui/components/SessionSelectionMenu.js +151 -0
- package/dist/src/tui/components/StepperView.js +166 -0
- package/dist/src/tui/components/TabBar.js +42 -0
- package/dist/src/tui/components/Toast.js +19 -0
- package/dist/src/tui/components/WaitingScreen.js +20 -0
- package/dist/src/tui/session-watcher.js +195 -0
- package/dist/src/tui/theme.js +114 -0
- package/dist/src/tui/utils/gradientText.js +24 -0
- package/dist/tui/__tests__/session-watcher.test.js +368 -0
- package/dist/tui/session-watcher.js +183 -0
- package/package.json +78 -0
- 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,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
|
+
}
|