auq-mcp-server 0.1.10 → 1.2.5

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.
Files changed (42) hide show
  1. package/README.md +24 -0
  2. package/dist/bin/auq.js +138 -7
  3. package/dist/package.json +17 -6
  4. package/dist/src/__tests__/schema-validation.test.js +34 -26
  5. package/dist/src/core/ask-user-questions.js +35 -0
  6. package/dist/src/server.js +11 -74
  7. package/dist/src/shared/schemas.js +64 -0
  8. package/dist/src/tui/components/Header.js +9 -1
  9. package/dist/src/tui/components/MultiLineTextInput.js +53 -14
  10. package/dist/src/tui/components/OptionsList.js +8 -0
  11. package/dist/src/tui/components/QuestionDisplay.js +10 -6
  12. package/dist/src/tui/components/ReviewScreen.js +6 -2
  13. package/dist/src/tui/components/StepperView.js +25 -3
  14. package/dist/src/tui/components/WaitingScreen.js +31 -4
  15. package/package.json +10 -4
  16. package/dist/__tests__/schema-validation.test.js +0 -137
  17. package/dist/__tests__/server.integration.test.js +0 -263
  18. package/dist/add.js +0 -1
  19. package/dist/add.test.js +0 -5
  20. package/dist/bin/test-session-menu.js +0 -28
  21. package/dist/bin/test-tabbar.js +0 -42
  22. package/dist/file-utils.js +0 -59
  23. package/dist/format/ResponseFormatter.js +0 -206
  24. package/dist/format/__tests__/ResponseFormatter.test.js +0 -380
  25. package/dist/server.js +0 -107
  26. package/dist/session/ResponseFormatter.js +0 -130
  27. package/dist/session/SessionManager.js +0 -474
  28. package/dist/session/__tests__/ResponseFormatter.test.js +0 -417
  29. package/dist/session/__tests__/SessionManager.test.js +0 -553
  30. package/dist/session/__tests__/atomic-operations.test.js +0 -345
  31. package/dist/session/__tests__/file-watcher.test.js +0 -311
  32. package/dist/session/__tests__/workflow.integration.test.js +0 -334
  33. package/dist/session/atomic-operations.js +0 -307
  34. package/dist/session/file-watcher.js +0 -218
  35. package/dist/session/index.js +0 -7
  36. package/dist/session/types.js +0 -20
  37. package/dist/session/utils.js +0 -125
  38. package/dist/session-manager.js +0 -171
  39. package/dist/session-watcher.js +0 -110
  40. package/dist/src/tui/components/SessionSelectionMenu.js +0 -151
  41. package/dist/tui/__tests__/session-watcher.test.js +0 -368
  42. package/dist/tui/session-watcher.js +0 -183
