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,170 @@
1
+ /**
2
+ * Minimal schema validation tests for Question/Option types
3
+ * Tests the most common edge cases to catch obvious bugs
4
+ */
5
+ import { describe, expect, it } from "vitest";
6
+ import { z } from "zod";
7
+ // Import schemas from server (in real implementation, might extract to validation module)
8
+ const OptionSchema = z.object({
9
+ description: z.string().optional(),
10
+ label: z.string(),
11
+ });
12
+ const QuestionSchema = z.object({
13
+ options: z.array(OptionSchema).min(1),
14
+ prompt: z.string(),
15
+ title: z.string().min(1),
16
+ multiSelect: z.boolean().optional(),
17
+ });
18
+ const QuestionsArraySchema = z.array(QuestionSchema).min(1);
19
+ describe("Schema Validation - Edge Cases", () => {
20
+ describe("Invalid Input (should reject)", () => {
21
+ it("should reject missing title field", () => {
22
+ const invalidQuestion = {
23
+ // title missing
24
+ options: [{ label: "Option 1" }],
25
+ prompt: "Test question?",
26
+ };
27
+ expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
28
+ });
29
+ it("should reject empty options array", () => {
30
+ const invalidQuestion = {
31
+ options: [], // Empty array
32
+ prompt: "Test question?",
33
+ title: "Test",
34
+ };
35
+ expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
36
+ });
37
+ it("should reject missing prompt", () => {
38
+ const invalidQuestion = {
39
+ options: [{ label: "Option 1" }],
40
+ title: "Test",
41
+ // prompt missing
42
+ };
43
+ expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
44
+ });
45
+ it("should reject missing options field", () => {
46
+ const invalidQuestion = {
47
+ // options missing
48
+ prompt: "Test question?",
49
+ title: "Test",
50
+ };
51
+ expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
52
+ });
53
+ it("should reject option with missing label", () => {
54
+ const invalidQuestion = {
55
+ options: [
56
+ {
57
+ description: "A description",
58
+ // label missing
59
+ },
60
+ ],
61
+ prompt: "Test question?",
62
+ title: "Test",
63
+ };
64
+ expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
65
+ });
66
+ it("should reject empty questions array", () => {
67
+ const invalidQuestions = [];
68
+ expect(() => QuestionsArraySchema.parse(invalidQuestions)).toThrow();
69
+ });
70
+ });
71
+ describe("Valid Input (should accept)", () => {
72
+ it("should accept valid question with title", () => {
73
+ const validQuestion = {
74
+ options: [
75
+ {
76
+ description: "A helpful description",
77
+ label: "Option 1",
78
+ },
79
+ ],
80
+ prompt: "What is your choice?",
81
+ title: "Language",
82
+ };
83
+ expect(() => QuestionSchema.parse(validQuestion)).not.toThrow();
84
+ const parsed = QuestionSchema.parse(validQuestion);
85
+ expect(parsed.title).toBe("Language");
86
+ expect(parsed.prompt).toBe("What is your choice?");
87
+ expect(parsed.options).toHaveLength(1);
88
+ });
89
+ it("should accept valid question with all fields", () => {
90
+ const validQuestion = {
91
+ options: [
92
+ {
93
+ description: "A helpful description",
94
+ label: "Option 1",
95
+ },
96
+ ],
97
+ prompt: "What is your choice?",
98
+ title: "Framework",
99
+ };
100
+ expect(() => QuestionSchema.parse(validQuestion)).not.toThrow();
101
+ const parsed = QuestionSchema.parse(validQuestion);
102
+ expect(parsed.prompt).toBe("What is your choice?");
103
+ expect(parsed.options).toHaveLength(1);
104
+ });
105
+ it("should accept valid question with description omitted", () => {
106
+ const validQuestion = {
107
+ options: [
108
+ {
109
+ label: "Option 1",
110
+ // description omitted (optional)
111
+ },
112
+ ],
113
+ prompt: "What is your choice?",
114
+ title: "Choice",
115
+ };
116
+ expect(() => QuestionSchema.parse(validQuestion)).not.toThrow();
117
+ const parsed = QuestionSchema.parse(validQuestion);
118
+ expect(parsed.options[0].description).toBeUndefined();
119
+ });
120
+ it("should accept multiple valid questions", () => {
121
+ const validQuestions = [
122
+ {
123
+ options: [{ label: "A" }],
124
+ prompt: "Question 1?",
125
+ title: "First",
126
+ },
127
+ {
128
+ options: [{ label: "B" }, { label: "C" }],
129
+ prompt: "Question 2?",
130
+ title: "Second",
131
+ },
132
+ ];
133
+ expect(() => QuestionsArraySchema.parse(validQuestions)).not.toThrow();
134
+ const parsed = QuestionsArraySchema.parse(validQuestions);
135
+ expect(parsed).toHaveLength(2);
136
+ });
137
+ it("should accept question with multiSelect: true", () => {
138
+ const multiSelectQuestion = {
139
+ options: [{ label: "A" }, { label: "B" }, { label: "C" }],
140
+ prompt: "Select multiple options",
141
+ title: "Features",
142
+ multiSelect: true,
143
+ };
144
+ expect(() => QuestionSchema.parse(multiSelectQuestion)).not.toThrow();
145
+ const parsed = QuestionSchema.parse(multiSelectQuestion);
146
+ expect(parsed.multiSelect).toBe(true);
147
+ });
148
+ it("should accept question with multiSelect: false", () => {
149
+ const singleSelectQuestion = {
150
+ options: [{ label: "A" }, { label: "B" }],
151
+ prompt: "Select one option",
152
+ title: "Choice",
153
+ multiSelect: false,
154
+ };
155
+ expect(() => QuestionSchema.parse(singleSelectQuestion)).not.toThrow();
156
+ const parsed = QuestionSchema.parse(singleSelectQuestion);
157
+ expect(parsed.multiSelect).toBe(false);
158
+ });
159
+ it("should accept question with multiSelect omitted (defaults to undefined)", () => {
160
+ const defaultQuestion = {
161
+ options: [{ label: "A" }],
162
+ prompt: "Default single-select",
163
+ title: "Default",
164
+ };
165
+ expect(() => QuestionSchema.parse(defaultQuestion)).not.toThrow();
166
+ const parsed = QuestionSchema.parse(defaultQuestion);
167
+ expect(parsed.multiSelect).toBeUndefined();
168
+ });
169
+ });
170
+ });
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Integration tests for FastMCP server and SessionManager interaction
3
+ */
4
+ import { promises as fs } from "fs";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { SessionManager } from "../session/index.js";
7
+ describe("Server Integration", () => {
8
+ let sessionManager;
9
+ const testBaseDir = "/tmp/auq-test-integration";
10
+ beforeEach(async () => {
11
+ // Clean up test directory before each test
12
+ await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
13
+ sessionManager = new SessionManager({
14
+ baseDir: testBaseDir,
15
+ maxSessions: 10,
16
+ sessionTimeout: 5000, // 5 seconds for integration tests
17
+ });
18
+ await sessionManager.initialize();
19
+ });
20
+ afterEach(async () => {
21
+ // Clean up test directory after each test
22
+ await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
23
+ });
24
+ describe("Session Creation Integration", () => {
25
+ it("should create session when ask_user_questions tool is called", async () => {
26
+ const questions = [
27
+ {
28
+ options: [
29
+ { description: "Dynamic language", label: "JavaScript" },
30
+ { description: "Static typing", label: "TypeScript" },
31
+ ],
32
+ prompt: "Which programming language do you prefer?",
33
+ title: "Language",
34
+ },
35
+ ];
36
+ const sessionId = await sessionManager.createSession(questions);
37
+ expect(sessionId).toBeDefined();
38
+ expect(typeof sessionId).toBe("string");
39
+ // Verify session data integrity
40
+ const request = await sessionManager.getSessionRequest(sessionId);
41
+ const status = await sessionManager.getSessionStatus(sessionId);
42
+ expect(request?.sessionId).toBe(sessionId);
43
+ expect(request?.questions).toEqual(questions);
44
+ expect(request?.status).toBe("pending");
45
+ expect(status?.sessionId).toBe(sessionId);
46
+ expect(status?.status).toBe("pending");
47
+ expect(status?.totalQuestions).toBe(questions.length);
48
+ });
49
+ it("should handle multiple questions correctly", async () => {
50
+ const questions = [
51
+ {
52
+ options: [
53
+ { description: "Dynamic language", label: "JavaScript" },
54
+ { description: "Static typing", label: "TypeScript" },
55
+ ],
56
+ prompt: "Which programming language?",
57
+ title: "Language",
58
+ },
59
+ {
60
+ options: [
61
+ { description: "Web application", label: "Web" },
62
+ { description: "Command-line tool", label: "CLI" },
63
+ { description: "Desktop application", label: "Desktop" },
64
+ ],
65
+ prompt: "What type of application?",
66
+ title: "App Type",
67
+ },
68
+ ];
69
+ const sessionId = await sessionManager.createSession(questions);
70
+ const request = await sessionManager.getSessionRequest(sessionId);
71
+ const status = await sessionManager.getSessionStatus(sessionId);
72
+ expect(request?.questions).toHaveLength(2);
73
+ expect(request?.questions[0].prompt).toBe("Which programming language?");
74
+ expect(request?.questions[1].prompt).toBe("What type of application?");
75
+ expect(status?.totalQuestions).toBe(2);
76
+ });
77
+ it("should create unique sessions for multiple calls", async () => {
78
+ const questions = [
79
+ {
80
+ options: [{ label: "Test option" }],
81
+ prompt: "Test question",
82
+ title: "Test",
83
+ },
84
+ ];
85
+ const sessionId1 = await sessionManager.createSession(questions);
86
+ const sessionId2 = await sessionManager.createSession(questions);
87
+ expect(sessionId1).not.toBe(sessionId2);
88
+ // Verify both sessions exist and are independent
89
+ expect(await sessionManager.sessionExists(sessionId1)).toBe(true);
90
+ expect(await sessionManager.sessionExists(sessionId2)).toBe(true);
91
+ const request1 = await sessionManager.getSessionRequest(sessionId1);
92
+ const request2 = await sessionManager.getSessionRequest(sessionId2);
93
+ expect(request1?.sessionId).toBe(sessionId1);
94
+ expect(request2?.sessionId).toBe(sessionId2);
95
+ });
96
+ });
97
+ describe("Session Data Persistence", () => {
98
+ it("should persist session data across manager instances", async () => {
99
+ const questions = [
100
+ {
101
+ options: [{ description: "Test description", label: "Test option" }],
102
+ prompt: "Test question for persistence",
103
+ title: "Persistence",
104
+ },
105
+ ];
106
+ // Create session with first manager instance
107
+ const sessionId = await sessionManager.createSession(questions);
108
+ // Create new manager instance with same directory
109
+ const newManager = new SessionManager({ baseDir: testBaseDir });
110
+ await newManager.initialize();
111
+ // Verify session data is accessible through new manager
112
+ const request = await newManager.getSessionRequest(sessionId);
113
+ const status = await newManager.getSessionStatus(sessionId);
114
+ expect(request?.sessionId).toBe(sessionId);
115
+ expect(request?.questions).toEqual(questions);
116
+ expect(status?.sessionId).toBe(sessionId);
117
+ expect(status?.totalQuestions).toBe(1);
118
+ });
119
+ it("should store session files with correct structure", async () => {
120
+ const questions = [
121
+ {
122
+ options: [{ description: "Description", label: "Option" }],
123
+ prompt: "File structure test",
124
+ title: "Structure",
125
+ },
126
+ ];
127
+ const sessionId = await sessionManager.createSession(questions);
128
+ const sessionDir = `${testBaseDir}/${sessionId}`;
129
+ // Verify request.json structure
130
+ const requestContent = await fs.readFile(`${sessionDir}/request.json`, "utf-8");
131
+ const requestData = JSON.parse(requestContent);
132
+ expect(requestData).toHaveProperty("sessionId", sessionId);
133
+ expect(requestData).toHaveProperty("questions");
134
+ expect(requestData).toHaveProperty("timestamp");
135
+ expect(requestData).toHaveProperty("status", "pending");
136
+ expect(requestData.questions).toEqual(questions);
137
+ // Verify status.json structure
138
+ const statusContent = await fs.readFile(`${sessionDir}/status.json`, "utf-8");
139
+ const statusData = JSON.parse(statusContent);
140
+ expect(statusData).toHaveProperty("sessionId", sessionId);
141
+ expect(statusData).toHaveProperty("status", "pending");
142
+ expect(statusData).toHaveProperty("createdAt");
143
+ expect(statusData).toHaveProperty("lastModified");
144
+ expect(statusData).toHaveProperty("totalQuestions", 1);
145
+ });
146
+ });
147
+ describe("Error Handling Integration", () => {
148
+ it("should handle invalid session directory gracefully", async () => {
149
+ const invalidManager = new SessionManager({
150
+ baseDir: "/root/invalid/path",
151
+ });
152
+ await expect(invalidManager.initialize()).rejects.toThrow();
153
+ });
154
+ it("should handle concurrent session creation", async () => {
155
+ const questions = [
156
+ {
157
+ options: [{ label: "Option" }],
158
+ prompt: "Concurrent test",
159
+ title: "Concurrent",
160
+ },
161
+ ];
162
+ // Create multiple sessions concurrently
163
+ const sessionPromises = Array.from({ length: 5 }, () => sessionManager.createSession(questions));
164
+ const sessionIds = await Promise.all(sessionPromises);
165
+ // Verify all sessions were created with unique IDs
166
+ const uniqueIds = new Set(sessionIds);
167
+ expect(uniqueIds.size).toBe(5);
168
+ // Verify all sessions exist and are valid
169
+ for (const sessionId of sessionIds) {
170
+ expect(await sessionManager.sessionExists(sessionId)).toBe(true);
171
+ const validation = await sessionManager.validateSession(sessionId);
172
+ expect(validation.isValid).toBe(true);
173
+ }
174
+ });
175
+ });
176
+ describe("Session Lifecycle Integration", () => {
177
+ it("should support complete session lifecycle", async () => {
178
+ const questions = [
179
+ {
180
+ options: [{ label: "Option" }],
181
+ prompt: "Lifecycle test",
182
+ title: "Lifecycle",
183
+ },
184
+ ];
185
+ // Create session
186
+ const sessionId = await sessionManager.createSession(questions);
187
+ expect(await sessionManager.sessionExists(sessionId)).toBe(true);
188
+ // Update status to in-progress
189
+ await sessionManager.updateSessionStatus(sessionId, "in-progress", {
190
+ currentQuestionIndex: 0,
191
+ });
192
+ let status = await sessionManager.getSessionStatus(sessionId);
193
+ expect(status?.status).toBe("in-progress");
194
+ expect(status?.currentQuestionIndex).toBe(0);
195
+ // Save answers
196
+ const answers = {
197
+ answers: [
198
+ {
199
+ questionIndex: 0,
200
+ selectedOption: "Option",
201
+ timestamp: new Date().toISOString(),
202
+ },
203
+ ],
204
+ sessionId,
205
+ timestamp: new Date().toISOString(),
206
+ };
207
+ await sessionManager.saveSessionAnswers(sessionId, answers);
208
+ // Verify final state
209
+ status = await sessionManager.getSessionStatus(sessionId);
210
+ expect(status?.status).toBe("completed");
211
+ const savedAnswers = await sessionManager.getSessionAnswers(sessionId);
212
+ expect(savedAnswers?.answers).toHaveLength(1);
213
+ expect(savedAnswers?.answers[0].selectedOption).toBe("Option");
214
+ // Cleanup
215
+ await sessionManager.deleteSession(sessionId);
216
+ expect(await sessionManager.sessionExists(sessionId)).toBe(false);
217
+ });
218
+ it("should handle session validation correctly", async () => {
219
+ const questions = [
220
+ {
221
+ options: [{ label: "Option" }],
222
+ prompt: "Validation test",
223
+ title: "Validation",
224
+ },
225
+ ];
226
+ const sessionId = await sessionManager.createSession(questions);
227
+ const validation = await sessionManager.validateSession(sessionId);
228
+ expect(validation.isValid).toBe(true);
229
+ expect(validation.issues).toEqual([]);
230
+ // Corrupt the session by removing a required file
231
+ const sessionDir = `${testBaseDir}/${sessionId}`;
232
+ await fs.rm(`${sessionDir}/status.json`);
233
+ const validation2 = await sessionManager.validateSession(sessionId);
234
+ expect(validation2.isValid).toBe(false);
235
+ expect(validation2.issues).toContain("Required file missing: status.json");
236
+ });
237
+ });
238
+ describe("Performance Integration", () => {
239
+ it("should handle session creation under time limits", async () => {
240
+ const questions = [
241
+ {
242
+ options: [{ label: "Option" }],
243
+ prompt: "Performance test",
244
+ title: "Performance",
245
+ },
246
+ ];
247
+ const startTime = Date.now();
248
+ const sessionId = await sessionManager.createSession(questions);
249
+ const endTime = Date.now();
250
+ // Session creation should be fast (under 100ms)
251
+ expect(endTime - startTime).toBeLessThan(100);
252
+ expect(sessionId).toBeDefined();
253
+ });
254
+ it("should handle multiple sessions efficiently", async () => {
255
+ const questions = [
256
+ {
257
+ options: [{ label: "Option" }],
258
+ prompt: "Efficiency test",
259
+ title: "Efficiency",
260
+ },
261
+ ];
262
+ const startTime = Date.now();
263
+ const sessionIds = await Promise.all(Array.from({ length: 10 }, () => sessionManager.createSession(questions)));
264
+ const endTime = Date.now();
265
+ // Creating 10 sessions should be fast (under 500ms)
266
+ expect(endTime - startTime).toBeLessThan(500);
267
+ expect(sessionIds).toHaveLength(10);
268
+ // Verify all sessions were created correctly
269
+ for (const sessionId of sessionIds) {
270
+ expect(await sessionManager.sessionExists(sessionId)).toBe(true);
271
+ }
272
+ });
273
+ });
274
+ });
@@ -0,0 +1 @@
1
+ export const add = (a, b) => a + b;
@@ -0,0 +1,5 @@
1
+ import { expect, it } from "vitest";
2
+ import { add } from "./add.js";
3
+ it("should add two numbers", () => {
4
+ expect(add(1, 2)).toBe(3);
5
+ });
@@ -0,0 +1,163 @@
1
+ import { FastMCP } from "fastmcp";
2
+ import { randomUUID } from "crypto";
3
+ import { z } from "zod";
4
+ import { SessionManager } from "./session/index.js";
5
+ import { getSessionDirectory } from "./session/utils.js";
6
+ // Get session directory (auto-detects global vs local install)
7
+ const sessionDir = getSessionDirectory();
8
+ // Log session directory for debugging
9
+ console.error(`[AUQ] Session directory: ${sessionDir}`);
10
+ // Initialize session manager with detected session directory
11
+ const sessionManager = new SessionManager({ baseDir: sessionDir });
12
+ const server = new FastMCP({
13
+ name: "AskUserQuestions",
14
+ instructions: "This MCP server provides a tool to ask structured questions to the user. " +
15
+ "Use the ask_user_questions tool when you need to:\n" +
16
+ "- Gather user preferences or requirements during execution\n" +
17
+ "- Clarify ambiguous instructions or implementation choices\n" +
18
+ "- Get decisions on what direction to take\n" +
19
+ "- Offer choices to the user about multiple valid approaches\n\n" +
20
+ "The tool allows AI models to pause execution and gather direct user input through an interactive TUI, " +
21
+ "returning formatted responses for continued reasoning. " +
22
+ "Each question supports 2-4 multiple-choice options with descriptions, and users can always provide custom text input. " +
23
+ "Both single-select and multi-select modes are supported.",
24
+ version: "0.1.0",
25
+ });
26
+ // Define the question and option schemas
27
+ const OptionSchema = z.object({
28
+ label: z
29
+ .string()
30
+ .describe("The display text for this option that the user will see and select. " +
31
+ "Should be concise (1-5 words) and clearly describe the choice."),
32
+ description: z
33
+ .string()
34
+ .optional()
35
+ .describe("Explanation of what this option means or what will happen if chosen. " +
36
+ "Useful for providing context about trade-offs or implications."),
37
+ });
38
+ const QuestionSchema = z.object({
39
+ prompt: z
40
+ .string()
41
+ .describe("The complete question to ask the user. Should be clear, specific, and end with a question mark. " +
42
+ "Example: 'Which programming language do you want to use?' " +
43
+ "If multiSelect is true, phrase it accordingly, e.g. 'Which features do you want to enable?'"),
44
+ title: z
45
+ .string()
46
+ .min(1, "Question title is required. Provide a short summary like 'Language' or 'Framework'.")
47
+ .describe("Very short label displayed as a chip/tag (max 12 chars). " +
48
+ "Examples: 'Auth method', 'Library', 'Approach'. " +
49
+ "This title appears in the interface to help users quickly identify questions."),
50
+ options: z
51
+ .array(OptionSchema)
52
+ .min(2)
53
+ .max(4)
54
+ .describe("The available choices for this question. Must have 2-4 options. " +
55
+ "Each option should be a distinct, mutually exclusive choice (unless multiSelect is enabled). " +
56
+ "There should be no 'Other' option, that will be provided automatically."),
57
+ multiSelect: z
58
+ .boolean()
59
+ .describe("Set to true to allow the user to select multiple options instead of just one. " +
60
+ "Use when choices are not mutually exclusive. Default: false (single-select)"),
61
+ });
62
+ // Add the ask_user_questions tool
63
+ server.addTool({
64
+ name: "ask_user_questions",
65
+ annotations: {
66
+ title: "Ask User Questions",
67
+ openWorldHint: true, // This tool interacts with the user's terminal
68
+ readOnlyHint: false, // This tool waits for user input
69
+ idempotentHint: true,
70
+ },
71
+ description: "Use this tool when you need to ask the user questions during execution. " +
72
+ "This allows you to:\n" +
73
+ "1. Gather user preferences or requirements\n" +
74
+ "2. Clarify ambiguous instructions\n" +
75
+ "3. Get decisions on implementation choices as you work\n" +
76
+ "4. Offer choices to the user about what direction to take\n\n" +
77
+ "FEATURES:\n" +
78
+ "- Ask 1-4 structured questions via an interactive terminal interface\n" +
79
+ "- Each question includes 2-4 multiple-choice options with explanatory descriptions\n" +
80
+ "- Users can always provide custom free-text input as an alternative to predefined options\n" +
81
+ "- Single-select mode (default): User picks ONE option or provides custom text\n" +
82
+ "- Multi-select mode (multiSelect: true): User can select MULTIPLE options\n\n" +
83
+ "USAGE NOTES:\n" +
84
+ "- Always provide a descriptive 'title' field (max 12 chars) for each question\n" +
85
+ "- Use multiSelect: true when choices are not mutually exclusive\n" +
86
+ "- Option labels should be concise (1-5 words)\n" +
87
+ "- Questions should end with a question mark\n" +
88
+ "- Don't include an 'Other' option - it's provided automatically\n\n" +
89
+ "- If user seems not aware how to answer the question, inform them that they need to install the 'auq' cli tool.\n\n" +
90
+ "Returns a formatted summary of all questions and answers.",
91
+ execute: async (args, { log }) => {
92
+ try {
93
+ // Initialize session manager if not already done
94
+ await sessionManager.initialize();
95
+ // Clean up old sessions on startup (non-blocking)
96
+ sessionManager
97
+ .cleanupExpiredSessions()
98
+ .then((count) => {
99
+ if (count > 0) {
100
+ log.info(`Cleaned up ${count} expired session(s)`);
101
+ }
102
+ })
103
+ .catch((error) => {
104
+ log.warn("Cleanup failed:", { error: String(error) });
105
+ });
106
+ // Validate questions (using existing Zod schema validation)
107
+ if (!args.questions || args.questions.length === 0) {
108
+ throw new Error("At least one question is required");
109
+ }
110
+ // Convert Zod-validated questions to our internal Question type
111
+ const questions = args.questions.map((q) => ({
112
+ options: q.options.map((opt) => ({
113
+ description: opt.description,
114
+ label: opt.label,
115
+ })),
116
+ prompt: q.prompt,
117
+ title: q.title,
118
+ multiSelect: q.multiSelect,
119
+ }));
120
+ log.info("Starting session and waiting for user answers...", {
121
+ questionCount: questions.length,
122
+ });
123
+ // Start complete session lifecycle - this will wait for user answers
124
+ // Generate a per-tool-call ID and persist it with the session
125
+ const callId = randomUUID();
126
+ const { formattedResponse, sessionId } = await sessionManager.startSession(questions, callId);
127
+ log.info("Session completed successfully", { sessionId, callId });
128
+ // Return formatted response to AI model
129
+ return {
130
+ content: [
131
+ {
132
+ text: formattedResponse,
133
+ type: "text",
134
+ },
135
+ ],
136
+ };
137
+ }
138
+ catch (error) {
139
+ log.error("Session failed", { error: String(error) });
140
+ return {
141
+ content: [
142
+ {
143
+ text: `Error in session: ${error}`,
144
+ type: "text",
145
+ },
146
+ ],
147
+ };
148
+ }
149
+ },
150
+ parameters: z.object({
151
+ questions: z
152
+ .array(QuestionSchema)
153
+ .min(1)
154
+ .max(4)
155
+ .describe("Questions to ask the user (1-4 questions). " +
156
+ "Each question must include: prompt (full question text), title (short label, max 12 chars), " +
157
+ "options (2-4 choices with labels and descriptions), and multiSelect (boolean)."),
158
+ }),
159
+ });
160
+ // Start the server with stdio transport
161
+ server.start({
162
+ transportType: "stdio",
163
+ });