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,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
+ });