auq-mcp-server 1.2.9 → 1.3.1

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.
81
+ ### 🌐 Question Set Rejection Support
86
82
 
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.
89
-
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
 
@@ -215,14 +204,6 @@ We now have **official OpenCode plugin support**! We support OpenCode because Op
215
204
 
216
205
  The OpenCode plugin allows OpenCode to call `auq ask` directly (without MCP), providing seamless integration with OpenCode's workflow.
217
206
 
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
- ```
225
-
226
207
  #### Configuration
227
208
 
228
209
  Add to `opencode.json`:
@@ -233,7 +214,6 @@ Add to `opencode.json`:
233
214
  }
234
215
  ```
235
216
 
236
-
237
217
  ---
238
218
 
239
219
  ## 💻 Usage
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.1",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "dist/bin/auq.js"
@@ -19,6 +19,7 @@
19
19
  "prepare": "npm run build",
20
20
  "postinstall": "node scripts/postinstall.cjs",
21
21
  "deploy": "node scripts/deploy.mjs",
22
+ "semantic-release": "semantic-release",
22
23
  "server": "node dist/src/server.js",
23
24
  "start": "tsx src/server.ts",
24
25
  "dev": "fastmcp dev src/server.ts",
@@ -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", () => {
@@ -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
  }
@@ -117,7 +118,7 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
117
118
  customValue ? "●" : "○",
118
119
  " Other (custom answer)"),
119
120
  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 }))),
121
+ React.createElement(MultiLineTextInput, { isFocused: true, onChange: onCustomChange, onSubmit: onAdvance, placeholder: "Type your answer... (Enter for newline, Tab to submit)", value: customValue }))),
121
122
  !isCustomInputFocused && customValue && (React.createElement(Box, { marginLeft: 2, marginTop: 0.5 },
122
123
  React.createElement(Text, { dimColor: true }, customLines.map((line, idx) => (React.createElement(React.Fragment, { key: idx },
123
124
  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,6 +18,7 @@ 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 [focusContext, setFocusContext] = useState("option");
21
22
  const currentQuestion = sessionRequest.questions[currentQuestionIndex];
22
23
  const sessionCreatedAt = useMemo(() => {
23
24
  const parsed = Date.parse(sessionRequest.timestamp);
@@ -163,11 +164,23 @@ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
163
164
  setShowRejectionConfirm(true);
164
165
  return;
165
166
  }
166
- // Question navigation with arrow keys
167
- if (key.leftArrow && currentQuestionIndex > 0) {
167
+ // Tab/Shift+Tab: Global question navigation (works in all contexts)
168
+ if (key.tab && key.shift && currentQuestionIndex > 0) {
169
+ setCurrentQuestionIndex((prev) => prev - 1);
170
+ return;
171
+ }
172
+ if (key.tab &&
173
+ !key.shift &&
174
+ currentQuestionIndex < sessionRequest.questions.length - 1) {
175
+ setCurrentQuestionIndex((prev) => prev + 1);
176
+ return;
177
+ }
178
+ const shouldNavigate = focusContext !== "custom-input";
179
+ if (shouldNavigate && key.leftArrow && currentQuestionIndex > 0) {
168
180
  setCurrentQuestionIndex((prev) => prev - 1);
169
181
  }
170
- if (key.rightArrow &&
182
+ if (shouldNavigate &&
183
+ key.rightArrow &&
171
184
  currentQuestionIndex < sessionRequest.questions.length - 1) {
172
185
  setCurrentQuestionIndex((prev) => prev + 1);
173
186
  }
@@ -189,5 +202,5 @@ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
189
202
  return (React.createElement(ReviewScreen, { answers: answers, elapsedLabel: elapsedLabel, onConfirm: handleConfirm, onGoBack: handleGoBack, questions: sessionRequest.questions, sessionId: sessionId }));
190
203
  }
191
204
  // 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 }));
205
+ 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
206
  };
@@ -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.1",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "dist/bin/auq.js"
@@ -19,6 +19,7 @@
19
19
  "prepare": "npm run build",
20
20
  "postinstall": "node scripts/postinstall.cjs",
21
21
  "deploy": "node scripts/deploy.mjs",
22
+ "semantic-release": "semantic-release",
22
23
  "server": "node dist/src/server.js",
23
24
  "start": "tsx src/server.ts",
24
25
  "dev": "fastmcp dev src/server.ts",