auq-mcp-server 2.0.1 → 2.1.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 +13 -0
- package/dist/package.json +6 -4
- package/dist/src/tui/components/MarkdownPrompt.js +59 -0
- package/dist/src/tui/components/OptionsList.js +65 -6
- package/dist/src/tui/components/QuestionDisplay.js +7 -8
- package/dist/src/tui/components/ReviewScreen.js +13 -10
- package/dist/src/tui/components/StepperView.js +19 -2
- package/dist/src/tui/components/__tests__/MarkdownPrompt.test.js +96 -0
- package/dist/src/tui/themes/catppuccin-latte.js +5 -0
- package/dist/src/tui/themes/catppuccin-mocha.js +5 -0
- package/dist/src/tui/themes/dark.js +5 -0
- package/dist/src/tui/themes/dracula.js +5 -0
- package/dist/src/tui/themes/github-dark.js +5 -0
- package/dist/src/tui/themes/github-light.js +5 -0
- package/dist/src/tui/themes/gruvbox-dark.js +5 -0
- package/dist/src/tui/themes/gruvbox-light.js +5 -0
- package/dist/src/tui/themes/light.js +5 -0
- package/dist/src/tui/themes/monokai.js +5 -0
- package/dist/src/tui/themes/nord.js +5 -0
- package/dist/src/tui/themes/one-dark.js +5 -0
- package/dist/src/tui/themes/rose-pine.js +5 -0
- package/dist/src/tui/themes/solarized-dark.js +5 -0
- package/dist/src/tui/themes/solarized-light.js +5 -0
- package/dist/src/tui/themes/tokyo-night.js +5 -0
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -62,6 +62,8 @@ First, install the **AUQ CLI**:
|
|
|
62
62
|
bun add -g auq-mcp-server
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
This serves as the 'answering interface' for you, the human.
|
|
66
|
+
|
|
65
67
|
_**Note:** Other package managers (npm, pnpm..) also work but not recommended._
|
|
66
68
|
|
|
67
69
|
<details><summary>Local (Project-specific) Installation</summary>
|
|
@@ -210,6 +212,17 @@ Whenever you need clarification on what you are working on, never guess, and cal
|
|
|
210
212
|
|
|
211
213
|
When the AI asks questions, you'll see them appear in the AUQ TUI. Answer them **at your convenience**.
|
|
212
214
|
|
|
215
|
+
### Markdown rendering in question prompts
|
|
216
|
+
|
|
217
|
+
Question prompts now support **Markdown formatting** in the `prompt` text.
|
|
218
|
+
|
|
219
|
+
- Supported: **bold**, _italic_, ~~strikethrough~~, `inline code`, links, and fenced code blocks (with syntax highlighting)
|
|
220
|
+
- Links render as `text (url)` for broad terminal compatibility
|
|
221
|
+
- Code blocks use theme-aware colors (background/text/border)
|
|
222
|
+
- Always enabled (no configuration needed)
|
|
223
|
+
- Plain text prompts pass through unchanged
|
|
224
|
+
- Graceful fallback: if Markdown parsing fails, the raw text is shown
|
|
225
|
+
|
|
213
226
|
> _Note: AUQ is an unopinionated tool and doesn't include prompts on **HOW** AI should leverage it. It is expected that you do your own prompt engineering to make the most out of it in your own workflows._
|
|
214
227
|
> _I personally enjoy prompting it to ask at least 30 questions repeatedly before action!_
|
|
215
228
|
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auq-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"auq": "dist/bin/auq.js"
|
|
@@ -53,11 +53,14 @@
|
|
|
53
53
|
"@modelcontextprotocol/sdk": "1.17.2",
|
|
54
54
|
"@types/uuid": "^10.0.0",
|
|
55
55
|
"chalk": "^5.6.2",
|
|
56
|
+
"cli-highlight": "^2.1.11",
|
|
56
57
|
"fastmcp": "^3.23.0",
|
|
57
58
|
"gradient-string": "^3.0.0",
|
|
58
59
|
"ink": "^6.4.0",
|
|
59
60
|
"ink-gradient": "^3.0.0",
|
|
61
|
+
"ink-markdown-es": "^1.1.0",
|
|
60
62
|
"ink-text-input": "^6.0.0",
|
|
63
|
+
"marked": "^17.0.3",
|
|
61
64
|
"node-notifier": "^10.0.1",
|
|
62
65
|
"react": "^19.2.0",
|
|
63
66
|
"string-width": "^8.1.1",
|
|
@@ -190,9 +193,7 @@
|
|
|
190
193
|
"package.json",
|
|
191
194
|
"CHANGELOG.md"
|
|
192
195
|
],
|
|
193
|
-
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
|
194
|
-
"authorName": "Paul Park",
|
|
195
|
-
"authorEmail": "2000mageia@gmail.com"
|
|
196
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
|
196
197
|
}
|
|
197
198
|
]
|
|
198
199
|
]
|
|
@@ -208,6 +209,7 @@
|
|
|
208
209
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
209
210
|
"eslint-config-prettier": "^10.1.3",
|
|
210
211
|
"eslint-plugin-perfectionist": "^4.12.3",
|
|
212
|
+
"ink-testing-library": "^4.0.0",
|
|
211
213
|
"prettier": "^3.5.3",
|
|
212
214
|
"semantic-release": "^24.2.3",
|
|
213
215
|
"typescript": "^5.8.3",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import Markdown from "ink-markdown-es";
|
|
4
|
+
import { lexer } from "marked";
|
|
5
|
+
import { useTheme } from "../ThemeContext.js";
|
|
6
|
+
/**
|
|
7
|
+
* MarkdownPrompt renders markdown text with basic formatting support.
|
|
8
|
+
* Falls back to plain text if markdown parsing fails.
|
|
9
|
+
*
|
|
10
|
+
* Supports:
|
|
11
|
+
* - **bold** text
|
|
12
|
+
* - *italic* text
|
|
13
|
+
* - `inline code`
|
|
14
|
+
* - ~~strikethrough~~
|
|
15
|
+
* - [links](url) rendered as "text (url)"
|
|
16
|
+
* - Fenced code blocks with syntax highlighting
|
|
17
|
+
*/
|
|
18
|
+
export const MarkdownPrompt = ({ text }) => {
|
|
19
|
+
const { theme } = useTheme();
|
|
20
|
+
// Fallback to plain text if text is empty
|
|
21
|
+
if (!text || text.trim().length === 0) {
|
|
22
|
+
return React.createElement(Text, null, text);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const tokens = lexer(text);
|
|
26
|
+
const hasBlockElements = tokens.some((token) => ["code", "list", "blockquote", "heading", "hr", "table"].includes(token.type));
|
|
27
|
+
const styles = {
|
|
28
|
+
code: {
|
|
29
|
+
backgroundColor: theme.components.markdown.codeBlockBg,
|
|
30
|
+
color: theme.components.markdown.codeBlockText,
|
|
31
|
+
borderColor: theme.components.markdown.codeBlockBorder,
|
|
32
|
+
borderStyle: "round",
|
|
33
|
+
paddingX: 1,
|
|
34
|
+
},
|
|
35
|
+
codespan: {
|
|
36
|
+
backgroundColor: theme.components.markdown.codeBlockBg,
|
|
37
|
+
color: theme.components.markdown.codeBlockText,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const baseRenderers = {
|
|
41
|
+
link: (linkText, href) => (React.createElement(Text, null,
|
|
42
|
+
linkText,
|
|
43
|
+
" (",
|
|
44
|
+
href,
|
|
45
|
+
")")),
|
|
46
|
+
};
|
|
47
|
+
if (!hasBlockElements) {
|
|
48
|
+
return (React.createElement(Markdown, { styles: styles, renderers: {
|
|
49
|
+
...baseRenderers,
|
|
50
|
+
paragraph: (content) => React.createElement(Text, null, content),
|
|
51
|
+
}, highlight: true }, text));
|
|
52
|
+
}
|
|
53
|
+
return (React.createElement(Markdown, { styles: styles, renderers: baseRenderers, highlight: true }, text));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Silently fall back to plain text on any error
|
|
57
|
+
return React.createElement(Text, null, text);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
@@ -4,7 +4,7 @@ import { t } from "../../i18n/index.js";
|
|
|
4
4
|
import { useConfig } from "../ConfigContext.js";
|
|
5
5
|
import { useTheme } from "../ThemeContext.js";
|
|
6
6
|
import { isRecommendedOption } from "../utils/recommended.js";
|
|
7
|
-
import { fitToVisualWidth } from "../utils/visualWidth.js";
|
|
7
|
+
import { fitToVisualWidth, getVisualWidth, padToVisualWidth, } from "../utils/visualWidth.js";
|
|
8
8
|
import { MultiLineTextInput } from "./MultiLineTextInput.js";
|
|
9
9
|
// isRecommendedOption is imported from ../utils/recommended.js
|
|
10
10
|
/**
|
|
@@ -23,6 +23,35 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
|
|
|
23
23
|
const fitRow = (text) => {
|
|
24
24
|
return fitToVisualWidth(text, rowWidth);
|
|
25
25
|
};
|
|
26
|
+
// Wrap text to multiple lines respecting visual width and explicit newlines
|
|
27
|
+
const wrapText = (text, width) => {
|
|
28
|
+
// First split on explicit newlines, then wrap each segment by visual width
|
|
29
|
+
const segments = text.split("\n");
|
|
30
|
+
const lines = [];
|
|
31
|
+
for (const segment of segments) {
|
|
32
|
+
if (getVisualWidth(segment) <= width) {
|
|
33
|
+
lines.push(segment);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
let currentLine = "";
|
|
37
|
+
let currentWidth = 0;
|
|
38
|
+
for (const char of segment) {
|
|
39
|
+
const charWidth = getVisualWidth(char);
|
|
40
|
+
if (currentWidth + charWidth > width && currentLine.length > 0) {
|
|
41
|
+
lines.push(currentLine);
|
|
42
|
+
currentLine = char;
|
|
43
|
+
currentWidth = charWidth;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
currentLine += char;
|
|
47
|
+
currentWidth += charWidth;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (currentLine)
|
|
51
|
+
lines.push(currentLine);
|
|
52
|
+
}
|
|
53
|
+
return lines;
|
|
54
|
+
};
|
|
26
55
|
// Calculate max index: include custom input and elaborate options if enabled
|
|
27
56
|
// Options: [0..n-1] = regular options, [n] = custom input, [n+1] = elaborate
|
|
28
57
|
const customInputIndex = options.length;
|
|
@@ -141,11 +170,41 @@ export const OptionsList = ({ isFocused, onSelect, options, selectedOption, show
|
|
|
141
170
|
: `${isFocusedOption ? ">" : " "} ${option.label}${isSelected ? " ✓" : ""}${starSuffix}`;
|
|
142
171
|
return (React.createElement(Box, { key: index, flexDirection: "column" },
|
|
143
172
|
React.createElement(Text, { backgroundColor: rowBg, bold: isFocusedOption || isSelected, color: rowColor }, fitRow(mainLine)),
|
|
144
|
-
option.description &&
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
173
|
+
option.description &&
|
|
174
|
+
(() => {
|
|
175
|
+
const descText = ` ${option.description}`;
|
|
176
|
+
const descBg = isFocusedOption
|
|
177
|
+
? theme.components.options.focusedBg
|
|
178
|
+
: isSelected
|
|
179
|
+
? theme.components.options.selectedBg
|
|
180
|
+
: undefined;
|
|
181
|
+
const wouldWrap = descText.includes("\n") ||
|
|
182
|
+
getVisualWidth(descText) > rowWidth;
|
|
183
|
+
if (isFocusedOption && wouldWrap) {
|
|
184
|
+
// Focused + multi-line: show full description wrapped across lines
|
|
185
|
+
const wrappedLines = wrapText(descText, rowWidth);
|
|
186
|
+
return (React.createElement(Box, { flexDirection: "column" }, wrappedLines.map((line, lineIdx) => (React.createElement(Text, { key: lineIdx, backgroundColor: descBg, color: theme.components.options.description, dimColor: false }, padToVisualWidth(line, rowWidth))))));
|
|
187
|
+
}
|
|
188
|
+
else if (!isFocusedOption && wouldWrap) {
|
|
189
|
+
// Not focused + would wrap: truncate to 1 line with "..."
|
|
190
|
+
const maxWidth = rowWidth - 3; // Reserve 3 chars for "..."
|
|
191
|
+
let result = "";
|
|
192
|
+
let width = 0;
|
|
193
|
+
for (const char of descText) {
|
|
194
|
+
const charWidth = getVisualWidth(char);
|
|
195
|
+
if (width + charWidth > maxWidth)
|
|
196
|
+
break;
|
|
197
|
+
result += char;
|
|
198
|
+
width += charWidth;
|
|
199
|
+
}
|
|
200
|
+
const finalText = `${result}...`;
|
|
201
|
+
return (React.createElement(Text, { backgroundColor: descBg, color: theme.components.options.description, dimColor: true }, padToVisualWidth(finalText, rowWidth)));
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
// Fits in 1 line (focused or not): show normally with padding
|
|
205
|
+
return (React.createElement(Text, { backgroundColor: descBg, color: theme.components.options.description, dimColor: !isFocusedOption && !isSelected }, fitRow(descText)));
|
|
206
|
+
}
|
|
207
|
+
})()));
|
|
149
208
|
}),
|
|
150
209
|
showCustomInput && (React.createElement(Box, { marginTop: 0 },
|
|
151
210
|
React.createElement(Box, { flexDirection: "column" },
|
|
@@ -3,6 +3,7 @@ import React, { useState } from "react";
|
|
|
3
3
|
import { t } from "../../i18n/index.js";
|
|
4
4
|
import { useTheme } from "../ThemeContext.js";
|
|
5
5
|
import { Footer } from "./Footer.js";
|
|
6
|
+
import { MarkdownPrompt } from "./MarkdownPrompt.js";
|
|
6
7
|
import { OptionsList } from "./OptionsList.js";
|
|
7
8
|
import { TabBar } from "./TabBar.js";
|
|
8
9
|
/**
|
|
@@ -35,19 +36,17 @@ export const QuestionDisplay = ({ currentQuestion, currentQuestionIndex, customA
|
|
|
35
36
|
React.createElement(Text, { color: theme.components.directory.label, dimColor: true }, "\uD83D\uDCC1"),
|
|
36
37
|
React.createElement(Text, { color: theme.components.directory.path }, ` ${workingDirectory}`))),
|
|
37
38
|
React.createElement(TabBar, { currentIndex: currentQuestionIndex, questions: questions, answers: answers, elaborateMarks: elaborateMarks }),
|
|
38
|
-
React.createElement(Box,
|
|
39
|
-
React.createElement(
|
|
40
|
-
React.createElement(
|
|
41
|
-
" ",
|
|
42
|
-
currentQuestion.prompt,
|
|
43
|
-
" "),
|
|
39
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
40
|
+
React.createElement(Box, null,
|
|
41
|
+
React.createElement(MarkdownPrompt, { text: currentQuestion.prompt }),
|
|
44
42
|
React.createElement(Text, { color: theme.components.questionDisplay.typeIndicator },
|
|
43
|
+
" ",
|
|
45
44
|
"[",
|
|
46
45
|
multiSelect
|
|
47
46
|
? t("question.multipleChoice")
|
|
48
47
|
: t("question.singleChoice"),
|
|
49
|
-
"]"),
|
|
50
|
-
|
|
48
|
+
"]")),
|
|
49
|
+
React.createElement(Box, null,
|
|
51
50
|
React.createElement(Text, { color: theme.components.questionDisplay.elapsed, dimColor: true }, elapsedLabel))),
|
|
52
51
|
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, onRecommendedDetected: onRecommendedDetected, questionKey: currentQuestionIndex, isElaborateMarked: elaborateMarks?.has(currentQuestionIndex), onElaborateSelect: onElaborateSelect, elaborateText: elaborateText, onElaborateTextChange: onElaborateTextChange }),
|
|
53
52
|
React.createElement(Footer, { focusContext: focusContext, multiSelect: multiSelect ?? false, customInputValue: customAnswer, hasRecommendedOptions: hasRecommendedOptions, hasAnyRecommendedInSession: hasAnyRecommendedInSession })));
|
|
@@ -3,6 +3,7 @@ import React from "react";
|
|
|
3
3
|
import { t } from "../../i18n/index.js";
|
|
4
4
|
import { useTheme } from "../ThemeContext.js";
|
|
5
5
|
import { Footer } from "./Footer.js";
|
|
6
|
+
import { MarkdownPrompt } from "./MarkdownPrompt.js";
|
|
6
7
|
/**
|
|
7
8
|
* ReviewScreen displays a summary of all answers for confirmation
|
|
8
9
|
* User can press Enter to confirm and submit, or 'n' to go back and edit
|
|
@@ -45,16 +46,18 @@ export const ReviewScreen = ({ answers, elapsedLabel, onConfirm, onGoBack, quest
|
|
|
45
46
|
const questionId = `Q${index}`;
|
|
46
47
|
const questionTitle = question.title || questionId;
|
|
47
48
|
return (React.createElement(Box, { flexDirection: "column", key: index, marginBottom: 1 },
|
|
48
|
-
React.createElement(Box,
|
|
49
|
-
React.createElement(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
50
|
+
React.createElement(Box, null,
|
|
51
|
+
React.createElement(Text, { color: theme.components.review.questionId },
|
|
52
|
+
"[",
|
|
53
|
+
questionId,
|
|
54
|
+
"]",
|
|
55
|
+
" "),
|
|
56
|
+
React.createElement(Text, { bold: true },
|
|
57
|
+
questionTitle,
|
|
58
|
+
".",
|
|
59
|
+
" ")),
|
|
60
|
+
React.createElement(MarkdownPrompt, { text: question.prompt })),
|
|
58
61
|
React.createElement(Box, { flexDirection: "column", marginLeft: 2, marginTop: 0.5 },
|
|
59
62
|
answer?.selectedOptions &&
|
|
60
63
|
answer.selectedOptions.length > 0 && (React.createElement(React.Fragment, null, answer.selectedOptions.map((option, idx) => (React.createElement(Text, { key: idx, color: theme.components.review.selectedOption },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box, useApp, useInput } from "ink";
|
|
1
|
+
import { Box, useApp, useInput, useStdout } from "ink";
|
|
2
2
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { t } from "../../i18n/index.js";
|
|
4
4
|
import { ResponseFormatter } from "../../session/ResponseFormatter.js";
|
|
@@ -44,6 +44,10 @@ export const StepperView = ({ onComplete, onProgress, sessionId, sessionRequest,
|
|
|
44
44
|
.map((value) => value.toString().padStart(2, "0"))
|
|
45
45
|
.join(":");
|
|
46
46
|
}, [elapsedSeconds]);
|
|
47
|
+
// Detect if content overflows terminal height to pause periodic re-renders
|
|
48
|
+
const { stdout } = useStdout();
|
|
49
|
+
const terminalRows = stdout?.rows ?? 24;
|
|
50
|
+
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
47
51
|
// Report progress when question index changes
|
|
48
52
|
useEffect(() => {
|
|
49
53
|
if (onProgress) {
|
|
@@ -151,13 +155,26 @@ export const StepperView = ({ onComplete, onProgress, sessionId, sessionRequest,
|
|
|
151
155
|
setHasAnyRecommendedInSession(anyHasRecommended);
|
|
152
156
|
}, [sessionId, sessionRequest.questions]);
|
|
153
157
|
// Update elapsed time since session creation
|
|
158
|
+
// IMPORTANT: Pause when content overflows terminal to prevent scroll-snapping
|
|
154
159
|
useEffect(() => {
|
|
160
|
+
if (isOverflowing)
|
|
161
|
+
return;
|
|
155
162
|
const timer = setInterval(() => {
|
|
156
163
|
const elapsed = Math.floor((Date.now() - sessionCreatedAt) / 1000);
|
|
157
164
|
setElapsedSeconds(elapsed >= 0 ? elapsed : 0);
|
|
158
165
|
}, 1000);
|
|
159
166
|
return () => clearInterval(timer);
|
|
160
|
-
}, [sessionCreatedAt]);
|
|
167
|
+
}, [sessionCreatedAt, isOverflowing]);
|
|
168
|
+
// Detect overflow: estimate content height vs terminal rows
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const currentQ = sessionRequest.questions[safeIndex];
|
|
171
|
+
const optionCount = currentQ?.options?.length ?? 0;
|
|
172
|
+
// Conservative estimate: header(2) + tabbar(3) + prompt(3) + options(2 each)
|
|
173
|
+
// + footer(2) + custom/elaborate(6) + padding(2)
|
|
174
|
+
const estimatedContentHeight = 2 + 3 + 3 + optionCount * 2 + 2 + 6 + 2;
|
|
175
|
+
const nextOverflow = estimatedContentHeight > terminalRows;
|
|
176
|
+
setIsOverflowing((prev) => (prev === nextOverflow ? prev : nextOverflow));
|
|
177
|
+
}, [safeIndex, sessionRequest.questions, terminalRows]);
|
|
161
178
|
// Handle answer confirmation
|
|
162
179
|
const handleConfirm = async (userAnswers) => {
|
|
163
180
|
setSubmitting(true);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { cleanup, render } from "ink-testing-library";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { ThemeContext } from "../../ThemeContext.js";
|
|
5
|
+
import { darkTheme } from "../../themes/dark.js";
|
|
6
|
+
import { MarkdownPrompt } from "../MarkdownPrompt.js";
|
|
7
|
+
const mockThemeValue = {
|
|
8
|
+
theme: darkTheme,
|
|
9
|
+
themeName: "AUQ dark",
|
|
10
|
+
cycleTheme: () => { },
|
|
11
|
+
};
|
|
12
|
+
function renderWithTheme(ui) {
|
|
13
|
+
return render(React.createElement(ThemeContext.Provider, { value: mockThemeValue }, ui));
|
|
14
|
+
}
|
|
15
|
+
function getOutput(frame) {
|
|
16
|
+
return (frame ?? "").replace(/\x1b\[[0-9;]*m/g, "").replace(/\r/g, "");
|
|
17
|
+
}
|
|
18
|
+
function compact(text) {
|
|
19
|
+
return text.replace(/\s+/g, " ").trim();
|
|
20
|
+
}
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
cleanup();
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
describe("MarkdownPrompt", () => {
|
|
26
|
+
it("renders plain text unchanged", () => {
|
|
27
|
+
const text = "Choose one option";
|
|
28
|
+
const instance = renderWithTheme(React.createElement(MarkdownPrompt, { text: text }));
|
|
29
|
+
expect(compact(getOutput(instance.lastFrame()))).toBe(text);
|
|
30
|
+
});
|
|
31
|
+
it("renders core inline markdown without raw markers", () => {
|
|
32
|
+
const text = "**bold** *italic* `code` ~~strike~~";
|
|
33
|
+
const instance = renderWithTheme(React.createElement(MarkdownPrompt, { text: text }));
|
|
34
|
+
const output = getOutput(instance.lastFrame());
|
|
35
|
+
expect(output).toContain("bold");
|
|
36
|
+
expect(output).toContain("italic");
|
|
37
|
+
expect(output).toContain("code");
|
|
38
|
+
expect(output).toContain("strike");
|
|
39
|
+
expect(output).not.toContain("**");
|
|
40
|
+
expect(output).not.toContain("~~");
|
|
41
|
+
expect(output).not.toContain("`code`");
|
|
42
|
+
});
|
|
43
|
+
it("keeps inline-only markdown in inline flow without extra block spacing", () => {
|
|
44
|
+
const text = "Hello **there** with `inline` markdown";
|
|
45
|
+
const instance = renderWithTheme(React.createElement(MarkdownPrompt, { text: text }));
|
|
46
|
+
const output = getOutput(instance.lastFrame());
|
|
47
|
+
expect(output).not.toContain("\n\n");
|
|
48
|
+
expect(compact(output)).toContain("Hello there with inline markdown");
|
|
49
|
+
});
|
|
50
|
+
it("renders links as text followed by URL in parentheses", () => {
|
|
51
|
+
const text = "Read [docs](https://example.com/docs) now";
|
|
52
|
+
const instance = renderWithTheme(React.createElement(MarkdownPrompt, { text: text }));
|
|
53
|
+
expect(compact(getOutput(instance.lastFrame()))).toContain("Read docs (https://example.com/docs) now");
|
|
54
|
+
});
|
|
55
|
+
it("renders fenced code blocks as distinct multi-line block content", () => {
|
|
56
|
+
const text = "Before\n\n```ts\nconst x = 1;\n```\n\nAfter";
|
|
57
|
+
const instance = renderWithTheme(React.createElement(MarkdownPrompt, { text: text }));
|
|
58
|
+
const output = getOutput(instance.lastFrame());
|
|
59
|
+
expect(output).toContain("Before");
|
|
60
|
+
expect(output).toContain("const x = 1;");
|
|
61
|
+
expect(output).toContain("After");
|
|
62
|
+
expect(output.split("\n").length).toBeGreaterThan(2);
|
|
63
|
+
});
|
|
64
|
+
it("handles empty and whitespace-only inputs without crashing", () => {
|
|
65
|
+
const empty = renderWithTheme(React.createElement(MarkdownPrompt, { text: "" }));
|
|
66
|
+
const spaces = renderWithTheme(React.createElement(MarkdownPrompt, { text: " " }));
|
|
67
|
+
expect(getOutput(empty.lastFrame())).toBe("");
|
|
68
|
+
expect(getOutput(spaces.lastFrame())).toBe("");
|
|
69
|
+
});
|
|
70
|
+
it("normalizes CRLF content during rendering", () => {
|
|
71
|
+
const text = "Line1\r\nLine2\r\n**Bold**";
|
|
72
|
+
const instance = renderWithTheme(React.createElement(MarkdownPrompt, { text: text }));
|
|
73
|
+
const output = getOutput(instance.lastFrame());
|
|
74
|
+
expect(output).not.toContain("\r");
|
|
75
|
+
expect(output).toContain("Line1");
|
|
76
|
+
expect(output).toContain("Line2");
|
|
77
|
+
expect(output).toContain("Bold");
|
|
78
|
+
});
|
|
79
|
+
it("renders nested inline markdown content", () => {
|
|
80
|
+
const text = "**bold and *italic* with `code`**";
|
|
81
|
+
const instance = renderWithTheme(React.createElement(MarkdownPrompt, { text: text }));
|
|
82
|
+
const output = getOutput(instance.lastFrame());
|
|
83
|
+
expect(output).toContain("bold and");
|
|
84
|
+
expect(output).toContain("italic");
|
|
85
|
+
expect(output).toContain("code");
|
|
86
|
+
expect(output).not.toContain("**");
|
|
87
|
+
expect(output).not.toContain("`code`");
|
|
88
|
+
});
|
|
89
|
+
it("does not crash on malformed markdown", () => {
|
|
90
|
+
const malformed = "Broken [link](https://example.com and **mixed";
|
|
91
|
+
expect(() => renderWithTheme(React.createElement(MarkdownPrompt, { text: malformed }))).not.toThrow();
|
|
92
|
+
const instance = renderWithTheme(React.createElement(MarkdownPrompt, { text: malformed }));
|
|
93
|
+
const output = getOutput(instance.lastFrame());
|
|
94
|
+
expect(output.length).toBeGreaterThan(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auq-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"auq": "dist/bin/auq.js"
|
|
@@ -53,11 +53,14 @@
|
|
|
53
53
|
"@modelcontextprotocol/sdk": "1.17.2",
|
|
54
54
|
"@types/uuid": "^10.0.0",
|
|
55
55
|
"chalk": "^5.6.2",
|
|
56
|
+
"cli-highlight": "^2.1.11",
|
|
56
57
|
"fastmcp": "^3.23.0",
|
|
57
58
|
"gradient-string": "^3.0.0",
|
|
58
59
|
"ink": "^6.4.0",
|
|
59
60
|
"ink-gradient": "^3.0.0",
|
|
61
|
+
"ink-markdown-es": "^1.1.0",
|
|
60
62
|
"ink-text-input": "^6.0.0",
|
|
63
|
+
"marked": "^17.0.3",
|
|
61
64
|
"node-notifier": "^10.0.1",
|
|
62
65
|
"react": "^19.2.0",
|
|
63
66
|
"string-width": "^8.1.1",
|
|
@@ -190,9 +193,7 @@
|
|
|
190
193
|
"package.json",
|
|
191
194
|
"CHANGELOG.md"
|
|
192
195
|
],
|
|
193
|
-
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
|
194
|
-
"authorName": "Paul Park",
|
|
195
|
-
"authorEmail": "2000mageia@gmail.com"
|
|
196
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
|
196
197
|
}
|
|
197
198
|
]
|
|
198
199
|
]
|
|
@@ -208,6 +209,7 @@
|
|
|
208
209
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
209
210
|
"eslint-config-prettier": "^10.1.3",
|
|
210
211
|
"eslint-plugin-perfectionist": "^4.12.3",
|
|
212
|
+
"ink-testing-library": "^4.0.0",
|
|
211
213
|
"prettier": "^3.5.3",
|
|
212
214
|
"semantic-release": "^24.2.3",
|
|
213
215
|
"typescript": "^5.8.3",
|