auq-mcp-server 2.2.2 → 2.4.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 (62) hide show
  1. package/README.md +82 -0
  2. package/dist/bin/auq.js +45 -39
  3. package/dist/bin/tui-app.js +78 -8
  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 +164 -0
  12. package/dist/src/cli/utils.js +95 -0
  13. package/dist/src/config/__tests__/ConfigLoader.test.js +41 -0
  14. package/dist/src/config/defaults.js +3 -0
  15. package/dist/src/config/types.js +4 -0
  16. package/dist/src/core/ask-user-questions.js +3 -2
  17. package/dist/src/i18n/locales/en.js +8 -1
  18. package/dist/src/i18n/locales/ko.js +8 -1
  19. package/dist/src/server.js +64 -11
  20. package/dist/src/session/SessionManager.js +69 -4
  21. package/dist/src/session/__tests__/SessionManager.test.js +65 -0
  22. package/dist/src/tui/ThemeProvider.js +2 -1
  23. package/dist/src/tui/__tests__/session-watcher.test.js +109 -0
  24. package/dist/src/tui/components/ConfirmationDialog.js +5 -4
  25. package/dist/src/tui/components/Footer.js +24 -23
  26. package/dist/src/tui/components/ReviewScreen.js +2 -1
  27. package/dist/src/tui/components/SessionDots.js +33 -4
  28. package/dist/src/tui/components/SessionPicker.js +27 -18
  29. package/dist/src/tui/components/Spinner.js +19 -0
  30. package/dist/src/tui/components/StepperView.js +71 -7
  31. package/dist/src/tui/components/WaitingScreen.js +2 -1
  32. package/dist/src/tui/components/__tests__/ConfirmationDialog.test.js +134 -0
  33. package/dist/src/tui/components/__tests__/Footer.test.js +121 -0
  34. package/dist/src/tui/components/__tests__/ReviewScreen.test.js +89 -0
  35. package/dist/src/tui/components/__tests__/SessionDots.test.js +160 -1
  36. package/dist/src/tui/components/__tests__/SessionPicker.test.js +43 -1
  37. package/dist/src/tui/components/__tests__/StepperView.abandoned.test.js +160 -0
  38. package/dist/src/tui/components/__tests__/StepperView.keyboard.test.js +135 -0
  39. package/dist/src/tui/components/__tests__/StepperView.state.test.js +1 -0
  40. package/dist/src/tui/components/__tests__/WaitingScreen.test.js +60 -0
  41. package/dist/src/tui/constants/keybindings.js +40 -0
  42. package/dist/src/tui/session-watcher.js +50 -0
  43. package/dist/src/tui/themes/catppuccin-latte.js +7 -0
  44. package/dist/src/tui/themes/catppuccin-mocha.js +7 -0
  45. package/dist/src/tui/themes/dark.js +7 -0
  46. package/dist/src/tui/themes/dracula.js +7 -0
  47. package/dist/src/tui/themes/github-dark.js +7 -0
  48. package/dist/src/tui/themes/github-light.js +7 -0
  49. package/dist/src/tui/themes/gruvbox-dark.js +7 -0
  50. package/dist/src/tui/themes/gruvbox-light.js +7 -0
  51. package/dist/src/tui/themes/light.js +7 -0
  52. package/dist/src/tui/themes/monokai.js +7 -0
  53. package/dist/src/tui/themes/nord.js +7 -0
  54. package/dist/src/tui/themes/one-dark.js +7 -0
  55. package/dist/src/tui/themes/rose-pine.js +7 -0
  56. package/dist/src/tui/themes/solarized-dark.js +7 -0
  57. package/dist/src/tui/themes/solarized-light.js +7 -0
  58. package/dist/src/tui/themes/tokyo-night.js +7 -0
  59. package/dist/src/tui/utils/__tests__/detectTheme.test.js +78 -0
  60. package/dist/src/tui/utils/__tests__/staleDetection.test.js +118 -0
  61. package/dist/src/tui/utils/staleDetection.js +51 -0
  62. package/package.json +1 -1
