auq-mcp-server 0.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.
Files changed (66) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +176 -0
  3. package/dist/__tests__/schema-validation.test.js +137 -0
  4. package/dist/__tests__/server.integration.test.js +263 -0
  5. package/dist/add.js +1 -0
  6. package/dist/add.test.js +5 -0
  7. package/dist/bin/auq.js +245 -0
  8. package/dist/bin/test-session-menu.js +28 -0
  9. package/dist/bin/test-tabbar.js +42 -0
  10. package/dist/file-utils.js +59 -0
  11. package/dist/format/ResponseFormatter.js +206 -0
  12. package/dist/format/__tests__/ResponseFormatter.test.js +380 -0
  13. package/dist/package.json +74 -0
  14. package/dist/server.js +107 -0
  15. package/dist/session/ResponseFormatter.js +130 -0
  16. package/dist/session/SessionManager.js +474 -0
  17. package/dist/session/__tests__/ResponseFormatter.test.js +417 -0
  18. package/dist/session/__tests__/SessionManager.test.js +553 -0
  19. package/dist/session/__tests__/atomic-operations.test.js +345 -0
  20. package/dist/session/__tests__/file-watcher.test.js +311 -0
  21. package/dist/session/__tests__/workflow.integration.test.js +334 -0
  22. package/dist/session/atomic-operations.js +307 -0
  23. package/dist/session/file-watcher.js +218 -0
  24. package/dist/session/index.js +7 -0
  25. package/dist/session/types.js +20 -0
  26. package/dist/session/utils.js +125 -0
  27. package/dist/session-manager.js +171 -0
  28. package/dist/session-watcher.js +110 -0
  29. package/dist/src/__tests__/schema-validation.test.js +170 -0
  30. package/dist/src/__tests__/server.integration.test.js +274 -0
  31. package/dist/src/add.js +1 -0
  32. package/dist/src/add.test.js +5 -0
  33. package/dist/src/server.js +163 -0
  34. package/dist/src/session/ResponseFormatter.js +163 -0
  35. package/dist/src/session/SessionManager.js +572 -0
  36. package/dist/src/session/__tests__/ResponseFormatter.test.js +741 -0
  37. package/dist/src/session/__tests__/SessionManager.test.js +593 -0
  38. package/dist/src/session/__tests__/atomic-operations.test.js +346 -0
  39. package/dist/src/session/__tests__/file-watcher.test.js +311 -0
  40. package/dist/src/session/atomic-operations.js +307 -0
  41. package/dist/src/session/file-watcher.js +227 -0
  42. package/dist/src/session/index.js +7 -0
  43. package/dist/src/session/types.js +20 -0
  44. package/dist/src/session/utils.js +180 -0
  45. package/dist/src/tui/__tests__/session-watcher.test.js +368 -0
  46. package/dist/src/tui/components/AnimatedGradient.js +45 -0
  47. package/dist/src/tui/components/ConfirmationDialog.js +89 -0
  48. package/dist/src/tui/components/CustomInput.js +14 -0
  49. package/dist/src/tui/components/Footer.js +55 -0
  50. package/dist/src/tui/components/Header.js +35 -0
  51. package/dist/src/tui/components/MultiLineTextInput.js +65 -0
  52. package/dist/src/tui/components/OptionsList.js +115 -0
  53. package/dist/src/tui/components/QuestionDisplay.js +36 -0
  54. package/dist/src/tui/components/ReviewScreen.js +57 -0
  55. package/dist/src/tui/components/SessionSelectionMenu.js +151 -0
  56. package/dist/src/tui/components/StepperView.js +166 -0
  57. package/dist/src/tui/components/TabBar.js +42 -0
  58. package/dist/src/tui/components/Toast.js +19 -0
  59. package/dist/src/tui/components/WaitingScreen.js +20 -0
  60. package/dist/src/tui/session-watcher.js +195 -0
  61. package/dist/src/tui/theme.js +114 -0
  62. package/dist/src/tui/utils/gradientText.js +24 -0
  63. package/dist/tui/__tests__/session-watcher.test.js +368 -0
  64. package/dist/tui/session-watcher.js +183 -0
  65. package/package.json +78 -0
  66. package/scripts/postinstall.cjs +51 -0
