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.
- package/client/dist/assets/AppContent-CRld2UWX.js +513 -0
- package/client/dist/assets/CanvasPanel-CB4sweQq.js +34 -0
- package/client/dist/assets/CanvasPanel-WhZulBJw.css +1 -0
- package/client/dist/assets/DashboardPanel-BXaA-b9z.js +1 -0
- package/client/dist/assets/LoginModal-BwkvjfPR.js +19 -0
- package/client/dist/assets/{Onboarding-CtIoXiTp.js → Onboarding-2A_5fPxy.js} +1 -1
- package/client/dist/assets/{SetupForm-B4p8im5O.js → SetupForm-CH5EA5W0.js} +1 -1
- package/client/dist/assets/WorkflowsPanel-CO5g5yGG.js +1 -0
- package/client/dist/assets/{ar-SA-G6X2FPQ2-2gfmdvHk.js → ar-SA-G6X2FPQ2-DoJuo98H.js} +2 -2
- package/client/dist/assets/{arc-DCZSHhoJ.js → arc-B0wBaTeh.js} +1 -1
- package/client/dist/assets/az-AZ-76LH7QW2-xdrt1Z13.js +1 -0
- package/client/dist/assets/{bg-BG-XCXSNQG7-D6__XtOK.js → bg-BG-XCXSNQG7-D8NAiF6Y.js} +2 -2
- package/client/dist/assets/{blockDiagram-38ab4fdb-Cfbaeyp6.js → blockDiagram-38ab4fdb-DSnyKzK4.js} +2 -2
- package/client/dist/assets/{bn-BD-2XOGV67Q-DHNJw3OG.js → bn-BD-2XOGV67Q-B0qWv8_J.js} +2 -2
- package/client/dist/assets/{c4Diagram-3d4e48cf-BBCnjOTy.js → c4Diagram-3d4e48cf-DoZJ13XA.js} +2 -2
- package/client/dist/assets/{ca-ES-6MX7JW3Y-r5g4o3zQ.js → ca-ES-6MX7JW3Y-RgLhfbZZ.js} +3 -3
- package/client/dist/assets/channel-BmO6nY0W.js +1 -0
- package/client/dist/assets/classDiagram-70f12bd4-GNyDrRCk.js +2 -0
- package/client/dist/assets/classDiagram-v2-f2320105-CxdGhHm2.js +2 -0
- package/client/dist/assets/clone-xuHMqFoD.js +1 -0
- package/client/dist/assets/{createText-2e5e7dd3-B8jCDmF_.js → createText-2e5e7dd3-DiPywQOa.js} +1 -1
- package/client/dist/assets/{cs-CZ-2BRQDIVT-p08jRLRC.js → cs-CZ-2BRQDIVT-BAjmnuoC.js} +2 -2
- package/client/dist/assets/{da-DK-5WZEPLOC-CnhOImFf.js → da-DK-5WZEPLOC-JxKVGt8o.js} +2 -2
- package/client/dist/assets/{de-DE-XR44H4JA-BunSXZ-Y.js → de-DE-XR44H4JA-CrnRlt4z.js} +2 -2
- package/client/dist/assets/{edges-e0da2a9e-CGBBhG8k.js → edges-e0da2a9e-DDsXzXLJ.js} +1 -1
- package/client/dist/assets/{el-GR-BZB4AONW-D4wv1oIz.js → el-GR-BZB4AONW-DQd8iogq.js} +2 -2
- package/client/dist/assets/{erDiagram-9861fffd-CYaF3q1I.js → erDiagram-9861fffd-CBiCC4rl.js} +2 -2
- package/client/dist/assets/{es-ES-U4NZUMDT-CGeTKXgd.js → es-ES-U4NZUMDT-vvUblc5i.js} +2 -2
- package/client/dist/assets/{eu-ES-A7QVB2H4-Cayx1TxR.js → eu-ES-A7QVB2H4-De4NNCc1.js} +2 -2
- package/client/dist/assets/{fa-IR-HGAKTJCU-CmUg8pmw.js → fa-IR-HGAKTJCU-DFBXqIqq.js} +2 -2
- package/client/dist/assets/{fi-FI-Z5N7JZ37-xvHcPhsU.js → fi-FI-Z5N7JZ37-DV9zESPg.js} +2 -2
- package/client/dist/assets/{flowDb-956e92f1-C-_LFz70.js → flowDb-956e92f1-BhdSHbdO.js} +1 -1
- package/client/dist/assets/{flowDiagram-66a62f08-C1sHdSjn.js → flowDiagram-66a62f08-M-fp1_Ie.js} +2 -2
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-C5eiN8Pg.js +1 -0
- package/client/dist/assets/{flowchart-elk-definition-4a651766-CNGfpudb.js → flowchart-elk-definition-4a651766-Bp0SonQx.js} +2 -2
- package/client/dist/assets/{fr-FR-RHASNOE6-DBoHEcNj.js → fr-FR-RHASNOE6-CKTMXuGk.js} +2 -2
- package/client/dist/assets/ganttDiagram-c361ad54-iA737GUS.js +257 -0
- package/client/dist/assets/{gitGraphDiagram-72cf32ee-DojCDvlS.js → gitGraphDiagram-72cf32ee-BX-wj-PV.js} +2 -2
- package/client/dist/assets/{gl-ES-HMX3MZ6V-p6hrn2cN.js → gl-ES-HMX3MZ6V-Cdiqq4jY.js} +2 -2
- package/client/dist/assets/{graph-DXM7lcy1.js → graph-Rxkx3sEa.js} +1 -1
- package/client/dist/assets/{he-IL-6SHJWFNN-y2jEX6-0.js → he-IL-6SHJWFNN-gYmR5_KT.js} +2 -2
- package/client/dist/assets/{hi-IN-IWLTKZ5I-99pNfyWr.js → hi-IN-IWLTKZ5I-pyqK94AR.js} +2 -2
- package/client/dist/assets/{hu-HU-A5ZG7DT2-hygceGMS.js → hu-HU-A5ZG7DT2-DpacJgJy.js} +2 -2
- package/client/dist/assets/{id-ID-SAP4L64H-CyIqi1hv.js → id-ID-SAP4L64H-CAvIX-mj.js} +2 -2
- package/client/dist/assets/{index-3862675e-4idOQN2N.js → index-3862675e-BX3Fpn6V.js} +1 -1
- package/client/dist/assets/{index-BHZfFT_V.js → index-BBlwbHq_.js} +4 -4
- package/client/dist/assets/{index-BGmwbRlb.js → index-ClfzLIqY.js} +6 -6
- package/client/dist/assets/index-Td4UdtLF.css +1 -0
- package/client/dist/assets/{infoDiagram-f8f76790-CFLrHqtc.js → infoDiagram-f8f76790-Ckv8imiv.js} +2 -2
- package/client/dist/assets/{it-IT-JPQ66NNP-DzVvVdQI.js → it-IT-JPQ66NNP-BtpNRSce.js} +2 -2
- package/client/dist/assets/{ja-JP-DBVTYXUO-BI4fPexV.js → ja-JP-DBVTYXUO-CwJRyY6M.js} +2 -2
- package/client/dist/assets/{journeyDiagram-49397b02-C3CFDo8z.js → journeyDiagram-49397b02-DWWZssji.js} +2 -2
- package/client/dist/assets/kaa-6HZHGXH3-DIWQEb4A.js +1 -0
- package/client/dist/assets/{kab-KAB-ZGHBKWFO-DBI_ri48.js → kab-KAB-ZGHBKWFO-DjGbqhUg.js} +2 -2
- package/client/dist/assets/kk-KZ-P5N5QNE5-B_VzJdWf.js +1 -0
- package/client/dist/assets/{km-KH-HSX4SM5Z-DOMFSres.js → km-KH-HSX4SM5Z-DUD5mi0o.js} +2 -2
- package/client/dist/assets/{ko-KR-MTYHY66A-tb08hXzd.js → ko-KR-MTYHY66A--sDB10db.js} +3 -3
- package/client/dist/assets/{ku-TR-6OUDTVRD-DlIQCCY4.js → ku-TR-6OUDTVRD-CKvKrkcX.js} +2 -2
- package/client/dist/assets/{layout-B_11mCXA.js → layout-CkB7sSeq.js} +1 -1
- package/client/dist/assets/{line-B-qmK_vI.js → line-DC7MA9qY.js} +1 -1
- package/client/dist/assets/{linear-Ph6uuYcX.js → linear-C1lBBthf.js} +1 -1
- package/client/dist/assets/{lt-LT-XHIRWOB4--qWy24_Z.js → lt-LT-XHIRWOB4-MSZf7xYG.js} +2 -2
- package/client/dist/assets/{lv-LV-5QDEKY6T-Bnd_1GDb.js → lv-LV-5QDEKY6T-C-gvvmBB.js} +2 -2
- package/client/dist/assets/{mindmap-definition-fc14e90a-Do79tIc0.js → mindmap-definition-fc14e90a-B3O7hztq.js} +2 -2
- package/client/dist/assets/{mr-IN-CRQNXWMA-BsV6HaD9.js → mr-IN-CRQNXWMA-XHtBUWQH.js} +2 -2
- package/client/dist/assets/my-MM-5M5IBNSE-D9eD2edL.js +1 -0
- package/client/dist/assets/{nb-NO-T6EIAALU-Cvf9FdSF.js → nb-NO-T6EIAALU-BlImC6gp.js} +3 -3
- package/client/dist/assets/{nl-NL-IS3SIHDZ-DA1yqpXw.js → nl-NL-IS3SIHDZ-CPFhnaSP.js} +2 -2
- package/client/dist/assets/{nn-NO-6E72VCQL-89lm3vku.js → nn-NO-6E72VCQL-BMvoJSKQ.js} +2 -2
- package/client/dist/assets/{oc-FR-POXYY2M6-BsrjTJQh.js → oc-FR-POXYY2M6-Buye63LS.js} +2 -2
- package/client/dist/assets/{pa-IN-N4M65BXN-CczefYaj.js → pa-IN-N4M65BXN-D9uQ3niy.js} +2 -2
- package/client/dist/assets/{percentages-BXMCSKIN-Be6p9phi.js → percentages-BXMCSKIN-BzXIakGM.js} +7 -7
- package/client/dist/assets/{pieDiagram-8a3498a8-CfblQHdm.js → pieDiagram-8a3498a8-BU38mzx-.js} +3 -3
- package/client/dist/assets/{pl-PL-T2D74RX3-DdhH-zcK.js → pl-PL-T2D74RX3-BqM4xdcg.js} +2 -2
- package/client/dist/assets/{pt-BR-5N22H2LF-gpwlheL6.js → pt-BR-5N22H2LF-rAjrxGyI.js} +2 -2
- package/client/dist/assets/{pt-PT-UZXXM6DQ-Cs87vICi.js → pt-PT-UZXXM6DQ-DXsqcwLt.js} +2 -2
- package/client/dist/assets/{quadrantDiagram-120e2f19-CRMSamSP.js → quadrantDiagram-120e2f19-HhK4H1WU.js} +2 -2
- package/client/dist/assets/{requirementDiagram-deff3bca-D3LBN016.js → requirementDiagram-deff3bca-aDrcyj-A.js} +2 -2
- package/client/dist/assets/{ro-RO-JPDTUUEW-CWTSJ1Dt.js → ro-RO-JPDTUUEW-D_F9UKer.js} +2 -2
- package/client/dist/assets/{ru-RU-B4JR7IUQ-Bq7aN2ep.js → ru-RU-B4JR7IUQ-MirqN29p.js} +2 -2
- package/client/dist/assets/sankeyDiagram-04a897e0-C6ij7qbQ.js +8 -0
- package/client/dist/assets/{sequenceDiagram-704730f1-BRYXVDGX.js → sequenceDiagram-704730f1-C0EKO3th.js} +2 -2
- package/client/dist/assets/si-LK-N5RQ5JYF-DyZC3mkC.js +1 -0
- package/client/dist/assets/{sk-SK-C5VTKIMK-ByjKQzUb.js → sk-SK-C5VTKIMK-D-ksz-WY.js} +2 -2
- package/client/dist/assets/{sl-SI-NN7IZMDC-B8WCyMBU.js → sl-SI-NN7IZMDC-CknuYoQ1.js} +2 -2
- package/client/dist/assets/stateDiagram-587899a1-CYoq2VjL.js +1 -0
- package/client/dist/assets/stateDiagram-v2-d93cdb3a-C5lbp5px.js +1 -0
- package/client/dist/assets/{styles-6aaf32cf-Dr-lfIOW.js → styles-6aaf32cf-Dkfsk8gt.js} +1 -1
- package/client/dist/assets/{styles-9a916d00-DS4wRpL7.js → styles-9a916d00-CMYqtcEN.js} +1 -1
- package/client/dist/assets/{styles-c10674c1-nKRF6NrH.js → styles-c10674c1-Bp-5OlRU.js} +1 -1
- package/client/dist/assets/{subset-shared.chunk-KT79s7KG.js → subset-shared.chunk-kfIB1Zam.js} +3 -3
- package/client/dist/assets/subset-worker.chunk-DwQBgc4z.js +1 -0
- package/client/dist/assets/{sv-SE-XGPEYMSR-BiIPUVbv.js → sv-SE-XGPEYMSR-DwN13se1.js} +2 -2
- package/client/dist/assets/{svgDrawCommon-08f97a94-C3uP9PYr.js → svgDrawCommon-08f97a94-CEgCMqs4.js} +1 -1
- package/client/dist/assets/{ta-IN-2NMHFXQM-Cidadso2.js → ta-IN-2NMHFXQM-ejDfFhwa.js} +2 -2
- package/client/dist/assets/th-TH-HPSO5L25-Bqc90ZNn.js +2 -0
- package/client/dist/assets/{timeline-definition-85554ec2-BSsLsIgF.js → timeline-definition-85554ec2-BmGdKqG0.js} +2 -2
- package/client/dist/assets/{tr-TR-DEFEU3FU-DaFcI-KL.js → tr-TR-DEFEU3FU-CJvlPbcW.js} +2 -2
- package/client/dist/assets/{uk-UA-QMV73CPH-DkBW36St.js → uk-UA-QMV73CPH-D26-cbWL.js} +3 -3
- package/client/dist/assets/vendor-codemirror-D_s0aGBu.js +35 -0
- package/client/dist/assets/{vendor-icons-Dh9m_Ydt.js → vendor-icons-aNdOvTr_.js} +159 -119
- package/client/dist/assets/{vi-VN-M7AON7JQ-KrtfxOzl.js → vi-VN-M7AON7JQ-MbqIIwYM.js} +2 -2
- package/client/dist/assets/{xychartDiagram-e933f94c-CgNgZ4pp.js → xychartDiagram-e933f94c-gfcTauxU.js} +2 -2
- package/client/dist/assets/{zh-CN-LNUGB5OW-BQu12RoD.js → zh-CN-LNUGB5OW-BZSmhUdL.js} +3 -3
- package/client/dist/assets/zh-HK-E62DVLB3-BJqejpiX.js +1 -0
- package/client/dist/assets/{zh-TW-RAJ6MFWO-ffJWgVxn.js → zh-TW-RAJ6MFWO-BBXtV-Uz.js} +2 -2
- package/client/dist/index.html +3 -3
- package/package.json +5 -2
- package/server/cli.js +1 -1
- package/server/database/auth.db +0 -0
- package/server/database/db.js +203 -1
- package/server/index.js +111 -18
- package/server/middleware/auth.js +11 -6
- package/server/projects.js +95 -202
- package/server/relay-client.js +47 -10
- package/server/routes/auth.js +6 -0
- package/server/routes/dashboard.js +52 -0
- package/server/routes/projects.js +38 -35
- package/server/routes/voice.js +198 -0
- package/server/routes/webhooks.js +166 -0
- package/server/routes/workflows.js +118 -0
- package/server/services/whisperService.js +84 -0
- package/server/services/workflowScheduler.js +186 -0
- package/client/dist/assets/AppContent-DTZ2FbvM.js +0 -513
- package/client/dist/assets/CanvasPanel-DlTW6Jh6.js +0 -6
- package/client/dist/assets/CanvasPanel-q4HEqNtV.css +0 -1
- package/client/dist/assets/LoginModal-CWoFm0au.js +0 -19
- package/client/dist/assets/az-AZ-76LH7QW2-CDdeucRZ.js +0 -1
- package/client/dist/assets/channel-O3ovC0x9.js +0 -1
- package/client/dist/assets/classDiagram-70f12bd4-D0lhAcxU.js +0 -2
- package/client/dist/assets/classDiagram-v2-f2320105-BuwUsF3F.js +0 -2
- package/client/dist/assets/clone-BG9u7vLi.js +0 -1
- package/client/dist/assets/flowDiagram-v2-96b9c2cf-Cd0Iascd.js +0 -1
- package/client/dist/assets/ganttDiagram-c361ad54-B8HJQqjt.js +0 -257
- package/client/dist/assets/index-B8wwD_Xo.css +0 -1
- package/client/dist/assets/kaa-6HZHGXH3-fwOleoQB.js +0 -1
- package/client/dist/assets/kk-KZ-P5N5QNE5-zpl7uvyF.js +0 -1
- package/client/dist/assets/my-MM-5M5IBNSE-kZQURVIi.js +0 -1
- package/client/dist/assets/sankeyDiagram-04a897e0-CsFqOQZN.js +0 -8
- package/client/dist/assets/si-LK-N5RQ5JYF-BBjcNYQh.js +0 -1
- package/client/dist/assets/stateDiagram-587899a1-BHoy9LtD.js +0 -1
- package/client/dist/assets/stateDiagram-v2-d93cdb3a-BvMUA6bS.js +0 -1
- package/client/dist/assets/subset-worker.chunk-BMx1eyv3.js +0 -1
- package/client/dist/assets/th-TH-HPSO5L25-CFNnJwSv.js +0 -2
- package/client/dist/assets/vendor-codemirror-langs-BH1ZcKHY.js +0 -20
- package/client/dist/assets/vendor-codemirror-rix45NST.js +0 -16
- package/client/dist/assets/zh-HK-E62DVLB3-zx9CvERq.js +0 -1
package/server/projects.js
CHANGED
|
@@ -387,8 +387,8 @@ async function extractProjectDirectory(projectName) {
|
|
|
387
387
|
}
|
|
388
388
|
|
|
389
389
|
async function getProjects(progressCallback = null) {
|
|
390
|
-
// Wrap with a
|
|
391
|
-
const timeoutMs =
|
|
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
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
499
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
465
|
+
// Fetch Cursor sessions
|
|
466
|
+
try {
|
|
467
|
+
project.cursorSessions = await getCursorSessions(actualProjectDir);
|
|
468
|
+
} catch (e) {
|
|
469
|
+
// No Cursor sessions
|
|
470
|
+
}
|
|
579
471
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
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
|
-
|
|
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
|
package/server/relay-client.js
CHANGED
|
@@ -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')
|
|
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
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
600
|
+
clearInterval(heartbeat);
|
|
569
601
|
});
|
|
570
602
|
|
|
571
|
-
// Heartbeat
|
|
603
|
+
// Heartbeat with pong timeout
|
|
572
604
|
const heartbeat = setInterval(() => {
|
|
573
|
-
if (ws.readyState
|
|
574
|
-
|
|
575
|
-
|
|
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
|
|
package/server/routes/auth.js
CHANGED
|
@@ -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
|
|
16
|
-
|
|
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
|
-
//
|
|
114
|
-
|
|
116
|
+
// If a workspace root is configured, enforce containment
|
|
117
|
+
if (WORKSPACES_ROOT) {
|
|
118
|
+
const resolvedWorkspaceRoot = await fs.realpath(WORKSPACES_ROOT);
|
|
115
119
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
});
|