auq-mcp-server 0.1.10 → 1.2.5

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 (42) hide show
  1. package/README.md +24 -0
  2. package/dist/bin/auq.js +138 -7
  3. package/dist/package.json +17 -6
  4. package/dist/src/__tests__/schema-validation.test.js +34 -26
  5. package/dist/src/core/ask-user-questions.js +35 -0
  6. package/dist/src/server.js +11 -74
  7. package/dist/src/shared/schemas.js +64 -0
  8. package/dist/src/tui/components/Header.js +9 -1
  9. package/dist/src/tui/components/MultiLineTextInput.js +53 -14
  10. package/dist/src/tui/components/OptionsList.js +8 -0
  11. package/dist/src/tui/components/QuestionDisplay.js +10 -6
  12. package/dist/src/tui/components/ReviewScreen.js +6 -2
  13. package/dist/src/tui/components/StepperView.js +25 -3
  14. package/dist/src/tui/components/WaitingScreen.js +31 -4
  15. package/package.json +10 -4
  16. package/dist/__tests__/schema-validation.test.js +0 -137
  17. package/dist/__tests__/server.integration.test.js +0 -263
  18. package/dist/add.js +0 -1
  19. package/dist/add.test.js +0 -5
  20. package/dist/bin/test-session-menu.js +0 -28
  21. package/dist/bin/test-tabbar.js +0 -42
  22. package/dist/file-utils.js +0 -59
  23. package/dist/format/ResponseFormatter.js +0 -206
  24. package/dist/format/__tests__/ResponseFormatter.test.js +0 -380
  25. package/dist/server.js +0 -107
  26. package/dist/session/ResponseFormatter.js +0 -130
  27. package/dist/session/SessionManager.js +0 -474
  28. package/dist/session/__tests__/ResponseFormatter.test.js +0 -417
  29. package/dist/session/__tests__/SessionManager.test.js +0 -553
  30. package/dist/session/__tests__/atomic-operations.test.js +0 -345
  31. package/dist/session/__tests__/file-watcher.test.js +0 -311
  32. package/dist/session/__tests__/workflow.integration.test.js +0 -334
  33. package/dist/session/atomic-operations.js +0 -307
  34. package/dist/session/file-watcher.js +0 -218
  35. package/dist/session/index.js +0 -7
  36. package/dist/session/types.js +0 -20
  37. package/dist/session/utils.js +0 -125
  38. package/dist/session-manager.js +0 -171
  39. package/dist/session-watcher.js +0 -110
  40. package/dist/src/tui/components/SessionSelectionMenu.js +0 -151
  41. package/dist/tui/__tests__/session-watcher.test.js +0 -368
  42. package/dist/tui/session-watcher.js +0 -183
package/README.md CHANGED
@@ -144,6 +144,7 @@ args = ["-y", "auq-mcp-server", "server"]
144
144
  # Optional: Whitelist additional env vars
145
145
  # env_vars = ["AUQ_SESSION_DIR"]
146
146
 
147
+
147
148
  # Optional: Working directory
148
149
  # cwd = "/Users/<user>/projects"
149
150
  ```
@@ -169,6 +170,29 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
169
170
 
170
171
  ---
171
172
 
173
+ ## 🔌 OpenCode Plugin (Optional)
174
+
175
+ If you want the OpenCode tool to call `auq ask` directly (without MCP), install
176
+ the plugin package and add it to your OpenCode config.
177
+
178
+ ```bash
179
+ npm install -g auq-mcp-server
180
+ npm install -g @paulp-o/opencode-auq
181
+ ```
182
+
183
+ Add to `opencode.json`:
184
+
185
+ ```json
186
+ {
187
+ "$schema": "https://opencode.ai/config.json",
188
+ "plugin": ["@paulp-o/opencode-auq"]
189
+ }
190
+ ```
191
+
192
+ The plugin assumes `auq` is available on `PATH` (global install or equivalent).
193
+
194
+ ---
195
+
172
196
  ## 💻 Usage
173
197
 
174
198
  ### Starting the CLI tool
package/dist/bin/auq.js CHANGED
@@ -15,10 +15,9 @@ import { createTUIWatcher } from "../src/tui/session-watcher.js";
15
15
  // Handle command-line arguments
16
16
  const args = process.argv.slice(2);
17
17
  const command = args[0];
18
- // Display help
19
18
  if (command === "--help" || command === "-h") {
20
19
  console.log(`