@@ -91,6 +91,7 @@ export const gruvboxLightTheme = {
91
91
  successPillBg: "#ebdbb2", // light1 - light bg
92
92
  error: "#9d0006",
93
93
  info: "#076678",
94
+ warning: "#b57614",
94
95
  border: "#d5c4a1",
95
96
  },
96
97
  markdown: {
@@ -105,6 +106,8 @@ export const gruvboxLightTheme = {
105
106
  untouched: "#a89984",
106
107
  number: "#3c3836",
107
108
  activeNumber: "#076678",
109
+ stale: "#b57614",
110
+ abandoned: "#9d0006",
108
111
  },
109
112
  sessionPicker: {
110
113
  border: "#076678",
@@ -115,6 +118,10 @@ export const gruvboxLightTheme = {
115
118
  highlightFg: "#79740e",
116
119
  activeMark: "#076678",
117
120
  progress: "#076678",
121
+ staleIcon: "#b57614",
122
+ staleText: "#b57614",
123
+ staleAge: "#b57614",
124
+ staleSubtitle: "#a89984",
118
125
  },
119
126
  },
120
127
  };
@@ -91,6 +91,7 @@ export const lightTheme = {
91
91
  successPillBg: "#E6F9EE",
92
92
  error: "#CF222E",
93
93
  info: "#007EA7",
94
+ warning: "#B07D00",
94
95
  border: "#D0D7DE",
95
96
  },
96
97
  markdown: {
@@ -105,6 +106,8 @@ export const lightTheme = {
105
106
  untouched: "#6E7781",
106
107
  number: "#24292F",
107
108
  activeNumber: "#007EA7",
109
+ stale: "#B07D00",
110
+ abandoned: "#CF222E",
108
111
  },
109
112
  sessionPicker: {
110
113
  border: "#007EA7",
@@ -115,6 +118,10 @@ export const lightTheme = {
115
118
  highlightFg: "#2DA44E",
116
119
  activeMark: "#007EA7",
117
120
  progress: "#007EA7",
121
+ staleIcon: "#B07D00",
122
+ staleText: "#B07D00",
123
+ staleAge: "#B07D00",
124
+ staleSubtitle: "#6E7781",
118
125
  },
119
126
  },
120
127
  };
@@ -92,6 +92,7 @@ export const monokaiTheme = {
92
92
  successPillBg: "#272822",
93
93
  error: "#F92672",
94
94
  info: "#66D9EF",
95
+ warning: "#E6DB74",
95
96
  border: "#49483E",
96
97
  },
97
98
  markdown: {
@@ -106,6 +107,8 @@ export const monokaiTheme = {
106
107
  untouched: "#908B78",
107
108
  number: "#F8F8F2",
108
109
  activeNumber: "#66D9EF",
110
+ stale: "#E6DB74",
111
+ abandoned: "#F92672",
109
112
  },
110
113
  sessionPicker: {
111
114
  border: "#66D9EF",
@@ -116,6 +119,10 @@ export const monokaiTheme = {
116
119
  highlightFg: "#A6E22E",
117
120
  activeMark: "#66D9EF",
118
121
  progress: "#66D9EF",
122
+ staleIcon: "#E6DB74",
123
+ staleText: "#E6DB74",
124
+ staleAge: "#E6DB74",
125
+ staleSubtitle: "#908B78",
119
126
  },
120
127
  },
121
128
  };
@@ -91,6 +91,7 @@ export const nordTheme = {
91
91
  successPillBg: "#3b4252", // nord1
92
92
  error: "#bf616a", // nord11
93
93
  info: "#88c0d0", // nord8
94
+ warning: "#ebcb8b",
94
95
  border: "#3b4252", // nord1
95
96
  },
