@phren/cli 0.0.45 → 0.0.46
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 +19 -0
- package/mcp/dist/entrypoint.js +4 -1
- package/mcp/dist/generated/memory-ui-graph.browser.js +1 -1
- package/mcp/dist/link/doctor.js +43 -0
- package/mcp/dist/memory-ui-graph.runtime.js +1 -1
- package/mcp/dist/status.js +27 -0
- package/mcp/dist/ui/data.js +80 -54
- package/mcp/dist/ui/server.js +24 -4
- package/package.json +8 -8
package/mcp/dist/status.js
CHANGED
|
@@ -284,6 +284,33 @@ export async function runStatus() {
|
|
|
284
284
|
if (missingAgents.length > 0) {
|
|
285
285
|
console.log(` ${DIM} Not configured: ${missingAgents.join(", ")} — run phren init to add${RESET}`);
|
|
286
286
|
}
|
|
287
|
+
// Stores
|
|
288
|
+
try {
|
|
289
|
+
const { resolveAllStores } = await import("./store-registry.js");
|
|
290
|
+
const stores = resolveAllStores(phrenPath);
|
|
291
|
+
if (stores.length > 0) {
|
|
292
|
+
const primaryCount = stores.filter((s) => s.role === "primary").length;
|
|
293
|
+
const teamCount = stores.filter((s) => s.role === "team").length;
|
|
294
|
+
const readonlyCount = stores.filter((s) => s.role === "readonly").length;
|
|
295
|
+
const roleParts = [];
|
|
296
|
+
if (primaryCount > 0)
|
|
297
|
+
roleParts.push(`${primaryCount} primary`);
|
|
298
|
+
if (teamCount > 0)
|
|
299
|
+
roleParts.push(`${teamCount} team`);
|
|
300
|
+
if (readonlyCount > 0)
|
|
301
|
+
roleParts.push(`${readonlyCount} readonly`);
|
|
302
|
+
console.log(`\n ${BOLD}Stores${RESET} ${DIM}(${stores.length} stores: ${roleParts.join(", ")})${RESET}`);
|
|
303
|
+
for (const store of stores) {
|
|
304
|
+
const exists = fs.existsSync(store.path);
|
|
305
|
+
const existsLabel = exists ? `${GREEN}yes${RESET}` : `${RED}no${RESET}`;
|
|
306
|
+
console.log(` ${store.name} ${DIM}(${store.role}, ${store.sync})${RESET} path=${existsLabel}${store.remote ? ` remote=${DIM}${store.remote}${RESET}` : ""}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
if ((process.env.PHREN_DEBUG))
|
|
312
|
+
logger.debug("status", `statusStores: ${errorMessage(err)}`);
|
|
313
|
+
}
|
|
287
314
|
// Stats
|
|
288
315
|
const projectDirs = getProjectDirs(phrenPath, profile);
|
|
289
316
|
let totalFindings = 0;
|
package/mcp/dist/ui/data.js
CHANGED
|
@@ -2,6 +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
6
|
import { errorMessage } from "../utils.js";
|
|
6
7
|
import { readInstallPreferences } from "../init/preferences.js";
|
|
7
8
|
import { readCustomHooks } from "../hooks.js";
|
|
@@ -520,6 +521,66 @@ export function recentAccepted(phrenPath) {
|
|
|
520
521
|
const lines = fs.readFileSync(audit, "utf8").split("\n").filter((line) => line.includes("approve_memory"));
|
|
521
522
|
return lines.slice(-40).reverse();
|
|
522
523
|
}
|
|
524
|
+
function buildProjectInfo(basePath, project, store) {
|
|
525
|
+
const dir = path.join(basePath, project);
|
|
526
|
+
const findingsPath = path.join(dir, "FINDINGS.md");
|
|
527
|
+
const taskPath = resolveTaskFilePath(basePath, project);
|
|
528
|
+
const claudeMdPath = path.join(dir, "CLAUDE.md");
|
|
529
|
+
const summaryPath = path.join(dir, "summary.md");
|
|
530
|
+
const refPath = path.join(dir, "reference");
|
|
531
|
+
let findingCount = 0;
|
|
532
|
+
if (fs.existsSync(findingsPath)) {
|
|
533
|
+
const content = fs.readFileSync(findingsPath, "utf8");
|
|
534
|
+
findingCount = (content.match(/^- /gm) || []).length;
|
|
535
|
+
}
|
|
536
|
+
const sparkline = new Array(8).fill(0);
|
|
537
|
+
if (fs.existsSync(findingsPath)) {
|
|
538
|
+
const now = Date.now();
|
|
539
|
+
const weekMs = 7 * 24 * 60 * 60 * 1000;
|
|
540
|
+
const sparkContent = fs.readFileSync(findingsPath, "utf8");
|
|
541
|
+
const dateRe = /(?:created[_:]?\s*"?|created_at[":]+\s*)(\d{4}-\d{2}-\d{2})/g;
|
|
542
|
+
let match;
|
|
543
|
+
while ((match = dateRe.exec(sparkContent)) !== null) {
|
|
544
|
+
const age = now - new Date(match[1]).getTime();
|
|
545
|
+
const weekIdx = Math.floor(age / weekMs);
|
|
546
|
+
if (weekIdx >= 0 && weekIdx < 8)
|
|
547
|
+
sparkline[7 - weekIdx]++;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
let taskCount = 0;
|
|
551
|
+
if (taskPath && fs.existsSync(taskPath)) {
|
|
552
|
+
const content = fs.readFileSync(taskPath, "utf8");
|
|
553
|
+
const queueMatch = content.match(/## Queue[\s\S]*?(?=## |$)/);
|
|
554
|
+
if (queueMatch)
|
|
555
|
+
taskCount = (queueMatch[0].match(/^- /gm) || []).length;
|
|
556
|
+
}
|
|
557
|
+
let summaryText = "";
|
|
558
|
+
if (fs.existsSync(summaryPath)) {
|
|
559
|
+
summaryText = fs.readFileSync(summaryPath, "utf8").trim();
|
|
560
|
+
if (summaryText.length > 300)
|
|
561
|
+
summaryText = `${summaryText.slice(0, 300)}...`;
|
|
562
|
+
}
|
|
563
|
+
let githubUrl;
|
|
564
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
565
|
+
githubUrl = extractGithubUrl(fs.readFileSync(claudeMdPath, "utf8"));
|
|
566
|
+
}
|
|
567
|
+
if (!githubUrl && fs.existsSync(summaryPath)) {
|
|
568
|
+
githubUrl = extractGithubUrl(fs.readFileSync(summaryPath, "utf8"));
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
name: project,
|
|
572
|
+
storePath: basePath,
|
|
573
|
+
store,
|
|
574
|
+
findingCount,
|
|
575
|
+
taskCount,
|
|
576
|
+
hasClaudeMd: fs.existsSync(claudeMdPath),
|
|
577
|
+
hasSummary: fs.existsSync(summaryPath),
|
|
578
|
+
hasReference: fs.existsSync(refPath) && fs.statSync(refPath).isDirectory(),
|
|
579
|
+
summaryText,
|
|
580
|
+
githubUrl,
|
|
581
|
+
sparkline,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
523
584
|
export function collectProjectsForUI(phrenPath, profile) {
|
|
524
585
|
const projects = getProjectDirs(phrenPath, profile).map((projectDir) => path.basename(projectDir)).filter((project) => project !== "global");
|
|
525
586
|
let allowedProjects = null;
|
|
@@ -539,65 +600,30 @@ export function collectProjectsForUI(phrenPath, profile) {
|
|
|
539
600
|
logger.debug("memory-ui", `memory-ui filterByProfile: ${errorMessage(err)}`);
|
|
540
601
|
}
|
|
541
602
|
const results = [];
|
|
603
|
+
const seen = new Set();
|
|
542
604
|
for (const project of projects) {
|
|
543
605
|
if (allowedProjects && !allowedProjects.has(project.toLowerCase()))
|
|
544
606
|
continue;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const sparkContent = fs.readFileSync(findingsPath, "utf8");
|
|
561
|
-
const dateRe = /(?:created[_:]?\s*"?|created_at[":]+\s*)(\d{4}-\d{2}-\d{2})/g;
|
|
562
|
-
let match;
|
|
563
|
-
while ((match = dateRe.exec(sparkContent)) !== null) {
|
|
564
|
-
const age = now - new Date(match[1]).getTime();
|
|
565
|
-
const weekIdx = Math.floor(age / weekMs);
|
|
566
|
-
if (weekIdx >= 0 && weekIdx < 8)
|
|
567
|
-
sparkline[7 - weekIdx]++;
|
|
607
|
+
seen.add(project);
|
|
608
|
+
results.push(buildProjectInfo(phrenPath, project));
|
|
609
|
+
}
|
|
610
|
+
// Include projects from non-primary stores
|
|
611
|
+
try {
|
|
612
|
+
const teamStores = getNonPrimaryStores(phrenPath);
|
|
613
|
+
for (const store of teamStores) {
|
|
614
|
+
if (!fs.existsSync(store.path))
|
|
615
|
+
continue;
|
|
616
|
+
const teamProjects = getProjectDirs(store.path).map((d) => path.basename(d)).filter((p) => p !== "global");
|
|
617
|
+
for (const project of teamProjects) {
|
|
618
|
+
if (seen.has(project))
|
|
619
|
+
continue; // skip if same name exists in primary
|
|
620
|
+
seen.add(project);
|
|
621
|
+
results.push(buildProjectInfo(store.path, project, store.name));
|
|
568
622
|
}
|
|
569
623
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const queueMatch = content.match(/## Queue[\s\S]*?(?=## |$)/);
|
|
574
|
-
if (queueMatch)
|
|
575
|
-
taskCount = (queueMatch[0].match(/^- /gm) || []).length;
|
|
576
|
-
}
|
|
577
|
-
let summaryText = "";
|
|
578
|
-
if (fs.existsSync(summaryPath)) {
|
|
579
|
-
summaryText = fs.readFileSync(summaryPath, "utf8").trim();
|
|
580
|
-
if (summaryText.length > 300)
|
|
581
|
-
summaryText = `${summaryText.slice(0, 300)}...`;
|
|
582
|
-
}
|
|
583
|
-
let githubUrl;
|
|
584
|
-
if (fs.existsSync(claudeMdPath)) {
|
|
585
|
-
githubUrl = extractGithubUrl(fs.readFileSync(claudeMdPath, "utf8"));
|
|
586
|
-
}
|
|
587
|
-
if (!githubUrl && fs.existsSync(summaryPath)) {
|
|
588
|
-
githubUrl = extractGithubUrl(fs.readFileSync(summaryPath, "utf8"));
|
|
589
|
-
}
|
|
590
|
-
results.push({
|
|
591
|
-
name: project,
|
|
592
|
-
findingCount,
|
|
593
|
-
taskCount,
|
|
594
|
-
hasClaudeMd: fs.existsSync(claudeMdPath),
|
|
595
|
-
hasSummary: fs.existsSync(summaryPath),
|
|
596
|
-
hasReference: fs.existsSync(refPath) && fs.statSync(refPath).isDirectory(),
|
|
597
|
-
summaryText,
|
|
598
|
-
githubUrl,
|
|
599
|
-
sparkline,
|
|
600
|
-
});
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
logger.debug("memory-ui", `collectProjectsForUI team stores: ${errorMessage(err)}`);
|
|
601
627
|
}
|
|
602
628
|
return results.sort((a, b) => (b.findingCount + b.taskCount) - (a.findingCount + a.taskCount));
|
|
603
629
|
}
|
package/mcp/dist/ui/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as path from "path";
|
|
|
6
6
|
import * as querystring from "querystring";
|
|
7
7
|
import { spawn, execFileSync } from "child_process";
|
|
8
8
|
import { computePhrenLiveStateToken, getProjectDirs, } from "../shared.js";
|
|
9
|
+
import { getNonPrimaryStores } from "../store-registry.js";
|
|
9
10
|
import { editFinding, readReviewQueue, removeFinding, readFindings, addFinding as addFindingStore, readTasksAcrossProjects, addTask as addTaskStore, completeTask as completeTaskStore, removeTask as removeTaskStore, updateTask as updateTaskStore, TASKS_FILENAME, } from "../data/access.js";
|
|
10
11
|
import { isValidProjectName, errorMessage, queueFilePath, safeProjectPath } from "../utils.js";
|
|
11
12
|
import { readInstallPreferences, writeInstallPreferences, writeGovernanceInstallPreferences } from "../init/preferences.js";
|
|
@@ -318,6 +319,20 @@ function handleGetHome(res, ctx) {
|
|
|
318
319
|
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
319
320
|
res.end(html);
|
|
320
321
|
}
|
|
322
|
+
/** Returns the store base path that contains the given project (primary or team store). */
|
|
323
|
+
function resolveProjectBasePath(phrenPath, project) {
|
|
324
|
+
const primaryDir = path.join(phrenPath, project);
|
|
325
|
+
if (fs.existsSync(primaryDir))
|
|
326
|
+
return phrenPath;
|
|
327
|
+
try {
|
|
328
|
+
for (const store of getNonPrimaryStores(phrenPath)) {
|
|
329
|
+
if (fs.existsSync(path.join(store.path, project)))
|
|
330
|
+
return store.path;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch { /* fall through */ }
|
|
334
|
+
return phrenPath;
|
|
335
|
+
}
|
|
321
336
|
function handleGetProjects(res, ctx) {
|
|
322
337
|
jsonOk(res, collectProjectsForUI(ctx.phrenPath, ctx.profile));
|
|
323
338
|
}
|
|
@@ -342,7 +357,8 @@ function handleGetProjectContent(res, url, ctx) {
|
|
|
342
357
|
const allowedFiles = ["FINDINGS.md", TASKS_FILENAME, "CLAUDE.md", "summary.md"];
|
|
343
358
|
if (!allowedFiles.includes(file))
|
|
344
359
|
return jsonErr(res, `File not allowed: ${file}`, 400);
|
|
345
|
-
const
|
|
360
|
+
const basePath = resolveProjectBasePath(ctx.phrenPath, project);
|
|
361
|
+
const filePath = safeProjectPath(basePath, project, file);
|
|
346
362
|
if (!filePath)
|
|
347
363
|
return jsonErr(res, "Invalid project or file path", 400);
|
|
348
364
|
if (!fs.existsSync(filePath))
|
|
@@ -353,17 +369,21 @@ function handleGetProjectTopics(res, url, ctx) {
|
|
|
353
369
|
const project = String(parseQs(url).project || "");
|
|
354
370
|
if (!project || !isValidProjectName(project))
|
|
355
371
|
return jsonErr(res, "Invalid project", 400);
|
|
356
|
-
|
|
372
|
+
const basePath = resolveProjectBasePath(ctx.phrenPath, project);
|
|
373
|
+
jsonOk(res, { ok: true, ...getProjectTopicsResponse(basePath, project) });
|
|
357
374
|
}
|
|
358
375
|
function handleGetProjectReferenceList(res, url, ctx) {
|
|
359
376
|
const project = String(parseQs(url).project || "");
|
|
360
377
|
if (!project || !isValidProjectName(project))
|
|
361
378
|
return jsonErr(res, "Invalid project", 400);
|
|
362
|
-
|
|
379
|
+
const basePath = resolveProjectBasePath(ctx.phrenPath, project);
|
|
380
|
+
jsonOk(res, { ok: true, ...listProjectReferenceDocs(basePath, project) });
|
|
363
381
|
}
|
|
364
382
|
function handleGetProjectReferenceContent(res, url, ctx) {
|
|
365
383
|
const qs = parseQs(url);
|
|
366
|
-
const
|
|
384
|
+
const project = String(qs.project || "");
|
|
385
|
+
const basePath = resolveProjectBasePath(ctx.phrenPath, project);
|
|
386
|
+
const contentResult = readReferenceContent(basePath, project, String(qs.file || ""));
|
|
367
387
|
res.writeHead(contentResult.ok ? 200 : 400, { "content-type": "application/json; charset=utf-8" });
|
|
368
388
|
res.end(JSON.stringify(contentResult.ok ? { ok: true, content: contentResult.content } : { ok: false, error: contentResult.error }));
|
|
369
389
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phren/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.46",
|
|
4
4
|
"description": "Knowledge layer for AI agents. Phren learns and recalls.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"scripts/preuninstall.mjs"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
18
18
|
"chalk": "^5.6.2",
|
|
19
19
|
"glob": "^13.0.6",
|
|
20
20
|
"graphology": "^0.26.0",
|
|
@@ -26,17 +26,17 @@
|
|
|
26
26
|
"zod": "^4.3.6"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"esbuild": "^0.27.4",
|
|
30
29
|
"@playwright/test": "^1.58.2",
|
|
31
30
|
"@types/js-yaml": "^4.0.9",
|
|
32
31
|
"@types/node": "^25.5.0",
|
|
33
|
-
"@typescript-eslint/eslint-plugin": "^8.57.
|
|
34
|
-
"@typescript-eslint/parser": "^8.57.
|
|
35
|
-
"@vitest/coverage-v8": "^4.1.
|
|
32
|
+
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
33
|
+
"@typescript-eslint/parser": "^8.57.2",
|
|
34
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
35
|
+
"esbuild": "^0.27.4",
|
|
36
36
|
"eslint": "^10.1.0",
|
|
37
37
|
"tsx": "^4.21.0",
|
|
38
|
-
"typescript": "^
|
|
39
|
-
"vitest": "^4.1.
|
|
38
|
+
"typescript": "^6.0.2",
|
|
39
|
+
"vitest": "^4.1.2"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "node scripts/build.mjs",
|