21
- AUQ MCP Server - Ask User Questions
20
+ AUQ - Ask User Questions
22
21
 
23
22
  Usage:
24
23
  auq [command] [options]
@@ -26,15 +25,42 @@ Usage:
26
25
  Commands:
27
26
  (default) Start the TUI (Terminal User Interface)
28
27
  server Start the MCP server (for use with MCP clients)
28
+ ask <json> Ask questions via CLI (alternative to MCP)
29
29
 
30
30
  Options:
31
31
  --help, -h Show this help message
32
32
  --version, -v Show version information
33
33
 
34
+ ASK COMMAND:
35
+ Use 'auq ask' when you need to ask the user questions during execution.
36
+ This allows you to:
37
+ 1. Gather user preferences or requirements
38
+ 2. Clarify ambiguous instructions
39
+ 3. Get decisions on implementation choices as you work
40
+ 4. Offer choices to the user about what direction to take
41
+
42
+ FEATURES:
43
+ - Ask 1-4 structured questions via an interactive terminal interface
44
+ - Each question includes 2-4 multiple-choice options with explanatory descriptions
45
+ - Users can always provide custom free-text input as an alternative to predefined options
46
+ - Single-select mode (default): User picks ONE option or provides custom text
47
+ - Multi-select mode (multiSelect: true): User can select MULTIPLE options
48
+
49
+ USAGE NOTES:
50
+ - Always provide a descriptive 'title' field (max 12 chars) for each question
51
+ - Use multiSelect: true when choices are not mutually exclusive
52
+ - Option labels should be concise (1-5 words)
53
+ - Questions should end with a question mark
54
+ - Don't include an 'Other' option - it's provided automatically
55
+ - Mark one option as recommended.
56
+
57
+ Returns a formatted summary of all questions and answers.
58
+
34
59
  Examples:
35
60
  auq # Start TUI (wait for questions from AI)
36
61
  auq server # Start MCP server (for Claude Desktop, etc.)
37
- auq --help # Show this help message
62
+ auq ask '{"questions": [{"prompt": "Which language?", "title": "Lang", "options": [{"label": "TypeScript (recommended)"}, {"label": "Python"}], "multiSelect": false}]}'
63
+ echo '{"questions": [...]}' | auq ask # Pipe JSON to ask command
38
64
 
39
65
  For more information, visit:
40
66
  https://github.com/paulp-o/ask-user-questions-mcp
@@ -43,12 +69,30 @@ For more information, visit:
43
69
  }
44
70
  // Display version
45
71
  if (command === "--version" || command === "-v") {
46
- // Read version from package.json
72
+ // Read version from package.json (handle both local dev and global install)
47
73
  const __filename = fileURLToPath(import.meta.url);
48
74
  const __dirname = dirname(__filename);
49
- const packageJsonPath = join(__dirname, "..", "package.json");
50
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
51
- console.log(`auq-mcp-server v${packageJson.version}`);
75
+ // Try different possible paths for package.json
76
+ const possiblePaths = [
77
+ join(__dirname, "..", "package.json"), // dist/../package.json (local dev)
78
+ join(__dirname, "..", "..", "package.json"), // dist/bin/../../package.json (global install)
79
+ ];
80
+ let packageJson = null;
81
+ for (const path of possiblePaths) {
82
+ try {
83
+ packageJson = JSON.parse(readFileSync(path, "utf-8"));
84
+ break;
85
+ }
86
+ catch {
87
+ // Try next path
88
+ }
89
+ }
90
+ if (packageJson) {
91
+ console.log(`auq-mcp-server v${packageJson.version}`);
92
+ }
93
+ else {
94
+ console.log("auq-mcp-server v0.1.17"); // Fallback version
95
+ }
52
96
  process.exit(0);
53
97
  }
