@phren/cli 0.0.1

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.
Files changed (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/mcp/dist/capabilities/cli.js +61 -0
  4. package/mcp/dist/capabilities/index.js +15 -0
  5. package/mcp/dist/capabilities/mcp.js +61 -0
  6. package/mcp/dist/capabilities/types.js +57 -0
  7. package/mcp/dist/capabilities/vscode.js +61 -0
  8. package/mcp/dist/capabilities/web-ui.js +61 -0
  9. package/mcp/dist/cli-actions.js +302 -0
  10. package/mcp/dist/cli-config.js +580 -0
  11. package/mcp/dist/cli-extract.js +305 -0
  12. package/mcp/dist/cli-govern.js +371 -0
  13. package/mcp/dist/cli-graph.js +169 -0
  14. package/mcp/dist/cli-hooks-citations.js +44 -0
  15. package/mcp/dist/cli-hooks-context.js +56 -0
  16. package/mcp/dist/cli-hooks-globs.js +83 -0
  17. package/mcp/dist/cli-hooks-output.js +130 -0
  18. package/mcp/dist/cli-hooks-retrieval.js +2 -0
  19. package/mcp/dist/cli-hooks-session.js +1402 -0
  20. package/mcp/dist/cli-hooks.js +350 -0
  21. package/mcp/dist/cli-namespaces.js +989 -0
  22. package/mcp/dist/cli-ops.js +253 -0
  23. package/mcp/dist/cli-search.js +407 -0
  24. package/mcp/dist/cli.js +108 -0
  25. package/mcp/dist/content-archive.js +278 -0
  26. package/mcp/dist/content-citation.js +391 -0
  27. package/mcp/dist/content-dedup.js +622 -0
  28. package/mcp/dist/content-learning.js +472 -0
  29. package/mcp/dist/content-metadata.js +186 -0
  30. package/mcp/dist/content-validate.js +462 -0
  31. package/mcp/dist/core-finding.js +54 -0
  32. package/mcp/dist/core-project.js +36 -0
  33. package/mcp/dist/core-search.js +50 -0
  34. package/mcp/dist/data-access.js +400 -0
  35. package/mcp/dist/data-tasks.js +821 -0
  36. package/mcp/dist/embedding.js +344 -0
  37. package/mcp/dist/entrypoint.js +387 -0
  38. package/mcp/dist/finding-context.js +172 -0
  39. package/mcp/dist/finding-impact.js +181 -0
  40. package/mcp/dist/finding-journal.js +122 -0
  41. package/mcp/dist/finding-lifecycle.js +259 -0
  42. package/mcp/dist/governance-audit.js +22 -0
  43. package/mcp/dist/governance-locks.js +96 -0
  44. package/mcp/dist/governance-policy.js +648 -0
  45. package/mcp/dist/governance-scores.js +355 -0
  46. package/mcp/dist/hooks.js +449 -0
  47. package/mcp/dist/impact-scoring.js +22 -0
  48. package/mcp/dist/index-query.js +168 -0
  49. package/mcp/dist/index.js +205 -0
  50. package/mcp/dist/init-config.js +336 -0
  51. package/mcp/dist/init-preferences.js +62 -0
  52. package/mcp/dist/init-setup.js +1305 -0
  53. package/mcp/dist/init-shared.js +29 -0
  54. package/mcp/dist/init.js +1730 -0
  55. package/mcp/dist/link-checksums.js +62 -0
  56. package/mcp/dist/link-context.js +257 -0
  57. package/mcp/dist/link-doctor.js +591 -0
  58. package/mcp/dist/link-skills.js +212 -0
  59. package/mcp/dist/link.js +596 -0
  60. package/mcp/dist/logger.js +15 -0
  61. package/mcp/dist/machine-identity.js +38 -0
  62. package/mcp/dist/mcp-config.js +254 -0
  63. package/mcp/dist/mcp-data.js +315 -0
  64. package/mcp/dist/mcp-extract-facts.js +78 -0
  65. package/mcp/dist/mcp-extract.js +133 -0
  66. package/mcp/dist/mcp-finding.js +557 -0
  67. package/mcp/dist/mcp-graph.js +339 -0
  68. package/mcp/dist/mcp-hooks.js +256 -0
  69. package/mcp/dist/mcp-memory.js +58 -0
  70. package/mcp/dist/mcp-ops.js +328 -0
  71. package/mcp/dist/mcp-search.js +628 -0
  72. package/mcp/dist/mcp-session.js +651 -0
  73. package/mcp/dist/mcp-skills.js +189 -0
  74. package/mcp/dist/mcp-tasks.js +551 -0
  75. package/mcp/dist/mcp-types.js +7 -0
  76. package/mcp/dist/memory-ui-assets.js +6 -0
  77. package/mcp/dist/memory-ui-data.js +513 -0
  78. package/mcp/dist/memory-ui-graph.js +1910 -0
  79. package/mcp/dist/memory-ui-page.js +353 -0
  80. package/mcp/dist/memory-ui-scripts.js +1387 -0
  81. package/mcp/dist/memory-ui-server.js +1218 -0
  82. package/mcp/dist/memory-ui-styles.js +555 -0
  83. package/mcp/dist/memory-ui.js +9 -0
  84. package/mcp/dist/package-metadata.js +13 -0
  85. package/mcp/dist/phren-art.js +52 -0
  86. package/mcp/dist/phren-core.js +108 -0
  87. package/mcp/dist/phren-dotenv.js +67 -0
  88. package/mcp/dist/phren-paths.js +476 -0
  89. package/mcp/dist/proactivity.js +172 -0
  90. package/mcp/dist/profile-store.js +228 -0
  91. package/mcp/dist/project-config.js +85 -0
  92. package/mcp/dist/project-locator.js +25 -0
  93. package/mcp/dist/project-topics.js +1134 -0
  94. package/mcp/dist/provider-adapters.js +176 -0
  95. package/mcp/dist/runtime-profile.js +18 -0
  96. package/mcp/dist/session-checkpoints.js +131 -0
  97. package/mcp/dist/session-utils.js +68 -0
  98. package/mcp/dist/shared-content.js +8 -0
  99. package/mcp/dist/shared-embedding-cache.js +143 -0
  100. package/mcp/dist/shared-fragment-graph.js +456 -0
  101. package/mcp/dist/shared-governance.js +4 -0
  102. package/mcp/dist/shared-index.js +1334 -0
  103. package/mcp/dist/shared-ollama.js +192 -0
  104. package/mcp/dist/shared-paths.js +1 -0
  105. package/mcp/dist/shared-retrieval.js +796 -0
  106. package/mcp/dist/shared-search-fallback.js +375 -0
  107. package/mcp/dist/shared-sqljs.js +42 -0
  108. package/mcp/dist/shared-stemmer.js +171 -0
  109. package/mcp/dist/shared-vector-index.js +199 -0
  110. package/mcp/dist/shared.js +114 -0
  111. package/mcp/dist/shell-entry.js +209 -0
  112. package/mcp/dist/shell-input.js +943 -0
  113. package/mcp/dist/shell-palette.js +119 -0
  114. package/mcp/dist/shell-render.js +252 -0
  115. package/mcp/dist/shell-state-store.js +81 -0
  116. package/mcp/dist/shell-types.js +13 -0
  117. package/mcp/dist/shell-view-list.js +14 -0
  118. package/mcp/dist/shell-view.js +707 -0
  119. package/mcp/dist/shell.js +352 -0
  120. package/mcp/dist/skill-files.js +117 -0
  121. package/mcp/dist/skill-registry.js +279 -0
  122. package/mcp/dist/skill-state.js +28 -0
  123. package/mcp/dist/startup-embedding.js +57 -0
  124. package/mcp/dist/status.js +323 -0
  125. package/mcp/dist/synonyms.json +670 -0
  126. package/mcp/dist/task-hygiene.js +251 -0
  127. package/mcp/dist/task-lifecycle.js +347 -0
  128. package/mcp/dist/tasks-github.js +76 -0
  129. package/mcp/dist/telemetry.js +165 -0
  130. package/mcp/dist/test-global-setup.js +37 -0
  131. package/mcp/dist/tool-registry.js +104 -0
  132. package/mcp/dist/update.js +97 -0
  133. package/mcp/dist/utils.js +543 -0
  134. package/package.json +67 -0
  135. package/skills/README.md +7 -0
  136. package/skills/consolidate/SKILL.md +152 -0
  137. package/skills/discover/SKILL.md +175 -0
  138. package/skills/init/SKILL.md +216 -0
  139. package/skills/profiles/SKILL.md +121 -0
  140. package/skills/sync/SKILL.md +261 -0
  141. package/starter/README.md +74 -0
  142. package/starter/global/CLAUDE.md +89 -0
  143. package/starter/global/skills/humanize.md +30 -0
  144. package/starter/global/skills/pipeline.md +35 -0
  145. package/starter/global/skills/release.md +35 -0
  146. package/starter/machines.yaml +8 -0
  147. package/starter/my-api/.claude/skills/README.md +7 -0
  148. package/starter/my-api/CLAUDE.md +33 -0
  149. package/starter/my-api/FINDINGS.md +9 -0
  150. package/starter/my-api/summary.md +7 -0
  151. package/starter/my-api/tasks.md +7 -0
  152. package/starter/my-first-project/.claude/skills/README.md +7 -0
  153. package/starter/my-first-project/CLAUDE.md +49 -0
  154. package/starter/my-first-project/FINDINGS.md +24 -0
  155. package/starter/my-first-project/summary.md +11 -0
  156. package/starter/my-first-project/tasks.md +25 -0
  157. package/starter/my-frontend/.claude/skills/README.md +7 -0
  158. package/starter/my-frontend/CLAUDE.md +33 -0
  159. package/starter/my-frontend/FINDINGS.md +9 -0
  160. package/starter/my-frontend/summary.md +7 -0
  161. package/starter/my-frontend/tasks.md +7 -0
  162. package/starter/profiles/default.yaml +4 -0
  163. package/starter/profiles/personal.yaml +4 -0
  164. package/starter/profiles/work.yaml +4 -0
  165. package/starter/templates/README.md +7 -0
  166. package/starter/templates/frontend/CLAUDE.md +23 -0
  167. package/starter/templates/frontend/FINDINGS.md +7 -0
  168. package/starter/templates/frontend/reference/README.md +4 -0
  169. package/starter/templates/frontend/summary.md +7 -0
  170. package/starter/templates/frontend/tasks.md +11 -0
  171. package/starter/templates/library/CLAUDE.md +22 -0
  172. package/starter/templates/library/FINDINGS.md +7 -0
  173. package/starter/templates/library/reference/README.md +4 -0
  174. package/starter/templates/library/summary.md +7 -0
  175. package/starter/templates/library/tasks.md +11 -0
  176. package/starter/templates/monorepo/CLAUDE.md +21 -0
  177. package/starter/templates/monorepo/FINDINGS.md +7 -0
  178. package/starter/templates/monorepo/reference/README.md +4 -0
  179. package/starter/templates/monorepo/summary.md +7 -0
  180. package/starter/templates/monorepo/tasks.md +11 -0
  181. package/starter/templates/python-project/CLAUDE.md +21 -0
  182. package/starter/templates/python-project/FINDINGS.md +7 -0
  183. package/starter/templates/python-project/reference/README.md +4 -0
  184. package/starter/templates/python-project/summary.md +7 -0
  185. package/starter/templates/python-project/tasks.md +10 -0
@@ -0,0 +1,513 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { createHash } from "crypto";
4
+ import { getProjectDirs, runtimeDir, runtimeHealthFile, memoryUsageLogFile, homePath, } from "./shared.js";
5
+ import { errorMessage } from "./utils.js";
6
+ import { readInstallPreferences } from "./init-preferences.js";
7
+ import { readCustomHooks } from "./hooks.js";
8
+ import { hookConfigPaths, hookConfigRoots } from "./provider-adapters.js";
9
+ import { getAllSkills } from "./skill-registry.js";
10
+ import { resolveTaskFilePath, readTasks, TASKS_FILENAME } from "./data-tasks.js";
11
+ import { buildIndex, queryRows } from "./shared-index.js";
12
+ import { readProjectTopics, classifyTopicForText } from "./project-topics.js";
13
+ import { entryScoreKey } from "./governance-scores.js";
14
+ function extractGithubUrl(content) {
15
+ const match = content.match(/https?:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+/);
16
+ return match ? match[0] : undefined;
17
+ }
18
+ function stableId(scope, ...parts) {
19
+ const hash = createHash("sha1");
20
+ for (const part of parts)
21
+ hash.update(part);
22
+ return `${scope}:${hash.digest("hex").slice(0, 12)}`;
23
+ }
24
+ function exactProjectMentions(text, projectSet, currentProject) {
25
+ const tokenMatches = text.toLowerCase().match(/[a-z0-9_-]+/g) ?? [];
26
+ const tokens = new Set(tokenMatches);
27
+ const matches = [];
28
+ for (const project of projectSet) {
29
+ if (project === currentProject)
30
+ continue;
31
+ if (tokens.has(project.toLowerCase()))
32
+ matches.push(project);
33
+ }
34
+ return matches;
35
+ }
36
+ function projectFromSourceDoc(sourceDoc) {
37
+ const slash = sourceDoc.indexOf("/");
38
+ return slash > 0 ? sourceDoc.slice(0, slash) : "";
39
+ }
40
+ export function readSyncSnapshot(phrenPath) {
41
+ try {
42
+ const runtimeHealth = runtimeHealthFile(phrenPath);
43
+ if (!fs.existsSync(runtimeHealth))
44
+ return {};
45
+ const parsed = JSON.parse(fs.readFileSync(runtimeHealth, "utf8"));
46
+ return {
47
+ autoSaveStatus: parsed.lastAutoSave?.status || "",
48
+ autoSaveDetail: parsed.lastAutoSave?.detail || "",
49
+ lastPullAt: parsed.lastSync?.lastPullAt || "",
50
+ lastPullStatus: parsed.lastSync?.lastPullStatus || "",
51
+ lastPushAt: parsed.lastSync?.lastPushAt || "",
52
+ lastPushStatus: parsed.lastSync?.lastPushStatus || "",
53
+ unsyncedCommits: parsed.lastSync?.unsyncedCommits || 0,
54
+ lastPushDetail: parsed.lastSync?.lastPushDetail || "",
55
+ };
56
+ }
57
+ catch {
58
+ return {};
59
+ }
60
+ }
61
+ export function isAllowedFilePath(filePath, phrenPath) {
62
+ const resolved = path.resolve(filePath);
63
+ const allowedRoots = hookConfigRoots(phrenPath);
64
+ if (!allowedRoots.some((root) => resolved === root || resolved.startsWith(root + path.sep))) {
65
+ return false;
66
+ }
67
+ let existingAncestor = resolved;
68
+ const pendingSegments = [];
69
+ while (!fs.existsSync(existingAncestor)) {
70
+ const parent = path.dirname(existingAncestor);
71
+ if (parent === existingAncestor)
72
+ break;
73
+ pendingSegments.unshift(path.basename(existingAncestor));
74
+ existingAncestor = parent;
75
+ }
76
+ let realResolved;
77
+ try {
78
+ const realAncestor = fs.realpathSync(existingAncestor);
79
+ realResolved = pendingSegments.length
80
+ ? path.resolve(realAncestor, ...pendingSegments)
81
+ : realAncestor;
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ const allowedRealRoots = allowedRoots.map((root) => {
87
+ try {
88
+ return fs.realpathSync(root);
89
+ }
90
+ catch {
91
+ return root;
92
+ }
93
+ });
94
+ return allowedRealRoots.some((root) => realResolved === root || realResolved.startsWith(root + path.sep));
95
+ }
96
+ export function collectSkillsForUI(phrenPath, profile = "") {
97
+ return getAllSkills(phrenPath, profile).map((skill) => ({
98
+ name: skill.name,
99
+ source: skill.source,
100
+ path: skill.path,
101
+ enabled: skill.enabled,
102
+ }));
103
+ }
104
+ export function getHooksData(phrenPath) {
105
+ const prefs = readInstallPreferences(phrenPath);
106
+ const globalEnabled = prefs.hooksEnabled !== false;
107
+ const toolPrefs = (prefs.hookTools && typeof prefs.hookTools === "object") ? prefs.hookTools : {};
108
+ const paths = hookConfigPaths(phrenPath);
109
+ const tools = ["claude", "copilot", "cursor", "codex"].map((tool) => ({
110
+ tool,
111
+ enabled: globalEnabled && toolPrefs[tool] !== false,
112
+ configPath: paths[tool],
113
+ exists: fs.existsSync(paths[tool]),
114
+ }));
115
+ return { globalEnabled, tools, customHooks: readCustomHooks(phrenPath) };
116
+ }
117
+ export async function buildGraph(phrenPath, profile, focusProject) {
118
+ const projects = getProjectDirs(phrenPath, profile).map((projectDir) => path.basename(projectDir)).filter((project) => project !== "global");
119
+ const nodes = [];
120
+ const links = [];
121
+ const projectSet = new Set(projects);
122
+ // Collect all unique topics across projects for the UI
123
+ const topicMetaMap = new Map();
124
+ for (const project of projects) {
125
+ // Load dynamic topics for this project
126
+ const { topics: projectTopics } = readProjectTopics(phrenPath, project);
127
+ for (const topic of projectTopics) {
128
+ if (!topicMetaMap.has(topic.slug)) {
129
+ topicMetaMap.set(topic.slug, { slug: topic.slug, label: topic.label });
130
+ }
131
+ }
132
+ const findingsPath = path.join(phrenPath, project, "FINDINGS.md");
133
+ if (!fs.existsSync(findingsPath)) {
134
+ nodes.push({
135
+ id: project,
136
+ label: project,
137
+ fullLabel: project,
138
+ group: "project",
139
+ refCount: 0,
140
+ project,
141
+ tagged: false,
142
+ });
143
+ continue;
144
+ }
145
+ nodes.push({
146
+ id: project,
147
+ label: project,
148
+ fullLabel: project,
149
+ group: "project",
150
+ refCount: 1,
151
+ project,
152
+ tagged: false,
153
+ });
154
+ const content = fs.readFileSync(findingsPath, "utf8");
155
+ const lines = content.split("\n");
156
+ // No cap for focused project; high caps otherwise
157
+ const isFocused = focusProject && project === focusProject;
158
+ const MAX_TAGGED = isFocused ? Infinity : 200;
159
+ const MAX_UNTAGGED = isFocused ? Infinity : 100;
160
+ let taggedCount = 0;
161
+ let untaggedAdded = 0;
162
+ for (const line of lines) {
163
+ // Support legacy tagged findings like [decision], [pitfall], etc.
164
+ const tagMatch = line.match(/^-\s+\[([a-z_-]+)\]\s+(.+?)(?:\s*<!--.*-->)?$/);
165
+ if (tagMatch) {
166
+ if (taggedCount >= MAX_TAGGED)
167
+ continue;
168
+ const tag = tagMatch[1];
169
+ const text = tagMatch[2].trim();
170
+ const label = text.length > 55 ? `${text.slice(0, 52)}...` : text;
171
+ // Classify the finding using the project's topic system
172
+ const topic = classifyTopicForText(`[${tag}] ${text}`, projectTopics);
173
+ const scoreKey = entryScoreKey(project, "FINDINGS.md", `[${tag}] ${text}`);
174
+ const nodeId = stableId("finding", scoreKey);
175
+ taggedCount++;
176
+ nodes.push({
177
+ id: nodeId,
178
+ label,
179
+ fullLabel: text,
180
+ group: `topic:${topic.slug}`,
181
+ refCount: taggedCount,
182
+ project,
183
+ tagged: true,
184
+ scoreKey,
185
+ scoreKeys: [scoreKey],
186
+ refDocs: [{ doc: `${project}/FINDINGS.md`, project, scoreKey }],
187
+ topicSlug: topic.slug,
188
+ topicLabel: topic.label,
189
+ });
190
+ links.push({ source: project, target: nodeId });
191
+ for (const other of exactProjectMentions(text, projectSet, project)) {
192
+ links.push({ source: project, target: other });
193
+ }
194
+ continue;
195
+ }
196
+ if (untaggedAdded >= MAX_UNTAGGED)
197
+ continue;
198
+ const plainMatch = line.match(/^-\s+(.+?)(?:\s*<!--.*-->)?$/);
199
+ if (!plainMatch)
200
+ continue;
201
+ const text = plainMatch[1].trim();
202
+ if (text.length < 10)
203
+ continue;
204
+ const label = text.length > 55 ? `${text.slice(0, 52)}...` : text;
205
+ // Classify using dynamic topics
206
+ const topic = classifyTopicForText(text, projectTopics);
207
+ const scoreKey = entryScoreKey(project, "FINDINGS.md", text);
208
+ const nodeId = stableId("finding", scoreKey);
209
+ untaggedAdded++;
210
+ nodes.push({
211
+ id: nodeId,
212
+ label,
213
+ fullLabel: text,
214
+ group: `topic:${topic.slug}`,
215
+ refCount: untaggedAdded,
216
+ project,
217
+ tagged: false,
218
+ scoreKey,
219
+ scoreKeys: [scoreKey],
220
+ refDocs: [{ doc: `${project}/FINDINGS.md`, project, scoreKey }],
221
+ topicSlug: topic.slug,
222
+ topicLabel: topic.label,
223
+ });
224
+ links.push({ source: project, target: nodeId });
225
+ }
226
+ }
227
+ // ── Tasks ──────────────────────────────────────────────────────────
228
+ try {
229
+ for (const project of projects) {
230
+ const taskResult = readTasks(phrenPath, project);
231
+ if (!taskResult.ok)
232
+ continue;
233
+ const doc = taskResult.data;
234
+ let taskCount = 0;
235
+ const MAX_TASKS = 50;
236
+ for (const section of ["Active", "Queue"]) {
237
+ const group = section === "Active" ? "task-active" : "task-queue";
238
+ for (const item of doc.items[section]) {
239
+ if (taskCount >= MAX_TASKS)
240
+ break;
241
+ const nodeId = `${project}:task:${item.id}`;
242
+ const label = item.line.length > 55 ? `${item.line.slice(0, 52)}...` : item.line;
243
+ const scoreKey = entryScoreKey(project, TASKS_FILENAME, item.line);
244
+ nodes.push({
245
+ id: nodeId,
246
+ label,
247
+ fullLabel: item.line,
248
+ group,
249
+ project,
250
+ tagged: false,
251
+ scoreKey,
252
+ scoreKeys: [scoreKey],
253
+ refDocs: [{ doc: `${project}/${TASKS_FILENAME}`, project, scoreKey }],
254
+ refCount: 0,
255
+ priority: item.priority,
256
+ section: item.section,
257
+ });
258
+ links.push({ source: project, target: nodeId });
259
+ taskCount++;
260
+ }
261
+ }
262
+ }
263
+ }
264
+ catch {
265
+ // task loading failed — continue with other data sources
266
+ }
267
+ // ── Fragments (entity graph) ───────────────────────────────────────
268
+ let db = null;
269
+ try {
270
+ db = await buildIndex(phrenPath, profile);
271
+ const rows = queryRows(db, `SELECT e.id, e.name, e.type, COUNT(DISTINCT el.source_doc) as ref_count
272
+ FROM entities e JOIN entity_links el ON el.target_id = e.id WHERE e.type != 'document'
273
+ GROUP BY e.id, e.name, e.type ORDER BY ref_count DESC LIMIT 500`, []);
274
+ const refRows = queryRows(db, `SELECT e.id, el.source_doc, d.content, d.filename
275
+ FROM entities e
276
+ JOIN entity_links el ON el.target_id = e.id
277
+ LEFT JOIN docs d ON d.source_key = el.source_doc
278
+ WHERE e.type != 'document'`, []);
279
+ const refsByEntity = new Map();
280
+ const seenEntityDoc = new Set();
281
+ if (refRows) {
282
+ for (const row of refRows) {
283
+ const entityId = typeof row[0] === "number" ? row[0] : -1;
284
+ if (entityId < 0)
285
+ continue;
286
+ const doc = String(row[1] ?? "");
287
+ if (!doc)
288
+ continue;
289
+ const entityDocKey = `${entityId}::${doc}`;
290
+ if (seenEntityDoc.has(entityDocKey))
291
+ continue;
292
+ seenEntityDoc.add(entityDocKey);
293
+ const project = projectFromSourceDoc(doc);
294
+ const content = typeof row[2] === "string" ? row[2] : "";
295
+ const filename = typeof row[3] === "string" ? row[3] : "";
296
+ const scoreKey = project && filename && content ? entryScoreKey(project, filename, content) : undefined;
297
+ const refs = refsByEntity.get(entityId) ?? [];
298
+ refs.push({ doc, project, scoreKey });
299
+ refsByEntity.set(entityId, refs);
300
+ }
301
+ }
302
+ if (rows) {
303
+ for (const row of rows) {
304
+ const entityId = typeof row[0] === "number" ? row[0] : -1;
305
+ if (entityId < 0)
306
+ continue;
307
+ const name = String(row[1] ?? "");
308
+ const type = String(row[2] ?? "");
309
+ const refCount = typeof row[3] === "number" ? row[3] : 0;
310
+ const refs = (refsByEntity.get(entityId) ?? []).slice().sort((a, b) => a.doc.localeCompare(b.doc));
311
+ const scoreKeys = refs
312
+ .map((ref) => ref.scoreKey)
313
+ .filter((key) => Boolean(key))
314
+ .sort();
315
+ const nodeId = `entity:${stableId("entity", type, name)}`;
316
+ nodes.push({
317
+ id: nodeId,
318
+ label: name.length > 55 ? `${name.slice(0, 52)}...` : name,
319
+ fullLabel: name,
320
+ group: "entity",
321
+ project: "",
322
+ tagged: false,
323
+ refCount,
324
+ scoreKey: scoreKeys[0],
325
+ scoreKeys,
326
+ entityType: type,
327
+ refDocs: refs,
328
+ });
329
+ // Link entity to each project it appears in
330
+ const linkedProjects = new Set();
331
+ for (const ref of refs) {
332
+ if (ref.project && projectSet.has(ref.project))
333
+ linkedProjects.add(ref.project);
334
+ }
335
+ for (const proj of linkedProjects) {
336
+ links.push({ source: nodeId, target: proj });
337
+ }
338
+ }
339
+ }
340
+ }
341
+ catch {
342
+ // entity loading failed — continue with other data sources
343
+ }
344
+ finally {
345
+ if (db) {
346
+ try {
347
+ db.close();
348
+ }
349
+ catch { /* already closed or failed — ignore */ }
350
+ }
351
+ }
352
+ // ── Reference docs ────────────────────────────────────────────────
353
+ try {
354
+ for (const project of projects) {
355
+ const refDir = path.join(phrenPath, project, "reference");
356
+ if (!fs.existsSync(refDir) || !fs.statSync(refDir).isDirectory())
357
+ continue;
358
+ const files = fs.readdirSync(refDir);
359
+ const MAX_REFS = 20;
360
+ let refCount = 0;
361
+ for (const file of files) {
362
+ if (refCount >= MAX_REFS)
363
+ break;
364
+ const nodeId = `${project}:ref:${file}`;
365
+ const docRef = `${project}/reference/${file}`;
366
+ nodes.push({
367
+ id: nodeId,
368
+ label: file.length > 55 ? `${file.slice(0, 52)}...` : file,
369
+ fullLabel: file,
370
+ group: "reference",
371
+ project,
372
+ tagged: false,
373
+ scoreKeys: [],
374
+ refDocs: [{ doc: docRef, project }],
375
+ refCount: 0,
376
+ });
377
+ links.push({ source: project, target: nodeId });
378
+ refCount++;
379
+ }
380
+ }
381
+ }
382
+ catch {
383
+ // reference doc loading failed — continue
384
+ }
385
+ // ── Memory scores ────────────────────────────────────────────────
386
+ let scores = {};
387
+ try {
388
+ const scoresPath = path.join(phrenPath, ".runtime", "memory-scores.json");
389
+ if (fs.existsSync(scoresPath)) {
390
+ const parsed = JSON.parse(fs.readFileSync(scoresPath, "utf8"));
391
+ if (parsed && typeof parsed.entries === "object") {
392
+ scores = parsed.entries;
393
+ }
394
+ }
395
+ }
396
+ catch {
397
+ // scores loading failed — return empty
398
+ }
399
+ const seen = new Set();
400
+ const dedupedLinks = links.filter((link) => {
401
+ const key = [link.source, link.target].sort().join("||");
402
+ if (seen.has(key))
403
+ return false;
404
+ seen.add(key);
405
+ return true;
406
+ });
407
+ // Remove orphan project nodes (0 edges) to avoid scattered floaters
408
+ const connectedIds = new Set();
409
+ for (const link of dedupedLinks) {
410
+ connectedIds.add(link.source);
411
+ connectedIds.add(link.target);
412
+ }
413
+ const filteredNodes = nodes.filter((n) => n.group !== "project" || connectedIds.has(n.id));
414
+ const total = filteredNodes.length;
415
+ const topics = Array.from(topicMetaMap.values());
416
+ return { nodes: filteredNodes, links: dedupedLinks, total, scores, topics };
417
+ }
418
+ export function recentUsage(phrenPath) {
419
+ const usage = memoryUsageLogFile(phrenPath);
420
+ if (!fs.existsSync(usage))
421
+ return [];
422
+ const lines = fs.readFileSync(usage, "utf8").trim().split("\n").filter(Boolean);
423
+ return lines.slice(-40).reverse();
424
+ }
425
+ export function recentAccepted(phrenPath) {
426
+ const audit = path.join(runtimeDir(phrenPath), "audit.log");
427
+ if (!fs.existsSync(audit))
428
+ return [];
429
+ const lines = fs.readFileSync(audit, "utf8").split("\n").filter((line) => line.includes("approve_memory"));
430
+ return lines.slice(-40).reverse();
431
+ }
432
+ export function collectProjectsForUI(phrenPath, profile) {
433
+ const projects = getProjectDirs(phrenPath, profile).map((projectDir) => path.basename(projectDir)).filter((project) => project !== "global");
434
+ let allowedProjects = null;
435
+ try {
436
+ const contextPath = homePath(".phren-context.md");
437
+ if (fs.existsSync(contextPath)) {
438
+ const contextContent = fs.readFileSync(contextPath, "utf8");
439
+ const activeMatch = contextContent.match(/Active projects?:\s*(.+)/i);
440
+ if (activeMatch) {
441
+ const names = activeMatch[1].split(/[,;]/).map((name) => name.trim().toLowerCase()).filter(Boolean);
442
+ if (names.length)
443
+ allowedProjects = new Set(names);
444
+ }
445
+ }
446
+ }
447
+ catch (err) {
448
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
449
+ process.stderr.write(`[phren] memory-ui filterByProfile: ${errorMessage(err)}\n`);
450
+ }
451
+ const results = [];
452
+ for (const project of projects) {
453
+ if (allowedProjects && !allowedProjects.has(project.toLowerCase()))
454
+ continue;
455
+ const dir = path.join(phrenPath, project);
456
+ const findingsPath = path.join(dir, "FINDINGS.md");
457
+ const taskPath = resolveTaskFilePath(phrenPath, project);
458
+ const claudeMdPath = path.join(dir, "CLAUDE.md");
459
+ const summaryPath = path.join(dir, "summary.md");
460
+ const refPath = path.join(dir, "reference");
461
+ let findingCount = 0;
462
+ if (fs.existsSync(findingsPath)) {
463
+ const content = fs.readFileSync(findingsPath, "utf8");
464
+ findingCount = (content.match(/^- \[/gm) || []).length;
465
+ }
466
+ const sparkline = new Array(8).fill(0);
467
+ if (fs.existsSync(findingsPath)) {
468
+ const now = Date.now();
469
+ const weekMs = 7 * 24 * 60 * 60 * 1000;
470
+ const sparkContent = fs.readFileSync(findingsPath, "utf8");
471
+ const dateRe = /(?:created[_:]?\s*"?|created_at[":]+\s*)(\d{4}-\d{2}-\d{2})/g;
472
+ let match;
473
+ while ((match = dateRe.exec(sparkContent)) !== null) {
474
+ const age = now - new Date(match[1]).getTime();
475
+ const weekIdx = Math.floor(age / weekMs);
476
+ if (weekIdx >= 0 && weekIdx < 8)
477
+ sparkline[7 - weekIdx]++;
478
+ }
479
+ }
480
+ let taskCount = 0;
481
+ if (taskPath && fs.existsSync(taskPath)) {
482
+ const content = fs.readFileSync(taskPath, "utf8");
483
+ const queueMatch = content.match(/## Queue[\s\S]*?(?=## |$)/);
484
+ if (queueMatch)
485
+ taskCount = (queueMatch[0].match(/^- /gm) || []).length;
486
+ }
487
+ let summaryText = "";
488
+ if (fs.existsSync(summaryPath)) {
489
+ summaryText = fs.readFileSync(summaryPath, "utf8").trim();
490
+ if (summaryText.length > 300)
491
+ summaryText = `${summaryText.slice(0, 300)}...`;
492
+ }
493
+ let githubUrl;
494
+ if (fs.existsSync(claudeMdPath)) {
495
+ githubUrl = extractGithubUrl(fs.readFileSync(claudeMdPath, "utf8"));
496
+ }
497
+ if (!githubUrl && fs.existsSync(summaryPath)) {
498
+ githubUrl = extractGithubUrl(fs.readFileSync(summaryPath, "utf8"));
499
+ }
500
+ results.push({
501
+ name: project,
502
+ findingCount,
503
+ taskCount,
504
+ hasClaudeMd: fs.existsSync(claudeMdPath),
505
+ hasSummary: fs.existsSync(summaryPath),
506
+ hasReference: fs.existsSync(refPath) && fs.statSync(refPath).isDirectory(),
507
+ summaryText,
508
+ githubUrl,
509
+ sparkline,
510
+ });
511
+ }
512
+ return results.sort((a, b) => (b.findingCount + b.taskCount) - (a.findingCount + a.taskCount));
513
+ }