auq-mcp-server 2.6.4 → 2.7.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 +56 -2
- package/dist/bin/auq.js +36 -3
- package/dist/bin/tui-app.js +27 -6
- package/dist/package.json +7 -2
- package/dist/src/__tests__/schema-validation.test.js +61 -1
- package/dist/src/cli/commands/__tests__/fetch-answers.test.js +310 -0
- package/dist/src/cli/commands/__tests__/history.test.js +211 -0
- package/dist/src/cli/commands/answer.js +11 -0
- package/dist/src/cli/commands/config.js +48 -0
- package/dist/src/cli/commands/fetch-answers.js +205 -0
- package/dist/src/cli/commands/history.js +375 -0
- package/dist/src/config/__tests__/ConfigLoader.test.js +38 -0
- package/dist/src/config/defaults.js +1 -0
- package/dist/src/config/types.js +1 -0
- package/dist/src/core/ask-user-questions.js +63 -0
- package/dist/src/i18n/locales/en.js +2 -2
- package/dist/src/server.js +59 -2
- package/dist/src/session/ResponseFormatter.js +79 -2
- package/dist/src/session/SessionManager.js +36 -0
- package/dist/src/session/__tests__/ResponseFormatter.test.js +86 -0
- package/dist/src/session/__tests__/SessionManager.test.js +129 -0
- package/dist/src/shared/schemas.js +8 -0
- package/dist/src/tui/ThemeProvider.js +3 -3
- package/dist/src/tui/components/Header.js +2 -1
- package/dist/src/tui/components/OptionsList.js +1 -1
- package/dist/src/tui/components/SessionPicker.js +1 -1
- package/dist/src/tui/components/StepperView.js +1 -1
- package/dist/src/tui/components/__tests__/ConfirmationDialog.test.js +1 -1
- package/dist/src/tui/components/__tests__/Footer.test.js +1 -1
- package/dist/src/tui/components/__tests__/MarkdownPrompt.test.js +1 -1
- package/dist/src/tui/components/__tests__/ReviewScreen.test.js +1 -1
- package/dist/src/tui/components/__tests__/SessionDots.test.js +1 -1
- package/dist/src/tui/components/__tests__/SessionPicker.test.js +1 -1
- package/dist/src/tui/components/__tests__/StepperView.abandoned.test.js +1 -1
- package/dist/src/tui/components/__tests__/StepperView.keyboard.test.js +1 -1
- package/dist/src/tui/components/__tests__/StepperView.state.test.js +1 -1
- package/dist/src/tui/components/__tests__/WaitingScreen.test.js +1 -1
- package/dist/src/tui/shared/session-events.js +4 -0
- package/dist/src/tui/shared/themes/catppuccin-latte.js +130 -0
- package/dist/src/tui/shared/themes/catppuccin-mocha.js +131 -0
- package/dist/src/tui/shared/themes/dark.js +131 -0
- package/dist/src/tui/shared/themes/dracula.js +131 -0
- package/dist/src/tui/shared/themes/github-dark.js +129 -0
- package/dist/src/tui/shared/themes/github-light.js +129 -0
- package/dist/src/tui/shared/themes/gruvbox-dark.js +130 -0
- package/dist/src/tui/shared/themes/gruvbox-light.js +130 -0
- package/dist/src/tui/shared/themes/index.js +70 -0
- package/dist/src/tui/shared/themes/light.js +130 -0
- package/dist/src/tui/shared/themes/loader.js +111 -0
- package/dist/src/tui/shared/themes/monokai.js +132 -0
- package/dist/src/tui/shared/themes/nord.js +130 -0
- package/dist/src/tui/shared/themes/one-dark.js +131 -0
- package/dist/src/tui/shared/themes/rose-pine.js +131 -0
- package/dist/src/tui/shared/themes/solarized-dark.js +130 -0
- package/dist/src/tui/shared/themes/solarized-light.js +130 -0
- package/dist/src/tui/shared/themes/tokyo-night.js +131 -0
- package/dist/src/tui/shared/themes/types.js +1 -0
- package/dist/src/tui/shared/types.js +1 -0
- package/dist/src/tui/shared/utils/config.js +80 -0
- package/dist/src/tui/shared/utils/detectTheme.js +33 -0
- package/dist/src/tui/shared/utils/index.js +6 -0
- package/dist/src/tui/shared/utils/recommended.js +52 -0
- package/dist/src/tui/shared/utils/relativeTime.js +24 -0
- package/dist/src/tui/shared/utils/sessionSwitching.js +56 -0
- package/dist/src/tui/shared/utils/staleDetection.js +51 -0
- package/dist/src/tui/themes/catppuccin-latte.js +2 -127
- package/dist/src/tui/themes/catppuccin-mocha.js +2 -127
- package/dist/src/tui/themes/dark.js +2 -128
- package/dist/src/tui/themes/dracula.js +2 -127
- package/dist/src/tui/themes/github-dark.js +2 -126
- package/dist/src/tui/themes/github-light.js +2 -126
- package/dist/src/tui/themes/gruvbox-dark.js +2 -127
- package/dist/src/tui/themes/gruvbox-light.js +2 -127
- package/dist/src/tui/themes/index.js +2 -70
- package/dist/src/tui/themes/light.js +2 -127
- package/dist/src/tui/themes/loader.js +2 -111
- package/dist/src/tui/themes/monokai.js +2 -128
- package/dist/src/tui/themes/nord.js +2 -127
- package/dist/src/tui/themes/one-dark.js +2 -127
- package/dist/src/tui/themes/rose-pine.js +2 -128
- package/dist/src/tui/themes/solarized-dark.js +2 -127
- package/dist/src/tui/themes/solarized-light.js +2 -127
- package/dist/src/tui/themes/tokyo-night.js +2 -127
- package/dist/src/tui/themes/types.js +2 -1
- package/dist/src/tui/types.js +1 -1
- package/dist/src/tui/utils/__tests__/recommended.test.js +1 -1
- package/dist/src/tui/utils/__tests__/relativeTime.test.js +1 -1
- package/dist/src/tui/utils/__tests__/sessionSwitching.test.js +1 -1
- package/dist/src/tui/utils/__tests__/staleDetection.test.js +1 -1
- package/dist/src/tui/utils/config.js +1 -80
- package/dist/src/tui/utils/detectTheme.js +1 -22
- package/dist/src/tui/utils/recommended.js +1 -52
- package/dist/src/tui/utils/relativeTime.js +1 -24
- package/dist/src/tui/utils/sessionSwitching.js +1 -56
- package/dist/src/tui/utils/staleDetection.js +1 -51
- package/package.json +7 -2
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI History Command — `auq history` and `auq history show <id>`
|
|
3
|
+
* List and browse historical sessions with filtering and search.
|
|
4
|
+
*/
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { SessionManager } from "../../session/SessionManager.js";
|
|
7
|
+
import { getSessionDirectory } from "../../session/utils.js";
|
|
8
|
+
import { formatAge, parseFlags } from "../utils.js";
|
|
9
|
+
// ── Helper Functions ────────────────────────────────────────────────
|
|
10
|
+
function getStatusIndicator(status) {
|
|
11
|
+
switch (status) {
|
|
12
|
+
case "completed":
|
|
13
|
+
return chalk.green("✓ completed");
|
|
14
|
+
case "rejected":
|
|
15
|
+
return chalk.red("✗ rejected");
|
|
16
|
+
case "pending":
|
|
17
|
+
return chalk.yellow("⏳ pending");
|
|
18
|
+
case "in-progress":
|
|
19
|
+
return chalk.yellow("⏳ in-progress");
|
|
20
|
+
case "timed_out":
|
|
21
|
+
return chalk.yellow("⏱ timed_out");
|
|
22
|
+
case "abandoned":
|
|
23
|
+
return chalk.dim("… abandoned");
|
|
24
|
+
default:
|
|
25
|
+
return status;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function getReadIndicator(lastReadAt) {
|
|
29
|
+
return lastReadAt ? "✓" : "─";
|
|
30
|
+
}
|
|
31
|
+
function truncatePreview(text, maxLen) {
|
|
32
|
+
if (text.length <= maxLen)
|
|
33
|
+
return text;
|
|
34
|
+
return text.slice(0, maxLen - 1) + "…";
|
|
35
|
+
}
|
|
36
|
+
function padColumn(text, width) {
|
|
37
|
+
// Strip ANSI escape codes for length calculation
|
|
38
|
+
const plainText = text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
39
|
+
const padding = Math.max(0, width - plainText.length);
|
|
40
|
+
return text + " ".repeat(padding);
|
|
41
|
+
}
|
|
42
|
+
async function resolveSessionId(sessionManager, idArg) {
|
|
43
|
+
// Try exact full UUID match first
|
|
44
|
+
const exists = await sessionManager.sessionExists(idArg);
|
|
45
|
+
if (exists)
|
|
46
|
+
return idArg;
|
|
47
|
+
// Try short ID prefix match
|
|
48
|
+
const allIds = await sessionManager.getAllSessionIds();
|
|
49
|
+
const shortMatches = allIds.filter((id) => id.startsWith(idArg));
|
|
50
|
+
if (shortMatches.length === 1)
|
|
51
|
+
return shortMatches[0];
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
// ── List History ────────────────────────────────────────────────────
|
|
55
|
+
async function listHistory(_positionals, flags) {
|
|
56
|
+
const jsonMode = flags.json === true;
|
|
57
|
+
const showAll = flags.all === true;
|
|
58
|
+
const filterUnread = flags.unread === true;
|
|
59
|
+
const sessionFilter = typeof flags.session === "string" ? flags.session : undefined;
|
|
60
|
+
const searchFilter = typeof flags.search === "string" ? flags.search.toLowerCase() : undefined;
|
|
61
|
+
const limitRaw = typeof flags.limit === "string" ? parseInt(flags.limit, 10) : 20;
|
|
62
|
+
const limit = isNaN(limitRaw) ? 20 : Math.max(1, limitRaw);
|
|
63
|
+
// Initialize SessionManager
|
|
64
|
+
const sessionManager = new SessionManager({
|
|
65
|
+
baseDir: getSessionDirectory(),
|
|
66
|
+
});
|
|
67
|
+
await sessionManager.initialize();
|
|
68
|
+
// Get all session IDs
|
|
69
|
+
const sessionIds = await sessionManager.getAllSessionIds();
|
|
70
|
+
// Build entries
|
|
71
|
+
const entries = [];
|
|
72
|
+
let abandonedCount = 0;
|
|
73
|
+
for (const sessionId of sessionIds) {
|
|
74
|
+
let status = null;
|
|
75
|
+
let request = null;
|
|
76
|
+
let answersData = null;
|
|
77
|
+
try {
|
|
78
|
+
status = await sessionManager.getSessionStatus(sessionId);
|
|
79
|
+
if (!status)
|
|
80
|
+
continue;
|
|
81
|
+
request = await sessionManager.getSessionRequest(sessionId);
|
|
82
|
+
try {
|
|
83
|
+
answersData = await sessionManager.getSessionAnswers(sessionId);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// answers.json may not exist — that's ok
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue; // Skip broken sessions
|
|
91
|
+
}
|
|
92
|
+
if (!status)
|
|
93
|
+
continue;
|
|
94
|
+
const isAbandoned = status.status === "abandoned";
|
|
95
|
+
if (isAbandoned)
|
|
96
|
+
abandonedCount++;
|
|
97
|
+
const questionCount = request?.questions?.length ?? status.totalQuestions ?? 0;
|
|
98
|
+
const answeredCount = answersData?.answers?.length ?? 0;
|
|
99
|
+
const lastReadAt = answersData?.lastReadAt;
|
|
100
|
+
// Build preview from first question prompt
|
|
101
|
+
const firstQuestion = request?.questions?.[0];
|
|
102
|
+
const preview = firstQuestion
|
|
103
|
+
? truncatePreview(firstQuestion.prompt, 40)
|
|
104
|
+
: "";
|
|
105
|
+
// Build search text from all question prompts and answer data
|
|
106
|
+
const searchTextParts = [];
|
|
107
|
+
if (request?.questions) {
|
|
108
|
+
for (const q of request.questions) {
|
|
109
|
+
searchTextParts.push(q.prompt.toLowerCase());
|
|
110
|
+
if (q.title)
|
|
111
|
+
searchTextParts.push(q.title.toLowerCase());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (answersData?.answers) {
|
|
115
|
+
for (const a of answersData.answers) {
|
|
116
|
+
if (a.selectedOption)
|
|
117
|
+
searchTextParts.push(a.selectedOption.toLowerCase());
|
|
118
|
+
if (a.selectedOptions) {
|
|
119
|
+
for (const opt of a.selectedOptions)
|
|
120
|
+
searchTextParts.push(opt.toLowerCase());
|
|
121
|
+
}
|
|
122
|
+
if (a.customText)
|
|
123
|
+
searchTextParts.push(a.customText.toLowerCase());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const searchText = searchTextParts.join(" ");
|
|
127
|
+
entries.push({
|
|
128
|
+
sessionId,
|
|
129
|
+
shortId: sessionId.slice(0, 8),
|
|
130
|
+
status: status.status,
|
|
131
|
+
createdAt: status.createdAt,
|
|
132
|
+
lastReadAt,
|
|
133
|
+
questionCount,
|
|
134
|
+
answeredCount,
|
|
135
|
+
preview,
|
|
136
|
+
searchText,
|
|
137
|
+
isAbandoned,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// Sort by createdAt descending (newest first)
|
|
141
|
+
entries.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
142
|
+
// Apply filters
|
|
143
|
+
let filtered = [...entries];
|
|
144
|
+
// Base filter: exclude abandoned unless --all
|
|
145
|
+
if (!showAll) {
|
|
146
|
+
filtered = filtered.filter((e) => !e.isAbandoned);
|
|
147
|
+
}
|
|
148
|
+
// --session filter
|
|
149
|
+
if (sessionFilter !== undefined) {
|
|
150
|
+
filtered = filtered.filter((e) => e.sessionId === sessionFilter ||
|
|
151
|
+
e.sessionId.startsWith(sessionFilter));
|
|
152
|
+
if (filtered.length === 0) {
|
|
153
|
+
if (jsonMode) {
|
|
154
|
+
console.log(JSON.stringify([], null, 2));
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.error(`Session not found: ${sessionFilter}`);
|
|
158
|
+
}
|
|
159
|
+
process.exitCode = 1;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// --unread filter
|
|
164
|
+
if (filterUnread) {
|
|
165
|
+
filtered = filtered.filter((e) => e.status === "completed" && !e.lastReadAt);
|
|
166
|
+
}
|
|
167
|
+
// --search filter
|
|
168
|
+
if (searchFilter !== undefined) {
|
|
169
|
+
filtered = filtered.filter((e) => e.searchText.includes(searchFilter));
|
|
170
|
+
}
|
|
171
|
+
// Apply limit
|
|
172
|
+
const displayed = filtered.slice(0, limit);
|
|
173
|
+
// JSON output
|
|
174
|
+
if (jsonMode) {
|
|
175
|
+
const result = displayed.map((e) => ({
|
|
176
|
+
sessionId: e.sessionId,
|
|
177
|
+
status: e.status,
|
|
178
|
+
createdAt: e.createdAt,
|
|
179
|
+
lastReadAt: e.lastReadAt ?? null,
|
|
180
|
+
questionCount: e.questionCount,
|
|
181
|
+
answeredCount: e.answeredCount,
|
|
182
|
+
preview: e.preview,
|
|
183
|
+
}));
|
|
184
|
+
console.log(JSON.stringify(result, null, 2));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// Empty state
|
|
188
|
+
if (displayed.length === 0) {
|
|
189
|
+
console.log("No sessions found.");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Table output
|
|
193
|
+
const headerLine = padColumn("ID", 10) +
|
|
194
|
+
padColumn("Status", 16) +
|
|
195
|
+
padColumn("Time", 10) +
|
|
196
|
+
padColumn("Read", 7) +
|
|
197
|
+
padColumn("Q", 6) +
|
|
198
|
+
"Preview";
|
|
199
|
+
console.log(chalk.dim(headerLine));
|
|
200
|
+
for (const entry of displayed) {
|
|
201
|
+
const statusStr = getStatusIndicator(entry.status);
|
|
202
|
+
const age = formatAge(entry.createdAt);
|
|
203
|
+
const readStr = getReadIndicator(entry.lastReadAt);
|
|
204
|
+
const questionsStr = `${entry.answeredCount}/${entry.questionCount}`;
|
|
205
|
+
const line = padColumn(entry.shortId, 10) +
|
|
206
|
+
padColumn(statusStr, 16) +
|
|
207
|
+
padColumn(age, 10) +
|
|
208
|
+
padColumn(readStr, 7) +
|
|
209
|
+
padColumn(questionsStr, 6) +
|
|
210
|
+
entry.preview;
|
|
211
|
+
console.log(line);
|
|
212
|
+
}
|
|
213
|
+
// Hint line when abandoned sessions are hidden
|
|
214
|
+
if (!showAll && abandonedCount > 0) {
|
|
215
|
+
console.log(chalk.dim(`\n${displayed.length} sessions shown (${abandonedCount} abandoned hidden, use --all)`));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// ── Show History ────────────────────────────────────────────────────
|
|
219
|
+
async function showHistory(positionals, flags) {
|
|
220
|
+
const jsonMode = flags.json === true;
|
|
221
|
+
const idArg = positionals[0];
|
|
222
|
+
if (!idArg) {
|
|
223
|
+
console.error("Missing session ID. Usage: auq history show <sessionId> [--json]");
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Initialize SessionManager
|
|
228
|
+
const sessionManager = new SessionManager({
|
|
229
|
+
baseDir: getSessionDirectory(),
|
|
230
|
+
});
|
|
231
|
+
await sessionManager.initialize();
|
|
232
|
+
// Resolve session ID (full UUID or short prefix)
|
|
233
|
+
const sessionId = await resolveSessionId(sessionManager, idArg);
|
|
234
|
+
if (!sessionId) {
|
|
235
|
+
console.error(`Session not found: ${idArg}`);
|
|
236
|
+
process.exitCode = 1;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Load session data
|
|
240
|
+
const status = await sessionManager.getSessionStatus(sessionId);
|
|
241
|
+
const request = await sessionManager.getSessionRequest(sessionId);
|
|
242
|
+
let answersData = null;
|
|
243
|
+
try {
|
|
244
|
+
answersData = await sessionManager.getSessionAnswers(sessionId);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// answers.json may not exist for pending/abandoned sessions
|
|
248
|
+
}
|
|
249
|
+
if (!status || !request) {
|
|
250
|
+
console.error(`Could not read session data for: ${idArg}`);
|
|
251
|
+
process.exitCode = 1;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const questions = request.questions;
|
|
255
|
+
const answers = answersData?.answers ?? null;
|
|
256
|
+
const lastReadAt = answersData?.lastReadAt;
|
|
257
|
+
const answeredCount = answers?.length ?? 0;
|
|
258
|
+
// Build answer lookup (questionIndex → answer)
|
|
259
|
+
const answerMap = new Map();
|
|
260
|
+
if (answers) {
|
|
261
|
+
for (const a of answers) {
|
|
262
|
+
answerMap.set(a.questionIndex, {
|
|
263
|
+
selectedOption: a.selectedOption,
|
|
264
|
+
selectedOptions: a.selectedOptions,
|
|
265
|
+
customText: a.customText,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// JSON output
|
|
270
|
+
if (jsonMode) {
|
|
271
|
+
const result = {
|
|
272
|
+
sessionId,
|
|
273
|
+
status: status.status,
|
|
274
|
+
createdAt: status.createdAt,
|
|
275
|
+
lastReadAt: lastReadAt ?? null,
|
|
276
|
+
questionCount: questions.length,
|
|
277
|
+
answeredCount,
|
|
278
|
+
questions: questions.map((q, i) => {
|
|
279
|
+
const answer = answerMap.get(i);
|
|
280
|
+
const selectedLabels = new Set();
|
|
281
|
+
if (answer?.selectedOption)
|
|
282
|
+
selectedLabels.add(answer.selectedOption);
|
|
283
|
+
if (answer?.selectedOptions) {
|
|
284
|
+
for (const opt of answer.selectedOptions)
|
|
285
|
+
selectedLabels.add(opt);
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
index: i,
|
|
289
|
+
title: q.title,
|
|
290
|
+
prompt: q.prompt,
|
|
291
|
+
multiSelect: q.multiSelect ?? false,
|
|
292
|
+
options: q.options.map((o) => ({
|
|
293
|
+
label: o.label,
|
|
294
|
+
description: o.description ?? null,
|
|
295
|
+
selected: selectedLabels.has(o.label),
|
|
296
|
+
})),
|
|
297
|
+
customText: answer?.customText ?? null,
|
|
298
|
+
};
|
|
299
|
+
}),
|
|
300
|
+
};
|
|
301
|
+
console.log(JSON.stringify(result, null, 2));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Human-readable output
|
|
305
|
+
const age = formatAge(status.createdAt);
|
|
306
|
+
const createdAbsolute = new Date(status.createdAt)
|
|
307
|
+
.toISOString()
|
|
308
|
+
.replace("T", " ")
|
|
309
|
+
.replace(/\.\d+Z$/, "Z");
|
|
310
|
+
console.log(`Session: ${sessionId}`);
|
|
311
|
+
console.log(`Status: ${getStatusIndicator(status.status)}`);
|
|
312
|
+
console.log(`Created: ${createdAbsolute} (${age})`);
|
|
313
|
+
if (lastReadAt) {
|
|
314
|
+
const readAbsolute = new Date(lastReadAt)
|
|
315
|
+
.toISOString()
|
|
316
|
+
.replace("T", " ")
|
|
317
|
+
.replace(/\.\d+Z$/, "Z");
|
|
318
|
+
console.log(`Read: ✓ ${readAbsolute}`);
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
console.log("Read: unread");
|
|
322
|
+
}
|
|
323
|
+
console.log(`Questions: ${answeredCount}/${questions.length} answered`);
|
|
324
|
+
console.log("");
|
|
325
|
+
// Display questions with options
|
|
326
|
+
for (let i = 0; i < questions.length; i++) {
|
|
327
|
+
const q = questions[i];
|
|
328
|
+
const answer = answerMap.get(i);
|
|
329
|
+
// Determine which option labels are selected
|
|
330
|
+
const selectedLabels = new Set();
|
|
331
|
+
if (answer?.selectedOption)
|
|
332
|
+
selectedLabels.add(answer.selectedOption);
|
|
333
|
+
if (answer?.selectedOptions) {
|
|
334
|
+
for (const opt of answer.selectedOptions)
|
|
335
|
+
selectedLabels.add(opt);
|
|
336
|
+
}
|
|
337
|
+
console.log(`${i + 1}. ${q.title}`);
|
|
338
|
+
console.log(` ${q.prompt}`);
|
|
339
|
+
let otherOptionHandled = false;
|
|
340
|
+
for (const opt of q.options) {
|
|
341
|
+
const isSelected = selectedLabels.has(opt.label);
|
|
342
|
+
const descPart = opt.description ? ` — ${opt.description}` : "";
|
|
343
|
+
// If this is the "Other" option and there's custom text, show custom text inline
|
|
344
|
+
if (isSelected && answer?.customText && opt.label === "Other") {
|
|
345
|
+
console.log(` (selected) Other: '${answer.customText}'`);
|
|
346
|
+
otherOptionHandled = true;
|
|
347
|
+
}
|
|
348
|
+
else if (isSelected) {
|
|
349
|
+
console.log(` (selected) ${opt.label}${descPart}`);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
console.log(` ${opt.label}${descPart}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// If customText exists but "Other" is not a listed option, show it as additional entry
|
|
356
|
+
if (answer?.customText && !otherOptionHandled) {
|
|
357
|
+
const hasOtherOption = q.options.some((o) => o.label === "Other");
|
|
358
|
+
if (!hasOtherOption) {
|
|
359
|
+
console.log(` (selected) Other: '${answer.customText}'`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
console.log("");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// ── History Command Dispatcher ──────────────────────────────────────
|
|
366
|
+
export async function runHistoryCommand(args) {
|
|
367
|
+
const { flags, positionals } = parseFlags(args);
|
|
368
|
+
const subcommand = positionals[0];
|
|
369
|
+
if (subcommand === "show") {
|
|
370
|
+
await showHistory(positionals.slice(1), flags);
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
await listHistory(positionals, flags);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -132,4 +132,42 @@ describe("ConfigLoader", () => {
|
|
|
132
132
|
expect(config.staleThreshold).toBe(DEFAULT_CONFIG.staleThreshold);
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
|
+
describe("renderer config options", () => {
|
|
136
|
+
it("should have 'ink' as default renderer when no config files exist", () => {
|
|
137
|
+
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
138
|
+
const config = loadConfig();
|
|
139
|
+
expect(config.renderer).toBe("ink");
|
|
140
|
+
});
|
|
141
|
+
it("should include renderer in DEFAULT_CONFIG as 'ink'", () => {
|
|
142
|
+
expect(DEFAULT_CONFIG.renderer).toBe("ink");
|
|
143
|
+
});
|
|
144
|
+
it("should load renderer: 'opentui' from config file", () => {
|
|
145
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
146
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ renderer: "opentui" }));
|
|
147
|
+
const config = loadConfig();
|
|
148
|
+
expect(config.renderer).toBe("opentui");
|
|
149
|
+
});
|
|
150
|
+
it("should load renderer: 'ink' from config file", () => {
|
|
151
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
152
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ renderer: "ink" }));
|
|
153
|
+
const config = loadConfig();
|
|
154
|
+
expect(config.renderer).toBe("ink");
|
|
155
|
+
});
|
|
156
|
+
it("should fall back to 'ink' for invalid renderer value", () => {
|
|
157
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
158
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ renderer: "chrome" }));
|
|
159
|
+
const config = loadConfig();
|
|
160
|
+
// Invalid enum value should be ignored, default ('ink') used
|
|
161
|
+
expect(config.renderer).toBe(DEFAULT_CONFIG.renderer);
|
|
162
|
+
expect(config.renderer).toBe("ink");
|
|
163
|
+
});
|
|
164
|
+
it("should preserve other config values alongside renderer setting", () => {
|
|
165
|
+
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
166
|
+
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ renderer: "opentui", theme: "dark" }));
|
|
167
|
+
const config = loadConfig();
|
|
168
|
+
expect(config.renderer).toBe("opentui");
|
|
169
|
+
expect(config.theme).toBe("dark");
|
|
170
|
+
expect(config.maxOptions).toBe(DEFAULT_CONFIG.maxOptions);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
135
173
|
});
|
package/dist/src/config/types.js
CHANGED
|
@@ -21,6 +21,7 @@ export const AUQConfigSchema = z.object({
|
|
|
21
21
|
language: z.string().default("auto"),
|
|
22
22
|
theme: z.string().default("system"),
|
|
23
23
|
autoSelectRecommended: z.boolean().default(true),
|
|
24
|
+
renderer: z.enum(["ink", "opentui"]).default("ink"),
|
|
24
25
|
// Stale/Orphan Session Detection
|
|
25
26
|
staleThreshold: z.number().min(0).default(7200000), // 2 hours in ms
|
|
26
27
|
notifyOnStale: z.boolean().default(true),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SessionManager } from "../session/index.js";
|
|
2
|
+
import { ResponseFormatter } from "../session/ResponseFormatter.js";
|
|
2
3
|
import { getSessionDirectory } from "../session/utils.js";
|
|
3
4
|
import { AskUserQuestionsParametersSchema, QuestionSchema, QuestionsSchema, } from "../shared/schemas.js";
|
|
4
5
|
// Re-export schemas for backward compatibility
|
|
@@ -27,8 +28,70 @@ export const createAskUserQuestionsCore = (options = {}) => {
|
|
|
27
28
|
const parsedQuestions = QuestionsSchema.parse(questions);
|
|
28
29
|
return sessionManager.startSession(normalizeQuestions(parsedQuestions), callId, workingDirectory, signal);
|
|
29
30
|
};
|
|
31
|
+
const askNonBlocking = async (questions, callId, workingDirectory) => {
|
|
32
|
+
await ensureInitialized();
|
|
33
|
+
const parsedQuestions = QuestionsSchema.parse(questions);
|
|
34
|
+
const sessionId = await sessionManager.createSession(normalizeQuestions(parsedQuestions), workingDirectory);
|
|
35
|
+
return { sessionId, questionCount: parsedQuestions.length };
|
|
36
|
+
};
|
|
37
|
+
const getAnsweredQuestions = async (sessionId, blocking, signal) => {
|
|
38
|
+
await ensureInitialized();
|
|
39
|
+
// Resolve short ID to full UUID if needed
|
|
40
|
+
let resolvedSessionId = sessionId;
|
|
41
|
+
if (sessionId.length < 36) {
|
|
42
|
+
const allIds = await sessionManager.getAllSessionIds();
|
|
43
|
+
const match = allIds.find((id) => id.startsWith(sessionId));
|
|
44
|
+
if (!match) {
|
|
45
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
46
|
+
}
|
|
47
|
+
resolvedSessionId = match;
|
|
48
|
+
}
|
|
49
|
+
const sessionStatus = await sessionManager.getSessionStatus(resolvedSessionId);
|
|
50
|
+
if (!sessionStatus) {
|
|
51
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
52
|
+
}
|
|
53
|
+
const shortId = resolvedSessionId.slice(0, 8);
|
|
54
|
+
const handleCompleted = async () => {
|
|
55
|
+
const answers = await sessionManager.getSessionAnswers(resolvedSessionId);
|
|
56
|
+
const request = await sessionManager.getSessionRequest(resolvedSessionId);
|
|
57
|
+
if (!answers || !request) {
|
|
58
|
+
throw new Error(`Session data incomplete: ${resolvedSessionId}`);
|
|
59
|
+
}
|
|
60
|
+
const formatted = ResponseFormatter.formatUserResponse(answers, request.questions);
|
|
61
|
+
const count = request.questions.length;
|
|
62
|
+
const header = `[Session: ${shortId} | Questions: ${count}]`;
|
|
63
|
+
const formattedResponse = `${header}\n\n${formatted}`;
|
|
64
|
+
await sessionManager.markSessionAsRead(resolvedSessionId);
|
|
65
|
+
return { formattedResponse, sessionId: resolvedSessionId, status: "completed" };
|
|
66
|
+
};
|
|
67
|
+
switch (sessionStatus.status) {
|
|
68
|
+
case "completed": {
|
|
69
|
+
return handleCompleted();
|
|
70
|
+
}
|
|
71
|
+
case "pending":
|
|
72
|
+
case "in-progress": {
|
|
73
|
+
if (blocking) {
|
|
74
|
+
await sessionManager.waitForAnswers(resolvedSessionId, 0, undefined, signal);
|
|
75
|
+
return handleCompleted();
|
|
76
|
+
}
|
|
77
|
+
const pendingResponse = `[Session: ${shortId} | Status: pending]\n\nNo answers yet.`;
|
|
78
|
+
return { formattedResponse: pendingResponse, sessionId: resolvedSessionId, status: "pending" };
|
|
79
|
+
}
|
|
80
|
+
case "rejected": {
|
|
81
|
+
const reason = sessionStatus.rejectionReason;
|
|
82
|
+
const rejectedResponse = `[Session: ${shortId} | Status: rejected]\n\nUser rejected this question set.${reason ? ` Reason: "${reason}"` : ""}`;
|
|
83
|
+
return { formattedResponse: rejectedResponse, sessionId: resolvedSessionId, status: "rejected" };
|
|
84
|
+
}
|
|
85
|
+
default: {
|
|
86
|
+
const defaultResponse = `[Session: ${shortId} | Status: ${sessionStatus.status}]\n\nSession is no longer active.`;
|
|
87
|
+
return { formattedResponse: defaultResponse, sessionId: resolvedSessionId, status: sessionStatus.status };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
30
91
|
return {
|
|
31
92
|
ask,
|
|
93
|
+
askNonBlocking,
|
|
94
|
+
getAnsweredQuestions,
|
|
32
95
|
cleanupExpiredSessions: () => sessionManager.cleanupExpiredSessions(),
|
|
33
96
|
ensureInitialized,
|
|
34
97
|
markAbandoned: (sessionId) => sessionManager.updateSessionStatus(sessionId, "abandoned"),
|
|
@@ -48,10 +48,10 @@ export const en = {
|
|
|
48
48
|
customAnswerLabel: "Custom answer",
|
|
49
49
|
customAnswerHint: "(Tab to submit)",
|
|
50
50
|
otherCustom: "Other (custom)",
|
|
51
|
-
placeholder: "Type your answer
|
|
51
|
+
placeholder: "Type your answer...",
|
|
52
52
|
singleLinePlaceholder: "Type here...",
|
|
53
53
|
multiLinePlaceholder: "Type your answer...",
|
|
54
|
-
elaboratePlaceholder: "Tell the AI what you need
|
|
54
|
+
elaboratePlaceholder: "Tell the AI what you need...",
|
|
55
55
|
},
|
|
56
56
|
question: {
|
|
57
57
|
multipleChoice: "Multiple Choice",
|
package/dist/src/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FastMCP } from "fastmcp";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { AskUserQuestionsParametersSchema, createAskUserQuestionsCore, } from "./core/ask-user-questions.js";
|
|
4
|
-
import { TOOL_DESCRIPTION } from "./shared/schemas.js";
|
|
4
|
+
import { GetAnsweredQuestionsArgsSchema, GET_ANSWERED_QUESTIONS_DESCRIPTION, TOOL_DESCRIPTION } from "./shared/schemas.js";
|
|
5
5
|
const askUserQuestionsCore = createAskUserQuestionsCore();
|
|
6
6
|
// Track active requests with their AbortControllers for disconnect handling
|
|
7
7
|
const activeRequests = new Map();
|
|
@@ -52,6 +52,17 @@ server.addTool({
|
|
|
52
52
|
const workingDirectory = ctx
|
|
53
53
|
.workingDirectory;
|
|
54
54
|
try {
|
|
55
|
+
// Handle non-blocking mode
|
|
56
|
+
if (args.nonBlocking) {
|
|
57
|
+
const { sessionId, questionCount } = await askUserQuestionsCore.askNonBlocking(args.questions, callId, workingDirectory);
|
|
58
|
+
const shortId = sessionId.slice(0, 8);
|
|
59
|
+
const responseText = `[Session: ${shortId} | Questions: ${questionCount} | Status: pending]\n\n` +
|
|
60
|
+
`Questions submitted successfully.\n` +
|
|
61
|
+
`Use get_answered_questions(session_id="${shortId}") or \`auq fetch-answers ${shortId}\` to retrieve answers.`;
|
|
62
|
+
return {
|
|
63
|
+
content: [{ text: responseText, type: "text" }],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
55
66
|
const { formattedResponse, sessionId } = await askUserQuestionsCore.ask(args.questions, callId, workingDirectory, controller.signal);
|
|
56
67
|
// Update entry with sessionId for disconnect handler
|
|
57
68
|
const entry = activeRequests.get(callId);
|
|
@@ -59,11 +70,16 @@ server.addTool({
|
|
|
59
70
|
entry.sessionId = sessionId;
|
|
60
71
|
}
|
|
61
72
|
log.info("Session completed successfully", { sessionId, callId });
|
|
73
|
+
// Prepend metadata header to blocking responses
|
|
74
|
+
const shortId = sessionId.slice(0, 8);
|
|
75
|
+
const count = args.questions.length;
|
|
76
|
+
const header = `[Session: ${shortId} | Questions: ${count}]`;
|
|
77
|
+
const responseWithHeader = `${header}\n\n${formattedResponse}`;
|
|
62
78
|
// Return formatted response to AI model
|
|
63
79
|
return {
|
|
64
80
|
content: [
|
|
65
81
|
{
|
|
66
|
-
text:
|
|
82
|
+
text: responseWithHeader,
|
|
67
83
|
type: "text",
|
|
68
84
|
},
|
|
69
85
|
],
|
|
@@ -102,6 +118,47 @@ server.addTool({
|
|
|
102
118
|
},
|
|
103
119
|
parameters: AskUserQuestionsParametersSchema,
|
|
104
120
|
});
|
|
121
|
+
// Add the get_answered_questions tool
|
|
122
|
+
server.addTool({
|
|
123
|
+
name: "get_answered_questions",
|
|
124
|
+
annotations: {
|
|
125
|
+
title: "Get Answered Questions",
|
|
126
|
+
openWorldHint: false,
|
|
127
|
+
readOnlyHint: true,
|
|
128
|
+
idempotentHint: true,
|
|
129
|
+
},
|
|
130
|
+
description: GET_ANSWERED_QUESTIONS_DESCRIPTION,
|
|
131
|
+
parameters: GetAnsweredQuestionsArgsSchema,
|
|
132
|
+
execute: async (args, ctx) => {
|
|
133
|
+
const { log } = ctx;
|
|
134
|
+
const callId = randomUUID();
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
activeRequests.set(callId, { controller });
|
|
137
|
+
try {
|
|
138
|
+
await askUserQuestionsCore.ensureInitialized();
|
|
139
|
+
const { formattedResponse, sessionId, status } = await askUserQuestionsCore.getAnsweredQuestions(args.session_id, args.blocking, controller.signal);
|
|
140
|
+
log.info("Fetched answered questions", { sessionId, status, callId });
|
|
141
|
+
return {
|
|
142
|
+
content: [{ text: formattedResponse, type: "text" }],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
if (error instanceof Error && error.message === "ABORTED") {
|
|
147
|
+
log.warn("Fetch aborted: AI client disconnected", { callId });
|
|
148
|
+
return {
|
|
149
|
+
content: [{ text: "Fetch aborted: AI client disconnected", type: "text" }],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
log.error("Fetch answered questions failed", { error: String(error) });
|
|
153
|
+
return {
|
|
154
|
+
content: [{ text: `Error fetching answers: ${error}`, type: "text" }],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
activeRequests.delete(callId);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
105
162
|
// Handle AI client disconnections gracefully
|
|
106
163
|
// Note: FastMCP disconnect event support depends on the version.
|
|
107
164
|
// If the event is not available, stale detection handles orphaned sessions as fallback.
|