upfynai-code 2.4.1 → 2.5.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.
Files changed (147) hide show
  1. package/client/dist/assets/AppContent-CRld2UWX.js +513 -0
  2. package/client/dist/assets/CanvasPanel-CB4sweQq.js +34 -0
  3. package/client/dist/assets/CanvasPanel-WhZulBJw.css +1 -0
  4. package/client/dist/assets/DashboardPanel-BXaA-b9z.js +1 -0
  5. package/client/dist/assets/LoginModal-BwkvjfPR.js +19 -0
  6. package/client/dist/assets/{Onboarding-CtIoXiTp.js → Onboarding-2A_5fPxy.js} +1 -1
  7. package/client/dist/assets/{SetupForm-B4p8im5O.js → SetupForm-CH5EA5W0.js} +1 -1
  8. package/client/dist/assets/WorkflowsPanel-CO5g5yGG.js +1 -0
  9. package/client/dist/assets/{ar-SA-G6X2FPQ2-2gfmdvHk.js → ar-SA-G6X2FPQ2-DoJuo98H.js} +2 -2
  10. package/client/dist/assets/{arc-DCZSHhoJ.js → arc-B0wBaTeh.js} +1 -1
  11. package/client/dist/assets/az-AZ-76LH7QW2-xdrt1Z13.js +1 -0
  12. package/client/dist/assets/{bg-BG-XCXSNQG7-D6__XtOK.js → bg-BG-XCXSNQG7-D8NAiF6Y.js} +2 -2
  13. package/client/dist/assets/{blockDiagram-38ab4fdb-Cfbaeyp6.js → blockDiagram-38ab4fdb-DSnyKzK4.js} +2 -2
  14. package/client/dist/assets/{bn-BD-2XOGV67Q-DHNJw3OG.js → bn-BD-2XOGV67Q-B0qWv8_J.js} +2 -2
  15. package/client/dist/assets/{c4Diagram-3d4e48cf-BBCnjOTy.js → c4Diagram-3d4e48cf-DoZJ13XA.js} +2 -2
  16. package/client/dist/assets/{ca-ES-6MX7JW3Y-r5g4o3zQ.js → ca-ES-6MX7JW3Y-RgLhfbZZ.js} +3 -3
  17. package/client/dist/assets/channel-BmO6nY0W.js +1 -0
  18. package/client/dist/assets/classDiagram-70f12bd4-GNyDrRCk.js +2 -0
  19. package/client/dist/assets/classDiagram-v2-f2320105-CxdGhHm2.js +2 -0
  20. package/client/dist/assets/clone-xuHMqFoD.js +1 -0
  21. package/client/dist/assets/{createText-2e5e7dd3-B8jCDmF_.js → createText-2e5e7dd3-DiPywQOa.js} +1 -1
  22. package/client/dist/assets/{cs-CZ-2BRQDIVT-p08jRLRC.js → cs-CZ-2BRQDIVT-BAjmnuoC.js} +2 -2
  23. package/client/dist/assets/{da-DK-5WZEPLOC-CnhOImFf.js → da-DK-5WZEPLOC-JxKVGt8o.js} +2 -2
  24. package/client/dist/assets/{de-DE-XR44H4JA-BunSXZ-Y.js → de-DE-XR44H4JA-CrnRlt4z.js} +2 -2
  25. package/client/dist/assets/{edges-e0da2a9e-CGBBhG8k.js → edges-e0da2a9e-DDsXzXLJ.js} +1 -1
  26. package/client/dist/assets/{el-GR-BZB4AONW-D4wv1oIz.js → el-GR-BZB4AONW-DQd8iogq.js} +2 -2
  27. package/client/dist/assets/{erDiagram-9861fffd-CYaF3q1I.js → erDiagram-9861fffd-CBiCC4rl.js} +2 -2
  28. package/client/dist/assets/{es-ES-U4NZUMDT-CGeTKXgd.js → es-ES-U4NZUMDT-vvUblc5i.js} +2 -2
  29. package/client/dist/assets/{eu-ES-A7QVB2H4-Cayx1TxR.js → eu-ES-A7QVB2H4-De4NNCc1.js} +2 -2
  30. package/client/dist/assets/{fa-IR-HGAKTJCU-CmUg8pmw.js → fa-IR-HGAKTJCU-DFBXqIqq.js} +2 -2
  31. package/client/dist/assets/{fi-FI-Z5N7JZ37-xvHcPhsU.js → fi-FI-Z5N7JZ37-DV9zESPg.js} +2 -2
  32. package/client/dist/assets/{flowDb-956e92f1-C-_LFz70.js → flowDb-956e92f1-BhdSHbdO.js} +1 -1
  33. package/client/dist/assets/{flowDiagram-66a62f08-C1sHdSjn.js → flowDiagram-66a62f08-M-fp1_Ie.js} +2 -2
  34. package/client/dist/assets/flowDiagram-v2-96b9c2cf-C5eiN8Pg.js +1 -0
  35. package/client/dist/assets/{flowchart-elk-definition-4a651766-CNGfpudb.js → flowchart-elk-definition-4a651766-Bp0SonQx.js} +2 -2
  36. package/client/dist/assets/{fr-FR-RHASNOE6-DBoHEcNj.js → fr-FR-RHASNOE6-CKTMXuGk.js} +2 -2
  37. package/client/dist/assets/ganttDiagram-c361ad54-iA737GUS.js +257 -0
  38. package/client/dist/assets/{gitGraphDiagram-72cf32ee-DojCDvlS.js → gitGraphDiagram-72cf32ee-BX-wj-PV.js} +2 -2
  39. package/client/dist/assets/{gl-ES-HMX3MZ6V-p6hrn2cN.js → gl-ES-HMX3MZ6V-Cdiqq4jY.js} +2 -2
  40. package/client/dist/assets/{graph-DXM7lcy1.js → graph-Rxkx3sEa.js} +1 -1
  41. package/client/dist/assets/{he-IL-6SHJWFNN-y2jEX6-0.js → he-IL-6SHJWFNN-gYmR5_KT.js} +2 -2
  42. package/client/dist/assets/{hi-IN-IWLTKZ5I-99pNfyWr.js → hi-IN-IWLTKZ5I-pyqK94AR.js} +2 -2
  43. package/client/dist/assets/{hu-HU-A5ZG7DT2-hygceGMS.js → hu-HU-A5ZG7DT2-DpacJgJy.js} +2 -2
  44. package/client/dist/assets/{id-ID-SAP4L64H-CyIqi1hv.js → id-ID-SAP4L64H-CAvIX-mj.js} +2 -2
  45. package/client/dist/assets/{index-3862675e-4idOQN2N.js → index-3862675e-BX3Fpn6V.js} +1 -1
  46. package/client/dist/assets/{index-BHZfFT_V.js → index-BBlwbHq_.js} +4 -4
  47. package/client/dist/assets/{index-BGmwbRlb.js → index-ClfzLIqY.js} +6 -6
  48. package/client/dist/assets/index-Td4UdtLF.css +1 -0
  49. package/client/dist/assets/{infoDiagram-f8f76790-CFLrHqtc.js → infoDiagram-f8f76790-Ckv8imiv.js} +2 -2
  50. package/client/dist/assets/{it-IT-JPQ66NNP-DzVvVdQI.js → it-IT-JPQ66NNP-BtpNRSce.js} +2 -2
  51. package/client/dist/assets/{ja-JP-DBVTYXUO-BI4fPexV.js → ja-JP-DBVTYXUO-CwJRyY6M.js} +2 -2
  52. package/client/dist/assets/{journeyDiagram-49397b02-C3CFDo8z.js → journeyDiagram-49397b02-DWWZssji.js} +2 -2
  53. package/client/dist/assets/kaa-6HZHGXH3-DIWQEb4A.js +1 -0
  54. package/client/dist/assets/{kab-KAB-ZGHBKWFO-DBI_ri48.js → kab-KAB-ZGHBKWFO-DjGbqhUg.js} +2 -2
  55. package/client/dist/assets/kk-KZ-P5N5QNE5-B_VzJdWf.js +1 -0
  56. package/client/dist/assets/{km-KH-HSX4SM5Z-DOMFSres.js → km-KH-HSX4SM5Z-DUD5mi0o.js} +2 -2
  57. package/client/dist/assets/{ko-KR-MTYHY66A-tb08hXzd.js → ko-KR-MTYHY66A--sDB10db.js} +3 -3
  58. package/client/dist/assets/{ku-TR-6OUDTVRD-DlIQCCY4.js → ku-TR-6OUDTVRD-CKvKrkcX.js} +2 -2
  59. package/client/dist/assets/{layout-B_11mCXA.js → layout-CkB7sSeq.js} +1 -1
  60. package/client/dist/assets/{line-B-qmK_vI.js → line-DC7MA9qY.js} +1 -1
  61. package/client/dist/assets/{linear-Ph6uuYcX.js → linear-C1lBBthf.js} +1 -1
  62. package/client/dist/assets/{lt-LT-XHIRWOB4--qWy24_Z.js → lt-LT-XHIRWOB4-MSZf7xYG.js} +2 -2
  63. package/client/dist/assets/{lv-LV-5QDEKY6T-Bnd_1GDb.js → lv-LV-5QDEKY6T-C-gvvmBB.js} +2 -2
  64. package/client/dist/assets/{mindmap-definition-fc14e90a-Do79tIc0.js → mindmap-definition-fc14e90a-B3O7hztq.js} +2 -2
  65. package/client/dist/assets/{mr-IN-CRQNXWMA-BsV6HaD9.js → mr-IN-CRQNXWMA-XHtBUWQH.js} +2 -2
  66. package/client/dist/assets/my-MM-5M5IBNSE-D9eD2edL.js +1 -0
  67. package/client/dist/assets/{nb-NO-T6EIAALU-Cvf9FdSF.js → nb-NO-T6EIAALU-BlImC6gp.js} +3 -3
  68. package/client/dist/assets/{nl-NL-IS3SIHDZ-DA1yqpXw.js → nl-NL-IS3SIHDZ-CPFhnaSP.js} +2 -2
  69. package/client/dist/assets/{nn-NO-6E72VCQL-89lm3vku.js → nn-NO-6E72VCQL-BMvoJSKQ.js} +2 -2
  70. package/client/dist/assets/{oc-FR-POXYY2M6-BsrjTJQh.js → oc-FR-POXYY2M6-Buye63LS.js} +2 -2
  71. package/client/dist/assets/{pa-IN-N4M65BXN-CczefYaj.js → pa-IN-N4M65BXN-D9uQ3niy.js} +2 -2
  72. package/client/dist/assets/{percentages-BXMCSKIN-Be6p9phi.js → percentages-BXMCSKIN-BzXIakGM.js} +7 -7
  73. package/client/dist/assets/{pieDiagram-8a3498a8-CfblQHdm.js → pieDiagram-8a3498a8-BU38mzx-.js} +3 -3
  74. package/client/dist/assets/{pl-PL-T2D74RX3-DdhH-zcK.js → pl-PL-T2D74RX3-BqM4xdcg.js} +2 -2
  75. package/client/dist/assets/{pt-BR-5N22H2LF-gpwlheL6.js → pt-BR-5N22H2LF-rAjrxGyI.js} +2 -2
  76. package/client/dist/assets/{pt-PT-UZXXM6DQ-Cs87vICi.js → pt-PT-UZXXM6DQ-DXsqcwLt.js} +2 -2
  77. package/client/dist/assets/{quadrantDiagram-120e2f19-CRMSamSP.js → quadrantDiagram-120e2f19-HhK4H1WU.js} +2 -2
  78. package/client/dist/assets/{requirementDiagram-deff3bca-D3LBN016.js → requirementDiagram-deff3bca-aDrcyj-A.js} +2 -2
  79. package/client/dist/assets/{ro-RO-JPDTUUEW-CWTSJ1Dt.js → ro-RO-JPDTUUEW-D_F9UKer.js} +2 -2
  80. package/client/dist/assets/{ru-RU-B4JR7IUQ-Bq7aN2ep.js → ru-RU-B4JR7IUQ-MirqN29p.js} +2 -2
  81. package/client/dist/assets/sankeyDiagram-04a897e0-C6ij7qbQ.js +8 -0
  82. package/client/dist/assets/{sequenceDiagram-704730f1-BRYXVDGX.js → sequenceDiagram-704730f1-C0EKO3th.js} +2 -2
  83. package/client/dist/assets/si-LK-N5RQ5JYF-DyZC3mkC.js +1 -0
  84. package/client/dist/assets/{sk-SK-C5VTKIMK-ByjKQzUb.js → sk-SK-C5VTKIMK-D-ksz-WY.js} +2 -2
  85. package/client/dist/assets/{sl-SI-NN7IZMDC-B8WCyMBU.js → sl-SI-NN7IZMDC-CknuYoQ1.js} +2 -2
  86. package/client/dist/assets/stateDiagram-587899a1-CYoq2VjL.js +1 -0
  87. package/client/dist/assets/stateDiagram-v2-d93cdb3a-C5lbp5px.js +1 -0
  88. package/client/dist/assets/{styles-6aaf32cf-Dr-lfIOW.js → styles-6aaf32cf-Dkfsk8gt.js} +1 -1
  89. package/client/dist/assets/{styles-9a916d00-DS4wRpL7.js → styles-9a916d00-CMYqtcEN.js} +1 -1
  90. package/client/dist/assets/{styles-c10674c1-nKRF6NrH.js → styles-c10674c1-Bp-5OlRU.js} +1 -1
  91. package/client/dist/assets/{subset-shared.chunk-KT79s7KG.js → subset-shared.chunk-kfIB1Zam.js} +3 -3
  92. package/client/dist/assets/subset-worker.chunk-DwQBgc4z.js +1 -0
  93. package/client/dist/assets/{sv-SE-XGPEYMSR-BiIPUVbv.js → sv-SE-XGPEYMSR-DwN13se1.js} +2 -2
  94. package/client/dist/assets/{svgDrawCommon-08f97a94-C3uP9PYr.js → svgDrawCommon-08f97a94-CEgCMqs4.js} +1 -1
  95. package/client/dist/assets/{ta-IN-2NMHFXQM-Cidadso2.js → ta-IN-2NMHFXQM-ejDfFhwa.js} +2 -2
  96. package/client/dist/assets/th-TH-HPSO5L25-Bqc90ZNn.js +2 -0
  97. package/client/dist/assets/{timeline-definition-85554ec2-BSsLsIgF.js → timeline-definition-85554ec2-BmGdKqG0.js} +2 -2
  98. package/client/dist/assets/{tr-TR-DEFEU3FU-DaFcI-KL.js → tr-TR-DEFEU3FU-CJvlPbcW.js} +2 -2
  99. package/client/dist/assets/{uk-UA-QMV73CPH-DkBW36St.js → uk-UA-QMV73CPH-D26-cbWL.js} +3 -3
  100. package/client/dist/assets/vendor-codemirror-D_s0aGBu.js +35 -0
  101. package/client/dist/assets/{vendor-icons-Dh9m_Ydt.js → vendor-icons-aNdOvTr_.js} +159 -119
  102. package/client/dist/assets/{vi-VN-M7AON7JQ-KrtfxOzl.js → vi-VN-M7AON7JQ-MbqIIwYM.js} +2 -2
  103. package/client/dist/assets/{xychartDiagram-e933f94c-CgNgZ4pp.js → xychartDiagram-e933f94c-gfcTauxU.js} +2 -2
  104. package/client/dist/assets/{zh-CN-LNUGB5OW-BQu12RoD.js → zh-CN-LNUGB5OW-BZSmhUdL.js} +3 -3
  105. package/client/dist/assets/zh-HK-E62DVLB3-BJqejpiX.js +1 -0
  106. package/client/dist/assets/{zh-TW-RAJ6MFWO-ffJWgVxn.js → zh-TW-RAJ6MFWO-BBXtV-Uz.js} +2 -2
  107. package/client/dist/index.html +3 -3
  108. package/package.json +5 -2
  109. package/server/cli.js +1 -1
  110. package/server/database/auth.db +0 -0
  111. package/server/database/db.js +203 -1
  112. package/server/index.js +111 -18
  113. package/server/middleware/auth.js +11 -6
  114. package/server/projects.js +95 -202
  115. package/server/relay-client.js +47 -10
  116. package/server/routes/auth.js +6 -0
  117. package/server/routes/dashboard.js +52 -0
  118. package/server/routes/projects.js +38 -35
  119. package/server/routes/voice.js +198 -0
  120. package/server/routes/webhooks.js +166 -0
  121. package/server/routes/workflows.js +118 -0
  122. package/server/services/whisperService.js +84 -0
  123. package/server/services/workflowScheduler.js +186 -0
  124. package/client/dist/assets/AppContent-DTZ2FbvM.js +0 -513
  125. package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +0 -6
  126. package/client/dist/assets/CanvasPanel-q4HEqNtV.css +0 -1
  127. package/client/dist/assets/LoginModal-CWoFm0au.js +0 -19
  128. package/client/dist/assets/az-AZ-76LH7QW2-CDdeucRZ.js +0 -1
  129. package/client/dist/assets/channel-O3ovC0x9.js +0 -1
  130. package/client/dist/assets/classDiagram-70f12bd4-D0lhAcxU.js +0 -2
  131. package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +0 -2
  132. package/client/dist/assets/clone-BG9u7vLi.js +0 -1
  133. package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +0 -1
  134. package/client/dist/assets/ganttDiagram-c361ad54-B8HJQqjt.js +0 -257
  135. package/client/dist/assets/index-B8wwD_Xo.css +0 -1
  136. package/client/dist/assets/kaa-6HZHGXH3-fwOleoQB.js +0 -1
  137. package/client/dist/assets/kk-KZ-P5N5QNE5-zpl7uvyF.js +0 -1
  138. package/client/dist/assets/my-MM-5M5IBNSE-kZQURVIi.js +0 -1
  139. package/client/dist/assets/sankeyDiagram-04a897e0-CsFqOQZN.js +0 -8
  140. package/client/dist/assets/si-LK-N5RQ5JYF-BBjcNYQh.js +0 -1
  141. package/client/dist/assets/stateDiagram-587899a1-BHoy9LtD.js +0 -1
  142. package/client/dist/assets/stateDiagram-v2-d93cdb3a-BvMUA6bS.js +0 -1
  143. package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +0 -1
  144. package/client/dist/assets/th-TH-HPSO5L25-CFNnJwSv.js +0 -2
  145. package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +0 -20
  146. package/client/dist/assets/vendor-codemirror-rix45NST.js +0 -16
  147. package/client/dist/assets/zh-HK-E62DVLB3-zx9CvERq.js +0 -1
