auq-mcp-server 1.2.9 โ 1.3.2
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 +54 -29
- package/dist/package.json +12 -2
- package/dist/src/__tests__/schema-validation.test.js +1 -1
- package/dist/src/server.js +2 -10
- package/dist/src/session/file-watcher.js +2 -2
- package/dist/src/shared/schemas.js +6 -20
- package/dist/src/tui/components/ConfirmationDialog.js +3 -3
- package/dist/src/tui/components/Footer.js +7 -9
- package/dist/src/tui/components/MultiLineTextInput.js +11 -24
- package/dist/src/tui/components/OptionsList.js +5 -9
- package/dist/src/tui/components/QuestionDisplay.js +6 -3
- package/dist/src/tui/components/SingleLineTextInput.js +59 -0
- package/dist/src/tui/components/StepperView.js +17 -5
- package/dist/src/tui/components/WaitingScreen.js +1 -1
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -67,27 +67,20 @@ Recent AI workflows often use parallel sub-agents for concurrent coding. AUQ han
|
|
|
67
67
|
AUQ is designed for the era of parallel multi-agent workflows, with several key advantages:
|
|
68
68
|
|
|
69
69
|
### ๐ Non-Blocking Parallel Operation
|
|
70
|
+
|
|
70
71
|
Unlike built-in ask tools that halt the entire AI workflow until you respond, AUQ **doesn't block the AI from continuing work**. Questions are queued asynchronously, allowing your AI assistants to keep coding while you review and answer questions at your own pace.
|
|
71
72
|
|
|
72
73
|
### ๐ฏ Multi-Agent Question Set Support
|
|
73
|
-
|
|
74
|
+
|
|
75
|
+
AUQ can handle question sets from **multiple agents simultaneously**. In modern AI coding workflows, you often have several sub-agents working in parallelโeach might need clarification on different aspects of your codebase. With AUQ:
|
|
74
76
|
|
|
75
77
|
- **No more screen switching** between different agent conversations
|
|
76
78
|
- **Unified queue** for all agent questions, regardless of which AI tool they're coming from
|
|
77
79
|
- **Sequential processing** of questions from multiple sources in one interface
|
|
78
80
|
|
|
79
|
-
### ๐
|
|
80
|
-
AUQ's **unified data origin** means you can answer questions from **different AI clients simultaneously**:
|
|
81
|
-
- Claude Code questions
|
|
82
|
-
- OpenCode questions
|
|
83
|
-
- Cursor/MCP questions
|
|
84
|
-
|
|
85
|
-
All appear in the **same CLI interface**, creating a single source of truth for all AI-agent questions across your entire development environment.
|
|
86
|
-
|
|
87
|
-
### ๐ก Perfect for Parallel Agent Workflows
|
|
88
|
-
As AI coding moves toward sophisticated multi-agent systems, AUQ becomes essential. Instead of managing blocking questions across multiple agent screens, you get one streamlined interface that handles questions from your entire AI coding ecosystemโkeeping your focus on the code, not the coordination.
|
|
81
|
+
### ๐ Question Set Rejection Support
|
|
89
82
|
|
|
90
|
-
**
|
|
83
|
+
**Skip irrelevant question sets entirely** - reject whole question batches that don't apply to your current context, saving time and maintaining focus on relevant AI-agent questions.
|
|
91
84
|
|
|
92
85
|
---
|
|
93
86
|
|
|
@@ -103,8 +96,6 @@ First, install the AUQ CLI tool:
|
|
|
103
96
|
# Install globally
|
|
104
97
|
npm install -g auq-mcp-server
|
|
105
98
|
|
|
106
|
-
# Start the TUI
|
|
107
|
-
auq
|
|
108
99
|
```
|
|
109
100
|
|
|
110
101
|
### Local Installation (Project-specific)
|
|
@@ -113,8 +104,6 @@ auq
|
|
|
113
104
|
# Install in your project
|
|
114
105
|
npm install auq-mcp-server
|
|
115
106
|
|
|
116
|
-
# Start the TUI from project directory
|
|
117
|
-
npx auq
|
|
118
107
|
```
|
|
119
108
|
|
|
120
109
|
**Session Storage:**
|
|
@@ -128,7 +117,7 @@ npx auq
|
|
|
128
117
|
|
|
129
118
|
AUQ supports multiple AI coding environments. Choose the one that fits your workflow:
|
|
130
119
|
|
|
131
|
-
### Option A: MCP Server
|
|
120
|
+
### Option A: MCP Server
|
|
132
121
|
|
|
133
122
|
### Cursor
|
|
134
123
|
|
|
@@ -211,17 +200,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
211
200
|
|
|
212
201
|
### Option B: Official OpenCode Plugin
|
|
213
202
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
The OpenCode plugin allows OpenCode to call `auq ask` directly (without MCP), providing seamless integration with OpenCode's workflow.
|
|
217
|
-
|
|
218
|
-
#### Installation
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
# Install both CLI and plugin
|
|
222
|
-
npm install -g auq-mcp-server
|
|
223
|
-
npm install -g @paulp-o/opencode-auq
|
|
224
|
-
```
|
|
203
|
+
**Direct integration** for OpenCode users. Bypasses MCP limitations by calling `auq ask` directly.
|
|
225
204
|
|
|
226
205
|
#### Configuration
|
|
227
206
|
|
|
@@ -233,7 +212,6 @@ Add to `opencode.json`:
|
|
|
233
212
|
}
|
|
234
213
|
```
|
|
235
214
|
|
|
236
|
-
|
|
237
215
|
---
|
|
238
216
|
|
|
239
217
|
## ๐ป Usage
|
|
@@ -256,6 +234,53 @@ auq --version # Show version
|
|
|
256
234
|
auq --help # Show help
|
|
257
235
|
```
|
|
258
236
|
|
|
237
|
+
<details>
|
|
238
|
+
<summary><strong>๐ auq ask ๋ช
๋ น์ด ์์ธ ์ค๋ช
(ํด๋ฆญํ์ฌ ํผ์น๊ธฐ)</strong></summary>
|
|
239
|
+
|
|
240
|
+
<br>
|
|
241
|
+
|
|
242
|
+
`auq ask`๋ AI ์ฝ๋ฉ ์์ด์ ํธ๊ฐ ์ฌ์ฉ์์๊ฒ ์ง๋ฌธ์ ํ ๋ ์ฌ์ฉํ๋ ํต์ฌ ๋ช
๋ น์ด์
๋๋ค.
|
|
243
|
+
|
|
244
|
+
#### ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
auq ask
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
์ด ๋ช
๋ น์ด๋ฅผ ์คํํ๋ฉด ๋ํํ ํฐ๋ฏธ๋ ์ธํฐํ์ด์ค๊ฐ ์์๋์ด AI ์์ด์ ํธ์ ์ง๋ฌธ ์ธํธ๋ฅผ ๊ธฐ๋ค๋ฆฝ๋๋ค.
|
|
251
|
+
|
|
252
|
+
#### ์๋ ๋ฐฉ์
|
|
253
|
+
|
|
254
|
+
1. **์ง๋ฌธ ์์ ๋๊ธฐ**: AI ์์ด์ ํธ(MCP ํด๋ผ์ด์ธํธ)๊ฐ ์ง๋ฌธ์ ๋ณด๋ด๋ฉด ์๋์ผ๋ก ํ์๋ฉ๋๋ค
|
|
255
|
+
2. **์ง๋ฌธ ์ธํธ ์ฒ๋ฆฌ**: ์ฌ๋ฌ ๊ฐ์ ์ฐ๊ด๋ ์ง๋ฌธ์ ํ๋์ ์ธํธ๋ก ๋ฌถ์ด์ ์ฒ๋ฆฌ
|
|
256
|
+
3. **์ฌ์ฉ์ ์๋ต**: ๊ฐ ์ง๋ฌธ์ ๋ํด ๋ต๋ณ์ ์
๋ ฅํ๊ฑฐ๋ ๊ฑด๋๋ธ ์ ์์
|
|
257
|
+
4. **์๋ต ์ ์ก**: ๋ชจ๋ ๋ต๋ณ์ ์๋ฃํ๋ฉด AI ์์ด์ ํธ๋ก ๊ฒฐ๊ณผ๊ฐ ์ ์ก๋ฉ๋๋ค
|
|
258
|
+
|
|
259
|
+
#### ์ฃผ์ ํน์ง
|
|
260
|
+
|
|
261
|
+
- **๋น์ฐจ๋จ ๋ฐฉ์**: AI๊ฐ ์ง๋ฌธ์ ๋ณด๋ด๋ ๋์์๋ ๊ณ์ ์์
ํ ์ ์์
|
|
262
|
+
- **๋ค์ค ์์ด์ ํธ ์ง์**: ์ฌ๋ฌ AI ์์ด์ ํธ์ ์ง๋ฌธ์ ๋์์ ์ฒ๋ฆฌ
|
|
263
|
+
- **์ธ์
๊ด๋ฆฌ**: ๊ฐ ์ง๋ฌธ ์ธํธ๋ ๋
๋ฆฝ์ ์ธ ์ธ์
์ผ๋ก ๊ด๋ฆฌ๋จ
|
|
264
|
+
- **ํ์์์ ์ฒ๋ฆฌ**: ์ผ์ ์๊ฐ ๋์ ์๋ต์ด ์์ผ๋ฉด ์ธ์
์ด ์๋ ์ข
๋ฃ
|
|
265
|
+
|
|
266
|
+
#### ์์ ์ํฌํ๋ก์ฐ
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
1. AI ์์ด์ ํธ๊ฐ ๋ณต์กํ ์ฝ๋๋ฅผ ์์ฑ ์ค
|
|
270
|
+
2. AI๊ฐ "์ด ํจ์์ ๋ฐํ ํ์
์ ์ด๋ป๊ฒ ํ ๊น์?"๋ผ๊ณ ์ง๋ฌธ
|
|
271
|
+
3. auq ask๊ฐ ์คํ๋์ด ์ง๋ฌธ์ด ํฐ๋ฏธ๋์ ํ์
|
|
272
|
+
4. ์ฌ์ฉ์๊ฐ ๋ต๋ณ ์
๋ ฅ
|
|
273
|
+
5. AI๊ฐ ๋ต๋ณ์ ๋ฐ์์ ์ฝ๋ ์์ฑ ๊ณ์ ์งํ
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
#### ๋ฌธ์ ํด๊ฒฐ
|
|
277
|
+
|
|
278
|
+
- **์ง๋ฌธ์ด ํ์๋์ง ์์**: MCP ์๋ฒ๊ฐ ์ ๋๋ก ์คํ๋๊ณ ์๋์ง ํ์ธ
|
|
279
|
+
- **์๋ต์ด ์ ์ก๋์ง ์์**: ๋คํธ์ํฌ ์ฐ๊ฒฐ๊ณผ ์ธ์
์ํ ํ์ธ
|
|
280
|
+
- **์ธ์
์ด ์ค๋จ๋จ**: ํ์์์ ์ค์ ์ด๋ ์ธ์
๊ด๋ฆฌ ํ์ธ
|
|
281
|
+
|
|
282
|
+
</details>
|
|
283
|
+
|
|
259
284
|
---
|
|
260
285
|
|
|
261
286
|
### Manual session cleanup
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auq-mcp-server",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"auq": "dist/bin/auq.js"
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"prepare": "npm run build",
|
|
20
20
|
"postinstall": "node scripts/postinstall.cjs",
|
|
21
21
|
"deploy": "node scripts/deploy.mjs",
|
|
22
|
+
"release": "semantic-release",
|
|
23
|
+
"release:dry-run": "semantic-release --dry-run",
|
|
22
24
|
"server": "node dist/src/server.js",
|
|
23
25
|
"start": "tsx src/server.ts",
|
|
24
26
|
"dev": "fastmcp dev src/server.ts",
|
|
@@ -58,17 +60,25 @@
|
|
|
58
60
|
},
|
|
59
61
|
"release": {
|
|
60
62
|
"branches": [
|
|
61
|
-
"main"
|
|
63
|
+
"main",
|
|
64
|
+
"master"
|
|
62
65
|
],
|
|
63
66
|
"plugins": [
|
|
64
67
|
"@semantic-release/commit-analyzer",
|
|
65
68
|
"@semantic-release/release-notes-generator",
|
|
69
|
+
[
|
|
70
|
+
"@semantic-release/changelog",
|
|
71
|
+
{
|
|
72
|
+
"changelogFile": "CHANGELOG.md"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
66
75
|
"@semantic-release/npm",
|
|
67
76
|
"@semantic-release/github"
|
|
68
77
|
]
|
|
69
78
|
},
|
|
70
79
|
"devDependencies": {
|
|
71
80
|
"@eslint/js": "^9.26.0",
|
|
81
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
72
82
|
"@tsconfig/node22": "^22.0.1",
|
|
73
83
|
"@types/node": "^22.13.0",
|
|
74
84
|
"@types/react": "^19.2.2",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Tests the most common edge cases to catch obvious bugs
|
|
4
4
|
*/
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
|
-
import { QuestionSchema, QuestionsSchema
|
|
6
|
+
import { QuestionSchema, QuestionsSchema } from "../core/ask-user-questions.js";
|
|
7
7
|
describe("Schema Validation - Edge Cases", () => {
|
|
8
8
|
describe("Invalid Input (should reject)", () => {
|
|
9
9
|
it("should reject missing title field", () => {
|
package/dist/src/server.js
CHANGED
|
@@ -4,16 +4,8 @@ import { AskUserQuestionsParametersSchema, createAskUserQuestionsCore, } from ".
|
|
|
4
4
|
const askUserQuestionsCore = createAskUserQuestionsCore();
|
|
5
5
|
const server = new FastMCP({
|
|
6
6
|
name: "AskUserQuestions",
|
|
7
|
-
instructions: "
|
|
8
|
-
"Use
|
|
9
|
-
"- Gather user preferences or requirements during execution\n" +
|
|
10
|
-
"- Clarify ambiguous instructions or implementation choices\n" +
|
|
11
|
-
"- Get decisions on what direction to take\n" +
|
|
12
|
-
"- Offer choices to the user about multiple valid approaches\n\n" +
|
|
13
|
-
"The tool allows AI models to pause execution and gather direct user input through an interactive TUI, " +
|
|
14
|
-
"returning formatted responses for continued reasoning. " +
|
|
15
|
-
"Each question supports 2-4 multiple-choice options with descriptions, and users can always provide custom text input. " +
|
|
16
|
-
"Both single-select and multi-select modes are supported.",
|
|
7
|
+
instructions: "MCP server for asking users structured questions during AI execution. " +
|
|
8
|
+
"Use ask_user_questions tool to gather preferences, clarify requirements, or make implementation decisions without blocking AI workflow.",
|
|
17
9
|
version: "0.1.17",
|
|
18
10
|
});
|
|
19
11
|
// Add the ask_user_questions tool
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { EventEmitter } from "events";
|
|
8
8
|
import { watch } from "fs";
|
|
9
|
-
import { join } from "path";
|
|
9
|
+
import { basename, join } from "path";
|
|
10
10
|
import { fileExists } from "./utils.js";
|
|
11
11
|
import { resolveSessionDirectory } from "./utils.js";
|
|
12
12
|
/**
|
|
@@ -171,7 +171,7 @@ export class PromiseFileWatcher extends EventEmitter {
|
|
|
171
171
|
if (!stats.isDirectory())
|
|
172
172
|
return;
|
|
173
173
|
// Extract session ID from directory name
|
|
174
|
-
const sessionId = sessionPath
|
|
174
|
+
const sessionId = basename(sessionPath);
|
|
175
175
|
if (!sessionId)
|
|
176
176
|
return;
|
|
177
177
|
// Verify it's a valid session (has request.json)
|
|
@@ -41,24 +41,10 @@ export const AskUserQuestionsParametersSchema = z.object({
|
|
|
41
41
|
"options (2-4 choices with labels and descriptions), and multiSelect (boolean). " +
|
|
42
42
|
"Mark one choice as recommended if possible."),
|
|
43
43
|
});
|
|
44
|
-
export const TOOL_DESCRIPTION = "
|
|
45
|
-
"This allows you to:\n" +
|
|
46
|
-
"1. Gather user preferences or requirements\n" +
|
|
47
|
-
"2. Clarify ambiguous instructions\n" +
|
|
48
|
-
"3. Get decisions on implementation choices as you work\n" +
|
|
49
|
-
"4. Offer choices to the user about what direction to take\n\n" +
|
|
44
|
+
export const TOOL_DESCRIPTION = "Ask users structured questions during execution to gather preferences, clarify requirements, or make implementation decisions.\n\n" +
|
|
50
45
|
"FEATURES:\n" +
|
|
51
|
-
"-
|
|
52
|
-
"-
|
|
53
|
-
"-
|
|
54
|
-
"-
|
|
55
|
-
"
|
|
56
|
-
"USAGE NOTES:\n" +
|
|
57
|
-
"- Always provide a descriptive 'title' field (max 12 chars) for each question\n" +
|
|
58
|
-
"- Use multiSelect: true when choices are not mutually exclusive\n" +
|
|
59
|
-
"- Option labels should be concise (1-5 words)\n" +
|
|
60
|
-
"- Questions should end with a question mark\n" +
|
|
61
|
-
"- Don't include an 'Other' option - it's provided automatically\n" +
|
|
62
|
-
"- Mark one choice as recommended if possible.\n\n" +
|
|
63
|
-
"- If user seems not aware how to answer the question, inform them that they need to install the 'auq' cli tool.\n\n" +
|
|
64
|
-
"Returns a formatted summary of all questions and answers.";
|
|
46
|
+
"- Non-blocking: doesn't halt AI workflow\n" +
|
|
47
|
+
"- 1-4 questions with 2-4 options each\n" +
|
|
48
|
+
"- Single/multi-select modes\n" +
|
|
49
|
+
"- Custom text input always available\n\n" +
|
|
50
|
+
"Returns formatted responses for continued reasoning.";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Box, Text, useInput } from "ink";
|
|
2
2
|
import React, { useState } from "react";
|
|
3
3
|
import { theme } from "../theme.js";
|
|
4
|
-
import {
|
|
4
|
+
import { SingleLineTextInput } from "./SingleLineTextInput.js";
|
|
5
5
|
/**
|
|
6
6
|
* ConfirmationDialog shows a 3-option prompt for session rejection
|
|
7
7
|
* Options: Reject & inform AI, Cancel, or Quit CLI
|
|
@@ -64,9 +64,9 @@ export const ConfirmationDialog = ({ message, onReject, onCancel, onQuit, }) =>
|
|
|
64
64
|
React.createElement(Box, { marginBottom: 1 },
|
|
65
65
|
React.createElement(Text, { dimColor: true }, "(Optional - helps the AI improve)")),
|
|
66
66
|
React.createElement(Box, { marginBottom: 1 },
|
|
67
|
-
React.createElement(
|
|
67
|
+
React.createElement(SingleLineTextInput, { isFocused: true, onChange: setRejectionReason, onSubmit: handleReasonSubmit, placeholder: "Type your reason here...", value: rejectionReason })),
|
|
68
68
|
React.createElement(Box, { marginTop: 1 },
|
|
69
|
-
React.createElement(Text, { dimColor: true }, "Enter Submit |
|
|
69
|
+
React.createElement(Text, { dimColor: true }, "Enter Submit | Esc Skip"))));
|
|
70
70
|
}
|
|
71
71
|
// Step 1: Confirmation options
|
|
72
72
|
return (React.createElement(Box, { borderColor: theme.borders.warning, borderStyle: "single", flexDirection: "column", padding: 1 },
|
|
@@ -16,25 +16,23 @@ export const Footer = ({ focusContext, multiSelect, isReviewScreen = false, cust
|
|
|
16
16
|
}
|
|
17
17
|
// Custom input focused
|
|
18
18
|
if (focusContext === "custom-input") {
|
|
19
|
-
|
|
20
|
-
const bindings = [
|
|
19
|
+
return [
|
|
21
20
|
{ key: "โโ", action: "Options" },
|
|
22
|
-
{ key: "
|
|
21
|
+
{ key: "โโ", action: "Cursor" },
|
|
22
|
+
{ key: "Tab/S+Tab", action: "Questions" },
|
|
23
|
+
{ key: "Enter", action: "Newline" },
|
|
24
|
+
{ key: "Esc", action: "Reject" },
|
|
23
25
|
];
|
|
24
|
-
if (hasContent) {
|
|
25
|
-
bindings.push({ key: "Enter", action: "Submit" });
|
|
26
|
-
}
|
|
27
|
-
bindings.push({ key: "Shift+Enter", action: "Newline" }, { key: "Esc", action: "Reject" });
|
|
28
|
-
return bindings;
|
|
29
26
|
}
|
|
30
27
|
// Option focused
|
|
31
28
|
if (focusContext === "option") {
|
|
32
29
|
const bindings = [
|
|
33
30
|
{ key: "โโ", action: "Options" },
|
|
34
31
|
{ key: "โโ", action: "Questions" },
|
|
32
|
+
{ key: "Tab/S+Tab", action: "Questions" },
|
|
35
33
|
];
|
|
36
34
|
if (multiSelect) {
|
|
37
|
-
bindings.push({ key: "Space", action: "Toggle" }
|
|
35
|
+
bindings.push({ key: "Space", action: "Toggle" });
|
|
38
36
|
}
|
|
39
37
|
else {
|
|
40
38
|
bindings.push({ key: "Enter", action: "Select" });
|
|
@@ -4,7 +4,7 @@ import { theme } from "../theme.js";
|
|
|
4
4
|
/**
|
|
5
5
|
* Multi-line text input component for Ink with cursor positioning
|
|
6
6
|
* Supports left/right arrow keys for cursor movement
|
|
7
|
-
*
|
|
7
|
+
* Enter for newlines, Tab to submit (portable across terminals)
|
|
8
8
|
*/
|
|
9
9
|
export const MultiLineTextInput = ({ isFocused = true, onChange, onSubmit, placeholder = "Type your answer...", value, }) => {
|
|
10
10
|
const [cursorPosition, setCursorPosition] = useState(value.length);
|
|
@@ -17,33 +17,22 @@ export const MultiLineTextInput = ({ isFocused = true, onChange, onSubmit, place
|
|
|
17
17
|
useInput((input, key) => {
|
|
18
18
|
if (!isFocused)
|
|
19
19
|
return;
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
else if (onSubmit) {
|
|
29
|
-
onSubmit();
|
|
30
|
-
}
|
|
20
|
+
// Tab: Submit (also triggers question navigation via parent)
|
|
21
|
+
if (key.tab && !key.shift) {
|
|
22
|
+
onSubmit?.();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Shift+Tab: Let parent handle for previous question navigation
|
|
26
|
+
if (key.tab && key.shift) {
|
|
31
27
|
return;
|
|
32
28
|
}
|
|
33
|
-
//
|
|
34
|
-
if (
|
|
29
|
+
// Enter: Always add newline (portable behavior)
|
|
30
|
+
if (input === "\r" || input === "\n" || key.return) {
|
|
35
31
|
const newValue = value.slice(0, cursorPosition) + "\n" + value.slice(cursorPosition);
|
|
36
32
|
onChange(newValue);
|
|
37
33
|
setCursorPosition(cursorPosition + 1);
|
|
38
34
|
return;
|
|
39
35
|
}
|
|
40
|
-
// Enter: Submit (empty input allowed)
|
|
41
|
-
if (key.return) {
|
|
42
|
-
if (onSubmit) {
|
|
43
|
-
onSubmit();
|
|
44
|
-
}
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
36
|
// Left arrow: Move cursor left
|
|
48
37
|
if (key.leftArrow) {
|
|
49
38
|
setCursorPosition(Math.max(0, cursorPosition - 1));
|
|
@@ -75,13 +64,11 @@ export const MultiLineTextInput = ({ isFocused = true, onChange, onSubmit, place
|
|
|
75
64
|
setCursorPosition(cursorPosition + 1);
|
|
76
65
|
}
|
|
77
66
|
}, { isActive: isFocused });
|
|
78
|
-
// Normalize any carriage returns that might already be present in value
|
|
79
67
|
const normalizedValue = value.replace(/\r\n?/g, "\n");
|
|
80
68
|
const hasContent = normalizedValue.length > 0;
|
|
81
69
|
const lines = hasContent ? normalizedValue.split("\n") : [placeholder];
|
|
82
|
-
// Calculate which line and position the cursor is on
|
|
83
70
|
const cursorLineIndex = normalizedValue.slice(0, cursorPosition).split("\n").length - 1;
|
|
84
|
-
const cursorLineStart = normalizedValue.
|
|
71
|
+
const cursorLineStart = normalizedValue.lastIndexOf("\n", cursorPosition - 1) + 1;
|
|
85
72
|
const cursorPositionInLine = cursorPosition - cursorLineStart;
|
|
86
73
|
return (React.createElement(Box, { flexDirection: "column" }, lines.map((line, index) => {
|
|
87
74
|
const isCursorLine = isFocused && index === cursorLineIndex && hasContent;
|
|
@@ -47,8 +47,9 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
|
|
|
47
47
|
onToggle?.(options[focusedIndex].label);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
if (key.return
|
|
51
|
-
// Enter
|
|
50
|
+
if (key.return) {
|
|
51
|
+
// Enter: Advance to next question (don't toggle)
|
|
52
|
+
// Note: Tab is handled globally in StepperView for question navigation
|
|
52
53
|
if (!isCustomInputFocused && onAdvance) {
|
|
53
54
|
onAdvance();
|
|
54
55
|
}
|
|
@@ -67,12 +68,7 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
|
|
|
67
68
|
onAdvance();
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
|
-
|
|
71
|
-
// Tab: Just advance (don't select)
|
|
72
|
-
if (onAdvance) {
|
|
73
|
-
onAdvance();
|
|
74
|
-
}
|
|
75
|
-
}
|
|
71
|
+
// Tab is handled globally in StepperView for question navigation
|
|
76
72
|
}
|
|
77
73
|
}, { isActive: isFocused });
|
|
78
74
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
@@ -117,7 +113,7 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
|
|
|
117
113
|
customValue ? "โ" : "โ",
|
|
118
114
|
" Other (custom answer)"),
|
|
119
115
|
isCustomInputFocused && onCustomChange && (React.createElement(Box, { marginLeft: 2, marginTop: 0.5 },
|
|
120
|
-
React.createElement(MultiLineTextInput, { isFocused: true, onChange: onCustomChange, onSubmit: onAdvance, placeholder: "Type your answer... (
|
|
116
|
+
React.createElement(MultiLineTextInput, { isFocused: true, onChange: onCustomChange, onSubmit: onAdvance, placeholder: "Type your answer... (Enter for newline, Tab to submit)", value: customValue }))),
|
|
121
117
|
!isCustomInputFocused && customValue && (React.createElement(Box, { marginLeft: 2, marginTop: 0.5 },
|
|
122
118
|
React.createElement(Text, { dimColor: true }, customLines.map((line, idx) => (React.createElement(React.Fragment, { key: idx },
|
|
123
119
|
idx === 0 ? "โฏ " : " ",
|
|
@@ -7,9 +7,12 @@ import { TabBar } from "./TabBar.js";
|
|
|
7
7
|
* QuestionDisplay shows a single question with its options
|
|
8
8
|
* Includes TabBar, question prompt, options list, and footer
|
|
9
9
|
*/
|
|
10
|
-
export const QuestionDisplay = ({ currentQuestion, currentQuestionIndex, customAnswer = "", elapsedLabel, onChangeCustomAnswer, onSelectOption, questions, selectedOption, onAdvanceToNext, answers, onToggleOption, multiSelect, }) => {
|
|
11
|
-
// Track focus context for Footer component
|
|
10
|
+
export const QuestionDisplay = ({ currentQuestion, currentQuestionIndex, customAnswer = "", elapsedLabel, onChangeCustomAnswer, onSelectOption, questions, selectedOption, onAdvanceToNext, answers, onToggleOption, multiSelect, onFocusContextChange, }) => {
|
|
12
11
|
const [focusContext, setFocusContext] = useState("option");
|
|
12
|
+
const handleFocusContextChange = (context) => {
|
|
13
|
+
setFocusContext(context);
|
|
14
|
+
onFocusContextChange?.(context);
|
|
15
|
+
};
|
|
13
16
|
// Handle option selection - clears custom answer only in single-select mode
|
|
14
17
|
const handleSelectOption = (label) => {
|
|
15
18
|
onSelectOption(label);
|
|
@@ -35,6 +38,6 @@ export const QuestionDisplay = ({ currentQuestion, currentQuestionIndex, customA
|
|
|
35
38
|
React.createElement(Text, { dimColor: true },
|
|
36
39
|
"Elapsed ",
|
|
37
40
|
elapsedLabel)),
|
|
38
|
-
React.createElement(OptionsList, { customValue: customAnswer, isFocused: true, onAdvance: onAdvanceToNext, onCustomChange: handleCustomAnswerChange, onSelect: handleSelectOption, options: currentQuestion.options, selectedOption: selectedOption, showCustomInput: true, onToggle: onToggleOption, multiSelect: multiSelect, selectedOptions: answers.get(currentQuestionIndex)?.selectedOptions, onFocusContextChange:
|
|
41
|
+
React.createElement(OptionsList, { customValue: customAnswer, isFocused: true, onAdvance: onAdvanceToNext, onCustomChange: handleCustomAnswerChange, onSelect: handleSelectOption, options: currentQuestion.options, selectedOption: selectedOption, showCustomInput: true, onToggle: onToggleOption, multiSelect: multiSelect, selectedOptions: answers.get(currentQuestionIndex)?.selectedOptions, onFocusContextChange: handleFocusContextChange }),
|
|
39
42
|
React.createElement(Footer, { focusContext: focusContext, multiSelect: multiSelect ?? false, customInputValue: customAnswer })));
|
|
40
43
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Text, useInput } from "ink";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { theme } from "../theme.js";
|
|
4
|
+
export const SingleLineTextInput = ({ isFocused = true, onChange, onSubmit, placeholder = "Type here...", value, }) => {
|
|
5
|
+
const [cursorPosition, setCursorPosition] = useState(value.length);
|
|
6
|
+
React.useEffect(() => {
|
|
7
|
+
if (cursorPosition > value.length) {
|
|
8
|
+
setCursorPosition(value.length);
|
|
9
|
+
}
|
|
10
|
+
}, [value.length, cursorPosition]);
|
|
11
|
+
useInput((input, key) => {
|
|
12
|
+
if (!isFocused)
|
|
13
|
+
return;
|
|
14
|
+
if (key.return) {
|
|
15
|
+
onSubmit?.();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (key.leftArrow) {
|
|
19
|
+
setCursorPosition(Math.max(0, cursorPosition - 1));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (key.rightArrow) {
|
|
23
|
+
setCursorPosition(Math.min(value.length, cursorPosition + 1));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (key.backspace || key.delete) {
|
|
27
|
+
if (cursorPosition > 0) {
|
|
28
|
+
const newValue = value.slice(0, cursorPosition - 1) + value.slice(cursorPosition);
|
|
29
|
+
onChange(newValue);
|
|
30
|
+
setCursorPosition(cursorPosition - 1);
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (input &&
|
|
35
|
+
!key.ctrl &&
|
|
36
|
+
!key.meta &&
|
|
37
|
+
!key.escape &&
|
|
38
|
+
!key.tab &&
|
|
39
|
+
input !== "\r" &&
|
|
40
|
+
input !== "\n") {
|
|
41
|
+
const newValue = value.slice(0, cursorPosition) + input + value.slice(cursorPosition);
|
|
42
|
+
onChange(newValue);
|
|
43
|
+
setCursorPosition(cursorPosition + 1);
|
|
44
|
+
}
|
|
45
|
+
}, { isActive: isFocused });
|
|
46
|
+
const hasContent = value.length > 0;
|
|
47
|
+
const displayText = hasContent ? value : placeholder;
|
|
48
|
+
if (hasContent && isFocused) {
|
|
49
|
+
const beforeCursor = value.slice(0, cursorPosition);
|
|
50
|
+
const afterCursor = value.slice(cursorPosition);
|
|
51
|
+
return (React.createElement(Text, null,
|
|
52
|
+
beforeCursor,
|
|
53
|
+
React.createElement(Text, { color: theme.colors.focused, dimColor: true }, "\u258C"),
|
|
54
|
+
afterCursor));
|
|
55
|
+
}
|
|
56
|
+
return (React.createElement(Text, { dimColor: !hasContent },
|
|
57
|
+
displayText,
|
|
58
|
+
isFocused && !hasContent && (React.createElement(Text, { color: theme.colors.focused, dimColor: true }, "\u258C"))));
|
|
59
|
+
};
|
|
@@ -18,7 +18,9 @@ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
|
|
|
18
18
|
const [submitting, setSubmitting] = useState(false);
|
|
19
19
|
const [showRejectionConfirm, setShowRejectionConfirm] = useState(false);
|
|
20
20
|
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
|
21
|
-
const
|
|
21
|
+
const [focusContext, setFocusContext] = useState("option");
|
|
22
|
+
const safeIndex = Math.min(currentQuestionIndex, sessionRequest.questions.length - 1);
|
|
23
|
+
const currentQuestion = sessionRequest.questions[safeIndex];
|
|
22
24
|
const sessionCreatedAt = useMemo(() => {
|
|
23
25
|
const parsed = Date.parse(sessionRequest.timestamp);
|
|
24
26
|
return Number.isNaN(parsed) ? Date.now() : parsed;
|
|
@@ -163,11 +165,21 @@ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
|
|
|
163
165
|
setShowRejectionConfirm(true);
|
|
164
166
|
return;
|
|
165
167
|
}
|
|
166
|
-
//
|
|
167
|
-
if (key.
|
|
168
|
+
// Tab/Shift+Tab: Global question navigation (works in all contexts)
|
|
169
|
+
if (key.tab && key.shift) {
|
|
170
|
+
setCurrentQuestionIndex((prev) => Math.max(0, prev - 1));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (key.tab && !key.shift) {
|
|
174
|
+
setCurrentQuestionIndex((prev) => Math.min(sessionRequest.questions.length - 1, prev + 1));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const shouldNavigate = focusContext !== "custom-input";
|
|
178
|
+
if (shouldNavigate && key.leftArrow && currentQuestionIndex > 0) {
|
|
168
179
|
setCurrentQuestionIndex((prev) => prev - 1);
|
|
169
180
|
}
|
|
170
|
-
if (
|
|
181
|
+
if (shouldNavigate &&
|
|
182
|
+
key.rightArrow &&
|
|
171
183
|
currentQuestionIndex < sessionRequest.questions.length - 1) {
|
|
172
184
|
setCurrentQuestionIndex((prev) => prev + 1);
|
|
173
185
|
}
|
|
@@ -189,5 +201,5 @@ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
|
|
|
189
201
|
return (React.createElement(ReviewScreen, { answers: answers, elapsedLabel: elapsedLabel, onConfirm: handleConfirm, onGoBack: handleGoBack, questions: sessionRequest.questions, sessionId: sessionId }));
|
|
190
202
|
}
|
|
191
203
|
// Show question display (default)
|
|
192
|
-
return (React.createElement(QuestionDisplay, { currentQuestion: currentQuestion, currentQuestionIndex: currentQuestionIndex, customAnswer: currentAnswer?.customText, elapsedLabel: elapsedLabel, onAdvanceToNext: handleAdvanceToNext, onChangeCustomAnswer: handleChangeCustomAnswer, onSelectOption: handleSelectOption, onToggleOption: handleToggleOption, multiSelect: currentQuestion.multiSelect, questions: sessionRequest.questions, selectedOption: currentAnswer?.selectedOption, answers: answers }));
|
|
204
|
+
return (React.createElement(QuestionDisplay, { currentQuestion: currentQuestion, currentQuestionIndex: currentQuestionIndex, customAnswer: currentAnswer?.customText, elapsedLabel: elapsedLabel, onAdvanceToNext: handleAdvanceToNext, onChangeCustomAnswer: handleChangeCustomAnswer, onSelectOption: handleSelectOption, onToggleOption: handleToggleOption, multiSelect: currentQuestion.multiSelect, questions: sessionRequest.questions, selectedOption: currentAnswer?.selectedOption, answers: answers, onFocusContextChange: setFocusContext }));
|
|
193
205
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auq-mcp-server",
|
|
3
|
-
"version": "1.2
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"auq": "dist/bin/auq.js"
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"prepare": "npm run build",
|
|
20
20
|
"postinstall": "node scripts/postinstall.cjs",
|
|
21
21
|
"deploy": "node scripts/deploy.mjs",
|
|
22
|
+
"release": "semantic-release",
|
|
23
|
+
"release:dry-run": "semantic-release --dry-run",
|
|
22
24
|
"server": "node dist/src/server.js",
|
|
23
25
|
"start": "tsx src/server.ts",
|
|
24
26
|
"dev": "fastmcp dev src/server.ts",
|
|
@@ -58,17 +60,25 @@
|
|
|
58
60
|
},
|
|
59
61
|
"release": {
|
|
60
62
|
"branches": [
|
|
61
|
-
"main"
|
|
63
|
+
"main",
|
|
64
|
+
"master"
|
|
62
65
|
],
|
|
63
66
|
"plugins": [
|
|
64
67
|
"@semantic-release/commit-analyzer",
|
|
65
68
|
"@semantic-release/release-notes-generator",
|
|
69
|
+
[
|
|
70
|
+
"@semantic-release/changelog",
|
|
71
|
+
{
|
|
72
|
+
"changelogFile": "CHANGELOG.md"
|
|
73
|
+
}
|
|
74
|
+
],
|
|
66
75
|
"@semantic-release/npm",
|
|
67
76
|
"@semantic-release/github"
|
|
68
77
|
]
|
|
69
78
|
},
|
|
70
79
|
"devDependencies": {
|
|
71
80
|
"@eslint/js": "^9.26.0",
|
|
81
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
72
82
|
"@tsconfig/node22": "^22.0.1",
|
|
73
83
|
"@types/node": "^22.13.0",
|
|
74
84
|
"@types/react": "^19.2.2",
|