54
98
  // Handle 'server' command
@@ -60,6 +104,82 @@ if (command === "server") {
60
104
  // Keep process alive
61
105
  await new Promise(() => { });
62
106
  }
107
+ // Handle 'ask' command
108
+ if (command === "ask") {
109
+ const { SessionManager } = await import("../src/session/index.js");
110
+ const { randomUUID } = await import("crypto");
111
+ const sessionDir = getSessionDirectory();
112
+ const sessionManager = new SessionManager({ baseDir: sessionDir });
113
+ await sessionManager.initialize();
114
+ let questionsJson = args[1];
115
+ if (!questionsJson) {
116
+ const chunks = [];
117
+ const stdin = process.stdin;
118
+ stdin.setEncoding("utf8");
119
+ // Only read from stdin if it's piped (not interactive TTY)
120
+ if (!stdin.isTTY) {
121
+ for await (const chunk of stdin) {
122
+ chunks.push(Buffer.from(chunk));
123
+ }
124
+ questionsJson = Buffer.concat(chunks).toString("utf8").trim();
125
+ }
126
+ }
127
+ if (!questionsJson) {
128
+ console.error("Error: Questions JSON required. Provide as argument or pipe to stdin.");
129
+ console.error("");
130
+ console.error("Usage:");
131
+ console.error(' auq ask \'{"questions": [{"prompt": "...", "title": "...", "options": [...], "multiSelect": false}]}\'');
132
+ console.error("");
133
+ console.error("Or pipe JSON:");
134
+ console.error(" echo '{...}' | auq ask");
135
+ process.exit(1);
136
+ }
137
+ try {
138
+ const input = JSON.parse(questionsJson);
139
+ const questions = input.questions;
140
+ if (!questions || !Array.isArray(questions) || questions.length === 0) {
141
+ console.error("Error: 'questions' array is required and must not be empty.");
142
+ process.exit(1);
143
+ }
144
+ for (let i = 0; i < questions.length; i++) {
145
+ const q = questions[i];
146
+ if (!q.prompt || typeof q.prompt !== "string") {
147
+ console.error(`Error: Question ${i + 1} missing 'prompt' field.`);
148
+ process.exit(1);
149
+ }
150
+ if (!q.title || typeof q.title !== "string") {
151
+ console.error(`Error: Question ${i + 1} missing 'title' field.`);
152
+ process.exit(1);
153
+ }
154
+ if (!q.options || !Array.isArray(q.options) || q.options.length < 2) {
155
+ console.error(`Error: Question ${i + 1} requires 'options' array with at least 2 options.`);
156
+ process.exit(1);
157
+ }
158
+ if (q.multiSelect === undefined) {
159
+ q.multiSelect = false;
160
+ }
161
+ }
162
+ const callId = randomUUID();
163
+ // Log status to stderr so stdout contains only the formatted response
164
+ console.error(`[AUQ] Session directory: ${sessionDir}`);
165
+ console.error(`[AUQ] Waiting for user to answer ${questions.length} question(s)...`);
166
+ console.error(`[AUQ] User should run 'auq' in another terminal to answer.`);
167
+ const { formattedResponse, sessionId } = await sessionManager.startSession(questions, callId);
168
+ console.error(`[AUQ] Session ${sessionId} completed.`);
169
+ console.log(formattedResponse);
170
+ process.exit(0);
171
+ }
172
+ catch (error) {
173
+ if (error instanceof SyntaxError) {
174
+ console.error("Error: Invalid JSON input.");
175
+ console.error(error.message);
176
+ }
177
+ else {
178
+ console.error("Error:", error instanceof Error ? error.message : String(error));
179
+ }
180
+ process.exit(1);
181
+ }
182
+ }
63
183
  const App = () => {
64
184
  const [state, setState] = useState({ mode: "WAITING" });
65
185
  const [sessionQueue, setSessionQueue] = useState([]);
@@ -201,12 +321,23 @@ const App = () => {
201
321
  // Clear terminal before showing app
202
322
  console.clear();
203
323
  const { waitUntilExit } = render(React.createElement(App, null));
324
+ // Periodically clear performance measures to prevent memory leak warnings
325
+ // React development mode creates performance marks that can accumulate over time
326
+ const clearPerformanceBuffer = () => {
327
+ if (typeof performance !== 'undefined' && performance.clearMeasures) {
328
+ performance.clearMeasures();
329
+ }
330
+ };
331
+ // Clear performance buffer every 5 minutes to prevent accumulation
332
+ const performanceCleanupInterval = setInterval(clearPerformanceBuffer, 5 * 60 * 1000);
204
333
  // Handle Ctrl+C gracefully
205
334
  process.on("SIGINT", () => {
335
+ clearInterval(performanceCleanupInterval);
206
336
  process.exit(0);
207
337
  });
208
338
  // Show goodbye after Ink unmounts
209
339
  waitUntilExit().then(() => {
340
+ clearInterval(performanceCleanupInterval);
210
341
  process.stdout.write("\n");
211
342
  console.log("👋 Goodbye! See you next time.");
212
343
  });
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "0.1.0",
3
+ "version": "1.2.5",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "dist/bin/auq.js"
@@ -11,14 +11,19 @@
11
11
  "README.md",
12
12
  "LICENSE"
13
13
  ],
14
+ "workspaces": [
15
+ "packages/opencode-plugin"
16
+ ],
14
17
  "scripts": {
15
- "build": "tsc",
18
+ "build": "npm run sync-schemas && tsc",
16
19
  "prepare": "npm run build",
17
20
  "postinstall": "node scripts/postinstall.cjs",
21
+ "deploy": "node scripts/deploy.mjs",
18
22
  "server": "node dist/src/server.js",
19
23
  "start": "tsx src/server.ts",
20
24
  "dev": "fastmcp dev src/server.ts",
21
25
  "lint": "prettier --check . && eslint . && tsc --noEmit",
26
+ "sync-schemas": "node scripts/sync-schemas.mjs",
22
27
  "test": "vitest run",
23
28
  "format": "prettier --write . && eslint --fix ."
24
29
  },
@@ -30,21 +35,26 @@
30
35
  "terminal"
31
36
  ],
