auq-mcp-server 0.1.9 → 0.1.24

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 (39) hide show
  1. package/README.md +24 -0
  2. package/dist/bin/auq.js +127 -7
  3. package/dist/package.json +15 -5
  4. package/dist/src/__tests__/schema-validation.test.js +32 -24
  5. package/dist/src/core/ask-user-questions.js +74 -0
  6. package/dist/src/server.js +11 -74
  7. package/dist/src/tui/components/Header.js +9 -1
  8. package/dist/src/tui/components/QuestionDisplay.js +10 -6
  9. package/dist/src/tui/components/ReviewScreen.js +6 -2
  10. package/dist/src/tui/components/StepperView.js +25 -3
  11. package/dist/src/tui/components/WaitingScreen.js +31 -4
  12. package/package.json +7 -1
  13. package/dist/__tests__/schema-validation.test.js +0 -137
  14. package/dist/__tests__/server.integration.test.js +0 -263
  15. package/dist/add.js +0 -1
  16. package/dist/add.test.js +0 -5
  17. package/dist/bin/test-session-menu.js +0 -28
  18. package/dist/bin/test-tabbar.js +0 -42
  19. package/dist/file-utils.js +0 -59
  20. package/dist/format/ResponseFormatter.js +0 -206
  21. package/dist/format/__tests__/ResponseFormatter.test.js +0 -380
  22. package/dist/server.js +0 -107
  23. package/dist/session/ResponseFormatter.js +0 -130
  24. package/dist/session/SessionManager.js +0 -474
  25. package/dist/session/__tests__/ResponseFormatter.test.js +0 -417
  26. package/dist/session/__tests__/SessionManager.test.js +0 -553
  27. package/dist/session/__tests__/atomic-operations.test.js +0 -345
  28. package/dist/session/__tests__/file-watcher.test.js +0 -311
  29. package/dist/session/__tests__/workflow.integration.test.js +0 -334
  30. package/dist/session/atomic-operations.js +0 -307
  31. package/dist/session/file-watcher.js +0 -218
  32. package/dist/session/index.js +0 -7
  33. package/dist/session/types.js +0 -20
  34. package/dist/session/utils.js +0 -125
  35. package/dist/session-manager.js +0 -171
  36. package/dist/session-watcher.js +0 -110
  37. package/dist/src/tui/components/SessionSelectionMenu.js +0 -151
  38. package/dist/tui/__tests__/session-watcher.test.js +0 -368
  39. package/dist/tui/session-watcher.js +0 -183
