upfynai-code 2.4.0 → 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 (150) 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 +64 -5
  110. package/server/constants/config.js +29 -3
  111. package/server/database/auth.db +0 -0
  112. package/server/database/db.js +203 -1
  113. package/server/index.js +348 -48
  114. package/server/mcp-server.js +2 -1
  115. package/server/middleware/auth.js +20 -9
  116. package/server/projects.js +95 -202
  117. package/server/relay-client.js +205 -11
  118. package/server/routes/auth.js +6 -0
  119. package/server/routes/commands.js +1 -1
  120. package/server/routes/dashboard.js +52 -0
  121. package/server/routes/projects.js +38 -35
  122. package/server/routes/voice.js +198 -0
  123. package/server/routes/webhooks.js +166 -0
  124. package/server/routes/workflows.js +118 -0
  125. package/server/services/whisperService.js +84 -0
  126. package/server/services/workflowScheduler.js +186 -0
  127. package/client/dist/assets/AppContent-DTZ2FbvM.js +0 -513
  128. package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +0 -6
  129. package/client/dist/assets/CanvasPanel-q4HEqNtV.css +0 -1
  130. package/client/dist/assets/LoginModal-CWoFm0au.js +0 -19
  131. package/client/dist/assets/az-AZ-76LH7QW2-CDdeucRZ.js +0 -1
  132. package/client/dist/assets/channel-O3ovC0x9.js +0 -1
  133. package/client/dist/assets/classDiagram-70f12bd4-D0lhAcxU.js +0 -2
  134. package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +0 -2
  135. package/client/dist/assets/clone-BG9u7vLi.js +0 -1
  136. package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +0 -1
  137. package/client/dist/assets/ganttDiagram-c361ad54-B8HJQqjt.js +0 -257
  138. package/client/dist/assets/index-B8wwD_Xo.css +0 -1
  139. package/client/dist/assets/kaa-6HZHGXH3-fwOleoQB.js +0 -1
  140. package/client/dist/assets/kk-KZ-P5N5QNE5-zpl7uvyF.js +0 -1
  141. package/client/dist/assets/my-MM-5M5IBNSE-kZQURVIi.js +0 -1
  142. package/client/dist/assets/sankeyDiagram-04a897e0-CsFqOQZN.js +0 -8
  143. package/client/dist/assets/si-LK-N5RQ5JYF-BBjcNYQh.js +0 -1
  144. package/client/dist/assets/stateDiagram-587899a1-BHoy9LtD.js +0 -1
  145. package/client/dist/assets/stateDiagram-v2-d93cdb3a-BvMUA6bS.js +0 -1
  146. package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +0 -1
  147. package/client/dist/assets/th-TH-HPSO5L25-CFNnJwSv.js +0 -2
  148. package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +0 -20
  149. package/client/dist/assets/vendor-codemirror-rix45NST.js +0 -16
  150. 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
@@ -14,7 +14,7 @@ import WebSocket from 'ws';
14
14
  import os from 'os';
15
15
  import fs from 'fs';
16
16
  import path from 'path';
17
- import { spawn } from 'child_process';
17
+ import { spawn, execSync } from 'child_process';
18
18
  import { promises as fsPromises } from 'fs';
19
19
  import crypto from 'crypto';
