@cluesmith/codev 1.3.0 → 1.4.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 (59) hide show
  1. package/dist/agent-farm/cli.d.ts.map +1 -1
  2. package/dist/agent-farm/cli.js +2 -1
  3. package/dist/agent-farm/cli.js.map +1 -1
  4. package/dist/agent-farm/commands/cleanup.js +12 -51
  5. package/dist/agent-farm/commands/cleanup.js.map +1 -1
  6. package/dist/agent-farm/commands/spawn.d.ts.map +1 -1
  7. package/dist/agent-farm/commands/spawn.js +13 -1
  8. package/dist/agent-farm/commands/spawn.js.map +1 -1
  9. package/dist/agent-farm/servers/dashboard-server.js +107 -5
  10. package/dist/agent-farm/servers/dashboard-server.js.map +1 -1
  11. package/dist/agent-farm/types.d.ts +1 -0
  12. package/dist/agent-farm/types.d.ts.map +1 -1
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +2 -17
  15. package/dist/cli.js.map +1 -1
  16. package/dist/commands/adopt.d.ts.map +1 -1
  17. package/dist/commands/adopt.js +27 -2
  18. package/dist/commands/adopt.js.map +1 -1
  19. package/dist/commands/consult/index.d.ts.map +1 -1
  20. package/dist/commands/consult/index.js +23 -7
  21. package/dist/commands/consult/index.js.map +1 -1
  22. package/dist/commands/doctor.d.ts.map +1 -1
  23. package/dist/commands/doctor.js +51 -0
  24. package/dist/commands/doctor.js.map +1 -1
  25. package/dist/commands/init.d.ts.map +1 -1
  26. package/dist/commands/init.js +23 -2
  27. package/dist/commands/init.js.map +1 -1
  28. package/dist/version.d.ts +3 -0
  29. package/dist/version.d.ts.map +1 -0
  30. package/dist/version.js +23 -0
  31. package/dist/version.js.map +1 -0
  32. package/package.json +1 -1
  33. package/skeleton/DEPENDENCIES.md +3 -3
  34. package/skeleton/protocols/maintain/protocol.md +2 -2
  35. package/skeleton/{docs → resources}/commands/codev.md +0 -39
  36. package/skeleton/{docs → resources}/commands/consult.md +12 -2
  37. package/skeleton/{docs → resources}/commands/overview.md +0 -1
  38. package/skeleton/roles/architect.md +22 -0
  39. package/skeleton/roles/builder.md +22 -0
  40. package/skeleton/templates/arch.md +56 -0
  41. package/skeleton/templates/pr-overview.md +73 -0
  42. package/templates/dashboard-split.html +408 -41
  43. package/templates/open.html +278 -0
  44. package/templates/tower.html +71 -12
  45. package/dist/agent-farm/index.d.ts +0 -7
  46. package/dist/agent-farm/index.d.ts.map +0 -1
  47. package/dist/agent-farm/index.js +0 -373
  48. package/dist/agent-farm/index.js.map +0 -1
  49. package/skeleton/bin/agent-farm +0 -7
  50. package/skeleton/bin/codev-doctor +0 -335
  51. package/skeleton/resources/lessons-learned.md +0 -30
  52. /package/skeleton/{roles/review-types → consult-types}/impl-review.md +0 -0
  53. /package/skeleton/{roles/review-types → consult-types}/integration-review.md +0 -0
  54. /package/skeleton/{roles/review-types → consult-types}/plan-review.md +0 -0
  55. /package/skeleton/{roles/review-types → consult-types}/pr-ready.md +0 -0
  56. /package/skeleton/{roles/review-types → consult-types}/spec-review.md +0 -0
  57. /package/skeleton/{docs → resources}/commands/agent-farm.md +0 -0
  58. /package/skeleton/{AGENTS.md.template → templates/AGENTS.md} +0 -0
  59. /package/skeleton/{CLAUDE.md.template → templates/CLAUDE.md} +0 -0
@@ -1288,6 +1288,198 @@
1288
1288
  color: var(--text-secondary);
1289
1289
  font-size: 13px;
1290
1290
  }