96
97
  markdown: {
@@ -105,6 +106,8 @@ export const nordTheme = {
105
106
  untouched: "#616E88",
106
107
  number: "#eceff4",
107
108
  activeNumber: "#88c0d0",
109
+ stale: "#ebcb8b",
110
+ abandoned: "#bf616a",
108
111
  },
109
112
  sessionPicker: {
110
113
  border: "#88c0d0",
@@ -115,6 +118,10 @@ export const nordTheme = {
115
118
  highlightFg: "#a3be8c",
116
119
  activeMark: "#88c0d0",
117
120
  progress: "#81a1c1",
121
+ staleIcon: "#ebcb8b",
122
+ staleText: "#ebcb8b",
123
+ staleAge: "#ebcb8b",
124
+ staleSubtitle: "#616E88",
118
125
  },
119
126
  },
120
127
  };
@@ -91,6 +91,7 @@ export const oneDarkTheme = {
91
91
  successPillBg: "#282c34",
92
92
  error: "#e06c75",
93
93
  info: "#61afef",
94
+ warning: "#d19a66",
94
95
  border: "#3e4451",
95
96
  },
96
97
  markdown: {
@@ -105,6 +106,8 @@ export const oneDarkTheme = {
105
106
  untouched: "#767D8A",
106
107
  number: "#abb2bf",
107
108
  activeNumber: "#61afef",
109
+ stale: "#d19a66",
110
+ abandoned: "#e06c75",
108
111
  },
109
112
  sessionPicker: {
110
113
  border: "#61afef",
@@ -115,6 +118,10 @@ export const oneDarkTheme = {
115
118
  highlightFg: "#98c379",
116
119
  activeMark: "#61afef",
117
120
  progress: "#56b6c2",
121
+ staleIcon: "#d19a66",
122
+ staleText: "#d19a66",
123
+ staleAge: "#d19a66",
124
+ staleSubtitle: "#767D8A",
118
125
  },
119
126
  },
120
127
  };
@@ -92,6 +92,7 @@ export const rosePineTheme = {
92
92
  successPillBg: "#191724",
93
93
  error: "#eb6f92",
94
94
  info: "#9ccfd8",
95
+ warning: "#f6c177",
95
96
  border: "#26233a",
96
97
  },
97
98
  markdown: {
@@ -106,6 +107,8 @@ export const rosePineTheme = {
106
107
  untouched: "#8884a0",
107
108
  number: "#e0def4",
108
109
  activeNumber: "#ebbcba",
110
+ stale: "#f6c177",
111
+ abandoned: "#eb6f92",
109
112
  },
110
113
  sessionPicker: {
111
114
  border: "#ebbcba",
@@ -116,6 +119,10 @@ export const rosePineTheme = {
116
119
  highlightFg: "#31748f",
117
120
  activeMark: "#ebbcba",
118
121
  progress: "#9ccfd8",
122
+ staleIcon: "#f6c177",
123
+ staleText: "#f6c177",
124
+ staleAge: "#f6c177",
125
+ staleSubtitle: "#8884a0",
119
126
  },
120
127
  },
121
128
  };
@@ -91,6 +91,7 @@ export const solarizedDarkTheme = {
91
91
  successPillBg: "#073642",
92
92
  error: "#dc322f",
93
93
  info: "#268bd2",
94
+ warning: "#b58900",
94
95
  border: "#073642",
95
96
  },
96
97
  markdown: {
@@ -105,6 +106,8 @@ export const solarizedDarkTheme = {
105
106
  untouched: "#6c7c83",
106
107
  number: "#839496",
107
108
  activeNumber: "#268bd2",
109
+ stale: "#b58900",
110
+ abandoned: "#dc322f",
108
111
  },
109
112
  sessionPicker: {
110
113
  border: "#268bd2",
@@ -115,6 +118,10 @@ export const solarizedDarkTheme = {
115
118
  highlightFg: "#859900",
116
119
  activeMark: "#268bd2",
117
120
  progress: "#2aa198",
121
+ staleIcon: "#b58900",
122
+ staleText: "#b58900",
123
+ staleAge: "#b58900",
124
+ staleSubtitle: "#6c7c83",
118
125
  },
119
126
  },
120
127
  };
