auq-mcp-server 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +122 -0
  2. package/dist/bin/auq.js +87 -93
  3. package/dist/bin/tui-app.js +183 -7
  4. package/dist/package.json +1 -1
  5. package/dist/src/__tests__/server.abort.test.js +214 -0
  6. package/dist/src/cli/commands/__tests__/answer.test.js +199 -0
  7. package/dist/src/cli/commands/__tests__/config.test.js +218 -0
  8. package/dist/src/cli/commands/__tests__/sessions.test.js +282 -0
  9. package/dist/src/cli/commands/answer.js +128 -0
  10. package/dist/src/cli/commands/config.js +263 -0
  11. package/dist/src/cli/commands/sessions.js +300 -0
  12. package/dist/src/cli/commands/update.js +124 -0
  13. package/dist/src/cli/utils.js +95 -0
  14. package/dist/src/config/__tests__/ConfigLoader.test.js +41 -0
  15. package/dist/src/config/__tests__/updateCheck.test.js +34 -0
  16. package/dist/src/config/defaults.js +5 -0
  17. package/dist/src/config/types.js +6 -0
  18. package/dist/src/core/ask-user-questions.js +3 -2
  19. package/dist/src/i18n/locales/en.js +7 -0
  20. package/dist/src/i18n/locales/ko.js +7 -0
  21. package/dist/src/server.js +64 -11
  22. package/dist/src/session/SessionManager.js +69 -4
  23. package/dist/src/session/__tests__/SessionManager.test.js +65 -0
  24. package/dist/src/tui/__tests__/session-watcher.test.js +109 -0
  25. package/dist/src/tui/components/Footer.js +4 -1
  26. package/dist/src/tui/components/Header.js +3 -1
  27. package/dist/src/tui/components/SessionDots.js +33 -4
  28. package/dist/src/tui/components/SessionPicker.js +25 -17
  29. package/dist/src/tui/components/StepperView.js +68 -5
  30. package/dist/src/tui/components/UpdateBadge.js +29 -0
  31. package/dist/src/tui/components/UpdateOverlay.js +199 -0
  32. package/dist/src/tui/components/__tests__/SessionDots.test.js +160 -1
  33. package/dist/src/tui/components/__tests__/SessionPicker.test.js +43 -1
  34. package/dist/src/tui/components/__tests__/StepperView.abandoned.test.js +160 -0
  35. package/dist/src/tui/components/__tests__/StepperView.state.test.js +1 -0
  36. package/dist/src/tui/constants/keybindings.js +3 -0
  37. package/dist/src/tui/session-watcher.js +50 -0
  38. package/dist/src/tui/themes/catppuccin-latte.js +7 -0
  39. package/dist/src/tui/themes/catppuccin-mocha.js +7 -0
  40. package/dist/src/tui/themes/dark.js +7 -0
  41. package/dist/src/tui/themes/dracula.js +7 -0
  42. package/dist/src/tui/themes/github-dark.js +7 -0
  43. package/dist/src/tui/themes/github-light.js +7 -0
  44. package/dist/src/tui/themes/gruvbox-dark.js +7 -0
  45. package/dist/src/tui/themes/gruvbox-light.js +7 -0
  46. package/dist/src/tui/themes/light.js +7 -0
  47. package/dist/src/tui/themes/monokai.js +7 -0
  48. package/dist/src/tui/themes/nord.js +7 -0
  49. package/dist/src/tui/themes/one-dark.js +7 -0
  50. package/dist/src/tui/themes/rose-pine.js +7 -0
  51. package/dist/src/tui/themes/solarized-dark.js +7 -0
  52. package/dist/src/tui/themes/solarized-light.js +7 -0
  53. package/dist/src/tui/themes/tokyo-night.js +7 -0
  54. package/dist/src/tui/utils/__tests__/staleDetection.test.js +118 -0
  55. package/dist/src/tui/utils/staleDetection.js +51 -0
  56. package/dist/src/update/__tests__/cache.test.js +136 -0
  57. package/dist/src/update/__tests__/changelog.test.js +86 -0
  58. package/dist/src/update/__tests__/checker.test.js +148 -0
  59. package/dist/src/update/__tests__/index.test.js +37 -0
  60. package/dist/src/update/__tests__/installer.test.js +117 -0
  61. package/dist/src/update/__tests__/package-manager.test.js +73 -0
  62. package/dist/src/update/__tests__/version.test.js +74 -0
  63. package/dist/src/update/cache.js +74 -0
  64. package/dist/src/update/changelog.js +63 -0
  65. package/dist/src/update/checker.js +121 -0
  66. package/dist/src/update/index.js +15 -0
  67. package/dist/src/update/installer.js +51 -0
  68. package/dist/src/update/package-manager.js +49 -0
  69. package/dist/src/update/types.js +7 -0
  70. package/dist/src/update/version.js +114 -0
  71. package/package.json +1 -1
