@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.
Files changed (55) hide show
  1. package/dist/assets/index-BGneYLVE.css +32 -0
  2. package/dist/assets/{index-D0xTNXrF.js → index-sqmQ9jF8.js} +210 -212
  3. package/dist/index.html +2 -2
  4. package/dist/sw.js +25 -1
  5. package/package.json +2 -1
  6. package/public/api-docs.html +879 -0
  7. package/public/clear-cache.html +85 -0
  8. package/public/convert-icons.md +53 -0
  9. package/public/favicon.png +0 -0
  10. package/public/favicon.svg +9 -0
  11. package/public/generate-icons.js +49 -0
  12. package/public/icons/claude-ai-icon.svg +1 -0
  13. package/public/icons/codex-white.svg +3 -0
  14. package/public/icons/codex.svg +3 -0
  15. package/public/icons/cursor-white.svg +12 -0
  16. package/public/icons/cursor.svg +1 -0
  17. package/public/icons/generate-icons.md +19 -0
  18. package/public/icons/icon-128x128.png +0 -0
  19. package/public/icons/icon-128x128.svg +12 -0
  20. package/public/icons/icon-144x144.png +0 -0
  21. package/public/icons/icon-144x144.svg +12 -0
  22. package/public/icons/icon-152x152.png +0 -0
  23. package/public/icons/icon-152x152.svg +12 -0
  24. package/public/icons/icon-192x192.png +0 -0
  25. package/public/icons/icon-192x192.svg +12 -0
  26. package/public/icons/icon-384x384.png +0 -0
  27. package/public/icons/icon-384x384.svg +12 -0
  28. package/public/icons/icon-512x512.png +0 -0
  29. package/public/icons/icon-512x512.svg +12 -0
  30. package/public/icons/icon-72x72.png +0 -0
  31. package/public/icons/icon-72x72.svg +12 -0
  32. package/public/icons/icon-96x96.png +0 -0
  33. package/public/icons/icon-96x96.svg +12 -0
  34. package/public/icons/icon-template.svg +12 -0
  35. package/public/logo-128.png +0 -0
  36. package/public/logo-256.png +0 -0
  37. package/public/logo-32.png +0 -0
  38. package/public/logo-512.png +0 -0
  39. package/public/logo-64.png +0 -0
  40. package/public/logo.svg +17 -0
  41. package/public/manifest.json +61 -0
  42. package/public/screenshots/cli-selection.png +0 -0
  43. package/public/screenshots/desktop-main.png +0 -0
  44. package/public/screenshots/mobile-chat.png +0 -0
  45. package/public/screenshots/tools-modal.png +0 -0
  46. package/public/sw.js +131 -0
  47. package/server/database/db.js +98 -0
  48. package/server/database/init.sql +13 -1
  49. package/server/external-session-detector.js +188 -48
  50. package/server/index.js +210 -7
  51. package/server/orchestrator/client.js +361 -16
  52. package/server/orchestrator/index.js +83 -8
  53. package/server/orchestrator/protocol.js +67 -0
  54. package/server/projects.js +2 -1
  55. 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 _ with -
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
- return res
2532
- .status(404)
2533
- .json({ error: "Session file not found", path: jsonlPath });
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