@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.
- package/dist/assets/index-DqxzEd_8.js +1245 -0
- package/dist/assets/index-r43D8sh4.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/database/db.js +222 -0
- package/server/database/init.sql +27 -1
- package/server/external-session-detector.js +403 -0
- package/server/index.js +885 -116
- package/server/orchestrator/client.js +361 -16
- package/server/orchestrator/index.js +83 -8
- package/server/orchestrator/protocol.js +67 -0
- package/server/projects-cache.js +196 -0
- package/server/projects.js +760 -464
- package/server/routes/projects.js +248 -92
- package/server/routes/sessions.js +106 -0
- package/server/session-lock.js +253 -0
- package/server/sessions-cache.js +183 -0
- package/server/tmux-manager.js +403 -0
- package/dist/assets/index-COkp1acE.js +0 -1231
- package/dist/assets/index-DfR9xEkp.css +0 -32
|
@@ -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
|
+
};
|