@phren/cli 0.0.49 → 0.0.52
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/cli/actions.js +17 -2
- package/mcp/dist/cli/cli.js +1 -1
- package/mcp/dist/cli/namespaces.js +35 -10
- package/mcp/dist/cli/ops.js +2 -2
- package/mcp/dist/content/validate.js +18 -0
- package/mcp/dist/data/access.js +27 -0
- package/mcp/dist/data/tasks.js +27 -2
- package/mcp/dist/generated/memory-ui-graph.browser.js +21 -21
- package/mcp/dist/governance/policy.js +3 -1
- package/mcp/dist/memory-ui-graph.runtime.js +21 -21
- package/mcp/dist/phren-core.js +1 -1
- package/mcp/dist/profile-store.js +20 -0
- package/mcp/dist/project-config.js +26 -0
- package/mcp/dist/shared/index.js +7 -1
- package/mcp/dist/shell/view.js +27 -9
- package/mcp/dist/store-routing.js +2 -2
- package/mcp/dist/task/lifecycle.js +11 -0
- package/mcp/dist/tools/config.js +23 -5
- package/mcp/dist/tools/data.js +17 -7
- package/mcp/dist/tools/extract.js +8 -5
- package/mcp/dist/tools/finding.js +14 -9
- package/mcp/dist/tools/graph.js +12 -3
- package/mcp/dist/tools/hooks.js +15 -2
- package/mcp/dist/tools/search.js +3 -2
- package/mcp/dist/tools/session.js +58 -18
- package/mcp/dist/tools/tasks.js +58 -44
- package/mcp/dist/ui/data.js +46 -19
- package/mcp/dist/ui/server.js +2 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mcpResponse } from "./types.js";
|
|
1
|
+
import { mcpResponse, resolveStoreForProject } from "./types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
@@ -305,16 +305,21 @@ export function listAllSessions(phrenPath, limit = 50) {
|
|
|
305
305
|
entries.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime());
|
|
306
306
|
return entries;
|
|
307
307
|
}
|
|
308
|
-
export function getSessionArtifacts(phrenPath, sessionId, project) {
|
|
308
|
+
export async function getSessionArtifacts(phrenPath, sessionId, project) {
|
|
309
309
|
const findings = [];
|
|
310
310
|
const tasks = [];
|
|
311
311
|
const shortId = sessionId.slice(0, 8);
|
|
312
312
|
try {
|
|
313
|
+
// Primary store projects
|
|
313
314
|
const projectDirs = getProjectDirs(phrenPath);
|
|
314
315
|
const targetProjects = project ? [project] : projectDirs.map((d) => path.basename(d));
|
|
315
|
-
|
|
316
|
+
const seen = new Set();
|
|
317
|
+
const readProjectArtifacts = (storePath, proj) => {
|
|
318
|
+
if (seen.has(proj))
|
|
319
|
+
return;
|
|
320
|
+
seen.add(proj);
|
|
316
321
|
// Findings with matching sessionId
|
|
317
|
-
const findingsResult = readFindings(
|
|
322
|
+
const findingsResult = readFindings(storePath, proj);
|
|
318
323
|
if (findingsResult.ok) {
|
|
319
324
|
for (const f of findingsResult.data) {
|
|
320
325
|
if (f.sessionId && (f.sessionId === sessionId || f.sessionId.startsWith(shortId))) {
|
|
@@ -328,7 +333,7 @@ export function getSessionArtifacts(phrenPath, sessionId, project) {
|
|
|
328
333
|
}
|
|
329
334
|
}
|
|
330
335
|
// Tasks with matching sessionId
|
|
331
|
-
const tasksResult = readTasks(
|
|
336
|
+
const tasksResult = readTasks(storePath, proj);
|
|
332
337
|
if (tasksResult.ok) {
|
|
333
338
|
for (const section of ["Active", "Queue", "Done"]) {
|
|
334
339
|
for (const t of tasksResult.data.items[section]) {
|
|
@@ -344,15 +349,32 @@ export function getSessionArtifacts(phrenPath, sessionId, project) {
|
|
|
344
349
|
}
|
|
345
350
|
}
|
|
346
351
|
}
|
|
352
|
+
};
|
|
353
|
+
for (const proj of targetProjects) {
|
|
354
|
+
readProjectArtifacts(phrenPath, proj);
|
|
355
|
+
}
|
|
356
|
+
// Team store projects
|
|
357
|
+
try {
|
|
358
|
+
const { getNonPrimaryStores } = await import("../store-registry.js");
|
|
359
|
+
for (const store of getNonPrimaryStores(phrenPath)) {
|
|
360
|
+
if (!fs.existsSync(store.path))
|
|
361
|
+
continue;
|
|
362
|
+
const storeDirs = getProjectDirs(store.path).map(d => path.basename(d)).filter(p => p !== "global");
|
|
363
|
+
const storeTargetProjects = project ? (storeDirs.includes(project) ? [project] : []) : storeDirs;
|
|
364
|
+
for (const proj of storeTargetProjects) {
|
|
365
|
+
readProjectArtifacts(store.path, proj);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
347
368
|
}
|
|
369
|
+
catch { /* store-registry not available */ }
|
|
348
370
|
}
|
|
349
371
|
catch (err) {
|
|
350
372
|
debugLog(`getSessionArtifacts error: ${errorMessage(err)}`);
|
|
351
373
|
}
|
|
352
374
|
return { findings, tasks };
|
|
353
375
|
}
|
|
354
|
-
function hasCompletedTasksInSession(phrenPath, sessionId, project) {
|
|
355
|
-
const artifacts = getSessionArtifacts(phrenPath, sessionId, project);
|
|
376
|
+
async function hasCompletedTasksInSession(phrenPath, sessionId, project) {
|
|
377
|
+
const artifacts = await getSessionArtifacts(phrenPath, sessionId, project);
|
|
356
378
|
return artifacts.tasks.some((task) => task.section === "Done" && task.checked);
|
|
357
379
|
}
|
|
358
380
|
/** Compute what changed since the last session ended. */
|
|
@@ -448,8 +470,16 @@ export function register(server, ctx) {
|
|
|
448
470
|
const activeProject = project ?? priorProject;
|
|
449
471
|
const activeScope = normalizedAgentScope;
|
|
450
472
|
if (activeProject && isValidProjectName(activeProject)) {
|
|
473
|
+
const projectStorePath = (() => {
|
|
474
|
+
try {
|
|
475
|
+
return resolveStoreForProject(ctx, activeProject).phrenPath;
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
return phrenPath;
|
|
479
|
+
}
|
|
480
|
+
})();
|
|
451
481
|
try {
|
|
452
|
-
const findings = readFindings(
|
|
482
|
+
const findings = readFindings(projectStorePath, activeProject);
|
|
453
483
|
if (findings.ok) {
|
|
454
484
|
const bullets = findings.data
|
|
455
485
|
.filter((entry) => isMemoryScopeVisible(normalizeMemoryScope(entry.scope), activeScope))
|
|
@@ -464,7 +494,7 @@ export function register(server, ctx) {
|
|
|
464
494
|
debugError("session_start findingsRead", err);
|
|
465
495
|
}
|
|
466
496
|
try {
|
|
467
|
-
const tasks = readTasks(
|
|
497
|
+
const tasks = readTasks(projectStorePath, activeProject);
|
|
468
498
|
if (tasks.ok) {
|
|
469
499
|
const activeItems = tasks.data.items.Active
|
|
470
500
|
.filter((entry) => isMemoryScopeVisible(normalizeMemoryScope(entry.scope), activeScope))
|
|
@@ -480,7 +510,7 @@ export function register(server, ctx) {
|
|
|
480
510
|
}
|
|
481
511
|
// Surface extracted preferences/facts for this project
|
|
482
512
|
try {
|
|
483
|
-
const facts = readExtractedFacts(
|
|
513
|
+
const facts = readExtractedFacts(projectStorePath, activeProject).slice(-10);
|
|
484
514
|
if (facts.length > 0) {
|
|
485
515
|
parts.push(`## Preferences (${activeProject})\n${facts.map(f => `- ${f.fact}`).join("\n")}`);
|
|
486
516
|
}
|
|
@@ -489,7 +519,7 @@ export function register(server, ctx) {
|
|
|
489
519
|
debugError("session_start factsRead", err);
|
|
490
520
|
}
|
|
491
521
|
try {
|
|
492
|
-
const checkpoints = listTaskCheckpoints(
|
|
522
|
+
const checkpoints = listTaskCheckpoints(projectStorePath, activeProject).slice(0, 3);
|
|
493
523
|
if (checkpoints.length > 0) {
|
|
494
524
|
const lines = [];
|
|
495
525
|
for (const checkpoint of checkpoints) {
|
|
@@ -600,19 +630,29 @@ export function register(server, ctx) {
|
|
|
600
630
|
writeLastSummary(phrenPath, effectiveSummary, state.sessionId, endedState.project);
|
|
601
631
|
}
|
|
602
632
|
if (endedState.project && isValidProjectName(endedState.project)) {
|
|
633
|
+
const projectStorePath = (() => {
|
|
634
|
+
if (!endedState.project)
|
|
635
|
+
return phrenPath;
|
|
636
|
+
try {
|
|
637
|
+
return resolveStoreForProject(ctx, endedState.project).phrenPath;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return phrenPath;
|
|
641
|
+
}
|
|
642
|
+
})();
|
|
603
643
|
try {
|
|
604
|
-
const trackedActiveTask = getActiveTaskForSession(
|
|
644
|
+
const trackedActiveTask = getActiveTaskForSession(projectStorePath, state.sessionId, endedState.project);
|
|
605
645
|
const activeTask = trackedActiveTask ?? (() => {
|
|
606
|
-
const tasks = readTasks(
|
|
646
|
+
const tasks = readTasks(projectStorePath, endedState.project);
|
|
607
647
|
if (!tasks.ok)
|
|
608
648
|
return null;
|
|
609
649
|
return tasks.data.items.Active[0] ?? null;
|
|
610
650
|
})();
|
|
611
651
|
if (activeTask) {
|
|
612
652
|
const taskId = activeTask.stableId || activeTask.id;
|
|
613
|
-
const projectConfig = readProjectConfig(
|
|
614
|
-
const snapshotRoot = getProjectSourcePath(
|
|
615
|
-
path.join(
|
|
653
|
+
const projectConfig = readProjectConfig(projectStorePath, endedState.project);
|
|
654
|
+
const snapshotRoot = getProjectSourcePath(projectStorePath, endedState.project, projectConfig) ||
|
|
655
|
+
path.join(projectStorePath, endedState.project);
|
|
616
656
|
const { gitStatus, editedFiles } = collectGitStatusSnapshot(snapshotRoot);
|
|
617
657
|
const resumptionHint = extractResumptionHint(effectiveSummary, activeTask.line, activeTask.context || "No prior attempt captured");
|
|
618
658
|
writeTaskCheckpoint(phrenPath, {
|
|
@@ -635,7 +675,7 @@ export function register(server, ctx) {
|
|
|
635
675
|
}
|
|
636
676
|
try {
|
|
637
677
|
const tasksCompleted = Number.isFinite(endedState.tasksCompleted) ? endedState.tasksCompleted : 0;
|
|
638
|
-
if (tasksCompleted > 0 || hasCompletedTasksInSession(phrenPath, state.sessionId, endedState.project)) {
|
|
678
|
+
if (tasksCompleted > 0 || await hasCompletedTasksInSession(phrenPath, state.sessionId, endedState.project)) {
|
|
639
679
|
markImpactEntriesCompletedForSession(phrenPath, state.sessionId, endedState.project);
|
|
640
680
|
}
|
|
641
681
|
}
|
|
@@ -707,7 +747,7 @@ export function register(server, ctx) {
|
|
|
707
747
|
const session = sessions.find(s => s.sessionId === targetSessionId || s.sessionId.startsWith(targetSessionId));
|
|
708
748
|
if (!session)
|
|
709
749
|
return mcpResponse({ ok: false, error: `Session ${targetSessionId} not found.` });
|
|
710
|
-
const artifacts = getSessionArtifacts(phrenPath, session.sessionId, project);
|
|
750
|
+
const artifacts = await getSessionArtifacts(phrenPath, session.sessionId, project);
|
|
711
751
|
const parts = [
|
|
712
752
|
`Session: ${session.sessionId.slice(0, 8)}`,
|
|
713
753
|
`Project: ${session.project ?? "none"}`,
|
package/mcp/dist/tools/tasks.js
CHANGED
|
@@ -109,7 +109,8 @@ export function register(server, ctx) {
|
|
|
109
109
|
return mcpResponse({ ok: false, error: "Provide `project` when looking up a single item." });
|
|
110
110
|
if (!isValidProjectName(project))
|
|
111
111
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
112
|
-
const
|
|
112
|
+
const resolvedPath = resolveStoreForProject(ctx, project).phrenPath;
|
|
113
|
+
const result = readTasks(resolvedPath, project);
|
|
113
114
|
if (!result.ok)
|
|
114
115
|
return mcpResponse({ ok: false, error: result.error });
|
|
115
116
|
const doc = result.data;
|
|
@@ -141,7 +142,8 @@ export function register(server, ctx) {
|
|
|
141
142
|
if (project) {
|
|
142
143
|
if (!isValidProjectName(project))
|
|
143
144
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
144
|
-
const
|
|
145
|
+
const resolvedPath = resolveStoreForProject(ctx, project).phrenPath;
|
|
146
|
+
const result = readTasks(resolvedPath, project);
|
|
145
147
|
if (!result.ok)
|
|
146
148
|
return mcpResponse({ ok: false, error: result.error });
|
|
147
149
|
const doc = result.data;
|
|
@@ -257,21 +259,24 @@ export function register(server, ctx) {
|
|
|
257
259
|
]).describe("The task(s) to complete. Pass a string for one, or an array for bulk."),
|
|
258
260
|
sessionId: z.string().optional().describe("Optional session ID from session_start. Pass this to track per-session task completion metrics."),
|
|
259
261
|
}),
|
|
260
|
-
}, async ({ project, item, sessionId }) => {
|
|
262
|
+
}, async ({ project: projectInput, item, sessionId }) => {
|
|
263
|
+
const resolved = resolveStoreForProject(ctx, projectInput);
|
|
264
|
+
const project = resolved.project;
|
|
265
|
+
const targetPath = resolved.phrenPath;
|
|
261
266
|
if (!isValidProjectName(project))
|
|
262
267
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
263
|
-
const completeTaskDenied = permissionDeniedError(
|
|
268
|
+
const completeTaskDenied = permissionDeniedError(targetPath, "complete_task", project);
|
|
264
269
|
if (completeTaskDenied)
|
|
265
270
|
return mcpResponse({ ok: false, error: completeTaskDenied });
|
|
266
271
|
if (Array.isArray(item)) {
|
|
267
272
|
return withWriteQueue(async () => {
|
|
268
273
|
const resolvedItems = item
|
|
269
274
|
.map((match) => {
|
|
270
|
-
const
|
|
271
|
-
return
|
|
275
|
+
const resolvedItem = resolveTaskItem(targetPath, project, match);
|
|
276
|
+
return resolvedItem.ok ? resolvedItem.data : null;
|
|
272
277
|
})
|
|
273
278
|
.filter((task) => task !== null);
|
|
274
|
-
const result = completeTasksBatch(
|
|
279
|
+
const result = completeTasksBatch(targetPath, project, item);
|
|
275
280
|
if (!result.ok)
|
|
276
281
|
return mcpResponse({ ok: false, error: result.error });
|
|
277
282
|
const { completed, errors } = result.data;
|
|
@@ -280,7 +285,7 @@ export function register(server, ctx) {
|
|
|
280
285
|
for (const task of resolvedItems) {
|
|
281
286
|
if (!completedSet.has(task.line))
|
|
282
287
|
continue;
|
|
283
|
-
clearTaskCheckpoint(
|
|
288
|
+
clearTaskCheckpoint(targetPath, {
|
|
284
289
|
project,
|
|
285
290
|
taskId: task.stableId ?? task.id,
|
|
286
291
|
stableId: task.stableId,
|
|
@@ -288,20 +293,20 @@ export function register(server, ctx) {
|
|
|
288
293
|
taskLine: task.line,
|
|
289
294
|
});
|
|
290
295
|
}
|
|
291
|
-
incrementSessionTasksCompleted(
|
|
296
|
+
incrementSessionTasksCompleted(targetPath, completed.length, sessionId, project);
|
|
292
297
|
}
|
|
293
298
|
if (completed.length > 0)
|
|
294
|
-
refreshTaskIndex(updateFileInIndex,
|
|
299
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
295
300
|
return mcpResponse({ ok: completed.length > 0, ...(completed.length === 0 ? { error: `No tasks completed: ${errors.join("; ")}` } : {}), message: `Completed ${completed.length}/${item.length} items`, data: { project, completed, errors } });
|
|
296
301
|
});
|
|
297
302
|
}
|
|
298
303
|
return withWriteQueue(async () => {
|
|
299
|
-
const before = resolveTaskItem(
|
|
300
|
-
const result = completeTaskStore(
|
|
304
|
+
const before = resolveTaskItem(targetPath, project, item);
|
|
305
|
+
const result = completeTaskStore(targetPath, project, item);
|
|
301
306
|
if (!result.ok)
|
|
302
307
|
return mcpResponse({ ok: false, error: result.error });
|
|
303
308
|
if (before.ok) {
|
|
304
|
-
clearTaskCheckpoint(
|
|
309
|
+
clearTaskCheckpoint(targetPath, {
|
|
305
310
|
project,
|
|
306
311
|
taskId: before.data.stableId ?? before.data.id,
|
|
307
312
|
stableId: before.data.stableId,
|
|
@@ -309,8 +314,8 @@ export function register(server, ctx) {
|
|
|
309
314
|
taskLine: before.data.line,
|
|
310
315
|
});
|
|
311
316
|
}
|
|
312
|
-
incrementSessionTasksCompleted(
|
|
313
|
-
refreshTaskIndex(updateFileInIndex,
|
|
317
|
+
incrementSessionTasksCompleted(targetPath, 1, sessionId, project);
|
|
318
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
314
319
|
return mcpResponse({ ok: true, message: result.data, data: { project, item } });
|
|
315
320
|
});
|
|
316
321
|
});
|
|
@@ -324,28 +329,31 @@ export function register(server, ctx) {
|
|
|
324
329
|
z.array(z.string()).describe("List of partial item texts or IDs to remove."),
|
|
325
330
|
]).describe("The task(s) to remove. Pass a string for one, or an array for bulk."),
|
|
326
331
|
}),
|
|
327
|
-
}, async ({ project, item }) => {
|
|
332
|
+
}, async ({ project: projectInput, item }) => {
|
|
333
|
+
const resolved = resolveStoreForProject(ctx, projectInput);
|
|
334
|
+
const project = resolved.project;
|
|
335
|
+
const targetPath = resolved.phrenPath;
|
|
328
336
|
if (!isValidProjectName(project))
|
|
329
337
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
330
|
-
const removeTaskDenied = permissionDeniedError(
|
|
338
|
+
const removeTaskDenied = permissionDeniedError(targetPath, "remove_task", project);
|
|
331
339
|
if (removeTaskDenied)
|
|
332
340
|
return mcpResponse({ ok: false, error: removeTaskDenied });
|
|
333
341
|
if (Array.isArray(item)) {
|
|
334
342
|
return withWriteQueue(async () => {
|
|
335
|
-
const result = removeTasksBatch(
|
|
343
|
+
const result = removeTasksBatch(targetPath, project, item);
|
|
336
344
|
if (!result.ok)
|
|
337
345
|
return mcpResponse({ ok: false, error: result.error });
|
|
338
346
|
const { removed, errors } = result.data;
|
|
339
347
|
if (removed.length > 0)
|
|
340
|
-
refreshTaskIndex(updateFileInIndex,
|
|
348
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
341
349
|
return mcpResponse({ ok: removed.length > 0, ...(removed.length === 0 ? { error: `No tasks removed: ${errors.join("; ")}` } : {}), message: `Removed ${removed.length}/${item.length} items`, data: { project, removed, errors } });
|
|
342
350
|
});
|
|
343
351
|
}
|
|
344
352
|
return withWriteQueue(async () => {
|
|
345
|
-
const result = removeTaskStore(
|
|
353
|
+
const result = removeTaskStore(targetPath, project, item);
|
|
346
354
|
if (!result.ok)
|
|
347
355
|
return mcpResponse({ ok: false, error: result.error });
|
|
348
|
-
refreshTaskIndex(updateFileInIndex,
|
|
356
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
349
357
|
return mcpResponse({ ok: true, message: result.data, data: { project, item } });
|
|
350
358
|
});
|
|
351
359
|
});
|
|
@@ -373,10 +381,13 @@ export function register(server, ctx) {
|
|
|
373
381
|
work_next: z.boolean().optional().describe("If true, pick the highest-priority Queue item and move it to Active. Ignores item param."),
|
|
374
382
|
}).describe("Fields to update. All are optional."),
|
|
375
383
|
}),
|
|
376
|
-
}, async ({ project, item, updates }) => {
|
|
384
|
+
}, async ({ project: projectInput, item, updates }) => {
|
|
385
|
+
const resolved = resolveStoreForProject(ctx, projectInput);
|
|
386
|
+
const project = resolved.project;
|
|
387
|
+
const targetPath = resolved.phrenPath;
|
|
377
388
|
if (!isValidProjectName(project))
|
|
378
389
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
379
|
-
const updateTaskDenied = permissionDeniedError(
|
|
390
|
+
const updateTaskDenied = permissionDeniedError(targetPath, "update_task", project);
|
|
380
391
|
if (updateTaskDenied)
|
|
381
392
|
return mcpResponse({ ok: false, error: updateTaskDenied });
|
|
382
393
|
// Runtime validation: item is required unless work_next is true
|
|
@@ -417,26 +428,26 @@ export function register(server, ctx) {
|
|
|
417
428
|
return withWriteQueue(async () => {
|
|
418
429
|
// Handle work_next: pick highest-priority Queue item, move to Active
|
|
419
430
|
if (updates.work_next) {
|
|
420
|
-
const result = workNextTask(
|
|
431
|
+
const result = workNextTask(targetPath, project);
|
|
421
432
|
if (!result.ok)
|
|
422
433
|
return mcpResponse({ ok: false, error: result.error });
|
|
423
|
-
refreshTaskIndex(updateFileInIndex,
|
|
434
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
424
435
|
return mcpResponse({ ok: true, message: result.data, data: { project } });
|
|
425
436
|
}
|
|
426
437
|
// Handle pin
|
|
427
438
|
if (updates.pin) {
|
|
428
|
-
const result = pinTask(
|
|
439
|
+
const result = pinTask(targetPath, project, item);
|
|
429
440
|
if (!result.ok)
|
|
430
441
|
return mcpResponse({ ok: false, error: result.error });
|
|
431
|
-
refreshTaskIndex(updateFileInIndex,
|
|
442
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
432
443
|
return mcpResponse({ ok: true, message: result.data, data: { project, item } });
|
|
433
444
|
}
|
|
434
445
|
// Handle promote (clear speculative flag)
|
|
435
446
|
if (updates.promote) {
|
|
436
|
-
const result = promoteTask(
|
|
447
|
+
const result = promoteTask(targetPath, project, item, updates.move_to_active ?? false);
|
|
437
448
|
if (!result.ok)
|
|
438
449
|
return mcpResponse({ ok: false, error: result.error });
|
|
439
|
-
refreshTaskIndex(updateFileInIndex,
|
|
450
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
440
451
|
return mcpResponse({
|
|
441
452
|
ok: true,
|
|
442
453
|
message: `Promoted task "${result.data.line}" in ${project}${updates.move_to_active ? " (moved to Active)" : ""}.`,
|
|
@@ -444,10 +455,10 @@ export function register(server, ctx) {
|
|
|
444
455
|
});
|
|
445
456
|
}
|
|
446
457
|
if (updates.create_issue) {
|
|
447
|
-
const
|
|
448
|
-
if (!
|
|
449
|
-
return mcpResponse({ ok: false, error:
|
|
450
|
-
const repo = resolveProjectGithubRepo(
|
|
458
|
+
const resolvedItem = resolveTaskItem(targetPath, project, item);
|
|
459
|
+
if (!resolvedItem.ok)
|
|
460
|
+
return mcpResponse({ ok: false, error: resolvedItem.error });
|
|
461
|
+
const repo = resolveProjectGithubRepo(targetPath, project);
|
|
451
462
|
if (!repo) {
|
|
452
463
|
return mcpResponse({
|
|
453
464
|
ok: false,
|
|
@@ -456,18 +467,18 @@ export function register(server, ctx) {
|
|
|
456
467
|
}
|
|
457
468
|
const created = createGithubIssueForTask({
|
|
458
469
|
repo,
|
|
459
|
-
title:
|
|
460
|
-
body: buildTaskIssueBody(project,
|
|
470
|
+
title: resolvedItem.data.line.replace(/\s*\[(high|medium|low)\]\s*$/i, "").trim(),
|
|
471
|
+
body: buildTaskIssueBody(project, resolvedItem.data),
|
|
461
472
|
});
|
|
462
473
|
if (!created.ok)
|
|
463
474
|
return mcpResponse({ ok: false, error: created.error, errorCode: created.code });
|
|
464
|
-
const linked = linkTaskIssue(
|
|
475
|
+
const linked = linkTaskIssue(targetPath, project, item, {
|
|
465
476
|
github_issue: created.data.issueNumber,
|
|
466
477
|
github_url: created.data.url,
|
|
467
478
|
});
|
|
468
479
|
if (!linked.ok)
|
|
469
480
|
return mcpResponse({ ok: false, error: linked.error, errorCode: linked.code });
|
|
470
|
-
refreshTaskIndex(updateFileInIndex,
|
|
481
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
471
482
|
return mcpResponse({
|
|
472
483
|
ok: true,
|
|
473
484
|
message: `Created GitHub issue ${created.data.issueNumber ? `#${created.data.issueNumber}` : created.data.url} for ${project} task.`,
|
|
@@ -487,14 +498,14 @@ export function register(server, ctx) {
|
|
|
487
498
|
if (updates.unlink_github && (updates.github_issue !== undefined || updates.github_url)) {
|
|
488
499
|
return mcpResponse({ ok: false, error: "Use either unlink_github=true or github_issue/github_url, not both." });
|
|
489
500
|
}
|
|
490
|
-
const result = linkTaskIssue(
|
|
501
|
+
const result = linkTaskIssue(targetPath, project, item, {
|
|
491
502
|
github_issue: updates.github_issue,
|
|
492
503
|
github_url: updates.github_url,
|
|
493
504
|
unlink: updates.unlink_github ?? false,
|
|
494
505
|
});
|
|
495
506
|
if (!result.ok)
|
|
496
507
|
return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
|
|
497
|
-
refreshTaskIndex(updateFileInIndex,
|
|
508
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
498
509
|
return mcpResponse({
|
|
499
510
|
ok: true,
|
|
500
511
|
message: updates.unlink_github
|
|
@@ -510,10 +521,10 @@ export function register(server, ctx) {
|
|
|
510
521
|
});
|
|
511
522
|
}
|
|
512
523
|
// Standard update path
|
|
513
|
-
const result = updateTaskStore(
|
|
524
|
+
const result = updateTaskStore(targetPath, project, item, updates);
|
|
514
525
|
if (!result.ok)
|
|
515
526
|
return mcpResponse({ ok: false, error: result.error });
|
|
516
|
-
refreshTaskIndex(updateFileInIndex,
|
|
527
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
517
528
|
return mcpResponse({ ok: true, message: result.data, data: { project, item, updates } });
|
|
518
529
|
});
|
|
519
530
|
});
|
|
@@ -525,15 +536,18 @@ export function register(server, ctx) {
|
|
|
525
536
|
keep: z.number().optional().describe("Number of recent Done items to keep. Default 30."),
|
|
526
537
|
dry_run: z.boolean().optional().describe("If true, preview changes without writing."),
|
|
527
538
|
}),
|
|
528
|
-
}, async ({ project, keep, dry_run }) => {
|
|
539
|
+
}, async ({ project: projectInput, keep, dry_run }) => {
|
|
540
|
+
const resolved = resolveStoreForProject(ctx, projectInput);
|
|
541
|
+
const project = resolved.project;
|
|
542
|
+
const targetPath = resolved.phrenPath;
|
|
529
543
|
if (!isValidProjectName(project))
|
|
530
544
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
531
545
|
return withWriteQueue(async () => {
|
|
532
|
-
const result = tidyDoneTasks(
|
|
546
|
+
const result = tidyDoneTasks(targetPath, project, keep ?? 30, dry_run ?? false);
|
|
533
547
|
if (!result.ok)
|
|
534
548
|
return mcpResponse({ ok: false, error: result.error });
|
|
535
549
|
if (!dry_run)
|
|
536
|
-
refreshTaskIndex(updateFileInIndex,
|
|
550
|
+
refreshTaskIndex(updateFileInIndex, targetPath, project);
|
|
537
551
|
return mcpResponse({ ok: true, message: result.data, data: { project, keep: keep ?? 30, dryRun: dry_run ?? false } });
|
|
538
552
|
});
|
|
539
553
|
});
|
package/mcp/dist/ui/data.js
CHANGED
|
@@ -151,42 +151,69 @@ export function getHooksData(phrenPath, profile) {
|
|
|
151
151
|
return { globalEnabled, tools, customHooks: readCustomHooks(phrenPath), projectOverrides };
|
|
152
152
|
}
|
|
153
153
|
export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
154
|
-
|
|
154
|
+
// Collect projects from primary store and all readable non-primary stores
|
|
155
|
+
const storeProjects = [];
|
|
156
|
+
const primaryProjects = getProjectDirs(phrenPath, profile)
|
|
157
|
+
.map((projectDir) => path.basename(projectDir))
|
|
158
|
+
.filter((project) => project !== "global");
|
|
159
|
+
for (const project of primaryProjects)
|
|
160
|
+
storeProjects.push({ storePath: phrenPath, project });
|
|
161
|
+
for (const store of getNonPrimaryStores(phrenPath)) {
|
|
162
|
+
if (!fs.existsSync(store.path))
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
const storeProjectDirs = getProjectDirs(store.path)
|
|
166
|
+
.map((projectDir) => path.basename(projectDir))
|
|
167
|
+
.filter((project) => project !== "global");
|
|
168
|
+
for (const project of storeProjectDirs)
|
|
169
|
+
storeProjects.push({ storePath: store.path, project });
|
|
170
|
+
}
|
|
171
|
+
catch { /* store not accessible — skip */ }
|
|
172
|
+
}
|
|
173
|
+
const projects = storeProjects.map((sp) => sp.project);
|
|
155
174
|
const nodes = [];
|
|
156
175
|
const links = [];
|
|
157
176
|
const projectSet = new Set(projects);
|
|
158
177
|
// Collect all unique topics across projects for the UI
|
|
159
178
|
const topicMetaMap = new Map();
|
|
160
|
-
|
|
179
|
+
// Track which project nodes have already been pushed (same name may appear in multiple stores)
|
|
180
|
+
const addedProjectNodeIds = new Set();
|
|
181
|
+
for (const { storePath, project } of storeProjects) {
|
|
161
182
|
// Load dynamic topics for this project
|
|
162
|
-
const { topics: projectTopics } = readProjectTopics(
|
|
183
|
+
const { topics: projectTopics } = readProjectTopics(storePath, project);
|
|
163
184
|
for (const topic of projectTopics) {
|
|
164
185
|
if (!topicMetaMap.has(topic.slug)) {
|
|
165
186
|
topicMetaMap.set(topic.slug, { slug: topic.slug, label: topic.label });
|
|
166
187
|
}
|
|
167
188
|
}
|
|
168
|
-
const findingsPath = path.join(
|
|
189
|
+
const findingsPath = path.join(storePath, project, "FINDINGS.md");
|
|
169
190
|
if (!fs.existsSync(findingsPath)) {
|
|
191
|
+
if (!addedProjectNodeIds.has(project)) {
|
|
192
|
+
addedProjectNodeIds.add(project);
|
|
193
|
+
nodes.push({
|
|
194
|
+
id: project,
|
|
195
|
+
label: project,
|
|
196
|
+
fullLabel: project,
|
|
197
|
+
group: "project",
|
|
198
|
+
refCount: 0,
|
|
199
|
+
project,
|
|
200
|
+
tagged: false,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!addedProjectNodeIds.has(project)) {
|
|
206
|
+
addedProjectNodeIds.add(project);
|
|
170
207
|
nodes.push({
|
|
171
208
|
id: project,
|
|
172
209
|
label: project,
|
|
173
210
|
fullLabel: project,
|
|
174
211
|
group: "project",
|
|
175
|
-
refCount:
|
|
212
|
+
refCount: 1,
|
|
176
213
|
project,
|
|
177
214
|
tagged: false,
|
|
178
215
|
});
|
|
179
|
-
continue;
|
|
180
216
|
}
|
|
181
|
-
nodes.push({
|
|
182
|
-
id: project,
|
|
183
|
-
label: project,
|
|
184
|
-
fullLabel: project,
|
|
185
|
-
group: "project",
|
|
186
|
-
refCount: 1,
|
|
187
|
-
project,
|
|
188
|
-
tagged: false,
|
|
189
|
-
});
|
|
190
217
|
const content = fs.readFileSync(findingsPath, "utf8");
|
|
191
218
|
const lines = content.split("\n");
|
|
192
219
|
// No cap for focused project; high caps otherwise
|
|
@@ -316,8 +343,8 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
316
343
|
}
|
|
317
344
|
// ── Tasks ──────────────────────────────────────────────────────────
|
|
318
345
|
try {
|
|
319
|
-
for (const project of
|
|
320
|
-
const taskResult = readTasks(
|
|
346
|
+
for (const { storePath, project } of storeProjects) {
|
|
347
|
+
const taskResult = readTasks(storePath, project);
|
|
321
348
|
if (!taskResult.ok)
|
|
322
349
|
continue;
|
|
323
350
|
const doc = taskResult.data;
|
|
@@ -443,8 +470,8 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
443
470
|
}
|
|
444
471
|
// ── Reference docs ────────────────────────────────────────────────
|
|
445
472
|
try {
|
|
446
|
-
for (const project of
|
|
447
|
-
const refDir = path.join(
|
|
473
|
+
for (const { storePath, project } of storeProjects) {
|
|
474
|
+
const refDir = path.join(storePath, project, "reference");
|
|
448
475
|
if (!fs.existsSync(refDir) || !fs.statSync(refDir).isDirectory())
|
|
449
476
|
continue;
|
|
450
477
|
const files = fs.readdirSync(refDir);
|
package/mcp/dist/ui/server.js
CHANGED
|
@@ -543,7 +543,8 @@ function handleGetFindings(res, pathname, ctx) {
|
|
|
543
543
|
const project = decodeURIComponent(pathname.slice("/api/findings/".length));
|
|
544
544
|
if (!project || !isValidProjectName(project))
|
|
545
545
|
return jsonErr(res, "Invalid project name", 400);
|
|
546
|
-
const
|
|
546
|
+
const basePath = resolveProjectBasePath(ctx.phrenPath, project);
|
|
547
|
+
const result = readFindings(basePath, project);
|
|
547
548
|
jsonOk(res, result.ok ? { ok: true, data: { project, findings: result.data } } : { ok: false, error: result.error });
|
|
548
549
|
}
|
|
549
550
|
// ── POST handlers ─────────────────────────────────────────────────────────────
|