package/README.md CHANGED
@@ -248,6 +248,7 @@ It is recommended to **disable** the built-in questioning tool in your harness (
248
248
  # you won't likely need these at all
249
249
  auq server # Start MCP server
250
250
  auq --version # Show version
251
+ auq update # Check for and install updates
251
252
  auq --help # Show help
252
253
  ```
253
254
 
@@ -255,6 +256,122 @@ auq --help # Show help
255
256
 
256
257
  ---
257
258
 
259
+ ### CLI Commands
260
+
261
+ AUQ provides headless CLI commands for managing sessions and configuration without the TUI.
262
+
263
+ #### Answer Sessions
264
+
265
+ ```bash
266
+ # Answer a session with JSON answers
267
+ auq answer <sessionId> --answers '{"0": {"selectedOption": "option1"}}'
268
+
269
+ # Reject a session
270
+ auq answer <sessionId> --reject --reason "Not applicable"
271
+
272
+ # Force answer an abandoned session
273
+ auq answer <sessionId> --answers '...' --force
274
+
275
+ # JSON output
276
+ auq answer <sessionId> --answers '...' --json
277
+ ```
278
+
279
+ #### Manage Sessions
280
+
281
+ ```bash
282
+ # List pending sessions
283
+ auq sessions list
284
+
285
+ # List stale sessions only
286
+ auq sessions list --stale
287
+
288
+ # List all sessions (including completed, abandoned)
289
+ auq sessions list --all
290
+
291
+ # Dismiss/archive a session
292
+ auq sessions dismiss <sessionId>
293
+
294
+ # JSON output
295
+ auq sessions list --json
296
+ ```
297
+
298
+ #### Configuration
299
+
300
+ ```bash
301
+ # View all configuration
302
+ auq config get
303
+
304
+ # View specific setting
305
+ auq config get staleThreshold
306
+
307
+ # Set a value (local .auqrc.json)
308
+ auq config set staleThreshold 3600000
309
+
310
+ # Set globally
311
+ auq config set staleThreshold 3600000 --global
312
+ ```
313
+
314
+ ---
315
+
316
+ ### Stale Session Detection
317
+
318
+ Sessions that remain unanswered longer than the configured threshold are marked as "stale" (potentially orphaned). This helps identify sessions where the AI may have disconnected or timed out.
319
+
320
+ - **Visual indicators**: Stale sessions show a ⚠ warning icon and yellow highlighting in the TUI
321
+ - **Toast notifications**: A notification appears when a session becomes stale (configurable)
322
+ - **Grace period**: Interacting with a stale session provides a 30-minute grace period
323
+ - **Configurable threshold**: Default is 2 hours (7,200,000ms)
324
+
325
+ ---
326
+
327
+ ### Abandoned Session Handling
328
+
329
+ When an AI client disconnects, associated sessions are marked as "abandoned". These sessions:
330
+
331
+ - Remain visible in the TUI with a red indicator
332
+ - Show a confirmation dialog before answering ("AI가 disconnect되었습니다")
333
+ - Can still be answered via CLI with the `--force` flag
334
+ - Are detectable via `auq sessions list --all`
335
+
336
+ ---
337
+
338
+ ### Auto-Update
339
+
340
+ AUQ automatically checks for updates and keeps itself up to date.
341
+
342
+ #### How it works
343
+
344
+ - **Patch updates** (e.g., 2.4.0 → 2.4.1): Automatically installed when the TUI starts. These are bug fixes and minor improvements.
345
+ - **Minor/Major updates** (e.g., 2.4.0 → 2.5.0 or 3.0.0): A fullscreen prompt is shown with changelog and options to update, skip, or defer.
346
+ - **CLI notification**: When running non-TUI commands, a one-line update notification is shown if a newer version is available.
347
+
348
+ #### Manual update
349
+
350
+ Run `auq update` to manually check for and install updates:
351
+
352
+ ```bash
353
+ auq update # Interactive update check
354
+ auq update -y # Skip confirmation prompt
355
+ ```
356
+
357
+ #### Disabling update checks
358
+
359
+ Disable automatic update checks via config:
360
+
361
+ ```bash
362
+ auq config set updateCheck false
363
+ ```
364
+
365
+ Or set the environment variable:
366
+
367
+ ```bash
368
+ NO_UPDATE_NOTIFIER=1 auq ask "question"
369
+ ```
370
+
371
+ Update checks are automatically disabled in CI environments (`CI=true`).
372
+
373
+ The `auq update` command always works regardless of these settings.
374
+
258
375
  ### 🎨 Themes
259
376
 
260
377
  AUQ supports **16 built-in color themes** with automatic persistence. Press `Ctrl+T` to cycle through themes.
@@ -439,6 +556,7 @@ _Settings from local config override global config, which overrides defaults._
439
556
  "language": "auto",
440
557
  "theme": "system",
441
558
  "autoSelectRecommended": true,
559
+ "updateCheck": true,
442
560
  "notifications": {
443
561
  "enabled": true,
444
562
  "sound": true
@@ -461,6 +579,10 @@ _Settings from local config override global config, which overrides defaults._
461
579
  | `retentionPeriod` | number | 604800000 | 0+ (milliseconds) | How long to keep completed sessions (default: 7 days) |
462
580
  | `notifications.enabled` | boolean | true | true/false | Enable desktop notifications for new questions |
463
581
  | `notifications.sound` | boolean | true | true/false | Play sound with notifications |
582
+ | `staleThreshold` | number | 7200000 | 0+ (milliseconds) | Time before a session is considered stale (2 hours) |
583
+ | `notifyOnStale` | boolean | true | true/false | Show toast notification when sessions become stale |
584
+ | `staleAction` | string | "warn" | "warn", "remove", "archive" | Action for stale sessions |
585
+ | `updateCheck` | boolean | true | true/false | Enable automatic update checks on startup |
464
586
 
465
587
  </details>
466
588
 
package/dist/bin/auq.js CHANGED
@@ -9,106 +9,44 @@ import { getSessionDirectory } from "../src/session/utils.js";
9
9
  const args = process.argv.slice(2);
10
10
  const command = args[0];
11
11
  if (command === "--help" || command === "-h") {
12
- console.log(`
13
- AUQ - Ask User Questions
12
+ console.log(`auq - Ask User Questions (MCP server + TUI)
14
13
 
15
- An MCP server and TUI for AI assistants to ask users structured questions.
16
-
17
- Usage:
18
- auq [command] [options]
14
+ Usage: auq [command] [options]
19
15
 
20
16
  Commands:
21
- (default) Start the interactive TUI (Terminal User Interface)
22
- server Start the MCP server (for use with AI assistants)
23
- ask <json> Ask questions via CLI (pipe or argument)
17
+ (default) Start interactive TUI
18
+ server Start MCP server (stdio)
19
+ ask <json> Ask questions via CLI
20
+ answer <id> [flags] Answer or reject a session
21
+ sessions <sub> [flags] List/dismiss sessions
22
+ config <sub> [flags] Get/set configuration
23
+ update Check for and install updates
24
+
25
+ Answer:
26
+ auq answer <id> --answers '<json>' Submit answers
27
+ auq answer <id> --reject [--reason] Reject session
28
+ Flags: --force --json
29
+
30
+ Sessions:
31
+ auq sessions list [--pending|--stale|--all] [--json]
32
+ auq sessions show <id> [--json]
33
+ auq sessions dismiss <id> [--force] [--json]
34
+
35
+ Config:
36
+ auq config get [key] [--json]
37
+ auq config set <key> <value> [--global] [--json]
24
38
 
25
39
  Options:
26
- --help, -h Show this help message
27
- --version, -v Show version information
28
-
29
- TUI Keyboard Shortcuts:
30
- Navigation:
31
- ↑/↓ Navigate options
32
- ←/→ Navigate questions
33
- Tab/Shift+Tab Navigate questions
34
-
35
- Selection:
36
- Space Select/toggle option (multi-select)
37
- Enter Select option & advance to next question
38
- R Select recommended option(s)
39
- Ctrl+R Quick submit (auto-select all recommended)
40
-
41
- Session Management:
42
- ] Next session
43
- [ Previous session
44
- 1-9 Jump to session by number
45
- Ctrl+S Open session picker
46
-
47
- Other:
48
- E Request elaboration on current question
49
- Ctrl+T Cycle color theme
50
- Esc Reject question set
51
-
52
- Ask Command:
53
- Use 'auq ask' when you need to ask the user questions during
54
- execution. This allows you to:
55
- 1. Gather user preferences or requirements
56
- 2. Clarify ambiguous instructions
57
- 3. Get decisions on implementation choices as you work
58
- 4. Offer choices to the user about what direction to take
59
-
60
- Features:
61
- - Ask 1-5 structured questions via an interactive terminal UI
62
- - Each question includes 2-5 multiple-choice options
63
- - Users can always provide custom free-text input
64
- - Single-select mode (default): pick ONE option or custom text
65
- - Multi-select mode (multiSelect: true): select MULTIPLE options
40
+ -h, --help Show this help
41
+ -v, --version Show version
66
42
 
67
- Usage Notes:
68
- - Provide a descriptive 'title' field (max 12 chars) per question
69
- - Use multiSelect: true when choices are not mutually exclusive
70
- - Option labels should be concise (1-5 words)
71
- - To mark recommended, append '(recommended)' to option label
72
- - Don't include an 'Other' option — it's provided automatically
43
+ Keys (TUI):
44
+ ↑↓ navigate ←→/Tab questions Space toggle Enter select
45
+ R recommend Ctrl+R quick-submit Esc reject
46
+ [/] sessions 1-9 jump Ctrl+S picker Ctrl+T theme
73
47
 
74
- Returns a formatted summary of all questions and answers.
75
-
76
- Configuration:
77
- Config file locations (searched in order, merged):
78
- ./.auqrc.json Project-level (highest priority)
79
- ~/.config/auq/.auqrc.json User-level (global)
80
-
81
- Available options (with defaults):
82
- maxOptions Max options per question (2-10, default: 5)
83
- maxQuestions Max questions per session (1-10, default: 5)
84
- recommendedOptions Recommended option count hint (default: 4)
85
- recommendedQuestions Recommended question count hint (default: 4)
86
- sessionTimeout Session timeout in ms (0 = infinite, default: 0)
87
- retentionPeriod Session retention in ms (default: 604800000 / 7d)
88
- language UI language ("auto" | "en" | "ko", default: "auto")
89
- theme Color theme ("system" | "dark" | "light" | custom,
90
- default: "system")
91
- autoSelectRecommended Pre-select recommended options (default: true)
92
- notifications.enabled Enable desktop notifications (default: true)
93
- notifications.sound Enable notification sounds (default: true)
94
-
95
- Custom themes: place .theme.json files in ~/.config/auq/themes/
96
-
97
- Environment Variables:
98
- AUQ_SESSION_DIR Override session storage directory
99
- XDG_CONFIG_HOME Override config base directory (default: ~/.config)
100
-
101
- Examples:
102
- auq # Start TUI (wait for questions from AI)
103
- auq server # Start MCP server (for Claude Desktop, etc.)
104
- auq ask '{"questions": [{"prompt": "Which language?", "title": "Lang",
105
- "options": [{"label": "TypeScript (recommended)"}, {"label": "Python"}],
106
- "multiSelect": false}]}'
107
- echo '{"questions": [...]}' | auq ask # Pipe JSON to ask command
108
-
109
- For more information, visit:
110
- https://github.com/paulp-o/ask-user-questions-mcp
111
- `);
48
+ Config: ./.auqrc.json (local) > ~/.config/auq/.auqrc.json (global)
49
+ Env: AUQ_SESSION_DIR XDG_CONFIG_HOME`);
112
50
  process.exit(0);
113
51
  }
114
52
  // Display version
@@ -148,6 +86,40 @@ if (command === "server") {
148
86
  // Keep process alive
149
87
  await new Promise(() => { });
150
88
  }
89
+ // Handle 'update' command
90
+ if (command === "update") {
91
+ const { runUpdateCommand } = await import("../src/cli/commands/update.js");
92
+ await runUpdateCommand(args.slice(1));
93
+ process.exit(0);
94
+ }
95
+ // ── Fire-and-forget update notification ────────────────────────────
96
+ // Start a non-blocking update check for non-TUI CLI commands.
97
+ // The result is awaited briefly after the main command finishes.
98
+ let updateNotification = null;
99
+ if (command &&
100
+ !["server", "--help", "-h", "--version", "-v", "update"].includes(command)) {
101
+ updateNotification = (async () => {
102
+ try {
103
+ if (process.env.NO_UPDATE_NOTIFIER === "1" ||
104
+ process.env.CI === "true" ||
105
+ process.env.CI === "1" ||
106
+ process.env.NODE_ENV === "test")
107
+ return;
108
+ const { UpdateChecker } = await import("../src/update/index.js");
109
+ const checker = new UpdateChecker();
110
+ const result = await Promise.race([
111
+ checker.check(),
112
+ new Promise((r) => setTimeout(() => r(null), 5000)),
113
+ ]);
114
+ if (result) {
115
+ process.stderr.write(`Update available: ${result.currentVersion} \u2192 ${result.latestVersion}. Run \`auq update\` to upgrade.\n`);
116
+ }
117
+ }
118
+ catch {
119
+ // Silently ignore — update checks must never break the main command
120
+ }
121
+ })();
122
+ }
151
123
  // Handle 'ask' command
