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 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
- **The killer feature**: 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
+
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
- ### ๐ŸŒ Cross-Platform Question Aggregation
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
- **TL;DR**: AUQ transforms AI-agent questions from blocking interruptions into a smooth, unified workflow that scales with your AI coding setup.
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 (Recommended for most users)
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
- We now have **official OpenCode plugin support**! We support OpenCode because OpenCode's MCP server seems to have some limitations (timeout), so we created an OpenCode-specific plugin that works perfectly together with OpenCode.
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.9",
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, } from "../core/ask-user-questions.js";
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", () => {
@@ -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: "This MCP server provides a tool to ask structured questions to the user. " +
8
- "Use the ask_user_questions tool when you need to:\n" +
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.split("/").pop() ?? "";
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 = "Use this tool when you need to ask the user questions during execution. " +
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
- "- Ask 1-4 structured questions via an interactive terminal interface\n" +
52
- "- Each question includes 2-4 multiple-choice options with explanatory descriptions\n" +
53
- "- Users can always provide custom free-text input as an alternative to predefined options\n" +
54
- "- Single-select mode (default): User picks ONE option or provides custom text\n" +
55
- "- Multi-select mode (multiSelect: true): User can select MULTIPLE options\n\n" +
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 { MultiLineTextInput } from "./MultiLineTextInput.js";
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(MultiLineTextInput, { isFocused: true, onChange: setRejectionReason, onSubmit: handleReasonSubmit, placeholder: "Type your reason here...", value: rejectionReason })),
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 | Shift+Enter Newline | Esc Skip"))));
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
- const hasContent = customInputValue.trim().length > 0;
20
- const bindings = [
19
+ return [
21
20
  { key: "โ†‘โ†“", action: "Options" },
22
- { key: "Tab", action: "Next" },
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" }, { key: "Tab", action: "Submit" });
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
- * Shift+Enter for newlines, Enter to submit
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
- // Normalize Enter key sequences that may arrive as raw input ("\r"/"\n").
21
- // Prevent accidental carriage return insertion which causes line overwrite in terminals.
22
- if (input === "\r" || input === "\n") {
23
- if (key.shift) {
24
- const newValue = value.slice(0, cursorPosition) + "\n" + value.slice(cursorPosition);
25
- onChange(newValue);
26
- setCursorPosition(cursorPosition + 1);
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
- // Shift+Enter: Add newline
34
- if (key.return && key.shift) {
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.split("\n").slice(0, cursorLineIndex).join("\n").length + (cursorLineIndex > 0 ? cursorLineIndex : 0);
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 || key.tab) {
51
- // Enter OR Tab: Advance to next question (don't toggle)
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
- if (key.tab) {
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... (Shift+Enter for newline)", value: customValue }))),
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: setFocusContext }),
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 currentQuestion = sessionRequest.questions[currentQuestionIndex];
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
- // Question navigation with arrow keys
167
- if (key.leftArrow && currentQuestionIndex > 0) {
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 (key.rightArrow &&
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
  };
@@ -19,7 +19,7 @@ export const WaitingScreen = ({ queueCount }) => {
19
19
  }, [startTime]);
20
20
  // Handle 'q' key to quit
21
21
  useInput((input, key) => {
22
- if (input === 'q') {
22
+ if (input === "q") {
23
23
  process.exit(0);
24
24
  }
25
25
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "1.2.9",
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",