auq-mcp-server 2.6.3 → 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 +30 -15
- 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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Fetch-Answers Command — `auq fetch-answers [<session-id>] [--blocking] [--json] [--unread]`
|
|
3
|
+
* Retrieves answers for a session asynchronously, or lists unread sessions.
|
|
4
|
+
*/
|
|
5
|
+
import { SessionManager } from "../../session/SessionManager.js";
|
|
6
|
+
import { ResponseFormatter } from "../../session/ResponseFormatter.js";
|
|
7
|
+
import { getSessionDirectory } from "../../session/utils.js";
|
|
8
|
+
import { formatAge, outputResult, parseFlags } from "../utils.js";
|
|
9
|
+
/**
|
|
10
|
+
* Run the `auq fetch-answers` command.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* auq fetch-answers # list all unread sessions (default)
|
|
14
|
+
* auq fetch-answers --unread # same as above
|
|
15
|
+
* auq fetch-answers <sessionId> # fetch answers for specific session
|
|
16
|
+
* auq fetch-answers <sessionId> --blocking # wait until answered, then fetch
|
|
17
|
+
* auq fetch-answers <sessionId> --json # output as JSON
|
|
18
|
+
*/
|
|
19
|
+
export async function runFetchAnswersCommand(args) {
|
|
20
|
+
const { flags, positionals } = parseFlags(args);
|
|
21
|
+
const jsonMode = flags.json === true;
|
|
22
|
+
const blockingMode = flags.blocking === true;
|
|
23
|
+
const unreadMode = flags.unread === true;
|
|
24
|
+
const sessionIdArg = positionals[0];
|
|
25
|
+
// ── Initialise SessionManager ─────────────────────────────────────
|
|
26
|
+
const sessionManager = new SessionManager({
|
|
27
|
+
baseDir: getSessionDirectory(),
|
|
28
|
+
});
|
|
29
|
+
await sessionManager.initialize();
|
|
30
|
+
// ── Mode B: List unread sessions ──────────────────────────────────
|
|
31
|
+
// Default (no session-id) OR explicit --unread flag
|
|
32
|
+
if (!sessionIdArg || unreadMode) {
|
|
33
|
+
// Guard: if --blocking was passed without a session ID, that's an error
|
|
34
|
+
if (blockingMode && !sessionIdArg) {
|
|
35
|
+
outputResult({ success: false, error: "--blocking requires a session ID.\n\nUsage:\n auq fetch-answers <session-id> [--blocking] [--json]\n auq fetch-answers --unread [--json]" }, jsonMode);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
return listUnreadSessions(sessionManager, jsonMode);
|
|
40
|
+
}
|
|
41
|
+
// ── Mode A: Fetch specific session ────────────────────────────────
|
|
42
|
+
return fetchSession(sessionManager, sessionIdArg, blockingMode, jsonMode);
|
|
43
|
+
}
|
|
44
|
+
// ── List Unread Sessions ──────────────────────────────────────────────
|
|
45
|
+
async function listUnreadSessions(sessionManager, jsonMode) {
|
|
46
|
+
const unreadIds = await sessionManager.getUnreadSessions();
|
|
47
|
+
if (jsonMode) {
|
|
48
|
+
const entries = await Promise.all(unreadIds.map(async (sessionId) => {
|
|
49
|
+
const status = await sessionManager.getSessionStatus(sessionId);
|
|
50
|
+
const request = await sessionManager.getSessionRequest(sessionId);
|
|
51
|
+
return {
|
|
52
|
+
sessionId,
|
|
53
|
+
status: status?.status ?? "unknown",
|
|
54
|
+
createdAt: status?.createdAt ?? null,
|
|
55
|
+
lastReadAt: null,
|
|
56
|
+
questionCount: request?.questions?.length ?? status?.totalQuestions ?? 0,
|
|
57
|
+
};
|
|
58
|
+
}));
|
|
59
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (unreadIds.length === 0) {
|
|
63
|
+
console.log("No unread sessions.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log("Unread sessions:");
|
|
67
|
+
console.log("");
|
|
68
|
+
// Build table rows
|
|
69
|
+
const rows = [];
|
|
70
|
+
for (const sessionId of unreadIds) {
|
|
71
|
+
const status = await sessionManager.getSessionStatus(sessionId);
|
|
72
|
+
const request = await sessionManager.getSessionRequest(sessionId);
|
|
73
|
+
rows.push({
|
|
74
|
+
id: sessionId.slice(0, 8),
|
|
75
|
+
status: status?.status ?? "unknown",
|
|
76
|
+
age: status ? formatAge(status.createdAt) : "?",
|
|
77
|
+
questions: request?.questions?.length ?? status?.totalQuestions ?? 0,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Print header
|
|
81
|
+
console.log("ID Status Age Questions");
|
|
82
|
+
for (const row of rows) {
|
|
83
|
+
const id = row.id.padEnd(10);
|
|
84
|
+
const status = row.status.padEnd(11);
|
|
85
|
+
const age = row.age.padEnd(8);
|
|
86
|
+
console.log(`${id}${status}${age}${row.questions}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ── Fetch Specific Session ────────────────────────────────────────────
|
|
90
|
+
async function fetchSession(sessionManager, sessionIdArg, blockingMode, jsonMode) {
|
|
91
|
+
// ── Resolve session ID (full UUID or 8-char short prefix) ─────────
|
|
92
|
+
let sessionId;
|
|
93
|
+
if (sessionIdArg.length === 8) {
|
|
94
|
+
// Try prefix resolution
|
|
95
|
+
const allIds = await sessionManager.getAllSessionIds();
|
|
96
|
+
const match = allIds.find((id) => id.startsWith(sessionIdArg));
|
|
97
|
+
if (!match) {
|
|
98
|
+
outputResult({ success: false, error: `Session not found: ${sessionIdArg}` }, jsonMode);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
sessionId = match;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
// Treat as full UUID
|
|
106
|
+
const exists = await sessionManager.sessionExists(sessionIdArg);
|
|
107
|
+
if (!exists) {
|
|
108
|
+
outputResult({ success: false, error: `Session not found: ${sessionIdArg}` }, jsonMode);
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
sessionId = sessionIdArg;
|
|
113
|
+
}
|
|
114
|
+
// ── Get current status ────────────────────────────────────────────
|
|
115
|
+
let status = await sessionManager.getSessionStatus(sessionId);
|
|
116
|
+
if (!status) {
|
|
117
|
+
outputResult({ success: false, error: `Could not read session data for: ${sessionId}` }, jsonMode);
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// ── Handle blocking wait for pending/in-progress sessions ─────────
|
|
122
|
+
const isPending = status.status === "pending" || status.status === "in-progress";
|
|
123
|
+
if (isPending && blockingMode) {
|
|
124
|
+
try {
|
|
125
|
+
await sessionManager.waitForAnswers(sessionId);
|
|
126
|
+
// Re-read status after wait
|
|
127
|
+
status = await sessionManager.getSessionStatus(sessionId);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
131
|
+
if (errMsg === "SESSION_REJECTED") {
|
|
132
|
+
// Will be handled below after re-reading status
|
|
133
|
+
status = await sessionManager.getSessionStatus(sessionId);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
outputResult({ success: false, error: `Error waiting for answers: ${errMsg}` }, jsonMode);
|
|
137
|
+
process.exitCode = 1;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Re-check status after potential wait
|
|
143
|
+
const currentStatus = status?.status ?? "unknown";
|
|
144
|
+
// ── Handle completed session ──────────────────────────────────────
|
|
145
|
+
if (currentStatus === "completed") {
|
|
146
|
+
const request = await sessionManager.getSessionRequest(sessionId);
|
|
147
|
+
let answersData = null;
|
|
148
|
+
try {
|
|
149
|
+
answersData = await sessionManager.getSessionAnswers(sessionId);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// answers.json may not exist yet, treat as pending
|
|
153
|
+
}
|
|
154
|
+
if (!answersData || !request) {
|
|
155
|
+
outputResult({ success: false, error: `Could not read answers for: ${sessionId}` }, jsonMode);
|
|
156
|
+
process.exitCode = 1;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Mark as read
|
|
160
|
+
await sessionManager.markSessionAsRead(sessionId);
|
|
161
|
+
const lastReadAt = new Date().toISOString();
|
|
162
|
+
if (jsonMode) {
|
|
163
|
+
console.log(JSON.stringify({
|
|
164
|
+
sessionId,
|
|
165
|
+
status: "completed",
|
|
166
|
+
answers: answersData.answers,
|
|
167
|
+
lastReadAt,
|
|
168
|
+
}, null, 2));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const formatted = ResponseFormatter.formatUserResponse(answersData, request.questions, sessionId);
|
|
172
|
+
console.log(formatted);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// ── Handle pending/in-progress (non-blocking) ─────────────────────
|
|
176
|
+
if (currentStatus === "pending" || currentStatus === "in-progress") {
|
|
177
|
+
if (jsonMode) {
|
|
178
|
+
console.log(JSON.stringify({ sessionId, status: currentStatus, answers: null, lastReadAt: null }, null, 2));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
console.log(ResponseFormatter.formatPendingStatus(sessionId));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// ── Handle rejected session ───────────────────────────────────────
|
|
185
|
+
if (currentStatus === "rejected") {
|
|
186
|
+
if (jsonMode) {
|
|
187
|
+
console.log(JSON.stringify({
|
|
188
|
+
sessionId,
|
|
189
|
+
status: "rejected",
|
|
190
|
+
answers: null,
|
|
191
|
+
lastReadAt: null,
|
|
192
|
+
rejectionReason: status?.rejectionReason ?? null,
|
|
193
|
+
}, null, 2));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
console.log(ResponseFormatter.formatRejectedStatus(sessionId, status?.rejectionReason));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// ── Handle abandoned / timed_out ──────────────────────────────────
|
|
200
|
+
if (jsonMode) {
|
|
201
|
+
console.log(JSON.stringify({ sessionId, status: currentStatus, answers: null, lastReadAt: null }, null, 2));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
console.log(ResponseFormatter.formatSessionStatus(sessionId, currentStatus));
|
|
205
|
+
}
|
|
@@ -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),
|