152
124
  if (command === "ask") {
153
125
  const { SessionManager } = await import("../src/session/index.js");
@@ -207,6 +179,7 @@ if (command === "ask") {
207
179
  const callId = randomUUID();
208
180
  const { formattedResponse, sessionId } = await sessionManager.startSession(questions, callId, workingDirectory);
209
181
  console.log(formattedResponse);
182
+ await updateNotification;
210
183
  process.exit(0);
211
184
  }
212
185
  catch (error) {
@@ -220,6 +193,27 @@ if (command === "ask") {
220
193
  process.exit(1);
221
194
  }
222
195
  }
196
+ // Handle 'answer' command
197
+ if (command === "answer") {
198
+ const { runAnswerCommand } = await import("../src/cli/commands/answer.js");
199
+ await runAnswerCommand(args.slice(1));
200
+ await updateNotification;
201
+ process.exit(0);
202
+ }
203
+ // Handle 'sessions' command
204
+ if (command === "sessions") {
205
+ const { runSessionsCommand } = await import("../src/cli/commands/sessions.js");
206
+ await runSessionsCommand(args.slice(1));
207
+ await updateNotification;
208
+ process.exit(0);
209
+ }
210
+ // Handle 'config' command
211
+ if (command === "config") {
212
+ const { runConfigCommand } = await import("../src/cli/commands/config.js");
213
+ await runConfigCommand(args.slice(1));
214
+ await updateNotification;
215
+ process.exit(0);
216
+ }
223
217
  // Default: Start TUI
224
218
  // Important: Lazy-load Ink/React so non-interactive commands (ask/server) don't pull them in.
225
219
  // Also force production mode before importing React/Ink to avoid perf_hooks measure accumulation warnings.
@@ -11,9 +11,12 @@ import { Toast } from "../src/tui/components/Toast.js";
11
11
  import { WaitingScreen } from "../src/tui/components/WaitingScreen.js";
12
12
  import { createNotificationBatcher, showProgress, clearProgress, calculateProgress, checkLinuxDependencies, } from "../src/tui/notifications/index.js";
13
13
  import { createTUIWatcher } from "../src/tui/session-watcher.js";
14
+ import { isSessionStale, isSessionAbandoned, formatStaleToastMessage, } from "../src/tui/utils/staleDetection.js";
14
15
  import { ThemeProvider } from "../src/tui/ThemeProvider.js";
15
16
  import { ConfigProvider } from "../src/tui/ConfigContext.js";
16
17
  import { getAdjustedIndexAfterRemoval, getDirectJumpIndex, getNextSessionIndex, getPrevSessionIndex, } from "../src/tui/utils/sessionSwitching.js";
18
+ import { UpdateChecker, fetchChangelog, installUpdate, detectPackageManager, readCache, writeCache, } from "../src/update/index.js";
19
+ import { UpdateOverlay } from "../src/tui/components/UpdateOverlay.js";
17
20
  import { KEYS } from "../src/tui/constants/keybindings.js";
18
21
  const App = ({ config }) => {
19
22
  const [state, setState] = useState({ mode: "WAITING" });
@@ -25,6 +28,15 @@ const App = ({ config }) => {
25
28
  const [showSessionLog, setShowSessionLog] = useState(true);
26
29
  const [showSessionPicker, setShowSessionPicker] = useState(false);
27
30
  const [isInReviewOrRejection, setIsInReviewOrRejection] = useState(false);
31
+ const [sessionMeta, setSessionMeta] = useState(new Map());
32
+ const [lastInteractions, setLastInteractions] = useState(new Map());
33
+ const [staleToastShown, setStaleToastShown] = useState(new Set());
34
+ const [updateInfo, setUpdateInfo] = useState(null);
35
+ const [showUpdateOverlay, setShowUpdateOverlay] = useState(false);
36
+ const [isInstallingUpdate, setIsInstallingUpdate] = useState(false);
37
+ const [installError, setInstallError] = useState(null);
38
+ const [changelogContent, setChangelogContent] = useState(null);
39
+ const [updateDismissed, setUpdateDismissed] = useState(false);
28
40
  // Get session directory for logging
29
41
  const sessionDir = getSessionDirectory();
30
42
  // Notification configuration from config
@@ -54,6 +66,7 @@ const App = ({ config }) => {
54
66
  // Step 1: Load existing pending sessions
55
67
  const watcher = createTUIWatcher();
56
68
  const sessionIds = await watcher.getPendingSessions();
69
+ const sessionsWithStatus = await watcher.getPendingSessionsWithStatus();
57
70
  const sessionData = await Promise.all(sessionIds.map(async (sessionId) => {
58
71
  const sessionRequest = await watcher.getSessionRequest(sessionId);
59
72
  if (!sessionRequest)
@@ -69,6 +82,12 @@ const App = ({ config }) => {
69
82
  .filter((s) => s !== null)
70
83
  .sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
71
84
  setSessionQueue(validSessions);
85
+ // Build initial sessionMeta from status data
86
+ const initialMeta = new Map();
87
+ for (const meta of sessionsWithStatus) {
88
+ initialMeta.set(meta.sessionId, { status: meta.status, createdAt: meta.createdAt });
89
+ }
90
+ setSessionMeta(initialMeta);
72
91
  setIsInitialized(true);
73
92
  // Step 2: Start persistent watcher for new sessions
74
93
  watcherInstance = createTUIWatcher({ autoLoadData: true });
@@ -113,6 +132,45 @@ const App = ({ config }) => {
113
132
  clearProgress(notificationConfig);
114
133
  };
115
134
  }, [notificationConfig]);
135
+ // ── Auto-update checker ─────────────────────────────────────
136
+ useEffect(() => {
137
+ // Skip update checks if disabled
138
+ if (config?.updateCheck === false)
139
+ return;
140
+ if (process.env.NO_UPDATE_NOTIFIER === "1")
141
+ return;
142
+ if (process.env.CI === "true" || process.env.CI === "1")
143
+ return;
144
+ if (process.env.NODE_ENV === "test")
145
+ return;
146
+ if (!process.stdout.isTTY)
147
+ return;
148
+ const checker = new UpdateChecker();
149
+ let intervalId = null;
150
+ const runCheck = async () => {
151
+ try {
152
+ const result = await checker.check();
153
+ if (result) {
154
+ setUpdateInfo(result);
155
+ // Fetch changelog for the overlay
156
+ const changelog = await fetchChangelog(result.latestVersion);
157
+ setChangelogContent(changelog.content);
158
+ }
159
+ }
160
+ catch {
161
+ // Silently fail — update checks should never break the TUI
162
+ }
163
+ };
164
+ runCheck();
165
+ intervalId = setInterval(() => {
166
+ checker.clearCache();
167
+ runCheck();
168
+ }, 3600000); // 1 hour
169
+ return () => {
170
+ if (intervalId)
171
+ clearInterval(intervalId);
172
+ };
173
+ }, [config?.updateCheck]);
116
174
  // Auto-transition: WAITING → PROCESSING when queue has items
117
175
  useEffect(() => {
118
176
  if (!isInitialized)
@@ -153,8 +211,7 @@ const App = ({ config }) => {
153
211
  const parsed = JSON.parse(content);
154
212
  if (parsed.status === "timed_out" ||
155
213
  parsed.status === "completed" ||
156
- parsed.status === "rejected" ||
157
- parsed.status === "abandoned") {
214
+ parsed.status === "rejected") {
158
215
  return {
159
216
  notifyAsTimedOut: parsed.status === "timed_out",
160
217
  session,
@@ -215,11 +272,47 @@ const App = ({ config }) => {
215
272
  const interval = setInterval(() => {
216
273
  void checkPausedSessionStatuses();
217
274
  }, 2000);
275
+ // --- Stale detection (runs alongside status polling) ---
276
+ const staleThreshold = config?.staleThreshold ?? 7200000;
277
+ const notifyOnStale = config?.notifyOnStale ?? true;
278
+ const runStaleDetection = async () => {
279
+ // Refresh session metadata from disk
280
+ const watcher = createTUIWatcher();
281
+ let freshMeta = [];
282
+ try {
283
+ freshMeta = await watcher.getPendingSessionsWithStatus();
284
+ }
285
+ catch {
286
+ // Non-critical — stale detection simply skips this cycle
287
+ }
288
+ if (freshMeta.length > 0) {
289
+ setSessionMeta((prev) => {
290
+ const next = new Map(prev);
291
+ for (const meta of freshMeta) {
292
+ next.set(meta.sessionId, { status: meta.status, createdAt: meta.createdAt });
293
+ }
294
+ return next;
295
+ });
296
+ }
297
+ // Show toast for newly stale sessions
298
+ for (const session of sessionQueue) {
299
+ const stale = isSessionStale(session.timestamp.getTime(), staleThreshold, lastInteractions.get(session.sessionId));
300
+ if (stale && notifyOnStale && !staleToastShown.has(session.sessionId)) {
301
+ const title = session.sessionRequest.questions[0]?.title ?? session.sessionId.slice(0, 8);
302
+ showToast(formatStaleToastMessage(title, session.timestamp.getTime()), "info");
303
+ setStaleToastShown((prev) => new Set(prev).add(session.sessionId));
304
+ }
305
+ }
306
+ };
307
+ const staleInterval = setInterval(() => {
308
+ void runStaleDetection();
309
+ }, 2000);
218
310
  return () => {
219
311
  isCancelled = true;
220
312
  clearInterval(interval);
313
+ clearInterval(staleInterval);
221
314
  };
222
- }, [activeSessionIndex, sessionDir, sessionQueue, state.mode]);
315
+ }, [activeSessionIndex, sessionDir, sessionQueue, state.mode, config?.staleThreshold, config?.notifyOnStale, lastInteractions, staleToastShown]);
223
316
  // Handle progress updates from StepperView
224
317
  const handleProgressUpdate = (answered, total) => {
225
318
  const percent = calculateProgress(answered, total);
@@ -230,10 +323,57 @@ const App = ({ config }) => {
230
323
  ...prev,
231
324
  [sessionId]: ui,
232
325
  }));
326
+ // Track interaction for stale grace time
327
+ setLastInteractions((prev) => new Map(prev).set(sessionId, Date.now()));
233
328
  }, []);
234
329
  const handleFlowStateChange = useCallback((flowState) => {
235
330
  setIsInReviewOrRejection(flowState.showReview || flowState.showRejectionConfirm);
236
331
  }, []);
332
+ // ── Auto-update handlers ────────────────────────────────────
333
+ const handleUpdateInstall = async () => {
334
+ try {
335
+ setIsInstallingUpdate(true);
336
+ setInstallError(null);
337
+ const pm = detectPackageManager();
338
+ const success = await installUpdate(pm);
339
+ if (success) {
340
+ setShowUpdateOverlay(false);
341
+ setToast({
342
+ message: `Updated to v${updateInfo.latestVersion}. Please restart auq.`,
343
+ type: "success",
344
+ });
345
+ // Exit after short delay so user sees the message
346
+ setTimeout(() => process.exit(0), 2000);
347
+ }
348
+ else {
349
+ setInstallError("Installation failed. Please try manually.");
350
+ }
351
+ setIsInstallingUpdate(false);
352
+ }
353
+ catch (err) {
354
+ setIsInstallingUpdate(false);
355
+ setInstallError(err instanceof Error ? err.message : "Installation failed");
356
+ }
357
+ };
358
+ const handleSkipVersion = async () => {
359
+ if (updateInfo) {
360
+ try {
361
+ const cache = await readCache();
362
+ if (cache) {
363
+ await writeCache({ ...cache, skippedVersion: updateInfo.latestVersion });
364
+ }
365
+ }
366
+ catch {
367
+ // Non-critical — skip-version simply won't persist
368
+ }
369
+ }
370
+ setShowUpdateOverlay(false);
371
+ setUpdateInfo(null);
372
+ };
373
+ const handleRemindLater = () => {
374
+ setShowUpdateOverlay(false);
375
+ setUpdateDismissed(true);
376
+ };
237
377
  const switchToSession = useCallback((targetIndex) => {
238
378
  if (state.mode !== "PROCESSING" || sessionQueue.length <= 1) {
239
379
  return;
@@ -248,6 +388,13 @@ const App = ({ config }) => {
248
388
  }
249
389
  setActiveSessionIndex(clampedIndex);
250
390
  setShowSessionPicker(false);
391
+ // Track interaction for stale grace time
392
+ setLastInteractions((prev) => {
393
+ const targetSession = sessionQueue[clampedIndex];
394
+ if (!targetSession)
395
+ return prev;
396
+ return new Map(prev).set(targetSession.sessionId, Date.now());
397
+ });
251
398
  }, [activeSessionIndex, sessionQueue, state.mode]);
252
399
  const activeSession = state.mode === "PROCESSING" ? sessionQueue[activeSessionIndex] : undefined;
253
400
  const canUseDirectJump = !activeSession ||
@@ -288,8 +435,23 @@ const App = ({ config }) => {
288
435
  isActive: state.mode === "PROCESSING" &&
289
436
  !isInReviewOrRejection &&
290
437
  !showSessionPicker &&
438
+ !showUpdateOverlay &&
291
439
  sessionQueue.length >= 2,
292
440
  });
441
+ // Update overlay keyboard shortcut (independent of session count)
442
+ useInput((input, key) => {
443
+ if (!key.ctrl && !key.meta && input === KEYS.UPDATE) {
444
+ if (updateInfo && !showUpdateOverlay) {
445
+ setShowUpdateOverlay(true);
446
+ }
447
+ }
448
+ }, {
449
+ isActive: state.mode === "PROCESSING" &&
450
+ !isInReviewOrRejection &&
451
+ !showSessionPicker &&
452
+ !showUpdateOverlay &&
453
+ !!updateInfo,
454
+ });
293
455
  // Handle session completion
294
456
  const handleSessionComplete = (wasRejected = false, rejectionReason) => {
295
457
  // Clear progress bar on session completion
@@ -341,7 +503,7 @@ const App = ({ config }) => {
341
503
  mainContent = React.createElement(WaitingScreen, { queueCount: sessionQueue.length });
342
504
  }
343
505
  else {
344
- mainContent = (React.createElement(StepperView, { key: session.sessionId, onComplete: handleSessionComplete, onProgress: handleProgressUpdate, initialState: sessionUIStates[session.sessionId], onStateSnapshot: handleStateSnapshot, onFlowStateChange: handleFlowStateChange, hasMultipleSessions: sessionQueue.length >= 2, sessionId: session.sessionId, sessionRequest: session.sessionRequest }));
506
+ mainContent = (React.createElement(StepperView, { key: session.sessionId, onComplete: handleSessionComplete, onProgress: handleProgressUpdate, initialState: sessionUIStates[session.sessionId], onStateSnapshot: handleStateSnapshot, onFlowStateChange: handleFlowStateChange, hasMultipleSessions: sessionQueue.length >= 2, sessionId: session.sessionId, sessionRequest: session.sessionRequest, isAbandoned: isSessionAbandoned(sessionMeta.get(session.sessionId)?.status ?? "") }));
345
507
  }
346
508
  }
347
509
  // Render with header, toast overlay, and main content
@@ -352,19 +514,33 @@ const App = ({ config }) => {
352
514
  React.createElement(Box, { flexDirection: "column", paddingX: 1 },
353
515
  React.createElement(Header, { pendingCount: state.mode === "PROCESSING"
354
516
  ? Math.max(0, sessionQueue.length - 1)
355
- : sessionQueue.length }),
517
+ : sessionQueue.length, updateInfo: !showUpdateOverlay && updateInfo
518
+ ? {
519
+ updateType: updateInfo.updateType,
520
+ latestVersion: updateInfo.latestVersion,
521
+ }
522
+ : null, onUpdateBadgeActivate: () => setShowUpdateOverlay(true) }),
356
523
  mainContent,
357
- state.mode === "PROCESSING" && sessionQueue.length >= 2 && (React.createElement(SessionDots, { sessions: sessionQueue, activeIndex: activeSessionIndex, sessionUIStates: sessionUIStates })),
524
+ state.mode === "PROCESSING" && sessionQueue.length >= 2 && (React.createElement(SessionDots, { sessions: sessionQueue.map((s) => ({
525
+ ...s,
526
+ isStale: isSessionStale(s.timestamp.getTime(), config?.staleThreshold ?? 7200000, lastInteractions.get(s.sessionId)),
527
+ isAbandoned: isSessionAbandoned(sessionMeta.get(s.sessionId)?.status ?? ""),
528
+ })), activeIndex: activeSessionIndex, sessionUIStates: sessionUIStates })),
358
529
  toast && (React.createElement(Box, { marginTop: 1, justifyContent: "center" },
359
530
  React.createElement(Toast, { message: toast.message, onDismiss: () => setToast(null), type: toast.type, title: toast.title, duration: 5000 }))),
360
531
  showSessionLog && (React.createElement(Box, { marginTop: 1 },
361
532
  React.createElement(Text, { dimColor: true },
362
533
  "[AUQ] Session directory: ",
363
534
  sessionDir))),
364
- state.mode === "PROCESSING" && (React.createElement(SessionPicker, { isOpen: showSessionPicker, sessions: sessionQueue, activeIndex: activeSessionIndex, sessionUIStates: sessionUIStates, onSelectIndex: (idx) => {
535
+ state.mode === "PROCESSING" && (React.createElement(SessionPicker, { isOpen: showSessionPicker, sessions: sessionQueue.map((s) => ({
536
+ ...s,
537
+ isStale: isSessionStale(s.timestamp.getTime(), config?.staleThreshold ?? 7200000, lastInteractions.get(s.sessionId)),
538
+ isAbandoned: isSessionAbandoned(sessionMeta.get(s.sessionId)?.status ?? ""),
539
+ })), activeIndex: activeSessionIndex, sessionUIStates: sessionUIStates, onSelectIndex: (idx) => {
365
540
  switchToSession(idx);
366
541
  setShowSessionPicker(false);
367
542
  }, onClose: () => setShowSessionPicker(false) })),
543
+ showUpdateOverlay && updateInfo && (React.createElement(UpdateOverlay, { isOpen: showUpdateOverlay, currentVersion: updateInfo.currentVersion, latestVersion: updateInfo.latestVersion, updateType: updateInfo.updateType, changelog: changelogContent, changelogUrl: updateInfo.changelogUrl, isInstalling: isInstallingUpdate, installError: installError, onInstall: handleUpdateInstall, onSkipVersion: handleSkipVersion, onRemindLater: handleRemindLater })),
368
544
  React.createElement(ThemeIndicator, null)))));
369
545
  };
370
546
  export const runTui = (config) => {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "dist/bin/auq.js"