@phren/cli 0.0.17 → 0.0.18
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-hooks-session.js +3 -3
- package/mcp/dist/cli-namespaces.js +1 -1
- package/mcp/dist/data-tasks.js +1 -1
- package/mcp/dist/entrypoint.js +1 -1
- package/mcp/dist/governance-policy.js +76 -32
- package/mcp/dist/governance-rbac.js +152 -0
- package/mcp/dist/init-preferences.js +1 -1
- package/mcp/dist/init-setup.js +7 -7
- package/mcp/dist/init.js +2 -2
- package/mcp/dist/link-checksums.js +1 -1
- package/mcp/dist/link-doctor.js +1 -1
- package/mcp/dist/mcp-config.js +102 -2
- package/mcp/dist/mcp-finding.js +16 -0
- package/mcp/dist/mcp-search.js +44 -5
- package/mcp/dist/mcp-session.js +4 -0
- package/mcp/dist/mcp-tasks.js +22 -0
- package/mcp/dist/memory-ui-assets.js +2 -2
- package/mcp/dist/memory-ui-page.js +20 -37
- package/mcp/dist/memory-ui-scripts.js +181 -75
- package/mcp/dist/memory-ui-server.js +57 -2
- package/mcp/dist/phren-core.js +1 -1
- package/mcp/dist/phren-paths.js +2 -2
- package/mcp/dist/proactivity.js +47 -0
- package/mcp/dist/profile-store.js +100 -1
- package/mcp/dist/shared-index.js +2 -2
- package/mcp/dist/shared-retrieval.js +42 -1
- package/mcp/dist/shell-input.js +1 -1
- package/package.json +1 -1
package/mcp/dist/mcp-finding.js
CHANGED
|
@@ -15,6 +15,7 @@ import { appendChildFinding, editFinding as editFindingCore, readFindings } from
|
|
|
15
15
|
import { getActiveTaskForSession } from "./task-lifecycle.js";
|
|
16
16
|
import { FINDING_PROVENANCE_SOURCES } from "./content-citation.js";
|
|
17
17
|
import { isInactiveFindingLine, supersedeFinding, retractFinding as retractFindingLifecycle, resolveFindingContradiction, } from "./finding-lifecycle.js";
|
|
18
|
+
import { permissionDeniedError } from "./governance-rbac.js";
|
|
18
19
|
const JACCARD_MAYBE_LOW = 0.30;
|
|
19
20
|
const JACCARD_MAYBE_HIGH = 0.55; // above this isDuplicateFinding already catches it
|
|
20
21
|
function findJaccardCandidates(phrenPath, project, finding) {
|
|
@@ -112,6 +113,9 @@ export function register(server, ctx) {
|
|
|
112
113
|
}, async ({ project, finding, citation, sessionId, source, findingType, scope }) => {
|
|
113
114
|
if (!isValidProjectName(project))
|
|
114
115
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
116
|
+
const addFindingDenied = permissionDeniedError(phrenPath, "add_finding", project);
|
|
117
|
+
if (addFindingDenied)
|
|
118
|
+
return mcpResponse({ ok: false, error: addFindingDenied });
|
|
115
119
|
if (finding.length > 5000)
|
|
116
120
|
return mcpResponse({ ok: false, error: "Finding text exceeds 5000 character limit." });
|
|
117
121
|
const normalizedScope = normalizeMemoryScope(scope ?? "shared");
|
|
@@ -214,6 +218,9 @@ export function register(server, ctx) {
|
|
|
214
218
|
}, async ({ project, findings, sessionId }) => {
|
|
215
219
|
if (!isValidProjectName(project))
|
|
216
220
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
221
|
+
const addFindingsDenied = permissionDeniedError(phrenPath, "add_finding", project);
|
|
222
|
+
if (addFindingsDenied)
|
|
223
|
+
return mcpResponse({ ok: false, error: addFindingsDenied });
|
|
217
224
|
if (findings.length > 100)
|
|
218
225
|
return mcpResponse({ ok: false, error: "Bulk add limited to 100 findings per call." });
|
|
219
226
|
if (findings.some((f) => f.length > 5000))
|
|
@@ -399,6 +406,9 @@ export function register(server, ctx) {
|
|
|
399
406
|
}, async ({ project, old_text, new_text }) => {
|
|
400
407
|
if (!isValidProjectName(project))
|
|
401
408
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
409
|
+
const editDenied = permissionDeniedError(phrenPath, "edit_finding", project);
|
|
410
|
+
if (editDenied)
|
|
411
|
+
return mcpResponse({ ok: false, error: editDenied });
|
|
402
412
|
return withWriteQueue(async () => {
|
|
403
413
|
const result = editFindingCore(phrenPath, project, old_text, new_text);
|
|
404
414
|
if (!result.ok)
|
|
@@ -424,6 +434,9 @@ export function register(server, ctx) {
|
|
|
424
434
|
}, async ({ project, finding }) => {
|
|
425
435
|
if (!isValidProjectName(project))
|
|
426
436
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
437
|
+
const removeDenied = permissionDeniedError(phrenPath, "remove_finding", project);
|
|
438
|
+
if (removeDenied)
|
|
439
|
+
return mcpResponse({ ok: false, error: removeDenied });
|
|
427
440
|
return withWriteQueue(async () => {
|
|
428
441
|
const result = removeFindingCore(phrenPath, project, finding);
|
|
429
442
|
if (result.ok) {
|
|
@@ -446,6 +459,9 @@ export function register(server, ctx) {
|
|
|
446
459
|
}, async ({ project, findings }) => {
|
|
447
460
|
if (!isValidProjectName(project))
|
|
448
461
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
462
|
+
const removeFindingsDenied = permissionDeniedError(phrenPath, "remove_finding", project);
|
|
463
|
+
if (removeFindingsDenied)
|
|
464
|
+
return mcpResponse({ ok: false, error: removeFindingsDenied });
|
|
449
465
|
return withWriteQueue(async () => {
|
|
450
466
|
const result = removeFindingsCore(phrenPath, project, findings);
|
|
451
467
|
if (result.ok) {
|
package/mcp/dist/mcp-search.js
CHANGED
|
@@ -10,7 +10,7 @@ import { decodeStringRow, queryRows, queryDocRows, queryEntityLinks, logEntityMi
|
|
|
10
10
|
import { runCustomHooks } from "./hooks.js";
|
|
11
11
|
import { entryScoreKey, getQualityMultiplier, getRetentionPolicy } from "./shared-governance.js";
|
|
12
12
|
import { callLlm } from "./content-dedup.js";
|
|
13
|
-
import { rankResults, searchKnowledgeRows, applyTrustFilter } from "./shared-retrieval.js";
|
|
13
|
+
import { rankResults, searchKnowledgeRows, applyTrustFilter, searchFederatedStores } from "./shared-retrieval.js";
|
|
14
14
|
import { parseSourceComment } from "./content-citation.js";
|
|
15
15
|
import { resolveActiveSessionScope } from "./mcp-session.js";
|
|
16
16
|
/**
|
|
@@ -235,9 +235,35 @@ export function register(server, ctx) {
|
|
|
235
235
|
const safeQuery = retrieval.safeQuery;
|
|
236
236
|
if (!safeQuery)
|
|
237
237
|
return mcpResponse({ ok: false, error: "Search query is empty after sanitization." });
|
|
238
|
-
let rows = retrieval.rows;
|
|
238
|
+
let rows = retrieval.rows ?? [];
|
|
239
239
|
const usedFallback = retrieval.usedFallback;
|
|
240
|
-
|
|
240
|
+
// Merge federated store results when PHREN_FEDERATION_PATHS is set and no project filter
|
|
241
|
+
// (federation is global by nature — per-project filter only makes sense within a single store)
|
|
242
|
+
if (!filterProject) {
|
|
243
|
+
try {
|
|
244
|
+
const federatedRows = await searchFederatedStores(phrenPath, {
|
|
245
|
+
query,
|
|
246
|
+
maxResults,
|
|
247
|
+
fetchLimit,
|
|
248
|
+
filterType,
|
|
249
|
+
});
|
|
250
|
+
if (federatedRows.length > 0) {
|
|
251
|
+
// Dedup by path to avoid duplicates if stores share files
|
|
252
|
+
const localPaths = new Set(rows.map((r) => r.path || `${r.project}/${r.filename}`));
|
|
253
|
+
const uniqueFederated = federatedRows.filter((r) => {
|
|
254
|
+
const key = r.path || `${r.project}/${r.filename}`;
|
|
255
|
+
return !localPaths.has(key);
|
|
256
|
+
});
|
|
257
|
+
rows = [...rows, ...uniqueFederated];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
if (process.env.PHREN_DEBUG) {
|
|
262
|
+
process.stderr.write(`[phren] search_knowledge federation: ${errorMessage(err)}\n`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (rows.length === 0) {
|
|
241
267
|
logSearchMiss(phrenPath, query, filterProject);
|
|
242
268
|
return mcpResponse({ ok: true, message: "No results found.", data: { query, results: [] } });
|
|
243
269
|
}
|
|
@@ -368,6 +394,7 @@ export function register(server, ctx) {
|
|
|
368
394
|
const results = rows.map((row) => {
|
|
369
395
|
const snippet = extractSnippet(row.content, query);
|
|
370
396
|
const lifecycle = row.type === "findings" ? lifecycleByRowKey.get(findingRowKey(row)) : undefined;
|
|
397
|
+
const federationSource = "federationSource" in row ? row.federationSource : undefined;
|
|
371
398
|
return {
|
|
372
399
|
project: row.project,
|
|
373
400
|
filename: row.filename,
|
|
@@ -376,6 +403,7 @@ export function register(server, ctx) {
|
|
|
376
403
|
path: row.path,
|
|
377
404
|
status: lifecycle?.primaryStatus,
|
|
378
405
|
statuses: lifecycle?.statuses,
|
|
406
|
+
...(federationSource ? { federation_source: federationSource } : {}),
|
|
379
407
|
};
|
|
380
408
|
});
|
|
381
409
|
let relatedFragments = [];
|
|
@@ -396,7 +424,10 @@ export function register(server, ctx) {
|
|
|
396
424
|
if ((process.env.PHREN_DEBUG))
|
|
397
425
|
process.stderr.write(`[phren] fragment query: ${errorMessage(err)}\n`);
|
|
398
426
|
}
|
|
399
|
-
const formatted = results.map((r) =>
|
|
427
|
+
const formatted = results.map((r) => {
|
|
428
|
+
const fedNote = r.federation_source ? ` [from: ${r.federation_source}]` : "";
|
|
429
|
+
return `### ${r.project}/${r.filename} (${r.type})${fedNote}\n${r.snippet}\n\n\`${r.path}\``;
|
|
430
|
+
});
|
|
400
431
|
// Memory synthesis: generate a concise paragraph from top results when requested
|
|
401
432
|
let synthesis;
|
|
402
433
|
if (synthesize && results.length > 0) {
|
|
@@ -576,7 +607,15 @@ export function register(server, ctx) {
|
|
|
576
607
|
const visibleItems = includeHistory
|
|
577
608
|
? allItems
|
|
578
609
|
: allItems.filter(f => f.tier !== "archived" && !HISTORY_FINDING_STATUSES.has(f.status));
|
|
579
|
-
|
|
610
|
+
// Apply scope filter: only show findings visible to the active scope
|
|
611
|
+
const activeScope = resolveActiveSessionScope(phrenPath, project);
|
|
612
|
+
const scopedItems = activeScope
|
|
613
|
+
? visibleItems.filter(f => {
|
|
614
|
+
const itemScope = normalizeMemoryScope(f.scope);
|
|
615
|
+
return isMemoryScopeVisible(itemScope, activeScope);
|
|
616
|
+
})
|
|
617
|
+
: visibleItems;
|
|
618
|
+
const filteredItems = status ? scopedItems.filter(f => f.status === status) : scopedItems;
|
|
580
619
|
if (!filteredItems.length) {
|
|
581
620
|
const msg = historyCount > 0 && !includeHistory
|
|
582
621
|
? `No findings found for "${project}" with current filters. ${historyCount} historical finding(s) hidden. Pass include_history=true to show history.`
|
package/mcp/dist/mcp-session.js
CHANGED
|
@@ -124,6 +124,10 @@ function findMostRecentSession(phrenPath) {
|
|
|
124
124
|
return { file: best.fullPath, state: best.data };
|
|
125
125
|
}
|
|
126
126
|
export function resolveActiveSessionScope(phrenPath, project) {
|
|
127
|
+
// PHREN_SCOPE env var takes priority over session-derived scope
|
|
128
|
+
const envScope = normalizeMemoryScope(process.env.PHREN_SCOPE);
|
|
129
|
+
if (envScope)
|
|
130
|
+
return envScope;
|
|
127
131
|
const dir = sessionsDir(phrenPath);
|
|
128
132
|
const results = scanSessionFiles(dir, readSessionStateFile, (state) => {
|
|
129
133
|
if (state.endedAt)
|
package/mcp/dist/mcp-tasks.js
CHANGED
|
@@ -9,6 +9,7 @@ import { buildTaskIssueBody, createGithubIssueForTask, parseGithubIssueUrl, reso
|
|
|
9
9
|
import { clearTaskCheckpoint } from "./session-checkpoints.js";
|
|
10
10
|
import { incrementSessionTasksCompleted } from "./mcp-session.js";
|
|
11
11
|
import { normalizeMemoryScope } from "./shared.js";
|
|
12
|
+
import { permissionDeniedError } from "./governance-rbac.js";
|
|
12
13
|
const TASK_SECTION_ORDER = ["Active", "Queue", "Done"];
|
|
13
14
|
const DEFAULT_TASK_LIMIT = 20;
|
|
14
15
|
/** Done items are historical — cap tightly by default to avoid large responses. */
|
|
@@ -206,6 +207,9 @@ export function register(server, ctx) {
|
|
|
206
207
|
}, async ({ project, item, scope }) => {
|
|
207
208
|
if (!isValidProjectName(project))
|
|
208
209
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
210
|
+
const addTaskDenied = permissionDeniedError(phrenPath, "add_task", project);
|
|
211
|
+
if (addTaskDenied)
|
|
212
|
+
return mcpResponse({ ok: false, error: addTaskDenied });
|
|
209
213
|
const normalizedScope = normalizeMemoryScope(scope ?? "shared");
|
|
210
214
|
if (!normalizedScope)
|
|
211
215
|
return mcpResponse({ ok: false, error: `Invalid scope: "${scope}". Use lowercase letters/numbers with '-' or '_' (max 64 chars), e.g. "researcher".` });
|
|
@@ -227,6 +231,9 @@ export function register(server, ctx) {
|
|
|
227
231
|
}, async ({ project, items }) => {
|
|
228
232
|
if (!isValidProjectName(project))
|
|
229
233
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
234
|
+
const addTasksDenied = permissionDeniedError(phrenPath, "add_task", project);
|
|
235
|
+
if (addTasksDenied)
|
|
236
|
+
return mcpResponse({ ok: false, error: addTasksDenied });
|
|
230
237
|
return withWriteQueue(async () => {
|
|
231
238
|
const result = addTasksBatch(phrenPath, project, items);
|
|
232
239
|
if (!result.ok)
|
|
@@ -248,6 +255,9 @@ export function register(server, ctx) {
|
|
|
248
255
|
}, async ({ project, item, sessionId }) => {
|
|
249
256
|
if (!isValidProjectName(project))
|
|
250
257
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
258
|
+
const completeTaskDenied = permissionDeniedError(phrenPath, "complete_task", project);
|
|
259
|
+
if (completeTaskDenied)
|
|
260
|
+
return mcpResponse({ ok: false, error: completeTaskDenied });
|
|
251
261
|
return withWriteQueue(async () => {
|
|
252
262
|
const before = resolveTaskItem(phrenPath, project, item);
|
|
253
263
|
const result = completeTaskStore(phrenPath, project, item);
|
|
@@ -278,6 +288,9 @@ export function register(server, ctx) {
|
|
|
278
288
|
}, async ({ project, items, sessionId }) => {
|
|
279
289
|
if (!isValidProjectName(project))
|
|
280
290
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
291
|
+
const completeTasksDenied = permissionDeniedError(phrenPath, "complete_task", project);
|
|
292
|
+
if (completeTasksDenied)
|
|
293
|
+
return mcpResponse({ ok: false, error: completeTasksDenied });
|
|
281
294
|
return withWriteQueue(async () => {
|
|
282
295
|
const resolvedItems = items
|
|
283
296
|
.map((match) => {
|
|
@@ -319,6 +332,9 @@ export function register(server, ctx) {
|
|
|
319
332
|
}, async ({ project, item }) => {
|
|
320
333
|
if (!isValidProjectName(project))
|
|
321
334
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
335
|
+
const removeTaskDenied = permissionDeniedError(phrenPath, "remove_task", project);
|
|
336
|
+
if (removeTaskDenied)
|
|
337
|
+
return mcpResponse({ ok: false, error: removeTaskDenied });
|
|
322
338
|
return withWriteQueue(async () => {
|
|
323
339
|
const result = removeTaskStore(phrenPath, project, item);
|
|
324
340
|
if (!result.ok)
|
|
@@ -337,6 +353,9 @@ export function register(server, ctx) {
|
|
|
337
353
|
}, async ({ project, items }) => {
|
|
338
354
|
if (!isValidProjectName(project))
|
|
339
355
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
356
|
+
const removeTasksDenied = permissionDeniedError(phrenPath, "remove_task", project);
|
|
357
|
+
if (removeTasksDenied)
|
|
358
|
+
return mcpResponse({ ok: false, error: removeTasksDenied });
|
|
340
359
|
return withWriteQueue(async () => {
|
|
341
360
|
const result = removeTasksBatch(phrenPath, project, items);
|
|
342
361
|
if (!result.ok)
|
|
@@ -367,6 +386,9 @@ export function register(server, ctx) {
|
|
|
367
386
|
}, async ({ project, item, updates }) => {
|
|
368
387
|
if (!isValidProjectName(project))
|
|
369
388
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
389
|
+
const updateTaskDenied = permissionDeniedError(phrenPath, "update_task", project);
|
|
390
|
+
if (updateTaskDenied)
|
|
391
|
+
return mcpResponse({ ok: false, error: updateTaskDenied });
|
|
370
392
|
return withWriteQueue(async () => {
|
|
371
393
|
const result = updateTaskStore(phrenPath, project, item, updates);
|
|
372
394
|
if (!result.ok)
|