@phren/cli 0.0.52 → 0.0.54
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 +84 -28
- package/mcp/dist/cli/namespaces.js +42 -2
- package/mcp/dist/content/validate.js +2 -3
- package/mcp/dist/data/access.js +2 -3
- package/mcp/dist/data/tasks.js +2 -2
- package/mcp/dist/generated/memory-ui-graph.browser.js +28 -25
- package/mcp/dist/memory-ui-graph.runtime.js +28 -25
- package/mcp/dist/profile-store.js +2 -3
- package/mcp/dist/shared/index.js +2 -2
- package/mcp/dist/shell/view.js +5 -3
- package/mcp/dist/store-registry.js +41 -0
- package/mcp/dist/tools/data.js +8 -7
- package/mcp/dist/tools/search.js +8 -9
- package/mcp/dist/tools/session.js +2 -2
- package/mcp/dist/ui/data.js +25 -9
- package/mcp/dist/ui/page.js +1 -0
- package/mcp/dist/ui/scripts.js +2 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import { errorMessage, isValidProjectName } from "./utils.js";
|
|
|
8
8
|
import { TASK_FILE_ALIASES } from "./data/tasks.js";
|
|
9
9
|
import { withSafeLock } from "./shared/data-utils.js";
|
|
10
10
|
import { logger } from "./logger.js";
|
|
11
|
+
import { getNonPrimaryStores, getStoreProjectDirs } from "./store-registry.js";
|
|
11
12
|
export function resolveActiveProfile(phrenPath, requestedProfile) {
|
|
12
13
|
const manifest = readRootManifest(phrenPath);
|
|
13
14
|
if (manifest?.installMode === "project-local") {
|
|
@@ -308,12 +309,10 @@ export function listProjectCards(phrenPath, profile) {
|
|
|
308
309
|
const seen = new Set(dirs.map((d) => path.basename(d)));
|
|
309
310
|
// Include projects from team stores
|
|
310
311
|
try {
|
|
311
|
-
const storeRegistry = require("./store-registry.js");
|
|
312
|
-
const { getNonPrimaryStores } = storeRegistry;
|
|
313
312
|
for (const store of getNonPrimaryStores(phrenPath)) {
|
|
314
313
|
if (!fs.existsSync(store.path))
|
|
315
314
|
continue;
|
|
316
|
-
for (const dir of
|
|
315
|
+
for (const dir of getStoreProjectDirs(store)) {
|
|
317
316
|
const name = path.basename(dir);
|
|
318
317
|
if (seen.has(name) || name === "global")
|
|
319
318
|
continue;
|
package/mcp/dist/shared/index.js
CHANGED
|
@@ -28,13 +28,13 @@ function getAllStoreProjectDirs(phrenPath, profile) {
|
|
|
28
28
|
*/
|
|
29
29
|
async function refreshStoreProjectDirs(phrenPath, profile) {
|
|
30
30
|
try {
|
|
31
|
-
const { getNonPrimaryStores } = await import("../store-registry.js");
|
|
31
|
+
const { getNonPrimaryStores, getStoreProjectDirs } = await import("../store-registry.js");
|
|
32
32
|
const otherStores = getNonPrimaryStores(phrenPath);
|
|
33
33
|
const dirs = [];
|
|
34
34
|
for (const store of otherStores) {
|
|
35
35
|
if (!fs.existsSync(store.path))
|
|
36
36
|
continue;
|
|
37
|
-
dirs.push(...
|
|
37
|
+
dirs.push(...getStoreProjectDirs(store));
|
|
38
38
|
}
|
|
39
39
|
_cachedStoreProjectDirs = dirs;
|
|
40
40
|
_cachedStorePhrenPath = phrenPath;
|
package/mcp/dist/shell/view.js
CHANGED
|
@@ -457,7 +457,8 @@ function renderMemoryQueueView(ctx, cursor, height) {
|
|
|
457
457
|
return vp.lines;
|
|
458
458
|
}
|
|
459
459
|
export function getProjectSkills(phrenPath, project) {
|
|
460
|
-
|
|
460
|
+
const storePath = resolveProjectStorePath(phrenPath, project);
|
|
461
|
+
return getScopedSkills(storePath, "", project).map((skill) => ({
|
|
461
462
|
name: skill.name,
|
|
462
463
|
path: skill.path,
|
|
463
464
|
enabled: skill.enabled,
|
|
@@ -541,10 +542,11 @@ const LIFECYCLE_HOOKS = [
|
|
|
541
542
|
export function getHookEntries(phrenPath, project) {
|
|
542
543
|
const prefs = readInstallPreferences(phrenPath);
|
|
543
544
|
const hooksEnabled = prefs.hooksEnabled !== false;
|
|
544
|
-
const
|
|
545
|
+
const storePath = project ? resolveProjectStorePath(phrenPath, project) : phrenPath;
|
|
546
|
+
const projectConfig = project ? readProjectConfig(storePath, project) : undefined;
|
|
545
547
|
return LIFECYCLE_HOOKS.map((h) => ({
|
|
546
548
|
...h,
|
|
547
|
-
enabled: hooksEnabled && isProjectHookEnabled(
|
|
549
|
+
enabled: hooksEnabled && isProjectHookEnabled(storePath, project, h.event, projectConfig),
|
|
548
550
|
}));
|
|
549
551
|
}
|
|
550
552
|
function renderHooksView(ctx, cursor, height) {
|
|
@@ -5,6 +5,7 @@ import * as yaml from "js-yaml";
|
|
|
5
5
|
import { expandHomePath, atomicWriteText } from "./phren-paths.js";
|
|
6
6
|
import { withFileLock } from "./governance/locks.js";
|
|
7
7
|
import { isRecord, PhrenError } from "./phren-core.js";
|
|
8
|
+
import { getProjectDirs } from "./shared.js";
|
|
8
9
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
9
10
|
const STORES_FILENAME = "stores.yaml";
|
|
10
11
|
const TEAM_BOOTSTRAP_FILENAME = ".phren-team.yaml";
|
|
@@ -110,6 +111,14 @@ export function getNonPrimaryStores(phrenPath) {
|
|
|
110
111
|
export function findStoreByName(phrenPath, name) {
|
|
111
112
|
return resolveAllStores(phrenPath).find((s) => s.name === name);
|
|
112
113
|
}
|
|
114
|
+
/** Get project directories for a store, filtered by the store's subscription list (if set). */
|
|
115
|
+
export function getStoreProjectDirs(store) {
|
|
116
|
+
const allDirs = getProjectDirs(store.path);
|
|
117
|
+
if (!store.projects || store.projects.length === 0)
|
|
118
|
+
return allDirs;
|
|
119
|
+
const allowed = new Set(store.projects);
|
|
120
|
+
return allDirs.filter(dir => path.basename(dir) !== "global" && allowed.has(path.basename(dir)));
|
|
121
|
+
}
|
|
113
122
|
// ── Team bootstrap ───────────────────────────────────────────────────────────
|
|
114
123
|
export function readTeamBootstrap(storePath) {
|
|
115
124
|
const filePath = path.join(storePath, TEAM_BOOTSTRAP_FILENAME);
|
|
@@ -178,6 +187,38 @@ export function updateStoreProjects(phrenPath, storeName, projects) {
|
|
|
178
187
|
writeStoreRegistry(phrenPath, registry);
|
|
179
188
|
});
|
|
180
189
|
}
|
|
190
|
+
/** Add projects to a store's subscription list. Deduplicates. Uses file locking. */
|
|
191
|
+
export function subscribeStoreProjects(phrenPath, storeName, projects) {
|
|
192
|
+
withFileLock(storesFilePath(phrenPath), () => {
|
|
193
|
+
const registry = readStoreRegistry(phrenPath);
|
|
194
|
+
if (!registry)
|
|
195
|
+
throw new Error(`${PhrenError.FILE_NOT_FOUND}: No stores.yaml found`);
|
|
196
|
+
const store = registry.stores.find((s) => s.name === storeName);
|
|
197
|
+
if (!store)
|
|
198
|
+
throw new Error(`${PhrenError.NOT_FOUND}: Store "${storeName}" not found`);
|
|
199
|
+
const existing = new Set(store.projects || []);
|
|
200
|
+
for (const project of projects) {
|
|
201
|
+
existing.add(project);
|
|
202
|
+
}
|
|
203
|
+
store.projects = Array.from(existing).sort().length > 0 ? Array.from(existing).sort() : undefined;
|
|
204
|
+
writeStoreRegistry(phrenPath, registry);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/** Remove projects from a store's subscription list. Uses file locking. */
|
|
208
|
+
export function unsubscribeStoreProjects(phrenPath, storeName, projects) {
|
|
209
|
+
withFileLock(storesFilePath(phrenPath), () => {
|
|
210
|
+
const registry = readStoreRegistry(phrenPath);
|
|
211
|
+
if (!registry)
|
|
212
|
+
throw new Error(`${PhrenError.FILE_NOT_FOUND}: No stores.yaml found`);
|
|
213
|
+
const store = registry.stores.find((s) => s.name === storeName);
|
|
214
|
+
if (!store)
|
|
215
|
+
throw new Error(`${PhrenError.NOT_FOUND}: Store "${storeName}" not found`);
|
|
216
|
+
const toRemove = new Set(projects);
|
|
217
|
+
const remaining = (store.projects || []).filter((p) => !toRemove.has(p));
|
|
218
|
+
store.projects = remaining.length > 0 ? remaining : undefined;
|
|
219
|
+
writeStoreRegistry(phrenPath, registry);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
181
222
|
// ── Validation ───────────────────────────────────────────────────────────────
|
|
182
223
|
function validateRegistry(registry) {
|
|
183
224
|
if (registry.version !== 1)
|
package/mcp/dist/tools/data.js
CHANGED
|
@@ -282,15 +282,16 @@ export function register(server, ctx) {
|
|
|
282
282
|
}, async ({ project, action }) => {
|
|
283
283
|
if (!isValidProjectName(project))
|
|
284
284
|
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
285
|
+
const resolved = resolveStoreForProject(ctx, project);
|
|
285
286
|
return withWriteQueue(async () => {
|
|
286
|
-
const activeProject = findProjectNameCaseInsensitive(phrenPath, project);
|
|
287
|
-
const archivedProject = findArchivedProjectNameCaseInsensitive(phrenPath, project);
|
|
287
|
+
const activeProject = findProjectNameCaseInsensitive(resolved.phrenPath, project);
|
|
288
|
+
const archivedProject = findArchivedProjectNameCaseInsensitive(resolved.phrenPath, project);
|
|
288
289
|
if (action === "archive") {
|
|
289
290
|
if (!activeProject) {
|
|
290
291
|
return mcpResponse({ ok: false, error: `Project "${project}" not found.` });
|
|
291
292
|
}
|
|
292
|
-
const projectDir = path.join(phrenPath, activeProject);
|
|
293
|
-
const archiveDir = path.join(phrenPath, `${activeProject}.archived`);
|
|
293
|
+
const projectDir = path.join(resolved.phrenPath, activeProject);
|
|
294
|
+
const archiveDir = path.join(resolved.phrenPath, `${activeProject}.archived`);
|
|
294
295
|
if (!fs.existsSync(projectDir)) {
|
|
295
296
|
return mcpResponse({ ok: false, error: `Project "${project}" not found.` });
|
|
296
297
|
}
|
|
@@ -316,12 +317,12 @@ export function register(server, ctx) {
|
|
|
316
317
|
return mcpResponse({ ok: false, error: `Project "${activeProject}" already exists as an active project.` });
|
|
317
318
|
}
|
|
318
319
|
if (!archivedProject) {
|
|
319
|
-
const entries = fs.readdirSync(phrenPath).filter((e) => e.endsWith(".archived"));
|
|
320
|
+
const entries = fs.readdirSync(resolved.phrenPath).filter((e) => e.endsWith(".archived"));
|
|
320
321
|
const available = entries.map((e) => e.replace(/\.archived$/, ""));
|
|
321
322
|
return mcpResponse({ ok: false, error: `No archive found for "${project}".`, data: { availableArchives: available } });
|
|
322
323
|
}
|
|
323
|
-
const projectDir = path.join(phrenPath, archivedProject);
|
|
324
|
-
const archiveDir = path.join(phrenPath, `${archivedProject}.archived`);
|
|
324
|
+
const projectDir = path.join(resolved.phrenPath, archivedProject);
|
|
325
|
+
const archiveDir = path.join(resolved.phrenPath, `${archivedProject}.archived`);
|
|
325
326
|
fs.renameSync(archiveDir, projectDir);
|
|
326
327
|
try {
|
|
327
328
|
await rebuildIndex();
|
package/mcp/dist/tools/search.js
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { createHash } from "crypto";
|
|
6
|
+
import { execFileSync } from "child_process";
|
|
6
7
|
import { isValidProjectName, errorMessage } from "../utils.js";
|
|
7
8
|
import { resolveAllStores } from "../store-registry.js";
|
|
8
9
|
import { readFindings } from "../data/access.js";
|
|
@@ -529,14 +530,13 @@ async function handleListProjects(ctx, { page, page_size }) {
|
|
|
529
530
|
? projectRows.map(row => decodeStringRow(row, 1, "list_projects.projects")[0])
|
|
530
531
|
: [];
|
|
531
532
|
// Gather projects from non-primary stores
|
|
532
|
-
const { getNonPrimaryStores } = await import("../store-registry.js");
|
|
533
|
-
const { getProjectDirs } = await import("../phren-paths.js");
|
|
533
|
+
const { getNonPrimaryStores, getStoreProjectDirs } = await import("../store-registry.js");
|
|
534
534
|
const nonPrimaryStores = getNonPrimaryStores(phrenPath);
|
|
535
535
|
const storeProjects = [];
|
|
536
536
|
for (const store of nonPrimaryStores) {
|
|
537
537
|
if (!fs.existsSync(store.path))
|
|
538
538
|
continue;
|
|
539
|
-
const dirs =
|
|
539
|
+
const dirs = getStoreProjectDirs(store);
|
|
540
540
|
for (const dir of dirs) {
|
|
541
541
|
const projName = path.basename(dir);
|
|
542
542
|
storeProjects.push({ name: projName, store: store.name });
|
|
@@ -691,12 +691,11 @@ async function handleStoreList(ctx) {
|
|
|
691
691
|
const { phrenPath } = ctx;
|
|
692
692
|
const stores = resolveAllStores(phrenPath);
|
|
693
693
|
const storeData = stores.map((store) => {
|
|
694
|
-
|
|
694
|
+
// Get last sync time from git log
|
|
695
|
+
let lastSync = null;
|
|
695
696
|
try {
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
health = JSON.parse(fs.readFileSync(healthPath, "utf8"))?.lastSync ?? null;
|
|
699
|
-
}
|
|
697
|
+
const result = execFileSync("git", ["log", "-1", "--format=%ci"], { cwd: store.path, encoding: "utf8", timeout: 3000 }).trim();
|
|
698
|
+
lastSync = result || null;
|
|
700
699
|
}
|
|
701
700
|
catch { /* non-critical */ }
|
|
702
701
|
return {
|
|
@@ -708,7 +707,7 @@ async function handleStoreList(ctx) {
|
|
|
708
707
|
remote: store.remote ?? null,
|
|
709
708
|
exists: fs.existsSync(store.path),
|
|
710
709
|
projects: store.projects ?? null,
|
|
711
|
-
lastSync
|
|
710
|
+
lastSync,
|
|
712
711
|
};
|
|
713
712
|
});
|
|
714
713
|
const lines = storeData.map((s) => {
|
|
@@ -355,11 +355,11 @@ export async function getSessionArtifacts(phrenPath, sessionId, project) {
|
|
|
355
355
|
}
|
|
356
356
|
// Team store projects
|
|
357
357
|
try {
|
|
358
|
-
const { getNonPrimaryStores } = await import("../store-registry.js");
|
|
358
|
+
const { getNonPrimaryStores, getStoreProjectDirs } = await import("../store-registry.js");
|
|
359
359
|
for (const store of getNonPrimaryStores(phrenPath)) {
|
|
360
360
|
if (!fs.existsSync(store.path))
|
|
361
361
|
continue;
|
|
362
|
-
const storeDirs =
|
|
362
|
+
const storeDirs = getStoreProjectDirs(store).map((d) => path.basename(d)).filter((p) => p !== "global");
|
|
363
363
|
const storeTargetProjects = project ? (storeDirs.includes(project) ? [project] : []) : storeDirs;
|
|
364
364
|
for (const proj of storeTargetProjects) {
|
|
365
365
|
readProjectArtifacts(store.path, proj);
|
package/mcp/dist/ui/data.js
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "fs";
|
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { createHash } from "crypto";
|
|
4
4
|
import { getProjectDirs, runtimeDir, runtimeHealthFile, memoryUsageLogFile, homePath, } from "../shared.js";
|
|
5
|
-
import { getNonPrimaryStores } from "../store-registry.js";
|
|
5
|
+
import { getNonPrimaryStores, getStoreProjectDirs } from "../store-registry.js";
|
|
6
6
|
import { errorMessage } from "../utils.js";
|
|
7
7
|
import { readInstallPreferences } from "../init/preferences.js";
|
|
8
8
|
import { readCustomHooks } from "../hooks.js";
|
|
@@ -158,11 +158,15 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
158
158
|
.filter((project) => project !== "global");
|
|
159
159
|
for (const project of primaryProjects)
|
|
160
160
|
storeProjects.push({ storePath: phrenPath, project });
|
|
161
|
+
// Map store paths to store names
|
|
162
|
+
const storePathToName = new Map();
|
|
163
|
+
storePathToName.set(phrenPath, "primary");
|
|
161
164
|
for (const store of getNonPrimaryStores(phrenPath)) {
|
|
162
165
|
if (!fs.existsSync(store.path))
|
|
163
166
|
continue;
|
|
167
|
+
storePathToName.set(store.path, store.name);
|
|
164
168
|
try {
|
|
165
|
-
const storeProjectDirs =
|
|
169
|
+
const storeProjectDirs = getStoreProjectDirs(store)
|
|
166
170
|
.map((projectDir) => path.basename(projectDir))
|
|
167
171
|
.filter((project) => project !== "global");
|
|
168
172
|
for (const project of storeProjectDirs)
|
|
@@ -187,6 +191,7 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
187
191
|
}
|
|
188
192
|
}
|
|
189
193
|
const findingsPath = path.join(storePath, project, "FINDINGS.md");
|
|
194
|
+
const storeName = storePathToName.get(storePath) || "unknown";
|
|
190
195
|
if (!fs.existsSync(findingsPath)) {
|
|
191
196
|
if (!addedProjectNodeIds.has(project)) {
|
|
192
197
|
addedProjectNodeIds.add(project);
|
|
@@ -197,6 +202,7 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
197
202
|
group: "project",
|
|
198
203
|
refCount: 0,
|
|
199
204
|
project,
|
|
205
|
+
store: storeName,
|
|
200
206
|
tagged: false,
|
|
201
207
|
});
|
|
202
208
|
}
|
|
@@ -211,6 +217,7 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
211
217
|
group: "project",
|
|
212
218
|
refCount: 1,
|
|
213
219
|
project,
|
|
220
|
+
store: storeName,
|
|
214
221
|
tagged: false,
|
|
215
222
|
});
|
|
216
223
|
}
|
|
@@ -263,6 +270,7 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
263
270
|
group: `topic:${topic.slug}`,
|
|
264
271
|
refCount: taggedCount,
|
|
265
272
|
project,
|
|
273
|
+
store: storeName,
|
|
266
274
|
tagged: true,
|
|
267
275
|
scoreKey,
|
|
268
276
|
scoreKeys: [scoreKey],
|
|
@@ -297,6 +305,7 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
297
305
|
group: `topic:${topic.slug}`,
|
|
298
306
|
refCount: taggedCount,
|
|
299
307
|
project,
|
|
308
|
+
store: storeName,
|
|
300
309
|
tagged: true,
|
|
301
310
|
scoreKey,
|
|
302
311
|
scoreKeys: [scoreKey],
|
|
@@ -331,6 +340,7 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
331
340
|
group: `topic:${topic.slug}`,
|
|
332
341
|
refCount: untaggedAdded,
|
|
333
342
|
project,
|
|
343
|
+
store: storeName,
|
|
334
344
|
tagged: false,
|
|
335
345
|
scoreKey,
|
|
336
346
|
scoreKeys: [scoreKey],
|
|
@@ -347,6 +357,7 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
347
357
|
const taskResult = readTasks(storePath, project);
|
|
348
358
|
if (!taskResult.ok)
|
|
349
359
|
continue;
|
|
360
|
+
const taskStoreName = storePathToName.get(storePath) || "unknown";
|
|
350
361
|
const doc = taskResult.data;
|
|
351
362
|
let taskCount = 0;
|
|
352
363
|
const MAX_TASKS = 50;
|
|
@@ -364,6 +375,7 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
364
375
|
fullLabel: item.line,
|
|
365
376
|
group,
|
|
366
377
|
project,
|
|
378
|
+
store: taskStoreName,
|
|
367
379
|
tagged: false,
|
|
368
380
|
scoreKey,
|
|
369
381
|
scoreKeys: [scoreKey],
|
|
@@ -431,6 +443,14 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
431
443
|
.map((ref) => ref.scoreKey)
|
|
432
444
|
.filter((key) => Boolean(key))
|
|
433
445
|
.sort();
|
|
446
|
+
// Only include entities that link to at least one visible project
|
|
447
|
+
const linkedProjects = new Set();
|
|
448
|
+
for (const ref of refs) {
|
|
449
|
+
if (ref.project && projectSet.has(ref.project))
|
|
450
|
+
linkedProjects.add(ref.project);
|
|
451
|
+
}
|
|
452
|
+
if (linkedProjects.size === 0)
|
|
453
|
+
continue; // skip orphan entities from other profiles/stores
|
|
434
454
|
const nodeId = `entity:${stableId("entity", type, name)}`;
|
|
435
455
|
nodes.push({
|
|
436
456
|
id: nodeId,
|
|
@@ -445,12 +465,6 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
445
465
|
entityType: type,
|
|
446
466
|
refDocs: refs,
|
|
447
467
|
});
|
|
448
|
-
// Link fragment to each project it appears in
|
|
449
|
-
const linkedProjects = new Set();
|
|
450
|
-
for (const ref of refs) {
|
|
451
|
-
if (ref.project && projectSet.has(ref.project))
|
|
452
|
-
linkedProjects.add(ref.project);
|
|
453
|
-
}
|
|
454
468
|
for (const proj of linkedProjects) {
|
|
455
469
|
links.push({ source: nodeId, target: proj });
|
|
456
470
|
}
|
|
@@ -471,6 +485,7 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
471
485
|
// ── Reference docs ────────────────────────────────────────────────
|
|
472
486
|
try {
|
|
473
487
|
for (const { storePath, project } of storeProjects) {
|
|
488
|
+
const refStoreName = storePathToName.get(storePath) || "unknown";
|
|
474
489
|
const refDir = path.join(storePath, project, "reference");
|
|
475
490
|
if (!fs.existsSync(refDir) || !fs.statSync(refDir).isDirectory())
|
|
476
491
|
continue;
|
|
@@ -488,6 +503,7 @@ export async function buildGraph(phrenPath, profile, focusProject, existingDb) {
|
|
|
488
503
|
fullLabel: file,
|
|
489
504
|
group: "reference",
|
|
490
505
|
project,
|
|
506
|
+
store: refStoreName,
|
|
491
507
|
tagged: false,
|
|
492
508
|
scoreKeys: [],
|
|
493
509
|
refDocs: [{ doc: docRef, project }],
|
|
@@ -640,7 +656,7 @@ export function collectProjectsForUI(phrenPath, profile) {
|
|
|
640
656
|
for (const store of teamStores) {
|
|
641
657
|
if (!fs.existsSync(store.path))
|
|
642
658
|
continue;
|
|
643
|
-
const teamProjects =
|
|
659
|
+
const teamProjects = getStoreProjectDirs(store).map((d) => path.basename(d)).filter((p) => p !== "global");
|
|
644
660
|
for (const project of teamProjects) {
|
|
645
661
|
if (seen.has(project))
|
|
646
662
|
continue; // skip if same name exists in primary
|
package/mcp/dist/ui/page.js
CHANGED
|
@@ -203,6 +203,7 @@ ${REVIEW_UI_STYLES}
|
|
|
203
203
|
<button id="graph-zoom-in" title="Zoom in">+</button>
|
|
204
204
|
<button id="graph-zoom-out" title="Zoom out">-</button>
|
|
205
205
|
<button id="graph-reset" title="Reset view">R</button>
|
|
206
|
+
<button id="graph-reset-layout" title="Re-run layout">L</button>
|
|
206
207
|
</div>
|
|
207
208
|
<div class="graph-filters">
|
|
208
209
|
<div class="graph-filter" id="graph-filter"></div>
|
package/mcp/dist/ui/scripts.js
CHANGED
|
@@ -1466,6 +1466,8 @@ export function renderEventWiringScript() {
|
|
|
1466
1466
|
if (graphZoomOut) graphZoomOut.addEventListener('click', function() { graphZoom(0.8); });
|
|
1467
1467
|
var graphResetBtn = document.getElementById('graph-reset');
|
|
1468
1468
|
if (graphResetBtn) graphResetBtn.addEventListener('click', function() { graphReset(); });
|
|
1469
|
+
var graphResetLayoutBtn = document.getElementById('graph-reset-layout');
|
|
1470
|
+
if (graphResetLayoutBtn) graphResetLayoutBtn.addEventListener('click', function() { if (typeof graphResetLayout === 'function') graphResetLayout(); });
|
|
1469
1471
|
|
|
1470
1472
|
// --- Tasks filters ---
|
|
1471
1473
|
var tasksFilterProject = document.getElementById('tasks-filter-project');
|