auq-mcp-server 0.1.6 → 0.1.8
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/dist/bin/auq.js +10 -30
- package/dist/src/server.js +1 -1
- package/dist/src/session/ResponseFormatter.js +6 -2
- package/dist/src/session/__tests__/ResponseFormatter.test.js +3 -13
- package/dist/src/session/file-watcher.js +4 -1
- package/dist/src/tui/components/ConfirmationDialog.js +6 -6
- package/dist/src/tui/components/Footer.js +1 -1
- package/dist/src/tui/components/ReviewScreen.js +13 -6
- package/dist/src/tui/components/StepperView.js +6 -2
- package/dist/src/tui/components/Toast.js +4 -3
- package/dist/src/tui/session-watcher.js +2 -1
- package/package.json +1 -1
- package/scripts/postinstall.cjs +21 -19
package/dist/bin/auq.js
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
import { readFileSync } from "fs";
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { Box, render, Text
|
|
6
|
+
import { Box, render, Text } from "ink";
|
|
7
7
|
import React, { useEffect, useState } from "react";
|
|
8
|
-
import { SessionManager } from "../src/session/SessionManager.js";
|
|
9
8
|
import { ensureDirectoryExists, getSessionDirectory, } from "../src/session/utils.js";
|
|
10
9
|
import { Header } from "../src/tui/components/Header.js";
|
|
11
10
|
import { StepperView } from "../src/tui/components/StepperView.js";
|
|
@@ -62,7 +61,6 @@ if (command === "server") {
|
|
|
62
61
|
await new Promise(() => { });
|
|
63
62
|
}
|
|
64
63
|
const App = () => {
|
|
65
|
-
const { exit } = useApp();
|
|
66
64
|
const [state, setState] = useState({ mode: "WAITING" });
|
|
67
65
|
const [sessionQueue, setSessionQueue] = useState([]);
|
|
68
66
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
@@ -147,38 +145,20 @@ const App = () => {
|
|
|
147
145
|
setState({ mode: "PROCESSING", session: nextSession });
|
|
148
146
|
}
|
|
149
147
|
}, [state, sessionQueue, isInitialized]);
|
|
150
|
-
// Global 'q' to quit anytime
|
|
151
|
-
useInput((input) => {
|
|
152
|
-
if (input === "q") {
|
|
153
|
-
// If processing a session, reject it before exiting
|
|
154
|
-
if (state.mode === "PROCESSING") {
|
|
155
|
-
const sessionManager = new SessionManager();
|
|
156
|
-
sessionManager
|
|
157
|
-
.rejectSession(state.session.sessionId)
|
|
158
|
-
.catch((error) => {
|
|
159
|
-
console.error("Failed to reject session on quit:", error);
|
|
160
|
-
})
|
|
161
|
-
.finally(() => {
|
|
162
|
-
exit();
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
exit();
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
148
|
// Show toast notification
|
|
171
|
-
const showToast = (message, type = "success") => {
|
|
172
|
-
setToast({ message, type });
|
|
149
|
+
const showToast = (message, type = "success", title) => {
|
|
150
|
+
setToast({ message, type, title });
|
|
173
151
|
};
|
|
174
152
|
// Handle session completion
|
|
175
153
|
const handleSessionComplete = (wasRejected = false, rejectionReason) => {
|
|
176
154
|
// Show appropriate toast
|
|
177
155
|
if (wasRejected) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
156
|
+
if (rejectionReason) {
|
|
157
|
+
showToast(`Rejection reason: ${rejectionReason}`, "info", "Question set rejected");
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
showToast("", "info", "Question set rejected");
|
|
161
|
+
}
|
|
182
162
|
}
|
|
183
163
|
else {
|
|
184
164
|
showToast("✓ Answers submitted successfully!", "success");
|
|
@@ -211,7 +191,7 @@ const App = () => {
|
|
|
211
191
|
return (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
|
|
212
192
|
React.createElement(Header, { pendingCount: sessionQueue.length }),
|
|
213
193
|
toast && (React.createElement(Box, { marginBottom: 1, marginTop: 1 },
|
|
214
|
-
React.createElement(Toast, { message: toast.message, onDismiss: () => setToast(null), type: toast.type }))),
|
|
194
|
+
React.createElement(Toast, { message: toast.message, onDismiss: () => setToast(null), type: toast.type, title: toast.title }))),
|
|
215
195
|
mainContent,
|
|
216
196
|
showSessionLog && (React.createElement(Box, { marginTop: 1 },
|
|
217
197
|
React.createElement(Text, { dimColor: true },
|
package/dist/src/server.js
CHANGED
|
@@ -21,7 +21,7 @@ const server = new FastMCP({
|
|
|
21
21
|
"returning formatted responses for continued reasoning. " +
|
|
22
22
|
"Each question supports 2-4 multiple-choice options with descriptions, and users can always provide custom text input. " +
|
|
23
23
|
"Both single-select and multi-select modes are supported.",
|
|
24
|
-
version: "0.1.
|
|
24
|
+
version: "0.1.8",
|
|
25
25
|
});
|
|
26
26
|
// Define the question and option schemas
|
|
27
27
|
const OptionSchema = z.object({
|
|
@@ -73,7 +73,9 @@ export class ResponseFormatter {
|
|
|
73
73
|
throw new Error(`Answer references invalid question index: ${answer.questionIndex}`);
|
|
74
74
|
}
|
|
75
75
|
// Check that answer has either selectedOption, selectedOptions, or customText
|
|
76
|
-
if (!answer.selectedOption &&
|
|
76
|
+
if (!answer.selectedOption &&
|
|
77
|
+
!answer.customText &&
|
|
78
|
+
!answer.selectedOptions) {
|
|
77
79
|
throw new Error(`Answer for question ${answer.questionIndex} has neither selectedOption, selectedOptions, nor customText`);
|
|
78
80
|
}
|
|
79
81
|
// If selectedOption is provided, verify it exists in the question's options
|
|
@@ -125,7 +127,9 @@ export class ResponseFormatter {
|
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
|
-
else if (answer.selectedOptions &&
|
|
130
|
+
else if (answer.selectedOptions &&
|
|
131
|
+
answer.selectedOptions.length === 0 &&
|
|
132
|
+
!answer.customText) {
|
|
129
133
|
// Multi-select with no selections and no custom text
|
|
130
134
|
hasAnswer = true;
|
|
131
135
|
lines.push("→ (No selection)");
|
|
@@ -485,10 +485,7 @@ describe("ResponseFormatter", () => {
|
|
|
485
485
|
it("should validate empty selectedOptions array (no selections)", () => {
|
|
486
486
|
const questions = [
|
|
487
487
|
{
|
|
488
|
-
options: [
|
|
489
|
-
{ label: "Feature A" },
|
|
490
|
-
{ label: "Feature B" },
|
|
491
|
-
],
|
|
488
|
+
options: [{ label: "Feature A" }, { label: "Feature B" }],
|
|
492
489
|
prompt: "Which features do you want?",
|
|
493
490
|
title: "Features",
|
|
494
491
|
multiSelect: true,
|
|
@@ -551,11 +548,7 @@ describe("ResponseFormatter", () => {
|
|
|
551
548
|
it("should format multi-select question with multiple selections (without descriptions)", () => {
|
|
552
549
|
const questions = [
|
|
553
550
|
{
|
|
554
|
-
options: [
|
|
555
|
-
{ label: "Red" },
|
|
556
|
-
{ label: "Green" },
|
|
557
|
-
{ label: "Blue" },
|
|
558
|
-
],
|
|
551
|
+
options: [{ label: "Red" }, { label: "Green" }, { label: "Blue" }],
|
|
559
552
|
prompt: "Select your favorite colors",
|
|
560
553
|
title: "Colors",
|
|
561
554
|
multiSelect: true,
|
|
@@ -581,10 +574,7 @@ describe("ResponseFormatter", () => {
|
|
|
581
574
|
it("should format multi-select question with empty selections", () => {
|
|
582
575
|
const questions = [
|
|
583
576
|
{
|
|
584
|
-
options: [
|
|
585
|
-
{ label: "Feature A" },
|
|
586
|
-
{ label: "Feature B" },
|
|
587
|
-
],
|
|
577
|
+
options: [{ label: "Feature A" }, { label: "Feature B" }],
|
|
588
578
|
prompt: "Which optional features do you want?",
|
|
589
579
|
title: "Optional Features",
|
|
590
580
|
multiSelect: true,
|
|
@@ -54,8 +54,11 @@ export class PromiseFileWatcher extends EventEmitter {
|
|
|
54
54
|
*/
|
|
55
55
|
async waitForFile(watchPath, fileName) {
|
|
56
56
|
const fullPath = join(watchPath, fileName);
|
|
57
|
+
// Set isWatching early so active() returns true immediately
|
|
58
|
+
this.isWatching = true;
|
|
57
59
|
// Fast-path: if file already exists, resolve immediately
|
|
58
60
|
if (await fileExists(fullPath)) {
|
|
61
|
+
this.isWatching = false; // Not actually watching since file exists
|
|
59
62
|
return fullPath;
|
|
60
63
|
}
|
|
61
64
|
return new Promise((resolve, reject) => {
|
|
@@ -96,11 +99,11 @@ export class PromiseFileWatcher extends EventEmitter {
|
|
|
96
99
|
this.cleanup();
|
|
97
100
|
reject(new Error(`File watcher error: ${error.message}`));
|
|
98
101
|
});
|
|
99
|
-
this.isWatching = true;
|
|
100
102
|
}
|
|
101
103
|
catch (error) {
|
|
102
104
|
if (timeoutId)
|
|
103
105
|
clearTimeout(timeoutId);
|
|
106
|
+
this.isWatching = false;
|
|
104
107
|
reject(new Error(`File watcher setup error: ${error}`));
|
|
105
108
|
}
|
|
106
109
|
});
|
|
@@ -18,9 +18,12 @@ export const ConfirmationDialog = ({ message, onReject, onCancel, onQuit, }) =>
|
|
|
18
18
|
onReject(null);
|
|
19
19
|
};
|
|
20
20
|
const options = [
|
|
21
|
-
{
|
|
21
|
+
{
|
|
22
|
+
key: "y",
|
|
23
|
+
label: "Yes, inform the AI that I rejected this question set",
|
|
24
|
+
action: () => setShowReasonInput(true),
|
|
25
|
+
},
|
|
22
26
|
{ key: "n", label: "No, go back to answering questions", action: onCancel },
|
|
23
|
-
{ key: "q", label: "I'm just trying to quit the CLI, I'll answer later", action: onQuit },
|
|
24
27
|
];
|
|
25
28
|
useInput((input, key) => {
|
|
26
29
|
// If in reason input mode, handle Esc to skip
|
|
@@ -48,9 +51,6 @@ export const ConfirmationDialog = ({ message, onReject, onCancel, onQuit, }) =>
|
|
|
48
51
|
if (input === "n" || input === "N") {
|
|
49
52
|
onCancel();
|
|
50
53
|
}
|
|
51
|
-
if (input === "q" || input === "Q") {
|
|
52
|
-
onQuit();
|
|
53
|
-
}
|
|
54
54
|
// Esc key - same as quit
|
|
55
55
|
if (key.escape) {
|
|
56
56
|
onQuit();
|
|
@@ -85,5 +85,5 @@ export const ConfirmationDialog = ({ message, onReject, onCancel, onQuit, }) =>
|
|
|
85
85
|
")")));
|
|
86
86
|
}),
|
|
87
87
|
React.createElement(Box, { marginTop: 1 },
|
|
88
|
-
React.createElement(Text, { dimColor: true }, "\u2191\u2193 Navigate | Enter Select | y/n
|
|
88
|
+
React.createElement(Text, { dimColor: true }, "\u2191\u2193 Navigate | Enter Select | y/n Shortcuts | Esc Quit"))));
|
|
89
89
|
};
|
|
@@ -39,7 +39,7 @@ export const Footer = ({ focusContext, multiSelect, isReviewScreen = false, cust
|
|
|
39
39
|
else {
|
|
40
40
|
bindings.push({ key: "Enter", action: "Select" });
|
|
41
41
|
}
|
|
42
|
-
bindings.push({ key: "Esc", action: "Reject" }
|
|
42
|
+
bindings.push({ key: "Esc", action: "Reject" });
|
|
43
43
|
return bindings;
|
|
44
44
|
}
|
|
45
45
|
return [];
|
|
@@ -12,7 +12,9 @@ export const ReviewScreen = ({ answers, onConfirm, onGoBack, questions, }) => {
|
|
|
12
12
|
// Convert answers to UserAnswer format
|
|
13
13
|
const userAnswers = [];
|
|
14
14
|
answers.forEach((answer, questionIndex) => {
|
|
15
|
-
if (answer.selectedOption ||
|
|
15
|
+
if (answer.selectedOption ||
|
|
16
|
+
answer.selectedOptions ||
|
|
17
|
+
answer.customText) {
|
|
16
18
|
userAnswers.push({
|
|
17
19
|
customText: answer.customText,
|
|
18
20
|
questionIndex,
|
|
@@ -40,21 +42,26 @@ export const ReviewScreen = ({ answers, onConfirm, onGoBack, questions, }) => {
|
|
|
40
42
|
". ",
|
|
41
43
|
question.prompt),
|
|
42
44
|
React.createElement(Box, { flexDirection: "column", marginLeft: 2, marginTop: 0.5 },
|
|
43
|
-
answer?.selectedOptions &&
|
|
45
|
+
answer?.selectedOptions &&
|
|
46
|
+
answer.selectedOptions.length > 0 && (React.createElement(React.Fragment, null, answer.selectedOptions.map((option, idx) => (React.createElement(Text, { key: idx, color: theme.components.review.selectedOption },
|
|
44
47
|
"\u2192 ",
|
|
45
48
|
option))))),
|
|
46
49
|
answer?.selectedOption && (React.createElement(Text, { color: theme.components.review.selectedOption },
|
|
47
50
|
"\u2192 ",
|
|
48
51
|
answer.selectedOption)),
|
|
49
|
-
answer?.customText && (React.createElement(React.Fragment, null, answer.customText
|
|
52
|
+
answer?.customText && (React.createElement(React.Fragment, null, answer.customText
|
|
53
|
+
.split("\n")
|
|
54
|
+
.map((line, lineIndex, lines) => {
|
|
50
55
|
const isFirstLine = lineIndex === 0;
|
|
51
56
|
const isLastLine = lineIndex === lines.length - 1;
|
|
52
57
|
return (React.createElement(Text, { key: lineIndex, color: theme.components.review.customAnswer },
|
|
53
|
-
isFirstLine ?
|
|
58
|
+
isFirstLine ? '→ Custom: "' : " ",
|
|
54
59
|
line,
|
|
55
|
-
isLastLine ? "
|
|
60
|
+
isLastLine ? '"' : ""));
|
|
56
61
|
}))),
|
|
57
|
-
!answer?.selectedOption &&
|
|
62
|
+
!answer?.selectedOption &&
|
|
63
|
+
!answer?.selectedOptions &&
|
|
64
|
+
!answer?.customText && (React.createElement(Text, { dimColor: true }, "\u2192 (No answer provided)")))));
|
|
58
65
|
})),
|
|
59
66
|
React.createElement(Footer, { focusContext: "option", multiSelect: false, isReviewScreen: true })));
|
|
60
67
|
};
|
|
@@ -78,7 +78,9 @@ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
|
|
|
78
78
|
const handleConfirm = async (userAnswers) => {
|
|
79
79
|
setSubmitting(true);
|
|
80
80
|
try {
|
|
81
|
-
const sessionManager = new SessionManager({
|
|
81
|
+
const sessionManager = new SessionManager({
|
|
82
|
+
baseDir: getSessionDirectory(),
|
|
83
|
+
});
|
|
82
84
|
await sessionManager.saveSessionAnswers(sessionId, {
|
|
83
85
|
answers: userAnswers,
|
|
84
86
|
sessionId,
|
|
@@ -115,7 +117,9 @@ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
|
|
|
115
117
|
// Handle session rejection
|
|
116
118
|
const handleRejectSession = async (reason) => {
|
|
117
119
|
try {
|
|
118
|
-
const sessionManager = new SessionManager({
|
|
120
|
+
const sessionManager = new SessionManager({
|
|
121
|
+
baseDir: getSessionDirectory(),
|
|
122
|
+
});
|
|
119
123
|
await sessionManager.rejectSession(sessionId, reason);
|
|
120
124
|
// Call onComplete with rejection flag and reason
|
|
121
125
|
if (onComplete) {
|
|
@@ -4,7 +4,7 @@ import React, { useEffect } from "react";
|
|
|
4
4
|
* Toast component for brief non-blocking notifications
|
|
5
5
|
* Auto-dismisses after specified duration (default 2000ms)
|
|
6
6
|
*/
|
|
7
|
-
export const Toast = ({ message, type = "success", onDismiss, duration = 2000, }) => {
|
|
7
|
+
export const Toast = ({ message, type = "success", onDismiss, duration = 2000, title, }) => {
|
|
8
8
|
// Auto-dismiss after duration
|
|
9
9
|
useEffect(() => {
|
|
10
10
|
const timer = setTimeout(() => {
|
|
@@ -14,6 +14,7 @@ export const Toast = ({ message, type = "success", onDismiss, duration = 2000, }
|
|
|
14
14
|
}, [duration, onDismiss]);
|
|
15
15
|
// Color based on type
|
|
16
16
|
const color = type === "success" ? "green" : type === "error" ? "red" : "cyan";
|
|
17
|
-
return (React.createElement(Box, { borderColor: color, borderStyle: "round", paddingX: 2, paddingY: 0 },
|
|
18
|
-
React.createElement(Text, { bold: true, color: color },
|
|
17
|
+
return (React.createElement(Box, { borderColor: color, borderStyle: "round", paddingX: 2, paddingY: 0, flexDirection: "column" },
|
|
18
|
+
title && (React.createElement(Text, { bold: true, color: color }, title)),
|
|
19
|
+
React.createElement(Text, { color: color }, message)));
|
|
19
20
|
};
|
|
@@ -55,7 +55,8 @@ export class EnhancedTUISessionWatcher extends TUISessionWatcher {
|
|
|
55
55
|
const status = JSON.parse(statusContent);
|
|
56
56
|
// Only include sessions that are actually pending or in-progress
|
|
57
57
|
// Exclude: rejected, completed, timed_out, abandoned
|
|
58
|
-
if (status.status === "pending" ||
|
|
58
|
+
if (status.status === "pending" ||
|
|
59
|
+
status.status === "in-progress") {
|
|
59
60
|
pendingSessions.push(entry.name);
|
|
60
61
|
}
|
|
61
62
|
}
|
package/package.json
CHANGED
package/scripts/postinstall.cjs
CHANGED
|
@@ -5,47 +5,49 @@
|
|
|
5
5
|
* Provides instructions for setting up shell aliases
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
const os = require(
|
|
9
|
-
const path = require(
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const path = require("path");
|
|
10
10
|
|
|
11
11
|
// Check if this is a global installation
|
|
12
|
-
const isGlobal = process.env.npm_config_global ===
|
|
12
|
+
const isGlobal = process.env.npm_config_global === "true";
|
|
13
13
|
|
|
14
14
|
if (!isGlobal) {
|
|
15
15
|
// Local install - no setup needed
|
|
16
16
|
process.exit(0);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
console.log(
|
|
20
|
-
console.log(
|
|
19
|
+
console.log("\n✅ AUQ MCP Server installed successfully!\n");
|
|
20
|
+
console.log("📝 Optional: Set up shell aliases for convenience\n");
|
|
21
21
|
|
|
22
22
|
const homeDir = os.homedir();
|
|
23
|
-
const shell = process.env.SHELL ||
|
|
23
|
+
const shell = process.env.SHELL || "";
|
|
24
24
|
|
|
25
25
|
// Determine shell config file
|
|
26
|
-
let configFile =
|
|
27
|
-
let aliasCommand =
|
|
26
|
+
let configFile = "";
|
|
27
|
+
let aliasCommand = "";
|
|
28
28
|
|
|
29
|
-
if (shell.includes(
|
|
30
|
-
configFile = path.join(homeDir,
|
|
29
|
+
if (shell.includes("zsh")) {
|
|
30
|
+
configFile = path.join(homeDir, ".zshrc");
|
|
31
31
|
aliasCommand = 'alias auq="npx auq-mcp-server"';
|
|
32
|
-
} else if (shell.includes(
|
|
33
|
-
configFile = path.join(homeDir,
|
|
32
|
+
} else if (shell.includes("bash")) {
|
|
33
|
+
configFile = path.join(homeDir, ".bashrc");
|
|
34
34
|
aliasCommand = 'alias auq="npx auq-mcp-server"';
|
|
35
|
-
} else if (shell.includes(
|
|
36
|
-
configFile = path.join(homeDir,
|
|
35
|
+
} else if (shell.includes("fish")) {
|
|
36
|
+
configFile = path.join(homeDir, ".config/fish/config.fish");
|
|
37
37
|
aliasCommand = 'alias auq "npx auq-mcp-server"';
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (configFile) {
|
|
41
|
-
console.log(
|
|
41
|
+
console.log(
|
|
42
|
+
`To set up a shell alias, add this to your ${path.basename(configFile)}:\n`,
|
|
43
|
+
);
|
|
42
44
|
console.log(` ${aliasCommand}\n`);
|
|
43
45
|
console.log(`Then restart your terminal or run: source ${configFile}\n`);
|
|
44
46
|
} else {
|
|
45
|
-
console.log(
|
|
47
|
+
console.log("To set up a shell alias, add this to your shell config:\n");
|
|
46
48
|
console.log(' alias auq="npx auq-mcp-server"\n');
|
|
47
|
-
console.log(
|
|
49
|
+
console.log("Then restart your terminal.\n");
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
console.log(
|
|
51
|
-
console.log(
|
|
52
|
+
console.log("For MCP server setup with Claude Desktop or Cursor, see:");
|
|
53
|
+
console.log(" https://github.com/paulp-o/ask-user-questions-mcp#setup\n");
|