32
37
  "repository": {
33
- "url": "https://github.com/paulp-o/ask-user-question-mcp"
38
+ "url": "git+https://github.com/paulp-o/ask-user-questions-mcp.git"
34
39
  },
35
40
  "author": "Paul Park",
36
- "homepage": "https://github.com/paulp-o/ask-user-question-mcp",
41
+ "homepage": "https://github.com/paulp-o/ask-user-questions-mcp",
37
42
  "type": "module",
38
43
  "license": "MIT",
39
44
  "description": "An MCP server that provides a tool to ask a user questions via the terminal",
40
45
  "dependencies": {
46
+ "@inkjs/ui": "^2.0.0",
47
+ "@modelcontextprotocol/sdk": "1.17.2",
41
48
  "@types/uuid": "^10.0.0",
42
- "fastmcp": "^1.27.3",
49
+ "chalk": "^5.6.2",
50
+ "fastmcp": "^3.23.0",
51
+ "gradient-string": "^3.0.0",
43
52
  "ink": "^6.4.0",
53
+ "ink-gradient": "^3.0.0",
44
54
  "ink-text-input": "^6.0.0",
45
55
  "react": "^19.2.0",
46
56
  "uuid": "^13.0.0",
47
- "zod": "^3.24.4"
57
+ "zod": "^4.1.13"
48
58
  },
