@phren/cli 0.0.44 → 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.
@@ -4,6 +4,46 @@ import * as os from "os";
4
4
  import * as crypto from "crypto";
5
5
  import { globSync } from "glob";
6
6
  import { debugLog, appendIndexEvent, getProjectDirs, collectNativeMemoryFiles, runtimeFile, homeDir, readRootManifest, } from "../shared.js";
7
+ /**
8
+ * Cached store project dirs to avoid repeated dynamic imports in sync code paths.
9
+ * Populated by `refreshStoreProjectDirs()`, consumed by `getAllStoreProjectDirs()`.
10
+ */
11
+ let _cachedStoreProjectDirs = null;
12
+ let _cachedStorePhrenPath = null;
13
+ /**
14
+ * Gather project directories from the primary store AND all non-primary stores.
15
+ * This enables the FTS5 index to include team store projects alongside personal ones.
16
+ * Uses a sync cache populated by the async buildIndex path.
17
+ */
18
+ function getAllStoreProjectDirs(phrenPath, profile) {
19
+ const dirs = [...getProjectDirs(phrenPath, profile)];
20
+ if (_cachedStoreProjectDirs && _cachedStorePhrenPath === phrenPath) {
21
+ dirs.push(..._cachedStoreProjectDirs);
22
+ }
23
+ return dirs;
24
+ }
25
+ /**
26
+ * Refresh the store project dirs cache. Called from async contexts (buildIndex, etc.)
27
+ * before sync code paths that need getAllStoreProjectDirs.
28
+ */
29
+ async function refreshStoreProjectDirs(phrenPath, profile) {
30
+ try {
31
+ const { getNonPrimaryStores } = await import("../store-registry.js");
32
+ const otherStores = getNonPrimaryStores(phrenPath);
33
+ const dirs = [];
34
+ for (const store of otherStores) {
35
+ if (!fs.existsSync(store.path))
36
+ continue;
37
+ dirs.push(...getProjectDirs(store.path, profile));
38
+ }
39
+ _cachedStoreProjectDirs = dirs;
40
+ _cachedStorePhrenPath = phrenPath;
41
+ }
42
+ catch {
43
+ _cachedStoreProjectDirs = [];
44
+ _cachedStorePhrenPath = phrenPath;
45
+ }
46
+ }
7
47
  import { getIndexPolicy, withFileLock } from "./governance.js";
8
48
  import { stripTaskDoneSection } from "./content.js";
9
49
  import { isInactiveFindingLine } from "../finding/lifecycle.js";
