@epiphytic/claudecodeui 1.2.2 → 1.3.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-CceDF8mT.css +32 -0
- package/dist/assets/index-Dlv06cpK.js +1245 -0
- package/dist/index.html +2 -2
- package/dist/sw.js +44 -4
- package/package.json +4 -2
- package/public/sw.js +44 -4
- package/server/database/db.js +26 -16
- package/server/database/init.sql +2 -1
- package/server/database.js +861 -0
- package/server/db-indexer.js +401 -0
- package/server/external-session-detector.js +48 -392
- package/server/history-cache.js +354 -0
- package/server/index.js +457 -48
- package/server/logger.js +59 -0
- package/server/maintenance-scheduler.js +172 -0
- package/server/messages-cache.js +485 -0
- package/server/openai-codex.js +110 -102
- package/server/orchestrator/client.js +52 -0
- package/server/process-cache.js +513 -0
- package/server/projects.js +64 -47
- package/server/routes/auth.js +18 -12
- package/server/routes/sessions.js +59 -33
- package/server/session-lock.js +2 -10
- package/server/sessions-cache.js +16 -0
- package/dist/assets/index-BGneYLVE.css +0 -32
- package/dist/assets/index-DM1BeYBg.js +0 -1245
package/server/index.js
CHANGED
|
@@ -86,6 +86,12 @@ import {
|
|
|
86
86
|
resizeSession as resizeTmuxSession,
|
|
87
87
|
} from "./tmux-manager.js";
|
|
88
88
|
import { detectExternalClaude } from "./external-session-detector.js";
|
|
89
|
+
import { createLogger } from "./logger.js";
|
|
90
|
+
import {
|
|
91
|
+
startCacheUpdater,
|
|
92
|
+
setProcessCacheActive,
|
|
93
|
+
CACHE_UPDATE_INTERVAL,
|
|
94
|
+
} from "./process-cache.js";
|
|
89
95
|
import {
|
|
90
96
|
spawnCursor,
|
|
91
97
|
abortCursorSession,
|
|
@@ -97,7 +103,12 @@ import {
|
|
|
97
103
|
abortCodexSession,
|
|
98
104
|
isCodexSessionActive,
|
|
99
105
|
getActiveCodexSessions,
|
|
106
|
+
cleanupCodexSessions,
|
|
100
107
|
} from "./openai-codex.js";
|
|
108
|
+
import {
|
|
109
|
+
registerTask,
|
|
110
|
+
setActive as setMaintenanceActive,
|
|
111
|
+
} from "./maintenance-scheduler.js";
|
|
101
112
|
import gitRoutes from "./routes/git.js";
|
|
102
113
|
import authRoutes from "./routes/auth.js";
|
|
103
114
|
import mcpRoutes from "./routes/mcp.js";
|
|
@@ -112,9 +123,27 @@ import cliAuthRoutes from "./routes/cli-auth.js";
|
|
|
112
123
|
import userRoutes from "./routes/user.js";
|
|
113
124
|
import codexRoutes from "./routes/codex.js";
|
|
114
125
|
import sessionsRoutes from "./routes/sessions.js";
|
|
115
|
-
import { updateSessionsCache } from "./sessions-cache.js";
|
|
126
|
+
import { updateSessionsCache, updateSessionTitle } from "./sessions-cache.js";
|
|
127
|
+
import {
|
|
128
|
+
getSessionTitleFromHistory,
|
|
129
|
+
invalidateCache as invalidateHistoryCache,
|
|
130
|
+
} from "./history-cache.js";
|
|
131
|
+
import {
|
|
132
|
+
getMessageList,
|
|
133
|
+
getMessageByNumber,
|
|
134
|
+
getMessagesByRange,
|
|
135
|
+
LIST_CACHE_TTL,
|
|
136
|
+
MESSAGE_CACHE_TTL,
|
|
137
|
+
} from "./messages-cache.js";
|
|
116
138
|
import { updateProjectsCache } from "./projects-cache.js";
|
|
117
139
|
import { initializeDatabase, tmuxSessionsDb } from "./database/db.js";
|
|
140
|
+
import {
|
|
141
|
+
getDatabase,
|
|
142
|
+
getProjectsFromDb,
|
|
143
|
+
getSessions as getSessionsFromDb,
|
|
144
|
+
getVersion,
|
|
145
|
+
} from "./database.js";
|
|
146
|
+
import { indexFile, indexAllProjects } from "./db-indexer.js";
|
|
118
147
|
import {
|
|
119
148
|
validateApiKey,
|
|
120
149
|
authenticateToken,
|
|
@@ -122,6 +151,9 @@ import {
|
|
|
122
151
|
} from "./middleware/auth.js";
|
|
123
152
|
import { initializeOrchestrator, StatusValues } from "./orchestrator/index.js";
|
|
124
153
|
|
|
154
|
+
// Initialize logger for main server
|
|
155
|
+
const log = createLogger("server");
|
|
156
|
+
|
|
125
157
|
// File system watcher for projects folder
|
|
126
158
|
let projectsWatcher = null;
|
|
127
159
|
const connectedClients = new Set();
|
|
@@ -185,10 +217,11 @@ async function setupProjectsWatcher() {
|
|
|
185
217
|
persistent: true,
|
|
186
218
|
ignoreInitial: true, // Don't fire events for existing files on startup
|
|
187
219
|
followSymlinks: false,
|
|
188
|
-
|
|
220
|
+
usePolling: false,
|
|
221
|
+
depth: 2, // Reduced depth limit for performance
|
|
189
222
|
awaitWriteFinish: {
|
|
190
|
-
stabilityThreshold:
|
|
191
|
-
pollInterval:
|
|
223
|
+
stabilityThreshold: 500, // Wait 500ms for file to stabilize
|
|
224
|
+
pollInterval: 200,
|
|
192
225
|
},
|
|
193
226
|
});
|
|
194
227
|
|
|
@@ -201,12 +234,18 @@ async function setupProjectsWatcher() {
|
|
|
201
234
|
// Clear project directory cache when files change
|
|
202
235
|
clearProjectDirectoryCache();
|
|
203
236
|
|
|
204
|
-
//
|
|
205
|
-
|
|
237
|
+
// Use SQLite incremental indexing instead of full re-parse
|
|
238
|
+
// This only processes new bytes in the changed file
|
|
239
|
+
const indexResult = await indexFile(filePath);
|
|
240
|
+
log.debug({ filePath, indexResult }, "File indexed");
|
|
241
|
+
|
|
242
|
+
// Get lightweight data from SQLite for client notification
|
|
243
|
+
// This is much cheaper than parsing all JSONL files
|
|
244
|
+
const projectsFromDb = getProjectsFromDb();
|
|
206
245
|
|
|
207
|
-
// Update
|
|
208
|
-
|
|
209
|
-
updateProjectsCache(
|
|
246
|
+
// Update in-memory caches from SQLite (for backward compatibility)
|
|
247
|
+
// TODO: Eventually remove these caches entirely
|
|
248
|
+
updateProjectsCache(projectsFromDb);
|
|
210
249
|
|
|
211
250
|
// Clean up stale tmux session entries from database
|
|
212
251
|
cleanupStaleTmuxSessions();
|
|
@@ -214,7 +253,7 @@ async function setupProjectsWatcher() {
|
|
|
214
253
|
// Notify all connected clients about the project changes
|
|
215
254
|
const updateMessage = JSON.stringify({
|
|
216
255
|
type: "projects_updated",
|
|
217
|
-
projects:
|
|
256
|
+
projects: projectsFromDb,
|
|
218
257
|
timestamp: new Date().toISOString(),
|
|
219
258
|
changeType: eventType,
|
|
220
259
|
changedFile: path.relative(claudeProjectsPath, filePath),
|
|
@@ -228,7 +267,7 @@ async function setupProjectsWatcher() {
|
|
|
228
267
|
} catch (error) {
|
|
229
268
|
console.error("[ERROR] Error handling project changes:", error);
|
|
230
269
|
}
|
|
231
|
-
},
|
|
270
|
+
}, 2000); // 2 second debounce to reduce UI refresh frequency
|
|
232
271
|
};
|
|
233
272
|
|
|
234
273
|
// Set up event listeners
|
|
@@ -522,6 +561,64 @@ app.use("/api", validateApiKey);
|
|
|
522
561
|
// Authentication routes (public)
|
|
523
562
|
app.use("/api/auth", authRoutes);
|
|
524
563
|
|
|
564
|
+
// Environment info endpoint (public) - used by version checker
|
|
565
|
+
app.get("/api/environment", async (req, res) => {
|
|
566
|
+
try {
|
|
567
|
+
const { execSync } = await import("child_process");
|
|
568
|
+
const serverDir = __dirname;
|
|
569
|
+
|
|
570
|
+
let isGitRepo = false;
|
|
571
|
+
let gitBranch = null;
|
|
572
|
+
let shouldCheckForUpdates = true;
|
|
573
|
+
|
|
574
|
+
// Check if server directory is a git repo
|
|
575
|
+
try {
|
|
576
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
577
|
+
cwd: serverDir,
|
|
578
|
+
stdio: "pipe",
|
|
579
|
+
});
|
|
580
|
+
isGitRepo = true;
|
|
581
|
+
|
|
582
|
+
// Get current branch
|
|
583
|
+
const branchOutput = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
584
|
+
cwd: serverDir,
|
|
585
|
+
encoding: "utf8",
|
|
586
|
+
stdio: "pipe",
|
|
587
|
+
});
|
|
588
|
+
gitBranch = branchOutput.trim();
|
|
589
|
+
|
|
590
|
+
// Only check for updates if on main/master branch
|
|
591
|
+
shouldCheckForUpdates = gitBranch === "main" || gitBranch === "master";
|
|
592
|
+
} catch {
|
|
593
|
+
// Not a git repo or git not available
|
|
594
|
+
// Likely running from npm/npx - should check for updates
|
|
595
|
+
shouldCheckForUpdates = true;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Detect if running from npx (temporary directory)
|
|
599
|
+
const isNpx =
|
|
600
|
+
process.argv[1]?.includes("_npx") ||
|
|
601
|
+
process.argv[1]?.includes("npx-") ||
|
|
602
|
+
process.env.npm_execpath?.includes("npx");
|
|
603
|
+
|
|
604
|
+
res.json({
|
|
605
|
+
isGitRepo,
|
|
606
|
+
gitBranch,
|
|
607
|
+
shouldCheckForUpdates,
|
|
608
|
+
isNpx: isNpx || false,
|
|
609
|
+
});
|
|
610
|
+
} catch (error) {
|
|
611
|
+
console.error("Environment check error:", error);
|
|
612
|
+
// Default to checking for updates on error
|
|
613
|
+
res.json({
|
|
614
|
+
isGitRepo: false,
|
|
615
|
+
gitBranch: null,
|
|
616
|
+
shouldCheckForUpdates: true,
|
|
617
|
+
isNpx: false,
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
525
622
|
// Projects API Routes (protected)
|
|
526
623
|
app.use("/api/projects", authenticateToken, projectsRoutes);
|
|
527
624
|
|
|
@@ -568,13 +665,9 @@ app.use(
|
|
|
568
665
|
etag: true,
|
|
569
666
|
lastModified: true,
|
|
570
667
|
setHeaders: (res, filePath) => {
|
|
571
|
-
// Cache icons and other static assets
|
|
668
|
+
// Cache icons and other static assets for 1 hour
|
|
572
669
|
if (filePath.match(/\.(svg|png|jpg|jpeg|gif|ico|woff2?|ttf|eot)$/)) {
|
|
573
|
-
|
|
574
|
-
res.setHeader(
|
|
575
|
-
"Cache-Control",
|
|
576
|
-
"public, max-age=604800, must-revalidate",
|
|
577
|
-
);
|
|
670
|
+
res.setHeader("Cache-Control", "public, max-age=3600, must-revalidate");
|
|
578
671
|
} else if (filePath.endsWith(".json")) {
|
|
579
672
|
// JSON files (like manifest.json) - short cache with revalidation
|
|
580
673
|
res.setHeader("Cache-Control", "public, max-age=3600, must-revalidate");
|
|
@@ -685,12 +778,62 @@ app.post("/api/system/update", authenticateToken, async (req, res) => {
|
|
|
685
778
|
app.get("/api/projects", authenticateToken, async (req, res) => {
|
|
686
779
|
try {
|
|
687
780
|
const projects = await getProjects();
|
|
781
|
+
|
|
782
|
+
// Set cache headers - 5 min cache with revalidation (public for Cloudflare)
|
|
783
|
+
const version = getVersion("projects");
|
|
784
|
+
res.set({
|
|
785
|
+
"Cache-Control": "public, max-age=300, stale-while-revalidate=60",
|
|
786
|
+
ETag: `"projects-${version.version}"`,
|
|
787
|
+
});
|
|
788
|
+
|
|
688
789
|
res.json(projects);
|
|
689
790
|
} catch (error) {
|
|
690
791
|
res.status(500).json({ error: error.message });
|
|
691
792
|
}
|
|
692
793
|
});
|
|
693
794
|
|
|
795
|
+
// External Claude session detection endpoint
|
|
796
|
+
// Returns cached process data, refreshed every 60 seconds
|
|
797
|
+
// Cacheable by clients for up to 60 seconds
|
|
798
|
+
app.get("/api/external-sessions", authenticateToken, async (req, res) => {
|
|
799
|
+
try {
|
|
800
|
+
const { projectPath } = req.query;
|
|
801
|
+
|
|
802
|
+
// Set cache headers - clients can cache for up to 60 seconds
|
|
803
|
+
res.setHeader("Cache-Control", "private, max-age=60");
|
|
804
|
+
|
|
805
|
+
const result = detectExternalClaude(projectPath || null);
|
|
806
|
+
|
|
807
|
+
log.debug(
|
|
808
|
+
{
|
|
809
|
+
projectPath,
|
|
810
|
+
hasExternalSession: result.hasExternalSession,
|
|
811
|
+
processCount: result.processes.length,
|
|
812
|
+
cacheAge: result.cacheAge,
|
|
813
|
+
},
|
|
814
|
+
"External session check",
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
res.json({
|
|
818
|
+
hasExternalSession: result.hasExternalSession,
|
|
819
|
+
detectionAvailable: result.detectionAvailable,
|
|
820
|
+
detectionError: result.detectionError,
|
|
821
|
+
cacheAge: result.cacheAge,
|
|
822
|
+
details: result.hasExternalSession
|
|
823
|
+
? {
|
|
824
|
+
processIds: result.processes.map((p) => p.pid),
|
|
825
|
+
commands: result.processes.map((p) => p.command),
|
|
826
|
+
tmuxSessions: result.tmuxSessions.map((s) => s.sessionName),
|
|
827
|
+
lockFile: result.lockFile.exists ? result.lockFile.lockFile : null,
|
|
828
|
+
}
|
|
829
|
+
: null,
|
|
830
|
+
});
|
|
831
|
+
} catch (error) {
|
|
832
|
+
log.error({ error: error.message }, "External session check failed");
|
|
833
|
+
res.status(500).json({ error: error.message });
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
|
|
694
837
|
app.get(
|
|
695
838
|
"/api/projects/:projectName/sessions",
|
|
696
839
|
authenticateToken,
|
|
@@ -764,6 +907,130 @@ app.get(
|
|
|
764
907
|
},
|
|
765
908
|
);
|
|
766
909
|
|
|
910
|
+
// NEW: Get message list (IDs and numbers only) - cached for 60 seconds
|
|
911
|
+
// This is the efficient way to check for new messages
|
|
912
|
+
app.get(
|
|
913
|
+
"/api/projects/:projectName/sessions/:sessionId/messages/list",
|
|
914
|
+
authenticateToken,
|
|
915
|
+
async (req, res) => {
|
|
916
|
+
try {
|
|
917
|
+
const { projectName, sessionId } = req.params;
|
|
918
|
+
|
|
919
|
+
const result = await getMessageList(projectName, sessionId);
|
|
920
|
+
|
|
921
|
+
// Generate ETag based on total count and cache timestamp
|
|
922
|
+
const currentETag = `"list-${sessionId}-${result.total}-${result.cachedAt}"`;
|
|
923
|
+
|
|
924
|
+
// Check If-None-Match header
|
|
925
|
+
const clientETag = req.headers["if-none-match"];
|
|
926
|
+
if (clientETag && clientETag === currentETag) {
|
|
927
|
+
return res.status(304).end();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Set caching headers - 60 second cache (public for Cloudflare)
|
|
931
|
+
res.set({
|
|
932
|
+
"Cache-Control": `public, max-age=${Math.floor(LIST_CACHE_TTL / 1000)}, stale-while-revalidate=30`,
|
|
933
|
+
ETag: currentETag,
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
res.json(result);
|
|
937
|
+
} catch (error) {
|
|
938
|
+
console.error("[MessagesListEndpoint] Error:", error.message);
|
|
939
|
+
res.status(500).json({ error: error.message });
|
|
940
|
+
}
|
|
941
|
+
},
|
|
942
|
+
);
|
|
943
|
+
|
|
944
|
+
// NEW: Get a single message by number (1-indexed) - cached for 30 minutes
|
|
945
|
+
// Messages are immutable, so they can be cached for a long time
|
|
946
|
+
app.get(
|
|
947
|
+
"/api/projects/:projectName/sessions/:sessionId/messages/number/:messageNumber",
|
|
948
|
+
authenticateToken,
|
|
949
|
+
async (req, res) => {
|
|
950
|
+
try {
|
|
951
|
+
const { projectName, sessionId, messageNumber } = req.params;
|
|
952
|
+
const num = parseInt(messageNumber, 10);
|
|
953
|
+
|
|
954
|
+
if (isNaN(num) || num < 1) {
|
|
955
|
+
return res.status(400).json({ error: "Invalid message number" });
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const message = await getMessageByNumber(projectName, sessionId, num);
|
|
959
|
+
|
|
960
|
+
if (!message) {
|
|
961
|
+
return res.status(404).json({ error: "Message not found" });
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Generate ETag based on message ID and timestamp
|
|
965
|
+
const msgId = message.uuid || message.id || `msg_${num}`;
|
|
966
|
+
const currentETag = `"msg-${sessionId}-${num}-${msgId}"`;
|
|
967
|
+
|
|
968
|
+
// Check If-None-Match header
|
|
969
|
+
const clientETag = req.headers["if-none-match"];
|
|
970
|
+
if (clientETag && clientETag === currentETag) {
|
|
971
|
+
return res.status(304).end();
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Set caching headers - 30 minute cache (messages are immutable, public for Cloudflare)
|
|
975
|
+
res.set({
|
|
976
|
+
"Cache-Control": `public, max-age=${Math.floor(MESSAGE_CACHE_TTL / 1000)}, immutable`,
|
|
977
|
+
ETag: currentETag,
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
res.json({ number: num, message });
|
|
981
|
+
} catch (error) {
|
|
982
|
+
console.error("[MessageByNumberEndpoint] Error:", error.message);
|
|
983
|
+
res.status(500).json({ error: error.message });
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
// NEW: Get messages by number range - for batch fetching
|
|
989
|
+
app.get(
|
|
990
|
+
"/api/projects/:projectName/sessions/:sessionId/messages/range/:start/:end",
|
|
991
|
+
authenticateToken,
|
|
992
|
+
async (req, res) => {
|
|
993
|
+
try {
|
|
994
|
+
const { projectName, sessionId, start, end } = req.params;
|
|
995
|
+
const startNum = parseInt(start, 10);
|
|
996
|
+
const endNum = parseInt(end, 10);
|
|
997
|
+
|
|
998
|
+
if (
|
|
999
|
+
isNaN(startNum) ||
|
|
1000
|
+
isNaN(endNum) ||
|
|
1001
|
+
startNum < 1 ||
|
|
1002
|
+
endNum < startNum
|
|
1003
|
+
) {
|
|
1004
|
+
return res.status(400).json({ error: "Invalid range" });
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Limit range to prevent abuse
|
|
1008
|
+
if (endNum - startNum > 100) {
|
|
1009
|
+
return res
|
|
1010
|
+
.status(400)
|
|
1011
|
+
.json({ error: "Range too large (max 100 messages)" });
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const messages = await getMessagesByRange(
|
|
1015
|
+
projectName,
|
|
1016
|
+
sessionId,
|
|
1017
|
+
startNum,
|
|
1018
|
+
endNum,
|
|
1019
|
+
);
|
|
1020
|
+
|
|
1021
|
+
// Set cache - range queries for initial load (public for Cloudflare)
|
|
1022
|
+
res.set({
|
|
1023
|
+
"Cache-Control": "public, max-age=60, stale-while-revalidate=30",
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
res.json({ messages, start: startNum, end: endNum });
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
console.error("[MessageRangeEndpoint] Error:", error.message);
|
|
1029
|
+
res.status(500).json({ error: error.message });
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
);
|
|
1033
|
+
|
|
767
1034
|
// Rename project endpoint
|
|
768
1035
|
app.put(
|
|
769
1036
|
"/api/projects/:projectName/rename",
|
|
@@ -779,6 +1046,42 @@ app.put(
|
|
|
779
1046
|
},
|
|
780
1047
|
);
|
|
781
1048
|
|
|
1049
|
+
// Avatar proxy endpoint - serves GitHub avatars with aggressive caching for Cloudflare
|
|
1050
|
+
app.get("/api/avatar/:githubId", authenticateToken, async (req, res) => {
|
|
1051
|
+
try {
|
|
1052
|
+
const { githubId } = req.params;
|
|
1053
|
+
const size = req.query.s || "80";
|
|
1054
|
+
|
|
1055
|
+
// Validate githubId (should be numeric)
|
|
1056
|
+
if (!/^\d+$/.test(githubId)) {
|
|
1057
|
+
return res.status(400).json({ error: "Invalid GitHub ID" });
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Fetch from GitHub
|
|
1061
|
+
const githubUrl = `https://avatars.githubusercontent.com/u/${githubId}?s=${size}&v=4`;
|
|
1062
|
+
const response = await fetch(githubUrl);
|
|
1063
|
+
|
|
1064
|
+
if (!response.ok) {
|
|
1065
|
+
return res.status(response.status).json({ error: "Avatar not found" });
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Set cache headers for aggressive caching (1 year, public for Cloudflare)
|
|
1069
|
+
res.set({
|
|
1070
|
+
"Content-Type": response.headers.get("content-type") || "image/png",
|
|
1071
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
1072
|
+
"CDN-Cache-Control": "public, max-age=31536000",
|
|
1073
|
+
"Cloudflare-CDN-Cache-Control": "public, max-age=31536000",
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
// Pipe the avatar to the response
|
|
1077
|
+
const buffer = await response.arrayBuffer();
|
|
1078
|
+
res.send(Buffer.from(buffer));
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
console.error("[AvatarProxy] Error:", error.message);
|
|
1081
|
+
res.status(500).json({ error: "Failed to fetch avatar" });
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
|
|
782
1085
|
// Delete session endpoint
|
|
783
1086
|
app.delete(
|
|
784
1087
|
"/api/projects/:projectName/sessions/:sessionId",
|
|
@@ -1231,6 +1534,10 @@ function handleChatConnection(ws) {
|
|
|
1231
1534
|
|
|
1232
1535
|
// Add to connected clients for project updates
|
|
1233
1536
|
connectedClients.add(ws);
|
|
1537
|
+
// Activate process cache and maintenance scheduler when clients are connected
|
|
1538
|
+
const hasClients = connectedClients.size > 0;
|
|
1539
|
+
setProcessCacheActive(hasClients);
|
|
1540
|
+
setMaintenanceActive(hasClients);
|
|
1234
1541
|
|
|
1235
1542
|
// Generate unique connection ID for orchestrator status tracking
|
|
1236
1543
|
const connectionId = `ws-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -1252,6 +1559,10 @@ function handleChatConnection(ws) {
|
|
|
1252
1559
|
console.log("🔌 Chat client disconnected");
|
|
1253
1560
|
// Remove from connected clients
|
|
1254
1561
|
connectedClients.delete(ws);
|
|
1562
|
+
// Update process cache and maintenance scheduler based on remaining clients
|
|
1563
|
+
const hasClients = connectedClients.size > 0;
|
|
1564
|
+
setProcessCacheActive(hasClients);
|
|
1565
|
+
setMaintenanceActive(hasClients);
|
|
1255
1566
|
// Track disconnection for orchestrator status (if enabled)
|
|
1256
1567
|
if (orchestratorStatusHooks) {
|
|
1257
1568
|
orchestratorStatusHooks.onConnectionClose(connectionId);
|
|
@@ -1270,26 +1581,28 @@ async function handleChatMessage(ws, writer, messageData) {
|
|
|
1270
1581
|
// Handle proactive external session check (before user submits a prompt)
|
|
1271
1582
|
if (data.type === "check-external-session") {
|
|
1272
1583
|
const projectPath = data.projectPath;
|
|
1273
|
-
|
|
1274
|
-
"[ExternalSessionCheck] Checking for external sessions:",
|
|
1275
|
-
projectPath,
|
|
1276
|
-
);
|
|
1584
|
+
log.debug({ projectPath }, "External session check requested");
|
|
1277
1585
|
if (projectPath) {
|
|
1278
1586
|
const externalCheck = detectExternalClaude(projectPath);
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1587
|
+
log.debug(
|
|
1588
|
+
{
|
|
1589
|
+
projectPath,
|
|
1590
|
+
hasExternalSession: externalCheck.hasExternalSession,
|
|
1591
|
+
detectionAvailable: externalCheck.detectionAvailable,
|
|
1592
|
+
detectionError: externalCheck.detectionError,
|
|
1593
|
+
processCount: externalCheck.processes.length,
|
|
1594
|
+
tmuxCount: externalCheck.tmuxSessions.length,
|
|
1595
|
+
hasLockFile: externalCheck.lockFile.exists,
|
|
1596
|
+
},
|
|
1597
|
+
"External session check result",
|
|
1598
|
+
);
|
|
1287
1599
|
writer.send({
|
|
1288
1600
|
type: "external-session-check-result",
|
|
1289
1601
|
projectPath,
|
|
1290
1602
|
hasExternalSession: externalCheck.hasExternalSession,
|
|
1291
1603
|
detectionAvailable: externalCheck.detectionAvailable,
|
|
1292
1604
|
detectionError: externalCheck.detectionError,
|
|
1605
|
+
cacheAge: externalCheck.cacheAge,
|
|
1293
1606
|
details: externalCheck.hasExternalSession
|
|
1294
1607
|
? {
|
|
1295
1608
|
processIds: externalCheck.processes.map((p) => p.pid),
|
|
@@ -1304,15 +1617,22 @@ async function handleChatMessage(ws, writer, messageData) {
|
|
|
1304
1617
|
: null,
|
|
1305
1618
|
});
|
|
1306
1619
|
} else {
|
|
1307
|
-
|
|
1620
|
+
log.debug("External session check: no projectPath provided");
|
|
1308
1621
|
}
|
|
1309
1622
|
return;
|
|
1310
1623
|
}
|
|
1311
1624
|
|
|
1312
1625
|
if (data.type === "claude-command") {
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1626
|
+
log.debug(
|
|
1627
|
+
{
|
|
1628
|
+
command: data.command
|
|
1629
|
+
? data.command.slice(0, 50)
|
|
1630
|
+
: "[Continue/Resume]",
|
|
1631
|
+
project: data.options?.projectPath || "Unknown",
|
|
1632
|
+
sessionMode: data.options?.sessionId ? "Resume" : "New",
|
|
1633
|
+
},
|
|
1634
|
+
"User message received",
|
|
1635
|
+
);
|
|
1316
1636
|
|
|
1317
1637
|
const projectPath = data.options?.projectPath;
|
|
1318
1638
|
const sessionId = data.options?.sessionId || sessionIdForTracking;
|
|
@@ -1341,10 +1661,19 @@ async function handleChatMessage(ws, writer, messageData) {
|
|
|
1341
1661
|
if (projectPath) {
|
|
1342
1662
|
const externalCheck = detectExternalClaude(projectPath);
|
|
1343
1663
|
if (externalCheck.hasExternalSession) {
|
|
1664
|
+
log.info(
|
|
1665
|
+
{
|
|
1666
|
+
projectPath,
|
|
1667
|
+
processCount: externalCheck.processes.length,
|
|
1668
|
+
tmuxCount: externalCheck.tmuxSessions.length,
|
|
1669
|
+
},
|
|
1670
|
+
"External Claude session detected during chat",
|
|
1671
|
+
);
|
|
1344
1672
|
writer.send({
|
|
1345
1673
|
type: "external-session-detected",
|
|
1346
1674
|
projectPath,
|
|
1347
1675
|
detectionAvailable: externalCheck.detectionAvailable,
|
|
1676
|
+
cacheAge: externalCheck.cacheAge,
|
|
1348
1677
|
details: {
|
|
1349
1678
|
processIds: externalCheck.processes.map((p) => p.pid),
|
|
1350
1679
|
tmuxSessions: externalCheck.tmuxSessions.map(
|
|
@@ -1413,6 +1742,27 @@ async function handleChatMessage(ws, writer, messageData) {
|
|
|
1413
1742
|
if (orchestratorStatusHooks) {
|
|
1414
1743
|
orchestratorStatusHooks.onQueryEnd(sessionIdForTracking);
|
|
1415
1744
|
}
|
|
1745
|
+
|
|
1746
|
+
// Update session title from history.jsonl (last user prompt)
|
|
1747
|
+
try {
|
|
1748
|
+
// Invalidate history cache to get fresh data
|
|
1749
|
+
invalidateHistoryCache();
|
|
1750
|
+
const newTitle = await getSessionTitleFromHistory(sessionId);
|
|
1751
|
+
if (newTitle) {
|
|
1752
|
+
const updated = updateSessionTitle(sessionId, newTitle);
|
|
1753
|
+
if (updated) {
|
|
1754
|
+
log.debug(
|
|
1755
|
+
{ sessionId, title: newTitle.slice(0, 50) },
|
|
1756
|
+
"Updated session title from history",
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
} catch (titleError) {
|
|
1761
|
+
log.debug(
|
|
1762
|
+
{ sessionId, error: titleError.message },
|
|
1763
|
+
"Failed to update session title",
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1416
1766
|
}
|
|
1417
1767
|
} else if (data.type === "cursor-command") {
|
|
1418
1768
|
console.log(
|
|
@@ -2997,28 +3347,87 @@ async function startServer() {
|
|
|
2997
3347
|
);
|
|
2998
3348
|
console.log("");
|
|
2999
3349
|
|
|
3000
|
-
//
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
// Initialize sessions and projects caches with initial project data
|
|
3350
|
+
// Initialize SQLite database and index all projects
|
|
3351
|
+
// NOTE: This must happen BEFORE starting the file watcher to avoid contention
|
|
3004
3352
|
try {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3353
|
+
console.log(`${c.info("[INFO]")} Initializing SQLite database...`);
|
|
3354
|
+
|
|
3355
|
+
// Initialize SQLite database connection
|
|
3356
|
+
getDatabase();
|
|
3357
|
+
|
|
3358
|
+
// Run full indexing of all projects
|
|
3359
|
+
// This is incremental - only processes changed/new files
|
|
3360
|
+
const indexResult = await indexAllProjects();
|
|
3361
|
+
|
|
3362
|
+
if (indexResult.success) {
|
|
3363
|
+
console.log(
|
|
3364
|
+
`${c.ok("[OK]")} SQLite database indexed: ${indexResult.stats.projects} projects, ${indexResult.stats.sessions} sessions`,
|
|
3365
|
+
);
|
|
3366
|
+
} else {
|
|
3367
|
+
console.warn(
|
|
3368
|
+
`${c.warn("[WARN]")} Indexing incomplete: ${indexResult.error}`,
|
|
3369
|
+
);
|
|
3370
|
+
}
|
|
3371
|
+
|
|
3372
|
+
// Populate in-memory caches from SQLite (for backward compatibility)
|
|
3373
|
+
// TODO: Eventually remove these caches and read directly from SQLite
|
|
3374
|
+
const projectsFromDb = getProjectsFromDb();
|
|
3375
|
+
updateProjectsCache(projectsFromDb);
|
|
3376
|
+
|
|
3011
3377
|
console.log(
|
|
3012
|
-
`${c.ok("[OK]")}
|
|
3378
|
+
`${c.ok("[OK]")} Caches initialized from SQLite with ${projectsFromDb.length} projects`,
|
|
3013
3379
|
);
|
|
3014
|
-
|
|
3015
|
-
// Clean up stale tmux session entries on startup
|
|
3016
|
-
cleanupStaleTmuxSessions();
|
|
3017
3380
|
} catch (cacheError) {
|
|
3018
3381
|
console.warn(
|
|
3019
|
-
`${c.warn("[WARN]")} Failed to initialize
|
|
3382
|
+
`${c.warn("[WARN]")} Failed to initialize database: ${cacheError.message}`,
|
|
3020
3383
|
);
|
|
3384
|
+
// Fall back to legacy getProjects if SQLite fails
|
|
3385
|
+
try {
|
|
3386
|
+
const initialProjects = await getProjects();
|
|
3387
|
+
updateSessionsCache(initialProjects);
|
|
3388
|
+
updateProjectsCache(initialProjects);
|
|
3389
|
+
console.log(
|
|
3390
|
+
`${c.ok("[OK]")} Caches initialized (legacy) with ${initialProjects.length} projects`,
|
|
3391
|
+
);
|
|
3392
|
+
} catch (fallbackError) {
|
|
3393
|
+
console.error(
|
|
3394
|
+
`${c.warn("[ERROR]")} Cache initialization failed completely`,
|
|
3395
|
+
);
|
|
3396
|
+
}
|
|
3021
3397
|
}
|
|
3398
|
+
|
|
3399
|
+
// Start watching the projects folder for changes (after cache is initialized)
|
|
3400
|
+
await setupProjectsWatcher();
|
|
3401
|
+
|
|
3402
|
+
// Clean up stale tmux session entries on startup
|
|
3403
|
+
cleanupStaleTmuxSessions();
|
|
3404
|
+
|
|
3405
|
+
// Start the process cache updater for external session detection
|
|
3406
|
+
startCacheUpdater();
|
|
3407
|
+
console.log(
|
|
3408
|
+
`${c.ok("[OK]")} Process cache started (updates every ${CACHE_UPDATE_INTERVAL / 1000}s)`,
|
|
3409
|
+
);
|
|
3410
|
+
|
|
3411
|
+
// Register maintenance tasks with centralized scheduler
|
|
3412
|
+
// These tasks only run when WebSocket clients are connected
|
|
3413
|
+
registerTask(
|
|
3414
|
+
"session-lock-cleanup",
|
|
3415
|
+
() => {
|
|
3416
|
+
const cleaned = sessionLock.cleanupStaleLocks();
|
|
3417
|
+
if (cleaned > 0) {
|
|
3418
|
+
console.log(`[SessionLock] Cleaned up ${cleaned} stale locks`);
|
|
3419
|
+
}
|
|
3420
|
+
},
|
|
3421
|
+
5 * 60 * 1000, // 5 minutes
|
|
3422
|
+
);
|
|
3423
|
+
registerTask(
|
|
3424
|
+
"codex-session-cleanup",
|
|
3425
|
+
cleanupCodexSessions,
|
|
3426
|
+
5 * 60 * 1000,
|
|
3427
|
+
);
|
|
3428
|
+
console.log(
|
|
3429
|
+
`${c.ok("[OK]")} Maintenance scheduler initialized (idle detection enabled)`,
|
|
3430
|
+
);
|
|
3022
3431
|
});
|
|
3023
3432
|
} catch (error) {
|
|
3024
3433
|
console.error("[ERROR] Failed to start server:", error);
|