20
20
  import {
@@ -137,6 +137,100 @@ async function handleRelayCommand(data, ws) {
137
137
  break;
138
138
  }
139
139
 
140
+ case 'codex-query': {
141
+ const { command, options } = data;
142
+ logRelayEvent('>', `Codex query: ${command?.slice(0, 60)}...`, 'cyan');
143
+
144
+ const codexArgs = ['--quiet'];
145
+ if (options?.projectPath || options?.cwd) {
146
+ codexArgs.push('--cwd', options.projectPath || options.cwd);
147
+ }
148
+ if (options?.model) codexArgs.push('--model', options.model);
149
+
150
+ const codexProc = spawn('codex', [...codexArgs, command || ''], {
151
+ shell: true,
152
+ cwd: options?.projectPath || options?.cwd || os.homedir(),
153
+ env: process.env,
154
+ });
155
+
156
+ codexProc.stdout.on('data', (chunk) => {
157
+ ws.send(JSON.stringify({
158
+ type: 'relay-stream',
159
+ requestId,
160
+ data: { type: 'codex-response', content: chunk.toString() }
161
+ }));
162
+ });
163
+
164
+ codexProc.stderr.on('data', (chunk) => {
165
+ ws.send(JSON.stringify({
166
+ type: 'relay-stream',
167
+ requestId,
168
+ data: { type: 'codex-error', content: chunk.toString() }
169
+ }));
170
+ });
171
+
172
+ codexProc.on('close', (code) => {
173
+ ws.send(JSON.stringify({
174
+ type: 'relay-complete',
175
+ requestId,
176
+ exitCode: code
177
+ }));
178
+ });
179
+ break;
180
+ }
181
+
182
+ case 'cursor-query': {
183
+ const { command, options } = data;
184
+ logRelayEvent('>', `Cursor query: ${command?.slice(0, 60)}...`, 'cyan');
185
+
186
+ const cursorArgs = [];
187
+ if (options?.projectPath || options?.cwd) {
188
+ cursorArgs.push('--cwd', options.projectPath || options.cwd);
189
+ }
190
+ if (options?.model) cursorArgs.push('--model', options.model);
191
+
192
+ const cursorProc = spawn('cursor-agent', [...cursorArgs, command || ''], {
193
+ shell: true,
194
+ cwd: options?.projectPath || options?.cwd || os.homedir(),
195
+ env: process.env,
196
+ });
197
+
198
+ cursorProc.stdout.on('data', (chunk) => {
199
+ ws.send(JSON.stringify({
200
+ type: 'relay-stream',
201
+ requestId,
202
+ data: { type: 'cursor-response', content: chunk.toString() }
203
+ }));
204
+ });
205
+
206
+ cursorProc.stderr.on('data', (chunk) => {
207
+ ws.send(JSON.stringify({
208
+ type: 'relay-stream',
209
+ requestId,
210
+ data: { type: 'cursor-error', content: chunk.toString() }
211
+ }));
212
+ });
213
+
214
+ cursorProc.on('close', (code) => {
215
+ ws.send(JSON.stringify({
216
+ type: 'relay-complete',
217
+ requestId,
218
+ exitCode: code
219
+ }));
220
+ });
221
+ break;
222
+ }
223
+
224
+ case 'detect-agents': {
225
+ const agents = detectInstalledAgents();
226
+ ws.send(JSON.stringify({
227
+ type: 'relay-response',
228
+ requestId,
229
+ data: { agents }
230
+ }));
231
+ break;
232
+ }
233
+
140
234
  case 'shell-command': {
141
235
  const { command: cmd, cwd } = data;
142
236
  // Block dangerous shell patterns
@@ -228,6 +322,39 @@ async function buildFileTree(dirPath, maxDepth, currentDepth = 0) {
228
322
  }
229
323
  }
230
324
 
325
+ /**
326
+ * Detect which AI CLI agents are installed on this machine
327
+ * Returns an object with agent names and their availability
328
+ */
329
+ function detectInstalledAgents() {
330
+ const isWindows = process.platform === 'win32';
331
+ const whichCmd = isWindows ? 'where' : 'which';
332
+
333
+ const agents = [
334
+ { name: 'claude', binary: 'claude', label: 'Claude Code' },
335
+ { name: 'codex', binary: 'codex', label: 'OpenAI Codex' },
336
+ { name: 'cursor', binary: 'cursor-agent', label: 'Cursor Agent' },
337
+ ];
338
+
339
+ const detected = {};
340
+ for (const agent of agents) {
341
+ try {
342
+ const result = execSync(`${whichCmd} ${agent.binary}`, { stdio: 'pipe', timeout: 5000 }).toString().trim();
343
+ detected[agent.name] = {
344
+ installed: true,
345
+ path: result.split('\n')[0].trim(),
346
+ label: agent.label,
347
+ };
348
+ } catch {
349
+ detected[agent.name] = {
350
+ installed: false,
351
+ label: agent.label,
352
+ };
353
+ }
354
+ }
355
+ return detected;
356
+ }
357
+
231
358
  /**
232
359
  * Create WebSocket connection with optional API key in handshake
233
360
  */
@@ -239,6 +366,8 @@ function createRelayConnection(wsUrl, config = {}) {
239
366
  }
240
367
  headers['x-upfyn-version'] = VERSION;
241
368
  headers['x-upfyn-machine'] = os.hostname();
369
+ headers['x-upfyn-platform'] = process.platform;
370
+ headers['x-upfyn-cwd'] = process.cwd();
242
371
 
243
372
  return new WebSocket(wsUrl, { headers });
244
373
  }
