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.
- package/README.md +24 -0
- package/dist/bin/auq.js +138 -7
- package/dist/package.json +17 -6
- package/dist/src/__tests__/schema-validation.test.js +34 -26
- package/dist/src/core/ask-user-questions.js +35 -0
- package/dist/src/server.js +11 -74
- package/dist/src/shared/schemas.js +64 -0
- package/dist/src/tui/components/Header.js +9 -1
- package/dist/src/tui/components/MultiLineTextInput.js +53 -14
- package/dist/src/tui/components/OptionsList.js +8 -0
- package/dist/src/tui/components/QuestionDisplay.js +10 -6
- package/dist/src/tui/components/ReviewScreen.js +6 -2
- package/dist/src/tui/components/StepperView.js +25 -3
- package/dist/src/tui/components/WaitingScreen.js +31 -4
- package/package.json +10 -4
- package/dist/__tests__/schema-validation.test.js +0 -137
- package/dist/__tests__/server.integration.test.js +0 -263
- package/dist/add.js +0 -1
- package/dist/add.test.js +0 -5
- package/dist/bin/test-session-menu.js +0 -28
- package/dist/bin/test-tabbar.js +0 -42
- package/dist/file-utils.js +0 -59
- package/dist/format/ResponseFormatter.js +0 -206
- package/dist/format/__tests__/ResponseFormatter.test.js +0 -380
- package/dist/server.js +0 -107
- package/dist/session/ResponseFormatter.js +0 -130
- package/dist/session/SessionManager.js +0 -474
- package/dist/session/__tests__/ResponseFormatter.test.js +0 -417
- package/dist/session/__tests__/SessionManager.test.js +0 -553
- package/dist/session/__tests__/atomic-operations.test.js +0 -345
- package/dist/session/__tests__/file-watcher.test.js +0 -311
- package/dist/session/__tests__/workflow.integration.test.js +0 -334
- package/dist/session/atomic-operations.js +0 -307
- package/dist/session/file-watcher.js +0 -218
- package/dist/session/index.js +0 -7
- package/dist/session/types.js +0 -20
- package/dist/session/utils.js +0 -125
- package/dist/session-manager.js +0 -171
- package/dist/session-watcher.js +0 -110
- package/dist/src/tui/components/SessionSelectionMenu.js +0 -151
- package/dist/tui/__tests__/session-watcher.test.js +0 -368
- 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
|
|
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
|
|
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
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
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": "
|
|
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-
|
|
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-
|
|
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
|
-
"
|
|
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": "^
|
|
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 {
|
|
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(() =>
|
|
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(
|
|
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(
|
|
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(() =>
|
|
134
|
-
const parsed =
|
|
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
|
|
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)).
|
|
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
|
+
};
|
package/dist/src/server.js
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { FastMCP } from "fastmcp";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
|
-
import {
|
|
4
|
-
|
|
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.
|
|
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
|
|
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,
|
|
49
|
+
execute: async (args, ctx) => {
|
|
50
|
+
const { log } = ctx;
|
|
92
51
|
try {
|
|
93
52
|
// Initialize session manager if not already done
|
|
94
|
-
await
|
|
53
|
+
await askUserQuestionsCore.ensureInitialized();
|
|
95
54
|
// Clean up old sessions on startup (non-blocking)
|
|
96
|
-
|
|
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
|
|
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:
|
|
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({
|