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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for the `auq history` CLI command.
|
|
3
|
+
*/
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import { SessionManager } from "../../../session/SessionManager.js";
|
|
7
|
+
import { runHistoryCommand } from "../history.js";
|
|
8
|
+
// ── Test directory ─────────────────────────────────────────────────────────
|
|
9
|
+
const testBaseDir = `/tmp/auq-test-history-${process.pid}`;
|
|
10
|
+
// Stub getSessionDirectory so the history command always targets our temp dir.
|
|
11
|
+
vi.mock("../../../session/utils.js", async (importOriginal) => {
|
|
12
|
+
const actual = (await importOriginal());
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
getSessionDirectory: () => testBaseDir,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
// ── Test fixtures ───────────────────────────────────────────────────────────
|
|
19
|
+
const sampleQuestions = [
|
|
20
|
+
{
|
|
21
|
+
title: "Database",
|
|
22
|
+
prompt: "Which database should we use?",
|
|
23
|
+
options: [
|
|
24
|
+
{ label: "Postgres", description: "Relational, reliable" },
|
|
25
|
+
{ label: "Redis", description: "In-memory cache" },
|
|
26
|
+
],
|
|
27
|
+
multiSelect: false,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
title: "Auth",
|
|
31
|
+
prompt: "Which auth method?",
|
|
32
|
+
options: [
|
|
33
|
+
{ label: "JWT", description: "Stateless tokens" },
|
|
34
|
+
{ label: "Cookies", description: "Server-side sessions" },
|
|
35
|
+
],
|
|
36
|
+
multiSelect: false,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
async function createCompletedSession(mgr, opts) {
|
|
40
|
+
const id = await mgr.createSession(sampleQuestions);
|
|
41
|
+
await mgr.saveSessionAnswers(id, {
|
|
42
|
+
sessionId: id,
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
answers: sampleQuestions.map((q, i) => ({
|
|
45
|
+
questionIndex: i,
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
47
|
+
selectedOption: q.options[0].label,
|
|
48
|
+
})),
|
|
49
|
+
});
|
|
50
|
+
await mgr.updateSessionStatus(id, "completed");
|
|
51
|
+
if (opts?.read) {
|
|
52
|
+
await mgr.markSessionAsRead(id);
|
|
53
|
+
}
|
|
54
|
+
return id;
|
|
55
|
+
}
|
|
56
|
+
async function createRejectedSession(mgr) {
|
|
57
|
+
const id = await mgr.createSession(sampleQuestions);
|
|
58
|
+
await mgr.rejectSession(id, "not relevant");
|
|
59
|
+
return id;
|
|
60
|
+
}
|
|
61
|
+
async function createAbandonedSession(mgr) {
|
|
62
|
+
const id = await mgr.createSession(sampleQuestions);
|
|
63
|
+
await mgr.updateSessionStatus(id, "abandoned");
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
66
|
+
// ── Test Suite ──────────────────────────────────────────────────────────────
|
|
67
|
+
describe("history command", () => {
|
|
68
|
+
let sessionManager;
|
|
69
|
+
let consoleLogSpy;
|
|
70
|
+
let consoleErrorSpy;
|
|
71
|
+
beforeEach(async () => {
|
|
72
|
+
await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
|
|
73
|
+
sessionManager = new SessionManager({ baseDir: testBaseDir });
|
|
74
|
+
await sessionManager.initialize();
|
|
75
|
+
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
76
|
+
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
77
|
+
process.exitCode = undefined;
|
|
78
|
+
});
|
|
79
|
+
afterEach(async () => {
|
|
80
|
+
consoleLogSpy.mockRestore();
|
|
81
|
+
consoleErrorSpy.mockRestore();
|
|
82
|
+
process.exitCode = undefined;
|
|
83
|
+
await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
|
|
84
|
+
});
|
|
85
|
+
// ── List (default) ──────────────────────────────────────────────────────
|
|
86
|
+
describe("list (default)", () => {
|
|
87
|
+
it("should list non-abandoned sessions by default", async () => {
|
|
88
|
+
await createCompletedSession(sessionManager);
|
|
89
|
+
const abandonedId = await createAbandonedSession(sessionManager);
|
|
90
|
+
await runHistoryCommand([]);
|
|
91
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
92
|
+
expect(output).toContain("completed");
|
|
93
|
+
// Abandoned session's short ID should not appear as a table row
|
|
94
|
+
expect(output).not.toContain(abandonedId.slice(0, 8));
|
|
95
|
+
});
|
|
96
|
+
it("should show abandoned hint when hidden", async () => {
|
|
97
|
+
await createCompletedSession(sessionManager);
|
|
98
|
+
await createAbandonedSession(sessionManager);
|
|
99
|
+
await runHistoryCommand([]);
|
|
100
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
101
|
+
expect(output).toContain("abandoned hidden");
|
|
102
|
+
expect(output).toContain("--all");
|
|
103
|
+
});
|
|
104
|
+
it("should show all with --all flag", async () => {
|
|
105
|
+
await createCompletedSession(sessionManager);
|
|
106
|
+
const abandonedId = await createAbandonedSession(sessionManager);
|
|
107
|
+
await runHistoryCommand(["--all"]);
|
|
108
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
109
|
+
expect(output).toContain(abandonedId.slice(0, 8));
|
|
110
|
+
});
|
|
111
|
+
it("should show empty state message", async () => {
|
|
112
|
+
await runHistoryCommand([]);
|
|
113
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
114
|
+
expect(output).toContain("No sessions found");
|
|
115
|
+
});
|
|
116
|
+
it("should limit output with --limit", async () => {
|
|
117
|
+
await createCompletedSession(sessionManager);
|
|
118
|
+
await createCompletedSession(sessionManager);
|
|
119
|
+
await createCompletedSession(sessionManager);
|
|
120
|
+
await runHistoryCommand(["--limit", "2"]);
|
|
121
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
122
|
+
// Match 8-character lowercase hex strings (UUID short IDs)
|
|
123
|
+
const matches = output.match(/[a-f0-9]{8}/g) || [];
|
|
124
|
+
const uniqueIds = [...new Set(matches)];
|
|
125
|
+
expect(uniqueIds.length).toBeLessThanOrEqual(2);
|
|
126
|
+
});
|
|
127
|
+
it("should filter unread with --unread", async () => {
|
|
128
|
+
const unreadId = await createCompletedSession(sessionManager);
|
|
129
|
+
await createCompletedSession(sessionManager, { read: true });
|
|
130
|
+
await runHistoryCommand(["--unread"]);
|
|
131
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
132
|
+
expect(output).toContain(unreadId.slice(0, 8));
|
|
133
|
+
});
|
|
134
|
+
it("should search with --search", async () => {
|
|
135
|
+
await createCompletedSession(sessionManager);
|
|
136
|
+
await runHistoryCommand(["--search", "database"]);
|
|
137
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
138
|
+
expect(output).toContain("completed");
|
|
139
|
+
});
|
|
140
|
+
it("should output JSON with --json", async () => {
|
|
141
|
+
await createCompletedSession(sessionManager);
|
|
142
|
+
await runHistoryCommand(["--json"]);
|
|
143
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
144
|
+
const parsed = JSON.parse(output);
|
|
145
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
146
|
+
expect(parsed[0]).toHaveProperty("sessionId");
|
|
147
|
+
expect(parsed[0]).toHaveProperty("status");
|
|
148
|
+
expect(parsed[0]).toHaveProperty("questionCount");
|
|
149
|
+
});
|
|
150
|
+
it("should find session with --session", async () => {
|
|
151
|
+
const id = await createCompletedSession(sessionManager);
|
|
152
|
+
await runHistoryCommand(["--session", id.slice(0, 8)]);
|
|
153
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
154
|
+
expect(output).toContain(id.slice(0, 8));
|
|
155
|
+
});
|
|
156
|
+
it("should error for --session with no match", async () => {
|
|
157
|
+
await runHistoryCommand(["--session", "00000000"]);
|
|
158
|
+
expect(process.exitCode).toBe(1);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
// ── Show ────────────────────────────────────────────────────────────────
|
|
162
|
+
describe("show", () => {
|
|
163
|
+
it("should show session details", async () => {
|
|
164
|
+
const id = await createCompletedSession(sessionManager);
|
|
165
|
+
await runHistoryCommand(["show", id]);
|
|
166
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
167
|
+
expect(output).toContain(id);
|
|
168
|
+
expect(output).toContain("completed");
|
|
169
|
+
expect(output).toContain("Which database should we use?");
|
|
170
|
+
});
|
|
171
|
+
it("should show all options with (selected) prefix for answered options", async () => {
|
|
172
|
+
const id = await createCompletedSession(sessionManager);
|
|
173
|
+
await runHistoryCommand(["show", id]);
|
|
174
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
175
|
+
expect(output).toContain("(selected)");
|
|
176
|
+
expect(output).toContain("Postgres");
|
|
177
|
+
expect(output).toContain("Redis");
|
|
178
|
+
});
|
|
179
|
+
it("should resolve short session ID", async () => {
|
|
180
|
+
const id = await createCompletedSession(sessionManager);
|
|
181
|
+
await runHistoryCommand(["show", id.slice(0, 8)]);
|
|
182
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
183
|
+
expect(output).toContain(id);
|
|
184
|
+
});
|
|
185
|
+
it("should error for non-existent session", async () => {
|
|
186
|
+
await runHistoryCommand(["show", "nonexistent-id"]);
|
|
187
|
+
expect(process.exitCode).toBe(1);
|
|
188
|
+
});
|
|
189
|
+
it("should show rejected session", async () => {
|
|
190
|
+
const id = await createRejectedSession(sessionManager);
|
|
191
|
+
await runHistoryCommand(["show", id]);
|
|
192
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
193
|
+
expect(output).toContain("rejected");
|
|
194
|
+
});
|
|
195
|
+
it("should show abandoned session", async () => {
|
|
196
|
+
const id = await createAbandonedSession(sessionManager);
|
|
197
|
+
await runHistoryCommand(["show", id]);
|
|
198
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
199
|
+
expect(output).toContain("abandoned");
|
|
200
|
+
});
|
|
201
|
+
it("should output JSON with --json", async () => {
|
|
202
|
+
const id = await createCompletedSession(sessionManager);
|
|
203
|
+
await runHistoryCommand(["show", id, "--json"]);
|
|
204
|
+
const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
205
|
+
const parsed = JSON.parse(output);
|
|
206
|
+
expect(parsed.sessionId).toBe(id);
|
|
207
|
+
expect(parsed.questions).toBeInstanceOf(Array);
|
|
208
|
+
expect(parsed.questions[0].options).toBeInstanceOf(Array);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -84,6 +84,17 @@ export async function runAnswerCommand(args) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
catch (err) {
|
|
87
|
+
if (!jsonMode) {
|
|
88
|
+
process.stderr.write(`Error: Invalid --answers JSON format.\n\n` +
|
|
89
|
+
`Expected format:\n` +
|
|
90
|
+
` auq answer <sessionId> --answers '{"0": {"selectedOption": "Label Name"}}'\n\n` +
|
|
91
|
+
`Answer object keys are question indices (0, 1, 2, ...). Each value can have:\n` +
|
|
92
|
+
` \u2022 selectedOption: "Label" \u2014 for single-select questions\n` +
|
|
93
|
+
` \u2022 selectedOptions: ["A", "B"] \u2014 for multi-select questions\n` +
|
|
94
|
+
` \u2022 customText: "free text" \u2014 for custom/other input\n\n` +
|
|
95
|
+
`Example (multi-question):\n` +
|
|
96
|
+
` auq answer abc123 --answers '{"0": {"selectedOption": "Yes"}, "1": {"customText": "my input"}}'\n\n`);
|
|
97
|
+
}
|
|
87
98
|
outputResult({
|
|
88
99
|
success: false,
|
|
89
100
|
error: `Invalid answers JSON: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -155,6 +155,36 @@ async function configGet(args) {
|
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
const value = getNestedValue(config, key);
|
|
158
|
+
// Special handling for renderer key: show env var override and source
|
|
159
|
+
if (key === "renderer") {
|
|
160
|
+
const envRenderer = process.env.AUQ_RENDERER;
|
|
161
|
+
let displayValue;
|
|
162
|
+
let source;
|
|
163
|
+
if (envRenderer) {
|
|
164
|
+
displayValue = envRenderer;
|
|
165
|
+
source = "AUQ_RENDERER";
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const paths = getConfigPaths();
|
|
169
|
+
const localConfig = readConfigFileForWrite(paths.local);
|
|
170
|
+
const globalConfig = readConfigFileForWrite(paths.global);
|
|
171
|
+
const explicitlySet = localConfig.renderer !== undefined || globalConfig.renderer !== undefined;
|
|
172
|
+
displayValue = String(value);
|
|
173
|
+
source = explicitlySet ? "config" : "default";
|
|
174
|
+
}
|
|
175
|
+
if (jsonMode) {
|
|
176
|
+
console.log(JSON.stringify({ success: true, key, value: displayValue, source }, null, 2));
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
const suffix = source === "AUQ_RENDERER"
|
|
180
|
+
? " (from AUQ_RENDERER)"
|
|
181
|
+
: source === "default"
|
|
182
|
+
? " (default)"
|
|
183
|
+
: "";
|
|
184
|
+
console.log(`${key} = ${displayValue}${suffix}`);
|
|
185
|
+
}
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
158
188
|
if (jsonMode) {
|
|
159
189
|
console.log(JSON.stringify({ success: true, key, value }, null, 2));
|
|
160
190
|
}
|
|
@@ -210,6 +240,22 @@ async function configSet(args) {
|
|
|
210
240
|
const issues = validation.error.issues
|
|
211
241
|
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
212
242
|
.join("; ");
|
|
243
|
+
// Build key-specific hint for human-readable mode
|
|
244
|
+
if (!jsonMode) {
|
|
245
|
+
const hints = {
|
|
246
|
+
maxOptions: "Expected: number between 2 and 10",
|
|
247
|
+
maxQuestions: "Expected: number between 1 and 10",
|
|
248
|
+
sessionTimeout: "Expected: number (milliseconds, e.g. 300000 for 5 minutes)",
|
|
249
|
+
renderer: 'Expected: \"ink\" or \"opentui\"',
|
|
250
|
+
staleAction: 'Expected: \"warn\", \"remove\", or \"archive\"',
|
|
251
|
+
theme: "Expected: a valid theme name (see auq config get theme)",
|
|
252
|
+
language: "Expected: a language code (e.g. \"en\", \"ko\")",
|
|
253
|
+
};
|
|
254
|
+
const hint = hints[key];
|
|
255
|
+
process.stderr.write(`Error: Invalid value "${rawValue}" for key "${key}".\n\n` +
|
|
256
|
+
(hint ? `${hint}\n\n` : "") +
|
|
257
|
+
`Usage: auq config set ${key} <value> [--global]\n\n`);
|
|
258
|
+
}
|
|
213
259
|
outputResult({
|
|
214
260
|
success: false,
|
|
215
261
|
error: `Invalid value for "${key}": ${issues}`,
|
|
@@ -255,6 +301,8 @@ export async function runConfigCommand(args) {
|
|
|
255
301
|
console.log(" auq config get staleThreshold --json");
|
|
256
302
|
console.log(" auq config set staleThreshold 3600000");
|
|
257
303
|
console.log(" auq config set notifyOnStale false --global");
|
|
304
|
+
console.log(" auq config get renderer");
|
|
305
|
+
console.log(" auq config set renderer opentui");
|
|
258
306
|
if (subcommand !== undefined) {
|
|
259
307
|
process.exitCode = 1;
|
|
260
308
|
}
|
|
@@ -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
|
+
}
|