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,368 @@
1
+ /**
2
+ * Unit tests for TUI session watcher functionality
3
+ */
4
+ import { promises as fs } from "fs";
5
+ import { join } from "path";
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { SESSION_FILES } from "../../session/types.js";
8
+ import { createTUIWatcher, EnhancedTUISessionWatcher, getNextPendingSession, } from "../session-watcher.js";
9
+ describe("TUI Session Watcher", () => {
10
+ const testDir = "/tmp/auq-tui-watcher-test";
11
+ const sessionDir = join(testDir, "sessions");
12
+ const testSessionId = "test-session-123";
13
+ const mockSessionRequest = {
14
+ questions: [
15
+ {
16
+ title: "Language",
17
+ options: [
18
+ { description: "Dynamic web language", label: "JavaScript" },
19
+ { description: "Typed JavaScript", label: "TypeScript" },
20
+ { description: "Versatile and readable", label: "Python" },
21
+ ],
22
+ prompt: "What is your favorite programming language?",
23
+ },
24
+ ],
25
+ sessionId: testSessionId,
26
+ status: "pending",
27
+ timestamp: new Date().toISOString(),
28
+ };
29
+ beforeEach(async () => {
30
+ // Clean up test directory before each test
31
+ await fs.rm(testDir, { force: true, recursive: true }).catch(() => { });
32
+ await fs.mkdir(sessionDir, { recursive: true });
33
+ });
34
+ afterEach(async () => {
35
+ // Clean up test directory after each test
36
+ await fs.rm(testDir, { force: true, recursive: true }).catch(() => { });
37
+ });
38
+ describe("EnhancedTUISessionWatcher", () => {
39
+ describe("session event detection", () => {
40
+ it("should detect new sessions with loaded data", async () => {
41
+ const watcher = new EnhancedTUISessionWatcher({
42
+ autoLoadData: true,
43
+ debounceMs: 100,
44
+ sessionDir,
45
+ });
46
+ const events = [];
47
+ watcher.startEnhancedWatching((event) => {
48
+ events.push(event);
49
+ });
50
+ // Give watcher time to initialize
51
+ await new Promise((resolve) => setTimeout(resolve, 50));
52
+ // Create a new session directory
53
+ const newSessionDir = join(sessionDir, testSessionId);
54
+ await fs.mkdir(newSessionDir);
55
+ // Create required session files
56
+ const requestFile = join(newSessionDir, SESSION_FILES.REQUEST);
57
+ const statusFile = join(newSessionDir, SESSION_FILES.STATUS);
58
+ await Promise.all([
59
+ fs.writeFile(requestFile, JSON.stringify(mockSessionRequest, null, 2)),
60
+ fs.writeFile(statusFile, JSON.stringify({
61
+ createdAt: new Date().toISOString(),
62
+ lastModified: new Date().toISOString(),
63
+ sessionId: testSessionId,
64
+ status: "pending",
65
+ totalQuestions: 1,
66
+ })),
67
+ ]);
68
+ // Wait for debounce (100ms) + processing time
69
+ await new Promise((resolve) => setTimeout(resolve, 300));
70
+ expect(events).toHaveLength(1);
71
+ expect(events[0].type).toBe("session-created");
72
+ expect(events[0].sessionId).toBe(testSessionId);
73
+ expect(events[0].sessionRequest).toEqual(mockSessionRequest);
74
+ watcher.stop();
75
+ });
76
+ it("should handle autoLoadData disabled", async () => {
77
+ const watcher = new EnhancedTUISessionWatcher({
78
+ autoLoadData: false,
79
+ debounceMs: 100,
80
+ sessionDir,
81
+ });
82
+ const events = [];
83
+ watcher.startEnhancedWatching((event) => {
84
+ events.push(event);
85
+ });
86
+ // Give watcher time to initialize
87
+ await new Promise((resolve) => setTimeout(resolve, 50));
88
+ // Create session directory and files
89
+ const newSessionDir = join(sessionDir, testSessionId);
90
+ await fs.mkdir(newSessionDir);
91
+ const requestFile = join(newSessionDir, SESSION_FILES.REQUEST);
92
+ await fs.writeFile(requestFile, JSON.stringify(mockSessionRequest, null, 2));
93
+ // Wait for debounce + processing
94
+ await new Promise((resolve) => setTimeout(resolve, 300));
95
+ expect(events).toHaveLength(1);
96
+ expect(events[0].type).toBe("session-created");
97
+ expect(events[0].sessionId).toBe(testSessionId);
98
+ expect(events[0].sessionRequest).toBeUndefined(); // Should not be loaded
99
+ watcher.stop();
100
+ });
101
+ it("should handle corrupted session files gracefully", async () => {
102
+ const watcher = new EnhancedTUISessionWatcher({
103
+ autoLoadData: true,
104
+ debounceMs: 100,
105
+ sessionDir,
106
+ });
107
+ const events = [];
108
+ const consoleSpy = vi
109
+ .spyOn(console, "warn")
110
+ .mockImplementation(() => { });
111
+ watcher.startEnhancedWatching((event) => {
112
+ events.push(event);
113
+ });
114
+ // Create session with corrupted request.json
115
+ const newSessionDir = join(sessionDir, testSessionId);
116
+ await fs.mkdir(newSessionDir);
117
+ const requestFile = join(newSessionDir, SESSION_FILES.REQUEST);
118
+ const statusFile = join(newSessionDir, SESSION_FILES.STATUS);
119
+ // Write corrupted request.json and valid status.json
120
+ await Promise.all([
121
+ fs.writeFile(requestFile, "invalid json content"),
122
+ fs.writeFile(statusFile, JSON.stringify({
123
+ createdAt: new Date().toISOString(),
124
+ lastModified: new Date().toISOString(),
125
+ sessionId: testSessionId,
126
+ status: "pending",
127
+ totalQuestions: 1,
128
+ })),
129
+ ]);
130
+ // Wait for debounce + processing
131
+ await new Promise((resolve) => setTimeout(resolve, 300));
132
+ expect(events).toHaveLength(1);
133
+ expect(events[0].type).toBe("session-created");
134
+ expect(events[0].sessionId).toBe(testSessionId);
135
+ expect(events[0].sessionRequest).toBeUndefined();
136
+ // Should have logged a warning about corrupted data
137
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to load session request"), expect.any(Error));
138
+ consoleSpy.mockRestore();
139
+ watcher.stop();
140
+ });
141
+ });
142
+ describe("event handlers", () => {
143
+ it("should support multiple event handlers", async () => {
144
+ const watcher = new EnhancedTUISessionWatcher({
145
+ debounceMs: 100,
146
+ sessionDir,
147
+ });
148
+ const mainEvents = [];
149
+ const customEvents = [];
150
+ watcher.addEventHandler("custom", (event) => {
151
+ customEvents.push(event);
152
+ });
153
+ watcher.startEnhancedWatching((event) => {
154
+ mainEvents.push(event);
155
+ });
156
+ // Give watcher time to initialize
157
+ await new Promise((resolve) => setTimeout(resolve, 50));
158
+ // Create session
159
+ const newSessionDir = join(sessionDir, testSessionId);
160
+ await fs.mkdir(newSessionDir);
161
+ const requestFile = join(newSessionDir, SESSION_FILES.REQUEST);
162
+ await fs.writeFile(requestFile, JSON.stringify(mockSessionRequest, null, 2));
163
+ // Wait for debounce + processing
164
+ await new Promise((resolve) => setTimeout(resolve, 300));
165
+ expect(mainEvents).toHaveLength(1);
166
+ expect(customEvents).toHaveLength(1);
167
+ expect(customEvents[0]).toEqual(mainEvents[0]);
168
+ watcher.removeEventHandler("custom");
169
+ watcher.stop();
170
+ });
171
+ it("should handle event handler removal", async () => {
172
+ const watcher = new EnhancedTUISessionWatcher({ sessionDir });
173
+ const customEvents = [];
174
+ watcher.addEventHandler("custom", (event) => {
175
+ customEvents.push(event);
176
+ });
177
+ watcher.removeEventHandler("custom");
178
+ watcher.startEnhancedWatching(() => { });
179
+ // Create session
180
+ const newSessionDir = join(sessionDir, testSessionId);
181
+ await fs.mkdir(newSessionDir);
182
+ const requestFile = join(newSessionDir, SESSION_FILES.REQUEST);
183
+ await fs.writeFile(requestFile, JSON.stringify(mockSessionRequest, null, 2));
184
+ // Wait for processing
185
+ await new Promise((resolve) => setTimeout(resolve, 100));
186
+ expect(customEvents).toHaveLength(0); // Should not have triggered
187
+ watcher.stop();
188
+ });
189
+ });
190
+ describe("session management", () => {
191
+ beforeEach(async () => {
192
+ // Create multiple test sessions
193
+ const sessions = [
194
+ { completed: false, id: "session-1" },
195
+ { completed: true, id: "session-2" },
196
+ { completed: false, id: "session-3" },
197
+ ];
198
+ for (const session of sessions) {
199
+ const sessionDir = join(testDir, "sessions", session.id);
200
+ await fs.mkdir(sessionDir, { recursive: true });
201
+ // Create request.json
202
+ const requestFile = join(sessionDir, SESSION_FILES.REQUEST);
203
+ await fs.writeFile(requestFile, JSON.stringify({
204
+ ...mockSessionRequest,
205
+ sessionId: session.id,
206
+ }));
207
+ // Create status.json
208
+ const statusFile = join(sessionDir, SESSION_FILES.STATUS);
209
+ await fs.writeFile(statusFile, JSON.stringify({
210
+ createdAt: new Date().toISOString(),
211
+ lastModified: new Date().toISOString(),
212
+ sessionId: session.id,
213
+ status: session.completed ? "completed" : "pending",
214
+ totalQuestions: 1,
215
+ }));
216
+ // Create answers.json for completed sessions
217
+ if (session.completed) {
218
+ const answersFile = join(sessionDir, SESSION_FILES.ANSWERS);
219
+ await fs.writeFile(answersFile, JSON.stringify({
220
+ answers: [
221
+ {
222
+ questionIndex: 0,
223
+ selectedOption: "JavaScript",
224
+ timestamp: new Date().toISOString(),
225
+ },
226
+ ],
227
+ sessionId: session.id,
228
+ timestamp: new Date().toISOString(),
229
+ }));
230
+ }
231
+ }
232
+ });
233
+ it("should get pending sessions correctly", async () => {
234
+ const watcher = new EnhancedTUISessionWatcher({ sessionDir });
235
+ const pendingSessions = await watcher.getPendingSessions();
236
+ expect(pendingSessions).toHaveLength(2);
237
+ expect(pendingSessions).toContain("session-1");
238
+ expect(pendingSessions).toContain("session-3");
239
+ expect(pendingSessions).not.toContain("session-2");
240
+ // Should be sorted
241
+ expect(pendingSessions[0]).toBe("session-1");
242
+ expect(pendingSessions[1]).toBe("session-3");
243
+ watcher.stop();
244
+ });
245
+ it("should get session request data", async () => {
246
+ const watcher = new EnhancedTUISessionWatcher({ sessionDir });
247
+ const sessionRequest = await watcher.getSessionRequest("session-1");
248
+ expect(sessionRequest).toBeTruthy();
249
+ expect(sessionRequest?.sessionId).toBe("session-1");
250
+ expect(sessionRequest?.questions).toHaveLength(1);
251
+ watcher.stop();
252
+ });
253
+ it("should handle non-existent session request", async () => {
254
+ const watcher = new EnhancedTUISessionWatcher({ sessionDir });
255
+ const consoleSpy = vi
256
+ .spyOn(console, "warn")
257
+ .mockImplementation(() => { });
258
+ const sessionRequest = await watcher.getSessionRequest("non-existent");
259
+ expect(sessionRequest).toBeNull();
260
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to load session request"), expect.any(Error));
261
+ consoleSpy.mockRestore();
262
+ watcher.stop();
263
+ });
264
+ it("should check session pending status correctly", async () => {
265
+ const watcher = new EnhancedTUISessionWatcher({ sessionDir });
266
+ expect(await watcher.isSessionPending("session-1")).toBe(true);
267
+ expect(await watcher.isSessionPending("session-2")).toBe(false);
268
+ expect(await watcher.isSessionPending("session-3")).toBe(true);
269
+ expect(await watcher.isSessionPending("non-existent")).toBe(false);
270
+ watcher.stop();
271
+ });
272
+ });
273
+ });
274
+ describe("Utility Functions", () => {
275
+ describe("createTUIWatcher", () => {
276
+ it("should create an enhanced TUI session watcher", () => {
277
+ const watcher = createTUIWatcher({ sessionDir });
278
+ expect(watcher).toBeInstanceOf(EnhancedTUISessionWatcher);
279
+ expect(watcher.watchedPath).toBe(sessionDir);
280
+ watcher.stop();
281
+ });
282
+ });
283
+ describe("getNextPendingSession", () => {
284
+ beforeEach(async () => {
285
+ // Create test sessions
286
+ const sessions = [
287
+ { id: "first-session", pending: true },
288
+ { id: "second-session", pending: true },
289
+ ];
290
+ for (const session of sessions) {
291
+ const sessionDir = join(testDir, "sessions", session.id);
292
+ await fs.mkdir(sessionDir, { recursive: true });
293
+ // Create required files
294
+ const requestFile = join(sessionDir, SESSION_FILES.REQUEST);
295
+ const statusFile = join(sessionDir, SESSION_FILES.STATUS);
296
+ await Promise.all([
297
+ fs.writeFile(requestFile, JSON.stringify({
298
+ ...mockSessionRequest,
299
+ sessionId: session.id,
300
+ })),
301
+ fs.writeFile(statusFile, JSON.stringify({
302
+ createdAt: new Date().toISOString(),
303
+ lastModified: new Date().toISOString(),
304
+ sessionId: session.id,
305
+ status: "pending",
306
+ totalQuestions: 1,
307
+ })),
308
+ ]);
309
+ }
310
+ });
311
+ it("should get the next pending session", async () => {
312
+ const result = await getNextPendingSession({ sessionDir });
313
+ expect(result).toBeTruthy();
314
+ expect(result?.sessionId).toBe("first-session");
315
+ expect(result?.sessionRequest).toBeTruthy();
316
+ expect(result?.sessionRequest.sessionId).toBe("first-session");
317
+ });
318
+ it("should return null when no pending sessions", async () => {
319
+ // Clear all sessions
320
+ await fs.rm(sessionDir, { recursive: true });
321
+ await fs.mkdir(sessionDir);
322
+ const result = await getNextPendingSession({ sessionDir });
323
+ expect(result).toBeNull();
324
+ });
325
+ it("should handle sessions with corrupted data", async () => {
326
+ // Create session with corrupted request.json
327
+ const corruptedSessionDir = join(sessionDir, "corrupted-session");
328
+ await fs.mkdir(corruptedSessionDir);
329
+ const statusFile = join(corruptedSessionDir, SESSION_FILES.STATUS);
330
+ await fs.writeFile(statusFile, JSON.stringify({
331
+ createdAt: new Date().toISOString(),
332
+ lastModified: new Date().toISOString(),
333
+ sessionId: "corrupted-session",
334
+ status: "pending",
335
+ totalQuestions: 1,
336
+ }));
337
+ // Create corrupted request.json
338
+ const requestFile = join(corruptedSessionDir, SESSION_FILES.REQUEST);
339
+ await fs.writeFile(requestFile, "invalid json");
340
+ const result = await getNextPendingSession({ sessionDir });
341
+ // Should skip corrupted session and return valid one
342
+ expect(result).toBeTruthy();
343
+ expect(result?.sessionId).toBe("first-session");
344
+ });
345
+ });
346
+ });
347
+ describe("Error Handling", () => {
348
+ it("should handle directory access errors", async () => {
349
+ const watcher = new EnhancedTUISessionWatcher({
350
+ sessionDir: "/invalid/directory/path",
351
+ });
352
+ const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
353
+ const pendingSessions = await watcher.getPendingSessions();
354
+ expect(pendingSessions).toEqual([]);
355
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to scan for pending sessions"), expect.any(Error));
356
+ consoleSpy.mockRestore();
357
+ watcher.stop();
358
+ });
359
+ it("should handle missing watched path gracefully", async () => {
360
+ const watcher = new EnhancedTUISessionWatcher({
361
+ sessionDir: "/non/existent/path",
362
+ });
363
+ const sessionRequest = await watcher.getSessionRequest("any-id");
364
+ expect(sessionRequest).toBeNull();
365
+ watcher.stop();
366
+ });
367
+ });
368
+ });
@@ -0,0 +1,183 @@
1
+ /**
2
+ * TUI Session Watcher Module
3
+ *
4
+ * Provides TUI applications with the ability to detect new question sessions
5
+ * and coordinate with the MCP server through file system events.
6
+ */
7
+ import { atomicReadFile } from "../session/atomic-operations.js";
8
+ import { TUISessionWatcher } from "../session/file-watcher.js";
9
+ import { SESSION_FILES } from "../session/types.js";
10
+ /**
11
+ * Enhanced TUI Session Watcher with session data loading
12
+ */
13
+ export class EnhancedTUISessionWatcher extends TUISessionWatcher {
14
+ autoLoadData;
15
+ eventHandlers = new Map();
16
+ constructor(config) {
17
+ // Map sessionDir to baseDir for parent class
18
+ super({
19
+ baseDir: config?.sessionDir,
20
+ debounceMs: config?.debounceMs,
21
+ });
22
+ this.autoLoadData = config?.autoLoadData ?? true;
23
+ }
24
+ /**
25
+ * Add custom event handler
26
+ */
27
+ addEventHandler(name, handler) {
28
+ this.eventHandlers.set(name, handler);
29
+ }
30
+ /**
31
+ * Get list of pending sessions (sessions without answers)
32
+ */
33
+ async getPendingSessions() {
34
+ const fs = await import("fs/promises");
35
+ const { join } = await import("path");
36
+ try {
37
+ const sessionDir = this.watchedPath;
38
+ const entries = await fs.readdir(sessionDir, { withFileTypes: true });
39
+ const pendingSessions = [];
40
+ for (const entry of entries) {
41
+ if (!entry.isDirectory())
42
+ continue;
43
+ const sessionPath = join(sessionDir, entry.name);
44
+ const answersPath = join(sessionPath, SESSION_FILES.ANSWERS);
45
+ const statusPath = join(sessionPath, SESSION_FILES.STATUS);
46
+ try {
47
+ // Check if answers file doesn't exist (pending session)
48
+ await fs.access(answersPath);
49
+ }
50
+ catch {
51
+ // Answers file doesn't exist - check if this is a valid session
52
+ try {
53
+ await fs.access(statusPath);
54
+ pendingSessions.push(entry.name);
55
+ }
56
+ catch {
57
+ // No status file - not a valid session
58
+ }
59
+ }
60
+ }
61
+ return pendingSessions.sort(); // Sort for consistent ordering
62
+ }
63
+ catch (error) {
64
+ console.warn("Failed to scan for pending sessions:", error);
65
+ return [];
66
+ }
67
+ }
68
+ /**
69
+ * Get session request data for a specific session
70
+ */
71
+ async getSessionRequest(sessionId) {
72
+ const { join } = await import("path");
73
+ const sessionPath = join(this.watchedPath, sessionId);
74
+ try {
75
+ return await this.loadSessionRequest(sessionPath);
76
+ }
77
+ catch (error) {
78
+ console.warn(`Failed to load session request for ${sessionId}:`, error);
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Check if a session exists and is pending
84
+ */
85
+ async isSessionPending(sessionId) {
86
+ const { join } = await import("path");
87
+ const sessionPath = join(this.watchedPath, sessionId);
88
+ const answersPath = join(sessionPath, SESSION_FILES.ANSWERS);
89
+ const statusPath = join(sessionPath, SESSION_FILES.STATUS);
90
+ try {
91
+ const fs = await import("fs/promises");
92
+ // Check if status file exists (valid session)
93
+ await fs.access(statusPath);
94
+ // Check if answers file doesn't exist (pending)
95
+ try {
96
+ await fs.access(answersPath);
97
+ return false; // Answers exist - session is completed
98
+ }
99
+ catch {
100
+ return true; // No answers - session is pending
101
+ }
102
+ }
103
+ catch {
104
+ return false; // Not a valid session
105
+ }
106
+ }
107
+ /**
108
+ * Remove event handler
109
+ */
110
+ removeEventHandler(name) {
111
+ this.eventHandlers.delete(name);
112
+ }
113
+ /**
114
+ * Start watching with enhanced event handling
115
+ */
116
+ startEnhancedWatching(onSessionEvent) {
117
+ this.startWatching(async (sessionId, sessionPath) => {
118
+ const event = {
119
+ sessionId,
120
+ sessionPath,
121
+ timestamp: Date.now(),
122
+ type: "session-created",
123
+ };
124
+ // Auto-load session data if requested
125
+ if (this.autoLoadData) {
126
+ try {
127
+ const sessionRequest = await this.loadSessionRequest(sessionPath);
128
+ event.sessionRequest = sessionRequest;
129
+ }
130
+ catch (error) {
131
+ console.warn(`Failed to load session request for ${sessionId}:`, error);
132
+ }
133
+ }
134
+ // Emit the event
135
+ onSessionEvent(event);
136
+ // Call any additional handlers
137
+ this.eventHandlers.forEach((handler) => handler(event));
138
+ });
139
+ }
140
+ /**
141
+ * Load session request data from file
142
+ */
143
+ async loadSessionRequest(sessionPath) {
144
+ const requestPath = `${sessionPath}/${SESSION_FILES.REQUEST}`;
145
+ try {
146
+ const content = await atomicReadFile(requestPath, {
147
+ encoding: "utf8",
148
+ maxRetries: 3,
149
+ retryDelay: 100,
150
+ });
151
+ return JSON.parse(content);
152
+ }
153
+ catch (error) {
154
+ throw new Error(`Failed to load session request from ${requestPath}: ${error}`);
155
+ }
156
+ }
157
+ }
158
+ /**
159
+ * Create a simple TUI session watcher instance
160
+ */
161
+ export function createTUIWatcher(config) {
162
+ return new EnhancedTUISessionWatcher(config);
163
+ }
164
+ /**
165
+ * Convenience function to get the next pending session
166
+ */
167
+ export async function getNextPendingSession(config) {
168
+ const watcher = createTUIWatcher(config);
169
+ const pendingSessions = await watcher.getPendingSessions();
170
+ if (pendingSessions.length === 0) {
171
+ return null;
172
+ }
173
+ // Try each pending session until we find one with valid data
174
+ for (const sessionId of pendingSessions) {
175
+ const sessionRequest = await watcher.getSessionRequest(sessionId);
176
+ if (sessionRequest) {
177
+ return { sessionId, sessionRequest };
178
+ }
179
+ // Skip corrupted sessions and continue to next one
180
+ }
181
+ // No valid sessions found
182
+ return null;
183
+ }
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "auq-mcp-server",
3
+ "version": "0.1.0",
4
+ "main": "dist/index.js",
5
+ "bin": {
6
+ "auq": "dist/bin/auq.js"
7
+ },
8
+ "files": [
9
+ "dist",
10
+ "scripts/postinstall.cjs",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepare": "npm run build",
17
+ "postinstall": "node scripts/postinstall.cjs",
18
+ "server": "node dist/src/server.js",
19
+ "start": "tsx src/server.ts",
20
+ "dev": "fastmcp dev src/server.ts",
21
+ "lint": "prettier --check . && eslint . && tsc --noEmit",
22
+ "test": "vitest run",
23
+ "format": "prettier --write . && eslint --fix ."
24
+ },
25
+ "keywords": [
26
+ "fastmcp",
27
+ "mcp",
28
+ "ask-user-questions",
29
+ "tui",
30
+ "terminal"
31
+ ],
32
+ "repository": {
33
+ "url": "git+https://github.com/paulp-o/ask-user-question-mcp.git"
34
+ },
35
+ "author": "Paul Park",
36
+ "homepage": "https://github.com/paulp-o/ask-user-question-mcp",
37
+ "type": "module",
38
+ "license": "MIT",
39
+ "description": "An MCP server that provides a tool to ask a user questions via the terminal",
40
+ "dependencies": {
41
+ "@inkjs/ui": "^2.0.0",
42
+ "@types/uuid": "^10.0.0",
43
+ "chalk": "^5.6.2",
44
+ "fastmcp": "^1.27.3",
45
+ "gradient-string": "^3.0.0",
46
+ "ink": "^6.4.0",
47
+ "ink-gradient": "^3.0.0",
48
+ "ink-text-input": "^6.0.0",
49
+ "react": "^19.2.0",
50
+ "uuid": "^13.0.0",
51
+ "zod": "^3.24.4"
52
+ },
53
+ "release": {
54
+ "branches": [
55
+ "main"
56
+ ],
57
+ "plugins": [
58
+ "@semantic-release/commit-analyzer",
59
+ "@semantic-release/release-notes-generator",
60
+ "@semantic-release/npm",
61
+ "@semantic-release/github"
62
+ ]
63
+ },
64
+ "devDependencies": {
65
+ "@eslint/js": "^9.26.0",
66
+ "@tsconfig/node22": "^22.0.1",
67
+ "@types/react": "^19.2.2",
68
+ "eslint-config-prettier": "^10.1.3",
69
+ "eslint-plugin-perfectionist": "^4.12.3",
70
+ "jiti": "^2.4.2",
71
+ "prettier": "^3.5.3",
72
+ "semantic-release": "^24.2.3",
73
+ "tsx": "^4.19.4",
74
+ "typescript": "^5.8.3",
75
+ "typescript-eslint": "^8.32.0",
76
+ "vitest": "^3.1.3"
77
+ }
78
+ }
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Post-install script for auq-mcp-server
5
+ * Provides instructions for setting up shell aliases
6
+ */
7
+
8
+ const os = require('os');
9
+ const path = require('path');
10
+
11
+ // Check if this is a global installation
12
+ const isGlobal = process.env.npm_config_global === 'true';
13
+
14
+ if (!isGlobal) {
15
+ // Local install - no setup needed
16
+ process.exit(0);
17
+ }
18
+
19
+ console.log('\nāœ… AUQ MCP Server installed successfully!\n');
20
+ console.log('šŸ“ Optional: Set up shell aliases for convenience\n');
21
+
22
+ const homeDir = os.homedir();
23
+ const shell = process.env.SHELL || '';
24
+
25
+ // Determine shell config file
26
+ let configFile = '';
27
+ let aliasCommand = '';
28
+
29
+ if (shell.includes('zsh')) {
30
+ configFile = path.join(homeDir, '.zshrc');
31
+ aliasCommand = 'alias auq="npx auq-mcp-server"';
32
+ } else if (shell.includes('bash')) {
33
+ configFile = path.join(homeDir, '.bashrc');
34
+ aliasCommand = 'alias auq="npx auq-mcp-server"';
35
+ } else if (shell.includes('fish')) {
36
+ configFile = path.join(homeDir, '.config/fish/config.fish');
37
+ aliasCommand = 'alias auq "npx auq-mcp-server"';
38
+ }
39
+
40
+ if (configFile) {
41
+ console.log(`To set up a shell alias, add this to your ${path.basename(configFile)}:\n`);
42
+ console.log(` ${aliasCommand}\n`);
43
+ console.log(`Then restart your terminal or run: source ${configFile}\n`);
44
+ } else {
45
+ console.log('To set up a shell alias, add this to your shell config:\n');
46
+ console.log(' alias auq="npx auq-mcp-server"\n');
47
+ console.log('Then restart your terminal.\n');
48
+ }
49
+
50
+ console.log('For MCP server setup with Claude Desktop or Cursor, see:');
51
+ console.log(' https://github.com/paulp-o/ask-user-question-mcp#setup\n');