@phren/cli 0.0.14 → 0.0.16
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-data.js +24 -2
- package/mcp/dist/memory-ui-graph.js +78 -38
- package/mcp/dist/memory-ui-page.js +2 -2
- package/mcp/dist/memory-ui-scripts.js +39 -19
- package/mcp/dist/memory-ui-server.js +117 -5
- 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
|
@@ -6,6 +6,7 @@ import { errorMessage } from "./utils.js";
|
|
|
6
6
|
import { readInstallPreferences } from "./init-preferences.js";
|
|
7
7
|
import { readCustomHooks } from "./hooks.js";
|
|
8
8
|
import { hookConfigPaths, hookConfigRoots } from "./provider-adapters.js";
|
|
9
|
+
import { readProjectConfig, isProjectHookEnabled, PROJECT_HOOK_EVENTS } from "./project-config.js";
|
|
9
10
|
import { getAllSkills } from "./skill-registry.js";
|
|
10
11
|
import { resolveTaskFilePath, readTasks, TASKS_FILENAME } from "./data-tasks.js";
|
|
11
12
|
import { buildIndex, queryDocBySourceKey, queryRows } from "./shared-index.js";
|
|
@@ -101,7 +102,7 @@ export function collectSkillsForUI(phrenPath, profile = "") {
|
|
|
101
102
|
enabled: skill.enabled,
|
|
102
103
|
}));
|
|
103
104
|
}
|
|
104
|
-
export function getHooksData(phrenPath) {
|
|
105
|
+
export function getHooksData(phrenPath, profile) {
|
|
105
106
|
const prefs = readInstallPreferences(phrenPath);
|
|
106
107
|
const globalEnabled = prefs.hooksEnabled !== false;
|
|
107
108
|
const toolPrefs = (prefs.hookTools && typeof prefs.hookTools === "object") ? prefs.hookTools : {};
|
|
@@ -112,7 +113,28 @@ export function getHooksData(phrenPath) {
|
|
|
112
113
|
configPath: paths[tool],
|
|
113
114
|
exists: fs.existsSync(paths[tool]),
|
|
114
115
|
}));
|
|
115
|
-
|
|
116
|
+
// Collect per-project hook overrides
|
|
117
|
+
const projectOverrides = [];
|
|
118
|
+
const projects = getProjectDirs(phrenPath, profile)
|
|
119
|
+
.map((dir) => path.basename(dir))
|
|
120
|
+
.filter((p) => p !== "global");
|
|
121
|
+
for (const project of projects) {
|
|
122
|
+
const config = readProjectConfig(phrenPath, project);
|
|
123
|
+
const hasOverrides = typeof config.hooks?.enabled === "boolean" ||
|
|
124
|
+
PROJECT_HOOK_EVENTS.some((ev) => typeof config.hooks?.[ev] === "boolean");
|
|
125
|
+
if (!hasOverrides)
|
|
126
|
+
continue;
|
|
127
|
+
projectOverrides.push({
|
|
128
|
+
project,
|
|
129
|
+
baseEnabled: typeof config.hooks?.enabled === "boolean" ? config.hooks.enabled : null,
|
|
130
|
+
events: PROJECT_HOOK_EVENTS.map((event) => ({
|
|
131
|
+
event,
|
|
132
|
+
configured: typeof config.hooks?.[event] === "boolean" ? config.hooks[event] : null,
|
|
133
|
+
enabled: isProjectHookEnabled(phrenPath, project, event, config),
|
|
134
|
+
})),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return { globalEnabled, tools, customHooks: readCustomHooks(phrenPath), projectOverrides };
|
|
116
138
|
}
|
|
117
139
|
export async function buildGraph(phrenPath, profile, focusProject) {
|
|
118
140
|
const projects = getProjectDirs(phrenPath, profile).map((projectDir) => path.basename(projectDir)).filter((project) => project !== "global");
|
|
@@ -164,27 +164,30 @@ export function renderGraphScript() {
|
|
|
164
164
|
return path;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
/* pin
|
|
168
|
-
var
|
|
167
|
+
/* pin nodes so the force simulation skips them (prevents jitter while phren walks) */
|
|
168
|
+
var phrenPinnedNodes = [];
|
|
169
169
|
|
|
170
170
|
function phrenPinNode(node) {
|
|
171
|
-
if (
|
|
172
|
-
/* unpin previous target — let physics resume on it */
|
|
173
|
-
phrenPinnedNode._phrenPinned = false;
|
|
174
|
-
phrenPinnedNode.vx = 0;
|
|
175
|
-
phrenPinnedNode.vy = 0;
|
|
176
|
-
}
|
|
177
|
-
if (node) {
|
|
171
|
+
if (node && !node._phrenPinned) {
|
|
178
172
|
node._phrenPinned = true;
|
|
179
173
|
node.vx = 0;
|
|
180
174
|
node.vy = 0;
|
|
175
|
+
phrenPinnedNodes.push(node);
|
|
181
176
|
}
|
|
182
|
-
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function phrenUnpinAll() {
|
|
180
|
+
for (var i = 0; i < phrenPinnedNodes.length; i++) {
|
|
181
|
+
phrenPinnedNodes[i]._phrenPinned = false;
|
|
182
|
+
phrenPinnedNodes[i].vx = 0;
|
|
183
|
+
phrenPinnedNodes[i].vy = 0;
|
|
184
|
+
}
|
|
185
|
+
phrenPinnedNodes = [];
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
function phrenMoveTo(x, y, targetNode) {
|
|
186
|
-
/*
|
|
187
|
-
|
|
189
|
+
/* unpin all previously pinned nodes before starting a new move */
|
|
190
|
+
phrenUnpinAll();
|
|
188
191
|
phren.targetX = x;
|
|
189
192
|
phren.targetY = y;
|
|
190
193
|
phren.moving = true;
|
|
@@ -199,6 +202,12 @@ export function renderGraphScript() {
|
|
|
199
202
|
phren.waypointIdx = 0;
|
|
200
203
|
phren.targetNodeId = targetNode ? targetNode.id : null;
|
|
201
204
|
phren.targetNodeRef = targetNode || null; /* live reference for position tracking */
|
|
205
|
+
/* pin target node and all waypoint nodes so physics doesn't move them during walk */
|
|
206
|
+
phrenPinNode(targetNode);
|
|
207
|
+
for (var i = 0; i < phren.waypoints.length; i++) {
|
|
208
|
+
var wpn = phrenNodeById(phren.waypoints[i]);
|
|
209
|
+
if (wpn) phrenPinNode(wpn);
|
|
210
|
+
}
|
|
202
211
|
/* ensure animation loop is running so phren movement renders */
|
|
203
212
|
if (!animFrame) animFrame = requestAnimationFrame(tick);
|
|
204
213
|
}
|
|
@@ -238,16 +247,45 @@ export function renderGraphScript() {
|
|
|
238
247
|
var dx = wx - phren.x;
|
|
239
248
|
var dy = wy - phren.y;
|
|
240
249
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
|
241
|
-
|
|
250
|
+
/* snap-to-target helper — used both by arrival check and overshoot clamp */
|
|
251
|
+
var snapped = false;
|
|
252
|
+
if (dist < 4) {
|
|
242
253
|
phren.x = wx;
|
|
243
254
|
phren.y = wy;
|
|
244
|
-
|
|
255
|
+
snapped = true;
|
|
256
|
+
} else {
|
|
257
|
+
/* ease-in-out via sine curve over the full trip distance */
|
|
258
|
+
var t = phren.tripDist > 0 ? Math.min(1, phren.tripProgress / phren.tripDist) : 1;
|
|
259
|
+
var easeInOut = 0.5 - 0.5 * Math.cos(Math.PI * t);
|
|
260
|
+
var baseSpeed = Math.max(3, phren.tripDist * 0.12);
|
|
261
|
+
var speed = Math.max(1.5, baseSpeed * (0.15 + 0.85 * easeInOut));
|
|
262
|
+
/* clamp speed to remaining distance — prevents overshoot oscillation */
|
|
263
|
+
if (speed >= dist) {
|
|
264
|
+
phren.x = wx;
|
|
265
|
+
phren.y = wy;
|
|
266
|
+
speed = dist;
|
|
267
|
+
snapped = true;
|
|
268
|
+
} else {
|
|
269
|
+
phren.x += (dx / dist) * speed;
|
|
270
|
+
phren.y += (dy / dist) * speed;
|
|
271
|
+
}
|
|
272
|
+
phren.tripProgress += speed;
|
|
273
|
+
/* record trail — longer buffer for gradual fade */
|
|
274
|
+
phren.trailPoints.push({ x: phren.x, y: phren.y, age: 0 });
|
|
275
|
+
if (phren.trailPoints.length > 50) phren.trailPoints.shift();
|
|
276
|
+
}
|
|
277
|
+
/* advance waypoint or finish when snapped to current target */
|
|
278
|
+
if (snapped) {
|
|
245
279
|
if (phren.waypoints.length > 0 && phren.waypointIdx < phren.waypoints.length) {
|
|
246
280
|
phren.waypointIdx++;
|
|
247
281
|
} else {
|
|
248
282
|
phren.moving = false;
|
|
249
283
|
phren.arriving = true;
|
|
250
284
|
phren.arriveTimer = 0;
|
|
285
|
+
/* clear trail on arrival so purple line doesn't linger */
|
|
286
|
+
phren.trailPoints = [];
|
|
287
|
+
/* unpin all nodes now that phren has arrived */
|
|
288
|
+
phrenUnpinAll();
|
|
251
289
|
/* update keyboard-nav current node */
|
|
252
290
|
if (phren.targetNodeId) {
|
|
253
291
|
phrenCurrentNodeId = phren.targetNodeId;
|
|
@@ -255,18 +293,6 @@ export function renderGraphScript() {
|
|
|
255
293
|
phrenRefreshAdjacentLinks();
|
|
256
294
|
}
|
|
257
295
|
}
|
|
258
|
-
} else {
|
|
259
|
-
/* ease-in-out via sine curve over the full trip distance */
|
|
260
|
-
var t = phren.tripDist > 0 ? Math.min(1, phren.tripProgress / phren.tripDist) : 1;
|
|
261
|
-
var easeInOut = 0.5 - 0.5 * Math.cos(Math.PI * t);
|
|
262
|
-
var baseSpeed = Math.max(3, phren.tripDist * 0.12);
|
|
263
|
-
var speed = Math.max(1.5, baseSpeed * (0.15 + 0.85 * easeInOut));
|
|
264
|
-
phren.x += (dx / dist) * speed;
|
|
265
|
-
phren.y += (dy / dist) * speed;
|
|
266
|
-
phren.tripProgress += speed;
|
|
267
|
-
/* record trail — longer buffer for gradual fade */
|
|
268
|
-
phren.trailPoints.push({ x: phren.x, y: phren.y, age: 0 });
|
|
269
|
-
if (phren.trailPoints.length > 50) phren.trailPoints.shift();
|
|
270
296
|
}
|
|
271
297
|
}
|
|
272
298
|
if (phren.arriving) {
|
|
@@ -312,10 +338,10 @@ export function renderGraphScript() {
|
|
|
312
338
|
/* sprite rendering */
|
|
313
339
|
if (phrenImgReady) {
|
|
314
340
|
ctx.save();
|
|
315
|
-
/* fixed
|
|
316
|
-
var spriteScreenSize =
|
|
341
|
+
/* fixed 48px size in screen pixels, scale up to 56px on arrival flash */
|
|
342
|
+
var spriteScreenSize = 48;
|
|
317
343
|
if (phren.arriving && phren.arriveTimer < 0.4) {
|
|
318
|
-
spriteScreenSize =
|
|
344
|
+
spriteScreenSize = 48 + 8 * (1 - phren.arriveTimer / 0.4);
|
|
319
345
|
}
|
|
320
346
|
var spriteSize = spriteScreenSize * s; /* convert to graph coords */
|
|
321
347
|
/* bob up/down when walking — sine wave synced to walk progress */
|
|
@@ -1428,8 +1454,8 @@ export function renderGraphScript() {
|
|
|
1428
1454
|
if (healthText) html += '<div style="margin-top:4px">' + healthText + '</div>';
|
|
1429
1455
|
if (node.project) {
|
|
1430
1456
|
html += '<div style="display:flex;gap:8px;margin-top:12px">';
|
|
1431
|
-
html += '<button class="btn btn-sm"
|
|
1432
|
-
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>';
|
|
1433
1459
|
html += '</div>';
|
|
1434
1460
|
}
|
|
1435
1461
|
|
|
@@ -1481,8 +1507,8 @@ export function renderGraphScript() {
|
|
|
1481
1507
|
html += '<div style="margin-top:8px;padding:8px 10px;border-radius:6px;border:1px solid var(--border);background:var(--surface-alt,var(--surface))">';
|
|
1482
1508
|
html += '<div style="font-size:12px;line-height:1.5;color:var(--ink)">' + esc(lfText) + '</div>';
|
|
1483
1509
|
html += '<div style="display:flex;gap:6px;margin-top:6px">';
|
|
1484
|
-
html += '<button class="btn btn-sm"
|
|
1485
|
-
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>';
|
|
1486
1512
|
html += '</div></div>';
|
|
1487
1513
|
}
|
|
1488
1514
|
}
|
|
@@ -2030,11 +2056,25 @@ export function renderGraphScript() {
|
|
|
2030
2056
|
renderGraphDetails(null);
|
|
2031
2057
|
};
|
|
2032
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
|
+
|
|
2033
2072
|
/* ── node edit/delete actions ────────────────────────────────────────── */
|
|
2034
2073
|
|
|
2035
|
-
function
|
|
2036
|
-
|
|
2037
|
-
|
|
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
|
+
};
|
|
2038
2078
|
|
|
2039
2079
|
window.graphNodeEdit = function(project, findingText) {
|
|
2040
2080
|
var panel = document.getElementById('graph-detail-panel');
|
|
@@ -2070,7 +2110,7 @@ export function renderGraphScript() {
|
|
|
2070
2110
|
body.set('old_text', findingText);
|
|
2071
2111
|
body.set('new_text', newText);
|
|
2072
2112
|
if (csrf) body.set('_csrf', csrf);
|
|
2073
|
-
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() })
|
|
2074
2114
|
.then(function(r) { return r.json(); })
|
|
2075
2115
|
.then(function(d) {
|
|
2076
2116
|
if (d.ok) {
|
|
@@ -2096,7 +2136,7 @@ export function renderGraphScript() {
|
|
|
2096
2136
|
var body = new URLSearchParams();
|
|
2097
2137
|
body.set('text', findingText);
|
|
2098
2138
|
if (csrf) body.set('_csrf', csrf);
|
|
2099
|
-
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() })
|
|
2100
2140
|
.then(function(r) { return r.json(); })
|
|
2101
2141
|
.then(function(d) {
|
|
2102
2142
|
if (d.ok) {
|
|
@@ -321,7 +321,7 @@ ${TASK_UI_STYLES}
|
|
|
321
321
|
</div>
|
|
322
322
|
|
|
323
323
|
<script${nonceAttr}>
|
|
324
|
-
${renderWebUiScript(
|
|
324
|
+
${renderWebUiScript(authToken || "")}
|
|
325
325
|
</script>
|
|
326
326
|
<script${nonceAttr}>
|
|
327
327
|
${renderGraphScript()}
|
|
@@ -330,7 +330,7 @@ ${renderGraphScript()}
|
|
|
330
330
|
${renderReviewQueueEditSyncScript()}
|
|
331
331
|
</script>
|
|
332
332
|
<script${nonceAttr}>
|
|
333
|
-
${renderSharedWebUiHelpers(
|
|
333
|
+
${renderSharedWebUiHelpers(authToken || "")}
|
|
334
334
|
</script>
|
|
335
335
|
<script${nonceAttr}>
|
|
336
336
|
${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
|
};
|
|
@@ -600,6 +601,10 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
600
601
|
return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(_tsAuthToken);
|
|
601
602
|
}
|
|
602
603
|
|
|
604
|
+
function loadJson(url) {
|
|
605
|
+
return fetch(url).then(function(r) { return r.json(); });
|
|
606
|
+
}
|
|
607
|
+
|
|
603
608
|
function priorityBadge(p) {
|
|
604
609
|
if (!p) return '';
|
|
605
610
|
var colors = { high: '#ef4444', medium: '#f59e0b', low: '#6b7280' };
|
|
@@ -679,6 +684,7 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
679
684
|
};
|
|
680
685
|
|
|
681
686
|
window.toggleDoneSection = function(btn) {
|
|
687
|
+
if (!btn) return;
|
|
682
688
|
var list = btn.nextElementSibling;
|
|
683
689
|
var arrow = btn.querySelector('.task-toggle-arrow');
|
|
684
690
|
if (!list) return;
|
|
@@ -708,10 +714,10 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
708
714
|
});
|
|
709
715
|
var doneTasks = showDone ? [] : _allTasks.filter(function(t) {
|
|
710
716
|
if (projectFilter && t.project !== projectFilter) return false;
|
|
711
|
-
return t.section === 'Done';
|
|
717
|
+
return t.section === 'Done' || t.checked;
|
|
712
718
|
});
|
|
713
719
|
|
|
714
|
-
var activeCount = tasks.filter(function(t) { return t.section !== 'Done'; }).length;
|
|
720
|
+
var activeCount = tasks.filter(function(t) { return t.section !== 'Done' && !t.checked; }).length;
|
|
715
721
|
var countEl = document.getElementById('tasks-count');
|
|
716
722
|
if (countEl) countEl.textContent = activeCount + ' active' + (doneTasks.length ? ', ' + doneTasks.length + ' done' : '');
|
|
717
723
|
|
|
@@ -732,12 +738,13 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
732
738
|
return pa - pb;
|
|
733
739
|
}
|
|
734
740
|
|
|
735
|
-
// Separate into priority groups
|
|
736
|
-
|
|
737
|
-
var
|
|
738
|
-
var
|
|
739
|
-
var
|
|
740
|
-
var
|
|
741
|
+
// Separate into priority groups (exclude checked tasks even if not in Done section)
|
|
742
|
+
function isActive(t) { return t.section !== 'Done' && !t.checked; }
|
|
743
|
+
var high = tasks.filter(function(t) { return t.priority === 'high' && isActive(t); }).sort(sortByPriority);
|
|
744
|
+
var medium = tasks.filter(function(t) { return t.priority === 'medium' && isActive(t); }).sort(sortByPriority);
|
|
745
|
+
var low = tasks.filter(function(t) { return t.priority === 'low' && isActive(t); }).sort(sortByPriority);
|
|
746
|
+
var noPriority = tasks.filter(function(t) { return !t.priority && isActive(t); }).sort(sortByPriority);
|
|
747
|
+
var doneVisible = tasks.filter(function(t) { return t.section === 'Done' || t.checked; });
|
|
741
748
|
|
|
742
749
|
function renderTaskCard(t) {
|
|
743
750
|
var borderClass = t.priority === 'high' ? ' task-card-high' : t.priority === 'medium' ? ' task-card-medium' : t.priority === 'low' ? ' task-card-low' : '';
|
|
@@ -755,10 +762,10 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
755
762
|
if (t.context) html += '<span class="task-card-context">' + esc(t.context) + '</span>';
|
|
756
763
|
html += '</div>';
|
|
757
764
|
html += '<div class="task-card-actions">';
|
|
758
|
-
if (t.section !== 'Done') {
|
|
759
|
-
html += '<button class="task-done-btn"
|
|
765
|
+
if (t.section !== 'Done' && !t.checked) {
|
|
766
|
+
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>';
|
|
760
767
|
}
|
|
761
|
-
html += '<button class="task-remove-btn"
|
|
768
|
+
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>';
|
|
762
769
|
html += '</div>';
|
|
763
770
|
html += '</div>';
|
|
764
771
|
return html;
|
|
@@ -766,12 +773,12 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
766
773
|
|
|
767
774
|
var html = '';
|
|
768
775
|
|
|
769
|
-
// Add task input at top
|
|
770
|
-
var projects = projectFilter ? [projectFilter] :
|
|
776
|
+
// Add task input at top (only when a specific project is selected)
|
|
777
|
+
var projects = projectFilter ? [projectFilter] : [];
|
|
771
778
|
projects.forEach(function(proj) {
|
|
772
779
|
html += '<div class="task-add-bar">';
|
|
773
|
-
html += '<input id="task-add-input-' + esc(proj) + '" type="text" class="task-add-input" placeholder="Add a task to ' + esc(proj) + '\u2026"
|
|
774
|
-
html += '<button class="task-add-btn"
|
|
780
|
+
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) + '">';
|
|
781
|
+
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>';
|
|
775
782
|
html += '</div>';
|
|
776
783
|
});
|
|
777
784
|
|
|
@@ -797,7 +804,7 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
797
804
|
var allDone = showDone ? doneVisible : doneTasks;
|
|
798
805
|
if (allDone.length) {
|
|
799
806
|
html += '<div class="task-done-section">';
|
|
800
|
-
html += '<button class="task-done-toggle"
|
|
807
|
+
html += '<button class="task-done-toggle" data-ts-action="toggleDoneSection">';
|
|
801
808
|
html += '<span class="task-toggle-arrow">\u25B6</span> Completed <span class="task-section-count">' + allDone.length + '</span></button>';
|
|
802
809
|
html += '<div class="task-done-list" style="display:none">';
|
|
803
810
|
html += '<div class="task-card-grid">';
|
|
@@ -897,7 +904,7 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
897
904
|
var scopeNote = document.getElementById('settings-scope-note');
|
|
898
905
|
if (scopeNote) {
|
|
899
906
|
scopeNote.textContent = selectedProject
|
|
900
|
-
? 'Showing effective config for "' + selectedProject + '". Overrides are saved to that project
|
|
907
|
+
? 'Showing effective config for "' + selectedProject + '". Overrides are saved to that project\\\'s phren.project.yaml.'
|
|
901
908
|
: 'Showing global settings. Select a project to view and edit per-project overrides.';
|
|
902
909
|
}
|
|
903
910
|
|
|
@@ -1217,7 +1224,11 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1217
1224
|
var actionEl = target.closest('[data-ts-action]');
|
|
1218
1225
|
if (!actionEl) return;
|
|
1219
1226
|
var action = actionEl.getAttribute('data-ts-action');
|
|
1220
|
-
if (action === '
|
|
1227
|
+
if (action === 'toggleDoneSection') { toggleDoneSection(actionEl); }
|
|
1228
|
+
else if (action === 'completeTask') { completeTaskFromUi(actionEl.getAttribute('data-project'), actionEl.getAttribute('data-item')); }
|
|
1229
|
+
else if (action === 'removeTask') { removeTaskFromUi(actionEl.getAttribute('data-project'), actionEl.getAttribute('data-item')); }
|
|
1230
|
+
else if (action === 'addTask') { addTaskFromUi(actionEl.getAttribute('data-project')); }
|
|
1231
|
+
else if (action === 'setFindingSensitivity') { setFindingSensitivity(actionEl.getAttribute('data-level')); }
|
|
1221
1232
|
else if (action === 'toggleAutoCapture') { setAutoCapture(actionEl.getAttribute('data-enabled') !== 'true'); }
|
|
1222
1233
|
else if (action === 'setTaskMode') { setTaskMode(actionEl.getAttribute('data-mode')); }
|
|
1223
1234
|
else if (action === 'setProactivity') { setProactivity(actionEl.getAttribute('data-level')); }
|
|
@@ -1254,6 +1265,15 @@ export function renderTasksAndSettingsScript(authToken) {
|
|
|
1254
1265
|
}
|
|
1255
1266
|
});
|
|
1256
1267
|
|
|
1268
|
+
// Keydown delegation for add-task inputs (Enter key)
|
|
1269
|
+
document.addEventListener('keydown', function(e) {
|
|
1270
|
+
var target = e.target;
|
|
1271
|
+
if (!target || !target.getAttribute) return;
|
|
1272
|
+
if (target.getAttribute('data-ts-action') === 'addTaskKeydown' && e.key === 'Enter') {
|
|
1273
|
+
addTaskFromUi(target.getAttribute('data-project'));
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1257
1277
|
window.setFindingSensitivity = function(level) {
|
|
1258
1278
|
var descriptions = {
|
|
1259
1279
|
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 } 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;
|
|
@@ -388,6 +390,111 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
388
390
|
}));
|
|
389
391
|
return;
|
|
390
392
|
}
|
|
393
|
+
// POST /api/approve — remove item from review queue (keep finding)
|
|
394
|
+
if (req.method === "POST" && pathname === "/api/approve") {
|
|
395
|
+
void readFormBody(req, res).then((parsed) => {
|
|
396
|
+
if (!parsed)
|
|
397
|
+
return;
|
|
398
|
+
if (!requirePostAuth(req, res, url, parsed, authToken, true))
|
|
399
|
+
return;
|
|
400
|
+
if (!requireCsrf(res, parsed, csrfTokens, true))
|
|
401
|
+
return;
|
|
402
|
+
const project = String(parsed.project || "");
|
|
403
|
+
const line = String(parsed.line || "");
|
|
404
|
+
if (!project || !isValidProjectName(project) || !line) {
|
|
405
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
406
|
+
res.end(JSON.stringify({ ok: false, error: "Missing project or line" }));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const qPath = queueFilePath(phrenPath, project);
|
|
411
|
+
if (fs.existsSync(qPath)) {
|
|
412
|
+
const content = fs.readFileSync(qPath, "utf8");
|
|
413
|
+
const lines = content.split("\n");
|
|
414
|
+
const filtered = lines.filter((l) => l.trim() !== line.trim());
|
|
415
|
+
fs.writeFileSync(qPath, filtered.join("\n"));
|
|
416
|
+
}
|
|
417
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
418
|
+
res.end(JSON.stringify({ ok: true }));
|
|
419
|
+
}
|
|
420
|
+
catch (err) {
|
|
421
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
422
|
+
res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// POST /api/reject — remove item from review queue AND remove finding
|
|
428
|
+
if (req.method === "POST" && pathname === "/api/reject") {
|
|
429
|
+
void readFormBody(req, res).then((parsed) => {
|
|
430
|
+
if (!parsed)
|
|
431
|
+
return;
|
|
432
|
+
if (!requirePostAuth(req, res, url, parsed, authToken, true))
|
|
433
|
+
return;
|
|
434
|
+
if (!requireCsrf(res, parsed, csrfTokens, true))
|
|
435
|
+
return;
|
|
436
|
+
const project = String(parsed.project || "");
|
|
437
|
+
const line = String(parsed.line || "");
|
|
438
|
+
if (!project || !isValidProjectName(project) || !line) {
|
|
439
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
440
|
+
res.end(JSON.stringify({ ok: false, error: "Missing project or line" }));
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
// Remove from review queue
|
|
445
|
+
const qPath = queueFilePath(phrenPath, project);
|
|
446
|
+
if (fs.existsSync(qPath)) {
|
|
447
|
+
const content = fs.readFileSync(qPath, "utf8");
|
|
448
|
+
const lines = content.split("\n");
|
|
449
|
+
const filtered = lines.filter((l) => l.trim() !== line.trim());
|
|
450
|
+
fs.writeFileSync(qPath, filtered.join("\n"));
|
|
451
|
+
}
|
|
452
|
+
// Also remove the finding from FINDINGS.md
|
|
453
|
+
// Extract text from the line (strip "- " prefix and inline metadata)
|
|
454
|
+
const findingText = line.replace(/^-\s*/, "").replace(/<!--.*?-->/g, "").trim();
|
|
455
|
+
if (findingText) {
|
|
456
|
+
removeFinding(phrenPath, project, findingText);
|
|
457
|
+
}
|
|
458
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
459
|
+
res.end(JSON.stringify({ ok: true }));
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
463
|
+
res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// POST /api/edit — edit a finding's text
|
|
469
|
+
if (req.method === "POST" && pathname === "/api/edit") {
|
|
470
|
+
void readFormBody(req, res).then((parsed) => {
|
|
471
|
+
if (!parsed)
|
|
472
|
+
return;
|
|
473
|
+
if (!requirePostAuth(req, res, url, parsed, authToken, true))
|
|
474
|
+
return;
|
|
475
|
+
if (!requireCsrf(res, parsed, csrfTokens, true))
|
|
476
|
+
return;
|
|
477
|
+
const project = String(parsed.project || "");
|
|
478
|
+
const line = String(parsed.line || "");
|
|
479
|
+
const newText = String(parsed.new_text || "");
|
|
480
|
+
if (!project || !isValidProjectName(project) || !line || !newText) {
|
|
481
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
482
|
+
res.end(JSON.stringify({ ok: false, error: "Missing project, line, or new_text" }));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const oldText = line.replace(/^-\s*/, "").replace(/<!--.*?-->/g, "").trim();
|
|
487
|
+
const result = editFinding(phrenPath, project, oldText, newText);
|
|
488
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
489
|
+
res.end(JSON.stringify({ ok: result.ok, error: result.ok ? undefined : result.error }));
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
493
|
+
res.end(JSON.stringify({ ok: false, error: errorMessage(err) }));
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
391
498
|
if (req.method === "GET" && pathname.startsWith("/api/project-content")) {
|
|
392
499
|
const qs = url.includes("?") ? querystring.parse(url.slice(url.indexOf("?") + 1)) : {};
|
|
393
500
|
const project = String(qs.project || "");
|
|
@@ -403,7 +510,12 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
403
510
|
res.end(JSON.stringify({ ok: false, error: `File not allowed: ${file}` }));
|
|
404
511
|
return;
|
|
405
512
|
}
|
|
406
|
-
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
|
+
}
|
|
407
519
|
if (!fs.existsSync(filePath)) {
|
|
408
520
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
409
521
|
res.end(JSON.stringify({ ok: false, error: `File not found: ${file}` }));
|
|
@@ -919,7 +1031,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
919
1031
|
if (!requireCsrf(res, parsed, csrfTokens, true))
|
|
920
1032
|
return;
|
|
921
1033
|
const value = String(parsed.value || "").trim().toLowerCase();
|
|
922
|
-
const valid =
|
|
1034
|
+
const valid = VALID_TASK_MODES;
|
|
923
1035
|
if (!valid.includes(value)) {
|
|
924
1036
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
925
1037
|
res.end(JSON.stringify({ ok: false, error: `Invalid task mode: "${value}". Must be one of: ${valid.join(", ")}` }));
|
|
@@ -1056,7 +1168,7 @@ export function createWebUiHttpServer(phrenPath, renderPage, profile, opts) {
|
|
|
1056
1168
|
});
|
|
1057
1169
|
const merged = mergeConfig(phrenPath, project);
|
|
1058
1170
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
|
1059
|
-
res.end(JSON.stringify({ ok: true, config: merged }));
|
|
1171
|
+
res.end(JSON.stringify({ ok: true, config: merged, ...(registrationWarning ? { warning: registrationWarning } : {}) }));
|
|
1060
1172
|
}
|
|
1061
1173
|
catch (err) {
|
|
1062
1174
|
res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
|
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) {
|