49
59
  "release": {
50
60
  "branches": [
@@ -60,6 +70,7 @@
60
70
  "devDependencies": {
61
71
  "@eslint/js": "^9.26.0",
62
72
  "@tsconfig/node22": "^22.0.1",
73
+ "@types/node": "^22.13.0",
63
74
  "@types/react": "^19.2.2",
64
75
  "eslint-config-prettier": "^10.1.3",
65
76
  "eslint-plugin-perfectionist": "^4.12.3",
@@ -3,26 +3,15 @@
3
3
  * Tests the most common edge cases to catch obvious bugs
4
4
  */
5
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);
6
+ import { QuestionSchema, QuestionsSchema, } from "../core/ask-user-questions.js";
19
7
  describe("Schema Validation - Edge Cases", () => {
20
8
  describe("Invalid Input (should reject)", () => {
21
9
  it("should reject missing title field", () => {
22
10
  const invalidQuestion = {
23
11
  // title missing
24
- options: [{ label: "Option 1" }],
12
+ options: [{ label: "Option 1" }, { label: "Option 2" }],
25
13
  prompt: "Test question?",
14
+ multiSelect: false,
26
15
  };
27
16
  expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
28
17
  });
@@ -31,14 +20,16 @@ describe("Schema Validation - Edge Cases", () => {
31
20
  options: [], // Empty array
32
21
  prompt: "Test question?",
33
22
  title: "Test",
23
+ multiSelect: false,
34
24
  };
35
25
  expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
36
26
  });
37
27
  it("should reject missing prompt", () => {
38
28
  const invalidQuestion = {
39
- options: [{ label: "Option 1" }],
29
+ options: [{ label: "Option 1" }, { label: "Option 2" }],
40
30
  title: "Test",
41
31
  // prompt missing
32
+ multiSelect: false,
42
33
  };
43
34
  expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
44
35
  });
@@ -47,6 +38,7 @@ describe("Schema Validation - Edge Cases", () => {
47
38
  // options missing
48
39
  prompt: "Test question?",
49
40
  title: "Test",
41
+ multiSelect: false,
50
42
  };
51
43
  expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
52
44
  });
@@ -57,15 +49,17 @@ describe("Schema Validation - Edge Cases", () => {
57
49
  description: "A description",
58
50
  // label missing
59
51
  },
52
+ { label: "Option 2" },
60
53
  ],
61
54
  prompt: "Test question?",
62
55
  title: "Test",
56
+ multiSelect: false,
63
57
  };
64
58
  expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
65
59
  });
66
60
  it("should reject empty questions array", () => {
67
61
  const invalidQuestions = [];
68
- expect(() => QuestionsArraySchema.parse(invalidQuestions)).toThrow();
62
+ expect(() => QuestionsSchema.parse(invalidQuestions)).toThrow();
69
63
  });
70
64
  });
71
65
  describe("Valid Input (should accept)", () => {
@@ -76,15 +70,20 @@ describe("Schema Validation - Edge Cases", () => {
76
70
  description: "A helpful description",
77
71
  label: "Option 1",
78
72
  },
73
+ {
74
+ description: "Another helpful description",
75
+ label: "Option 2",
76
+ },
79
77
  ],
80
78
  prompt: "What is your choice?",
81
79
  title: "Language",
80
+ multiSelect: false,
82
81
  };
83
82
  expect(() => QuestionSchema.parse(validQuestion)).not.toThrow();
84
83
  const parsed = QuestionSchema.parse(validQuestion);
85
84
  expect(parsed.title).toBe("Language");
86
85
  expect(parsed.prompt).toBe("What is your choice?");
87
- expect(parsed.options).toHaveLength(1);
86
+ expect(parsed.options).toHaveLength(2);
88
87
  });