@@ -91,6 +91,7 @@ export const solarizedLightTheme = {
91
91
  successPillBg: "#eee8d5",
92
92
  error: "#dc322f",
93
93
  info: "#268bd2",
94
+ warning: "#b58900",
94
95
  border: "#eee8d5",
95
96
  },
96
97
  markdown: {
@@ -105,6 +106,8 @@ export const solarizedLightTheme = {
105
106
  untouched: "#a3b1b1",
106
107
  number: "#657b83",
107
108
  activeNumber: "#268bd2",
109
+ stale: "#b58900",
110
+ abandoned: "#dc322f",
108
111
  },
109
112
  sessionPicker: {
110
113
  border: "#268bd2",
@@ -115,6 +118,10 @@ export const solarizedLightTheme = {
115
118
  highlightFg: "#859900",
116
119
  activeMark: "#268bd2",
117
120
  progress: "#2aa198",
121
+ staleIcon: "#b58900",
122
+ staleText: "#b58900",
123
+ staleAge: "#b58900",
124
+ staleSubtitle: "#a3b1b1",
118
125
  },
119
126
  },
120
127
  };
@@ -91,6 +91,7 @@ export const tokyoNightTheme = {
91
91
  successPillBg: "#1a1b26",
92
92
  error: "#f7768e",
93
93
  info: "#7aa2f7",
94
+ warning: "#e0af68",
94
95
  border: "#24283b",
95
96
  },
96
97
  markdown: {
@@ -105,6 +106,8 @@ export const tokyoNightTheme = {
105
106
  untouched: "#7078A3",
106
107
  number: "#c0caf5",
107
108
  activeNumber: "#7aa2f7",
109
+ stale: "#e0af68",
110
+ abandoned: "#f7768e",
108
111
  },
109
112
  sessionPicker: {
110
113
  border: "#7aa2f7",
@@ -115,6 +118,10 @@ export const tokyoNightTheme = {
115
118
  highlightFg: "#9ece6a",
116
119
  activeMark: "#7aa2f7",
117
120
  progress: "#7dcfff",
121
+ staleIcon: "#e0af68",
122
+ staleText: "#e0af68",
123
+ staleAge: "#e0af68",
124
+ staleSubtitle: "#7078A3",
118
125
  },
119
126
  },
120
127
  };