1291
+
1292
+ /* Dashboard Tab Styles (Spec 0057) */
1293
+ .dashboard-container {
1294
+ flex: 1;
1295
+ overflow-y: auto;
1296
+ display: flex;
1297
+ flex-direction: column;
1298
+ }
1299
+
1300
+ .dashboard-header {
1301
+ display: grid;
1302
+ grid-template-columns: 1fr 1fr;
1303
+ gap: 16px;
1304
+ padding: 16px;
1305
+ flex-shrink: 0;
1306
+ }
1307
+
1308
+ @media (max-width: 900px) {
1309
+ .dashboard-header {
1310
+ grid-template-columns: 1fr;
1311
+ }
1312
+ }
1313
+
1314
+ .dashboard-column {
1315
+ background: var(--bg-secondary);
1316
+ border: 1px solid var(--border);
1317
+ border-radius: 8px;
1318
+ padding: 12px;
1319
+ overflow: hidden;
1320
+ display: flex;
1321
+ flex-direction: column;
1322
+ max-height: 280px;
1323
+ }
1324
+
1325
+ .dashboard-column-header {
1326
+ display: flex;
1327
+ justify-content: space-between;
1328
+ align-items: center;
1329
+ margin-bottom: 8px;
1330
+ flex-shrink: 0;
1331
+ }
1332
+
1333
+ .dashboard-column-header h3 {
1334
+ font-size: 12px;
1335
+ text-transform: uppercase;
1336
+ color: var(--text-muted);
1337
+ letter-spacing: 0.5px;
1338
+ margin: 0;
1339
+ }
1340
+
1341
+ .dashboard-column-header .header-actions {
1342
+ display: flex;
1343
+ gap: 4px;
1344
+ }
1345
+
1346
+ .dashboard-column-header .header-actions button {
1347
+ padding: 4px 8px;
1348
+ border-radius: 4px;
1349
+ border: 1px solid var(--border);
1350
+ background: var(--bg-tertiary);
1351
+ color: var(--text-secondary);
1352
+ cursor: pointer;
1353
+ font-size: 11px;
1354
+ }
1355
+
1356
+ .dashboard-column-header .header-actions button:hover {
1357
+ background: var(--tab-hover);
1358
+ color: var(--text-primary);
1359
+ }
1360
+
1361
+ .dashboard-tabs-list {
1362
+ flex: 1;
1363
+ overflow-y: auto;
1364
+ margin-bottom: 8px;
1365
+ }
1366
+
1367
+ .dashboard-tab-item {
1368
+ display: flex;
1369
+ align-items: center;
1370
+ gap: 8px;
1371
+ padding: 6px 8px;
1372
+ border-radius: 4px;
1373
+ cursor: pointer;
1374
+ font-size: 13px;
1375
+ color: var(--text-secondary);
1376
+ }
1377
+
1378
+ .dashboard-tab-item:hover {
1379
+ background: var(--bg-tertiary);
1380
+ }
1381
+
1382
+ .dashboard-tab-item.active {
1383
+ background: var(--accent);
1384
+ color: white;
1385
+ }
1386
+
1387
+ .dashboard-tab-item .tab-icon {
1388
+ font-size: 14px;
1389
+ flex-shrink: 0;
1390
+ }
1391
+
1392
+ .dashboard-tab-item .tab-name {
1393
+ flex: 1;
1394
+ overflow: hidden;
1395
+ text-overflow: ellipsis;
1396
+ white-space: nowrap;
1397
+ }
1398
+
1399
+ .dashboard-actions {
1400
+ flex-shrink: 0;
1401
+ display: flex;
1402
+ gap: 8px;
1403
+ }
1404
+
1405
+ .dashboard-actions .btn-action {
1406
+ flex: 1;
1407
+ padding: 8px 12px;
1408
+ border-radius: 4px;
1409
+ border: 1px dashed var(--border);
1410
+ background: transparent;
1411
+ color: var(--text-muted);
1412
+ cursor: pointer;
1413
+ font-size: 12px;
1414
+ display: flex;
1415
+ align-items: center;
1416
+ justify-content: center;
1417
+ gap: 4px;
1418
+ }
1419
+
1420
+ .dashboard-actions .btn-action:hover {
1421
+ border-style: solid;
1422
+ color: var(--text-secondary);
1423
+ background: var(--bg-tertiary);
1424
+ }
1425
+
1426
+ .dashboard-files-list {
1427
+ flex: 1;
1428
+ overflow-y: auto;
1429
+ }
1430
+
1431
+ .dashboard-files-list .tree-item {
1432
+ padding: 3px 6px;
1433
+ font-size: 12px;
1434
+ }
1435
+
1436
+ .dashboard-files-list .tree-item-name {
1437
+ font-size: 12px;
1438
+ }
1439
+
1440
+ .dashboard-empty-state {
1441
+ color: var(--text-muted);
1442
+ font-size: 13px;
1443
+ padding: 12px;
1444
+ text-align: center;
1445
+ }
1446
+
1447
+ .dashboard-projects {
1448
+ flex: 1;
1449
+ overflow-y: auto;
1450
+ padding: 0 16px 16px 16px;
1451
+ }
1452
+
1453
+ /* Status indicators in dashboard tab list */
1454
+ .dashboard-status-indicator {
1455
+ width: 8px;
1456
+ height: 8px;
1457
+ border-radius: 50%;
1458
+ flex-shrink: 0;
1459
+ }
1460
+
1461
+ .dashboard-status-working {
1462
+ background: var(--status-active);
1463
+ animation: status-pulse 2s ease-in-out infinite;
1464
+ }
1465
+
1466
+ .dashboard-status-idle {
1467
+ background: var(--status-waiting);
1468
+ animation: status-blink-slow 3s ease-in-out infinite;
1469
+ }
1470
+
1471
+ .dashboard-status-blocked {
1472
+ background: var(--status-error);
1473
+ animation: status-blink-fast 0.8s ease-in-out infinite;
1474
+ }
1475
+
1476
+ @media (prefers-reduced-motion: reduce) {
1477
+ .dashboard-status-working,
1478
+ .dashboard-status-idle,
1479
+ .dashboard-status-blocked {
1480
+ animation: none;
1481
+ }
1482
+ }
1291
1483
  </style>
