auq-mcp-server 1.2.7 → 1.2.9

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
@@ -1,18 +1,22 @@
1
1
  ![AUQ Demo](media/demo.png)
2
2
 
3
- # AUQ - ask-user-questions MCP
3
+ # AUQ - Ask User Questions
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/auq-mcp-server.svg)](https://www.npmjs.com/package/auq-mcp-server)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![Install MCP Server](https://cursor.com/deeplink/mcp-install-light.svg)](https://cursor.com/en-US/install-mcp?name=ask-user-questions&config=eyJlbnYiOnt9LCJjb21tYW5kIjoibnB4IC15IGF1cS1tY3Atc2VydmVyIHNlcnZlciJ9)
8
8
 
9
- **A lightweight MCP server & CLI tool that allows your LLMs to ask questions to you in a clean, separate space with great terminal UX. Made for multi-agent parallel coding workflows.**
9
+ **A lightweight CLI tool that allows your LLMs to ask questions to you in a separate space with clean terminal UX. Supports both MCP server and official OpenCode plugin integration. Made for multi-agent parallel coding workflows.**
10
+
11
+ 🤔 [Why do I need it when I already have question tool in CC/OC?](#-why-auq-vs-built-in-ask-tools)
12
+
13
+ [Setup](#setup-instructions) • [Features](#-features)
10
14
 
11
15
  ---
12
16
 
13
17
  ## What does it do?
14
18
 
15
- This MCP server lets your AI assistants generate clarifying questions consisting of multiple-choice/single-choice questions (with an "Other" option for custom input) while coding or working, and wait for your answers through a separate CLI tool without messing up your workflow.
19
+ AUQ lets your AI assistants generate clarifying questions consisting of multiple-choice/single-choice questions (with an "Other" option for custom input) while coding or working, and wait for your answers through a separate CLI tool without messing up your workflow.
16
20
 
17
21
  You can keep the CLI running in advance, or start it when questions are pending. With simple arrow key navigation, you can select answers and send them back to the AI—all within a clean terminal interface.
18
22
 
@@ -22,7 +26,7 @@ In AI-assisted coding, guiding LLMs to ask **clarifying questions** have been wi
22
26
 
23
27
  On October 18th, Claude Code 2.0.21 introduced an internal `ask-user-question` tool. Inspired by it, I decided to build a similar tool that is:
24
28
 
25
- - **Tool-agnostic** - Works with any MCP client (Claude Desktop, Cursor, etc.)
29
+ - **Integration-flexible** - Works with MCP clients (Claude Desktop, Cursor, etc.) and has official OpenCode plugin support
26
30
  - **Non-invasive** - Doesn't heavily integrate with your coding CLI workflow or occupy UI space
27
31
  - **Multi-agent friendly** - Supports receiving questions from multiple agents simultaneously in parallel workflows
28
32
 
@@ -56,9 +60,42 @@ Recent AI workflows often use parallel sub-agents for concurrent coding. AUQ han
56
60
 
57
61
  ---
58
62
 
63
+ ## 🤔 Why AUQ vs. Built-in Ask Tools?
64
+
65
+ **Why should I use AUQ instead of the built-in "Question" tools in OpenCode, Claude Code, or other coding agents?**
66
+
67
+ AUQ is designed for the era of parallel multi-agent workflows, with several key advantages:
68
+
69
+ ### 🚀 Non-Blocking Parallel Operation
70
+ 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
+ ### 🎯 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
+ - **No more screen switching** between different agent conversations
76
+ - **Unified queue** for all agent questions, regardless of which AI tool they're coming from
77
+ - **Sequential processing** of questions from multiple sources in one interface
78
+
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.
89
+
90
+ **TL;DR**: AUQ transforms AI-agent questions from blocking interruptions into a smooth, unified workflow that scales with your AI coding setup.
91
+
92
+ ---
93
+
59
94
  # Setup Instructions
60
95
 
61
- ## 🚀 Step 1: Setup CLI
96
+ ## 🚀 Install CLI Tool
97
+
98
+ First, install the AUQ CLI tool:
62
99
 
63
100
  ### Global Installation (Recommended)
64
101
 
@@ -87,7 +124,11 @@ npx auq
87
124
 
88
125
  ---
89
126
 
90
- ## 🔌 Step 2: Setup MCP Server
127
+ ## 🔌 Choose Your Integration Method
128
+
129
+ AUQ supports multiple AI coding environments. Choose the one that fits your workflow:
130
+
131
+ ### Option A: MCP Server (Recommended for most users)
91
132
 
92
133
  ### Cursor
93
134
 
@@ -168,28 +209,30 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
168
209
 
169
210
  **Restart Claude Desktop** after saving.
170
211
 
171
- ---
212
+ ### Option B: Official OpenCode Plugin
172
213
 
173
- ## 🔌 OpenCode Plugin (Optional)
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.
174
215
 
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.
216
+ The OpenCode plugin allows OpenCode to call `auq ask` directly (without MCP), providing seamless integration with OpenCode's workflow.
217
+
218
+ #### Installation
177
219
 
178
220
  ```bash
221
+ # Install both CLI and plugin
179
222
  npm install -g auq-mcp-server
180
223
  npm install -g @paulp-o/opencode-auq
181
224
  ```
182
225
 
226
+ #### Configuration
227
+
183
228
  Add to `opencode.json`:
184
229
 
185
230
  ```json
186
231
  {
187
- "$schema": "https://opencode.ai/config.json",
188
232
  "plugin": ["@paulp-o/opencode-auq"]
189
233
  }
190
234
  ```
191
235
 
192
- The plugin assumes `auq` is available on `PATH` (global install or equivalent).
193
236
 
194
237
  ---
195
238
 
package/dist/bin/auq.js CHANGED
@@ -3,14 +3,7 @@
3
3
  import { readFileSync } from "fs";
4
4
  import { dirname, join } from "path";
5
5
  import { fileURLToPath } from "url";
6
- import { Box, render, Text } from "ink";
7
- import React, { useEffect, useState } from "react";
8
- import { ensureDirectoryExists, getSessionDirectory, } from "../src/session/utils.js";
9
- import { Header } from "../src/tui/components/Header.js";
10
- import { StepperView } from "../src/tui/components/StepperView.js";
11
- import { Toast } from "../src/tui/components/Toast.js";
12
- import { WaitingScreen } from "../src/tui/components/WaitingScreen.js";
13
- import { createTUIWatcher } from "../src/tui/session-watcher.js";
6
+ import { getSessionDirectory } from "../src/session/utils.js";
14
7
  // import { goodbyeText } from "../src/tui/utils/gradientText.js";
15
8
  // Handle command-line arguments
16
9
  const args = process.argv.slice(2);
@@ -180,153 +173,11 @@ if (command === "ask") {
180
173
  process.exit(1);
181
174
  }
182
175
  }
183
- const App = () => {
184
- const [state, setState] = useState({ mode: "WAITING" });
185
- const [sessionQueue, setSessionQueue] = useState([]);
186
- const [isInitialized, setIsInitialized] = useState(false);
187
- const [toast, setToast] = useState(null);
188
- const [showSessionLog, setShowSessionLog] = useState(true);
189
- // Get session directory for logging
190
- const sessionDir = getSessionDirectory();
191
- // Auto-hide session log after 3 seconds
192
- useEffect(() => {
193
- const timer = setTimeout(() => {
194
- setShowSessionLog(false);
195
- }, 3000);
196
- return () => clearTimeout(timer);
197
- }, []);
198
- // Initialize: Load existing sessions + start persistent watcher
199
- useEffect(() => {
200
- let watcherInstance = null;
201
- const initialize = async () => {
202
- try {
203
- // Step 0: Ensure session directory exists
204
- await ensureDirectoryExists(sessionDir);
205
- // Step 1: Load existing pending sessions
206
- const watcher = createTUIWatcher();
207
- const sessionIds = await watcher.getPendingSessions();
208
- const sessionData = await Promise.all(sessionIds.map(async (sessionId) => {
209
- const sessionRequest = await watcher.getSessionRequest(sessionId);
210
- if (!sessionRequest)
211
- return null;
212
- return {
213
- sessionId,
214
- sessionRequest,
215
- timestamp: new Date(sessionRequest.timestamp),
216
- };
217
- }));
218
- // Filter out null entries and sort by timestamp (FIFO - oldest first)
219
- const validSessions = sessionData
220
- .filter((s) => s !== null)
221
- .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
222
- setSessionQueue(validSessions);
223
- setIsInitialized(true);
224
- // Step 2: Start persistent watcher for new sessions
225
- watcherInstance = createTUIWatcher({ autoLoadData: true });
226
- watcherInstance.startEnhancedWatching((event) => {
227
- // Add new session to queue (FIFO - append to end)
228
- setSessionQueue((prev) => {
229
- // Check for duplicates
230
- if (prev.some((s) => s.sessionId === event.sessionId)) {
231
- return prev;
232
- }
233
- // Add to end of queue
234
- return [
235
- ...prev,
236
- {
237
- sessionId: event.sessionId,
238
- sessionRequest: event.sessionRequest,
239
- timestamp: new Date(event.timestamp),
240
- },
241
- ];
242
- });
243
- });
244
- }
245
- catch (error) {
246
- console.error("Failed to initialize:", error);
247
- setIsInitialized(true); // Continue even if initialization fails
248
- }
249
- };
250
- initialize();
251
- // Cleanup: stop watcher on unmount
252
- return () => {
253
- if (watcherInstance) {
254
- watcherInstance.stop();
255
- }
256
- };
257
- }, []);
258
- // Auto-transition: WAITING → PROCESSING when queue has items
259
- useEffect(() => {
260
- if (!isInitialized)
261
- return;
262
- if (state.mode === "WAITING" && sessionQueue.length > 0) {
263
- const [nextSession, ...rest] = sessionQueue;
264
- setSessionQueue(rest);
265
- setState({ mode: "PROCESSING", session: nextSession });
266
- }
267
- }, [state, sessionQueue, isInitialized]);
268
- // Show toast notification
269
- const showToast = (message, type = "success", title) => {
270
- setToast({ message, type, title });
271
- };
272
- // Handle session completion
273
- const handleSessionComplete = (wasRejected = false, rejectionReason) => {
274
- // Show appropriate toast
275
- if (wasRejected) {
276
- if (rejectionReason) {
277
- showToast(`Rejection reason: ${rejectionReason}`, "info", "Question set rejected");
278
- }
279
- else {
280
- showToast("", "info", "Question set rejected");
281
- }
282
- }
283
- else {
284
- showToast("✓ Answers submitted successfully!", "success");
285
- }
286
- if (sessionQueue.length > 0) {
287
- // Auto-load next session
288
- const [nextSession, ...rest] = sessionQueue;
289
- setSessionQueue(rest);
290
- setState({ mode: "PROCESSING", session: nextSession });
291
- }
292
- else {
293
- // Return to WAITING
294
- setState({ mode: "WAITING" });
295
- }
296
- };
297
- // Render based on state
298
- if (!isInitialized) {
299
- return React.createElement(Text, null, "Loading...");
300
- }
301
- let mainContent;
302
- if (state.mode === "WAITING") {
303
- mainContent = React.createElement(WaitingScreen, { queueCount: sessionQueue.length });
304
- }
305
- else {
306
- // PROCESSING mode
307
- const { session } = state;
308
- mainContent = (React.createElement(StepperView, { key: session.sessionId, onComplete: handleSessionComplete, sessionId: session.sessionId, sessionRequest: session.sessionRequest }));
309
- }
310
- // Render with header, toast overlay, and main content
311
- return (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
312
- React.createElement(Header, { pendingCount: sessionQueue.length }),
313
- toast && (React.createElement(Box, { marginBottom: 1, marginTop: 1 },
314
- React.createElement(Toast, { message: toast.message, onDismiss: () => setToast(null), type: toast.type, title: toast.title }))),
315
- mainContent,
316
- showSessionLog && (React.createElement(Box, { marginTop: 1 },
317
- React.createElement(Text, { dimColor: true },
318
- "[AUQ] Session directory: ",
319
- sessionDir)))));
320
- };
321
- // Clear terminal before showing app
322
- console.clear();
323
- const { waitUntilExit } = render(React.createElement(App, null));
324
- // Handle Ctrl+C gracefully
325
- process.on("SIGINT", () => {
326
- process.exit(0);
327
- });
328
- // Show goodbye after Ink unmounts
329
- waitUntilExit().then(() => {
330
- process.stdout.write("\n");
331
- console.log("👋 Goodbye! See you next time.");
332
- });
176
+ // Default: Start TUI
177
+ // Important: Lazy-load Ink/React so non-interactive commands (ask/server) don't pull them in.
178
+ // Also force production mode before importing React/Ink to avoid perf_hooks measure accumulation warnings.
179
+ if (!process.env.NODE_ENV) {
180
+ process.env.NODE_ENV = "production";
181
+ }
182
+ const { runTui } = await import("./tui-app.js");
183
+ runTui();
@@ -0,0 +1,160 @@
1
+ import { Box, render, Text } from "ink";
2
+ import React, { useEffect, useState } from "react";
3
+ import { ensureDirectoryExists, getSessionDirectory, } from "../src/session/utils.js";
4
+ import { Header } from "../src/tui/components/Header.js";
5
+ import { StepperView } from "../src/tui/components/StepperView.js";
6
+ import { Toast } from "../src/tui/components/Toast.js";
7
+ import { WaitingScreen } from "../src/tui/components/WaitingScreen.js";
8
+ import { createTUIWatcher } from "../src/tui/session-watcher.js";
9
+ const App = () => {
10
+ const [state, setState] = useState({ mode: "WAITING" });
11
+ const [sessionQueue, setSessionQueue] = useState([]);
12
+ const [isInitialized, setIsInitialized] = useState(false);
13
+ const [toast, setToast] = useState(null);
14
+ const [showSessionLog, setShowSessionLog] = useState(true);
15
+ // Get session directory for logging
16
+ const sessionDir = getSessionDirectory();
17
+ // Auto-hide session log after 3 seconds
18
+ useEffect(() => {
19
+ const timer = setTimeout(() => {
20
+ setShowSessionLog(false);
21
+ }, 3000);
22
+ return () => clearTimeout(timer);
23
+ }, []);
24
+ // Initialize: Load existing sessions + start persistent watcher
25
+ useEffect(() => {
26
+ let watcherInstance = null;
27
+ const initialize = async () => {
28
+ try {
29
+ // Step 0: Ensure session directory exists
30
+ await ensureDirectoryExists(sessionDir);
31
+ // Step 1: Load existing pending sessions
32
+ const watcher = createTUIWatcher();
33
+ const sessionIds = await watcher.getPendingSessions();
34
+ const sessionData = await Promise.all(sessionIds.map(async (sessionId) => {
35
+ const sessionRequest = await watcher.getSessionRequest(sessionId);
36
+ if (!sessionRequest)
37
+ return null;
38
+ return {
39
+ sessionId,
40
+ sessionRequest,
41
+ timestamp: new Date(sessionRequest.timestamp),
42
+ };
43
+ }));
44
+ // Filter out null entries and sort by timestamp (FIFO - oldest first)
45
+ const validSessions = sessionData
46
+ .filter((s) => s !== null)
47
+ .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
48
+ setSessionQueue(validSessions);
49
+ setIsInitialized(true);
50
+ // Step 2: Start persistent watcher for new sessions
51
+ watcherInstance = createTUIWatcher({ autoLoadData: true });
52
+ watcherInstance.startEnhancedWatching((event) => {
53
+ // Add new session to queue (FIFO - append to end)
54
+ setSessionQueue((prev) => {
55
+ // Check for duplicates
56
+ if (prev.some((s) => s.sessionId === event.sessionId)) {
57
+ return prev;
58
+ }
59
+ // Add to end of queue
60
+ return [
61
+ ...prev,
62
+ {
63
+ sessionId: event.sessionId,
64
+ sessionRequest: event.sessionRequest,
65
+ timestamp: new Date(event.timestamp),
66
+ },
67
+ ];
68
+ });
69
+ });
70
+ }
71
+ catch (error) {
72
+ console.error("Failed to initialize:", error);
73
+ setIsInitialized(true); // Continue even if initialization fails
74
+ }
75
+ };
76
+ initialize();
77
+ // Cleanup: stop watcher on unmount
78
+ return () => {
79
+ if (watcherInstance) {
80
+ watcherInstance.stop();
81
+ }
82
+ };
83
+ }, []);
84
+ // Auto-transition: WAITING → PROCESSING when queue has items
85
+ useEffect(() => {
86
+ if (!isInitialized)
87
+ return;
88
+ if (state.mode === "WAITING" && sessionQueue.length > 0) {
89
+ const [nextSession, ...rest] = sessionQueue;
90
+ setSessionQueue(rest);
91
+ setState({ mode: "PROCESSING", session: nextSession });
92
+ }
93
+ }, [state, sessionQueue, isInitialized]);
94
+ // Show toast notification
95
+ const showToast = (message, type = "success", title) => {
96
+ setToast({ message, type, title });
97
+ };
98
+ // Handle session completion
99
+ const handleSessionComplete = (wasRejected = false, rejectionReason) => {
100
+ // Show appropriate toast
101
+ if (wasRejected) {
102
+ if (rejectionReason) {
103
+ showToast(`Rejection reason: ${rejectionReason}`, "info", "Question set rejected");
104
+ }
105
+ else {
106
+ showToast("", "info", "Question set rejected");
107
+ }
108
+ }
109
+ else {
110
+ showToast("✓ Answers submitted successfully!", "success");
111
+ }
112
+ if (sessionQueue.length > 0) {
113
+ // Auto-load next session
114
+ const [nextSession, ...rest] = sessionQueue;
115
+ setSessionQueue(rest);
116
+ setState({ mode: "PROCESSING", session: nextSession });
117
+ }
118
+ else {
119
+ // Return to WAITING
120
+ setState({ mode: "WAITING" });
121
+ }
122
+ };
123
+ // Render based on state
124
+ if (!isInitialized) {
125
+ return React.createElement(Text, null, "Loading...");
126
+ }
127
+ let mainContent;
128
+ if (state.mode === "WAITING") {
129
+ mainContent = React.createElement(WaitingScreen, { queueCount: sessionQueue.length });
130
+ }
131
+ else {
132
+ // PROCESSING mode
133
+ const { session } = state;
134
+ mainContent = (React.createElement(StepperView, { key: session.sessionId, onComplete: handleSessionComplete, sessionId: session.sessionId, sessionRequest: session.sessionRequest }));
135
+ }
136
+ // Render with header, toast overlay, and main content
137
+ return (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
138
+ React.createElement(Header, { pendingCount: sessionQueue.length }),
139
+ toast && (React.createElement(Box, { marginBottom: 1, marginTop: 1 },
140
+ React.createElement(Toast, { message: toast.message, onDismiss: () => setToast(null), type: toast.type, title: toast.title }))),
141
+ mainContent,
142
+ showSessionLog && (React.createElement(Box, { marginTop: 1 },
143
+ React.createElement(Text, { dimColor: true },
144
+ "[AUQ] Session directory: ",
145
+ sessionDir)))));
146
+ };
147
+ export const runTui = () => {
148
+ // Clear terminal before showing app
149
+ console.clear();
150
+ const { waitUntilExit } = render(React.createElement(App, null));
151
+ // Handle Ctrl+C gracefully
152
+ process.on("SIGINT", () => {
153
+ process.exit(0);
154
+ });
155
+ // Show goodbye after Ink unmounts
156
+ waitUntilExit().then(() => {
157
+ process.stdout.write("\n");
158
+ console.log("👋 Goodbye! See you next time.");
159
+ });
160
+ };
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "dist/bin/auq.js"
@@ -22,6 +22,15 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
22
22
  useInput((input, key) => {
23
23
  if (!isFocused)
24
24
  return;
25
+ // Handle up/down navigation even when custom input is focused
26
+ if (key.upArrow) {
27
+ setFocusedIndex((prev) => Math.max(0, prev - 1));
28
+ return;
29
+ }
30
+ if (key.downArrow) {
31
+ setFocusedIndex((prev) => Math.min(maxIndex, prev + 1));
32
+ return;
33
+ }
25
34
  // When custom input is focused, only handle escape to exit, let MultiLineTextInput handle other keys
26
35
  if (isCustomInputFocused) {
27
36
  if (key.escape) {
@@ -30,12 +39,6 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
30
39
  }
31
40
  return;
32
41
  }
33
- if (key.upArrow) {
34
- setFocusedIndex((prev) => Math.max(0, prev - 1));
35
- }
36
- if (key.downArrow) {
37
- setFocusedIndex((prev) => Math.min(maxIndex, prev + 1));
38
- }
39
42
  if (multiSelect) {
40
43
  // Multi-select mode
41
44
  if (input === " ") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "dist/bin/auq.js"