@@ -0,0 +1,78 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { detectSystemTheme, clearThemeCache } from "../detectTheme.js";
3
+ describe("detectSystemTheme", () => {
4
+ const originalEnv = process.env;
5
+ beforeEach(() => {
6
+ // Reset environment and clear cache before each test
7
+ process.env = { ...originalEnv };
8
+ clearThemeCache();
9
+ });
10
+ afterEach(() => {
11
+ process.env = originalEnv;
12
+ clearThemeCache();
13
+ });
14
+ describe("COLORFGBG detection", () => {
15
+ it("should detect dark theme when background is 0 (black)", () => {
16
+ process.env.COLORFGBG = "15;0";
17
+ expect(detectSystemTheme()).toBe("dark");
18
+ });
19
+ it("should detect dark theme when background is 6", () => {
20
+ process.env.COLORFGBG = "7;6";
21
+ expect(detectSystemTheme()).toBe("dark");
22
+ });
23
+ it("should detect light theme when background is 7 (white/light gray)", () => {
24
+ process.env.COLORFGBG = "0;7";
25
+ expect(detectSystemTheme()).toBe("light");
26
+ });
27
+ it("should detect light theme when background is 15 (bright white)", () => {
28
+ process.env.COLORFGBG = "0;15";
29
+ expect(detectSystemTheme()).toBe("light");
30
+ });
31
+ it("should handle three-part COLORFGBG format", () => {
32
+ // Some terminals use foreground;middle;background format
33
+ process.env.COLORFGBG = "15;default;0";
34
+ expect(detectSystemTheme()).toBe("dark");
35
+ });
36
+ it("should handle three-part COLORFGBG with light background", () => {
37
+ process.env.COLORFGBG = "0;default;15";
38
+ expect(detectSystemTheme()).toBe("light");
39
+ });
40
+ });
41
+ describe("fallback behavior", () => {
42
+ it("should fallback to dark when COLORFGBG is not set", () => {
43
+ delete process.env.COLORFGBG;
44
+ expect(detectSystemTheme()).toBe("dark");
45
+ });
46
+ it("should fallback to dark when COLORFGBG is invalid", () => {
47
+ process.env.COLORFGBG = "invalid";
48
+ expect(detectSystemTheme()).toBe("dark");
49
+ });
50
+ it("should fallback to dark when COLORFGBG has invalid number", () => {
51
+ process.env.COLORFGBG = "15;abc";
52
+ expect(detectSystemTheme()).toBe("dark");
53
+ });
54
+ it("should fallback to dark when COLORFGBG is empty", () => {
55
+ process.env.COLORFGBG = "";
56
+ expect(detectSystemTheme()).toBe("dark");
57
+ });
58
+ });
59
+ describe("caching", () => {
60
+ it("should cache the result", () => {
61
+ process.env.COLORFGBG = "15;0";
62
+ expect(detectSystemTheme()).toBe("dark");
63
+ // Change the environment variable
64
+ process.env.COLORFGBG = "0;15";
65
+ // Should still return cached result
66
+ expect(detectSystemTheme()).toBe("dark");
67
+ });
68
+ it("should return fresh result after cache is cleared", () => {
69
+ process.env.COLORFGBG = "15;0";
70
+ expect(detectSystemTheme()).toBe("dark");
71
+ // Clear cache and change environment
72
+ clearThemeCache();
73
+ process.env.COLORFGBG = "0;15";
74
+ // Should return new result
75
+ expect(detectSystemTheme()).toBe("light");
76
+ });
77
+ });
78
+ });
@@ -0,0 +1,118 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { DEFAULT_GRACE_PERIOD, formatStaleToastMessage, isSessionAbandoned, isSessionStale, } from "../staleDetection.js";
3
+ describe("staleDetection", () => {
4
+ const now = new Date("2026-01-01T12:00:00.000Z");
5
+ beforeEach(() => {
6
+ vi.useFakeTimers();
7
+ vi.setSystemTime(now);
8
+ });
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ });
12
+ describe("isSessionStale", () => {
13
+ const TWO_HOURS = 7200000; // default staleThreshold from config
14
+ it("should return true when session is older than threshold", () => {
15
+ // Session created 3 hours ago, threshold is 2 hours
16
+ const threeHoursAgo = now.getTime() - 3 * 3600000;
17
+ expect(isSessionStale(threeHoursAgo, TWO_HOURS)).toBe(true);
18
+ });
19
+ it("should return false when session is younger than threshold", () => {
20
+ // Session created 1 hour ago, threshold is 2 hours
21
+ const oneHourAgo = now.getTime() - 1 * 3600000;
22
+ expect(isSessionStale(oneHourAgo, TWO_HOURS)).toBe(false);
23
+ });
24
+ it("should return false when session is exactly at threshold", () => {
25
+ // Edge case: exactly at threshold boundary (not greater than)
26
+ const exactlyAtThreshold = now.getTime() - TWO_HOURS;
27
+ expect(isSessionStale(exactlyAtThreshold, TWO_HOURS)).toBe(false);
28
+ });
29
+ it("should return false when stale session has recent interaction within grace period", () => {
30
+ // Session created 3 hours ago (stale), but user interacted 10 minutes ago
31
+ const threeHoursAgo = now.getTime() - 3 * 3600000;
32
+ const tenMinutesAgo = now.getTime() - 10 * 60000;
33
+ expect(isSessionStale(threeHoursAgo, TWO_HOURS, tenMinutesAgo)).toBe(false);
34
+ });
35
+ it("should return true when stale session has old interaction outside grace period", () => {
36
+ // Session created 3 hours ago, last interaction 45 minutes ago (outside 30min grace)
37
+ const threeHoursAgo = now.getTime() - 3 * 3600000;
38
+ const fortyFiveMinutesAgo = now.getTime() - 45 * 60000;
39
+ expect(isSessionStale(threeHoursAgo, TWO_HOURS, fortyFiveMinutesAgo)).toBe(true);
40
+ });
41
+ it("should respect custom grace period", () => {
42
+ // Session created 3 hours ago, last interaction 45 minutes ago
43
+ // With a 1-hour custom grace period, should NOT be stale
44
+ const threeHoursAgo = now.getTime() - 3 * 3600000;
45
+ const fortyFiveMinutesAgo = now.getTime() - 45 * 60000;
46
+ const oneHourGrace = 3600000;
47
+ expect(isSessionStale(threeHoursAgo, TWO_HOURS, fortyFiveMinutesAgo, oneHourGrace)).toBe(false);
48
+ });
49
+ it("should use DEFAULT_GRACE_PERIOD when no grace period provided", () => {
50
+ // Interaction 29 minutes ago (within default 30-minute grace)
51
+ const threeHoursAgo = now.getTime() - 3 * 3600000;
52
+ const twentyNineMinutesAgo = now.getTime() - 29 * 60000;
53
+ expect(isSessionStale(threeHoursAgo, TWO_HOURS, twentyNineMinutesAgo)).toBe(false);
54
+ // Interaction 31 minutes ago (outside default 30-minute grace)
55
+ const thirtyOneMinutesAgo = now.getTime() - 31 * 60000;
56
+ expect(isSessionStale(threeHoursAgo, TWO_HOURS, thirtyOneMinutesAgo)).toBe(true);
57
+ });
58
+ it("should handle undefined lastInteraction", () => {
59
+ const threeHoursAgo = now.getTime() - 3 * 3600000;
60
+ // No interaction => stale if age exceeds threshold
61
+ expect(isSessionStale(threeHoursAgo, TWO_HOURS, undefined)).toBe(true);
62
+ });
63
+ it("should handle zero threshold (always stale if age > 0)", () => {
64
+ const oneSecondAgo = now.getTime() - 1000;
65
+ expect(isSessionStale(oneSecondAgo, 0)).toBe(true);
66
+ });
67
+ });
68
+ describe("isSessionAbandoned", () => {
69
+ it("should return true for 'abandoned' status", () => {
70
+ expect(isSessionAbandoned("abandoned")).toBe(true);
71
+ });
72
+ it("should return false for 'pending' status", () => {
73
+ expect(isSessionAbandoned("pending")).toBe(false);
74
+ });
75
+ it("should return false for 'completed' status", () => {
76
+ expect(isSessionAbandoned("completed")).toBe(false);
77
+ });
78
+ it("should return false for 'in-progress' status", () => {
79
+ expect(isSessionAbandoned("in-progress")).toBe(false);
80
+ });
81
+ it("should return false for 'rejected' status", () => {
82
+ expect(isSessionAbandoned("rejected")).toBe(false);
83
+ });
84
+ it("should return false for 'timed_out' status", () => {
85
+ expect(isSessionAbandoned("timed_out")).toBe(false);
86
+ });
87
+ it("should return false for empty string", () => {
88
+ expect(isSessionAbandoned("")).toBe(false);
89
+ });
90
+ });
91
+ describe("formatStaleToastMessage", () => {
92
+ it("should format hours correctly for a 3-hour-old session", () => {
93
+ const threeHoursAgo = now.getTime() - 3 * 3600000;
94
+ const message = formatStaleToastMessage("Test Session", threeHoursAgo);
95
+ expect(message).toBe('Session "Test Session" may be orphaned (created 3h ago)');
96
+ });
97
+ it("should show 0h for very recent sessions", () => {
98
+ const thirtyMinutesAgo = now.getTime() - 30 * 60000;
99
+ const message = formatStaleToastMessage("New Session", thirtyMinutesAgo);
100
+ expect(message).toBe('Session "New Session" may be orphaned (created 0h ago)');
101
+ });
102
+ it("should show large hour counts for old sessions", () => {
103
+ const twoDaysAgo = now.getTime() - 48 * 3600000;
104
+ const message = formatStaleToastMessage("Old Session", twoDaysAgo);
105
+ expect(message).toBe('Session "Old Session" may be orphaned (created 48h ago)');
106
+ });
107
+ it("should handle session title with special characters", () => {
108
+ const twoHoursAgo = now.getTime() - 2 * 3600000;
109
+ const message = formatStaleToastMessage("Session \"quoted\"", twoHoursAgo);
110
+ expect(message).toBe('Session "Session "quoted"" may be orphaned (created 2h ago)');
111
+ });
112
+ });
113
+ describe("DEFAULT_GRACE_PERIOD", () => {
114
+ it("should be 30 minutes in milliseconds", () => {
115
+ expect(DEFAULT_GRACE_PERIOD).toBe(1800000);
116
+ });
117
+ });
118
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Stale Session Detection Utilities
3
+ *
4
+ * Pure functions for computing session staleness. No I/O or React dependencies.
5
+ * Design Decision 4: "Stale is computed flag, not persisted status"
6
+ * Design Decision 8: "Interaction resets stale check for that session"
7
+ */
8
+ /**
9
+ * Default grace period in milliseconds (30 minutes).
10
+ * If a user interacted with a session within this window, it is not considered stale.
11
+ */
12
+ export const DEFAULT_GRACE_PERIOD = 1800000; // 30 minutes
13
+ /**
14
+ * Determine whether a session should be considered stale based on its age,
15
+ * a configurable threshold, and an optional last-interaction timestamp.
16
+ *
17
+ * @param requestTimestamp - When the session was originally created (epoch ms)
18
+ * @param staleThreshold - Maximum allowed age before a session is stale (ms)
19
+ * @param lastInteraction - Last time the user interacted with this session (epoch ms)
20
+ * @param gracePeriod - Time after an interaction during which the session is not stale (ms)
21
+ * @returns true if the session is stale
22
+ */
23
+ export function isSessionStale(requestTimestamp, staleThreshold, lastInteraction, gracePeriod = DEFAULT_GRACE_PERIOD) {
24
+ const now = Date.now();
25
+ // If the user interacted recently (within grace period), session is not stale
26
+ if (lastInteraction && now - lastInteraction < gracePeriod) {
27
+ return false;
28
+ }
29
+ return now - requestTimestamp > staleThreshold;
30
+ }
31
+ /**
32
+ * Check whether a session has been explicitly marked as abandoned
33
+ * (e.g. AI disconnected).
34
+ *
35
+ * @param status - The session's current status string
36
+ * @returns true if status is "abandoned"
37
+ */
38
+ export function isSessionAbandoned(status) {
39
+ return status === "abandoned";
40
+ }
41
+ /**
42
+ * Build a human-readable toast message for a stale session.
43
+ *
44
+ * @param sessionTitle - Display name or ID of the session
45
+ * @param createdAtMs - Session creation time in epoch milliseconds
46
+ * @returns Formatted message string
47
+ */
48
+ export function formatStaleToastMessage(sessionTitle, createdAtMs) {
49
+ const hoursAgo = Math.floor((Date.now() - createdAtMs) / 3600000);
50
+ return `Session "${sessionTitle}" may be orphaned (created ${hoursAgo}h ago)`;
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auq-mcp-server",
3
- "version": "2.2.2",
3
+ "version": "2.4.0",
4
4
  "main": "dist/index.js",
5
5
  "bin": {
6
6
  "auq": "dist/bin/auq.js"