89
88
  it("should accept valid question with all fields", () => {
90
89
  const validQuestion = {
@@ -93,14 +92,19 @@ describe("Schema Validation - Edge Cases", () => {
93
92
  description: "A helpful description",
94
93
  label: "Option 1",
95
94
  },
95
+ {
96
+ description: "Another helpful description",
97
+ label: "Option 2",
98
+ },
96
99
  ],
97
100
  prompt: "What is your choice?",
98
101
  title: "Framework",
102
+ multiSelect: false,
99
103
  };
100
104
  expect(() => QuestionSchema.parse(validQuestion)).not.toThrow();
101
105
  const parsed = QuestionSchema.parse(validQuestion);
102
106
  expect(parsed.prompt).toBe("What is your choice?");
103
- expect(parsed.options).toHaveLength(1);
107
+ expect(parsed.options).toHaveLength(2);
104
108
  });
105
109
  it("should accept valid question with description omitted", () => {
106
110
  const validQuestion = {
@@ -109,9 +113,13 @@ describe("Schema Validation - Edge Cases", () => {
109
113
  label: "Option 1",
110
114
  // description omitted (optional)
111
115
  },
116
+ {
117
+ label: "Option 2",
118
+ },
112
119
  ],
113
120
  prompt: "What is your choice?",
114
121
  title: "Choice",
122
+ multiSelect: false,
115
123
  };
116
124
  expect(() => QuestionSchema.parse(validQuestion)).not.toThrow();
117
125
  const parsed = QuestionSchema.parse(validQuestion);
@@ -120,18 +128,20 @@ describe("Schema Validation - Edge Cases", () => {
120
128
  it("should accept multiple valid questions", () => {
121
129
  const validQuestions = [
122
130
  {
123
- options: [{ label: "A" }],
131
+ options: [{ label: "A" }, { label: "B" }],
124
132
  prompt: "Question 1?",
125
133
  title: "First",
134
+ multiSelect: false,
126
135
  },
127
136
  {
128
137
  options: [{ label: "B" }, { label: "C" }],
129
138
  prompt: "Question 2?",
130
139
  title: "Second",
140
+ multiSelect: false,
131
141
  },
132
142
  ];
133
- expect(() => QuestionsArraySchema.parse(validQuestions)).not.toThrow();
134
- const parsed = QuestionsArraySchema.parse(validQuestions);
143
+ expect(() => QuestionsSchema.parse(validQuestions)).not.toThrow();
144
+ const parsed = QuestionsSchema.parse(validQuestions);
135
145
  expect(parsed).toHaveLength(2);
136
146
  });
137
147
  it("should accept question with multiSelect: true", () => {
@@ -156,15 +166,13 @@ describe("Schema Validation - Edge Cases", () => {
156
166
  const parsed = QuestionSchema.parse(singleSelectQuestion);
157
167
  expect(parsed.multiSelect).toBe(false);
158
168
  });
159
- it("should accept question with multiSelect omitted (defaults to undefined)", () => {
169
+ it("should reject question with multiSelect omitted", () => {
160
170
  const defaultQuestion = {
161
- options: [{ label: "A" }],
171
+ options: [{ label: "A" }, { label: "B" }],
162
172
  prompt: "Default single-select",
163
173
  title: "Default",
164
174
  };
165
- expect(() => QuestionSchema.parse(defaultQuestion)).not.toThrow();
166
- const parsed = QuestionSchema.parse(defaultQuestion);
167
- expect(parsed.multiSelect).toBeUndefined();
175
+ expect(() => QuestionSchema.parse(defaultQuestion)).toThrow();
168
176
  });
169
177
  });
170
178
  });
@@ -0,0 +1,35 @@
1
+ import { SessionManager } from "../session/index.js";
2
+ import { getSessionDirectory } from "../session/utils.js";
3
+ import { AskUserQuestionsParametersSchema, QuestionSchema, QuestionsSchema, } from "../shared/schemas.js";
4
+ // Re-export schemas for backward compatibility
5
+ export { QuestionSchema, QuestionsSchema, AskUserQuestionsParametersSchema };
6
+ export const createAskUserQuestionsCore = (options = {}) => {
7
+ const baseDir = options.baseDir ?? getSessionDirectory();
8
+ const sessionManager = options.sessionManager ?? new SessionManager({ baseDir });
9
+ let initialized = false;
10
+ const ensureInitialized = async () => {
11
+ if (initialized)
12
+ return;
13
+ await sessionManager.initialize();
14
+ initialized = true;
15
+ };
16
+ const normalizeQuestions = (questions) => questions.map((question) => ({
17
+ options: question.options.map((option) => ({
18
+ description: option.description,
19
+ label: option.label,
20
+ })),
21
+ prompt: question.prompt,
22
+ title: question.title,
23
+ multiSelect: question.multiSelect,
24
+ }));
25
+ const ask = async (questions, callId) => {
26
+ await ensureInitialized();
27
+ const parsedQuestions = QuestionsSchema.parse(questions);
28
+ return sessionManager.startSession(normalizeQuestions(parsedQuestions), callId);
29
+ };
30
+ return {
31
+ ask,
32
+ cleanupExpiredSessions: () => sessionManager.cleanupExpiredSessions(),
33
+ ensureInitialized,
34
+ };
35
+ };
@@ -1,14 +1,7 @@
1
1
  import { FastMCP } from "fastmcp";
2
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 });
3
+ import { AskUserQuestionsParametersSchema, createAskUserQuestionsCore, } from "./core/ask-user-questions.js";
4
+ const askUserQuestionsCore = createAskUserQuestionsCore();
12
5
  const server = new FastMCP({
13
6
  name: "AskUserQuestions",
14
7
  instructions: "This MCP server provides a tool to ask structured questions to the user. " +
@@ -21,43 +14,7 @@ const server = new FastMCP({
21
14
  "returning formatted responses for continued reasoning. " +
22
15
  "Each question supports 2-4 multiple-choice options with descriptions, and users can always provide custom text input. " +
23
16
  "Both single-select and multi-select modes are supported.",
24
- version: "0.1.8",
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)"),
17
+ version: "0.1.17",
61
18
  });
62
19
  // Add the ask_user_questions tool
63
20
  server.addTool({
@@ -85,15 +42,17 @@ server.addTool({
85
42
  "- Use multiSelect: true when choices are not mutually exclusive\n" +
86
43
  "- Option labels should be concise (1-5 words)\n" +
87
44
  "- Questions should end with a question mark\n" +
88
- "- Don't include an 'Other' option - it's provided automatically\n\n" +
45
+ "- Don't include an 'Other' option - it's provided automatically\n" +
46
+ "- Mark one option as recommended.\n\n" +
89
47
  "- If user seems not aware how to answer the question, inform them that they need to install the 'auq' cli tool.\n\n" +
90
48
  "Returns a formatted summary of all questions and answers.",
91
- execute: async (args, { log }) => {
49
+ execute: async (args, ctx) => {
50
+ const { log } = ctx;
92
51
  try {
93
52
  // Initialize session manager if not already done
94
- await sessionManager.initialize();
53
+ await askUserQuestionsCore.ensureInitialized();
95
54
  // Clean up old sessions on startup (non-blocking)
96
- sessionManager
55
+ askUserQuestionsCore
97
56
  .cleanupExpiredSessions()
98
57
  .then((count) => {
99
58
  if (count > 0) {
@@ -107,23 +66,9 @@ server.addTool({
107
66
  if (!args.questions || args.questions.length === 0) {
108
67
  throw new Error("At least one question is required");
109
68
  }
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
69
  // Generate a per-tool-call ID and persist it with the session
125
70
  const callId = randomUUID();
126
- const { formattedResponse, sessionId } = await sessionManager.startSession(questions, callId);
71
+ const { formattedResponse, sessionId } = await askUserQuestionsCore.ask(args.questions, callId);
127
72
  log.info("Session completed successfully", { sessionId, callId });
128
73
  // Return formatted response to AI model
129
74
  return {
@@ -147,15 +92,7 @@ server.addTool({
147
92
  };
148
93
  }
149
94
  },
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
- }),
95
+ parameters: AskUserQuestionsParametersSchema,
159
96
  });
160
97
  // Start the server with stdio transport
161
98
  server.start({