@@ -387,8 +387,8 @@ async function extractProjectDirectory(projectName) {
387
387
  }
388
388
 
389
389
  async function getProjects(progressCallback = null) {
390
- // Wrap with a 10s timeout to prevent hanging on slow filesystems
391
- const timeoutMs = 10000;
390
+ // Wrap with a timeout to prevent hanging on slow filesystems
391
+ const timeoutMs = 15000;
392
392
  const result = await Promise.race([
393
393
  _getProjectsImpl(progressCallback),
394
394
  new Promise((_, reject) => setTimeout(() => reject(new Error('Projects scan timed out')), timeoutMs))
@@ -403,220 +403,103 @@ async function _getProjectsImpl(progressCallback = null) {
403
403
  const claudeDir = path.join(os.homedir(), '.claude', 'projects');
404
404
  const config = await loadProjectConfig();
405
405
  const projects = [];
406
- const existingProjects = new Set();
407
406
  const codexSessionsIndexRef = { sessionsByProject: null };
408
- let totalProjects = 0;
407
+
408
+ // Only load projects that were explicitly added by the user (manuallyAdded).
409
+ // No auto-scanning of ~/.claude/projects/ — the user adds projects via the UI.
410
+ const manualEntries = Object.entries(config).filter(([, cfg]) => cfg.manuallyAdded);
411
+ const totalProjects = manualEntries.length;
409
412
  let processedProjects = 0;
410
- let directories = [];
411
413
 
412
- try {
413
- // Check if the .claude/projects directory exists
414
- await fs.access(claudeDir);
415
-
416
- // First, get existing Claude projects from the file system
417
- const entries = await fs.readdir(claudeDir, { withFileTypes: true });
418
- directories = entries.filter(e => e.isDirectory());
419
-
420
- // Build set of existing project names for later
421
- directories.forEach(e => existingProjects.add(e.name));
422
-
423
- // Count manual projects not already in directories
424
- const manualProjectsCount = Object.entries(config)
425
- .filter(([name, cfg]) => cfg.manuallyAdded && !existingProjects.has(name))
426
- .length;
427
-
428
- totalProjects = directories.length + manualProjectsCount;
429
-
430
- for (const entry of directories) {
431
- processedProjects++;
432
-
433
- // Emit progress
434
- if (progressCallback) {
435
- progressCallback({
436
- phase: 'loading',
437
- current: processedProjects,
438
- total: totalProjects,
439
- currentProject: entry.name
440
- });
441
- }
414
+ for (const [projectName, projectConfig] of manualEntries) {
415
+ processedProjects++;
442
416
 
443
- // Extract actual project directory from JSONL sessions
444
- const actualProjectDir = await extractProjectDirectory(entry.name);
445
-
446
- // Get display name from config or generate one
447
- const customName = config[entry.name]?.displayName;
448
- const autoDisplayName = await generateDisplayName(entry.name, actualProjectDir);
449
- const fullPath = actualProjectDir;
450
-
451
- const project = {
452
- name: entry.name,
453
- path: actualProjectDir,
454
- displayName: customName || autoDisplayName,
455
- fullPath: fullPath,
456
- isCustomName: !!customName,
457
- sessions: [],
458
- sessionMeta: {
459
- hasMore: false,
460
- total: 0
461
- }
462
- };
463
-
464
- // Try to get sessions for this project (just first 5 for performance)
465
- try {
466
- const sessionResult = await getSessions(entry.name, 5, 0);
467
- project.sessions = sessionResult.sessions || [];
468
- project.sessionMeta = {
469
- hasMore: sessionResult.hasMore,
470
- total: sessionResult.total
471
- };
472
- } catch (e) {
473
- console.warn(`Could not load sessions for project ${entry.name}:`, e.message);
474
- project.sessionMeta = {
475
- hasMore: false,
476
- total: 0
477
- };
478
- }
479
-
480
- // Also fetch Cursor sessions for this project
481
- try {
482
- project.cursorSessions = await getCursorSessions(actualProjectDir);
483
- } catch (e) {
484
- console.warn(`Could not load Cursor sessions for project ${entry.name}:`, e.message);
485
- project.cursorSessions = [];
486
- }
487
-
488
- // Also fetch Codex sessions for this project
489
- try {
490
- project.codexSessions = await getCodexSessions(actualProjectDir, {
491
- indexRef: codexSessionsIndexRef,
492
- });
493
- } catch (e) {
494
- console.warn(`Could not load Codex sessions for project ${entry.name}:`, e.message);
495
- project.codexSessions = [];
496
- }
417
+ if (progressCallback) {
418
+ progressCallback({
419
+ phase: 'loading',
420
+ current: processedProjects,
421
+ total: totalProjects,
422
+ currentProject: projectName
423
+ });
424
+ }
497
425
 
498
- // Add TaskMaster detection
499
- try {
500
- const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
501
- project.taskmaster = {
502
- hasTaskmaster: taskMasterResult.hasTaskmaster,
503
- hasEssentialFiles: taskMasterResult.hasEssentialFiles,
504
- metadata: taskMasterResult.metadata,
505
- status: taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'configured' : 'not-configured'
506
- };
507
- } catch (e) {
508
- console.warn(`Could not detect TaskMaster for project ${entry.name}:`, e.message);
509
- project.taskmaster = {
510
- hasTaskmaster: false,
511
- hasEssentialFiles: false,
512
- metadata: null,
513
- status: 'error'
514
- };
515
- }
426
+ // Use the original path if available, otherwise extract from potential sessions
427
+ let actualProjectDir = projectConfig.originalPath;
516
428
 
517
- projects.push(project);
518
- }
519
- } catch (error) {
520
- // If the directory doesn't exist (ENOENT), that's okay - just continue with empty projects
521
- if (error.code !== 'ENOENT') {
522
- console.error('Error reading projects directory:', error);
523
- }
524
- // Calculate total for manual projects only (no directories exist)
525
- totalProjects = Object.entries(config)
526
- .filter(([name, cfg]) => cfg.manuallyAdded)
527
- .length;
528
- }
529
-
530
- // Add manually configured projects that don't exist as folders yet
531
- for (const [projectName, projectConfig] of Object.entries(config)) {
532
- if (!existingProjects.has(projectName) && projectConfig.manuallyAdded) {
533
- processedProjects++;
534
-
535
- // Emit progress for manual projects
536
- if (progressCallback) {
537
- progressCallback({
538
- phase: 'loading',
539
- current: processedProjects,
540
- total: totalProjects,
541
- currentProject: projectName
542
- });
429
+ if (!actualProjectDir) {
430
+ try {
431
+ actualProjectDir = await extractProjectDirectory(projectName);
432
+ } catch (error) {
433
+ // Fall back to decoded project name
434
+ actualProjectDir = projectName.replace(/-/g, '/');
543
435
  }
436
+ }
544
437
 
545
- // Use the original path if available, otherwise extract from potential sessions
546
- let actualProjectDir = projectConfig.originalPath;
547
-
548
- if (!actualProjectDir) {
549
- try {
550
- actualProjectDir = await extractProjectDirectory(projectName);
551
- } catch (error) {
552
- // Fall back to decoded project name
553
- actualProjectDir = projectName.replace(/-/g, '/');
554
- }
555
- }
556
-
557
- const project = {
558
- name: projectName,
559
- path: actualProjectDir,
560
- displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
561
- fullPath: actualProjectDir,
562
- isCustomName: !!projectConfig.displayName,
563
- isManuallyAdded: true,
564
- sessions: [],
565
- sessionMeta: {
566
- hasMore: false,
567
- total: 0
568
- },
569
- cursorSessions: [],
570
- codexSessions: []
438
+ const project = {
439
+ name: projectName,
440
+ path: actualProjectDir,
441
+ displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
442
+ fullPath: actualProjectDir,
443
+ isCustomName: !!projectConfig.displayName,
444
+ isManuallyAdded: true,
445
+ sessions: [],
446
+ sessionMeta: { hasMore: false, total: 0 },
447
+ cursorSessions: [],
448
+ codexSessions: []
449
+ };
450
+
451
+ // Check if a Claude project folder exists for this project (for session history)
452
+ const projectDir = path.join(claudeDir, projectName);
453
+ try {
454
+ await fs.access(projectDir);
455
+ const sessionResult = await getSessions(projectName, 5, 0);
456
+ project.sessions = sessionResult.sessions || [];
457
+ project.sessionMeta = {
458
+ hasMore: sessionResult.hasMore,
459
+ total: sessionResult.total
571
460
  };
461
+ } catch (e) {
462
+ // No Claude sessions — that's fine
463
+ }
572
464
 
573
- // Try to fetch Cursor sessions for manual projects too
574
- try {
575
- project.cursorSessions = await getCursorSessions(actualProjectDir);
576
- } catch (e) {
577
- console.warn(`Could not load Cursor sessions for manual project ${projectName}:`, e.message);
578
- }
465
+ // Fetch Cursor sessions
466
+ try {
467
+ project.cursorSessions = await getCursorSessions(actualProjectDir);
468
+ } catch (e) {
469
+ // No Cursor sessions
470
+ }
579
471
 
580
- // Try to fetch Codex sessions for manual projects too
581
- try {
582
- project.codexSessions = await getCodexSessions(actualProjectDir, {
583
- indexRef: codexSessionsIndexRef,
584
- });
585
- } catch (e) {
586
- console.warn(`Could not load Codex sessions for manual project ${projectName}:`, e.message);
587
- }
472
+ // Fetch Codex sessions
473
+ try {
474
+ project.codexSessions = await getCodexSessions(actualProjectDir, {
475
+ indexRef: codexSessionsIndexRef,
476
+ });
477
+ } catch (e) {
478
+ // No Codex sessions
479
+ }
588
480
 
589
- // Add TaskMaster detection for manual projects
590
- try {
591
- const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
592
-
593
- // Determine TaskMaster status
594
- let taskMasterStatus = 'not-configured';
595
- if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
596
- taskMasterStatus = 'taskmaster-only'; // We don't check MCP for manual projects in bulk
597
- }
598
-
599
- project.taskmaster = {
600
- status: taskMasterStatus,
601
- hasTaskmaster: taskMasterResult.hasTaskmaster,
602
- hasEssentialFiles: taskMasterResult.hasEssentialFiles,
603
- metadata: taskMasterResult.metadata
604
- };
605
- } catch (error) {
606
- console.warn(`TaskMaster detection failed for manual project ${projectName}:`, error.message);
607
- project.taskmaster = {
608
- status: 'error',
609
- hasTaskmaster: false,
610
- hasEssentialFiles: false,
611
- error: error.message
612
- };
613
- }
614
-
615
- projects.push(project);
481
+ // TaskMaster detection
482
+ try {
483
+ const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
484
+ project.taskmaster = {
485
+ status: taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles
486
+ ? 'configured' : 'not-configured',
487
+ hasTaskmaster: taskMasterResult.hasTaskmaster,
488
+ hasEssentialFiles: taskMasterResult.hasEssentialFiles,
489
+ metadata: taskMasterResult.metadata
490
+ };
491
+ } catch (error) {
492
+ project.taskmaster = {
493
+ status: 'error',
494
+ hasTaskmaster: false,
495
+ hasEssentialFiles: false,
496
+ error: error.message
497
+ };
616
498
  }
499
+
500
+ projects.push(project);
617
501
  }
618
502
 
619
- // Emit completion after all projects (including manual) are processed
620
503
  if (progressCallback) {
621
504
  progressCallback({
622
505
  phase: 'complete',
@@ -1225,7 +1108,17 @@ async function addProjectManually(projectPath, displayName = null) {
1225
1108
  const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
1226
1109
 
1227
1110
  if (config[projectName]) {
1228
- throw new Error(`Project already configured for path: ${absolutePath}`);
1111
+ // Project already exists return it instead of erroring
1112
+ return {
1113
+ name: projectName,
1114
+ path: absolutePath,
1115
+ fullPath: absolutePath,
1116
+ displayName: config[projectName].displayName || await generateDisplayName(projectName, absolutePath),
1117
+ isManuallyAdded: true,
1118
+ alreadyExists: true,
1119
+ sessions: [],
1120
+ cursorSessions: []
1121
+ };
1229
1122
  }
1230
1123
 
1231
1124
  // Allow adding projects even if the directory exists - this enables tracking
@@ -413,11 +413,15 @@ export async function connectToServer(options = {}) {
413
413
  let reconnectAttempts = 0;
414
414
  const MAX_RECONNECT = 10;
415
415
 
416
+ let lastPongTime = Date.now();
417
+
416
418
  function connect() {
417
419
  const ws = createRelayConnection(wsUrl, config);
420
+ lastPongTime = Date.now();
418
421
 
419
422
  ws.on('open', () => {
420
423
  reconnectAttempts = 0;
424
+ lastPongTime = Date.now();
421
425
  // Don't stop spinner yet — wait for relay-connected message
422
426
  });
423
427
 
@@ -468,7 +472,14 @@ export async function connectToServer(options = {}) {
468
472
  return;
469
473
  }
470
474
 
471
- if (data.type === 'pong') return;
475
+ if (data.type === 'pong' || data.type === 'server-ping') {
476
+ lastPongTime = Date.now();
477
+ // Reply to server-ping so server knows we're alive
478
+ if (data.type === 'server-ping') {
479
+ ws.send(JSON.stringify({ type: 'ping' }));
480
+ }
481
+ return;
482
+ }
472
483
 
473
484
  if (data.type === 'error') {
474
485
  spinner.fail(`Server error: ${data.error}`);
@@ -503,14 +514,24 @@ export async function connectToServer(options = {}) {
503
514
  // close handler will trigger reconnect
504
515
  });
505
516
 
506
- // Heartbeat every 30 seconds
517
+ // Heartbeat every 30 seconds with pong timeout detection
507
518
  const heartbeat = setInterval(() => {
508
- if (ws.readyState === 1) {
509
- ws.send(JSON.stringify({ type: 'ping' }));
510
- } else {
519
+ if (ws.readyState !== 1) {
520
+ clearInterval(heartbeat);
521
+ return;
522
+ }
523
+ // If no pong received in 75s, consider connection dead
524
+ if (Date.now() - lastPongTime > 75000) {
511
525
  clearInterval(heartbeat);
526
+ logRelayEvent('!', 'No heartbeat response — connection stale, reconnecting...', 'yellow');
527
+ ws.terminate();
528
+ return;
512
529
  }
530
+ ws.send(JSON.stringify({ type: 'ping' }));
513
531
  }, 30000);
532
+
533
+ ws.on('close', () => clearInterval(heartbeat));
534
+ ws.on('error', () => clearInterval(heartbeat));
514
535
  }
515
536
 
516
537
  connect();
@@ -538,13 +559,22 @@ export function connectToServerBackground(options = {}) {
538
559
 
539
560
  let reconnectAttempts = 0;
540
561
  const MAX_RECONNECT = 5;
562
+ let lastPongTime = Date.now();
541
563
 
542
564
  function connect() {
543
565
  const ws = createRelayConnection(wsUrl, config);
566
+ lastPongTime = Date.now();
544
567
 
545
568
  ws.on('message', (rawMessage) => {
546
569
  try {
547
570
  const data = JSON.parse(rawMessage);
571
+ if (data.type === 'pong' || data.type === 'server-ping') {
572
+ lastPongTime = Date.now();
573
+ if (data.type === 'server-ping') {
574
+ ws.send(JSON.stringify({ type: 'ping' }));
575
+ }
576
+ return;
577
+ }
548
578
  if (data.type === 'relay-command') {
549
579
  handleRelayCommand(data, ws);
550
580
  }
@@ -553,9 +583,11 @@ export function connectToServerBackground(options = {}) {
553
583
 
554
584
  ws.on('open', () => {
555
585
  reconnectAttempts = 0;
586
+ lastPongTime = Date.now();
556
587
  });
557
588
 
558
589
  ws.on('close', (code) => {
590
+ clearInterval(heartbeat);
559
591
  if (code === 1000) return;
560
592
  reconnectAttempts++;
561
593
  if (reconnectAttempts <= MAX_RECONNECT) {
@@ -565,16 +597,21 @@ export function connectToServerBackground(options = {}) {
565
597
  });
566
598
 
567
599
  ws.on('error', () => {
568
- // silent — close handler will reconnect
600
+ clearInterval(heartbeat);
569
601
  });
570
602
 
571
- // Heartbeat
603
+ // Heartbeat with pong timeout
572
604
  const heartbeat = setInterval(() => {
573
- if (ws.readyState === 1) {
574
- ws.send(JSON.stringify({ type: 'ping' }));
575
- } else {
605
+ if (ws.readyState !== 1) {
606
+ clearInterval(heartbeat);
607
+ return;
608
+ }
609
+ if (Date.now() - lastPongTime > 75000) {
576
610
  clearInterval(heartbeat);
611
+ ws.terminate();
612
+ return;
577
613
  }
614
+ ws.send(JSON.stringify({ type: 'ping' }));
578
615
  }, 30000);
579
616
  }
580
617
 
@@ -220,6 +220,12 @@ router.get('/user', authenticateToken, async (req, res) => {
220
220
  }
221
221
  });
222
222
 
223
+ // Get a fresh JWT for the current session (used by frontend to pass to iframe)
224
+ router.get('/token', authenticateToken, (req, res) => {
225
+ const token = generateToken(req.user);
226
+ res.json({ token });
227
+ });
228
+
223
229
  // Get user's tokens — relay token (for Connect + MCP) and API key
224
230
  router.get('/connect-token', authenticateToken, async (req, res) => {
225
231
  try {
@@ -0,0 +1,52 @@
1
+ import { Router } from 'express';
2
+ import { getProjects, getSessions } from '../projects.js';
3
+
4
+ const router = Router();
5
+
6
+ /**
7
+ * GET /api/dashboard/stats — Dashboard usage analytics
8
+ * Returns session counts, provider breakdown, and today's activity.
9
+ */
10
+ router.get('/stats', async (req, res) => {
11
+ try {
12
+ const projects = await getProjects();
13
+ let totalSessions = 0;
14
+ let todaySessions = 0;
15
+ const providers = {};
16
+ const today = new Date().toISOString().slice(0, 10);
17
+
18
+ for (const project of projects) {
19
+ // Count sessions per provider
20
+ for (const provider of ['claude', 'cursor', 'codex']) {
21
+ try {
22
+ const sessions = await getSessions(project.name, provider);
23
+ if (sessions && sessions.length) {
24
+ totalSessions += sessions.length;
25
+ providers[provider] = (providers[provider] || 0) + sessions.length;
26
+
27
+ // Count today's sessions
28
+ for (const s of sessions) {
29
+ const created = s.created_at || s.createdAt || '';
30
+ if (created.startsWith(today)) {
31
+ todaySessions++;
32
+ }
33
+ }
34
+ }
35
+ } catch (e) {
36
+ // Provider not available for this project
37
+ }
38
+ }
39
+ }
40
+
41
+ res.json({
42
+ total: totalSessions,
43
+ today: todaySessions,
44
+ providers,
45
+ projectCount: projects.length,
46
+ });
47
+ } catch (error) {
48
+ res.status(500).json({ error: 'Failed to fetch dashboard stats' });
49
+ }
50
+ });
51
+
52
+ export default router;
@@ -12,8 +12,11 @@ function sanitizeGitError(message, token) {
12
12
  return message.replace(new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '***');
13
13
  }
14
14
 
15
- // Configure allowed workspace root (defaults to user's home directory)
16
- export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir();
15
+ // Configure allowed workspace root.
16
+ // In local/platform mode, allow any path (no root restriction) unless explicitly set.
17
+ // In hosted mode, default to user's home directory for security.
18
+ const IS_LOCAL = !process.env.RAILWAY_ENVIRONMENT && !process.env.VERCEL && !process.env.RENDER;
19
+ export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || (IS_LOCAL ? null : os.homedir());
17
20
 
18
21
  // System-critical paths that should never be used as workspace directories
19
22
  export const FORBIDDEN_PATHS = [
@@ -110,42 +113,41 @@ export async function validateWorkspacePath(requestedPath) {
110
113
  }
111
114
  }
112
115
 
113
- // Resolve the workspace root to its real path
114
- const resolvedWorkspaceRoot = await fs.realpath(WORKSPACES_ROOT);
116
+ // If a workspace root is configured, enforce containment
117
+ if (WORKSPACES_ROOT) {
118
+ const resolvedWorkspaceRoot = await fs.realpath(WORKSPACES_ROOT);
115
119
 
116
- // Ensure the resolved path is contained within the allowed workspace root
117
- if (!realPath.startsWith(resolvedWorkspaceRoot + path.sep) &&
118
- realPath !== resolvedWorkspaceRoot) {
119
- return {
120
- valid: false,
121
- error: `Workspace path must be within the allowed workspace root: ${WORKSPACES_ROOT}`
122
- };
123
- }
120
+ if (!realPath.startsWith(resolvedWorkspaceRoot + path.sep) &&
121
+ realPath !== resolvedWorkspaceRoot) {
122
+ return {
123
+ valid: false,
124
+ error: `Workspace path must be within the allowed workspace root: ${WORKSPACES_ROOT}`
125
+ };
126
+ }
124
127
 
125
- // Additional symlink check for existing paths
126
- try {
127
- await fs.access(absolutePath);
128
- const stats = await fs.lstat(absolutePath);
129
-
130
- if (stats.isSymbolicLink()) {
131
- // Verify symlink target is also within allowed root
132
- const linkTarget = await fs.readlink(absolutePath);
133
- const resolvedTarget = path.resolve(path.dirname(absolutePath), linkTarget);
134
- const realTarget = await fs.realpath(resolvedTarget);
135
-
136
- if (!realTarget.startsWith(resolvedWorkspaceRoot + path.sep) &&
137
- realTarget !== resolvedWorkspaceRoot) {
138
- return {
139
- valid: false,
140
- error: 'Symlink target is outside the allowed workspace root'
141
- };
128
+ // Additional symlink check for existing paths
129
+ try {
130
+ await fs.access(absolutePath);
131
+ const stats = await fs.lstat(absolutePath);
132
+
133
+ if (stats.isSymbolicLink()) {
134
+ const linkTarget = await fs.readlink(absolutePath);
135
+ const resolvedTarget = path.resolve(path.dirname(absolutePath), linkTarget);
136
+ const realTarget = await fs.realpath(resolvedTarget);
137
+
138
+ if (!realTarget.startsWith(resolvedWorkspaceRoot + path.sep) &&
139
+ realTarget !== resolvedWorkspaceRoot) {
140
+ return {
141
+ valid: false,
142
+ error: 'Symlink target is outside the allowed workspace root'
143
+ };
144
+ }
145
+ }
146
+ } catch (error) {
147
+ if (error.code !== 'ENOENT') {
148
+ throw error;
142
149
  }
143
150
  }
144
- } catch (error) {
145
- if (error.code !== 'ENOENT') {
146
- throw error;
147
- }
148
- // Path doesn't exist - that's fine for new workspace creation
149
151
  }
150
152
 
151
153
  return {
@@ -301,7 +303,8 @@ router.post('/create-workspace', async (req, res) => {
301
303
  } catch (error) {
302
304
  // workspace creation error
303
305
  res.status(500).json({
304
- error: 'Failed to create workspace'
306
+ error: 'Failed to create workspace',
307
+ details: error.message
305
308
  });
306
309
  }
307
310
  });