@@ -284,11 +413,15 @@ export async function connectToServer(options = {}) {
284
413
  let reconnectAttempts = 0;
285
414
  const MAX_RECONNECT = 10;
286
415
 
416
+ let lastPongTime = Date.now();
417
+
287
418
  function connect() {
288
419
  const ws = createRelayConnection(wsUrl, config);
420
+ lastPongTime = Date.now();
289
421
 
290
422
  ws.on('open', () => {
291
423
  reconnectAttempts = 0;
424
+ lastPongTime = Date.now();
292
425
  // Don't stop spinner yet — wait for relay-connected message
293
426
  });
294
427
 
@@ -302,6 +435,34 @@ export async function connectToServer(options = {}) {
302
435
  const nameMatch = data.message?.match(/Connected as (.+?)\./);
303
436
  const username = nameMatch ? nameMatch[1] : 'Unknown';
304
437
  showConnectionBanner(username, serverUrl);
438
+
439
+ // Detect and report installed agents
440
+ const agents = detectInstalledAgents();
441
+ const installed = Object.entries(agents)
442
+ .filter(([, info]) => info.installed)
443
+ .map(([name, info]) => info.label);
444
+ const missing = Object.entries(agents)
445
+ .filter(([, info]) => !info.installed)
446
+ .map(([name, info]) => info.label);
447
+
448
+ if (installed.length > 0) {
449
+ logRelayEvent('+', `Agents found: ${installed.join(', ')}`, 'green');
450
+ }
451
+ if (missing.length > 0) {
452
+ logRelayEvent('~', `Not found: ${missing.join(', ')} (install to enable)`, 'yellow');
453
+ }
454
+
455
+ // Send agent capabilities to server
456
+ ws.send(JSON.stringify({
457
+ type: 'agent-capabilities',
458
+ agents,
459
+ machine: {
460
+ hostname: os.hostname(),
461
+ platform: process.platform,
462
+ cwd: process.cwd(),
463
+ }
464
+ }));
465
+
305
466
  logRelayEvent('*', 'Relay active -- waiting for commands...', 'green');
306
467
  return;
307
468
  }
@@ -311,7 +472,14 @@ export async function connectToServer(options = {}) {
311
472
  return;
312
473
  }
313
474
 
314
- 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
+ }
315
483
 
316
484
  if (data.type === 'error') {
317
485
  spinner.fail(`Server error: ${data.error}`);
@@ -346,14 +514,24 @@ export async function connectToServer(options = {}) {
346
514
  // close handler will trigger reconnect
347
515
  });
348
516
 
349
- // Heartbeat every 30 seconds
517
+ // Heartbeat every 30 seconds with pong timeout detection
350
518
  const heartbeat = setInterval(() => {
351
- if (ws.readyState === 1) {
352
- ws.send(JSON.stringify({ type: 'ping' }));
353
- } else {
519
+ if (ws.readyState !== 1) {
354
520
  clearInterval(heartbeat);
521
+ return;
355
522
  }
523
+ // If no pong received in 75s, consider connection dead
524
+ if (Date.now() - lastPongTime > 75000) {
525
+ clearInterval(heartbeat);
526
+ logRelayEvent('!', 'No heartbeat response — connection stale, reconnecting...', 'yellow');
527
+ ws.terminate();
528
+ return;
529
+ }
530
+ ws.send(JSON.stringify({ type: 'ping' }));
356
531
  }, 30000);
532
+
533
+ ws.on('close', () => clearInterval(heartbeat));
534
+ ws.on('error', () => clearInterval(heartbeat));
357
535
  }
358
536
 
359
537
  connect();
@@ -381,13 +559,22 @@ export function connectToServerBackground(options = {}) {
381
559
 
382
560
  let reconnectAttempts = 0;
383
561
  const MAX_RECONNECT = 5;
562
+ let lastPongTime = Date.now();
384
563
 
385
564
  function connect() {
386
565
  const ws = createRelayConnection(wsUrl, config);
566
+ lastPongTime = Date.now();
387
567
 
388
568
  ws.on('message', (rawMessage) => {
389
569
  try {
390
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
+ }
391
578
  if (data.type === 'relay-command') {
392
579
  handleRelayCommand(data, ws);
393
580
  }
@@ -396,9 +583,11 @@ export function connectToServerBackground(options = {}) {
396
583
 
397
584
  ws.on('open', () => {
398
585
  reconnectAttempts = 0;
586
+ lastPongTime = Date.now();
399
587
  });
400
588
 
401
589
  ws.on('close', (code) => {
590
+ clearInterval(heartbeat);
402
591
  if (code === 1000) return;
403
592
  reconnectAttempts++;
404
593
  if (reconnectAttempts <= MAX_RECONNECT) {
@@ -408,16 +597,21 @@ export function connectToServerBackground(options = {}) {
408
597
  });
409
598
 
410
599
  ws.on('error', () => {
411
- // silent — close handler will reconnect
600
+ clearInterval(heartbeat);
412
601
  });
413
602
 
414
- // Heartbeat
603
+ // Heartbeat with pong timeout
415
604
  const heartbeat = setInterval(() => {
416
- if (ws.readyState === 1) {
417
- ws.send(JSON.stringify({ type: 'ping' }));
418
- } else {
605
+ if (ws.readyState !== 1) {
606
+ clearInterval(heartbeat);
607
+ return;
608
+ }
609
+ if (Date.now() - lastPongTime > 75000) {
419
610
  clearInterval(heartbeat);
611
+ ws.terminate();
612
+ return;
420
613
  }
614
+ ws.send(JSON.stringify({ type: 'ping' }));
421
615
  }, 30000);
422
616
  }
423
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 {
@@ -293,7 +293,7 @@ Custom commands can be created in:
293
293
  // Read version from package.json
294
294
  const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
295
295
  let version = 'unknown';
296
- let packageName = 'upfyn-code';
296
+ let packageName = 'upfynai-code';
297
297
 
298
298
  try {
299
299
  const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));