@phren/cli 0.0.15 → 0.0.17
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/mcp/dist/capabilities/cli.js +1 -1
- package/mcp/dist/capabilities/mcp.js +1 -1
- package/mcp/dist/capabilities/vscode.js +1 -1
- package/mcp/dist/capabilities/web-ui.js +1 -1
- package/mcp/dist/cli-graph.js +4 -1
- package/mcp/dist/cli-hooks-context.js +1 -1
- package/mcp/dist/cli-namespaces.js +17 -7
- package/mcp/dist/hooks.js +126 -26
- package/mcp/dist/mcp-data.js +38 -39
- package/mcp/dist/mcp-graph.js +2 -2
- package/mcp/dist/mcp-hooks.js +5 -64
- package/mcp/dist/mcp-skills.js +13 -5
- package/mcp/dist/memory-ui-assets.js +3 -2
- package/mcp/dist/memory-ui-graph.js +26 -12
- package/mcp/dist/memory-ui-page.js +18 -20
- package/mcp/dist/memory-ui-scripts.js +78 -21
- package/mcp/dist/memory-ui-server.js +13 -5
- package/mcp/dist/memory-ui-styles.js +102 -0
- package/mcp/dist/phren-paths.js +17 -0
- package/mcp/dist/shared.js +1 -1
- package/mcp/dist/skill-registry.js +25 -2
- package/package.json +1 -1
|
@@ -338,10 +338,10 @@ export function renderGraphScript() {
|
|
|
338
338
|
/* sprite rendering */
|
|
339
339
|
if (phrenImgReady) {
|
|
340
340
|
ctx.save();
|
|
341
|
-
/* fixed
|
|
342
|
-
var spriteScreenSize =
|
|
341
|
+
/* fixed 48px size in screen pixels, scale up to 56px on arrival flash */
|
|
342
|
+
var spriteScreenSize = 48;
|
|
343
343
|
if (phren.arriving && phren.arriveTimer < 0.4) {
|
|
344
|
-
spriteScreenSize =
|
|
344
|
+
spriteScreenSize = 48 + 8 * (1 - phren.arriveTimer / 0.4);
|
|
345
345
|
}
|
|
346
346
|
var spriteSize = spriteScreenSize * s; /* convert to graph coords */
|
|
347
347
|
/* bob up/down when walking — sine wave synced to walk progress */
|
|
@@ -1454,8 +1454,8 @@ export function renderGraphScript() {
|
|
|
1454
1454
|
if (healthText) html += '<div style="margin-top:4px">' + healthText + '</div>';
|
|
1455
1455
|
if (node.project) {
|
|
1456
1456
|
html += '<div style="display:flex;gap:8px;margin-top:12px">';
|
|
1457
|
-
html += '<button class="btn btn-sm"
|
|
1458
|
-
html += '<button class="btn btn-sm"
|
|
1457
|
+
html += '<button class="btn btn-sm" data-action="graphNodeEdit" data-project="' + esc(node.project) + '" data-finding="' + esc(findingText) + '" style="padding:5px 14px;font-size:12px">Edit</button>';
|
|
1458
|
+
html += '<button class="btn btn-sm" data-action="graphNodeDelete" data-project="' + esc(node.project) + '" data-finding="' + esc(findingText) + '" style="padding:5px 14px;font-size:12px;color:#ef4444;border-color:#ef4444">Delete</button>';
|
|
1459
1459
|
html += '</div>';
|
|
1460
1460
|
}
|
|
1461
1461
|
|
|
@@ -1507,8 +1507,8 @@ export function renderGraphScript() {
|
|
|
1507
1507
|
html += '<div style="margin-top:8px;padding:8px 10px;border-radius:6px;border:1px solid var(--border);background:var(--surface-alt,var(--surface))">';
|
|
1508
1508
|
html += '<div style="font-size:12px;line-height:1.5;color:var(--ink)">' + esc(lfText) + '</div>';
|
|
1509
1509
|
html += '<div style="display:flex;gap:6px;margin-top:6px">';
|
|
1510
|
-
html += '<button class="btn btn-sm"
|
|
1511
|
-
html += '<button class="btn btn-sm"
|
|
1510
|
+
html += '<button class="btn btn-sm" data-action="graphNodeEdit" data-project="' + esc(lf.project) + '" data-finding="' + esc(lfText) + '" style="padding:3px 10px;font-size:11px">Edit</button>';
|
|
1511
|
+
html += '<button class="btn btn-sm" data-action="graphNodeDelete" data-project="' + esc(lf.project) + '" data-finding="' + esc(lfText) + '" style="padding:3px 10px;font-size:11px;color:#ef4444;border-color:#ef4444">Delete</button>';
|
|
1512
1512
|
html += '</div></div>';
|
|
1513
1513
|
}
|
|
1514
1514
|
}
|
|
@@ -2056,11 +2056,25 @@ export function renderGraphScript() {
|
|
|
2056
2056
|
renderGraphDetails(null);
|
|
2057
2057
|
};
|
|
2058
2058
|
|
|
2059
|
+
/* ── delegated click handler for graph detail panel buttons ─────────── */
|
|
2060
|
+
document.addEventListener('click', function(e) {
|
|
2061
|
+
var target = e.target;
|
|
2062
|
+
if (!target || typeof target.closest !== 'function') return;
|
|
2063
|
+
var actionEl = target.closest('[data-action]');
|
|
2064
|
+
if (!actionEl) return;
|
|
2065
|
+
var action = actionEl.getAttribute('data-action');
|
|
2066
|
+
var project = actionEl.getAttribute('data-project');
|
|
2067
|
+
var finding = actionEl.getAttribute('data-finding');
|
|
2068
|
+
if (action === 'graphNodeEdit' && project && finding) { window.graphNodeEdit(project, finding); }
|
|
2069
|
+
else if (action === 'graphNodeDelete' && project && finding) { window.graphNodeDelete(project, finding); }
|
|
2070
|
+
});
|
|
2071
|
+
|
|
2059
2072
|
/* ── node edit/delete actions ────────────────────────────────────────── */
|
|
2060
2073
|
|
|
2061
|
-
function
|
|
2062
|
-
|
|
2063
|
-
|
|
2074
|
+
var authUrl = window._phrenAuthUrl || function(u) { return u; };
|
|
2075
|
+
var fetchCsrfToken = window._phrenFetchCsrfToken || function(cb) {
|
|
2076
|
+
fetch(authUrl('/api/csrf-token')).then(function(r) { return r.json(); }).then(function(d) { cb(d.token || ''); }).catch(function() { cb(''); });
|
|
2077
|
+
};
|
|
2064
2078
|
|
|
2065
2079
|
window.graphNodeEdit = function(project, findingText) {
|
|
2066
2080
|
var panel = document.getElementById('graph-detail-panel');
|
|
@@ -2096,7 +2110,7 @@ export function renderGraphScript() {
|
|
|
2096
2110
|
body.set('old_text', findingText);
|
|
2097
2111
|
body.set('new_text', newText);
|
|
2098
2112
|
if (csrf) body.set('_csrf', csrf);
|
|
2099
|
-
fetch('/api/findings/' + encodeURIComponent(project), { method: 'PUT', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() })
|
|
2113
|
+
fetch(authUrl('/api/findings/' + encodeURIComponent(project)), { method: 'PUT', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() })
|
|
2100
2114
|
.then(function(r) { return r.json(); })
|
|
2101
2115
|
.then(function(d) {
|
|
2102
2116
|
if (d.ok) {
|
|
@@ -2122,7 +2136,7 @@ export function renderGraphScript() {
|
|
|
2122
2136
|
var body = new URLSearchParams();
|
|
2123
2137
|
body.set('text', findingText);
|
|
2124
2138
|
if (csrf) body.set('_csrf', csrf);
|
|
2125
|
-
fetch('/api/findings/' + encodeURIComponent(project), { method: 'DELETE', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() })
|
|
2139
|
+
fetch(authUrl('/api/findings/' + encodeURIComponent(project)), { method: 'DELETE', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() })
|
|
2126
2140
|
.then(function(r) { return r.json(); })
|
|
2127
2141
|
.then(function(d) {
|
|
2128
2142
|
if (d.ok) {
|
|
@@ -98,24 +98,11 @@ ${TASK_UI_STYLES}
|
|
|
98
98
|
</div>
|
|
99
99
|
</div>
|
|
100
100
|
<details class="review-help" style="margin-bottom:16px">
|
|
101
|
-
<summary>
|
|
102
|
-
<
|
|
103
|
-
<dt>What is the Review Queue?</dt>
|
|
104
|
-
<dd>Fragments flagged by governance for human review. Items accumulate here when <code>phren maintain govern</code> is run.</dd>
|
|
105
|
-
<dt>Can I approve, reject, or edit items here?</dt>
|
|
106
|
-
<dd>Yes. Each review card has <strong>Approve</strong>, <strong>Reject</strong>, and <strong>Edit</strong> buttons. Approve accepts the fragment, Reject removes it, and Edit lets you revise the text before accepting. You can also use batch actions to approve or reject multiple items at once.</dd>
|
|
107
|
-
<dt>How do I clear items?</dt>
|
|
108
|
-
<dd>Approve or reject items directly in the UI, or use maintenance flows such as <code>phren maintain prune</code>.</dd>
|
|
109
|
-
<dt>Is this automatic?</dt>
|
|
110
|
-
<dd>No. Agents do not auto-accept review-queue items.</dd>
|
|
111
|
-
<dt>How do items get here?</dt>
|
|
112
|
-
<dd><code>phren maintain govern</code> flags stale or low-confidence fragments for review.</dd>
|
|
113
|
-
<dt>How to reduce noise?</dt>
|
|
114
|
-
<dd>Run <code>phren maintain prune</code> to auto-remove expired items without manual review.</dd>
|
|
115
|
-
</dl>
|
|
101
|
+
<summary>How review works</summary>
|
|
102
|
+
<p style="margin-top:8px;font-size:var(--text-sm);color:var(--muted)">Items waiting for your review. Approve to keep, reject to remove. You can also edit the text before approving, or use the checkboxes to bulk approve or reject multiple items at once.</p>
|
|
116
103
|
</details>
|
|
117
104
|
|
|
118
|
-
<p style="font-size:var(--text-sm);color:var(--muted);margin-bottom:12px;letter-spacing:-0.01em">
|
|
105
|
+
<p style="font-size:var(--text-sm);color:var(--muted);margin-bottom:12px;letter-spacing:-0.01em">Items waiting for your review. Approve to keep, reject to remove.</p>
|
|
119
106
|
|
|
120
107
|
<div id="review-summary-banner" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;align-items:center"></div>
|
|
121
108
|
|
|
@@ -130,7 +117,18 @@ ${TASK_UI_STYLES}
|
|
|
130
117
|
<option value="">All models</option>
|
|
131
118
|
</select>
|
|
132
119
|
<span id="review-filter-count" class="text-muted" style="font-size:var(--text-sm);margin-left:8px"></span>
|
|
133
|
-
<
|
|
120
|
+
<label id="review-select-all" style="display:none;margin-left:auto;align-items:center;gap:6px;font-size:var(--text-sm);color:var(--muted);cursor:pointer;user-select:none">
|
|
121
|
+
<input type="checkbox" onchange="toggleSelectAll(this.checked)" style="width:14px;height:14px;cursor:pointer;accent-color:var(--accent)" />
|
|
122
|
+
Select all
|
|
123
|
+
</label>
|
|
124
|
+
<button class="btn btn-sm" id="highlight-only-btn">Flagged only</button>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div id="batch-bar" class="batch-bar">
|
|
128
|
+
<span id="batch-count" class="batch-bar-count"></span>
|
|
129
|
+
<button class="btn btn-sm btn-approve" onclick="batchAction('approve')">Approve selected</button>
|
|
130
|
+
<button class="btn btn-sm btn-reject" onclick="batchAction('reject')">Reject selected</button>
|
|
131
|
+
<button class="btn btn-sm" onclick="clearBatchSelection()">Clear</button>
|
|
134
132
|
</div>
|
|
135
133
|
|
|
136
134
|
<div id="review-kbd-hints" style="font-size:var(--text-xs);color:var(--muted);margin-bottom:12px;display:none;gap:16px;flex-wrap:wrap">
|
|
@@ -224,7 +222,7 @@ ${TASK_UI_STYLES}
|
|
|
224
222
|
<div style="padding:20px;color:var(--muted)">Loading...</div>
|
|
225
223
|
</div>
|
|
226
224
|
<div class="split-reader" id="hooks-reader">
|
|
227
|
-
<div class="reader-empty">Select a hook config to view its contents.</div>
|
|
225
|
+
<div class="reader-empty">Select a hook config to view its contents.<br/><span style="font-size:var(--text-sm);color:var(--muted);margin-top:8px;display:inline-block">Per-project hooks can also be configured in Settings > [project name].</span></div>
|
|
228
226
|
</div>
|
|
229
227
|
</div>
|
|
230
228
|
</div>
|
|
@@ -321,7 +319,7 @@ ${TASK_UI_STYLES}
|
|
|
321
319
|
</div>
|
|
322
320
|
|
|
323
321
|
<script${nonceAttr}>
|
|
324
|
-
${renderWebUiScript(
|
|
322
|
+
${renderWebUiScript(authToken || "")}
|
|
325
323
|
</script>
|
|
326
324
|
<script${nonceAttr}>
|
|
327
325
|
${renderGraphScript()}
|
|
@@ -330,7 +328,7 @@ ${renderGraphScript()}
|
|
|
330
328
|
${renderReviewQueueEditSyncScript()}
|
|
331
329
|
</script>
|
|
332
330
|
<script${nonceAttr}>
|
|
333
|
-
${renderSharedWebUiHelpers(
|
|
331
|
+
${renderSharedWebUiHelpers(authToken || "")}
|
|
334
332
|
</script>
|
|
335
333
|
<script${nonceAttr}>
|
|
336
334
|
${renderSkillUiEnhancementScript(h(authToken || ""))}
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* window._phrenFetchCsrfToken(cb) — fetch the CSRF token and call cb(token)
|
|
8
8
|
*/
|
|
9
9
|
export function renderSharedWebUiHelpers(authToken) {
|
|
10
|
+
const safeToken = JSON.stringify(authToken).slice(1, -1); // escape for JS string literal
|
|
10
11
|
return `(function() {
|
|
11
|
-
window._phrenAuthToken = '${
|
|
12
|
+
window._phrenAuthToken = '${safeToken}';
|
|
12
13
|
window._phrenEsc = function(s) {
|
|
13
14
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
14
15
|
};
|
|
@@ -604,6 +605,8 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
604
605
|
return fetch(url).then(function(r) { return r.json(); });
|
|
605
606
|
}
|
|
606
607
|
|
|
608
|
+
var _taskViewMode = 'list';
|
|
609
|
+
|
|
607
610
|
function priorityBadge(p) {
|
|
608
611
|
if (!p) return '';
|
|
609
612
|
var colors = { high: '#ef4444', medium: '#f59e0b', low: '#6b7280' };
|
|
@@ -611,6 +614,12 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
611
614
|
return '<span class="task-priority-badge task-priority-' + esc(p) + '">' + esc(p) + '</span>';
|
|
612
615
|
}
|
|
613
616
|
|
|
617
|
+
function sessionBadge(sessionId) {
|
|
618
|
+
if (!sessionId) return '';
|
|
619
|
+
var short = sessionId.length > 8 ? sessionId.slice(0, 8) : sessionId;
|
|
620
|
+
return '<span class="task-session-badge" title="Session ' + esc(sessionId) + '">' + esc(short) + '</span>';
|
|
621
|
+
}
|
|
622
|
+
|
|
614
623
|
function statusChip(section, checked) {
|
|
615
624
|
if (checked || section === 'Done') return '<span class="task-status-chip task-status-done" title="Done"><svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M3 8.5l3.5 3.5 6.5-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Done</span>';
|
|
616
625
|
if (section === 'Active') return '<span class="task-status-chip task-status-active" title="In Progress">In Progress</span>';
|
|
@@ -683,6 +692,7 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
683
692
|
};
|
|
684
693
|
|
|
685
694
|
window.toggleDoneSection = function(btn) {
|
|
695
|
+
if (!btn) return;
|
|
686
696
|
var list = btn.nextElementSibling;
|
|
687
697
|
var arrow = btn.querySelector('.task-toggle-arrow');
|
|
688
698
|
if (!list) return;
|
|
@@ -712,10 +722,10 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
712
722
|
});
|
|
713
723
|
var doneTasks = showDone ? [] : _allTasks.filter(function(t) {
|
|
714
724
|
if (projectFilter && t.project !== projectFilter) return false;
|
|
715
|
-
return t.section === 'Done';
|
|
725
|
+
return t.section === 'Done' || t.checked;
|
|
716
726
|
});
|
|
717
727
|
|
|
718
|
-
var activeCount = tasks.filter(function(t) { return t.section !== 'Done'; }).length;
|
|
728
|
+
var activeCount = tasks.filter(function(t) { return t.section !== 'Done' && !t.checked; }).length;
|
|
719
729
|
var countEl = document.getElementById('tasks-count');
|
|
720
730
|
if (countEl) countEl.textContent = activeCount + ' active' + (doneTasks.length ? ', ' + doneTasks.length + ' done' : '');
|
|
721
731
|
|
|
@@ -729,19 +739,26 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
729
739
|
// Group by priority within sections
|
|
730
740
|
var priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
731
741
|
function sortByPriority(a, b) {
|
|
732
|
-
|
|
733
|
-
var
|
|
742
|
+
// Unchecked before checked
|
|
743
|
+
var aChecked = a.checked || a.section === 'Done' ? 1 : 0;
|
|
744
|
+
var bChecked = b.checked || b.section === 'Done' ? 1 : 0;
|
|
745
|
+
if (aChecked !== bChecked) return aChecked - bChecked;
|
|
746
|
+
// Pinned first
|
|
734
747
|
if (a.pinned && !b.pinned) return -1;
|
|
735
748
|
if (!a.pinned && b.pinned) return 1;
|
|
749
|
+
// Then by priority
|
|
750
|
+
var pa = priorityOrder[a.priority] !== undefined ? priorityOrder[a.priority] : 1;
|
|
751
|
+
var pb = priorityOrder[b.priority] !== undefined ? priorityOrder[b.priority] : 1;
|
|
736
752
|
return pa - pb;
|
|
737
753
|
}
|
|
738
754
|
|
|
739
|
-
// Separate into priority groups
|
|
740
|
-
|
|
741
|
-
var
|
|
742
|
-
var
|
|
743
|
-
var
|
|
744
|
-
var
|
|
755
|
+
// Separate into priority groups (exclude checked tasks even if not in Done section)
|
|
756
|
+
function isActive(t) { return t.section !== 'Done' && !t.checked; }
|
|
757
|
+
var high = tasks.filter(function(t) { return t.priority === 'high' && isActive(t); }).sort(sortByPriority);
|
|
758
|
+
var medium = tasks.filter(function(t) { return t.priority === 'medium' && isActive(t); }).sort(sortByPriority);
|
|
759
|
+
var low = tasks.filter(function(t) { return t.priority === 'low' && isActive(t); }).sort(sortByPriority);
|
|
760
|
+
var noPriority = tasks.filter(function(t) { return !t.priority && isActive(t); }).sort(sortByPriority);
|
|
761
|
+
var doneVisible = tasks.filter(function(t) { return t.section === 'Done' || t.checked; });
|
|
745
762
|
|
|
746
763
|
function renderTaskCard(t) {
|
|
747
764
|
var borderClass = t.priority === 'high' ? ' task-card-high' : t.priority === 'medium' ? ' task-card-medium' : t.priority === 'low' ? ' task-card-low' : '';
|
|
@@ -753,16 +770,17 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
753
770
|
html += pinIndicator(t.pinned);
|
|
754
771
|
html += githubBadge(t.githubIssue, t.githubUrl);
|
|
755
772
|
html += priorityBadge(t.priority);
|
|
773
|
+
html += sessionBadge(t.sessionId);
|
|
756
774
|
html += '</div>';
|
|
757
775
|
html += '<div class="task-card-body">';
|
|
758
776
|
html += '<span class="task-card-text">' + esc(t.line) + '</span>';
|
|
759
777
|
if (t.context) html += '<span class="task-card-context">' + esc(t.context) + '</span>';
|
|
760
778
|
html += '</div>';
|
|
761
779
|
html += '<div class="task-card-actions">';
|
|
762
|
-
if (t.section !== 'Done') {
|
|
763
|
-
html += '<button class="task-done-btn"
|
|
780
|
+
if (t.section !== 'Done' && !t.checked) {
|
|
781
|
+
html += '<button class="task-done-btn" data-ts-action="completeTask" data-project="' + esc(t.project) + '" data-item="' + esc(t.line) + '" title="Mark done"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M3 8.5l3.5 3.5 6.5-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Done</button>';
|
|
764
782
|
}
|
|
765
|
-
html += '<button class="task-remove-btn"
|
|
783
|
+
html += '<button class="task-remove-btn" data-ts-action="removeTask" data-project="' + esc(t.project) + '" data-item="' + esc(t.line) + '" title="Delete task" style="background:none;border:1px solid var(--border);border-radius:var(--radius-sm);padding:2px 8px;cursor:pointer;color:var(--muted);font-size:var(--text-xs)"><svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>';
|
|
766
784
|
html += '</div>';
|
|
767
785
|
html += '</div>';
|
|
768
786
|
return html;
|
|
@@ -770,21 +788,46 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
770
788
|
|
|
771
789
|
var html = '';
|
|
772
790
|
|
|
773
|
-
//
|
|
774
|
-
var
|
|
791
|
+
// Summary bar
|
|
792
|
+
var allActive = _allTasks.filter(function(t) { return t.section !== 'Done' && !t.checked; });
|
|
793
|
+
var highCount = allActive.filter(function(t) { return t.priority === 'high'; }).length;
|
|
794
|
+
var medCount = allActive.filter(function(t) { return t.priority === 'medium'; }).length;
|
|
795
|
+
var lowCount = allActive.filter(function(t) { return t.priority === 'low'; }).length;
|
|
796
|
+
var projectCounts = {};
|
|
797
|
+
allActive.forEach(function(t) { projectCounts[t.project] = (projectCounts[t.project] || 0) + 1; });
|
|
798
|
+
var topProjects = Object.keys(projectCounts).sort(function(a, b) { return projectCounts[b] - projectCounts[a]; }).slice(0, 3);
|
|
799
|
+
html += '<div class="task-summary-bar">';
|
|
800
|
+
html += '<span class="task-summary-total">' + allActive.length + ' active</span>';
|
|
801
|
+
if (highCount) html += '<span class="task-summary-pill task-summary-high">' + highCount + ' high</span>';
|
|
802
|
+
if (medCount) html += '<span class="task-summary-pill task-summary-medium">' + medCount + ' medium</span>';
|
|
803
|
+
if (lowCount) html += '<span class="task-summary-pill task-summary-low">' + lowCount + ' low</span>';
|
|
804
|
+
if (topProjects.length) {
|
|
805
|
+
html += '<span class="task-summary-projects">';
|
|
806
|
+
topProjects.forEach(function(p) { html += '<span class="task-summary-project">' + esc(p) + ' (' + projectCounts[p] + ')</span>'; });
|
|
807
|
+
html += '</span>';
|
|
808
|
+
}
|
|
809
|
+
html += '<span class="task-view-toggle" style="margin-left:auto">';
|
|
810
|
+
html += '<button class="task-view-btn' + (_taskViewMode === 'list' ? ' active' : '') + '" data-ts-action="setTaskView" data-mode="list" title="List view"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M2 4h12M2 8h12M2 12h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></button>';
|
|
811
|
+
html += '<button class="task-view-btn' + (_taskViewMode === 'compact' ? ' active' : '') + '" data-ts-action="setTaskView" data-mode="compact" title="Compact view"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><rect x="1" y="2" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="9" y="2" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="1" y="9" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="9" y="9" width="6" height="5" rx="1" stroke="currentColor" stroke-width="1.2"/></svg></button>';
|
|
812
|
+
html += '</span>';
|
|
813
|
+
html += '</div>';
|
|
814
|
+
|
|
815
|
+
// Add task input at top (only when a specific project is selected)
|
|
816
|
+
var projects = projectFilter ? [projectFilter] : [];
|
|
775
817
|
projects.forEach(function(proj) {
|
|
776
818
|
html += '<div class="task-add-bar">';
|
|
777
|
-
html += '<input id="task-add-input-' + esc(proj) + '" type="text" class="task-add-input" placeholder="Add a task to ' + esc(proj) + '\u2026"
|
|
778
|
-
html += '<button class="task-add-btn"
|
|
819
|
+
html += '<input id="task-add-input-' + esc(proj) + '" type="text" class="task-add-input" placeholder="Add a task to ' + esc(proj) + '\u2026" data-ts-action="addTaskKeydown" data-project="' + esc(proj) + '">';
|
|
820
|
+
html += '<button class="task-add-btn" data-ts-action="addTask" data-project="' + esc(proj) + '"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> Add</button>';
|
|
779
821
|
html += '</div>';
|
|
780
822
|
});
|
|
781
823
|
|
|
782
824
|
// Priority sections
|
|
783
825
|
function renderSection(title, items, icon) {
|
|
784
826
|
if (!items.length) return '';
|
|
827
|
+
var gridClass = _taskViewMode === 'compact' ? 'task-card-grid task-card-grid-compact' : 'task-card-grid';
|
|
785
828
|
var shtml = '<div class="task-priority-section">';
|
|
786
829
|
shtml += '<div class="task-section-header"><span class="task-section-icon">' + icon + '</span> ' + title + ' <span class="task-section-count">' + items.length + '</span></div>';
|
|
787
|
-
shtml += '<div class="
|
|
830
|
+
shtml += '<div class="' + gridClass + '">';
|
|
788
831
|
items.forEach(function(t) { shtml += renderTaskCard(t); });
|
|
789
832
|
shtml += '</div></div>';
|
|
790
833
|
return shtml;
|
|
@@ -801,7 +844,7 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
801
844
|
var allDone = showDone ? doneVisible : doneTasks;
|
|
802
845
|
if (allDone.length) {
|
|
803
846
|
html += '<div class="task-done-section">';
|
|
804
|
-
html += '<button class="task-done-toggle"
|
|
847
|
+
html += '<button class="task-done-toggle" data-ts-action="toggleDoneSection">';
|
|
805
848
|
html += '<span class="task-toggle-arrow">\u25B6</span> Completed <span class="task-section-count">' + allDone.length + '</span></button>';
|
|
806
849
|
html += '<div class="task-done-list" style="display:none">';
|
|
807
850
|
html += '<div class="task-card-grid">';
|
|
@@ -1221,7 +1264,12 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1221
1264
|
var actionEl = target.closest('[data-ts-action]');
|
|
1222
1265
|
if (!actionEl) return;
|
|
1223
1266
|
var action = actionEl.getAttribute('data-ts-action');
|
|
1224
|
-
if (action === '
|
|
1267
|
+
if (action === 'toggleDoneSection') { toggleDoneSection(actionEl); }
|
|
1268
|
+
else if (action === 'completeTask') { completeTaskFromUi(actionEl.getAttribute('data-project'), actionEl.getAttribute('data-item')); }
|
|
1269
|
+
else if (action === 'removeTask') { removeTaskFromUi(actionEl.getAttribute('data-project'), actionEl.getAttribute('data-item')); }
|
|
1270
|
+
else if (action === 'addTask') { addTaskFromUi(actionEl.getAttribute('data-project')); }
|
|
1271
|
+
else if (action === 'setTaskView') { _taskViewMode = actionEl.getAttribute('data-mode') || 'list'; filterTasks(); }
|
|
1272
|
+
else if (action === 'setFindingSensitivity') { setFindingSensitivity(actionEl.getAttribute('data-level')); }
|
|
1225
1273
|
else if (action === 'toggleAutoCapture') { setAutoCapture(actionEl.getAttribute('data-enabled') !== 'true'); }
|
|
1226
1274
|
else if (action === 'setTaskMode') { setTaskMode(actionEl.getAttribute('data-mode')); }
|
|
1227
1275
|
else if (action === 'setProactivity') { setProactivity(actionEl.getAttribute('data-level')); }
|
|
@@ -1258,6 +1306,15 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1258
1306
|
}
|
|
1259
1307
|
});
|
|
1260
1308
|
|
|
1309
|
+
// Keydown delegation for add-task inputs (Enter key)
|
|
1310
|
+
document.addEventListener('keydown', function(e) {
|
|
1311
|
+
var target = e.target;
|
|
1312
|
+
if (!target || !target.getAttribute) return;
|
|
1313
|
+
if (target.getAttribute('data-ts-action') === 'addTaskKeydown' && e.key === 'Enter') {
|
|
1314
|
+
addTaskFromUi(target.getAttribute('data-project'));
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1261
1318
|
window.setFindingSensitivity = function(level) {
|
|
1262
1319
|
var descriptions = {
|
|
1263
1320
|
high: 'Capture findings proactively, including minor observations.',
|
|
@@ -7,12 +7,12 @@ import * as querystring from "querystring";
|
|
|
7
7
|
import { spawn, execFileSync } from "child_process";
|
|
8
8
|
import { computePhrenLiveStateToken, getProjectDirs, } from "./shared.js";
|
|
9
9
|
import { editFinding, readReviewQueue, removeFinding, readFindings, addFinding as addFindingStore, readTasksAcrossProjects, addTask as addTaskStore, completeTask as completeTaskStore, removeTask as removeTaskStore, TASKS_FILENAME, } from "./data-access.js";
|
|
10
|
-
import { isValidProjectName, errorMessage, queueFilePath } from "./utils.js";
|
|
10
|
+
import { isValidProjectName, errorMessage, queueFilePath, safeProjectPath } from "./utils.js";
|
|
11
11
|
import { readInstallPreferences, writeInstallPreferences, writeGovernanceInstallPreferences } from "./init-preferences.js";
|
|
12
12
|
import { buildGraph, collectProjectsForUI, collectSkillsForUI, getHooksData, isAllowedFilePath, readSyncSnapshot, recentAccepted, recentUsage, } from "./memory-ui-data.js";
|
|
13
13
|
import { CONSOLIDATION_ENTRY_THRESHOLD } from "./content-validate.js";
|
|
14
14
|
import { ensureTopicReferenceDoc, getProjectTopicsResponse, listProjectReferenceDocs, pinProjectTopicSuggestion, readReferenceContent, reclassifyLegacyTopicDocs, unpinProjectTopicSuggestion, writeProjectTopics, } from "./project-topics.js";
|
|
15
|
-
import { getWorkflowPolicy, updateWorkflowPolicy, mergeConfig, getRetentionPolicy, getProjectConfigOverrides } from "./governance-policy.js";
|
|
15
|
+
import { getWorkflowPolicy, updateWorkflowPolicy, mergeConfig, getRetentionPolicy, getProjectConfigOverrides, VALID_TASK_MODES } from "./governance-policy.js";
|
|
16
16
|
import { updateProjectConfigOverrides } from "./project-config.js";
|
|
17
17
|
import { findSkill } from "./skill-registry.js";
|
|
18
18
|
import { setSkillEnabledAndSync } from "./skill-files.js";
|
|
@@ -193,6 +193,8 @@ function readFormBody(req, res) {
|
|
|
193
193
|
req.on("data", (chunk) => {
|
|
194
194
|
received += chunk.length;
|
|
195
195
|
if (received > MAX_FORM_BODY_BYTES) {
|
|
196
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
197
|
+
res.end(JSON.stringify({ ok: false, error: "Request body too large" }));
|
|
196
198
|
req.destroy();
|
|
197
199
|
resolve(null);
|
|
198
200
|
return;
|
|
@@ -508,7 +510,12 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
508
510
|
res.end(JSON.stringify({ ok: false, error: `File not allowed: ${file}` }));
|
|
509
511
|
return;
|
|
510
512
|
}
|
|
511
|
-
const filePath =
|
|
513
|
+
const filePath = safeProjectPath(phrenPath, project, file);
|
|
514
|
+
if (!filePath) {
|
|
515
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
516
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid project or file path" }));
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
512
519
|
if (!fs.existsSync(filePath)) {
|
|
513
520
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
514
521
|
res.end(JSON.stringify({ ok: false, error: `File not found: ${file}` }));
|
|
@@ -847,6 +854,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
847
854
|
githubUrl: item.githubUrl,
|
|
848
855
|
context: item.context,
|
|
849
856
|
checked: item.checked,
|
|
857
|
+
sessionId: item.sessionId,
|
|
850
858
|
});
|
|
851
859
|
}
|
|
852
860
|
}
|
|
@@ -1024,7 +1032,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
1024
1032
|
if (!requireCsrf(res, parsed, csrfTokens, true))
|
|
1025
1033
|
return;
|
|
1026
1034
|
const value = String(parsed.value || "").trim().toLowerCase();
|
|
1027
|
-
const valid =
|
|
1035
|
+
const valid = VALID_TASK_MODES;
|
|
1028
1036
|
if (!valid.includes(value)) {
|
|
1029
1037
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
1030
1038
|
res.end(JSON.stringify({ ok: false, error: `Invalid task mode: "${value}". Must be one of: ${valid.join(", ")}` }));
|
|
@@ -1161,7 +1169,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
1161
1169
|
});
|
|
1162
1170
|
const merged = mergeConfig(phrenPath, project);
|
|
1163
1171
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
1164
|
-
res.end(JSON.stringify({ ok: true, config: merged }));
|
|
1172
|
+
res.end(JSON.stringify({ ok: true, config: merged, ...(registrationWarning ? { warning: registrationWarning } : {}) }));
|
|
1165
1173
|
}
|
|
1166
1174
|
catch (err) {
|
|
1167
1175
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
@@ -552,4 +552,106 @@ export const TASK_UI_STYLES = `
|
|
|
552
552
|
transition: transform 0.2s;
|
|
553
553
|
}
|
|
554
554
|
.task-done-list { padding-top: 8px; }
|
|
555
|
+
|
|
556
|
+
/* ── Task Summary Bar ──────────────────────────── */
|
|
557
|
+
.task-summary-bar {
|
|
558
|
+
display: flex;
|
|
559
|
+
align-items: center;
|
|
560
|
+
gap: 10px;
|
|
561
|
+
padding: 10px 14px;
|
|
562
|
+
background: var(--surface-sunken, var(--surface));
|
|
563
|
+
border: 1px solid var(--border);
|
|
564
|
+
border-radius: var(--radius);
|
|
565
|
+
margin-bottom: 16px;
|
|
566
|
+
flex-wrap: wrap;
|
|
567
|
+
font-size: var(--text-sm);
|
|
568
|
+
}
|
|
569
|
+
.task-summary-total {
|
|
570
|
+
font-weight: 600;
|
|
571
|
+
color: var(--ink);
|
|
572
|
+
font-size: var(--text-base);
|
|
573
|
+
}
|
|
574
|
+
.task-summary-pill {
|
|
575
|
+
display: inline-flex;
|
|
576
|
+
align-items: center;
|
|
577
|
+
padding: 2px 8px;
|
|
578
|
+
border-radius: 999px;
|
|
579
|
+
font-size: 11px;
|
|
580
|
+
font-weight: 600;
|
|
581
|
+
}
|
|
582
|
+
.task-summary-high { background: #ef444422; color: #ef4444; }
|
|
583
|
+
.task-summary-medium { background: #f59e0b22; color: #f59e0b; }
|
|
584
|
+
.task-summary-low { background: #6b728022; color: #6b7280; }
|
|
585
|
+
.task-summary-projects {
|
|
586
|
+
display: flex;
|
|
587
|
+
gap: 6px;
|
|
588
|
+
align-items: center;
|
|
589
|
+
}
|
|
590
|
+
.task-summary-project {
|
|
591
|
+
font-size: 11px;
|
|
592
|
+
color: var(--muted);
|
|
593
|
+
padding: 1px 6px;
|
|
594
|
+
border: 1px solid var(--border);
|
|
595
|
+
border-radius: var(--radius-sm);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/* ── Task Session Badge ──────────────────────────── */
|
|
599
|
+
.task-session-badge {
|
|
600
|
+
display: inline-block;
|
|
601
|
+
padding: 1px 6px;
|
|
602
|
+
border-radius: var(--radius-sm);
|
|
603
|
+
font-size: 10px;
|
|
604
|
+
font-family: var(--mono, monospace);
|
|
605
|
+
background: var(--surface-sunken, var(--surface));
|
|
606
|
+
color: var(--muted);
|
|
607
|
+
border: 1px solid var(--border);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/* ── Task View Toggle ──────────────────────────── */
|
|
611
|
+
.task-view-toggle {
|
|
612
|
+
display: flex;
|
|
613
|
+
gap: 2px;
|
|
614
|
+
border: 1px solid var(--border);
|
|
615
|
+
border-radius: var(--radius-sm);
|
|
616
|
+
overflow: hidden;
|
|
617
|
+
}
|
|
618
|
+
.task-view-btn {
|
|
619
|
+
display: flex;
|
|
620
|
+
align-items: center;
|
|
621
|
+
justify-content: center;
|
|
622
|
+
width: 30px;
|
|
623
|
+
height: 28px;
|
|
624
|
+
background: var(--surface);
|
|
625
|
+
border: none;
|
|
626
|
+
color: var(--muted);
|
|
627
|
+
cursor: pointer;
|
|
628
|
+
transition: background 0.15s, color 0.15s;
|
|
629
|
+
}
|
|
630
|
+
.task-view-btn:hover { color: var(--ink); }
|
|
631
|
+
.task-view-btn.active {
|
|
632
|
+
background: var(--accent-dim);
|
|
633
|
+
color: var(--accent);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/* ── Task Compact Grid ──────────────────────────── */
|
|
637
|
+
.task-card-grid-compact {
|
|
638
|
+
display: grid !important;
|
|
639
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
640
|
+
gap: 8px;
|
|
641
|
+
}
|
|
642
|
+
.task-card-grid-compact .task-card {
|
|
643
|
+
padding: 10px 12px;
|
|
644
|
+
}
|
|
645
|
+
.task-card-grid-compact .task-card-body {
|
|
646
|
+
gap: 2px;
|
|
647
|
+
}
|
|
648
|
+
.task-card-grid-compact .task-card-text {
|
|
649
|
+
font-size: var(--text-sm);
|
|
650
|
+
display: -webkit-box;
|
|
651
|
+
-webkit-line-clamp: 2;
|
|
652
|
+
-webkit-box-orient: vertical;
|
|
653
|
+
overflow: hidden;
|
|
654
|
+
}
|
|
655
|
+
.task-card-grid-compact .task-card-context { display: none; }
|
|
656
|
+
.task-card-grid-compact .task-card-actions { margin-top: 6px; }
|
|
555
657
|
`;
|
package/mcp/dist/phren-paths.js
CHANGED
|
@@ -305,6 +305,23 @@ export function findProjectNameCaseInsensitive(phrenPath, name) {
|
|
|
305
305
|
}
|
|
306
306
|
return null;
|
|
307
307
|
}
|
|
308
|
+
export function findArchivedProjectNameCaseInsensitive(phrenPath, name) {
|
|
309
|
+
const needle = name.toLowerCase();
|
|
310
|
+
try {
|
|
311
|
+
for (const entry of fs.readdirSync(phrenPath, { withFileTypes: true })) {
|
|
312
|
+
if (!entry.isDirectory() || !entry.name.endsWith(".archived"))
|
|
313
|
+
continue;
|
|
314
|
+
const archivedName = entry.name.slice(0, -".archived".length);
|
|
315
|
+
if (archivedName.toLowerCase() === needle)
|
|
316
|
+
return archivedName;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
if ((process.env.PHREN_DEBUG))
|
|
321
|
+
process.stderr.write(`[phren] findArchivedProjectNameCaseInsensitive: ${errorMessage(err)}\n`);
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
308
325
|
function getLocalProjectDirs(phrenPath, manifest) {
|
|
309
326
|
const primaryProject = manifest.primaryProject;
|
|
310
327
|
if (!primaryProject || !isValidProjectName(primaryProject))
|
package/mcp/dist/shared.js
CHANGED
|
@@ -4,7 +4,7 @@ import { debugLog, runtimeFile } from "./phren-paths.js";
|
|
|
4
4
|
import { errorMessage } from "./utils.js";
|
|
5
5
|
export { HOOK_TOOL_NAMES, hookConfigPath } from "./provider-adapters.js";
|
|
6
6
|
export { EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, PhrenError, phrenOk, phrenErr, forwardErr, parsePhrenErrorCode, isRecord, withDefaults, FINDING_TYPES, FINDING_TAGS, KNOWN_OBSERVATION_TAGS, DOC_TYPES, capCache, RESERVED_PROJECT_DIR_NAMES, } from "./phren-core.js";
|
|
7
|
-
export { ROOT_MANIFEST_FILENAME, homeDir, homePath, expandHomePath, defaultPhrenPath, rootManifestPath, readRootManifest, writeRootManifest, resolveInstallContext, findNearestPhrenPath, isProjectLocalMode, runtimeDir, tryUnlink, sessionsDir, runtimeFile, installPreferencesFile, runtimeHealthFile, shellStateFile, sessionMetricsFile, memoryScoresFile, memoryUsageLogFile, sessionMarker, debugLog, appendIndexEvent, resolveFindingsPath, findPhrenPath, ensurePhrenPath, findPhrenPathWithArg, normalizeProjectNameForCreate, findProjectNameCaseInsensitive, getProjectDirs, collectNativeMemoryFiles, computePhrenLiveStateToken, getPhrenPath, qualityMarkers, atomicWriteText, } from "./phren-paths.js";
|
|
7
|
+
export { ROOT_MANIFEST_FILENAME, homeDir, homePath, expandHomePath, defaultPhrenPath, rootManifestPath, readRootManifest, writeRootManifest, resolveInstallContext, findNearestPhrenPath, isProjectLocalMode, runtimeDir, tryUnlink, sessionsDir, runtimeFile, installPreferencesFile, runtimeHealthFile, shellStateFile, sessionMetricsFile, memoryScoresFile, memoryUsageLogFile, sessionMarker, debugLog, appendIndexEvent, resolveFindingsPath, findPhrenPath, ensurePhrenPath, findPhrenPathWithArg, normalizeProjectNameForCreate, findProjectNameCaseInsensitive, findArchivedProjectNameCaseInsensitive, getProjectDirs, collectNativeMemoryFiles, computePhrenLiveStateToken, getPhrenPath, qualityMarkers, atomicWriteText, } from "./phren-paths.js";
|
|
8
8
|
export { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForFindings, getProactivityLevelForTask, hasExplicitFindingSignal, hasExplicitTaskSignal, hasExecutionIntent, hasDiscoveryIntent, shouldAutoCaptureFindingsForLevel, shouldAutoCaptureTaskForLevel, } from "./proactivity.js";
|
|
9
9
|
const MEMORY_SCOPE_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
10
10
|
export function normalizeMemoryScope(scope) {
|
|
@@ -3,6 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import { getProjectDirs } from "./shared.js";
|
|
4
4
|
import { parseSkillFrontmatter } from "./link-skills.js";
|
|
5
5
|
import { isSkillEnabled } from "./skill-state.js";
|
|
6
|
+
import { safeProjectPath } from "./utils.js";
|
|
6
7
|
function normalizeCommand(raw, fallbackName) {
|
|
7
8
|
const value = typeof raw === "string" && raw.trim() ? raw.trim() : `/${fallbackName}`;
|
|
8
9
|
return value.startsWith("/") ? value : `/${value}`;
|
|
@@ -28,13 +29,35 @@ function collectSkills(phrenPath, root, sourceLabel, scopeType, sourceKind, seen
|
|
|
28
29
|
if (!fs.existsSync(root))
|
|
29
30
|
return [];
|
|
30
31
|
const results = [];
|
|
32
|
+
const realRoot = (() => {
|
|
33
|
+
try {
|
|
34
|
+
return fs.realpathSync(root);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return path.resolve(root);
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
31
40
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
32
41
|
const isFolder = entry.isDirectory();
|
|
33
|
-
const
|
|
42
|
+
const candidatePath = isFolder
|
|
34
43
|
? path.join(root, entry.name, "SKILL.md")
|
|
35
44
|
: entry.name.endsWith(".md") ? path.join(root, entry.name) : null;
|
|
36
|
-
if (!
|
|
45
|
+
if (!candidatePath)
|
|
46
|
+
continue;
|
|
47
|
+
const safeCandidate = safeProjectPath(root, path.relative(root, candidatePath));
|
|
48
|
+
if (!safeCandidate || seen.has(safeCandidate) || !fs.existsSync(safeCandidate))
|
|
37
49
|
continue;
|
|
50
|
+
try {
|
|
51
|
+
if (fs.lstatSync(safeCandidate).isSymbolicLink())
|
|
52
|
+
continue;
|
|
53
|
+
const realCandidate = fs.realpathSync(safeCandidate);
|
|
54
|
+
if (realCandidate !== realRoot && !realCandidate.startsWith(realRoot + path.sep))
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const filePath = safeCandidate;
|
|
38
61
|
seen.add(filePath);
|
|
39
62
|
const name = isFolder ? entry.name : entry.name.replace(/\.md$/, "");
|
|
40
63
|
const { frontmatter } = parseSkillFrontmatter(fs.readFileSync(filePath, "utf8"));
|