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.
- package/LICENSE +25 -0
- package/README.md +176 -0
- package/dist/__tests__/schema-validation.test.js +137 -0
- package/dist/__tests__/server.integration.test.js +263 -0
- package/dist/add.js +1 -0
- package/dist/add.test.js +5 -0
- package/dist/bin/auq.js +245 -0
- package/dist/bin/test-session-menu.js +28 -0
- package/dist/bin/test-tabbar.js +42 -0
- package/dist/file-utils.js +59 -0
- package/dist/format/ResponseFormatter.js +206 -0
- package/dist/format/__tests__/ResponseFormatter.test.js +380 -0
- package/dist/package.json +74 -0
- package/dist/server.js +107 -0
- package/dist/session/ResponseFormatter.js +130 -0
- package/dist/session/SessionManager.js +474 -0
- package/dist/session/__tests__/ResponseFormatter.test.js +417 -0
- package/dist/session/__tests__/SessionManager.test.js +553 -0
- package/dist/session/__tests__/atomic-operations.test.js +345 -0
- package/dist/session/__tests__/file-watcher.test.js +311 -0
- package/dist/session/__tests__/workflow.integration.test.js +334 -0
- package/dist/session/atomic-operations.js +307 -0
- package/dist/session/file-watcher.js +218 -0
- package/dist/session/index.js +7 -0
- package/dist/session/types.js +20 -0
- package/dist/session/utils.js +125 -0
- package/dist/session-manager.js +171 -0
- package/dist/session-watcher.js +110 -0
- package/dist/src/__tests__/schema-validation.test.js +170 -0
- package/dist/src/__tests__/server.integration.test.js +274 -0
- package/dist/src/add.js +1 -0
- package/dist/src/add.test.js +5 -0
- package/dist/src/server.js +163 -0
- package/dist/src/session/ResponseFormatter.js +163 -0
- package/dist/src/session/SessionManager.js +572 -0
- package/dist/src/session/__tests__/ResponseFormatter.test.js +741 -0
- package/dist/src/session/__tests__/SessionManager.test.js +593 -0
- package/dist/src/session/__tests__/atomic-operations.test.js +346 -0
- package/dist/src/session/__tests__/file-watcher.test.js +311 -0
- package/dist/src/session/atomic-operations.js +307 -0
- package/dist/src/session/file-watcher.js +227 -0
- package/dist/src/session/index.js +7 -0
- package/dist/src/session/types.js +20 -0
- package/dist/src/session/utils.js +180 -0
- package/dist/src/tui/__tests__/session-watcher.test.js +368 -0
- package/dist/src/tui/components/AnimatedGradient.js +45 -0
- package/dist/src/tui/components/ConfirmationDialog.js +89 -0
- package/dist/src/tui/components/CustomInput.js +14 -0
- package/dist/src/tui/components/Footer.js +55 -0
- package/dist/src/tui/components/Header.js +35 -0
- package/dist/src/tui/components/MultiLineTextInput.js +65 -0
- package/dist/src/tui/components/OptionsList.js +115 -0
- package/dist/src/tui/components/QuestionDisplay.js +36 -0
- package/dist/src/tui/components/ReviewScreen.js +57 -0
- package/dist/src/tui/components/SessionSelectionMenu.js +151 -0
- package/dist/src/tui/components/StepperView.js +166 -0
- package/dist/src/tui/components/TabBar.js +42 -0
- package/dist/src/tui/components/Toast.js +19 -0
- package/dist/src/tui/components/WaitingScreen.js +20 -0
- package/dist/src/tui/session-watcher.js +195 -0
- package/dist/src/tui/theme.js +114 -0
- package/dist/src/tui/utils/gradientText.js +24 -0
- package/dist/tui/__tests__/session-watcher.test.js +368 -0
- package/dist/tui/session-watcher.js +183 -0
- package/package.json +78 -0
- 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
|
+
}
|