1292
1484
  </head>
1293
1485
  <body>
@@ -1516,11 +1708,11 @@
1516
1708
  const previousTabIds = new Set(tabs.map(t => t.id));
1517
1709
  tabs = [];
1518
1710
 
1519
- // Projects tab is ALWAYS first and uncloseable (Spec 0045)
1711
+ // Dashboard tab is ALWAYS first and uncloseable (Spec 0045, 0057)
1520
1712
  tabs.push({
1521
- id: 'projects',
1522
- type: 'projects',
1523
- name: 'Projects',
1713
+ id: 'dashboard',
1714
+ type: 'dashboard',
1715
+ name: 'Dashboard',
1524
1716
  closeable: false
1525
1717
  });
1526
1718
 
@@ -1569,7 +1761,7 @@
1569
1761
 
1570
1762
  // Detect new tabs and auto-switch to them (skip projects tab)
1571
1763
  for (const tab of tabs) {
1572
- if (tab.id !== 'projects' && tab.id !== 'files' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
1764
+ if (tab.id !== 'dashboard' && tab.id !== 'files' && !knownTabIds.has(tab.id) && previousTabIds.size > 0) {
1573
1765
  // This is a new tab - switch to it
1574
1766
  activeTabId = tab.id;
1575
1767
  break;
@@ -1579,9 +1771,9 @@
1579
1771
  // Update known tab IDs
1580
1772
  knownTabIds = new Set(tabs.map(t => t.id));
1581
1773
 
1582
- // Set active tab to Projects on first load if none selected
1774
+ // Set active tab to Dashboard on first load if none selected
1583
1775
  if (!activeTabId) {
1584
- activeTabId = 'projects';
1776
+ activeTabId = 'dashboard';
1585
1777
  }
1586
1778
  }
1587
1779
 
@@ -1663,7 +1855,7 @@
1663
1855
  // Get tab icon
1664
1856
  function getTabIcon(type) {
1665
1857
  switch (type) {
1666
- case 'projects': return '📋';
1858
+ case 'dashboard': return '📋';
1667
1859
  case 'files': return '📁';
1668
1860
  case 'file': return '📄';
1669
1861
  case 'builder': return '🔨';
@@ -1762,12 +1954,12 @@
1762
1954
  return;
1763
1955
  }
1764
1956
 
1765
- // Handle projects tab specially (no iframe, inline content)
1766
- if (tab.type === 'projects') {
1767
- if (currentTabType !== 'projects') {
1768
- currentTabType = 'projects';
1957
+ // Handle dashboard tab specially (no iframe, inline content)
1958
+ if (tab.type === 'dashboard') {
1959
+ if (currentTabType !== 'dashboard') {
1960
+ currentTabType = 'dashboard';
1769
1961
  currentTabPort = null;
1770
- renderProjectsTab();
1962
+ renderDashboardTab();
1771
1963
  }
1772
1964
  return;
1773
1965
  }
@@ -3113,13 +3305,26 @@
3113
3305
  } else {
3114
3306
  filesTreeExpanded.add(path);
3115
3307
  }
3116
- renderFilesTabContent();
3308
+ rerenderFilesBrowser();
3309
+ }
3310
+
3311
+ // Re-render file browser in current context (dashboard or files tab)
3312
+ function rerenderFilesBrowser() {
3313
+ if (activeTabId === 'dashboard') {
3314
+ // Re-render just the files column in dashboard
3315
+ const filesListEl = document.getElementById('dashboard-files-list');
3316
+ if (filesListEl) {
3317
+ filesListEl.innerHTML = renderDashboardFilesBrowser();
3318
+ }
3319
+ } else if (activeTabId === 'files') {
3320
+ renderFilesTabContent();
3321
+ }
3117
3322
  }
3118
3323
 
3119
3324
  // Collapse all folders
3120
3325
  function collapseAllFolders() {
3121
3326
  filesTreeExpanded.clear();
3122
- renderFilesTabContent();
3327
+ rerenderFilesBrowser();
3123
3328
  }
3124
3329
 
3125
3330
  // Expand all folders
@@ -3135,13 +3340,13 @@
3135
3340
  }
3136
3341
  }
3137
3342
  collectPaths(filesTreeData);
3138
- renderFilesTabContent();
3343
+ rerenderFilesBrowser();
3139
3344
  }
