auq-mcp-server 2.0.1 → 2.1.0

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
@@ -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.0.1",
3
+ "version": "2.1.0",
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
+ };
@@ -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, null,
39
- React.createElement(Text, null,
40
- React.createElement(Text, { bold: true },
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
- React.createElement(Text, null, " "),
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, null,
49
- React.createElement(Text, { color: theme.components.review.questionId },
50
- "[",
51
- questionId,
52
- "]",
53
- " "),
54
- React.createElement(Text, { bold: true },
55
- questionTitle,
56
- ". ",
57
- question.prompt)),
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 },
@@ -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
+ });
@@ -93,5 +93,10 @@ export const catppuccinLatteTheme = {
93
93
  info: "#1e66f5",
94
94
  border: "#ccd0da",
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#eff1f5",
98
+ codeBlockText: "#4c4f69",
99
+ codeBlockBorder: "#ccd0da",
100
+ },
96
101
  },
97
102
  };
@@ -93,5 +93,10 @@ export const catppuccinMochaTheme = {
93
93
  info: "#89b4fa",
94
94
  border: "#313244",
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#1e1e2e",
98
+ codeBlockText: "#cdd6f4",
99
+ codeBlockBorder: "#313244",
100
+ },
96
101
  },
97
102
  };
@@ -94,5 +94,10 @@ export const darkTheme = {
94
94
  info: "#46D9FF",
95
95
  border: "#2A3238",
96
96
  },
97
+ markdown: {
98
+ codeBlockBg: "#0F1419",
99
+ codeBlockText: "#E7EEF5",
100
+ codeBlockBorder: "#2A3238",
101
+ },
97
102
  },
98
103
  };
@@ -93,5 +93,10 @@ export const draculaTheme = {
93
93
  info: "#bd93f9",
94
94
  border: "#44475a",
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#282a36",
98
+ codeBlockText: "#f8f8f2",
99
+ codeBlockBorder: "#44475a",
100
+ },
96
101
  },
97
102
  };
@@ -92,5 +92,10 @@ export const githubDarkTheme = {
92
92
  info: "#58a6ff",
93
93
  border: "#30363d",
94
94
  },
95
+ markdown: {
96
+ codeBlockBg: "#0d1117",
97
+ codeBlockText: "#c9d1d9",
98
+ codeBlockBorder: "#30363d",
99
+ },
95
100
  },
96
101
  };
@@ -92,5 +92,10 @@ export const githubLightTheme = {
92
92
  info: "#0969DA",
93
93
  border: "#D0D7DE",
94
94
  },
95
+ markdown: {
96
+ codeBlockBg: "#f6f8fa",
97
+ codeBlockText: "#24292F",
98
+ codeBlockBorder: "#D0D7DE",
99
+ },
95
100
  },
96
101
  };
@@ -93,5 +93,10 @@ export const gruvboxDarkTheme = {
93
93
  info: "#458588",
94
94
  border: "#3c3836",
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#282828",
98
+ codeBlockText: "#ebdbb2",
99
+ codeBlockBorder: "#3c3836",
100
+ },
96
101
  },
97
102
  };
@@ -93,5 +93,10 @@ export const gruvboxLightTheme = {
93
93
  info: "#076678",
94
94
  border: "#d5c4a1",
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#f9f5d9",
98
+ codeBlockText: "#3c3836",
99
+ codeBlockBorder: "#d5c4a1",
100
+ },
96
101
  },
97
102
  };
@@ -93,5 +93,10 @@ export const lightTheme = {
93
93
  info: "#007EA7",
94
94
  border: "#D0D7DE",
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#F6F8FA",
98
+ codeBlockText: "#24292F",
99
+ codeBlockBorder: "#D0D7DE",
100
+ },
96
101
  },
97
102
  };
@@ -94,5 +94,10 @@ export const monokaiTheme = {
94
94
  info: "#66D9EF",
95
95
  border: "#49483E",
96
96
  },
97
+ markdown: {
98
+ codeBlockBg: "#272822",
99
+ codeBlockText: "#F8F8F2",
100
+ codeBlockBorder: "#49483E",
101
+ },
97
102
  },
98
103
  };
@@ -93,5 +93,10 @@ export const nordTheme = {
93
93
  info: "#88c0d0", // nord8
94
94
  border: "#3b4252", // nord1
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#2e3440",
98
+ codeBlockText: "#eceff4",
99
+ codeBlockBorder: "#3b4252",
100
+ },
96
101
  },
97
102
  };
@@ -93,5 +93,10 @@ export const oneDarkTheme = {
93
93
  info: "#61afef",
94
94
  border: "#3e4451",
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#21252b",
98
+ codeBlockText: "#abb2bf",
99
+ codeBlockBorder: "#3e4451",
100
+ },
96
101
  },
97
102
  };
@@ -94,5 +94,10 @@ export const rosePineTheme = {
94
94
  info: "#9ccfd8",
95
95
  border: "#26233a",
96
96
  },
97
+ markdown: {
98
+ codeBlockBg: "#191724",
99
+ codeBlockText: "#e0def4",
100
+ codeBlockBorder: "#26233a",
101
+ },
97
102
  },
98
103
  };
@@ -93,5 +93,10 @@ export const solarizedDarkTheme = {
93
93
  info: "#268bd2",
94
94
  border: "#073642",
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#002b36",
98
+ codeBlockText: "#839496",
99
+ codeBlockBorder: "#073642",
100
+ },
96
101
  },
97
102
  };
@@ -93,5 +93,10 @@ export const solarizedLightTheme = {
93
93
  info: "#268bd2",
94
94
  border: "#eee8d5",
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#fdf6e3",
98
+ codeBlockText: "#657b83",
99
+ codeBlockBorder: "#eee8d5",
100
+ },
96
101
  },
97
102
  };
@@ -93,5 +93,10 @@ export const tokyoNightTheme = {
93
93
  info: "#7aa2f7",
94
94
  border: "#24283b",
95
95
  },
96
+ markdown: {
97
+ codeBlockBg: "#1a1b26",
98
+ codeBlockText: "#c0caf5",
99
+ codeBlockBorder: "#24283b",
100
+ },
96
101
  },
97
102
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
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",