@@ -0,0 +1,64 @@
1
+ import { z } from "zod";
2
+ export const OptionSchema = z.object({
3
+ label: z
4
+ .string()
5
+ .describe("The display text for this option that the user will see and select. " +
6
+ "Should be concise (1-5 words) and clearly describe the choice."),
7
+ description: z
8
+ .string()
9
+ .optional()
10
+ .describe("Explanation of what this option means or what will happen if chosen. " +
11
+ "Useful for providing context about trade-offs or implications."),
12
+ });
13
+ export const QuestionSchema = z.object({
14
+ prompt: z
15
+ .string()
16
+ .describe("The complete question to ask the user. Should be clear, specific, and end with a question mark. " +
17
+ "Example: 'Which programming language do you want to use?' " +
18
+ "If multiSelect is true, phrase it accordingly, e.g. 'Which features do you want to enable?'"),
19
+ title: z
20
+ .string()
21
+ .min(1, "Question title is required. Provide a short summary like 'Language' or 'Framework'.")
22
+ .describe("Very short label displayed as a chip/tag (max 12 chars). " +
23
+ "Examples: 'Auth method', 'Library', 'Approach'. " +
24
+ "This title appears in the interface to help users quickly identify questions."),
25
+ options: z
26
+ .array(OptionSchema)
27
+ .min(2)
28
+ .max(4)
29
+ .describe("The available choices for this question. Must have 2-4 options. " +
30
+ "Each option should be a distinct, mutually exclusive choice (unless multiSelect is enabled). " +
31
+ "There should be no 'Other' option, that will be provided automatically."),
32
+ multiSelect: z
33
+ .boolean()
34
+ .describe("Set to true to allow the user to select multiple options instead of just one. " +
35
+ "Use when choices are not mutually exclusive. Default: false (single-select)"),
36
+ });
37
+ export const QuestionsSchema = z.array(QuestionSchema).min(1).max(4);
38
+ export const AskUserQuestionsParametersSchema = z.object({
39
+ questions: QuestionsSchema.describe("Questions to ask the user (1-4 questions). " +
40
+ "Each question must include: prompt (full question text), title (short label, max 12 chars), " +
41
+ "options (2-4 choices with labels and descriptions), and multiSelect (boolean). " +
42
+ "Mark one choice as recommended if possible."),
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" +
50
+ "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.";
@@ -2,6 +2,7 @@ import { Box, Text } from "ink";
2
2
  import gradient from "gradient-string";
3
3
  import React, { useEffect, useState } from "react";
4
4
  import { theme } from "../theme.js";
5
+ import packageJson from "../../../package.json" with { type: "json" };
5
6
  /**
6
7
  * Header component - displays app logo and status
7
8
  * Shows at the top of the TUI with gradient branding and live-updating pending queue count
@@ -18,12 +19,19 @@ export const Header = ({ pendingCount }) => {
18
19
  return () => clearTimeout(timer);
19
20
  }
20
21
  }, [pendingCount, prevCount]);
22
+ // Get version from package.json
23
+ const version = React.useMemo(() => {
24
+ return packageJson.version || "unknown";
25
+ }, []);
21
26
  // Use the selected gradient theme from theme.ts
22
27
  const headerText = gradient[theme.headerGradient](".𖥔 AUQ ⋆ Ask User Questions MCP ⋆ ");
23
28
  return (React.createElement(Box, { borderColor: theme.components.header.border, borderStyle: "single", flexDirection: "row", justifyContent: "space-between", paddingX: 1 },
24
29
  React.createElement(Text, { bold: true }, headerText),
25
30
  React.createElement(Box, null,
26
- React.createElement(Text, { dimColor: true }, "\u2502"),
31
+ React.createElement(Text, { dimColor: true },
32
+ "v",
33
+ version,
34
+ " \u2502"),
27
35
  React.createElement(Text, { bold: flash, color: flash
28
36
  ? theme.components.header.queueFlash
29
37
  : pendingCount > 0
@@ -1,12 +1,19 @@
1
1
  import { Box, Text, useInput } from "ink";
2
- import React from "react";
2
+ import React, { useState } from "react";
3
3
  import { theme } from "../theme.js";
4
4
  /**
5
- * Multi-line text input component for Ink
6
- * Append-only mode: Shift+Enter for newlines, Enter to submit
7
- * Note: No cursor positioning - text is append-only for simplicity
5
+ * Multi-line text input component for Ink with cursor positioning
6
+ * Supports left/right arrow keys for cursor movement
7
+ * Shift+Enter for newlines, Enter to submit
8
8
  */
9
9
  export const MultiLineTextInput = ({ isFocused = true, onChange, onSubmit, placeholder = "Type your answer...", value, }) => {
10
+ const [cursorPosition, setCursorPosition] = useState(value.length);
11
+ // Update cursor position when value changes externally
12
+ React.useEffect(() => {
13
+ if (cursorPosition > value.length) {
14
+ setCursorPosition(value.length);
15
+ }
16
+ }, [value.length, cursorPosition]);
10
17
  useInput((input, key) => {
11
18
  if (!isFocused)
12
19
  return;
@@ -14,7 +21,9 @@ export const MultiLineTextInput = ({ isFocused = true, onChange, onSubmit, place
14
21
  // Prevent accidental carriage return insertion which causes line overwrite in terminals.
15
22
  if (input === "\r" || input === "\n") {
16
23
  if (key.shift) {
17
- onChange(value + "\n");
24
+ const newValue = value.slice(0, cursorPosition) + "\n" + value.slice(cursorPosition);
25
+ onChange(newValue);
26
+ setCursorPosition(cursorPosition + 1);
18
27
  }
19
28
  else if (onSubmit) {
20
29
  onSubmit();
@@ -23,7 +32,9 @@ export const MultiLineTextInput = ({ isFocused = true, onChange, onSubmit, place
23
32
  }
24
33
  // Shift+Enter: Add newline
25
34
  if (key.return && key.shift) {
26
- onChange(value + "\n");
35
+ const newValue = value.slice(0, cursorPosition) + "\n" + value.slice(cursorPosition);
36
+ onChange(newValue);
37
+ setCursorPosition(cursorPosition + 1);
27
38
  return;
28
39
  }
29
40
  // Enter: Submit (empty input allowed)
@@ -33,33 +44,61 @@ export const MultiLineTextInput = ({ isFocused = true, onChange, onSubmit, place
33
44
  }
34
45
  return;
35
46
  }
36
- // Backspace: Remove last character
47
+ // Left arrow: Move cursor left
48
+ if (key.leftArrow) {
49
+ setCursorPosition(Math.max(0, cursorPosition - 1));
50
+ return;
51
+ }
52
+ // Right arrow: Move cursor right
53
+ if (key.rightArrow) {
54
+ setCursorPosition(Math.min(value.length, cursorPosition + 1));
55
+ return;
56
+ }
57
+ // Backspace: Remove character before cursor
37
58
  if (key.backspace || key.delete) {
38
- onChange(value.slice(0, -1));
59
+ if (cursorPosition > 0) {
60
+ const newValue = value.slice(0, cursorPosition - 1) + value.slice(cursorPosition);
61
+ onChange(newValue);
62
+ setCursorPosition(cursorPosition - 1);
63
+ }
39
64
  return;
40
65
  }
41
- // Regular character input (append)
66
+ // Regular character input (insert at cursor)
42
67
  if (input &&
43
68
  !key.ctrl &&
44
69
  !key.meta &&
45
70
  !key.escape &&
46
71
  input !== "\r" &&
47
72
  input !== "\n") {
48
- onChange(value + input);
73
+ const newValue = value.slice(0, cursorPosition) + input + value.slice(cursorPosition);
74
+ onChange(newValue);
75
+ setCursorPosition(cursorPosition + 1);
49
76
  }
50
77
  }, { isActive: isFocused });
51
78
  // Normalize any carriage returns that might already be present in value
52
79
  const normalizedValue = value.replace(/\r\n?/g, "\n");
53
80
  const hasContent = normalizedValue.length > 0;
54
81
  const lines = hasContent ? normalizedValue.split("\n") : [placeholder];
82
+ // Calculate which line and position the cursor is on
83
+ 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);
85
+ const cursorPositionInLine = cursorPosition - cursorLineStart;
55
86
  return (React.createElement(Box, { flexDirection: "column" }, lines.map((line, index) => {
56
- const isLastLine = index === lines.length - 1;
57
- const showCursor = isFocused && isLastLine;
87
+ const isCursorLine = isFocused && index === cursorLineIndex && hasContent;
58
88
  const isPlaceholder = !hasContent;
59
89
  const lineHasContent = line.length > 0;
60
- const displayText = lineHasContent ? line : showCursor ? "" : " ";
90
+ const displayText = lineHasContent ? line : isCursorLine ? "" : " ";
91
+ if (isCursorLine) {
92
+ // Split the line at cursor position
93
+ const beforeCursor = line.slice(0, cursorPositionInLine);
94
+ const afterCursor = line.slice(cursorPositionInLine);
95
+ return (React.createElement(Text, { key: index },
96
+ beforeCursor,
97
+ React.createElement(Text, { color: theme.colors.focused, dimColor: true }, "\u258C"),
98
+ afterCursor));
99
+ }
61
100
  return (React.createElement(Text, { key: index, dimColor: isPlaceholder },
62
101
  displayText,
63
- showCursor && (React.createElement(Text, { color: theme.colors.focused, dimColor: true }, "\u258C"))));
102
+ isFocused && index === lines.length - 1 && !hasContent && (React.createElement(Text, { color: theme.colors.focused, dimColor: true }, "\u258C"))));
64
103
  })));
65
104
  };
@@ -22,6 +22,14 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
22
22
  useInput((input, key) => {
23
23
  if (!isFocused)
24
24
  return;
25
+ // When custom input is focused, only handle escape to exit, let MultiLineTextInput handle other keys
26
+ if (isCustomInputFocused) {
27
+ if (key.escape) {
28
+ // Escape: Exit custom input mode and go back to option navigation
29
+ setFocusedIndex(options.length - 1); // Focus on last option
30
+ }
31
+ return;
32
+ }
25
33
  if (key.upArrow) {
26
34
  setFocusedIndex((prev) => Math.max(0, prev - 1));
27
35
  }
@@ -7,7 +7,7 @@ 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 = "", onChangeCustomAnswer, onSelectOption, questions, selectedOption, onAdvanceToNext, answers, onToggleOption, multiSelect, }) => {
10
+ export const QuestionDisplay = ({ currentQuestion, currentQuestionIndex, customAnswer = "", elapsedLabel, onChangeCustomAnswer, onSelectOption, questions, selectedOption, onAdvanceToNext, answers, onToggleOption, multiSelect, }) => {
11
11
  // Track focus context for Footer component
12
12
  const [focusContext, setFocusContext] = useState("option");
13
13
  // Handle option selection - clears custom answer only in single-select mode
@@ -26,11 +26,15 @@ export const QuestionDisplay = ({ currentQuestion, currentQuestionIndex, customA
26
26
  };
27
27
  return (React.createElement(Box, { flexDirection: "column" },
28
28
  React.createElement(TabBar, { currentIndex: currentQuestionIndex, questions: questions, answers: answers }),
29
- React.createElement(Box, { marginTop: 1 },
30
- React.createElement(Text, { bold: true },
31
- currentQuestion.prompt,
32
- " "),
33
- React.createElement(Text, { dimColor: true }, multiSelect ? "[Multiple Choice]" : "[Single Choice]")),
29
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", marginTop: 1 },
30
+ React.createElement(Box, null,
31
+ React.createElement(Text, { bold: true },
32
+ currentQuestion.prompt,
33
+ " "),
34
+ React.createElement(Text, { dimColor: true }, multiSelect ? "[Multiple Choice]" : "[Single Choice]")),
35
+ React.createElement(Text, { dimColor: true },
36
+ "Elapsed ",
37
+ elapsedLabel)),
34
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 }),
35
39
  React.createElement(Footer, { focusContext: focusContext, multiSelect: multiSelect ?? false, customInputValue: customAnswer })));
36
40
  };
@@ -6,7 +6,7 @@ import { Footer } from "./Footer.js";
6
6
  * ReviewScreen displays a summary of all answers for confirmation
7
7
  * User can press Enter to confirm and submit, or 'n' to go back and edit
8
8
  */
9
- export const ReviewScreen = ({ answers, onConfirm, onGoBack, questions, }) => {
9
+ export const ReviewScreen = ({ answers, elapsedLabel, onConfirm, onGoBack, questions, }) => {
10
10
  useInput((input, key) => {
11
11
  if (key.return) {
12
12
  // Convert answers to UserAnswer format
@@ -32,7 +32,11 @@ export const ReviewScreen = ({ answers, onConfirm, onGoBack, questions, }) => {
32
32
  });
33
33
  return (React.createElement(Box, { flexDirection: "column" },
34
34
  React.createElement(Box, { borderColor: theme.components.review.border, borderStyle: "single", marginBottom: 1, padding: 0.5 },
35
- React.createElement(Text, { bold: true, color: theme.components.review.border }, "Review Your Answers")),
35
+ React.createElement(Box, { flexDirection: "row", justifyContent: "space-between", width: "100%" },
36
+ React.createElement(Text, { bold: true, color: theme.components.review.border }, "Review Your Answers"),
37
+ React.createElement(Text, { dimColor: true },
38
+ "Elapsed ",
39
+ elapsedLabel))),
36
40
  React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, questions.map((question, index) => {
37
41
  const answer = answers.get(index);
38
42
  const questionTitle = question.title || `Q${index + 1}`;
@@ -1,5 +1,5 @@
1
1
  import { Box, Text, useApp, useInput } from "ink";
2
- import React, { useEffect, useRef, useState } from "react";
2
+ import React, { useEffect, useMemo, useRef, useState } from "react";
3
3
  import { SessionManager } from "../../session/SessionManager.js";
4
4
  import { getSessionDirectory } from "../../session/utils.js";
5
5
  import { theme } from "../theme.js";
@@ -17,7 +17,20 @@ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
17
17
  const [showReview, setShowReview] = useState(false);
18
18
  const [submitting, setSubmitting] = useState(false);
19
19
  const [showRejectionConfirm, setShowRejectionConfirm] = useState(false);
20
+ const [elapsedSeconds, setElapsedSeconds] = useState(0);
20
21
  const currentQuestion = sessionRequest.questions[currentQuestionIndex];
22
+ const sessionCreatedAt = useMemo(() => {
23
+ const parsed = Date.parse(sessionRequest.timestamp);
24
+ return Number.isNaN(parsed) ? Date.now() : parsed;
25
+ }, [sessionRequest.timestamp]);
26
+ const elapsedLabel = useMemo(() => {
27
+ const hours = Math.floor(elapsedSeconds / 3600);
28
+ const minutes = Math.floor((elapsedSeconds % 3600) / 60);
29
+ const seconds = elapsedSeconds % 60;
30
+ return [hours, minutes, seconds]
31
+ .map((value) => value.toString().padStart(2, "0"))
32
+ .join(":");
33
+ }, [elapsedSeconds]);
21
34
  // Handle option selection
22
35
  const handleSelectOption = (label) => {
23
36
  setAnswers((prev) => {
@@ -73,7 +86,16 @@ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
73
86
  setShowReview(false);
74
87
  setSubmitting(false);
75
88
  setShowRejectionConfirm(false);
89
+ setElapsedSeconds(0);
76
90
  }, [sessionId]);
91
+ // Update elapsed time since session creation
92
+ useEffect(() => {
93
+ const timer = setInterval(() => {
94
+ const elapsed = Math.floor((Date.now() - sessionCreatedAt) / 1000);
95
+ setElapsedSeconds(elapsed >= 0 ? elapsed : 0);
96
+ }, 1000);
97
+ return () => clearInterval(timer);
98
+ }, [sessionCreatedAt]);
77
99
  // Handle answer confirmation
78
100
  const handleConfirm = async (userAnswers) => {
79
101
  setSubmitting(true);
@@ -164,8 +186,8 @@ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
164
186
  }
165
187
  // Show review screen
166
188
  if (showReview) {
167
- return (React.createElement(ReviewScreen, { answers: answers, onConfirm: handleConfirm, onGoBack: handleGoBack, questions: sessionRequest.questions, sessionId: sessionId }));
189
+ return (React.createElement(ReviewScreen, { answers: answers, elapsedLabel: elapsedLabel, onConfirm: handleConfirm, onGoBack: handleGoBack, questions: sessionRequest.questions, sessionId: sessionId }));
168
190
  }
169
191
  // Show question display (default)
170
- return (React.createElement(QuestionDisplay, { currentQuestion: currentQuestion, currentQuestionIndex: currentQuestionIndex, customAnswer: currentAnswer?.customText, onAdvanceToNext: handleAdvanceToNext, onChangeCustomAnswer: handleChangeCustomAnswer, onSelectOption: handleSelectOption, onToggleOption: handleToggleOption, multiSelect: currentQuestion.multiSelect, questions: sessionRequest.questions, selectedOption: currentAnswer?.selectedOption, answers: answers }));
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 }));
171
193
  };
@@ -1,20 +1,47 @@
1
- import { Box, Text } from "ink";
2
- import React from "react";
1
+ import { Box, Text, useInput } from "ink";
2
+ import React, { useEffect, useState } from "react";
3
3
  import { AnimatedGradient } from "./AnimatedGradient.js";
4
4
  /**
5
5
  * WaitingScreen displays when no question sets are being processed
6
6
  * Shows "Waiting for AI..." message or queue status
7
7
  */
8
8
  export const WaitingScreen = ({ queueCount }) => {
9
+ const [startTime] = useState(new Date());
10
+ const [elapsedSeconds, setElapsedSeconds] = useState(0);
11
+ // Update elapsed time every second
12
+ useEffect(() => {
13
+ const timer = setInterval(() => {
14
+ const now = new Date();
15
+ const elapsed = Math.floor((now.getTime() - startTime.getTime()) / 1000);
16
+ setElapsedSeconds(elapsed);
17
+ }, 1000);
18
+ return () => clearInterval(timer);
19
+ }, [startTime]);
20
+ // Handle 'q' key to quit
21
+ useInput((input, key) => {
22
+ if (input === 'q') {
23
+ process.exit(0);
24
+ }
25
+ });
9
26
  if (queueCount === 0) {
10
27
  return (React.createElement(Box, { flexDirection: "column" },
11
28
  React.createElement(Box, { justifyContent: "center", paddingY: 1 },
12
29
  React.createElement(AnimatedGradient, { text: "Waiting for AI to ask questions\u2026" })),
13
30
  React.createElement(Box, { justifyContent: "center", paddingY: 1 },
14
- React.createElement(Text, { dimColor: true }, "Press q to quit"))));
31
+ React.createElement(Text, { dimColor: true }, "Press Ctrl+C to quit")),
32
+ React.createElement(Box, { justifyContent: "center", paddingY: 1 },
33
+ React.createElement(Text, { dimColor: true },
34
+ "Time elapsed: ",
35
+ elapsedSeconds,
36
+ "s"))));
15
37
  }
16
38
  return (React.createElement(Box, { flexDirection: "column" },
17
39
  React.createElement(AnimatedGradient, { text: `Processing question set... (${queueCount} remaining in queue)` }),
18
40
  React.createElement(Box, { justifyContent: "center", paddingY: 1 },
19
- React.createElement(Text, { dimColor: true }, "Press q to quit"))));
41
+ React.createElement(Text, { dimColor: true }, "Press Ctrl+C to quit")),
42
+ React.createElement(Box, { justifyContent: "center", paddingY: 1 },
43
+ React.createElement(Text, { dimColor: true },
44
+ "Time elapsed: ",
45
+ elapsedSeconds,
46
+ "s"))));
20
47
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "0.1.10",
3
+ "version": "1.2.5",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "dist/bin/auq.js"
@@ -11,14 +11,19 @@
11
11
  "README.md",
12
12
  "LICENSE"
13
13
  ],
14
+ "workspaces": [
15
+ "packages/opencode-plugin"
16
+ ],
14
17
  "scripts": {
15
- "build": "tsc",
18
+ "build": "npm run sync-schemas && tsc",
16
19
  "prepare": "npm run build",
17
20
  "postinstall": "node scripts/postinstall.cjs",
21
+ "deploy": "node scripts/deploy.mjs",
18
22
  "server": "node dist/src/server.js",
19
23
  "start": "tsx src/server.ts",
20
24
  "dev": "fastmcp dev src/server.ts",
21
25
  "lint": "prettier --check . && eslint . && tsc --noEmit",
26
+ "sync-schemas": "node scripts/sync-schemas.mjs",
22
27
  "test": "vitest run",
23
28
  "format": "prettier --write . && eslint --fix ."
24
29
  },
@@ -39,6 +44,7 @@
39
44
  "description": "An MCP server that provides a tool to ask a user questions via the terminal",
40
45
  "dependencies": {
41
46
  "@inkjs/ui": "^2.0.0",
47
+ "@modelcontextprotocol/sdk": "1.17.2",
42
48
  "@types/uuid": "^10.0.0",
43
49
  "chalk": "^5.6.2",
44
50
  "fastmcp": "^3.23.0",
@@ -48,8 +54,7 @@
48
54
  "ink-text-input": "^6.0.0",
49
55
  "react": "^19.2.0",
50
56
  "uuid": "^13.0.0",
51
- "zod": "^4.1.13",
52
- "@modelcontextprotocol/sdk": "1.17.2"
57
+ "zod": "^4.1.13"
53
58
  },
54
59
  "release": {
55
60
  "branches": [
@@ -65,6 +70,7 @@
65
70
  "devDependencies": {
66
71
  "@eslint/js": "^9.26.0",
67
72
  "@tsconfig/node22": "^22.0.1",
73
+ "@types/node": "^22.13.0",
68
74
  "@types/react": "^19.2.2",
69
75
  "eslint-config-prettier": "^10.1.3",
70
76
  "eslint-plugin-perfectionist": "^4.12.3",
@@ -1,137 +0,0 @@
1
- /**
2
- * Minimal schema validation tests for Question/Option types
3
- * Tests the most common edge cases to catch obvious bugs
4
- */
5
- import { describe, expect, it } from "vitest";
6
- import { z } from "zod";
7
- // Import schemas from server (in real implementation, might extract to validation module)
8
- const OptionSchema = z.object({
9
- description: z.string().optional(),
10
- label: z.string(),
11
- });
12
- const QuestionSchema = z.object({
13
- title: z.string(),
14
- options: z.array(OptionSchema).min(1),
15
- prompt: z.string(),
16
- });
17
- const QuestionsArraySchema = z.array(QuestionSchema).min(1);
18
- describe("Schema Validation - Edge Cases", () => {
19
- describe("Invalid Input (should reject)", () => {
20
- it("should reject missing title field", () => {
21
- const invalidQuestion = {
22
- // title missing
23
- options: [{ label: "Option 1" }],
24
- prompt: "Test question?",
25
- };
26
- expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
27
- });
28
- it("should reject empty options array", () => {
29
- const invalidQuestion = {
30
- title: "Test",
31
- options: [], // Empty array
32
- prompt: "Test question?",
33
- };
34
- expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
35
- });
36
- it("should reject missing prompt", () => {
37
- const invalidQuestion = {
38
- title: "Test",
39
- options: [{ label: "Option 1" }],
40
- // prompt missing
41
- };
42
- expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
43
- });
44
- it("should reject missing options field", () => {
45
- const invalidQuestion = {
46
- title: "Test",
47
- // options missing
48
- prompt: "Test question?",
49
- };
50
- expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
51
- });
52
- it("should reject option with missing label", () => {
53
- const invalidQuestion = {
54
- title: "Test",
55
- options: [
56
- {
57
- description: "A description",
58
- // label missing
59
- },
60
- ],
61
- prompt: "Test question?",
62
- };
63
- expect(() => QuestionSchema.parse(invalidQuestion)).toThrow();
64
- });
65
- it("should reject empty questions array", () => {
66
- const invalidQuestions = [];
67
- expect(() => QuestionsArraySchema.parse(invalidQuestions)).toThrow();
68
- });
69
- });
70
- describe("Valid Input (should accept)", () => {
71
- it("should accept valid question with title", () => {
72
- const validQuestion = {
73
- title: "Language",
74
- options: [
75
- {
76
- description: "A helpful description",
77
- label: "Option 1",
78
- },
79
- ],
80
- prompt: "What is your choice?",
81
- };
82
- expect(() => QuestionSchema.parse(validQuestion)).not.toThrow();
83
- const parsed = QuestionSchema.parse(validQuestion);
84
- expect(parsed.title).toBe("Language");
85
- expect(parsed.prompt).toBe("What is your choice?");
86
- expect(parsed.options).toHaveLength(1);
87
- });
88
- it("should accept valid question with all fields", () => {
89
- const validQuestion = {
90
- title: "Framework",
91
- options: [
92
- {
93
- description: "A helpful description",
94
- label: "Option 1",
95
- },
96
- ],
97
- prompt: "What is your choice?",
98
- };
99
- expect(() => QuestionSchema.parse(validQuestion)).not.toThrow();
100
- const parsed = QuestionSchema.parse(validQuestion);
101
- expect(parsed.prompt).toBe("What is your choice?");
102
- expect(parsed.options).toHaveLength(1);
103
- });
104
- it("should accept valid question with description omitted", () => {
105
- const validQuestion = {
106
- title: "Choice",
107
- options: [
108
- {
109
- label: "Option 1",
110
- // description omitted (optional)
111
- },
112
- ],
113
- prompt: "What is your choice?",
114
- };
115
- expect(() => QuestionSchema.parse(validQuestion)).not.toThrow();
116
- const parsed = QuestionSchema.parse(validQuestion);
117
- expect(parsed.options[0].description).toBeUndefined();
118
- });
119
- it("should accept multiple valid questions", () => {
120
- const validQuestions = [
121
- {
122
- title: "First",
123
- options: [{ label: "A" }],
124
- prompt: "Question 1?",
125
- },
126
- {
127
- title: "Second",
128
- options: [{ label: "B" }, { label: "C" }],
129
- prompt: "Question 2?",
130
- },
131
- ];
132
- expect(() => QuestionsArraySchema.parse(validQuestions)).not.toThrow();
133
- const parsed = QuestionsArraySchema.parse(validQuestions);
134
- expect(parsed).toHaveLength(2);
135
- });
136
- });
137
- });