@epiphytic/claudecodeui 1.0.1 → 1.1.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-D0xTNXrF.js +1247 -0
- package/dist/assets/index-DKDK7xNY.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/database/db.js +124 -0
- package/server/database/init.sql +15 -1
- package/server/external-session-detector.js +403 -0
- package/server/index.js +816 -110
- package/server/projects-cache.js +196 -0
- package/server/projects.js +759 -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
|
@@ -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
|
+
};
|