@@ -191,7 +231,7 @@ function touchSentinel(phrenPath) {
191
231
  function computePhrenHash(phrenPath, profile, preGlobbed) {
192
232
  const policy = getIndexPolicy(phrenPath);
193
233
  const hash = crypto.createHash("sha1");
194
- const topicConfigEntries = getProjectDirs(phrenPath, profile)
234
+ const topicConfigEntries = getAllStoreProjectDirs(phrenPath, profile)
195
235
  .map((dir) => path.join(dir, "topic-config.json"))
196
236
  .filter((configPath) => fs.existsSync(configPath));
197
237
  if (preGlobbed) {
@@ -215,9 +255,9 @@ function computePhrenHash(phrenPath, profile, preGlobbed) {
215
255
  }
216
256
  }
217
257
  else {
218
- const projectDirs = getProjectDirs(phrenPath, profile);
258
+ const allProjectDirs = getAllStoreProjectDirs(phrenPath, profile);
219
259
  const files = [];
220
- for (const dir of projectDirs) {
260
+ for (const dir of allProjectDirs) {
221
261
  const projectName = path.basename(dir);
222
262
  const config = readProjectConfig(phrenPath, projectName);
223
263
  const ownership = getProjectOwnershipMode(phrenPath, projectName, config);
@@ -399,7 +439,7 @@ function getRepoManagedInstructionEntries(phrenPath, project) {
399
439
  return entries;
400
440
  }
401
441
  function globAllFiles(phrenPath, profile) {
402
- const projectDirs = getProjectDirs(phrenPath, profile);
442
+ const projectDirs = getAllStoreProjectDirs(phrenPath, profile);
403
443
  const indexPolicy = getIndexPolicy(phrenPath);
404
444
  const entries = [];
405
445
  const allAbsolutePaths = [];
@@ -826,7 +866,8 @@ function mergeManualLinks(db, phrenPath) {
826
866
  }
827
867
  async function buildIndexImpl(phrenPath, profile) {
828
868
  const t0 = Date.now();
829
- const projectDirs = getProjectDirs(phrenPath, profile);
869
+ await refreshStoreProjectDirs(phrenPath, profile);
870
+ const projectDirs = getAllStoreProjectDirs(phrenPath, profile);
830
871
  beginUserFragmentBuildCache(phrenPath, projectDirs.map(dir => path.basename(dir)));
831
872
  try {
832
873
  // ── Cache dir + hash sentinel ─────────────────────────────────────────────
@@ -1348,12 +1389,16 @@ export function detectProject(phrenPath, cwd, profile) {
1348
1389
  if (manifest?.installMode === "project-local") {
1349
1390
  return manifest.primaryProject || null;
1350
1391
  }
1351
- const projectDirs = getProjectDirs(phrenPath, profile);
1392
+ const projectDirs = getAllStoreProjectDirs(phrenPath, profile);
1352
1393
  const resolvedCwd = path.resolve(cwd);
1353
1394
  let bestMatch = null;
1354
1395
  for (const dir of projectDirs) {
1355
1396
  const projectName = path.basename(dir);
1356
- const sourcePath = getProjectSourcePath(phrenPath, projectName);
1397
+ // Try the project's own store path first (handles team store projects),
1398
+ // then fall back to primary phrenPath
1399
+ const storePhrenPath = path.dirname(dir);
1400
+ const sourcePath = getProjectSourcePath(storePhrenPath, projectName)
1401
+ || getProjectSourcePath(phrenPath, projectName);
1357
1402
  if (!sourcePath)
1358
1403
  continue;
1359
1404
  const matches = resolvedCwd === sourcePath || resolvedCwd.startsWith(sourcePath + path.sep);
@@ -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;
@@ -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
- const dir = path.join(phrenPath, project);
546
- const findingsPath = path.join(dir, "FINDINGS.md");
547
- const taskPath = resolveTaskFilePath(phrenPath, project);
548
- const claudeMdPath = path.join(dir, "CLAUDE.md");
549
- const summaryPath = path.join(dir, "summary.md");
550
- const refPath = path.join(dir, "reference");
551
- let findingCount = 0;
552
- if (fs.existsSync(findingsPath)) {
553
- const content = fs.readFileSync(findingsPath, "utf8");
554
- findingCount = (content.match(/^- /gm) || []).length;
555
- }
556
- const sparkline = new Array(8).fill(0);
557
- if (fs.existsSync(findingsPath)) {
558
- const now = Date.now();
559
- const weekMs = 7 * 24 * 60 * 60 * 1000;
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
- let taskCount = 0;
571
- if (taskPath && fs.existsSync(taskPath)) {
572
- const content = fs.readFileSync(taskPath, "utf8");
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
  }
@@ -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 filePath = safeProjectPath(ctx.phrenPath, project, file);
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
- jsonOk(res, { ok: true, ...getProjectTopicsResponse(ctx.phrenPath, project) });
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
- jsonOk(res, { ok: true, ...listProjectReferenceDocs(ctx.phrenPath, project) });
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 contentResult = readReferenceContent(ctx.phrenPath, String(qs.project || ""), String(qs.file || ""));
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.44",
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.27.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.1",
34
- "@typescript-eslint/parser": "^8.57.1",
35
- "@vitest/coverage-v8": "^4.1.0",
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": "^5.9.3",
39
- "vitest": "^4.1.0"
38
+ "typescript": "^6.0.2",
39
+ "vitest": "^4.1.2"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "node scripts/build.mjs",