@@ -1,151 +0,0 @@
1
- import React, { useState, useEffect } from "react";
2
- import { Box, Text, useInput, useApp } from "ink";
3
- import { createTUIWatcher } from "../session-watcher.js";
4
- /**
5
- * SessionSelectionMenu displays a list of pending question sets and allows user to select one
6
- * Uses ↑↓ for navigation, Enter to select, q to quit
7
- */
8
- export const SessionSelectionMenu = ({ onSessionSelect, }) => {
9
- const { exit } = useApp();
10
- const [sessions, setSessions] = useState([]);
11
- const [selectedIndex, setSelectedIndex] = useState(0);
12
- const [isLoading, setIsLoading] = useState(true);
13
- const [error, setError] = useState(null);
14
- // Load pending sessions on mount and start persistent watcher
15
- useEffect(() => {
16
- let watcherInstance = null;
17
- const initialize = async () => {
18
- try {
19
- setIsLoading(true);
20
- // Step 1: Load existing pending sessions
21
- const watcher = createTUIWatcher();
22
- const sessionIds = await watcher.getPendingSessions();
23
- const sessionData = await Promise.all(sessionIds.map(async (sessionId) => {
24
- const sessionRequest = await watcher.getSessionRequest(sessionId);
25
- if (!sessionRequest)
26
- return null;
27
- return {
28
- sessionId,
29
- sessionRequest,
30
- timestamp: new Date(sessionRequest.timestamp),
31
- };
32
- }));
33
- // Filter out null entries and sort by timestamp (newest first)
34
- const validSessions = sessionData
35
- .filter((s) => s !== null)
36
- .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
37
- setSessions(validSessions);
38
- setIsLoading(false);
39
- // Step 2: Start persistent watcher for new sessions
40
- watcherInstance = createTUIWatcher({ autoLoadData: true });
41
- watcherInstance.startEnhancedWatching((event) => {
42
- // Add new session to queue (FIFO - append to end)
43
- setSessions((prev) => {
44
- // Check for duplicates
45
- if (prev.some((s) => s.sessionId === event.sessionId)) {
46
- return prev;
47
- }
48
- // Add to end of queue
49
- return [
50
- ...prev,
51
- {
52
- sessionId: event.sessionId,
53
- sessionRequest: event.sessionRequest,
54
- timestamp: new Date(event.timestamp),
55
- },
56
- ];
57
- });
58
- });
59
- }
60
- catch (err) {
61
- setError(err instanceof Error ? err.message : "Failed to load question sets");
62
- setIsLoading(false);
63
- }
64
- };
65
- initialize();
66
- // Cleanup: stop watcher on unmount
67
- return () => {
68
- if (watcherInstance) {
69
- watcherInstance.stop();
70
- }
71
- };
72
- }, []);
73
- // Handle keyboard input
74
- useInput((input, key) => {
75
- if (isLoading)
76
- return;
77
- if (key.upArrow && sessions.length > 0) {
78
- setSelectedIndex((prev) => Math.max(0, prev - 1));
79
- }
80
- if (key.downArrow && sessions.length > 0) {
81
- setSelectedIndex((prev) => Math.min(sessions.length - 1, prev + 1));
82
- }
83
- if (key.return && sessions[selectedIndex]) {
84
- const { sessionId, sessionRequest } = sessions[selectedIndex];
85
- onSessionSelect(sessionId, sessionRequest);
86
- }
87
- if (input === "q") {
88
- exit();
89
- }
90
- });
91
- // Loading state
92
- if (isLoading) {
93
- return (React.createElement(Box, { padding: 1 },
94
- React.createElement(Text, null, "Loading question sets...")));
95
- }
96
- // Error state
97
- if (error) {
98
- return (React.createElement(Box, { flexDirection: "column", padding: 1 },
99
- React.createElement(Text, { color: "red" },
100
- "Error: ",
101
- error),
102
- React.createElement(Text, { dimColor: true }, "Press q to quit")));
103
- }
104
- // Zero sessions state
105
- if (sessions.length === 0) {
106
- return (React.createElement(Box, { flexDirection: "column", padding: 1 },
107
- React.createElement(Text, { color: "yellow" }, "No pending question sets found."),
108
- React.createElement(Text, { dimColor: true }, "Waiting for AI to ask questions..."),
109
- React.createElement(Text, { dimColor: true }, "Press q to quit")));
110
- }
111
- // Session selection menu
112
- return (React.createElement(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan" },
113
- React.createElement(Text, { bold: true }, "Select a pending question set:"),
114
- React.createElement(Box, { marginTop: 1 }),
115
- sessions.map((session, idx) => {
116
- const isSelected = idx === selectedIndex;
117
- const indicator = isSelected ? "→" : " ";
118
- const questionCount = session.sessionRequest.questions.length;
119
- const relativeTime = formatRelativeTime(session.sessionRequest.timestamp);
120
- return (React.createElement(Text, { key: session.sessionId, color: isSelected ? "cyan" : "white" },
121
- indicator,
122
- " Question Set ",
123
- idx + 1,
124
- " (",
125
- questionCount,
126
- " ",
127
- questionCount === 1 ? "question" : "questions",
128
- ") - ",
129
- relativeTime));
130
- }),
131
- React.createElement(Box, { marginTop: 1 }),
132
- React.createElement(Text, { dimColor: true }, "\u2191\u2193 Navigate | Enter Select | q Quit")));
133
- };
134
- /**
135
- * Format timestamp as relative time (e.g., "5m ago", "2h ago")
136
- */
137
- function formatRelativeTime(timestamp) {
138
- const now = Date.now();
139
- const then = new Date(timestamp).getTime();
140
- const diffMs = now - then;
141
- const diffMins = Math.floor(diffMs / 60000);
142
- if (diffMins < 1)
143
- return "just now";
144
- if (diffMins < 60)
145
- return `${diffMins}m ago`;
146
- const diffHours = Math.floor(diffMins / 60);
147
- if (diffHours < 24)
148
- return `${diffHours}h ago`;
149
- const diffDays = Math.floor(diffHours / 24);
150
- return `${diffDays}d ago`;
151
- }
@@ -1,368 +0,0 @@
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
- });