@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.
- package/LICENSE +21 -0
- package/README.md +590 -0
- package/mcp/dist/capabilities/cli.js +61 -0
- package/mcp/dist/capabilities/index.js +15 -0
- package/mcp/dist/capabilities/mcp.js +61 -0
- package/mcp/dist/capabilities/types.js +57 -0
- package/mcp/dist/capabilities/vscode.js +61 -0
- package/mcp/dist/capabilities/web-ui.js +61 -0
- package/mcp/dist/cli-actions.js +302 -0
- package/mcp/dist/cli-config.js +580 -0
- package/mcp/dist/cli-extract.js +305 -0
- package/mcp/dist/cli-govern.js +371 -0
- package/mcp/dist/cli-graph.js +169 -0
- package/mcp/dist/cli-hooks-citations.js +44 -0
- package/mcp/dist/cli-hooks-context.js +56 -0
- package/mcp/dist/cli-hooks-globs.js +83 -0
- package/mcp/dist/cli-hooks-output.js +130 -0
- package/mcp/dist/cli-hooks-retrieval.js +2 -0
- package/mcp/dist/cli-hooks-session.js +1402 -0
- package/mcp/dist/cli-hooks.js +350 -0
- package/mcp/dist/cli-namespaces.js +989 -0
- package/mcp/dist/cli-ops.js +253 -0
- package/mcp/dist/cli-search.js +407 -0
- package/mcp/dist/cli.js +108 -0
- package/mcp/dist/content-archive.js +278 -0
- package/mcp/dist/content-citation.js +391 -0
- package/mcp/dist/content-dedup.js +622 -0
- package/mcp/dist/content-learning.js +472 -0
- package/mcp/dist/content-metadata.js +186 -0
- package/mcp/dist/content-validate.js +462 -0
- package/mcp/dist/core-finding.js +54 -0
- package/mcp/dist/core-project.js +36 -0
- package/mcp/dist/core-search.js +50 -0
- package/mcp/dist/data-access.js +400 -0
- package/mcp/dist/data-tasks.js +821 -0
- package/mcp/dist/embedding.js +344 -0
- package/mcp/dist/entrypoint.js +387 -0
- package/mcp/dist/finding-context.js +172 -0
- package/mcp/dist/finding-impact.js +181 -0
- package/mcp/dist/finding-journal.js +122 -0
- package/mcp/dist/finding-lifecycle.js +259 -0
- package/mcp/dist/governance-audit.js +22 -0
- package/mcp/dist/governance-locks.js +96 -0
- package/mcp/dist/governance-policy.js +648 -0
- package/mcp/dist/governance-scores.js +355 -0
- package/mcp/dist/hooks.js +449 -0
- package/mcp/dist/impact-scoring.js +22 -0
- package/mcp/dist/index-query.js +168 -0
- package/mcp/dist/index.js +205 -0
- package/mcp/dist/init-config.js +336 -0
- package/mcp/dist/init-preferences.js +62 -0
- package/mcp/dist/init-setup.js +1305 -0
- package/mcp/dist/init-shared.js +29 -0
- package/mcp/dist/init.js +1730 -0
- package/mcp/dist/link-checksums.js +62 -0
- package/mcp/dist/link-context.js +257 -0
- package/mcp/dist/link-doctor.js +591 -0
- package/mcp/dist/link-skills.js +212 -0
- package/mcp/dist/link.js +596 -0
- package/mcp/dist/logger.js +15 -0
- package/mcp/dist/machine-identity.js +38 -0
- package/mcp/dist/mcp-config.js +254 -0
- package/mcp/dist/mcp-data.js +315 -0
- package/mcp/dist/mcp-extract-facts.js +78 -0
- package/mcp/dist/mcp-extract.js +133 -0
- package/mcp/dist/mcp-finding.js +557 -0
- package/mcp/dist/mcp-graph.js +339 -0
- package/mcp/dist/mcp-hooks.js +256 -0
- package/mcp/dist/mcp-memory.js +58 -0
- package/mcp/dist/mcp-ops.js +328 -0
- package/mcp/dist/mcp-search.js +628 -0
- package/mcp/dist/mcp-session.js +651 -0
- package/mcp/dist/mcp-skills.js +189 -0
- package/mcp/dist/mcp-tasks.js +551 -0
- package/mcp/dist/mcp-types.js +7 -0
- package/mcp/dist/memory-ui-assets.js +6 -0
- package/mcp/dist/memory-ui-data.js +513 -0
- package/mcp/dist/memory-ui-graph.js +1910 -0
- package/mcp/dist/memory-ui-page.js +353 -0
- package/mcp/dist/memory-ui-scripts.js +1387 -0
- package/mcp/dist/memory-ui-server.js +1218 -0
- package/mcp/dist/memory-ui-styles.js +555 -0
- package/mcp/dist/memory-ui.js +9 -0
- package/mcp/dist/package-metadata.js +13 -0
- package/mcp/dist/phren-art.js +52 -0
- package/mcp/dist/phren-core.js +108 -0
- package/mcp/dist/phren-dotenv.js +67 -0
- package/mcp/dist/phren-paths.js +476 -0
- package/mcp/dist/proactivity.js +172 -0
- package/mcp/dist/profile-store.js +228 -0
- package/mcp/dist/project-config.js +85 -0
- package/mcp/dist/project-locator.js +25 -0
- package/mcp/dist/project-topics.js +1134 -0
- package/mcp/dist/provider-adapters.js +176 -0
- package/mcp/dist/runtime-profile.js +18 -0
- package/mcp/dist/session-checkpoints.js +131 -0
- package/mcp/dist/session-utils.js +68 -0
- package/mcp/dist/shared-content.js +8 -0
- package/mcp/dist/shared-embedding-cache.js +143 -0
- package/mcp/dist/shared-fragment-graph.js +456 -0
- package/mcp/dist/shared-governance.js +4 -0
- package/mcp/dist/shared-index.js +1334 -0
- package/mcp/dist/shared-ollama.js +192 -0
- package/mcp/dist/shared-paths.js +1 -0
- package/mcp/dist/shared-retrieval.js +796 -0
- package/mcp/dist/shared-search-fallback.js +375 -0
- package/mcp/dist/shared-sqljs.js +42 -0
- package/mcp/dist/shared-stemmer.js +171 -0
- package/mcp/dist/shared-vector-index.js +199 -0
- package/mcp/dist/shared.js +114 -0
- package/mcp/dist/shell-entry.js +209 -0
- package/mcp/dist/shell-input.js +943 -0
- package/mcp/dist/shell-palette.js +119 -0
- package/mcp/dist/shell-render.js +252 -0
- package/mcp/dist/shell-state-store.js +81 -0
- package/mcp/dist/shell-types.js +13 -0
- package/mcp/dist/shell-view-list.js +14 -0
- package/mcp/dist/shell-view.js +707 -0
- package/mcp/dist/shell.js +352 -0
- package/mcp/dist/skill-files.js +117 -0
- package/mcp/dist/skill-registry.js +279 -0
- package/mcp/dist/skill-state.js +28 -0
- package/mcp/dist/startup-embedding.js +57 -0
- package/mcp/dist/status.js +323 -0
- package/mcp/dist/synonyms.json +670 -0
- package/mcp/dist/task-hygiene.js +251 -0
- package/mcp/dist/task-lifecycle.js +347 -0
- package/mcp/dist/tasks-github.js +76 -0
- package/mcp/dist/telemetry.js +165 -0
- package/mcp/dist/test-global-setup.js +37 -0
- package/mcp/dist/tool-registry.js +104 -0
- package/mcp/dist/update.js +97 -0
- package/mcp/dist/utils.js +543 -0
- package/package.json +67 -0
- package/skills/README.md +7 -0
- package/skills/consolidate/SKILL.md +152 -0
- package/skills/discover/SKILL.md +175 -0
- package/skills/init/SKILL.md +216 -0
- package/skills/profiles/SKILL.md +121 -0
- package/skills/sync/SKILL.md +261 -0
- package/starter/README.md +74 -0
- package/starter/global/CLAUDE.md +89 -0
- package/starter/global/skills/humanize.md +30 -0
- package/starter/global/skills/pipeline.md +35 -0
- package/starter/global/skills/release.md +35 -0
- package/starter/machines.yaml +8 -0
- package/starter/my-api/.claude/skills/README.md +7 -0
- package/starter/my-api/CLAUDE.md +33 -0
- package/starter/my-api/FINDINGS.md +9 -0
- package/starter/my-api/summary.md +7 -0
- package/starter/my-api/tasks.md +7 -0
- package/starter/my-first-project/.claude/skills/README.md +7 -0
- package/starter/my-first-project/CLAUDE.md +49 -0
- package/starter/my-first-project/FINDINGS.md +24 -0
- package/starter/my-first-project/summary.md +11 -0
- package/starter/my-first-project/tasks.md +25 -0
- package/starter/my-frontend/.claude/skills/README.md +7 -0
- package/starter/my-frontend/CLAUDE.md +33 -0
- package/starter/my-frontend/FINDINGS.md +9 -0
- package/starter/my-frontend/summary.md +7 -0
- package/starter/my-frontend/tasks.md +7 -0
- package/starter/profiles/default.yaml +4 -0
- package/starter/profiles/personal.yaml +4 -0
- package/starter/profiles/work.yaml +4 -0
- package/starter/templates/README.md +7 -0
- package/starter/templates/frontend/CLAUDE.md +23 -0
- package/starter/templates/frontend/FINDINGS.md +7 -0
- package/starter/templates/frontend/reference/README.md +4 -0
- package/starter/templates/frontend/summary.md +7 -0
- package/starter/templates/frontend/tasks.md +11 -0
- package/starter/templates/library/CLAUDE.md +22 -0
- package/starter/templates/library/FINDINGS.md +7 -0
- package/starter/templates/library/reference/README.md +4 -0
- package/starter/templates/library/summary.md +7 -0
- package/starter/templates/library/tasks.md +11 -0
- package/starter/templates/monorepo/CLAUDE.md +21 -0
- package/starter/templates/monorepo/FINDINGS.md +7 -0
- package/starter/templates/monorepo/reference/README.md +4 -0
- package/starter/templates/monorepo/summary.md +7 -0
- package/starter/templates/monorepo/tasks.md +11 -0
- package/starter/templates/python-project/CLAUDE.md +21 -0
- package/starter/templates/python-project/FINDINGS.md +7 -0
- package/starter/templates/python-project/reference/README.md +4 -0
- package/starter/templates/python-project/summary.md +7 -0
- 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
|
+
}
|