@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,400 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { phrenErr, PhrenError, phrenOk, forwardErr, getProjectDirs, } from "./shared.js";
|
|
4
|
+
import { normalizeQueueEntryText, withFileLock as withFileLockRaw, } from "./shared-governance.js";
|
|
5
|
+
import { addFindingToFile, } from "./shared-content.js";
|
|
6
|
+
import { isValidProjectName, queueFilePath, safeProjectPath, errorMessage } from "./utils.js";
|
|
7
|
+
import { parseCitationComment, parseSourceComment, } from "./content-citation.js";
|
|
8
|
+
import { parseFindingLifecycle, } from "./finding-lifecycle.js";
|
|
9
|
+
import { METADATA_REGEX, isCitationLine, isArchiveStart, isArchiveEnd, parseFindingId, parseAllContradictions, stripComments, } from "./content-metadata.js";
|
|
10
|
+
export { readTasks, readTasksAcrossProjects, resolveTaskItem, addTask, addTasks, completeTasks, completeTask, removeTask, updateTask, linkTaskIssue, pinTask, unpinTask, workNextTask, tidyDoneTasks, taskMarkdown, appendChildFinding, promoteTask, TASKS_FILENAME, TASK_FILE_ALIASES, canonicalTaskFilePath, resolveTaskFilePath, isTaskFileName, } from "./data-tasks.js";
|
|
11
|
+
export { addProjectToProfile, listMachines, listProfiles, listProjectCards, removeProjectFromProfile, setMachineProfile, } from "./profile-store.js";
|
|
12
|
+
export { loadShellState, readRuntimeHealth, resetShellState, saveShellState, } from "./shell-state-store.js";
|
|
13
|
+
function withSafeLock(filePath, fn) {
|
|
14
|
+
try {
|
|
15
|
+
return withFileLockRaw(filePath, fn);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const msg = errorMessage(err);
|
|
19
|
+
if (msg.includes("could not acquire lock")) {
|
|
20
|
+
return phrenErr(`Could not acquire write lock for "${path.basename(filePath)}". Another write may be in progress; please retry.`, PhrenError.LOCK_TIMEOUT);
|
|
21
|
+
}
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function ensureProject(phrenPath, project) {
|
|
26
|
+
if (!isValidProjectName(project))
|
|
27
|
+
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
28
|
+
const dir = safeProjectPath(phrenPath, project);
|
|
29
|
+
if (!dir)
|
|
30
|
+
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
31
|
+
if (!fs.existsSync(dir)) {
|
|
32
|
+
return phrenErr(`No project "${project}" found. Add it with 'cd ~/your-project && phren add'.`, PhrenError.PROJECT_NOT_FOUND);
|
|
33
|
+
}
|
|
34
|
+
return phrenOk(dir);
|
|
35
|
+
}
|
|
36
|
+
function extractDateHeading(line) {
|
|
37
|
+
const heading = line.match(/^##\s+(.+)$/);
|
|
38
|
+
if (!heading)
|
|
39
|
+
return null;
|
|
40
|
+
const raw = heading[1].trim();
|
|
41
|
+
const direct = raw.match(/^(\d{4}-\d{2}-\d{2})$/);
|
|
42
|
+
if (direct)
|
|
43
|
+
return direct[1];
|
|
44
|
+
const archived = raw.match(/^Archived\s+(\d{4}-\d{2}-\d{2})$/i);
|
|
45
|
+
if (archived)
|
|
46
|
+
return archived[1];
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
function normalizeFindingGroupKey(item) {
|
|
50
|
+
if (item.stableId)
|
|
51
|
+
return `fid:${item.stableId}`;
|
|
52
|
+
return item.text.replace(/\s+/g, " ").trim().toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
function findingTimelineDate(item) {
|
|
55
|
+
return item.status_updated || item.date || "0000-00-00";
|
|
56
|
+
}
|
|
57
|
+
export function readFindings(phrenPath, project, opts = {}) {
|
|
58
|
+
const ensured = ensureProject(phrenPath, project);
|
|
59
|
+
if (!ensured.ok)
|
|
60
|
+
return forwardErr(ensured);
|
|
61
|
+
const findingsPath = path.join(ensured.data, 'FINDINGS.md');
|
|
62
|
+
const file = findingsPath;
|
|
63
|
+
if (!fs.existsSync(file))
|
|
64
|
+
return phrenOk([]);
|
|
65
|
+
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
66
|
+
const items = [];
|
|
67
|
+
let date = "unknown";
|
|
68
|
+
let index = 1;
|
|
69
|
+
let inArchiveBlock = false;
|
|
70
|
+
const includeArchived = opts.includeArchived ?? false;
|
|
71
|
+
for (let i = 0; i < lines.length; i++) {
|
|
72
|
+
const line = lines[i];
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
const archiveStartMatch = isArchiveStart(line);
|
|
75
|
+
const archiveEnd = isArchiveEnd(line);
|
|
76
|
+
if (archiveStartMatch) {
|
|
77
|
+
inArchiveBlock = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (archiveEnd) {
|
|
81
|
+
inArchiveBlock = false;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (inArchiveBlock && !includeArchived) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const extractedDate = extractDateHeading(line);
|
|
88
|
+
if (extractedDate) {
|
|
89
|
+
date = extractedDate;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (!line.startsWith("- "))
|
|
93
|
+
continue;
|
|
94
|
+
const next = lines[i + 1] || "";
|
|
95
|
+
const citation = isCitationLine(next) ? next.trim() : undefined;
|
|
96
|
+
const citationData = citation ? parseCitationComment(citation) ?? undefined : undefined;
|
|
97
|
+
const source = parseSourceComment(line) ?? undefined;
|
|
98
|
+
const stableId = parseFindingId(line);
|
|
99
|
+
const rawText = line.replace(/^-\s+/, "").trim();
|
|
100
|
+
const textWithoutComments = stripComments(rawText);
|
|
101
|
+
const confMatch = textWithoutComments.match(/\s*\[confidence\s+([01](?:\.\d+)?)\]\s*$/i);
|
|
102
|
+
const confidence = confMatch ? parseFloat(confMatch[1]) : undefined;
|
|
103
|
+
const text = confMatch
|
|
104
|
+
? textWithoutComments.slice(0, textWithoutComments.length - confMatch[0].length).trim()
|
|
105
|
+
: textWithoutComments;
|
|
106
|
+
// Parse lifecycle annotations
|
|
107
|
+
const supersededByMatch = line.match(METADATA_REGEX.supersededBy);
|
|
108
|
+
const supersedesMatch = line.match(METADATA_REGEX.supersedes);
|
|
109
|
+
const contradictsMatches = parseAllContradictions(line);
|
|
110
|
+
const lifecycle = parseFindingLifecycle(line);
|
|
111
|
+
items.push({
|
|
112
|
+
id: `L${index}`,
|
|
113
|
+
stableId,
|
|
114
|
+
date,
|
|
115
|
+
text,
|
|
116
|
+
confidence,
|
|
117
|
+
source: source?.source ?? "unknown",
|
|
118
|
+
citation,
|
|
119
|
+
citationData,
|
|
120
|
+
taskItem: citationData?.task_item,
|
|
121
|
+
machine: source?.machine,
|
|
122
|
+
actor: source?.actor,
|
|
123
|
+
tool: source?.tool,
|
|
124
|
+
model: source?.model,
|
|
125
|
+
sessionId: source?.session_id,
|
|
126
|
+
scope: source?.scope,
|
|
127
|
+
supersededBy: supersededByMatch ? supersededByMatch[1] : undefined,
|
|
128
|
+
supersedes: supersedesMatch ? supersedesMatch[1] : undefined,
|
|
129
|
+
contradicts: contradictsMatches.length > 0 ? contradictsMatches : undefined,
|
|
130
|
+
status: lifecycle.status,
|
|
131
|
+
status_updated: lifecycle.status_updated,
|
|
132
|
+
status_reason: lifecycle.status_reason,
|
|
133
|
+
status_ref: lifecycle.status_ref,
|
|
134
|
+
archived: inArchiveBlock,
|
|
135
|
+
tier: inArchiveBlock ? "archived" : "current",
|
|
136
|
+
});
|
|
137
|
+
if (citation)
|
|
138
|
+
i += 1;
|
|
139
|
+
index++;
|
|
140
|
+
}
|
|
141
|
+
return phrenOk(items);
|
|
142
|
+
}
|
|
143
|
+
export function readFindingHistory(phrenPath, project, findingId) {
|
|
144
|
+
const result = readFindings(phrenPath, project, { includeArchived: true });
|
|
145
|
+
if (!result.ok)
|
|
146
|
+
return forwardErr(result);
|
|
147
|
+
const allItems = result.data;
|
|
148
|
+
const needle = findingId?.trim().toLowerCase();
|
|
149
|
+
const fidNeedle = needle ? needle.replace(/^fid:/, "") : undefined;
|
|
150
|
+
const scopedItems = needle
|
|
151
|
+
? allItems.filter((item) => {
|
|
152
|
+
if (fidNeedle && /^[a-z0-9]{8}$/.test(fidNeedle) && item.stableId?.toLowerCase() === fidNeedle)
|
|
153
|
+
return true;
|
|
154
|
+
if (item.id.toLowerCase() === needle)
|
|
155
|
+
return true;
|
|
156
|
+
return item.text.toLowerCase().includes(needle);
|
|
157
|
+
})
|
|
158
|
+
: allItems;
|
|
159
|
+
if (needle && scopedItems.length === 0) {
|
|
160
|
+
return phrenErr(`No finding history matching "${findingId}" in project "${project}".`, PhrenError.NOT_FOUND);
|
|
161
|
+
}
|
|
162
|
+
const groups = new Map();
|
|
163
|
+
for (const item of scopedItems) {
|
|
164
|
+
const key = normalizeFindingGroupKey(item);
|
|
165
|
+
const bucket = groups.get(key) ?? [];
|
|
166
|
+
bucket.push(item);
|
|
167
|
+
groups.set(key, bucket);
|
|
168
|
+
}
|
|
169
|
+
const history = [...groups.values()].map((timelineItems) => {
|
|
170
|
+
const timeline = [...timelineItems].sort((a, b) => findingTimelineDate(a).localeCompare(findingTimelineDate(b)));
|
|
171
|
+
const currentCandidates = timeline.filter(item => item.tier === "current");
|
|
172
|
+
const current = currentCandidates.length > 0
|
|
173
|
+
? currentCandidates.sort((a, b) => findingTimelineDate(b).localeCompare(findingTimelineDate(a)))[0]
|
|
174
|
+
: undefined;
|
|
175
|
+
const latest = timeline[timeline.length - 1];
|
|
176
|
+
const stableId = current?.stableId ?? latest.stableId;
|
|
177
|
+
return {
|
|
178
|
+
id: stableId ? `fid:${stableId}` : latest.id,
|
|
179
|
+
stableId,
|
|
180
|
+
text: current?.text ?? latest.text,
|
|
181
|
+
timeline,
|
|
182
|
+
current,
|
|
183
|
+
archivedCount: timeline.filter(item => item.tier === "archived").length,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
history.sort((a, b) => {
|
|
187
|
+
const aKey = a.timeline[a.timeline.length - 1] ? findingTimelineDate(a.timeline[a.timeline.length - 1]) : "";
|
|
188
|
+
const bKey = b.timeline[b.timeline.length - 1] ? findingTimelineDate(b.timeline[b.timeline.length - 1]) : "";
|
|
189
|
+
return bKey.localeCompare(aKey);
|
|
190
|
+
});
|
|
191
|
+
return phrenOk(history);
|
|
192
|
+
}
|
|
193
|
+
export function addFinding(phrenPath, project, learning) {
|
|
194
|
+
if (!isValidProjectName(project))
|
|
195
|
+
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
196
|
+
const resolved = safeProjectPath(phrenPath, project);
|
|
197
|
+
if (!resolved)
|
|
198
|
+
return phrenErr(`Project name "${project}" is not valid. Use lowercase letters, numbers, and hyphens (e.g. "my-project").`, PhrenError.INVALID_PROJECT_NAME);
|
|
199
|
+
// addFindingToFile handles its own file lock; no double-wrap
|
|
200
|
+
return addFindingToFile(phrenPath, project, learning);
|
|
201
|
+
}
|
|
202
|
+
export function removeFinding(phrenPath, project, match) {
|
|
203
|
+
const ensured = ensureProject(phrenPath, project);
|
|
204
|
+
if (!ensured.ok)
|
|
205
|
+
return forwardErr(ensured);
|
|
206
|
+
const findingsPath = path.join(ensured.data, 'FINDINGS.md');
|
|
207
|
+
const filePath = findingsPath;
|
|
208
|
+
if (!fs.existsSync(filePath))
|
|
209
|
+
return phrenErr(`No FINDINGS.md file found for "${project}". Add a finding first with add_finding or :find add.`, PhrenError.FILE_NOT_FOUND);
|
|
210
|
+
return withSafeLock(filePath, () => {
|
|
211
|
+
const lines = fs.readFileSync(filePath, "utf8").split("\n");
|
|
212
|
+
const needle = match.trim().toLowerCase();
|
|
213
|
+
const bulletLines = lines.map((line, i) => ({ line, i })).filter(({ line }) => line.startsWith("- "));
|
|
214
|
+
// 0) Stable finding ID match (fid:XXXXXXXX or just the 8-char hex)
|
|
215
|
+
const fidNeedle = needle.replace(/^fid:/, "");
|
|
216
|
+
const fidMatch = /^[a-z0-9]{8}$/.test(fidNeedle)
|
|
217
|
+
? bulletLines.filter(({ line }) => new RegExp(`<!--\\s*fid:${fidNeedle}\\s*-->`).test(line))
|
|
218
|
+
: [];
|
|
219
|
+
// 1) Exact text match (strip bullet prefix + metadata for comparison)
|
|
220
|
+
const exactMatches = bulletLines.filter(({ line }) => line.replace(/^-\s+/, "").replace(/<!--.*?-->/g, "").trim().toLowerCase() === needle);
|
|
221
|
+
// 2) Unique partial substring match
|
|
222
|
+
const partialMatches = bulletLines.filter(({ line }) => line.toLowerCase().includes(needle));
|
|
223
|
+
let idx;
|
|
224
|
+
if (fidMatch.length === 1) {
|
|
225
|
+
idx = fidMatch[0].i;
|
|
226
|
+
}
|
|
227
|
+
else if (exactMatches.length === 1) {
|
|
228
|
+
idx = exactMatches[0].i;
|
|
229
|
+
}
|
|
230
|
+
else if (exactMatches.length > 1) {
|
|
231
|
+
return phrenErr(`"${match}" is ambiguous (${exactMatches.length} exact matches). Use a more specific phrase.`, PhrenError.AMBIGUOUS_MATCH);
|
|
232
|
+
}
|
|
233
|
+
else if (partialMatches.length === 1) {
|
|
234
|
+
idx = partialMatches[0].i;
|
|
235
|
+
}
|
|
236
|
+
else if (partialMatches.length > 1) {
|
|
237
|
+
return phrenErr(`"${match}" is ambiguous (${partialMatches.length} partial matches). Use a more specific phrase.`, PhrenError.AMBIGUOUS_MATCH);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
return phrenErr(`No finding matching "${match}" in project "${project}". Try a different search term or check :findings view.`, PhrenError.NOT_FOUND);
|
|
241
|
+
}
|
|
242
|
+
const removeCount = isCitationLine(lines[idx + 1] || "") ? 2 : 1;
|
|
243
|
+
const matched = lines[idx];
|
|
244
|
+
lines.splice(idx, removeCount);
|
|
245
|
+
const normalized = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
246
|
+
fs.writeFileSync(filePath, normalized);
|
|
247
|
+
return phrenOk(`Removed from ${project}: ${matched}`);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
export function editFinding(phrenPath, project, oldText, newText) {
|
|
251
|
+
const ensured = ensureProject(phrenPath, project);
|
|
252
|
+
if (!ensured.ok)
|
|
253
|
+
return forwardErr(ensured);
|
|
254
|
+
const newTextTrimmed = newText.trim();
|
|
255
|
+
if (!newTextTrimmed)
|
|
256
|
+
return phrenErr("New finding text cannot be empty.", PhrenError.EMPTY_INPUT);
|
|
257
|
+
const findingsPath = path.join(ensured.data, "FINDINGS.md");
|
|
258
|
+
if (!fs.existsSync(findingsPath))
|
|
259
|
+
return phrenErr(`No FINDINGS.md file found for "${project}".`, PhrenError.FILE_NOT_FOUND);
|
|
260
|
+
return withSafeLock(findingsPath, () => {
|
|
261
|
+
const lines = fs.readFileSync(findingsPath, "utf8").split("\n");
|
|
262
|
+
const needle = oldText.trim().toLowerCase();
|
|
263
|
+
const bulletLines = lines.map((line, i) => ({ line, i })).filter(({ line }) => line.startsWith("- "));
|
|
264
|
+
// Stable finding ID match
|
|
265
|
+
const fidNeedle = needle.replace(/^fid:/, "");
|
|
266
|
+
const fidMatch = /^[a-z0-9]{8}$/.test(fidNeedle)
|
|
267
|
+
? bulletLines.filter(({ line }) => new RegExp(`<!--\\s*fid:${fidNeedle}\\s*-->`).test(line))
|
|
268
|
+
: [];
|
|
269
|
+
const exactMatches = bulletLines.filter(({ line }) => line.replace(/^-\s+/, "").replace(/<!--.*?-->/g, "").trim().toLowerCase() === needle);
|
|
270
|
+
const partialMatches = bulletLines.filter(({ line }) => line.toLowerCase().includes(needle));
|
|
271
|
+
let idx;
|
|
272
|
+
if (fidMatch.length === 1) {
|
|
273
|
+
idx = fidMatch[0].i;
|
|
274
|
+
}
|
|
275
|
+
else if (exactMatches.length === 1) {
|
|
276
|
+
idx = exactMatches[0].i;
|
|
277
|
+
}
|
|
278
|
+
else if (exactMatches.length > 1) {
|
|
279
|
+
return phrenErr(`"${oldText}" is ambiguous (${exactMatches.length} exact matches). Use a more specific phrase.`, PhrenError.AMBIGUOUS_MATCH);
|
|
280
|
+
}
|
|
281
|
+
else if (partialMatches.length === 1) {
|
|
282
|
+
idx = partialMatches[0].i;
|
|
283
|
+
}
|
|
284
|
+
else if (partialMatches.length > 1) {
|
|
285
|
+
return phrenErr(`"${oldText}" is ambiguous (${partialMatches.length} partial matches). Use a more specific phrase.`, PhrenError.AMBIGUOUS_MATCH);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
return phrenErr(`No finding matching "${oldText}" in project "${project}".`, PhrenError.NOT_FOUND);
|
|
289
|
+
}
|
|
290
|
+
// Preserve existing metadata comment (fid, citations, etc.)
|
|
291
|
+
const existing = lines[idx];
|
|
292
|
+
const metaMatch = existing.match(/(<!--.*?-->)/g);
|
|
293
|
+
const metaSuffix = metaMatch ? " " + metaMatch.join(" ") : "";
|
|
294
|
+
lines[idx] = `- ${newTextTrimmed}${metaSuffix}`;
|
|
295
|
+
const normalized = lines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
296
|
+
fs.writeFileSync(findingsPath, normalized);
|
|
297
|
+
return phrenOk(`Updated finding in ${project}`);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
// Use shared queueFilePath from utils.ts; alias for local brevity.
|
|
301
|
+
const queuePath = queueFilePath;
|
|
302
|
+
function parseQueueLine(line) {
|
|
303
|
+
const parsed = line.match(/^- \[(\d{4}-\d{2}-\d{2})\]\s*(.+)$/);
|
|
304
|
+
const rawText = parsed ? parsed[2] : line.replace(/^-\s+/, "").trim();
|
|
305
|
+
const confidence = rawText.match(/\[confidence\s+([01](?:\.\d+)?)\]/i);
|
|
306
|
+
const source = parseSourceComment(line);
|
|
307
|
+
let machine = source?.machine;
|
|
308
|
+
let model = source?.model;
|
|
309
|
+
// Strip the confidence marker from the canonical text so it doesn't pollute FINDINGS.md
|
|
310
|
+
const sanitized = normalizeQueueEntryText(rawText.replace(/\s*\[confidence\s+[01](?:\.\d+)?\]/gi, "").trim(), { truncate: true });
|
|
311
|
+
const text = sanitized.ok ? sanitized.data.text : "";
|
|
312
|
+
return {
|
|
313
|
+
date: parsed?.[1],
|
|
314
|
+
text,
|
|
315
|
+
confidence: confidence ? Number.parseFloat(confidence[1]) : undefined,
|
|
316
|
+
machine,
|
|
317
|
+
model,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export function readReviewQueue(phrenPath, project) {
|
|
321
|
+
const ensured = ensureProject(phrenPath, project);
|
|
322
|
+
if (!ensured.ok)
|
|
323
|
+
return forwardErr(ensured);
|
|
324
|
+
const file = queuePath(phrenPath, project);
|
|
325
|
+
if (!fs.existsSync(file))
|
|
326
|
+
return phrenOk([]);
|
|
327
|
+
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
328
|
+
const items = [];
|
|
329
|
+
let section = "Review";
|
|
330
|
+
let index = 1;
|
|
331
|
+
for (const line of lines) {
|
|
332
|
+
const trimmed = line.trim();
|
|
333
|
+
const queueHeading = trimmed.match(/^##\s+(.+?)[\s]*$/i);
|
|
334
|
+
if (queueHeading) {
|
|
335
|
+
const qToken = queueHeading[1].replace(/\s+/g, " ").trim().toLowerCase();
|
|
336
|
+
if (qToken === "review") {
|
|
337
|
+
section = "Review";
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (qToken === "stale") {
|
|
341
|
+
section = "Stale";
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (qToken === "conflicts") {
|
|
345
|
+
section = "Conflicts";
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (!line.startsWith("- "))
|
|
350
|
+
continue;
|
|
351
|
+
const parsed = parseQueueLine(line);
|
|
352
|
+
const risky = section !== "Review" || (parsed.confidence !== undefined && parsed.confidence < 0.7);
|
|
353
|
+
items.push({
|
|
354
|
+
id: `M${index}`,
|
|
355
|
+
section,
|
|
356
|
+
date: parsed.date || "unknown",
|
|
357
|
+
text: parsed.text,
|
|
358
|
+
line,
|
|
359
|
+
confidence: parsed.confidence,
|
|
360
|
+
risky,
|
|
361
|
+
machine: parsed.machine,
|
|
362
|
+
model: parsed.model,
|
|
363
|
+
});
|
|
364
|
+
index++;
|
|
365
|
+
}
|
|
366
|
+
return phrenOk(items);
|
|
367
|
+
}
|
|
368
|
+
export function readReviewQueueAcrossProjects(phrenPath, profile) {
|
|
369
|
+
const projects = getProjectDirs(phrenPath, profile)
|
|
370
|
+
.map((dir) => path.basename(dir))
|
|
371
|
+
.filter((project) => project !== "global")
|
|
372
|
+
.sort();
|
|
373
|
+
const sectionOrder = {
|
|
374
|
+
Review: 0,
|
|
375
|
+
Stale: 1,
|
|
376
|
+
Conflicts: 2,
|
|
377
|
+
};
|
|
378
|
+
const items = [];
|
|
379
|
+
for (const project of projects) {
|
|
380
|
+
const result = readReviewQueue(phrenPath, project);
|
|
381
|
+
if (!result.ok)
|
|
382
|
+
continue;
|
|
383
|
+
for (const item of result.data) {
|
|
384
|
+
items.push({ project, ...item });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
items.sort((a, b) => {
|
|
388
|
+
const aDate = a.date === "unknown" ? "" : a.date;
|
|
389
|
+
const bDate = b.date === "unknown" ? "" : b.date;
|
|
390
|
+
if (a.section !== b.section)
|
|
391
|
+
return sectionOrder[a.section] - sectionOrder[b.section];
|
|
392
|
+
if (aDate !== bDate)
|
|
393
|
+
return bDate.localeCompare(aDate);
|
|
394
|
+
const projectCmp = a.project.localeCompare(b.project);
|
|
395
|
+
if (projectCmp !== 0)
|
|
396
|
+
return projectCmp;
|
|
397
|
+
return a.id.localeCompare(b.id);
|
|
398
|
+
});
|
|
399
|
+
return phrenOk(items);
|
|
400
|
+
}
|