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,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for Response Formatter
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { ResponseFormatter } from "../ResponseFormatter.js";
|
|
6
|
+
describe("ResponseFormatter", () => {
|
|
7
|
+
const mockSessionRequest = {
|
|
8
|
+
sessionId: "test-session-123",
|
|
9
|
+
status: "pending",
|
|
10
|
+
timestamp: "2025-01-01T12:00:00.000Z",
|
|
11
|
+
questions: [
|
|
12
|
+
{
|
|
13
|
+
prompt: "What is your favorite programming language?",
|
|
14
|
+
options: [
|
|
15
|
+
{ label: "JavaScript", description: "Dynamic web language" },
|
|
16
|
+
{ label: "TypeScript", description: "Typed JavaScript" },
|
|
17
|
+
{ label: "Python", description: "Versatile and readable" },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
prompt: "What type of application are you building?",
|
|
22
|
+
options: [
|
|
23
|
+
{ label: "Web", description: "Frontend or backend web application" },
|
|
24
|
+
{ label: "CLI", description: "Command-line tool" },
|
|
25
|
+
{ label: "Mobile", description: "Mobile application" },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
prompt: "Any additional requirements?",
|
|
30
|
+
options: [
|
|
31
|
+
{ label: "No additional requirements", description: "Standard setup only" },
|
|
32
|
+
{ label: "Security focus", description: "High security requirements" },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
const mockSessionAnswer = {
|
|
38
|
+
sessionId: "test-session-123",
|
|
39
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
40
|
+
answers: [
|
|
41
|
+
{
|
|
42
|
+
questionIndex: 0,
|
|
43
|
+
selectedOption: "TypeScript",
|
|
44
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
questionIndex: 1,
|
|
48
|
+
customText: "Desktop app with Electron",
|
|
49
|
+
timestamp: "2025-01-01T12:02:00.000Z",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
questionIndex: 2,
|
|
53
|
+
selectedOption: "Security focus",
|
|
54
|
+
timestamp: "2025-01-01T12:03:00.000Z",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
describe("formatResponse", () => {
|
|
59
|
+
it("should format a complete response with all question types", () => {
|
|
60
|
+
const result = ResponseFormatter.formatResponse(mockSessionRequest, mockSessionAnswer);
|
|
61
|
+
expect(result.formatted_response).toContain("Here are the user's answers:");
|
|
62
|
+
expect(result.formatted_response).toContain("1. What is your favorite programming language?");
|
|
63
|
+
expect(result.formatted_response).toContain("→ TypeScript — Typed JavaScript");
|
|
64
|
+
expect(result.formatted_response).toContain("2. What type of application are you building?");
|
|
65
|
+
expect(result.formatted_response).toContain("→ Other: 'Desktop app with Electron'");
|
|
66
|
+
expect(result.formatted_response).toContain("3. Any additional requirements?");
|
|
67
|
+
expect(result.formatted_response).toContain("→ Security focus — High security requirements");
|
|
68
|
+
expect(result.metadata).toEqual({
|
|
69
|
+
totalQuestions: 3,
|
|
70
|
+
answeredQuestions: 3,
|
|
71
|
+
sessionDuration: 5 * 60 * 1000, // 5 minutes
|
|
72
|
+
hasCustomAnswers: true,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
it("should handle responses with only selected options", () => {
|
|
76
|
+
const answerOnlyOptions = {
|
|
77
|
+
sessionId: "test-session-123",
|
|
78
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
79
|
+
answers: [
|
|
80
|
+
{
|
|
81
|
+
questionIndex: 0,
|
|
82
|
+
selectedOption: "Python",
|
|
83
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
const result = ResponseFormatter.formatResponse(mockSessionRequest, answerOnlyOptions);
|
|
88
|
+
expect(result.formatted_response).toContain("→ Python — Versatile and readable");
|
|
89
|
+
expect(result.formatted_response).not.toContain("Other:");
|
|
90
|
+
expect(result.metadata.hasCustomAnswers).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
it("should handle responses with only custom text", () => {
|
|
93
|
+
const answerOnlyCustom = {
|
|
94
|
+
sessionId: "test-session-123",
|
|
95
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
96
|
+
answers: [
|
|
97
|
+
{
|
|
98
|
+
questionIndex: 0,
|
|
99
|
+
customText: "Rust for performance and safety",
|
|
100
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
const result = ResponseFormatter.formatResponse(mockSessionRequest, answerOnlyCustom);
|
|
105
|
+
expect(result.formatted_response).toContain("→ Other: 'Rust for performance and safety'");
|
|
106
|
+
expect(result.metadata.hasCustomAnswers).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
it("should handle options without descriptions", () => {
|
|
109
|
+
const questionWithoutDescription = {
|
|
110
|
+
...mockSessionRequest,
|
|
111
|
+
questions: [
|
|
112
|
+
{
|
|
113
|
+
prompt: "Simple question?",
|
|
114
|
+
options: [{ label: "Option 1" }, { label: "Option 2" }],
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
const simpleAnswer = {
|
|
119
|
+
sessionId: "test-session-123",
|
|
120
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
121
|
+
answers: [
|
|
122
|
+
{
|
|
123
|
+
questionIndex: 0,
|
|
124
|
+
selectedOption: "Option 1",
|
|
125
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
const result = ResponseFormatter.formatResponse(questionWithoutDescription, simpleAnswer);
|
|
130
|
+
expect(result.formatted_response).toContain("→ Option 1");
|
|
131
|
+
expect(result.formatted_response).not.toContain("—");
|
|
132
|
+
});
|
|
133
|
+
it("should handle unanswered questions", () => {
|
|
134
|
+
const incompleteAnswer = {
|
|
135
|
+
sessionId: "test-session-123",
|
|
136
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
137
|
+
answers: [
|
|
138
|
+
{
|
|
139
|
+
questionIndex: 0,
|
|
140
|
+
selectedOption: "JavaScript",
|
|
141
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
142
|
+
},
|
|
143
|
+
// Missing answers for questions 1 and 2
|
|
144
|
+
],
|
|
145
|
+
};
|
|
146
|
+
const result = ResponseFormatter.formatResponse(mockSessionRequest, incompleteAnswer);
|
|
147
|
+
expect(result.formatted_response).toContain("→ JavaScript — Dynamic web language");
|
|
148
|
+
expect(result.formatted_response).toContain("2. What type of application are you building?");
|
|
149
|
+
expect(result.formatted_response).toContain("→ [No answer provided]");
|
|
150
|
+
expect(result.formatted_response).toContain("3. Any additional requirements?");
|
|
151
|
+
expect(result.formatted_response).toContain("→ [No answer provided]");
|
|
152
|
+
expect(result.metadata.answeredQuestions).toBe(1);
|
|
153
|
+
expect(result.metadata.totalQuestions).toBe(3);
|
|
154
|
+
});
|
|
155
|
+
it("should handle custom formatting options", () => {
|
|
156
|
+
const result = ResponseFormatter.formatResponse(mockSessionRequest, mockSessionAnswer, {
|
|
157
|
+
includePreamble: false,
|
|
158
|
+
includeTimestamps: true,
|
|
159
|
+
maxLineLength: 50,
|
|
160
|
+
});
|
|
161
|
+
expect(result.formatted_response).not.toContain("Here are the user's answers:");
|
|
162
|
+
expect(result.formatted_response).toContain("(Answered:");
|
|
163
|
+
// Note: Text wrapping might work differently than expected
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe("validateAnswers", () => {
|
|
167
|
+
it("should validate complete and correct answers", () => {
|
|
168
|
+
const validation = ResponseFormatter.validateAnswers(mockSessionRequest, mockSessionAnswer);
|
|
169
|
+
expect(validation.isValid).toBe(true);
|
|
170
|
+
expect(validation.issues).toHaveLength(0);
|
|
171
|
+
expect(validation.warnings).toHaveLength(0);
|
|
172
|
+
});
|
|
173
|
+
it("should detect missing answers", () => {
|
|
174
|
+
const incompleteAnswer = {
|
|
175
|
+
sessionId: "test-session-123",
|
|
176
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
177
|
+
answers: [
|
|
178
|
+
{
|
|
179
|
+
questionIndex: 0,
|
|
180
|
+
selectedOption: "JavaScript",
|
|
181
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
182
|
+
},
|
|
183
|
+
// Missing answers for questions 1 and 2
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
const validation = ResponseFormatter.validateAnswers(mockSessionRequest, incompleteAnswer);
|
|
187
|
+
expect(validation.isValid).toBe(false);
|
|
188
|
+
expect(validation.issues).toHaveLength(1);
|
|
189
|
+
expect(validation.issues[0]).toContain("2 question(s) were not answered");
|
|
190
|
+
});
|
|
191
|
+
it("should detect duplicate answers", () => {
|
|
192
|
+
const duplicateAnswer = {
|
|
193
|
+
sessionId: "test-session-123",
|
|
194
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
195
|
+
answers: [
|
|
196
|
+
{
|
|
197
|
+
questionIndex: 0,
|
|
198
|
+
selectedOption: "JavaScript",
|
|
199
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
questionIndex: 0,
|
|
203
|
+
selectedOption: "Python",
|
|
204
|
+
timestamp: "2025-01-01T12:02:00.000Z",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
const validation = ResponseFormatter.validateAnswers(mockSessionRequest, duplicateAnswer);
|
|
209
|
+
expect(validation.isValid).toBe(false);
|
|
210
|
+
expect(validation.issues).toContain("Duplicate answers detected for some questions");
|
|
211
|
+
});
|
|
212
|
+
it("should detect invalid question indices", () => {
|
|
213
|
+
const invalidIndexAnswer = {
|
|
214
|
+
sessionId: "test-session-123",
|
|
215
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
216
|
+
answers: [
|
|
217
|
+
{
|
|
218
|
+
questionIndex: 5, // Invalid index (only 0-2 exist)
|
|
219
|
+
selectedOption: "JavaScript",
|
|
220
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
const validation = ResponseFormatter.validateAnswers(mockSessionRequest, invalidIndexAnswer);
|
|
225
|
+
expect(validation.isValid).toBe(false);
|
|
226
|
+
expect(validation.issues.length).toBeGreaterThan(0);
|
|
227
|
+
});
|
|
228
|
+
it("should detect invalid selected options", () => {
|
|
229
|
+
// Provide answers for all questions to avoid missing answer detection
|
|
230
|
+
const invalidOptionAnswer = {
|
|
231
|
+
sessionId: "test-session-123",
|
|
232
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
233
|
+
answers: [
|
|
234
|
+
{
|
|
235
|
+
questionIndex: 0,
|
|
236
|
+
selectedOption: "NonExistentOption", // Not in question options
|
|
237
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
questionIndex: 1,
|
|
241
|
+
selectedOption: "Web",
|
|
242
|
+
timestamp: "2025-01-01T12:02:00.000Z",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
questionIndex: 2,
|
|
246
|
+
selectedOption: "No additional requirements",
|
|
247
|
+
timestamp: "2025-01-01T12:03:00.000Z",
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
const validation = ResponseFormatter.validateAnswers(mockSessionRequest, invalidOptionAnswer);
|
|
252
|
+
expect(validation.isValid).toBe(false);
|
|
253
|
+
expect(validation.issues).toContain("Answer 1 references non-existent option: NonExistentOption");
|
|
254
|
+
});
|
|
255
|
+
it("should detect empty answers", () => {
|
|
256
|
+
// Provide answers for all questions but one is empty
|
|
257
|
+
const emptyAnswer = {
|
|
258
|
+
sessionId: "test-session-123",
|
|
259
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
260
|
+
answers: [
|
|
261
|
+
{
|
|
262
|
+
questionIndex: 0,
|
|
263
|
+
// No selected option or custom text
|
|
264
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
questionIndex: 1,
|
|
268
|
+
selectedOption: "Web",
|
|
269
|
+
timestamp: "2025-01-01T12:02:00.000Z",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
questionIndex: 2,
|
|
273
|
+
selectedOption: "No additional requirements",
|
|
274
|
+
timestamp: "2025-01-01T12:03:00.000Z",
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
const validation = ResponseFormatter.validateAnswers(mockSessionRequest, emptyAnswer);
|
|
279
|
+
expect(validation.isValid).toBe(false);
|
|
280
|
+
expect(validation.issues).toContain("Answer 1 has no selected option or custom text");
|
|
281
|
+
});
|
|
282
|
+
it("should warn about very long custom answers", () => {
|
|
283
|
+
// Provide answers for all questions to avoid missing answer detection
|
|
284
|
+
const longAnswer = {
|
|
285
|
+
sessionId: "test-session-123",
|
|
286
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
287
|
+
answers: [
|
|
288
|
+
{
|
|
289
|
+
questionIndex: 0,
|
|
290
|
+
customText: "x".repeat(1500), // Very long answer
|
|
291
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
questionIndex: 1,
|
|
295
|
+
selectedOption: "Web",
|
|
296
|
+
timestamp: "2025-01-01T12:02:00.000Z",
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
questionIndex: 2,
|
|
300
|
+
selectedOption: "No additional requirements",
|
|
301
|
+
timestamp: "2025-01-01T12:03:00.000Z",
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
};
|
|
305
|
+
const validation = ResponseFormatter.validateAnswers(mockSessionRequest, longAnswer);
|
|
306
|
+
expect(validation.isValid).toBe(true);
|
|
307
|
+
expect(validation.warnings.length).toBeGreaterThanOrEqual(1);
|
|
308
|
+
const longTextWarning = validation.warnings.find(w => w.includes("very long custom text"));
|
|
309
|
+
expect(longTextWarning).toBeTruthy();
|
|
310
|
+
expect(longTextWarning).toContain("1500 characters");
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
describe("createSessionSummary", () => {
|
|
314
|
+
it("should create a comprehensive session summary", () => {
|
|
315
|
+
const summary = ResponseFormatter.createSessionSummary(mockSessionRequest, mockSessionAnswer);
|
|
316
|
+
expect(summary).toContain("Session Summary:");
|
|
317
|
+
expect(summary).toContain("Session ID: test-session-123");
|
|
318
|
+
expect(summary).toContain("Total Questions: 3");
|
|
319
|
+
expect(summary).toContain("Answered Questions: 3");
|
|
320
|
+
expect(summary).toContain("Duration: 300s"); // 5 minutes
|
|
321
|
+
expect(summary).toContain("Custom Answers: 1");
|
|
322
|
+
expect(summary).toContain("Status: pending");
|
|
323
|
+
});
|
|
324
|
+
it("should handle empty questions and answers", () => {
|
|
325
|
+
const emptySession = {
|
|
326
|
+
sessionId: "empty-session",
|
|
327
|
+
status: "completed",
|
|
328
|
+
timestamp: "2025-01-01T12:00:00.000Z",
|
|
329
|
+
questions: [],
|
|
330
|
+
};
|
|
331
|
+
const emptyAnswer = {
|
|
332
|
+
sessionId: "empty-session",
|
|
333
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
334
|
+
answers: [],
|
|
335
|
+
};
|
|
336
|
+
const summary = ResponseFormatter.createSessionSummary(emptySession, emptyAnswer);
|
|
337
|
+
expect(summary).toContain("Total Questions: 0");
|
|
338
|
+
expect(summary).toContain("Answered Questions: 0");
|
|
339
|
+
expect(summary).toContain("Custom Answers: 0");
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
describe("Edge Cases", () => {
|
|
343
|
+
it("should handle empty custom text", () => {
|
|
344
|
+
const emptyCustomAnswer = {
|
|
345
|
+
sessionId: "test-session-123",
|
|
346
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
347
|
+
answers: [
|
|
348
|
+
{
|
|
349
|
+
questionIndex: 0,
|
|
350
|
+
customText: " ", // Only whitespace
|
|
351
|
+
timestamp: "2025-01-01T12:01:00.000Z",
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
const result = ResponseFormatter.formatResponse(mockSessionRequest, emptyCustomAnswer);
|
|
356
|
+
expect(result.formatted_response).toContain("→ [No answer provided]");
|
|
357
|
+
});
|
|
358
|
+
it("should handle malformed session data gracefully", () => {
|
|
359
|
+
const malformedRequest = {
|
|
360
|
+
sessionId: "test-session-123",
|
|
361
|
+
status: "pending",
|
|
362
|
+
timestamp: "2025-01-01T12:00:00.000Z",
|
|
363
|
+
questions: [
|
|
364
|
+
{
|
|
365
|
+
prompt: "Test question",
|
|
366
|
+
options: [],
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
};
|
|
370
|
+
const malformedAnswer = {
|
|
371
|
+
sessionId: "test-session-123",
|
|
372
|
+
timestamp: "2025-01-01T12:05:00.000Z",
|
|
373
|
+
answers: [],
|
|
374
|
+
};
|
|
375
|
+
const validation = ResponseFormatter.validateAnswers(malformedRequest, malformedAnswer);
|
|
376
|
+
expect(validation.isValid).toBe(false);
|
|
377
|
+
expect(validation.issues.length).toBeGreaterThan(0);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
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": "https://github.com/paulp-o/ask-user-question-mcp"
|
|
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
|
+
"@types/uuid": "^10.0.0",
|
|
42
|
+
"fastmcp": "^1.27.3",
|
|
43
|
+
"ink": "^6.4.0",
|
|
44
|
+
"ink-text-input": "^6.0.0",
|
|
45
|
+
"react": "^19.2.0",
|
|
46
|
+
"uuid": "^13.0.0",
|
|
47
|
+
"zod": "^3.24.4"
|
|
48
|
+
},
|
|
49
|
+
"release": {
|
|
50
|
+
"branches": [
|
|
51
|
+
"main"
|
|
52
|
+
],
|
|
53
|
+
"plugins": [
|
|
54
|
+
"@semantic-release/commit-analyzer",
|
|
55
|
+
"@semantic-release/release-notes-generator",
|
|
56
|
+
"@semantic-release/npm",
|
|
57
|
+
"@semantic-release/github"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@eslint/js": "^9.26.0",
|
|
62
|
+
"@tsconfig/node22": "^22.0.1",
|
|
63
|
+
"@types/react": "^19.2.2",
|
|
64
|
+
"eslint-config-prettier": "^10.1.3",
|
|
65
|
+
"eslint-plugin-perfectionist": "^4.12.3",
|
|
66
|
+
"jiti": "^2.4.2",
|
|
67
|
+
"prettier": "^3.5.3",
|
|
68
|
+
"semantic-release": "^24.2.3",
|
|
69
|
+
"tsx": "^4.19.4",
|
|
70
|
+
"typescript": "^5.8.3",
|
|
71
|
+
"typescript-eslint": "^8.32.0",
|
|
72
|
+
"vitest": "^3.1.3"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { FastMCP } from "fastmcp";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { SessionManager } from "./session/index.js";
|
|
4
|
+
// import { resolveSessionDirectory } from "./session/utils.js";
|
|
5
|
+
// Initialize session manager
|
|
6
|
+
const sessionManager = new SessionManager();
|
|
7
|
+
const server = new FastMCP({
|
|
8
|
+
instructions: "This MCP server provides a tool to ask users structured questions via the terminal. " +
|
|
9
|
+
"The ask_user_questions tool allows AI models to pause and gather direct user input through " +
|
|
10
|
+
"an interactive TUI, returning formatted responses for continued reasoning.",
|
|
11
|
+
name: "AskUserQuery",
|
|
12
|
+
version: "0.1.0",
|
|
13
|
+
});
|
|
14
|
+
// Define the question and option schemas
|
|
15
|
+
const OptionSchema = z.object({
|
|
16
|
+
description: z
|
|
17
|
+
.string()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Optional explanatory note for this option"),
|
|
20
|
+
label: z.string().describe("The visible text of the choice"),
|
|
21
|
+
});
|
|
22
|
+
const QuestionSchema = z.object({
|
|
23
|
+
title: z.string().describe("Short 1-2 word summary for UI display"),
|
|
24
|
+
options: z
|
|
25
|
+
.array(OptionSchema)
|
|
26
|
+
.min(1)
|
|
27
|
+
.describe("Non-empty list of predefined answer choices"),
|
|
28
|
+
prompt: z.string().describe("The full question text"),
|
|
29
|
+
});
|
|
30
|
+
// Add the ask_user_questions tool
|
|
31
|
+
server.addTool({
|
|
32
|
+
annotations: {
|
|
33
|
+
openWorldHint: true, // This tool interacts with the user's terminal
|
|
34
|
+
readOnlyHint: false, // This tool waits for user input
|
|
35
|
+
title: "Ask User Questions",
|
|
36
|
+
},
|
|
37
|
+
description: "Ask the user one or more structured questions via an interactive terminal interface. " +
|
|
38
|
+
"Each question includes multiple-choice options and allows custom free-text responses. " +
|
|
39
|
+
"Returns a formatted summary of all questions and answers.",
|
|
40
|
+
execute: async (args, { log }) => {
|
|
41
|
+
try {
|
|
42
|
+
// Initialize session manager if not already done
|
|
43
|
+
await sessionManager.initialize();
|
|
44
|
+
// Clean up old sessions on startup (non-blocking)
|
|
45
|
+
sessionManager
|
|
46
|
+
.cleanupExpiredSessions()
|
|
47
|
+
.then((count) => {
|
|
48
|
+
if (count > 0) {
|
|
49
|
+
log.info(`Cleaned up ${count} expired session(s)`);
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.catch((error) => {
|
|
53
|
+
log.warn("Cleanup failed:", { error: String(error) });
|
|
54
|
+
});
|
|
55
|
+
// Validate questions (using existing Zod schema validation)
|
|
56
|
+
if (!args.questions || args.questions.length === 0) {
|
|
57
|
+
throw new Error("At least one question is required");
|
|
58
|
+
}
|
|
59
|
+
// Convert Zod-validated questions to our internal Question type
|
|
60
|
+
const questions = args.questions.map((q) => ({
|
|
61
|
+
title: q.title,
|
|
62
|
+
options: q.options.map((opt) => ({
|
|
63
|
+
description: opt.description,
|
|
64
|
+
label: opt.label,
|
|
65
|
+
})),
|
|
66
|
+
prompt: q.prompt,
|
|
67
|
+
}));
|
|
68
|
+
log.info("Starting session and waiting for user answers...", {
|
|
69
|
+
questionCount: questions.length,
|
|
70
|
+
});
|
|
71
|
+
// Start complete session lifecycle - this will wait for user answers
|
|
72
|
+
const { formattedResponse, sessionId } = await sessionManager.startSession(questions);
|
|
73
|
+
log.info("Session completed successfully", { sessionId });
|
|
74
|
+
// Return formatted response to AI model
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
text: formattedResponse,
|
|
79
|
+
type: "text",
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
log.error("Session failed", { error: String(error) });
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
text: `Error in session: ${error}`,
|
|
90
|
+
type: "text",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
name: "ask_user_questions",
|
|
97
|
+
parameters: z.object({
|
|
98
|
+
questions: z
|
|
99
|
+
.array(QuestionSchema)
|
|
100
|
+
.min(1)
|
|
101
|
+
.describe("Array of questions to ask the user"),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
// Start the server with stdio transport
|
|
105
|
+
server.start({
|
|
106
|
+
transportType: "stdio",
|
|
107
|
+
});
|