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.
- package/README.md +122 -0
- package/dist/bin/auq.js +87 -93
- package/dist/bin/tui-app.js +183 -7
- package/dist/package.json +1 -1
- package/dist/src/__tests__/server.abort.test.js +214 -0
- package/dist/src/cli/commands/__tests__/answer.test.js +199 -0
- package/dist/src/cli/commands/__tests__/config.test.js +218 -0
- package/dist/src/cli/commands/__tests__/sessions.test.js +282 -0
- package/dist/src/cli/commands/answer.js +128 -0
- package/dist/src/cli/commands/config.js +263 -0
- package/dist/src/cli/commands/sessions.js +300 -0
- package/dist/src/cli/commands/update.js +124 -0
- package/dist/src/cli/utils.js +95 -0
- package/dist/src/config/__tests__/ConfigLoader.test.js +41 -0
- package/dist/src/config/__tests__/updateCheck.test.js +34 -0
- package/dist/src/config/defaults.js +5 -0
- package/dist/src/config/types.js +6 -0
- package/dist/src/core/ask-user-questions.js +3 -2
- package/dist/src/i18n/locales/en.js +7 -0
- package/dist/src/i18n/locales/ko.js +7 -0
- package/dist/src/server.js +64 -11
- package/dist/src/session/SessionManager.js +69 -4
- package/dist/src/session/__tests__/SessionManager.test.js +65 -0
- package/dist/src/tui/__tests__/session-watcher.test.js +109 -0
- package/dist/src/tui/components/Footer.js +4 -1
- package/dist/src/tui/components/Header.js +3 -1
- package/dist/src/tui/components/SessionDots.js +33 -4
- package/dist/src/tui/components/SessionPicker.js +25 -17
- package/dist/src/tui/components/StepperView.js +68 -5
- package/dist/src/tui/components/UpdateBadge.js +29 -0
- package/dist/src/tui/components/UpdateOverlay.js +199 -0
- package/dist/src/tui/components/__tests__/SessionDots.test.js +160 -1
- package/dist/src/tui/components/__tests__/SessionPicker.test.js +43 -1
- package/dist/src/tui/components/__tests__/StepperView.abandoned.test.js +160 -0
- package/dist/src/tui/components/__tests__/StepperView.state.test.js +1 -0
- package/dist/src/tui/constants/keybindings.js +3 -0
- package/dist/src/tui/session-watcher.js +50 -0
- package/dist/src/tui/themes/catppuccin-latte.js +7 -0
- package/dist/src/tui/themes/catppuccin-mocha.js +7 -0
- package/dist/src/tui/themes/dark.js +7 -0
- package/dist/src/tui/themes/dracula.js +7 -0
- package/dist/src/tui/themes/github-dark.js +7 -0
- package/dist/src/tui/themes/github-light.js +7 -0
- package/dist/src/tui/themes/gruvbox-dark.js +7 -0
- package/dist/src/tui/themes/gruvbox-light.js +7 -0
- package/dist/src/tui/themes/light.js +7 -0
- package/dist/src/tui/themes/monokai.js +7 -0
- package/dist/src/tui/themes/nord.js +7 -0
- package/dist/src/tui/themes/one-dark.js +7 -0
- package/dist/src/tui/themes/rose-pine.js +7 -0
- package/dist/src/tui/themes/solarized-dark.js +7 -0
- package/dist/src/tui/themes/solarized-light.js +7 -0
- package/dist/src/tui/themes/tokyo-night.js +7 -0
- package/dist/src/tui/utils/__tests__/staleDetection.test.js +118 -0
- package/dist/src/tui/utils/staleDetection.js +51 -0
- package/dist/src/update/__tests__/cache.test.js +136 -0
- package/dist/src/update/__tests__/changelog.test.js +86 -0
- package/dist/src/update/__tests__/checker.test.js +148 -0
- package/dist/src/update/__tests__/index.test.js +37 -0
- package/dist/src/update/__tests__/installer.test.js +117 -0
- package/dist/src/update/__tests__/package-manager.test.js +73 -0
- package/dist/src/update/__tests__/version.test.js +74 -0
- package/dist/src/update/cache.js +74 -0
- package/dist/src/update/changelog.js +63 -0
- package/dist/src/update/checker.js +121 -0
- package/dist/src/update/index.js +15 -0
- package/dist/src/update/installer.js +51 -0
- package/dist/src/update/package-manager.js +49 -0
- package/dist/src/update/types.js +7 -0
- package/dist/src/update/version.js +114 -0
- 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
|
-
|
|
16
|
-
|
|
17
|
-
Usage:
|
|
18
|
-
auq [command] [options]
|
|
14
|
+
Usage: auq [command] [options]
|
|
19
15
|
|
|
20
16
|
Commands:
|
|
21
|
-
(default)
|
|
22
|
-
server
|
|
23
|
-
ask <json>
|
|
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
|
|
27
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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.
|
package/dist/bin/tui-app.js
CHANGED
|
@@ -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
|
|
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
|
|
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) => {
|