@epiphytic/claudecodeui 1.0.1 → 1.2.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.
@@ -17,6 +17,8 @@ export const OutboundMessageTypes = {
17
17
  RESPONSE_COMPLETE: "response_complete",
18
18
  ERROR: "error",
19
19
  HTTP_PROXY_RESPONSE: "http_proxy_response",
20
+ // Pending mode messages
21
+ PENDING_REGISTER: "pending_register",
20
22
  };
21
23
 
22
24
  /**
@@ -30,6 +32,11 @@ export const InboundMessageTypes = {
30
32
  USER_REQUEST: "user_request",
31
33
  USER_REQUEST_FOLLOW_UP: "user_request_follow_up",
32
34
  HTTP_PROXY_REQUEST: "http_proxy_request",
35
+ // Pending mode responses
36
+ PENDING_REGISTERED: "pending_registered",
37
+ TOKEN_GRANTED: "token_granted",
38
+ AUTHORIZATION_DENIED: "authorization_denied",
39
+ AUTHORIZATION_TIMEOUT: "authorization_timeout",
33
40
  };
34
41
 
35
42
  /**
@@ -97,6 +104,29 @@ export function createPingMessage(clientId) {
97
104
  };
98
105
  }
99
106
 
107
+ /**
108
+ * Creates a pending register message for pending mode
109
+ * @param {string} pendingId - Unique pending client identifier
110
+ * @param {string} hostname - Machine hostname
111
+ * @param {string} project - Current project/working directory
112
+ * @param {string} platform - Operating system platform
113
+ * @returns {Object} Pending register message
114
+ */
115
+ export function createPendingRegisterMessage(
116
+ pendingId,
117
+ hostname,
118
+ project,
119
+ platform,
120
+ ) {
121
+ return {
122
+ type: OutboundMessageTypes.PENDING_REGISTER,
123
+ pending_id: pendingId,
124
+ hostname,
125
+ project,
126
+ platform,
127
+ };
128
+ }
129
+
100
130
  /**
101
131
  * Creates a response message (for proxied requests)
102
132
  * @param {string} requestId - Original request ID
@@ -254,6 +284,43 @@ export function validateInboundMessage(message) {
254
284
  typeof message.path === "string"
255
285
  );
256
286
 
287
+ case InboundMessageTypes.PENDING_REGISTERED:
288
+ // pending_registered message structure:
289
+ // {
290
+ // type: "pending_registered",
291
+ // success: boolean,
292
+ // message?: string
293
+ // }
294
+ return typeof message.success === "boolean";
295
+
296
+ case InboundMessageTypes.TOKEN_GRANTED:
297
+ // token_granted message structure:
298
+ // {
299
+ // type: "token_granted",
300
+ // token: string, // Full token: "ao_xxx_yyy"
301
+ // client_id: string // Assigned client ID
302
+ // }
303
+ return (
304
+ typeof message.token === "string" &&
305
+ typeof message.client_id === "string"
306
+ );
307
+
308
+ case InboundMessageTypes.AUTHORIZATION_DENIED:
309
+ // authorization_denied message structure:
310
+ // {
311
+ // type: "authorization_denied",
312
+ // reason: string
313
+ // }
314
+ return typeof message.reason === "string";
315
+
316
+ case InboundMessageTypes.AUTHORIZATION_TIMEOUT:
317
+ // authorization_timeout message structure:
318
+ // {
319
+ // type: "authorization_timeout",
320
+ // message: string
321
+ // }
322
+ return typeof message.message === "string";
323
+
257
324
  default:
258
325
  // Unknown message types are considered valid (forward compatibility)
259
326
  return true;
@@ -0,0 +1,196 @@
1
+ /**
2
+ * PROJECTS CACHE MODULE
3
+ * =====================
4
+ *
5
+ * In-memory cache for projects data with ETag support.
6
+ * Updated by the chokidar watcher when project files change.
7
+ * Mirrors the sessions-cache.js pattern for consistency.
8
+ */
9
+
10
+ import crypto from "crypto";
11
+
12
+ // Cache state
13
+ let cachedProjects = [];
14
+ let cacheVersion = 0;
15
+ let cacheTimestamp = null;
16
+
17
+ /**
18
+ * Timeframe definitions in milliseconds
19
+ * (Same as sessions-cache.js for consistency)
20
+ */
21
+ const TIMEFRAME_MS = {
22
+ "1h": 60 * 60 * 1000,
23
+ "8h": 8 * 60 * 60 * 1000,
24
+ "1d": 24 * 60 * 60 * 1000,
25
+ "1w": 7 * 24 * 60 * 60 * 1000,
26
+ "2w": 14 * 24 * 60 * 60 * 1000,
27
+ "1m": 30 * 24 * 60 * 60 * 1000,
28
+ all: Infinity,
29
+ };
30
+
31
+ /**
32
+ * Calculate last activity timestamp for a project
33
+ * Based on the most recent session across all providers
34
+ */
35
+ function calculateLastActivity(project) {
36
+ let lastActivity = null;
37
+
38
+ // Check Claude sessions
39
+ if (project.sessions && project.sessions.length > 0) {
40
+ for (const session of project.sessions) {
41
+ const sessionDate = new Date(session.lastActivity);
42
+ if (!lastActivity || sessionDate > lastActivity) {
43
+ lastActivity = sessionDate;
44
+ }
45
+ }
46
+ }
47
+
48
+ // Check Cursor sessions
49
+ if (project.cursorSessions && project.cursorSessions.length > 0) {
50
+ for (const session of project.cursorSessions) {
51
+ const sessionDate = new Date(session.createdAt || session.lastActivity);
52
+ if (!lastActivity || sessionDate > lastActivity) {
53
+ lastActivity = sessionDate;
54
+ }
55
+ }
56
+ }
57
+
58
+ // Check Codex sessions
59
+ if (project.codexSessions && project.codexSessions.length > 0) {
60
+ for (const session of project.codexSessions) {
61
+ const sessionDate = new Date(session.lastActivity || session.createdAt);
62
+ if (!lastActivity || sessionDate > lastActivity) {
63
+ lastActivity = sessionDate;
64
+ }
65
+ }
66
+ }
67
+
68
+ return lastActivity ? lastActivity.toISOString() : null;
69
+ }
70
+
71
+ /**
72
+ * Transform full project data to slim format
73
+ */
74
+ function toSlimProject(project) {
75
+ const claudeCount = project.sessions?.length || 0;
76
+ const cursorCount = project.cursorSessions?.length || 0;
77
+ const codexCount = project.codexSessions?.length || 0;
78
+
79
+ return {
80
+ name: project.name,
81
+ displayName: project.displayName,
82
+ fullPath: project.fullPath || project.path,
83
+ sessionCount: claudeCount + cursorCount + codexCount,
84
+ lastActivity: calculateLastActivity(project),
85
+ hasClaudeSessions: claudeCount > 0,
86
+ hasCursorSessions: cursorCount > 0,
87
+ hasCodexSessions: codexCount > 0,
88
+ hasTaskmaster: project.taskmaster?.hasTaskmaster || false,
89
+ sessionMeta: project.sessionMeta,
90
+ isManuallyAdded: project.isManuallyAdded || false,
91
+ isCustomName: project.isCustomName || false,
92
+ };
93
+ }
94
+
95
+ /**
96
+ * Update the projects cache from full projects data
97
+ * Called after getProjects() completes
98
+ */
99
+ function updateProjectsCache(projects) {
100
+ // Transform to slim format
101
+ cachedProjects = projects.map(toSlimProject);
102
+
103
+ // Sort by lastActivity descending (most recent first)
104
+ cachedProjects.sort((a, b) => {
105
+ const dateA = a.lastActivity ? new Date(a.lastActivity) : new Date(0);
106
+ const dateB = b.lastActivity ? new Date(b.lastActivity) : new Date(0);
107
+ return dateB - dateA;
108
+ });
109
+
110
+ cacheVersion++;
111
+ cacheTimestamp = new Date().toISOString();
112
+ }
113
+
114
+ /**
115
+ * Get projects filtered by timeframe
116
+ * Projects are included if their lastActivity is within the timeframe
117
+ */
118
+ function getProjectsByTimeframe(timeframe = "1w") {
119
+ const now = Date.now();
120
+ const cutoffMs = TIMEFRAME_MS[timeframe] || TIMEFRAME_MS["1w"];
121
+
122
+ if (cutoffMs === Infinity) {
123
+ return {
124
+ projects: cachedProjects,
125
+ totalCount: cachedProjects.length,
126
+ filteredCount: cachedProjects.length,
127
+ };
128
+ }
129
+
130
+ const cutoffTime = now - cutoffMs;
131
+ const filteredProjects = cachedProjects.filter((project) => {
132
+ if (!project.lastActivity) {
133
+ return false; // Exclude projects with no sessions
134
+ }
135
+ const projectTime = new Date(project.lastActivity).getTime();
136
+ return projectTime >= cutoffTime;
137
+ });
138
+
139
+ return {
140
+ projects: filteredProjects,
141
+ totalCount: cachedProjects.length,
142
+ filteredCount: filteredProjects.length,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Generate ETag for current cache state + timeframe
148
+ */
149
+ function generateETag(timeframe = "1w") {
150
+ const hash = crypto.createHash("md5");
151
+ hash.update(`projects-${cacheVersion}-${cacheTimestamp}-${timeframe}`);
152
+ return `"${hash.digest("hex")}"`;
153
+ }
154
+
155
+ /**
156
+ * Get cache metadata
157
+ */
158
+ function getCacheMeta() {
159
+ return {
160
+ version: cacheVersion,
161
+ timestamp: cacheTimestamp,
162
+ projectCount: cachedProjects.length,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Check if cache is initialized
168
+ */
169
+ function isCacheInitialized() {
170
+ return cacheTimestamp !== null;
171
+ }
172
+
173
+ /**
174
+ * Get the raw cached projects (for initial load)
175
+ */
176
+ function getCachedProjects() {
177
+ return cachedProjects;
178
+ }
179
+
180
+ /**
181
+ * Get a single project from cache by name
182
+ */
183
+ function getProjectFromCache(projectName) {
184
+ return cachedProjects.find((p) => p.name === projectName) || null;
185
+ }
186
+
187
+ export {
188
+ updateProjectsCache,
189
+ getProjectsByTimeframe,
190
+ generateETag,
191
+ getCacheMeta,
192
+ isCacheInitialized,
193
+ getCachedProjects,
194
+ getProjectFromCache,
195
+ TIMEFRAME_MS,
196
+ };