@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.
@@ -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
- for (const proj of targetProjects) {
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(phrenPath, proj);
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(phrenPath, proj);
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(phrenPath, activeProject);
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(phrenPath, activeProject);
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(phrenPath, activeProject).slice(-10);
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(phrenPath, activeProject).slice(0, 3);
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(phrenPath, state.sessionId, endedState.project);
644
+ const trackedActiveTask = getActiveTaskForSession(projectStorePath, state.sessionId, endedState.project);
605
645
  const activeTask = trackedActiveTask ?? (() => {
606
- const tasks = readTasks(phrenPath, endedState.project);
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(phrenPath, endedState.project);
614
- const snapshotRoot = getProjectSourcePath(phrenPath, endedState.project, projectConfig) ||
615
- path.join(phrenPath, endedState.project);
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"}`,
@@ -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 result = readTasks(phrenPath, project);
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 result = readTasks(phrenPath, project);
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(phrenPath, "complete_task", project);
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 resolved = resolveTaskItem(phrenPath, project, match);
271
- return resolved.ok ? resolved.data : null;
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(phrenPath, project, item);
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(phrenPath, {
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(phrenPath, completed.length, sessionId, project);
296
+ incrementSessionTasksCompleted(targetPath, completed.length, sessionId, project);
292
297
  }
293
298
  if (completed.length > 0)
294
- refreshTaskIndex(updateFileInIndex, phrenPath, project);
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(phrenPath, project, item);
300
- const result = completeTaskStore(phrenPath, project, item);
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(phrenPath, {
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(phrenPath, 1, sessionId, project);
313
- refreshTaskIndex(updateFileInIndex, phrenPath, project);
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(phrenPath, "remove_task", project);
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(phrenPath, project, item);
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, phrenPath, project);
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(phrenPath, project, item);
353
+ const result = removeTaskStore(targetPath, project, item);
346
354
  if (!result.ok)
347
355
  return mcpResponse({ ok: false, error: result.error });
348
- refreshTaskIndex(updateFileInIndex, phrenPath, project);
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(phrenPath, "update_task", project);
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(phrenPath, project);
431
+ const result = workNextTask(targetPath, project);
421
432
  if (!result.ok)
422
433
  return mcpResponse({ ok: false, error: result.error });
423
- refreshTaskIndex(updateFileInIndex, phrenPath, project);
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(phrenPath, project, item);
439
+ const result = pinTask(targetPath, project, item);
429
440
  if (!result.ok)
430
441
  return mcpResponse({ ok: false, error: result.error });
431
- refreshTaskIndex(updateFileInIndex, phrenPath, project);
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(phrenPath, project, item, updates.move_to_active ?? false);
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, phrenPath, project);
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 resolved = resolveTaskItem(phrenPath, project, item);
448
- if (!resolved.ok)
449
- return mcpResponse({ ok: false, error: resolved.error });
450
- const repo = resolveProjectGithubRepo(phrenPath, project);
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: resolved.data.line.replace(/\s*\[(high|medium|low)\]\s*$/i, "").trim(),
460
- body: buildTaskIssueBody(project, resolved.data),
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(phrenPath, project, item, {
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, phrenPath, project);
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(phrenPath, project, item, {
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, phrenPath, project);
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(phrenPath, project, item, updates);
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, phrenPath, project);
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(phrenPath, project, keep ?? 30, dry_run ?? false);
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, phrenPath, project);
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
  });
@@ -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
- const projects = getProjectDirs(phrenPath, profile).map((projectDir) => path.basename(projectDir)).filter((project) => project !== "global");
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
- for (const project of projects) {
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(phrenPath, project);
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(phrenPath, project, "FINDINGS.md");
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: 0,
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 projects) {
320
- const taskResult = readTasks(phrenPath, project);
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 projects) {
447
- const refDir = path.join(phrenPath, project, "reference");
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);
@@ -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 result = readFindings(ctx.phrenPath, project);
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 ─────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phren/cli",
3
- "version": "0.0.49",
3
+ "version": "0.0.52",
4
4
  "description": "Knowledge layer for AI agents. Phren learns and recalls.",
5
5
  "type": "module",
6
6
  "bin": {