@@ -0,0 +1,166 @@
1
+ import { Box, Text, useApp, useInput } from "ink";
2
+ import React, { useEffect, useRef, useState } from "react";
3
+ import { SessionManager } from "../../session/SessionManager.js";
4
+ import { theme } from "../theme.js";
5
+ import { ConfirmationDialog } from "./ConfirmationDialog.js";
6
+ import { QuestionDisplay } from "./QuestionDisplay.js";
7
+ import { ReviewScreen } from "./ReviewScreen.js";
8
+ /**
9
+ * StepperView orchestrates the question-answering flow
10
+ * Manages state for current question, answers, and navigation
11
+ */
12
+ export const StepperView = ({ onComplete, sessionId, sessionRequest, }) => {
13
+ const { exit } = useApp();
14
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
15
+ const [answers, setAnswers] = useState(new Map());
16
+ const [showReview, setShowReview] = useState(false);
17
+ const [submitting, setSubmitting] = useState(false);
18
+ const [showRejectionConfirm, setShowRejectionConfirm] = useState(false);
19
+ const currentQuestion = sessionRequest.questions[currentQuestionIndex];
20
+ // Handle option selection
21
+ const handleSelectOption = (label) => {
22
+ setAnswers((prev) => {
23
+ const newAnswers = new Map(prev);
24
+ const existing = newAnswers.get(currentQuestionIndex) || {};
25
+ newAnswers.set(currentQuestionIndex, {
26
+ ...existing,
27
+ selectedOption: label,
28
+ });
29
+ return newAnswers;
30
+ });
31
+ };
32
+ const handleToggleOption = (label) => {
33
+ setAnswers((prev) => {
34
+ const newAnswers = new Map(prev);
35
+ const existing = newAnswers.get(currentQuestionIndex) || {};
36
+ const currentSelections = existing.selectedOptions || [];
37
+ const newSelections = currentSelections.includes(label)
38
+ ? currentSelections.filter((l) => l !== label) // Remove if already selected
39
+ : [...currentSelections, label]; // Add if not selected
40
+ newAnswers.set(currentQuestionIndex, {
41
+ selectedOptions: newSelections,
42
+ // Keep customText in multi-select mode (allow both)
43
+ customText: existing.customText,
44
+ });
45
+ return newAnswers;
46
+ });
47
+ };
48
+ // Handle custom answer text
49
+ const handleChangeCustomAnswer = (text) => {
50
+ setAnswers((prev) => {
51
+ const newAnswers = new Map(prev);
52
+ const existing = newAnswers.get(currentQuestionIndex) || {};
53
+ newAnswers.set(currentQuestionIndex, {
54
+ ...existing,
55
+ customText: text,
56
+ });
57
+ return newAnswers;
58
+ });
59
+ };
60
+ // Track mount status to avoid state updates after unmount
61
+ const isMountedRef = useRef(true);
62
+ useEffect(() => {
63
+ isMountedRef.current = true;
64
+ return () => {
65
+ isMountedRef.current = false;
66
+ };
67
+ }, []);
68
+ // Reset internal stepper state when the session changes (safety in case component isn't remounted)
69
+ useEffect(() => {
70
+ setCurrentQuestionIndex(0);
71
+ setAnswers(new Map());
72
+ setShowReview(false);
73
+ setSubmitting(false);
74
+ setShowRejectionConfirm(false);
75
+ }, [sessionId]);
76
+ // Handle answer confirmation
77
+ const handleConfirm = async (userAnswers) => {
78
+ setSubmitting(true);
79
+ try {
80
+ const sessionManager = new SessionManager();
81
+ await sessionManager.saveSessionAnswers(sessionId, {
82
+ answers: userAnswers,
83
+ sessionId,
84
+ timestamp: new Date().toISOString(),
85
+ callId: sessionRequest.callId,
86
+ });
87
+ // Signal completion (successful submission)
88
+ onComplete?.(false);
89
+ }
90
+ catch (error) {
91
+ console.error("Failed to save answers:", error);
92
+ }
93
+ finally {
94
+ if (isMountedRef.current) {
95
+ setSubmitting(false);
96
+ }
97
+ }
98
+ };
99
+ // Handle going back from review
100
+ const handleGoBack = () => {
101
+ setShowReview(false);
102
+ };
103
+ // Handle advance to next question or review
104
+ const handleAdvanceToNext = () => {
105
+ if (currentQuestionIndex < sessionRequest.questions.length - 1) {
106
+ // Move to next question
107
+ setCurrentQuestionIndex((prev) => prev + 1);
108
+ }
109
+ else {
110
+ // Last question - show review
111
+ setShowReview(true);
112
+ }
113
+ };
114
+ // Handle session rejection
115
+ const handleRejectSession = async (reason) => {
116
+ try {
117
+ const sessionManager = new SessionManager();
118
+ await sessionManager.rejectSession(sessionId, reason);
119
+ // Call onComplete with rejection flag
120
+ if (onComplete) {
121
+ onComplete(true);
122
+ }
123
+ }
124
+ catch (error) {
125
+ console.error("Failed to reject session:", error);
126
+ setShowRejectionConfirm(false);
127
+ }
128
+ };
129
+ // Global keyboard shortcuts and navigation
130
+ useInput((input, key) => {
131
+ // Don't handle navigation when showing review, submitting, or confirming rejection
132
+ if (showReview || submitting || showRejectionConfirm)
133
+ return;
134
+ // Esc key - show rejection confirmation
135
+ if (key.escape) {
136
+ setShowRejectionConfirm(true);
137
+ return;
138
+ }
139
+ // Question navigation with arrow keys
140
+ if (key.leftArrow && currentQuestionIndex > 0) {
141
+ setCurrentQuestionIndex((prev) => prev - 1);
142
+ }
143
+ if (key.rightArrow &&
144
+ currentQuestionIndex < sessionRequest.questions.length - 1) {
145
+ setCurrentQuestionIndex((prev) => prev + 1);
146
+ }
147
+ });
148
+ const currentAnswer = answers.get(currentQuestionIndex);
149
+ // Show rejection confirmation
150
+ if (showRejectionConfirm) {
151
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
152
+ React.createElement(ConfirmationDialog, { message: "Are you sure you want to reject this question set?", onCancel: () => setShowRejectionConfirm(false), onQuit: () => exit(), onReject: handleRejectSession })));
153
+ }
154
+ // Show submitting message
155
+ if (submitting) {
156
+ return (React.createElement(Box, { flexDirection: "column", padding: 1 },
157
+ React.createElement(Box, { borderColor: theme.colors.pending, borderStyle: "single", padding: 1 },
158
+ React.createElement(Text, { color: theme.colors.pending }, "Submitting answers..."))));
159
+ }
160
+ // Show review screen
161
+ if (showReview) {
162
+ return (React.createElement(ReviewScreen, { answers: answers, onConfirm: handleConfirm, onGoBack: handleGoBack, questions: sessionRequest.questions, sessionId: sessionId }));
163
+ }
164
+ // Show question display (default)
165
+ 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 }));
166
+ };
@@ -0,0 +1,42 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ import { theme } from "../theme.js";
4
+ /**
5
+ * TabBar component displays question titles horizontally with progress indicator
6
+ * Visual: ☑ Language ☐ App Type ☐ Framework (2/3)
7
+ * where ☑ indicates answered questions, ☐ indicates unanswered
8
+ * Active tab has underline, cyan text, and blue background
9
+ */
10
+ export const TabBar = ({ currentIndex, questions, answers, }) => {
11
+ return (React.createElement(Box, { flexWrap: "wrap" },
12
+ React.createElement(Box, { paddingRight: 1 },
13
+ React.createElement(Text, { color: theme.colors.info }, "\u2190")),
14
+ questions.map((question, index) => {
15
+ const isActive = index === currentIndex;
16
+ // Use provided title or fallback to "Q1", "Q2", etc.
17
+ const title = question.title || `Q${index + 1}`;
18
+ // Check if question is answered
19
+ const answer = answers.get(index);
20
+ const isAnswered = answer && (answer.selectedOption || answer.customText);
21
+ const icon = isAnswered ? "☑" : "☐";
22
+ const iconColor = isAnswered
23
+ ? theme.components.tabBar.answered
24
+ : theme.components.tabBar.unanswered;
25
+ return (React.createElement(Box, { key: index, minWidth: 15, paddingRight: 1 },
26
+ React.createElement(Text, { color: iconColor },
27
+ icon,
28
+ " "),
29
+ React.createElement(Text, { bold: isActive, color: isActive
30
+ ? theme.components.tabBar.selected
31
+ : theme.components.tabBar.default, backgroundColor: isActive ? theme.components.tabBar.selectedBg : undefined, underline: isActive }, title)));
32
+ }),
33
+ React.createElement(Box, { paddingRight: 1 },
34
+ React.createElement(Text, { color: theme.colors.info }, "\u2192")),
35
+ React.createElement(Box, { minWidth: 10 },
36
+ React.createElement(Text, { color: theme.colors.info },
37
+ "[",
38
+ currentIndex + 1,
39
+ "/",
40
+ questions.length,
41
+ "]"))));
42
+ };
@@ -0,0 +1,19 @@
1
+ import { Box, Text } from "ink";
2
+ import React, { useEffect } from "react";
3
+ /**
4
+ * Toast component for brief non-blocking notifications
5
+ * Auto-dismisses after specified duration (default 2000ms)
6
+ */
7
+ export const Toast = ({ message, type = "success", onDismiss, duration = 2000, }) => {
8
+ // Auto-dismiss after duration
9
+ useEffect(() => {
10
+ const timer = setTimeout(() => {
11
+ onDismiss();
12
+ }, duration);
13
+ return () => clearTimeout(timer);
14
+ }, [duration, onDismiss]);
15
+ // Color based on type
16
+ const color = type === "success" ? "green" : type === "error" ? "red" : "cyan";
17
+ return (React.createElement(Box, { borderColor: color, borderStyle: "round", paddingX: 2, paddingY: 0.5 },
18
+ React.createElement(Text, { bold: true, color: color }, message)));
19
+ };
@@ -0,0 +1,20 @@
1
+ import { Box, Text } from "ink";
2
+ import React from "react";
3
+ import { AnimatedGradient } from "./AnimatedGradient.js";
4
+ /**
5
+ * WaitingScreen displays when no question sets are being processed
6
+ * Shows "Waiting for AI..." message or queue status
7
+ */
8
+ export const WaitingScreen = ({ queueCount }) => {
9
+ if (queueCount === 0) {
10
+ return (React.createElement(Box, { flexDirection: "column" },
11
+ React.createElement(Box, { justifyContent: "center", paddingY: 1 },
12
+ React.createElement(AnimatedGradient, { text: "Waiting for AI to ask questions\u2026" })),
13
+ React.createElement(Box, { justifyContent: "center", paddingY: 1 },
14
+ React.createElement(Text, { dimColor: true }, "Press q to quit"))));
15
+ }
16
+ return (React.createElement(Box, { flexDirection: "column" },
17
+ React.createElement(AnimatedGradient, { text: `Processing question set... (${queueCount} remaining in queue)` }),
18
+ React.createElement(Box, { justifyContent: "center", paddingY: 1 },
19
+ React.createElement(Text, { dimColor: true }, "Press q to quit"))));
20
+ };
@@ -0,0 +1,195 @@
1
+ /**
2
+ * TUI Session Watcher Module
3
+ *
4
+ * Provides TUI applications with the ability to detect new question sessions
5
+ * and coordinate with the MCP server through file system events.
6
+ */
7
+ import { atomicReadFile } from "../session/atomic-operations.js";
8
+ import { TUISessionWatcher } from "../session/file-watcher.js";
9
+ import { SESSION_FILES } from "../session/types.js";
10
+ import { getSessionDirectory } from "../session/utils.js";
11
+ /**
12
+ * Enhanced TUI Session Watcher with session data loading
13
+ */
14
+ export class EnhancedTUISessionWatcher extends TUISessionWatcher {
15
+ autoLoadData;
16
+ eventHandlers = new Map();
17
+ constructor(config) {
18
+ // Map sessionDir to baseDir for parent class
19
+ super({
20
+ baseDir: config?.sessionDir,
21
+ debounceMs: config?.debounceMs,
22
+ });
23
+ this.autoLoadData = config?.autoLoadData ?? true;
24
+ }
25
+ /**
26
+ * Add custom event handler
27
+ */
28
+ addEventHandler(name, handler) {
29
+ this.eventHandlers.set(name, handler);
30
+ }
31
+ /**
32
+ * Get list of pending sessions (sessions without answers and status is pending/in-progress)
33
+ */
34
+ async getPendingSessions() {
35
+ const fs = await import("fs/promises");
36
+ const { join } = await import("path");
37
+ try {
38
+ const sessionDir = this.watchedPath;
39
+ const entries = await fs.readdir(sessionDir, { withFileTypes: true });
40
+ const pendingSessions = [];
41
+ for (const entry of entries) {
42
+ if (!entry.isDirectory())
43
+ continue;
44
+ const sessionPath = join(sessionDir, entry.name);
45
+ const answersPath = join(sessionPath, SESSION_FILES.ANSWERS);
46
+ const statusPath = join(sessionPath, SESSION_FILES.STATUS);
47
+ try {
48
+ // Check if answers file doesn't exist (pending session)
49
+ await fs.access(answersPath);
50
+ }
51
+ catch {
52
+ // Answers file doesn't exist - check status
53
+ try {
54
+ const statusContent = await fs.readFile(statusPath, "utf-8");
55
+ const status = JSON.parse(statusContent);
56
+ // Only include sessions that are actually pending or in-progress
57
+ // Exclude: rejected, completed, timed_out, abandoned
58
+ if (status.status === "pending" || status.status === "in-progress") {
59
+ pendingSessions.push(entry.name);
60
+ }
61
+ }
62
+ catch {
63
+ // No valid status file - not a valid session
64
+ }
65
+ }
66
+ }
67
+ return pendingSessions.sort(); // Sort for consistent ordering
68
+ }
69
+ catch (error) {
70
+ console.warn("Failed to scan for pending sessions:", error);
71
+ return [];
72
+ }
73
+ }
74
+ /**
75
+ * Get session request data for a specific session
76
+ */
77
+ async getSessionRequest(sessionId) {
78
+ const { join } = await import("path");
79
+ const sessionPath = join(this.watchedPath, sessionId);
80
+ try {
81
+ return await this.loadSessionRequest(sessionPath);
82
+ }
83
+ catch (error) {
84
+ console.warn(`Failed to load session request for ${sessionId}:`, error);
85
+ return null;
86
+ }
87
+ }
88
+ /**
89
+ * Check if a session exists and is pending
90
+ */
91
+ async isSessionPending(sessionId) {
92
+ const { join } = await import("path");
93
+ const sessionPath = join(this.watchedPath, sessionId);
94
+ const answersPath = join(sessionPath, SESSION_FILES.ANSWERS);
95
+ const statusPath = join(sessionPath, SESSION_FILES.STATUS);
96
+ try {
97
+ const fs = await import("fs/promises");
98
+ // Check if status file exists (valid session)
99
+ await fs.access(statusPath);
100
+ // Check if answers file doesn't exist (pending)
101
+ try {
102
+ await fs.access(answersPath);
103
+ return false; // Answers exist - session is completed
104
+ }
105
+ catch {
106
+ return true; // No answers - session is pending
107
+ }
108
+ }
109
+ catch {
110
+ return false; // Not a valid session
111
+ }
112
+ }
113
+ /**
114
+ * Remove event handler
115
+ */
116
+ removeEventHandler(name) {
117
+ this.eventHandlers.delete(name);
118
+ }
119
+ /**
120
+ * Start watching with enhanced event handling
121
+ */
122
+ startEnhancedWatching(onSessionEvent) {
123
+ this.startWatching(async (sessionId, sessionPath) => {
124
+ const event = {
125
+ sessionId,
126
+ sessionPath,
127
+ timestamp: Date.now(),
128
+ type: "session-created",
129
+ };
130
+ // Auto-load session data if requested
131
+ if (this.autoLoadData) {
132
+ try {
133
+ const sessionRequest = await this.loadSessionRequest(sessionPath);
134
+ event.sessionRequest = sessionRequest;
135
+ }
136
+ catch (error) {
137
+ console.warn(`Failed to load session request for ${sessionId}:`, error);
138
+ }
139
+ }
140
+ // Emit the event
141
+ onSessionEvent(event);
142
+ // Call any additional handlers
143
+ this.eventHandlers.forEach((handler) => handler(event));
144
+ });
145
+ }
146
+ /**
147
+ * Load session request data from file
148
+ */
149
+ async loadSessionRequest(sessionPath) {
150
+ const requestPath = `${sessionPath}/${SESSION_FILES.REQUEST}`;
151
+ try {
152
+ const content = await atomicReadFile(requestPath, {
153
+ encoding: "utf8",
154
+ maxRetries: 3,
155
+ retryDelay: 100,
156
+ });
157
+ return JSON.parse(content);
158
+ }
159
+ catch (error) {
160
+ throw new Error(`Failed to load session request from ${requestPath}: ${error}`);
161
+ }
162
+ }
163
+ }
164
+ /**
165
+ * Create a simple TUI session watcher instance
166
+ * Auto-detects session directory based on global vs local install
167
+ */
168
+ export function createTUIWatcher(config) {
169
+ // Auto-detect session directory if not provided in config
170
+ const sessionDir = config?.sessionDir ?? getSessionDirectory();
171
+ return new EnhancedTUISessionWatcher({
172
+ ...config,
173
+ sessionDir,
174
+ });
175
+ }
176
+ /**
177
+ * Convenience function to get the next pending session
178
+ */
179
+ export async function getNextPendingSession(config) {
180
+ const watcher = createTUIWatcher(config);
181
+ const pendingSessions = await watcher.getPendingSessions();
182
+ if (pendingSessions.length === 0) {
183
+ return null;
184
+ }
185
+ // Try each pending session until we find one with valid data
186
+ for (const sessionId of pendingSessions) {
187
+ const sessionRequest = await watcher.getSessionRequest(sessionId);
188
+ if (sessionRequest) {
189
+ return { sessionId, sessionRequest };
190
+ }
191
+ // Skip corrupted sessions and continue to next one
192
+ }
193
+ // No valid sessions found
194
+ return null;
195
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Centralized Theme Configuration
3
+ * Customize all colors and gradients in one place
4
+ *
5
+ * HOW TO CUSTOMIZE:
6
+ * Simply edit the color values below and rebuild with `npm run build`
7
+ *
8
+ * Available terminal colors:
9
+ * - Basic: "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"
10
+ * - Bright: "gray", "grey", "brightRed", "brightGreen", etc.
11
+ * - Or use hex colors: "#FF5733"
12
+ *
13
+ * HEADER GRADIENT THEMES (from gradient-string):
14
+ * Change 'headerGradient' below to any of these beautiful premade gradients:
15
+ * - "cristal" - Crystal clear blues
16
+ * - "teen" - Vibrant teen energy
17
+ * - "mind" - Psychedelic colors
18
+ * - "morning" - Sunrise colors
19
+ * - "vice" - Neon vice city
20
+ * - "passion" - Passionate reds
21
+ * - "fruit" - Fruity colors
22
+ * - "instagram" - Instagram gradient
23
+ * - "atlas" - Atlas blue
24
+ * - "retro" - Retro gaming
25
+ * - "summer" - Summer vibes
26
+ * - "pastel" - Soft pastels
27
+ * - "rainbow" - Full rainbow
28
+ */
29
+ export const theme = {
30
+ /**
31
+ * Header Gradient Theme
32
+ * To change: Replace "pastel" with any gradient name from the list above
33
+ */
34
+ headerGradient: "vice",
35
+ /**
36
+ * Gradient Colors (deprecated - use headerGradient instead)
37
+ * Used for logo, welcome messages, and decorative text
38
+ */
39
+ gradient: {
40
+ start: "cyan",
41
+ middle: "green",
42
+ end: "yellow",
43
+ },
44
+ /**
45
+ * UI State Colors
46
+ */
47
+ colors: {
48
+ // Primary UI colors
49
+ primary: "cyan",
50
+ success: "green",
51
+ warning: "yellow",
52
+ error: "red",
53
+ info: "blue",
54
+ // Interactive states
55
+ focused: "cyan",
56
+ selected: "green",
57
+ pending: "yellow",
58
+ // Text colors
59
+ text: "white",
60
+ textDim: "gray",
61
+ textBold: "white",
62
+ },
63
+ /**
64
+ * Border Colors
65
+ */
66
+ borders: {
67
+ primary: "cyan",
68
+ warning: "yellow",
69
+ error: "red",
70
+ neutral: "gray",
71
+ },
72
+ /**
73
+ * Component-Specific Colors
74
+ */
75
+ components: {
76
+ header: {
77
+ border: "cyan",
78
+ queueActive: "yellow",
79
+ queueEmpty: "green",
80
+ queueFlash: "cyan",
81
+ },
82
+ tabBar: {
83
+ selected: "cyan",
84
+ selectedBg: "black",
85
+ default: "white",
86
+ answered: "green",
87
+ unanswered: "gray",
88
+ },
89
+ options: {
90
+ focused: "cyan",
91
+ selected: "green",
92
+ default: "white",
93
+ },
94
+ review: {
95
+ border: "cyan",
96
+ confirmBorder: "yellow",
97
+ selectedOption: "green",
98
+ customAnswer: "yellow",
99
+ },
100
+ toast: {
101
+ success: "green",
102
+ error: "red",
103
+ info: "cyan",
104
+ },
105
+ },
106
+ };
107
+ /**
108
+ * Export gradient colors as array for gradient-string
109
+ */
110
+ export const gradientColors = [
111
+ theme.gradient.start,
112
+ theme.gradient.middle,
113
+ theme.gradient.end,
114
+ ];
@@ -0,0 +1,24 @@
1
+ import gradient from "gradient-string";
2
+ import { gradientColors } from "../theme.js";
3
+ /**
4
+ * Theme-based gradient for decorative text in the TUI.
5
+ * Colors are defined in theme.ts and can be customized in one place.
6
+ */
7
+ export const themeGradient = gradient(gradientColors);
8
+ /**
9
+ * Apply theme gradient to welcome/decorative text.
10
+ * Returns ANSI-colored string compatible with Ink <Text> components.
11
+ */
12
+ export function welcomeText(text) {
13
+ return themeGradient(text);
14
+ }
15
+ /**
16
+ * Apply theme gradient to goodbye/decorative text.
17
+ * Returns ANSI-colored string compatible with Ink <Text> components.
18
+ */
19
+ export function goodbyeText(text) {
20
+ return themeGradient(text);
21
+ }
22
+ export function gradientText(text) {
23
+ return themeGradient(text);
24
+ }