3140
3345
 
3141
3346
  // Refresh files tree
3142
3347
  async function refreshFilesTree() {
3143
3348
  await loadFilesTree();
3144
- renderFilesTabContent();
3349
+ rerenderFilesBrowser();
3145
3350
  showToast('Files refreshed', 'success');
3146
3351
  }
3147
3352
 
@@ -3197,47 +3402,209 @@
3197
3402
  `;
3198
3403
  }
3199
3404
 
3200
- // Render the projects tab content (internal - called after data is loaded)
3201
- function renderProjectsTabContent() {
3405
+ // Render the dashboard tab content (internal - called after data is loaded)
3406
+ function renderDashboardTabContent() {
3202
3407
  const content = document.getElementById('tab-content');
3203
3408
 
3204
- if (projectlistError) {
3205
- content.innerHTML = `
3206
- <div class="projects-container">
3207
- ${renderErrorBanner(projectlistError)}
3409
+ content.innerHTML = `
3410
+ <div class="dashboard-container">
3411
+ <div class="dashboard-header">
3412
+ <!-- Left Column: Tabs -->
3413
+ <div class="dashboard-column">
3414
+ <div class="dashboard-column-header">
3415
+ <h3>Tabs</h3>
3416
+ </div>
3417
+ <div class="dashboard-tabs-list" id="dashboard-tabs-list">
3418
+ ${renderDashboardTabsList()}
3419
+ </div>
3420
+ <div class="dashboard-actions">
3421
+ <button class="btn-action" onclick="createNewShell()" title="New utility shell">+ New Shell</button>
3422
+ <button class="btn-action" onclick="createNewWorktreeShell()" title="New worktree shell">+ New Worktree</button>
3423
+ </div>
3424
+ </div>
3425
+ <!-- Right Column: Files -->
3426
+ <div class="dashboard-column">
3427
+ <div class="dashboard-column-header">
3428
+ <h3>Files</h3>
3429
+ <div class="header-actions">
3430
+ <button onclick="collapseAllFolders()" title="Collapse All">⊟</button>
3431
+ <button onclick="expandAllFolders()" title="Expand All">⊞</button>
3432
+ </div>
3433
+ </div>
3434
+ <div class="dashboard-files-list" id="dashboard-files-list">
3435
+ ${renderDashboardFilesBrowser()}
3436
+ </div>
3437
+ </div>
3438
+ </div>
3439
+ <div class="dashboard-projects" id="dashboard-projects">
3440
+ ${renderDashboardProjectsSection()}
3441
+ </div>
3442
+ </div>
3443
+ `;
3444
+ }
3445
+
3446
+ // Render the tabs list for dashboard
3447
+ function renderDashboardTabsList() {
3448
+ // Filter to show terminal tabs only (not Dashboard/Files tabs)
3449
+ const terminalTabs = tabs.filter(t => t.type !== 'dashboard' && t.type !== 'files');
3450
+
3451
+ if (terminalTabs.length === 0) {
3452
+ return '<div class="dashboard-empty-state">No tabs open</div>';
3453
+ }
3454
+
3455
+ return terminalTabs.map(tab => {
3456
+ const isActive = tab.id === activeTabId;
3457
+ const icon = getTabIcon(tab.type);
3458
+ const statusIndicator = getDashboardStatusIndicator(tab);
3459
+
3460
+ return `
3461
+ <div class="dashboard-tab-item ${isActive ? 'active' : ''}" onclick="selectTab('${tab.id}')">
3462
+ ${statusIndicator}
3463
+ <span class="tab-icon">${icon}</span>
3464
+ <span class="tab-name">${escapeHtml(tab.name)}</span>
3208
3465
  </div>
3209
3466
  `;
3210
- return;
3467
+ }).join('');
3468
+ }
3469
+
3470
+ // Get status indicator for dashboard tab list
3471
+ function getDashboardStatusIndicator(tab) {
3472
+ if (tab.type !== 'builder') return '';
3473
+
3474
+ // Use builder status from state
3475
+ const builderState = (state.builders || []).find(b => `builder-${b.id}` === tab.id);
3476
+ if (!builderState) return '';
3477
+
3478
+ const status = builderState.status;
3479
+ if (['spawning', 'implementing'].includes(status)) {
3480
+ return '<span class="dashboard-status-indicator dashboard-status-working" title="Working"></span>';
3481
+ }
3482
+ if (status === 'blocked') {
3483
+ return '<span class="dashboard-status-indicator dashboard-status-blocked" title="Blocked"></span>';
3484
+ }
3485
+ if (['pr-ready', 'complete'].includes(status)) {
3486
+ return '<span class="dashboard-status-indicator dashboard-status-idle" title="Idle"></span>';
3487
+ }
3488
+ return '';
3489
+ }
3490
+
3491
+ // Render compact file browser for dashboard
3492
+ function renderDashboardFilesBrowser() {
3493
+ if (filesTreeError) {
3494
+ return `<div class="dashboard-empty-state">${escapeHtml(filesTreeError)}</div>`;
3495
+ }
3496
+
3497
+ if (!filesTreeLoaded || filesTreeData.length === 0) {
3498
+ return '<div class="dashboard-empty-state">Loading files...</div>';
3499
+ }
3500
+
3501
+ return renderTreeNodes(filesTreeData, 0);
3502
+ }
3503
+
3504
+ // Render the projects section for dashboard
3505
+ function renderDashboardProjectsSection() {
3506
+ if (projectlistError) {
3507
+ return renderErrorBanner(projectlistError);
3211
3508
  }
3212
3509
 
3213
3510
  if (projectsData.length === 0) {
3214
- content.innerHTML = `
3215
- <div class="projects-container">
3216
- ${renderWelcomeScreen()}
3511
+ // No welcome screen - just a helpful message
3512
+ return `
3513
+ <div class="dashboard-empty-state" style="padding: 24px;">
3514
+ No projects yet. Ask the Architect to create your first project.
3217
3515
  </div>
3218
3516
  `;
3219
- return;
3220
3517
  }
3221
3518
 
3222
- content.innerHTML = `
3223
- <div class="projects-container">
3224
- ${renderInfoHeader()}
3225
- ${renderKanbanGrid(projectsData)}
3226
- ${renderTerminalProjects(projectsData)}
3227
- </div>
3519
+ // Render the existing project view (unchanged)
3520
+ return `
3521
+ ${renderInfoHeader()}
3522
+ ${renderKanbanGrid(projectsData)}
3523
+ ${renderTerminalProjects(projectsData)}
3228
3524
  `;
3229
3525
  }
3230
3526
 
3231
- // Render the projects tab (entry point - loads data first)
3232
- async function renderProjectsTab() {
3527
+ // Create new utility shell (quick action button)
3528
+ async function createNewShell() {
3529
+ try {
3530
+ const response = await fetch('/api/tabs/shell', { method: 'POST' });
3531
+ const data = await response.json();
3532
+ if (!data.success && data.error) {
3533
+ showToast(data.error || 'Failed to create shell', 'error');
3534
+ return;
3535
+ }
3536
+ await refresh();
3537
+ if (data.id) {
3538
+ selectTab(`shell-${data.id}`);
3539
+ }
3540
+ showToast('Shell created', 'success');
3541
+ } catch (err) {
3542
+ showToast('Network error: ' + err.message, 'error');
3543
+ }
3544
+ }
3545
+
3546
+ // Create new worktree shell (quick action button)
3547
+ async function createNewWorktreeShell() {
3548
+ const branch = prompt('Branch name (leave empty for temp worktree):');
3549
+ if (branch === null) return; // User cancelled
3550
+
3551
+ try {
3552
+ const response = await fetch('/api/tabs/shell', {
3553
+ method: 'POST',
3554
+ headers: { 'Content-Type': 'application/json' },
3555
+ body: JSON.stringify({ worktree: true, branch: branch || undefined })
3556
+ });
3557
+ const data = await response.json();
3558
+ if (!data.success && data.error) {
3559
+ showToast(data.error || 'Failed to create worktree shell', 'error');
3560
+ return;
3561
+ }
3562
+ await refresh();
3563
+ // Auto-select the newly created tab (consistent with createNewShell behavior)
3564
+ if (data.id) {
3565
+ selectTab(`shell-${data.id}`);
3566
+ }
3567
+ showToast('Worktree shell created', 'success');
3568
+ } catch (err) {
3569
+ showToast('Network error: ' + err.message, 'error');
3570
+ }
3571
+ }
3572
+
3573
+ // Render the dashboard tab (entry point - loads data first)
3574
+ async function renderDashboardTab() {
3233
3575
  const content = document.getElementById('tab-content');
3234
- content.innerHTML = '<div class="projects-container"><p style="color: var(--text-muted);">Loading projects...</p></div>';
3576
+ content.innerHTML = '<div class="dashboard-container"><p style="color: var(--text-muted); padding: 16px;">Loading dashboard...</p></div>';
3235
3577
 
3236
- await loadProjectlist();
3237
- renderProjectsTabContent();
3578
+ // Load both projectlist and files tree in parallel
3579
+ await Promise.all([
3580
+ loadProjectlist(),
3581
+ loadFilesTreeIfNeeded()
3582
+ ]);
3583
+
3584
+ renderDashboardTabContent();
3238
3585
  checkStarterMode(); // Update polling state after initial load
3239
3586
  }
3240
3587
 
3588
+ // Load files tree if not already loaded
3589
+ async function loadFilesTreeIfNeeded() {
3590
+ if (!filesTreeLoaded) {
3591
+ await loadFilesTree();
3592
+ }
3593
+ }
3594
+
3595
+ // Legacy function for backward compatibility (still used by polling)
3596
+ function renderProjectsTabContent() {
3597
+ // If dashboard tab is active, re-render dashboard instead
3598
+ if (activeTabId === 'dashboard') {
3599
+ renderDashboardTabContent();
3600
+ }
3601
+ }
3602
+
3603
+ // Legacy function alias
3604
+ async function renderProjectsTab() {
3605
+ await renderDashboardTab();
3606
+ }
3607
+
3241
3608
  // Load projectlist.md from disk
3242
3609
  async function loadProjectlist() {
3243
3610
  try {
@@ -3282,8 +3649,8 @@
3282
3649
 
3283
3650
  // Poll projectlist for changes (every 5 seconds)
3284
3651
  async function pollProjectlist() {
3285
- // Only poll if projects tab is active
3286
- if (activeTabId !== 'projects') return;
3652
+ // Only poll if dashboard tab is active
3653
+ if (activeTabId !== 'dashboard') return;
3287
3654
 
3288
3655
  try {
3289
3656
  const response = await fetch('/file?path=codev/projectlist.md');