@phren/cli 0.0.5 → 0.0.7
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/README.md +3 -3
- package/mcp/dist/cli-hooks-session.js +9 -3
- package/mcp/dist/cli-hooks.js +27 -9
- package/mcp/dist/content-citation.js +24 -3
- package/mcp/dist/content-learning.js +28 -5
- package/mcp/dist/data-access.js +96 -53
- package/mcp/dist/finding-impact.js +23 -0
- package/mcp/dist/finding-lifecycle.js +56 -29
- package/mcp/dist/governance-locks.js +11 -6
- package/mcp/dist/index.js +2 -1
- package/mcp/dist/init-preferences.js +18 -3
- package/mcp/dist/init.js +11 -0
- package/mcp/dist/mcp-config.js +0 -8
- package/mcp/dist/mcp-data.js +22 -3
- package/mcp/dist/mcp-extract.js +1 -0
- package/mcp/dist/mcp-finding.js +1 -1
- package/mcp/dist/mcp-hooks.js +36 -16
- package/mcp/dist/mcp-memory.js +0 -1
- package/mcp/dist/mcp-ops.js +5 -10
- package/mcp/dist/mcp-search.js +7 -1
- package/mcp/dist/mcp-session.js +66 -6
- package/mcp/dist/mcp-skills.js +5 -2
- package/mcp/dist/mcp-tasks.js +7 -4
- package/mcp/dist/memory-ui-assets.js +2 -2
- package/mcp/dist/memory-ui-data.js +7 -7
- package/mcp/dist/memory-ui-graph.js +178 -23
- package/mcp/dist/project-config.js +37 -18
- package/mcp/dist/shared-content.js +1 -1
- package/mcp/dist/shared-index.js +16 -3
- package/mcp/dist/shared-retrieval.js +64 -34
- package/mcp/dist/shared.js +1 -10
- package/mcp/dist/test-global-setup.js +3 -4
- package/package.json +2 -2
|
@@ -8,7 +8,7 @@ import { readCustomHooks } from "./hooks.js";
|
|
|
8
8
|
import { hookConfigPaths, hookConfigRoots } from "./provider-adapters.js";
|
|
9
9
|
import { getAllSkills } from "./skill-registry.js";
|
|
10
10
|
import { resolveTaskFilePath, readTasks, TASKS_FILENAME } from "./data-tasks.js";
|
|
11
|
-
import { buildIndex, queryRows } from "./shared-index.js";
|
|
11
|
+
import { buildIndex, queryDocBySourceKey, queryRows } from "./shared-index.js";
|
|
12
12
|
import { readProjectTopics, classifyTopicForText } from "./project-topics.js";
|
|
13
13
|
import { entryScoreKey } from "./governance-scores.js";
|
|
14
14
|
function extractGithubUrl(content) {
|
|
@@ -271,10 +271,9 @@ export async function buildGraph(phrenPath, profile, focusProject) {
|
|
|
271
271
|
const rows = queryRows(db, `SELECT e.id, e.name, e.type, COUNT(DISTINCT el.source_doc) as ref_count
|
|
272
272
|
FROM entities e JOIN entity_links el ON el.target_id = e.id WHERE e.type != 'document'
|
|
273
273
|
GROUP BY e.id, e.name, e.type ORDER BY ref_count DESC LIMIT 500`, []);
|
|
274
|
-
const refRows = queryRows(db, `SELECT e.id, el.source_doc
|
|
274
|
+
const refRows = queryRows(db, `SELECT e.id, el.source_doc
|
|
275
275
|
FROM entities e
|
|
276
276
|
JOIN entity_links el ON el.target_id = e.id
|
|
277
|
-
LEFT JOIN docs d ON d.source_key = el.source_doc
|
|
278
277
|
WHERE e.type != 'document'`, []);
|
|
279
278
|
const refsByEntity = new Map();
|
|
280
279
|
const seenEntityDoc = new Set();
|
|
@@ -291,8 +290,9 @@ export async function buildGraph(phrenPath, profile, focusProject) {
|
|
|
291
290
|
continue;
|
|
292
291
|
seenEntityDoc.add(entityDocKey);
|
|
293
292
|
const project = projectFromSourceDoc(doc);
|
|
294
|
-
const
|
|
295
|
-
const
|
|
293
|
+
const docRow = queryDocBySourceKey(db, phrenPath, doc);
|
|
294
|
+
const content = docRow?.content ?? "";
|
|
295
|
+
const filename = docRow?.filename ?? "";
|
|
296
296
|
const scoreKey = project && filename && content ? entryScoreKey(project, filename, content) : undefined;
|
|
297
297
|
const refs = refsByEntity.get(entityId) ?? [];
|
|
298
298
|
refs.push({ doc, project, scoreKey });
|
|
@@ -445,7 +445,7 @@ export function collectProjectsForUI(phrenPath, profile) {
|
|
|
445
445
|
}
|
|
446
446
|
}
|
|
447
447
|
catch (err) {
|
|
448
|
-
if (
|
|
448
|
+
if (process.env.PHREN_DEBUG)
|
|
449
449
|
process.stderr.write(`[phren] memory-ui filterByProfile: ${errorMessage(err)}\n`);
|
|
450
450
|
}
|
|
451
451
|
const results = [];
|
|
@@ -461,7 +461,7 @@ export function collectProjectsForUI(phrenPath, profile) {
|
|
|
461
461
|
let findingCount = 0;
|
|
462
462
|
if (fs.existsSync(findingsPath)) {
|
|
463
463
|
const content = fs.readFileSync(findingsPath, "utf8");
|
|
464
|
-
findingCount = (content.match(/^-
|
|
464
|
+
findingCount = (content.match(/^- /gm) || []).length;
|
|
465
465
|
}
|
|
466
466
|
const sparkline = new Array(8).fill(0);
|
|
467
467
|
if (fs.existsSync(findingsPath)) {
|
|
@@ -50,7 +50,10 @@ export function renderGraphScript() {
|
|
|
50
50
|
trailPoints: [], /* fading trail behind movement */
|
|
51
51
|
initialized: false,
|
|
52
52
|
waypoints: [], /* edge-walk waypoints [{x,y}] */
|
|
53
|
-
waypointIdx: 0
|
|
53
|
+
waypointIdx: 0, /* current waypoint index */
|
|
54
|
+
tripDist: 0, /* total distance of current trip (for ease-in-out) */
|
|
55
|
+
tripProgress: 0, /* distance traveled so far this trip */
|
|
56
|
+
targetNodeId: null /* id of destination node (set in phrenMoveTo) */
|
|
54
57
|
};
|
|
55
58
|
|
|
56
59
|
/* ── phren sprite image ─────────────────────────────────────────────── */
|
|
@@ -74,6 +77,18 @@ export function renderGraphScript() {
|
|
|
74
77
|
phren.targetX = phren.x;
|
|
75
78
|
phren.targetY = phren.y;
|
|
76
79
|
phren.initialized = true;
|
|
80
|
+
/* set initial current node to nearest visible node */
|
|
81
|
+
var _initNearest = null, _initNearestD = Infinity;
|
|
82
|
+
for (var _ni = 0; _ni < nodes.length; _ni++) {
|
|
83
|
+
var _nnd = nodes[_ni];
|
|
84
|
+
var _ndx = _nnd.x - phren.x, _ndy = _nnd.y - phren.y;
|
|
85
|
+
var _ndd = _ndx * _ndx + _ndy * _ndy;
|
|
86
|
+
if (_ndd < _initNearestD) { _initNearestD = _ndd; _initNearest = _nnd; }
|
|
87
|
+
}
|
|
88
|
+
if (_initNearest) {
|
|
89
|
+
phrenCurrentNodeId = _initNearest.id;
|
|
90
|
+
phrenRefreshAdjacentLinks();
|
|
91
|
+
}
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
/* find shortest edge path from nearest node to target node via BFS */
|
|
@@ -140,13 +155,31 @@ export function renderGraphScript() {
|
|
|
140
155
|
phren.moving = true;
|
|
141
156
|
phren.arriving = false;
|
|
142
157
|
phren.trailPoints = [{ x: phren.x, y: phren.y, age: 0 }];
|
|
158
|
+
/* record trip distance for ease-in-out interpolation */
|
|
159
|
+
var tdx = x - phren.x, tdy = y - phren.y;
|
|
160
|
+
phren.tripDist = Math.sqrt(tdx * tdx + tdy * tdy);
|
|
161
|
+
phren.tripProgress = 0;
|
|
143
162
|
/* try edge-walking if a target node is given */
|
|
144
163
|
phren.waypoints = targetNode ? phrenFindEdgePath(targetNode) : [];
|
|
145
164
|
phren.waypointIdx = 0;
|
|
165
|
+
phren.targetNodeId = targetNode ? targetNode.id : null;
|
|
146
166
|
/* ensure animation loop is running so phren movement renders */
|
|
147
167
|
if (!animFrame) animFrame = requestAnimationFrame(tick);
|
|
148
168
|
}
|
|
149
169
|
|
|
170
|
+
function phrenRefreshAdjacentLinks() {
|
|
171
|
+
phrenAdjacentLinks = [];
|
|
172
|
+
if (!phrenCurrentNodeId) return;
|
|
173
|
+
for (var i = 0; i < visibleLinks.length; i++) {
|
|
174
|
+
var lk = visibleLinks[i];
|
|
175
|
+
var sid = lk._source ? lk._source.id : (lk.source && typeof lk.source === 'object' ? lk.source.id : lk.source);
|
|
176
|
+
var tid = lk._target ? lk._target.id : (lk.target && typeof lk.target === 'object' ? lk.target.id : lk.target);
|
|
177
|
+
if (sid === phrenCurrentNodeId || tid === phrenCurrentNodeId) {
|
|
178
|
+
phrenAdjacentLinks.push(lk);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
150
183
|
function phrenUpdate(dt) {
|
|
151
184
|
phren.idlePhase += dt;
|
|
152
185
|
if (phren.moving) {
|
|
@@ -172,25 +205,35 @@ export function renderGraphScript() {
|
|
|
172
205
|
phren.moving = false;
|
|
173
206
|
phren.arriving = true;
|
|
174
207
|
phren.arriveTimer = 0;
|
|
208
|
+
/* update keyboard-nav current node */
|
|
209
|
+
if (phren.targetNodeId) {
|
|
210
|
+
phrenCurrentNodeId = phren.targetNodeId;
|
|
211
|
+
phrenSelectedEdgeIdx = -1;
|
|
212
|
+
phrenRefreshAdjacentLinks();
|
|
213
|
+
}
|
|
175
214
|
}
|
|
176
215
|
} else {
|
|
177
|
-
/* ease-out
|
|
178
|
-
var
|
|
216
|
+
/* ease-in-out via sine curve over the full trip distance */
|
|
217
|
+
var t = phren.tripDist > 0 ? Math.min(1, phren.tripProgress / phren.tripDist) : 1;
|
|
218
|
+
var easeInOut = 0.5 - 0.5 * Math.cos(Math.PI * t);
|
|
219
|
+
var baseSpeed = Math.max(3, phren.tripDist * 0.12);
|
|
220
|
+
var speed = Math.max(1.5, baseSpeed * (0.15 + 0.85 * easeInOut));
|
|
179
221
|
phren.x += (dx / dist) * speed;
|
|
180
222
|
phren.y += (dy / dist) * speed;
|
|
181
|
-
|
|
223
|
+
phren.tripProgress += speed;
|
|
224
|
+
/* record trail — longer buffer for gradual fade */
|
|
182
225
|
phren.trailPoints.push({ x: phren.x, y: phren.y, age: 0 });
|
|
183
|
-
if (phren.trailPoints.length >
|
|
226
|
+
if (phren.trailPoints.length > 50) phren.trailPoints.shift();
|
|
184
227
|
}
|
|
185
228
|
}
|
|
186
229
|
if (phren.arriving) {
|
|
187
230
|
phren.arriveTimer += dt;
|
|
188
|
-
if (phren.arriveTimer > 0
|
|
231
|
+
if (phren.arriveTimer > 1.0) phren.arriving = false;
|
|
189
232
|
}
|
|
190
|
-
/* age trail points */
|
|
233
|
+
/* age trail points — 2s lifetime for a longer, gradual fade */
|
|
191
234
|
for (var i = phren.trailPoints.length - 1; i >= 0; i--) {
|
|
192
235
|
phren.trailPoints[i].age += dt;
|
|
193
|
-
if (phren.trailPoints[i].age >
|
|
236
|
+
if (phren.trailPoints[i].age > 2.0) phren.trailPoints.splice(i, 1);
|
|
194
237
|
}
|
|
195
238
|
}
|
|
196
239
|
|
|
@@ -199,15 +242,16 @@ export function renderGraphScript() {
|
|
|
199
242
|
var px = phren.x, py = phren.y;
|
|
200
243
|
var s = 1 / scale; /* unit size in graph coords */
|
|
201
244
|
|
|
202
|
-
/* trail — purple tinted */
|
|
245
|
+
/* trail — purple tinted, 2s fade for a longer gradual tail */
|
|
203
246
|
if (phren.trailPoints.length > 1) {
|
|
204
247
|
for (var i = 1; i < phren.trailPoints.length; i++) {
|
|
205
248
|
var pt = phren.trailPoints[i];
|
|
206
249
|
var prev = phren.trailPoints[i - 1];
|
|
207
|
-
var
|
|
250
|
+
var fadeT = 1 - pt.age / 2.0;
|
|
251
|
+
var alpha = Math.max(0, 0.38 * fadeT * fadeT); /* quadratic for softer tail */
|
|
208
252
|
ctx.beginPath();
|
|
209
253
|
ctx.strokeStyle = 'rgba(123,104,174,' + alpha + ')';
|
|
210
|
-
ctx.lineWidth = (
|
|
254
|
+
ctx.lineWidth = (3.0 * fadeT) * s;
|
|
211
255
|
ctx.moveTo(prev.x, prev.y);
|
|
212
256
|
ctx.lineTo(pt.x, pt.y);
|
|
213
257
|
ctx.stroke();
|
|
@@ -231,16 +275,33 @@ export function renderGraphScript() {
|
|
|
231
275
|
spriteScreenSize = 48 + 8 * (1 - phren.arriveTimer / 0.4);
|
|
232
276
|
}
|
|
233
277
|
var spriteSize = spriteScreenSize * s; /* convert to graph coords */
|
|
234
|
-
/* bob up/down
|
|
235
|
-
var
|
|
278
|
+
/* bob up/down when walking — sine wave synced to walk progress */
|
|
279
|
+
var walkPhase = phren.tripDist > 0
|
|
280
|
+
? (phren.tripProgress / phren.tripDist) * Math.PI * 6
|
|
281
|
+
: phren.idlePhase * 8;
|
|
282
|
+
var bobOffset = phren.moving ? Math.sin(walkPhase) * 2.5 * s : 0;
|
|
283
|
+
/* bounce on arrival — damped overshoot then settle */
|
|
284
|
+
var bounceOffset = (phren.arriving && phren.arriveTimer < 0.55)
|
|
285
|
+
? -3 * Math.sin(phren.arriveTimer * Math.PI * 4.5) * Math.exp(-phren.arriveTimer * 8) * s
|
|
286
|
+
: 0;
|
|
287
|
+
/* idle breathing — gentle scale pulse (3s loop) when resting */
|
|
288
|
+
var idleScale = (!phren.moving && !phren.arriving)
|
|
289
|
+
? 1.0 + 0.02 * Math.sin(phren.idlePhase * (2 * Math.PI / 3))
|
|
290
|
+
: 1.0;
|
|
291
|
+
var totalYOffset = bobOffset + bounceOffset;
|
|
236
292
|
/* crisp pixel art — no smoothing */
|
|
237
293
|
ctx.imageSmoothingEnabled = false;
|
|
238
|
-
|
|
294
|
+
if (idleScale !== 1.0) {
|
|
295
|
+
ctx.translate(px, py);
|
|
296
|
+
ctx.scale(idleScale, idleScale);
|
|
297
|
+
ctx.translate(-px, -py);
|
|
298
|
+
}
|
|
299
|
+
ctx.drawImage(phrenImg, px - spriteSize / 2, py - spriteSize / 2 + totalYOffset, spriteSize, spriteSize);
|
|
239
300
|
/* arrival flash: cyan glow ring */
|
|
240
|
-
if (phren.arriving && phren.arriveTimer < 0.
|
|
241
|
-
var ringAlpha = 0.6 * (1 - phren.arriveTimer / 0.
|
|
301
|
+
if (phren.arriving && phren.arriveTimer < 0.6) {
|
|
302
|
+
var ringAlpha = 0.6 * (1 - phren.arriveTimer / 0.6);
|
|
242
303
|
ctx.beginPath();
|
|
243
|
-
ctx.arc(px, py +
|
|
304
|
+
ctx.arc(px, py + totalYOffset, spriteSize * 0.55, 0, Math.PI * 2);
|
|
244
305
|
ctx.strokeStyle = 'rgba(0,229,255,' + ringAlpha + ')';
|
|
245
306
|
ctx.lineWidth = 2 * s;
|
|
246
307
|
ctx.shadowColor = 'rgba(0,229,255,' + (ringAlpha * 0.5) + ')';
|
|
@@ -296,11 +357,15 @@ export function renderGraphScript() {
|
|
|
296
357
|
var alpha = 1.0;
|
|
297
358
|
var animFrame = null;
|
|
298
359
|
var canvas, ctx, tooltip;
|
|
360
|
+
var _nodeSelectCb = null; /* external callback for node selection */
|
|
299
361
|
var pulseT = 0;
|
|
300
362
|
var _tooltipNode = null, _tooltipTimer = null;
|
|
301
363
|
var _prevVisibleCount = 0;
|
|
302
364
|
var focusedNodeIndex = -1;
|
|
303
365
|
var liveRegion = null;
|
|
366
|
+
var phrenCurrentNodeId = null; /* id of node phren is currently at */
|
|
367
|
+
var phrenSelectedEdgeIdx = -1; /* index into phrenAdjacentLinks (-1 = none) */
|
|
368
|
+
var phrenAdjacentLinks = []; /* edges connected to current node */
|
|
304
369
|
|
|
305
370
|
/* ── helpers ────────────────────────────────────────────────────────── */
|
|
306
371
|
function clamp(v, lo, hi) { return v < lo ? lo : v > hi ? hi : v; }
|
|
@@ -932,6 +997,44 @@ export function renderGraphScript() {
|
|
|
932
997
|
}
|
|
933
998
|
ctx.lineWidth = baseEdgeWidth;
|
|
934
999
|
|
|
1000
|
+
/* 1b. selected edge highlight for phren keyboard navigation */
|
|
1001
|
+
if (phrenSelectedEdgeIdx >= 0 && phrenSelectedEdgeIdx < phrenAdjacentLinks.length) {
|
|
1002
|
+
var selLk = phrenAdjacentLinks[phrenSelectedEdgeIdx];
|
|
1003
|
+
if (selLk._source && selLk._target) {
|
|
1004
|
+
var selSid = selLk._source.id;
|
|
1005
|
+
var selDest = (selSid === phrenCurrentNodeId) ? selLk._target : selLk._source;
|
|
1006
|
+
ctx.save();
|
|
1007
|
+
/* glow edge */
|
|
1008
|
+
ctx.beginPath();
|
|
1009
|
+
ctx.strokeStyle = 'rgba(0,229,255,0.85)';
|
|
1010
|
+
ctx.lineWidth = 2.5 / scale;
|
|
1011
|
+
ctx.shadowColor = 'rgba(0,229,255,0.45)';
|
|
1012
|
+
ctx.shadowBlur = 10 / scale;
|
|
1013
|
+
ctx.moveTo(selLk._source.x, selLk._source.y);
|
|
1014
|
+
ctx.lineTo(selLk._target.x, selLk._target.y);
|
|
1015
|
+
ctx.stroke();
|
|
1016
|
+
ctx.shadowBlur = 0;
|
|
1017
|
+
/* destination label near edge midpoint */
|
|
1018
|
+
var selMx = (selLk._source.x + selLk._target.x) / 2;
|
|
1019
|
+
var selMy = (selLk._source.y + selLk._target.y) / 2;
|
|
1020
|
+
var selLbl = (selDest.label || selDest.id || '').slice(0, 32);
|
|
1021
|
+
if (selLbl) {
|
|
1022
|
+
var selFs = Math.max(10, Math.round(11 / scale));
|
|
1023
|
+
ctx.font = '600 ' + selFs + 'px sans-serif';
|
|
1024
|
+
ctx.textAlign = 'center';
|
|
1025
|
+
ctx.textBaseline = 'bottom';
|
|
1026
|
+
var selTw = ctx.measureText(selLbl).width;
|
|
1027
|
+
var selPad = 5 / scale;
|
|
1028
|
+
var selBh = selFs + 2 * selPad;
|
|
1029
|
+
ctx.fillStyle = 'rgba(10,12,30,0.88)';
|
|
1030
|
+
ctx.fillRect(selMx - selTw / 2 - selPad, selMy - selBh - 4 / scale, selTw + 2 * selPad, selBh);
|
|
1031
|
+
ctx.fillStyle = 'rgba(0,229,255,1)';
|
|
1032
|
+
ctx.fillText(selLbl, selMx, selMy - 4 / scale);
|
|
1033
|
+
}
|
|
1034
|
+
ctx.restore();
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
935
1038
|
/* 2. health rings with dash patterns + text labels (WCAG 1.4.1) */
|
|
936
1039
|
for (var i = 0; i < nodes.length; i++) {
|
|
937
1040
|
var nd = nodes[i];
|
|
@@ -1106,6 +1209,7 @@ export function renderGraphScript() {
|
|
|
1106
1209
|
|
|
1107
1210
|
/* ── hit testing ────────────────────────────────────────────────────── */
|
|
1108
1211
|
function hitTest(mx, my) {
|
|
1212
|
+
/* mx/my must be CSS pixels (same as e.clientX - rect.left from mouse events) */
|
|
1109
1213
|
var gx = (mx - panX) / scale;
|
|
1110
1214
|
var gy = (my - panY) / scale;
|
|
1111
1215
|
var closest = null, closestDist = Infinity;
|
|
@@ -1129,6 +1233,13 @@ export function renderGraphScript() {
|
|
|
1129
1233
|
if (node && typeof node.x === 'number' && typeof node.y === 'number') {
|
|
1130
1234
|
phrenMoveTo(node.x, node.y, node);
|
|
1131
1235
|
}
|
|
1236
|
+
/* fire external callback so VS Code extension can react to selection */
|
|
1237
|
+
if (_nodeSelectCb && node) {
|
|
1238
|
+
var rect = canvas ? canvas.getBoundingClientRect() : { left: 0, top: 0 };
|
|
1239
|
+
var cx = node.x * scale + panX + rect.left;
|
|
1240
|
+
var cy = node.y * scale + panY + rect.top;
|
|
1241
|
+
try { _nodeSelectCb(node, cx, cy); } catch(e) { /* swallow callback errors */ }
|
|
1242
|
+
}
|
|
1132
1243
|
var metaEl = document.getElementById('graph-detail-meta');
|
|
1133
1244
|
var bodyEl = document.getElementById('graph-detail-body');
|
|
1134
1245
|
if (!metaEl || !bodyEl) return;
|
|
@@ -1413,7 +1524,20 @@ export function renderGraphScript() {
|
|
|
1413
1524
|
render();
|
|
1414
1525
|
break;
|
|
1415
1526
|
case 'Enter':
|
|
1416
|
-
|
|
1527
|
+
case ' ':
|
|
1528
|
+
e.preventDefault();
|
|
1529
|
+
if (phrenSelectedEdgeIdx >= 0 && phrenSelectedEdgeIdx < phrenAdjacentLinks.length) {
|
|
1530
|
+
/* walk phren along selected edge */
|
|
1531
|
+
var wLk = phrenAdjacentLinks[phrenSelectedEdgeIdx];
|
|
1532
|
+
var wSid = wLk._source ? wLk._source.id : null;
|
|
1533
|
+
var wDest = (wLk._source && wLk._target)
|
|
1534
|
+
? (wSid === phrenCurrentNodeId ? wLk._target : wLk._source)
|
|
1535
|
+
: null;
|
|
1536
|
+
if (wDest) {
|
|
1537
|
+
phrenMoveTo(wDest.x, wDest.y, wDest);
|
|
1538
|
+
announce('Walking to ' + (wDest.label || wDest.id || ''));
|
|
1539
|
+
}
|
|
1540
|
+
} else if (e.key === 'Enter' && focusedNodeIndex >= 0 && focusedNodeIndex < visibleNodes.length) {
|
|
1417
1541
|
var sn = visibleNodes[focusedNodeIndex];
|
|
1418
1542
|
renderGraphDetails(sn);
|
|
1419
1543
|
announceNode(sn);
|
|
@@ -1423,7 +1547,10 @@ export function renderGraphScript() {
|
|
|
1423
1547
|
}
|
|
1424
1548
|
break;
|
|
1425
1549
|
case 'Escape':
|
|
1426
|
-
if (
|
|
1550
|
+
if (phrenSelectedEdgeIdx >= 0) {
|
|
1551
|
+
phrenSelectedEdgeIdx = -1;
|
|
1552
|
+
render();
|
|
1553
|
+
} else if (selectedNode) {
|
|
1427
1554
|
var prevFocus = focusedNodeIndex;
|
|
1428
1555
|
renderGraphDetails(null);
|
|
1429
1556
|
focusedNodeIndex = prevFocus;
|
|
@@ -1436,12 +1563,22 @@ export function renderGraphScript() {
|
|
|
1436
1563
|
break;
|
|
1437
1564
|
case 'ArrowLeft':
|
|
1438
1565
|
e.preventDefault();
|
|
1439
|
-
|
|
1566
|
+
if (phrenAdjacentLinks.length > 0) {
|
|
1567
|
+
phrenSelectedEdgeIdx = phrenSelectedEdgeIdx < 0
|
|
1568
|
+
? phrenAdjacentLinks.length - 1
|
|
1569
|
+
: (phrenSelectedEdgeIdx - 1 + phrenAdjacentLinks.length) % phrenAdjacentLinks.length;
|
|
1570
|
+
} else {
|
|
1571
|
+
panX += PAN_STEP;
|
|
1572
|
+
}
|
|
1440
1573
|
render();
|
|
1441
1574
|
break;
|
|
1442
1575
|
case 'ArrowRight':
|
|
1443
1576
|
e.preventDefault();
|
|
1444
|
-
|
|
1577
|
+
if (phrenAdjacentLinks.length > 0) {
|
|
1578
|
+
phrenSelectedEdgeIdx = (phrenSelectedEdgeIdx + 1) % phrenAdjacentLinks.length;
|
|
1579
|
+
} else {
|
|
1580
|
+
panX -= PAN_STEP;
|
|
1581
|
+
}
|
|
1445
1582
|
render();
|
|
1446
1583
|
break;
|
|
1447
1584
|
case 'ArrowUp':
|
|
@@ -1678,7 +1815,8 @@ export function renderGraphScript() {
|
|
|
1678
1815
|
lastTickTime = timestamp;
|
|
1679
1816
|
simulate();
|
|
1680
1817
|
render();
|
|
1681
|
-
|
|
1818
|
+
var phrenStillActive = phren.moving || phren.arriving || phren.trailPoints.length > 0;
|
|
1819
|
+
if (alpha > 0 || dragging || phrenStillActive) {
|
|
1682
1820
|
animFrame = requestAnimationFrame(tick);
|
|
1683
1821
|
} else {
|
|
1684
1822
|
animFrame = null;
|
|
@@ -1897,12 +2035,29 @@ export function renderGraphScript() {
|
|
|
1897
2035
|
}
|
|
1898
2036
|
|
|
1899
2037
|
applyFilters();
|
|
1900
|
-
phrenInit(visibleNodes);
|
|
1901
2038
|
buildFilterBar();
|
|
1902
2039
|
initPositions();
|
|
2040
|
+
phrenInit(visibleNodes); /* must run after initPositions so node coords are valid */
|
|
1903
2041
|
setupInteraction();
|
|
1904
2042
|
startSimulation();
|
|
1905
2043
|
announceGraphSummary();
|
|
2044
|
+
},
|
|
2045
|
+
/** Register a callback fired when the user selects a node. */
|
|
2046
|
+
onNodeSelect: function(cb) { _nodeSelectCb = cb; },
|
|
2047
|
+
/** Hit-test at CSS screen coordinates (same units as e.clientX/clientY minus canvas rect).
|
|
2048
|
+
* Note: pass CSS pixels, NOT canvas.width/height (which are native/DPR-scaled). */
|
|
2049
|
+
getNodeAt: function(x, y) { return hitTest(x, y); },
|
|
2050
|
+
/** Returns the node id where phren is currently located, or null. */
|
|
2051
|
+
getCurrentNode: function() { return phrenCurrentNodeId; },
|
|
2052
|
+
/** Programmatically move the phren character to a node (by id). */
|
|
2053
|
+
walkTo: function(nodeId) {
|
|
2054
|
+
for (var i = 0; i < visibleNodes.length; i++) {
|
|
2055
|
+
if (visibleNodes[i].id === nodeId) {
|
|
2056
|
+
phrenMoveTo(visibleNodes[i].x, visibleNodes[i].y, visibleNodes[i]);
|
|
2057
|
+
return true;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
return false;
|
|
1906
2061
|
}
|
|
1907
2062
|
};
|
|
1908
2063
|
})();
|
|
@@ -5,13 +5,14 @@ import * as yaml from "js-yaml";
|
|
|
5
5
|
import { readInstallPreferences } from "./init-preferences.js";
|
|
6
6
|
import { debugLog } from "./shared.js";
|
|
7
7
|
import { errorMessage } from "./utils.js";
|
|
8
|
+
import { withFileLock } from "./governance-locks.js";
|
|
8
9
|
export const PROJECT_OWNERSHIP_MODES = ["phren-managed", "detached", "repo-managed"];
|
|
9
10
|
export const PROJECT_HOOK_EVENTS = ["UserPromptSubmit", "Stop", "SessionStart", "PostToolUse"];
|
|
10
11
|
export function parseProjectOwnershipMode(raw) {
|
|
11
12
|
if (!raw)
|
|
12
13
|
return undefined;
|
|
13
14
|
const normalized = raw.trim().toLowerCase();
|
|
14
|
-
if (normalized === "phren" || normalized === "managed"
|
|
15
|
+
if (normalized === "phren" || normalized === "managed")
|
|
15
16
|
return "phren-managed";
|
|
16
17
|
if (normalized === "repo" || normalized === "external")
|
|
17
18
|
return "repo-managed";
|
|
@@ -37,17 +38,22 @@ export function readProjectConfig(phrenPath, project) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
export function writeProjectConfig(phrenPath, project, patch) {
|
|
40
|
-
const configPath = projectConfigPath(phrenPath, project);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
const configPath = path.resolve(projectConfigPath(phrenPath, project));
|
|
42
|
+
if (!configPath.startsWith(phrenPath + path.sep) && configPath !== phrenPath) {
|
|
43
|
+
throw new Error(`Project config path escapes phren store`);
|
|
44
|
+
}
|
|
45
|
+
return withFileLock(configPath, () => {
|
|
46
|
+
const current = readProjectConfig(phrenPath, project);
|
|
47
|
+
const next = {
|
|
48
|
+
...current,
|
|
49
|
+
...patch,
|
|
50
|
+
};
|
|
51
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
52
|
+
const tmpPath = `${configPath}.tmp-${crypto.randomUUID()}`;
|
|
53
|
+
fs.writeFileSync(tmpPath, yaml.dump(next, { lineWidth: 1000 }));
|
|
54
|
+
fs.renameSync(tmpPath, configPath);
|
|
55
|
+
return next;
|
|
56
|
+
});
|
|
51
57
|
}
|
|
52
58
|
export function getProjectSourcePath(phrenPath, project, config) {
|
|
53
59
|
const raw = (config ?? readProjectConfig(phrenPath, project)).sourcePath;
|
|
@@ -75,11 +81,24 @@ export function isProjectHookEnabled(phrenPath, project, event, config) {
|
|
|
75
81
|
return true;
|
|
76
82
|
}
|
|
77
83
|
export function writeProjectHookConfig(phrenPath, project, patch) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
// Move read+merge inside the lock so concurrent writers cannot clobber each other.
|
|
85
|
+
const configPath = path.resolve(projectConfigPath(phrenPath, project));
|
|
86
|
+
if (!configPath.startsWith(phrenPath + path.sep) && configPath !== phrenPath) {
|
|
87
|
+
throw new Error(`Project config path escapes phren store`);
|
|
88
|
+
}
|
|
89
|
+
return withFileLock(configPath, () => {
|
|
90
|
+
const current = readProjectConfig(phrenPath, project);
|
|
91
|
+
const next = {
|
|
92
|
+
...current,
|
|
93
|
+
hooks: {
|
|
94
|
+
...normalizeHookConfig(current),
|
|
95
|
+
...patch,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
99
|
+
const tmpPath = `${configPath}.tmp-${crypto.randomUUID()}`;
|
|
100
|
+
fs.writeFileSync(tmpPath, yaml.dump(next, { lineWidth: 1000 }));
|
|
101
|
+
fs.renameSync(tmpPath, configPath);
|
|
102
|
+
return next;
|
|
84
103
|
});
|
|
85
104
|
}
|
|
@@ -3,6 +3,6 @@ export { checkConsolidationNeeded, validateFindingsFormat, stripTaskDoneSection,
|
|
|
3
3
|
export { filterTrustedFindings, filterTrustedFindingsDetailed, } from "./content-citation.js";
|
|
4
4
|
export { scanForSecrets, resolveCoref, isDuplicateFinding, detectConflicts, extractDynamicEntities, checkSemanticDedup, checkSemanticConflicts, } from "./content-dedup.js";
|
|
5
5
|
export { countActiveFindings, autoArchiveToReference, } from "./content-archive.js";
|
|
6
|
-
export { upsertCanonical, addFindingToFile, addFindingsToFile, } from "./content-learning.js";
|
|
6
|
+
export { upsertCanonical, addFindingToFile, addFindingsToFile, autoDetectFindingType, } from "./content-learning.js";
|
|
7
7
|
export { FINDING_LIFECYCLE_STATUSES, FINDING_TYPE_DECAY, extractFindingType, parseFindingLifecycle, buildLifecycleComments, isInactiveFindingLine, } from "./finding-lifecycle.js";
|
|
8
8
|
export { METADATA_REGEX, parseStatus, parseStatusField, parseSupersession, parseSupersedesRef, parseContradiction, parseAllContradictions, parseFindingId, parseCreatedDate, isCitationLine, isArchiveStart, isArchiveEnd, stripLifecycleMetadata, stripRelationMetadata, stripAllMetadata, stripComments, addMetadata, } from "./content-metadata.js";
|
package/mcp/dist/shared-index.js
CHANGED
|
@@ -6,12 +6,13 @@ import { globSync } from "glob";
|
|
|
6
6
|
import { debugLog, appendIndexEvent, getProjectDirs, collectNativeMemoryFiles, runtimeFile, homeDir, readRootManifest, } from "./shared.js";
|
|
7
7
|
import { getIndexPolicy, withFileLock } from "./shared-governance.js";
|
|
8
8
|
import { stripTaskDoneSection } from "./shared-content.js";
|
|
9
|
+
import { isInactiveFindingLine } from "./finding-lifecycle.js";
|
|
9
10
|
import { invalidateDfCache } from "./shared-search-fallback.js";
|
|
10
11
|
import { errorMessage } from "./utils.js";
|
|
11
12
|
import { beginUserFragmentBuildCache, endUserFragmentBuildCache, extractAndLinkFragments, ensureGlobalEntitiesTable, } from "./shared-fragment-graph.js";
|
|
12
13
|
import { bootstrapSqlJs } from "./shared-sqljs.js";
|
|
13
14
|
import { getProjectOwnershipMode, getProjectSourcePath, readProjectConfig } from "./project-config.js";
|
|
14
|
-
import { buildSourceDocKey,
|
|
15
|
+
import { buildSourceDocKey, queryDocBySourceKey, queryDocRows, } from "./index-query.js";
|
|
15
16
|
import { classifyTopicForText, readProjectTopics, } from "./project-topics.js";
|
|
16
17
|
export { porterStem } from "./shared-stemmer.js";
|
|
17
18
|
export { cosineFallback } from "./shared-search-fallback.js";
|
|
@@ -101,6 +102,11 @@ export function classifyFile(filename, relPath) {
|
|
|
101
102
|
}
|
|
102
103
|
const IMPORT_RE = /^@import\s+(.+)$/gm;
|
|
103
104
|
const MAX_IMPORT_DEPTH = 5;
|
|
105
|
+
const IMPORT_ROOT_PREFIX = "shared/";
|
|
106
|
+
function isAllowedImportPath(importPath) {
|
|
107
|
+
const normalized = importPath.replace(/\\/g, "/");
|
|
108
|
+
return normalized.startsWith(IMPORT_ROOT_PREFIX) && normalized.toLowerCase().endsWith(".md");
|
|
109
|
+
}
|
|
104
110
|
/**
|
|
105
111
|
* Internal recursive helper for resolveImports. Tracks `seen` (cycle detection) and `depth` (runaway
|
|
106
112
|
* recursion guard) — callers should never pass these; use the public `resolveImports` instead.
|
|
@@ -110,6 +116,9 @@ function _resolveImportsRecursive(content, phrenPath, seen, depth) {
|
|
|
110
116
|
return content;
|
|
111
117
|
return content.replace(IMPORT_RE, (_match, importPath) => {
|
|
112
118
|
const trimmed = importPath.trim();
|
|
119
|
+
if (!isAllowedImportPath(trimmed)) {
|
|
120
|
+
return "<!-- @import blocked: only shared/*.md allowed -->";
|
|
121
|
+
}
|
|
113
122
|
const globalRoot = path.resolve(phrenPath, "global");
|
|
114
123
|
const resolved = path.join(globalRoot, trimmed);
|
|
115
124
|
// Use lexical resolution first for the prefix check
|
|
@@ -464,6 +473,10 @@ export function normalizeIndexedContent(content, type, phrenPath, maxChars) {
|
|
|
464
473
|
if (type === "task") {
|
|
465
474
|
normalized = stripTaskDoneSection(normalized);
|
|
466
475
|
}
|
|
476
|
+
if (type === "findings") {
|
|
477
|
+
const lines = normalized.split("\n");
|
|
478
|
+
normalized = lines.filter(line => !isInactiveFindingLine(line)).join("\n");
|
|
479
|
+
}
|
|
467
480
|
if (typeof maxChars === "number" && maxChars >= 0) {
|
|
468
481
|
normalized = normalized.slice(0, maxChars);
|
|
469
482
|
}
|
|
@@ -782,8 +795,8 @@ function mergeManualLinks(db, phrenPath) {
|
|
|
782
795
|
for (const link of manualLinks) {
|
|
783
796
|
try {
|
|
784
797
|
// Validate: skip manual links whose sourceDoc no longer exists in the index
|
|
785
|
-
const docCheck =
|
|
786
|
-
if (!docCheck
|
|
798
|
+
const docCheck = queryDocBySourceKey(db, phrenPath, link.sourceDoc);
|
|
799
|
+
if (!docCheck) {
|
|
787
800
|
if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
|
|
788
801
|
process.stderr.write(`[phren] manualLinks: pruning stale link to "${link.sourceDoc}"\n`);
|
|
789
802
|
pruned = true;
|