@epiphytic/claudecodeui 1.1.0 → 1.2.1
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-BGneYLVE.css +32 -0
- package/dist/assets/{index-D0xTNXrF.js → index-sqmQ9jF8.js} +210 -212
- package/dist/index.html +2 -2
- package/dist/sw.js +25 -1
- package/package.json +2 -1
- package/public/api-docs.html +879 -0
- package/public/clear-cache.html +85 -0
- package/public/convert-icons.md +53 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +9 -0
- package/public/generate-icons.js +49 -0
- package/public/icons/claude-ai-icon.svg +1 -0
- package/public/icons/codex-white.svg +3 -0
- package/public/icons/codex.svg +3 -0
- package/public/icons/cursor-white.svg +12 -0
- package/public/icons/cursor.svg +1 -0
- package/public/icons/generate-icons.md +19 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-128x128.svg +12 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-144x144.svg +12 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-152x152.svg +12 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-192x192.svg +12 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-384x384.svg +12 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-512x512.svg +12 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-72x72.svg +12 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/icons/icon-96x96.svg +12 -0
- package/public/icons/icon-template.svg +12 -0
- package/public/logo-128.png +0 -0
- package/public/logo-256.png +0 -0
- package/public/logo-32.png +0 -0
- package/public/logo-512.png +0 -0
- package/public/logo-64.png +0 -0
- package/public/logo.svg +17 -0
- package/public/manifest.json +61 -0
- package/public/screenshots/cli-selection.png +0 -0
- package/public/screenshots/desktop-main.png +0 -0
- package/public/screenshots/mobile-chat.png +0 -0
- package/public/screenshots/tools-modal.png +0 -0
- package/public/sw.js +131 -0
- package/server/database/db.js +98 -0
- package/server/database/init.sql +13 -1
- package/server/external-session-detector.js +188 -48
- package/server/index.js +210 -7
- package/server/orchestrator/client.js +361 -16
- package/server/orchestrator/index.js +83 -8
- package/server/orchestrator/protocol.js +67 -0
- package/server/projects.js +2 -1
- package/dist/assets/index-DKDK7xNY.css +0 -32
package/server/index.js
CHANGED
|
@@ -379,6 +379,70 @@ const wss = new WebSocketServer({
|
|
|
379
379
|
app.locals.wss = wss;
|
|
380
380
|
|
|
381
381
|
app.use(cors());
|
|
382
|
+
|
|
383
|
+
// Request/Response logging middleware for debugging
|
|
384
|
+
app.use((req, res, next) => {
|
|
385
|
+
const startTime = Date.now();
|
|
386
|
+
const originalSend = res.send;
|
|
387
|
+
const originalJson = res.json;
|
|
388
|
+
|
|
389
|
+
// Capture response for logging
|
|
390
|
+
res.send = function (body) {
|
|
391
|
+
const duration = Date.now() - startTime;
|
|
392
|
+
const statusCode = res.statusCode;
|
|
393
|
+
|
|
394
|
+
// Log 4xx errors with headers
|
|
395
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
396
|
+
console.log(
|
|
397
|
+
`[HTTP ${statusCode}] ${req.method} ${req.originalUrl} (${duration}ms)`,
|
|
398
|
+
);
|
|
399
|
+
console.log(` Request Headers:`, JSON.stringify(req.headers, null, 2));
|
|
400
|
+
console.log(
|
|
401
|
+
` Response:`,
|
|
402
|
+
typeof body === "string" ? body.slice(0, 500) : body,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
// Log 5xx errors with full details
|
|
406
|
+
else if (statusCode >= 500) {
|
|
407
|
+
console.error(
|
|
408
|
+
`[HTTP ${statusCode}] ${req.method} ${req.originalUrl} (${duration}ms)`,
|
|
409
|
+
);
|
|
410
|
+
console.error(` Request Headers:`, JSON.stringify(req.headers, null, 2));
|
|
411
|
+
console.error(` Request Body:`, req.body);
|
|
412
|
+
console.error(` Response:`, body);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return originalSend.call(this, body);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
res.json = function (body) {
|
|
419
|
+
const duration = Date.now() - startTime;
|
|
420
|
+
const statusCode = res.statusCode;
|
|
421
|
+
|
|
422
|
+
// Log 4xx errors with headers
|
|
423
|
+
if (statusCode >= 400 && statusCode < 500) {
|
|
424
|
+
console.log(
|
|
425
|
+
`[HTTP ${statusCode}] ${req.method} ${req.originalUrl} (${duration}ms)`,
|
|
426
|
+
);
|
|
427
|
+
console.log(` Request Headers:`, JSON.stringify(req.headers, null, 2));
|
|
428
|
+
console.log(` Response:`, JSON.stringify(body, null, 2).slice(0, 500));
|
|
429
|
+
}
|
|
430
|
+
// Log 5xx errors with full details
|
|
431
|
+
else if (statusCode >= 500) {
|
|
432
|
+
console.error(
|
|
433
|
+
`[HTTP ${statusCode}] ${req.method} ${req.originalUrl} (${duration}ms)`,
|
|
434
|
+
);
|
|
435
|
+
console.error(` Request Headers:`, JSON.stringify(req.headers, null, 2));
|
|
436
|
+
console.error(` Request Body:`, req.body);
|
|
437
|
+
console.error(` Response:`, JSON.stringify(body, null, 2));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return originalJson.call(this, body);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
next();
|
|
444
|
+
});
|
|
445
|
+
|
|
382
446
|
app.use(
|
|
383
447
|
express.json({
|
|
384
448
|
limit: "50mb",
|
|
@@ -402,6 +466,56 @@ app.get("/health", (req, res) => {
|
|
|
402
466
|
});
|
|
403
467
|
});
|
|
404
468
|
|
|
469
|
+
// Explicit route for manifest.json (PWA manifest)
|
|
470
|
+
app.get("/manifest.json", (req, res) => {
|
|
471
|
+
const manifestPath = path.join(__dirname, "../public/manifest.json");
|
|
472
|
+
const distManifestPath = path.join(__dirname, "../dist/manifest.json");
|
|
473
|
+
|
|
474
|
+
console.log("[manifest.json] Request received");
|
|
475
|
+
console.log("[manifest.json] __dirname:", __dirname);
|
|
476
|
+
console.log("[manifest.json] Checking paths:");
|
|
477
|
+
console.log(
|
|
478
|
+
" - public:",
|
|
479
|
+
manifestPath,
|
|
480
|
+
"exists:",
|
|
481
|
+
fs.existsSync(manifestPath),
|
|
482
|
+
);
|
|
483
|
+
console.log(
|
|
484
|
+
" - dist:",
|
|
485
|
+
distManifestPath,
|
|
486
|
+
"exists:",
|
|
487
|
+
fs.existsSync(distManifestPath),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// Try public first, then dist
|
|
491
|
+
if (fs.existsSync(manifestPath)) {
|
|
492
|
+
console.log("[manifest.json] Serving from public");
|
|
493
|
+
res.setHeader("Content-Type", "application/manifest+json");
|
|
494
|
+
res.setHeader("Cache-Control", "public, max-age=3600, must-revalidate");
|
|
495
|
+
res.sendFile(manifestPath, (err) => {
|
|
496
|
+
if (err) {
|
|
497
|
+
console.error("[manifest.json] sendFile error:", err);
|
|
498
|
+
res.status(500).json({ error: "Failed to send manifest.json" });
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
} else if (fs.existsSync(distManifestPath)) {
|
|
502
|
+
console.log("[manifest.json] Serving from dist");
|
|
503
|
+
res.setHeader("Content-Type", "application/manifest+json");
|
|
504
|
+
res.setHeader("Cache-Control", "public, max-age=3600, must-revalidate");
|
|
505
|
+
res.sendFile(distManifestPath, (err) => {
|
|
506
|
+
if (err) {
|
|
507
|
+
console.error("[manifest.json] sendFile error:", err);
|
|
508
|
+
res.status(500).json({ error: "Failed to send manifest.json" });
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
} else {
|
|
512
|
+
console.error("[ERROR] manifest.json not found in public or dist");
|
|
513
|
+
console.error(" Checked:", manifestPath);
|
|
514
|
+
console.error(" Checked:", distManifestPath);
|
|
515
|
+
res.status(404).json({ error: "manifest.json not found" });
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
405
519
|
// Optional API key validation (if configured)
|
|
406
520
|
app.use("/api", validateApiKey);
|
|
407
521
|
|
|
@@ -447,7 +561,7 @@ app.use("/api/sessions", authenticateToken, sessionsRoutes);
|
|
|
447
561
|
// Agent API Routes (uses API key authentication)
|
|
448
562
|
app.use("/api/agent", agentRoutes);
|
|
449
563
|
|
|
450
|
-
// Serve public files (like api-docs.html, icons)
|
|
564
|
+
// Serve public files (like api-docs.html, icons, manifest.json)
|
|
451
565
|
// Enable ETag generation for conditional requests (304 support)
|
|
452
566
|
app.use(
|
|
453
567
|
express.static(path.join(__dirname, "../public"), {
|
|
@@ -461,6 +575,9 @@ app.use(
|
|
|
461
575
|
"Cache-Control",
|
|
462
576
|
"public, max-age=604800, must-revalidate",
|
|
463
577
|
);
|
|
578
|
+
} else if (filePath.endsWith(".json")) {
|
|
579
|
+
// JSON files (like manifest.json) - short cache with revalidation
|
|
580
|
+
res.setHeader("Cache-Control", "public, max-age=3600, must-revalidate");
|
|
464
581
|
} else if (filePath.endsWith(".html")) {
|
|
465
582
|
// HTML files should not be cached
|
|
466
583
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
@@ -483,6 +600,9 @@ app.use(
|
|
|
483
600
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
484
601
|
res.setHeader("Pragma", "no-cache");
|
|
485
602
|
res.setHeader("Expires", "0");
|
|
603
|
+
} else if (filePath.endsWith(".json")) {
|
|
604
|
+
// JSON files (like manifest.json) - short cache with revalidation
|
|
605
|
+
res.setHeader("Cache-Control", "public, max-age=3600, must-revalidate");
|
|
486
606
|
} else if (
|
|
487
607
|
filePath.match(/\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$/)
|
|
488
608
|
) {
|
|
@@ -589,7 +709,7 @@ app.get(
|
|
|
589
709
|
},
|
|
590
710
|
);
|
|
591
711
|
|
|
592
|
-
// Get messages for a specific session
|
|
712
|
+
// Get messages for a specific session with ETag caching support
|
|
593
713
|
app.get(
|
|
594
714
|
"/api/projects/:projectName/sessions/:sessionId/messages",
|
|
595
715
|
authenticateToken,
|
|
@@ -609,6 +729,27 @@ app.get(
|
|
|
609
729
|
parsedOffset,
|
|
610
730
|
);
|
|
611
731
|
|
|
732
|
+
// Generate ETag based on message count and last timestamp
|
|
733
|
+
const messages = Array.isArray(result) ? result : result.messages || [];
|
|
734
|
+
const total = Array.isArray(result) ? messages.length : result.total || 0;
|
|
735
|
+
const lastTimestamp =
|
|
736
|
+
messages.length > 0
|
|
737
|
+
? messages[messages.length - 1]?.timestamp || ""
|
|
738
|
+
: "";
|
|
739
|
+
const currentETag = `"${sessionId}-${total}-${Buffer.from(lastTimestamp).toString("base64").slice(0, 16)}"`;
|
|
740
|
+
|
|
741
|
+
// Check If-None-Match header for conditional request
|
|
742
|
+
const clientETag = req.headers["if-none-match"];
|
|
743
|
+
if (clientETag && clientETag === currentETag) {
|
|
744
|
+
return res.status(304).end();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Set caching headers
|
|
748
|
+
res.set({
|
|
749
|
+
"Cache-Control": "private, max-age=5",
|
|
750
|
+
ETag: currentETag,
|
|
751
|
+
});
|
|
752
|
+
|
|
612
753
|
// Handle both old and new response formats
|
|
613
754
|
if (Array.isArray(result)) {
|
|
614
755
|
// Backward compatibility: no pagination parameters were provided
|
|
@@ -1126,6 +1267,48 @@ async function handleChatMessage(ws, writer, messageData) {
|
|
|
1126
1267
|
const sessionIdForTracking =
|
|
1127
1268
|
data.options?.sessionId || data.sessionId || `session-${Date.now()}`;
|
|
1128
1269
|
|
|
1270
|
+
// Handle proactive external session check (before user submits a prompt)
|
|
1271
|
+
if (data.type === "check-external-session") {
|
|
1272
|
+
const projectPath = data.projectPath;
|
|
1273
|
+
console.log(
|
|
1274
|
+
"[ExternalSessionCheck] Checking for external sessions:",
|
|
1275
|
+
projectPath,
|
|
1276
|
+
);
|
|
1277
|
+
if (projectPath) {
|
|
1278
|
+
const externalCheck = detectExternalClaude(projectPath);
|
|
1279
|
+
console.log("[ExternalSessionCheck] Result:", {
|
|
1280
|
+
hasExternalSession: externalCheck.hasExternalSession,
|
|
1281
|
+
detectionAvailable: externalCheck.detectionAvailable,
|
|
1282
|
+
detectionError: externalCheck.detectionError,
|
|
1283
|
+
processCount: externalCheck.processes.length,
|
|
1284
|
+
tmuxCount: externalCheck.tmuxSessions.length,
|
|
1285
|
+
hasLockFile: externalCheck.lockFile.exists,
|
|
1286
|
+
});
|
|
1287
|
+
writer.send({
|
|
1288
|
+
type: "external-session-check-result",
|
|
1289
|
+
projectPath,
|
|
1290
|
+
hasExternalSession: externalCheck.hasExternalSession,
|
|
1291
|
+
detectionAvailable: externalCheck.detectionAvailable,
|
|
1292
|
+
detectionError: externalCheck.detectionError,
|
|
1293
|
+
details: externalCheck.hasExternalSession
|
|
1294
|
+
? {
|
|
1295
|
+
processIds: externalCheck.processes.map((p) => p.pid),
|
|
1296
|
+
commands: externalCheck.processes.map((p) => p.command),
|
|
1297
|
+
tmuxSessions: externalCheck.tmuxSessions.map(
|
|
1298
|
+
(s) => s.sessionName,
|
|
1299
|
+
),
|
|
1300
|
+
lockFile: externalCheck.lockFile.exists
|
|
1301
|
+
? externalCheck.lockFile.lockFile
|
|
1302
|
+
: null,
|
|
1303
|
+
}
|
|
1304
|
+
: null,
|
|
1305
|
+
});
|
|
1306
|
+
} else {
|
|
1307
|
+
console.log("[ExternalSessionCheck] No projectPath provided");
|
|
1308
|
+
}
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1129
1312
|
if (data.type === "claude-command") {
|
|
1130
1313
|
console.log("[DEBUG] User message:", data.command || "[Continue/Resume]");
|
|
1131
1314
|
console.log("📁 Project:", data.options?.projectPath || "Unknown");
|
|
@@ -1161,6 +1344,7 @@ async function handleChatMessage(ws, writer, messageData) {
|
|
|
1161
1344
|
writer.send({
|
|
1162
1345
|
type: "external-session-detected",
|
|
1163
1346
|
projectPath,
|
|
1347
|
+
detectionAvailable: externalCheck.detectionAvailable,
|
|
1164
1348
|
details: {
|
|
1165
1349
|
processIds: externalCheck.processes.map((p) => p.pid),
|
|
1166
1350
|
tmuxSessions: externalCheck.tmuxSessions.map(
|
|
@@ -2389,6 +2573,9 @@ app.get(
|
|
|
2389
2573
|
try {
|
|
2390
2574
|
const { projectName, sessionId } = req.params;
|
|
2391
2575
|
const { provider = "claude" } = req.query;
|
|
2576
|
+
console.log(
|
|
2577
|
+
`[TOKEN-USAGE] Request for project: ${projectName}, session: ${sessionId}, provider: ${provider}`,
|
|
2578
|
+
);
|
|
2392
2579
|
const homeDir = os.homedir();
|
|
2393
2580
|
|
|
2394
2581
|
// Allow only safe characters in sessionId
|
|
@@ -2507,8 +2694,8 @@ app.get(
|
|
|
2507
2694
|
|
|
2508
2695
|
// Construct the JSONL file path
|
|
2509
2696
|
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
|
|
2510
|
-
// The encoding replaces /, spaces, ~, and
|
|
2511
|
-
const encodedPath = projectPath.replace(/[\\/:\s~_]/g, "-");
|
|
2697
|
+
// The encoding replaces /, spaces, ~, _, and . with -
|
|
2698
|
+
const encodedPath = projectPath.replace(/[\\/:\s~_.]/g, "-");
|
|
2512
2699
|
const projectDir = path.join(homeDir, ".claude", "projects", encodedPath);
|
|
2513
2700
|
|
|
2514
2701
|
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|
|
@@ -2528,9 +2715,22 @@ app.get(
|
|
|
2528
2715
|
fileContent = await fsPromises.readFile(jsonlPath, "utf8");
|
|
2529
2716
|
} catch (error) {
|
|
2530
2717
|
if (error.code === "ENOENT") {
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2718
|
+
// Session file doesn't exist yet (new session with no messages)
|
|
2719
|
+
// Return zero token usage instead of 404
|
|
2720
|
+
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
|
2721
|
+
const contextWindow = Number.isFinite(parsedContextWindow)
|
|
2722
|
+
? parsedContextWindow
|
|
2723
|
+
: 160000;
|
|
2724
|
+
return res.json({
|
|
2725
|
+
used: 0,
|
|
2726
|
+
total: contextWindow,
|
|
2727
|
+
breakdown: {
|
|
2728
|
+
input: 0,
|
|
2729
|
+
cacheCreation: 0,
|
|
2730
|
+
cacheRead: 0,
|
|
2731
|
+
},
|
|
2732
|
+
newSession: true,
|
|
2733
|
+
});
|
|
2534
2734
|
}
|
|
2535
2735
|
throw error; // Re-throw other errors to be caught by outer try-catch
|
|
2536
2736
|
}
|
|
@@ -2589,6 +2789,9 @@ app.get(
|
|
|
2589
2789
|
app.get("*", (req, res) => {
|
|
2590
2790
|
// Skip requests for static assets (files with extensions)
|
|
2591
2791
|
if (path.extname(req.path)) {
|
|
2792
|
+
console.log(
|
|
2793
|
+
`[404] Static file not found: ${req.path} (not served by express.static)`,
|
|
2794
|
+
);
|
|
2592
2795
|
return res.status(404).send("Not found");
|
|
2593
2796
|
}
|
|
2594
2797
|
|