@phren/cli 0.0.27 → 0.0.32
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/mcp/dist/capabilities/cli.js +2 -5
- package/mcp/dist/capabilities/mcp.js +5 -8
- package/mcp/dist/capabilities/types.js +2 -5
- package/mcp/dist/capabilities/vscode.js +2 -5
- package/mcp/dist/capabilities/web-ui.js +2 -5
- package/mcp/dist/{cli-actions.js → cli/actions.js} +22 -21
- package/mcp/dist/{cli.js → cli/cli.js} +13 -13
- package/mcp/dist/{cli-config.js → cli/config.js} +9 -9
- package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
- package/mcp/dist/{cli-govern.js → cli/govern.js} +10 -9
- package/mcp/dist/{cli-graph.js → cli/graph.js} +10 -9
- package/mcp/dist/{cli-hooks-citations.js → cli/hooks-citations.js} +2 -2
- package/mcp/dist/{cli-hooks-context.js → cli/hooks-context.js} +23 -23
- package/mcp/dist/{cli-hooks-globs.js → cli/hooks-globs.js} +4 -4
- package/mcp/dist/{cli-hooks-output.js → cli/hooks-output.js} +9 -10
- package/mcp/dist/{cli-hooks-session.js → cli/hooks-session.js} +42 -57
- package/mcp/dist/{cli-hooks.js → cli/hooks.js} +27 -26
- package/mcp/dist/{cli-namespaces.js → cli/namespaces.js} +25 -24
- package/mcp/dist/{cli-ops.js → cli/ops.js} +9 -9
- package/mcp/dist/{cli-search.js → cli/search.js} +8 -7
- package/mcp/dist/cli-hooks-git.js +243 -0
- package/mcp/dist/cli-hooks-prompt.js +319 -0
- package/mcp/dist/cli-hooks-session-handlers.js +349 -0
- package/mcp/dist/cli-hooks-stop.js +557 -0
- package/mcp/dist/{content-archive.js → content/archive.js} +8 -9
- package/mcp/dist/{content-citation.js → content/citation.js} +5 -5
- package/mcp/dist/{content-dedup.js → content/dedup.js} +9 -12
- package/mcp/dist/{content-learning.js → content/learning.js} +12 -12
- package/mcp/dist/{content-validate.js → content/validate.js} +5 -5
- package/mcp/dist/{core-finding.js → core/finding.js} +4 -4
- package/mcp/dist/{core-project.js → core/project.js} +4 -4
- package/mcp/dist/{core-search.js → core/search.js} +2 -2
- package/mcp/dist/{data-access.js → data/access.js} +131 -13
- package/mcp/dist/{data-tasks.js → data/tasks.js} +7 -5
- package/mcp/dist/embedding.js +9 -14
- package/mcp/dist/entrypoint.js +13 -11
- package/mcp/dist/{finding-context.js → finding/context.js} +2 -2
- package/mcp/dist/{finding-impact.js → finding/impact.js} +3 -3
- package/mcp/dist/{finding-journal.js → finding/journal.js} +4 -4
- package/mcp/dist/{finding-lifecycle.js → finding/lifecycle.js} +4 -4
- package/mcp/dist/{governance-audit.js → governance/audit.js} +2 -2
- package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
- package/mcp/dist/{governance-policy.js → governance/policy.js} +10 -12
- package/mcp/dist/{governance-rbac.js → governance/rbac.js} +3 -3
- package/mcp/dist/{governance-scores.js → governance/scores.js} +8 -10
- package/mcp/dist/hooks.js +39 -31
- package/mcp/dist/index-query.js +4 -1
- package/mcp/dist/index.js +53 -29
- package/mcp/dist/{init-config.js → init/config.js} +6 -6
- package/mcp/dist/{init.js → init/init.js} +28 -29
- package/mcp/dist/{init-preferences.js → init/preferences.js} +3 -3
- package/mcp/dist/{init-setup.js → init/setup.js} +17 -19
- package/mcp/dist/{init-shared.js → init/shared.js} +3 -3
- package/mcp/dist/init-bootstrap.js +68 -0
- package/mcp/dist/init-detect.js +38 -0
- package/mcp/dist/init-dryrun.js +55 -0
- package/mcp/dist/init-env.js +114 -0
- package/mcp/dist/init-fresh.js +239 -0
- package/mcp/dist/init-hooks.js +26 -0
- package/mcp/dist/init-mcp.js +65 -0
- package/mcp/dist/init-migrate.js +51 -0
- package/mcp/dist/init-modes.js +135 -0
- package/mcp/dist/init-npm.js +37 -0
- package/mcp/dist/init-project-local.js +99 -0
- package/mcp/dist/init-semantic.js +48 -0
- package/mcp/dist/init-types.js +1 -0
- package/mcp/dist/init-uninstall.js +482 -0
- package/mcp/dist/init-update.js +96 -0
- package/mcp/dist/init-walkthrough-merge.js +90 -0
- package/mcp/dist/init-walkthrough.js +529 -0
- package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
- package/mcp/dist/{link-context.js → link/context.js} +4 -4
- package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
- package/mcp/dist/{link.js → link/link.js} +26 -31
- package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
- package/mcp/dist/logger.js +11 -3
- package/mcp/dist/phren-art.js +0 -6
- package/mcp/dist/phren-paths.js +30 -12
- package/mcp/dist/proactivity.js +2 -2
- package/mcp/dist/profile-store.js +5 -6
- package/mcp/dist/project-config.js +2 -2
- package/mcp/dist/project-topics.js +1 -1
- package/mcp/dist/query-correlation.js +1 -1
- package/mcp/dist/{session-checkpoints.js → session/checkpoints.js} +3 -3
- package/mcp/dist/{session-utils.js → session/utils.js} +1 -1
- package/mcp/dist/{shared-content.js → shared/content.js} +7 -7
- package/mcp/dist/{shared-data-utils.js → shared/data-utils.js} +3 -3
- package/mcp/dist/{shared-embedding-cache.js → shared/embedding-cache.js} +3 -3
- package/mcp/dist/{shared-fragment-graph.js → shared/fragment-graph.js} +15 -24
- package/mcp/dist/shared/governance.js +4 -0
- package/mcp/dist/{shared-index.js → shared/index.js} +92 -123
- package/mcp/dist/{shared-ollama.js → shared/ollama.js} +2 -2
- package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +16 -21
- package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +17 -20
- package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
- package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
- package/mcp/dist/shared.js +4 -59
- package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
- package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
- package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
- package/mcp/dist/{shell-render.js → shell/render.js} +1 -1
- package/mcp/dist/{shell.js → shell/shell.js} +11 -11
- package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
- package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
- package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
- package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
- package/mcp/dist/{skill-registry.js → skill/registry.js} +4 -4
- package/mcp/dist/{skill-state.js → skill/state.js} +1 -1
- package/mcp/dist/startup-embedding.js +2 -2
- package/mcp/dist/status.js +15 -14
- package/mcp/dist/{tasks-github.js → task/github.js} +2 -2
- package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
- package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +7 -7
- package/mcp/dist/telemetry.js +3 -4
- package/mcp/dist/tool-registry.js +29 -17
- package/mcp/dist/tools/config.js +515 -0
- package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
- package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
- package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
- package/mcp/dist/{mcp-finding.js → tools/finding.js} +97 -124
- package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
- package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
- package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
- package/mcp/dist/{mcp-ops.js → tools/ops.js} +169 -71
- package/mcp/dist/{mcp-search.js → tools/search.js} +19 -23
- package/mcp/dist/{mcp-session.js → tools/session.js} +48 -23
- package/mcp/dist/{mcp-skills.js → tools/skills.js} +33 -35
- package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
- package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
- package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
- package/mcp/dist/{memory-ui-page.js → ui/page.js} +4 -6
- package/mcp/dist/{memory-ui-server.js → ui/server.js} +30 -22
- package/mcp/dist/update.js +2 -2
- package/mcp/dist/utils.js +51 -11
- package/package.json +17 -11
- package/scripts/preuninstall.mjs +139 -0
- package/starter/global/CLAUDE.md +3 -2
- package/mcp/dist/mcp-config.js +0 -551
- package/mcp/dist/shared-governance.js +0 -4
- package/starter/global/skills/pipeline.md +0 -35
- package/starter/global/skills/release.md +0 -35
- /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
- /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
- /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
- /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
- /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
- /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
- /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
- /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from "fs";
|
|
8
8
|
import * as path from "path";
|
|
9
|
-
import { debugLog } from "
|
|
10
|
-
import { safeProjectPath, isFeatureEnabled, errorMessage } from "
|
|
11
|
-
import { callLlm } from "
|
|
12
|
-
import { withFileLock } from "
|
|
9
|
+
import { debugLog } from "../shared.js";
|
|
10
|
+
import { safeProjectPath, isFeatureEnabled, errorMessage } from "../utils.js";
|
|
11
|
+
import { callLlm } from "../content/dedup.js";
|
|
12
|
+
import { withFileLock } from "../shared/governance.js";
|
|
13
|
+
import { logger } from "../logger.js";
|
|
13
14
|
const FACT_EXTRACT_FLAG = "PHREN_FEATURE_FACT_EXTRACT";
|
|
14
15
|
const MAX_FACTS = 50;
|
|
15
16
|
function preferencesPath(phrenPath, project) {
|
|
@@ -25,8 +26,7 @@ export function readExtractedFacts(phrenPath, project) {
|
|
|
25
26
|
return Array.isArray(data) ? data : [];
|
|
26
27
|
}
|
|
27
28
|
catch (err) {
|
|
28
|
-
|
|
29
|
-
process.stderr.write(`[phren] readExtractedFacts: ${errorMessage(err)}\n`);
|
|
29
|
+
logger.debug("extract-facts", `readExtractedFacts: ${errorMessage(err)}`);
|
|
30
30
|
return [];
|
|
31
31
|
}
|
|
32
32
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { mcpResponse } from "./
|
|
1
|
+
import { mcpResponse } from "./types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { isValidProjectName, safeProjectPath, errorMessage } from "
|
|
4
|
-
import { addFindingsToFile } from "
|
|
5
|
-
import { checkOllamaAvailable, checkModelAvailable, generateText, getOllamaUrl, getExtractModel } from "
|
|
6
|
-
import { debugLog } from "
|
|
7
|
-
import { getProactivityLevelForFindings, shouldAutoCaptureFindingsForLevel } from "
|
|
3
|
+
import { isValidProjectName, safeProjectPath, errorMessage } from "../utils.js";
|
|
4
|
+
import { addFindingsToFile } from "../shared/content.js";
|
|
5
|
+
import { checkOllamaAvailable, checkModelAvailable, generateText, getOllamaUrl, getExtractModel } from "../shared/ollama.js";
|
|
6
|
+
import { debugLog } from "../shared.js";
|
|
7
|
+
import { getProactivityLevelForFindings, shouldAutoCaptureFindingsForLevel } from "../proactivity.js";
|
|
8
8
|
import * as path from "path";
|
|
9
9
|
const EXTRACT_PROMPT = `You are extracting non-obvious engineering insights from text.
|
|
10
10
|
Output ONLY a JSON array of strings. Each string is a specific, actionable finding.
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import { mcpResponse } from "./
|
|
1
|
+
import { mcpResponse } from "./types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
import { isValidProjectName, safeProjectPath, errorMessage } from "../utils.js";
|
|
7
|
+
import { removeFinding as removeFindingCore, removeFindings as removeFindingsCore, } from "../core/finding.js";
|
|
8
|
+
import { debugLog, EXEC_TIMEOUT_MS, FINDING_TYPES, normalizeMemoryScope, RESERVED_PROJECT_DIR_NAMES, } from "../shared.js";
|
|
9
|
+
import { addFindingToFile, addFindingsToFile, checkSemanticConflicts, autoMergeConflicts, } from "../shared/content.js";
|
|
10
|
+
import { jaccardTokenize, jaccardSimilarity, stripMetadata } from "../content/dedup.js";
|
|
11
|
+
import { runCustomHooks } from "../hooks.js";
|
|
12
|
+
import { incrementSessionFindings } from "./session.js";
|
|
13
|
+
import { extractFragmentNames } from "../shared/fragment-graph.js";
|
|
14
|
+
import { extractFactFromFinding } from "./extract-facts.js";
|
|
15
|
+
import { appendChildFinding, editFinding as editFindingCore, readFindings } from "../data/access.js";
|
|
16
|
+
import { getActiveTaskForSession } from "../task/lifecycle.js";
|
|
17
|
+
import { FINDING_PROVENANCE_SOURCES } from "../content/citation.js";
|
|
18
|
+
import { isInactiveFindingLine, supersedeFinding, retractFinding as retractFindingLifecycle, resolveFindingContradiction, } from "../finding/lifecycle.js";
|
|
19
|
+
import { permissionDeniedError } from "../governance/rbac.js";
|
|
19
20
|
const JACCARD_MAYBE_LOW = 0.30;
|
|
20
21
|
const JACCARD_MAYBE_HIGH = 0.55; // above this isDuplicateFinding already catches it
|
|
21
22
|
function findJaccardCandidates(phrenPath, project, finding) {
|
|
@@ -86,13 +87,17 @@ export function register(server, ctx) {
|
|
|
86
87
|
const { phrenPath, withWriteQueue, updateFileInIndex } = ctx;
|
|
87
88
|
server.registerTool("add_finding", {
|
|
88
89
|
title: "◆ phren · save finding",
|
|
89
|
-
description: "Tell phren
|
|
90
|
+
description: "Tell phren one or more insights for a project's FINDINGS.md. Call this the moment you discover " +
|
|
90
91
|
"a non-obvious pattern, hit a subtle bug, find a workaround, or learn something that would " +
|
|
91
92
|
"save time in a future session. Do not wait until the end of the session." +
|
|
93
|
+
" Pass a single string or an array of strings." +
|
|
92
94
|
" Optionally classify with findingType: decision, pitfall, pattern, tradeoff, architecture, or bug.",
|
|
93
95
|
inputSchema: z.object({
|
|
94
96
|
project: z.string().describe("Project name (must match a directory in your phren store)."),
|
|
95
|
-
finding: z.
|
|
97
|
+
finding: z.union([
|
|
98
|
+
z.string().describe("A single insight, written as a bullet point."),
|
|
99
|
+
z.array(z.string()).describe("Multiple insights to record in one call."),
|
|
100
|
+
]).describe("The insight(s) to save. Pass a string for one finding, or an array for bulk."),
|
|
96
101
|
citation: z.object({
|
|
97
102
|
file: z.string().optional().describe("Source file path that supports this finding."),
|
|
98
103
|
line: z.number().int().positive().optional().describe("1-based line number in file."),
|
|
@@ -100,7 +105,7 @@ export function register(server, ctx) {
|
|
|
100
105
|
commit: z.string().optional().describe("Git commit SHA that supports this finding."),
|
|
101
106
|
supersedes: z.string().optional().describe("First 60 chars of the old finding this one replaces. The old entry will be marked as superseded."),
|
|
102
107
|
task_item: z.string().optional().describe("Task item stable ID like bid:abcd1234, positional ID like A1, or item text to link this finding to."),
|
|
103
|
-
}).optional().describe("Optional source citation for traceability."),
|
|
108
|
+
}).optional().describe("Optional source citation for traceability (only used when finding is a single string)."),
|
|
104
109
|
sessionId: z.string().optional().describe("Optional session ID from session_start. Pass this if you want session metrics to include this write."),
|
|
105
110
|
source: z.enum(FINDING_PROVENANCE_SOURCES)
|
|
106
111
|
.optional()
|
|
@@ -116,6 +121,55 @@ export function register(server, ctx) {
|
|
|
116
121
|
const addFindingDenied = permissionDeniedError(phrenPath, "add_finding", project);
|
|
117
122
|
if (addFindingDenied)
|
|
118
123
|
return mcpResponse({ ok: false, error: addFindingDenied });
|
|
124
|
+
if (Array.isArray(finding)) {
|
|
125
|
+
const findings = finding;
|
|
126
|
+
if (findings.length > 100)
|
|
127
|
+
return mcpResponse({ ok: false, error: "Bulk add limited to 100 findings per call." });
|
|
128
|
+
if (findings.some((f) => f.length > 5000))
|
|
129
|
+
return mcpResponse({ ok: false, error: "One or more findings exceed 5000 character limit." });
|
|
130
|
+
return withWriteQueue(async () => {
|
|
131
|
+
runCustomHooks(phrenPath, "pre-finding", { PHREN_PROJECT: project });
|
|
132
|
+
const allPotentialDuplicates = [];
|
|
133
|
+
const extraAnnotationsByFinding = [];
|
|
134
|
+
for (const f of findings) {
|
|
135
|
+
const candidates = findJaccardCandidates(phrenPath, project, f);
|
|
136
|
+
if (candidates.length > 0)
|
|
137
|
+
allPotentialDuplicates.push({ finding: f, candidates });
|
|
138
|
+
try {
|
|
139
|
+
const conflicts = await checkSemanticConflicts(phrenPath, project, f);
|
|
140
|
+
extraAnnotationsByFinding.push(conflicts.checked && conflicts.annotations.length > 0 ? conflicts.annotations : []);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
logger.debug("add_finding", `bulk semanticConflict: ${errorMessage(err)}`);
|
|
144
|
+
extraAnnotationsByFinding.push([]);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const result = addFindingsToFile(phrenPath, project, findings, {
|
|
148
|
+
extraAnnotationsByFinding,
|
|
149
|
+
sessionId,
|
|
150
|
+
});
|
|
151
|
+
if (!result.ok)
|
|
152
|
+
return mcpResponse({ ok: false, error: result.error });
|
|
153
|
+
const { added, skipped, rejected } = result.data;
|
|
154
|
+
if (added.length > 0) {
|
|
155
|
+
runCustomHooks(phrenPath, "post-finding", { PHREN_PROJECT: project });
|
|
156
|
+
incrementSessionFindings(phrenPath, added.length, sessionId, project);
|
|
157
|
+
updateFileInIndex(path.join(phrenPath, project, "FINDINGS.md"));
|
|
158
|
+
}
|
|
159
|
+
const rejectedMsg = rejected.length > 0 ? `, ${rejected.length} rejected` : "";
|
|
160
|
+
return mcpResponse({
|
|
161
|
+
ok: true,
|
|
162
|
+
message: `Added ${added.length}/${findings.length} findings (${skipped.length} duplicates skipped${rejectedMsg})`,
|
|
163
|
+
data: {
|
|
164
|
+
project,
|
|
165
|
+
added,
|
|
166
|
+
skipped,
|
|
167
|
+
rejected,
|
|
168
|
+
...(allPotentialDuplicates.length > 0 ? { potentialDuplicates: allPotentialDuplicates } : {}),
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
119
173
|
if (finding.length > 5000)
|
|
120
174
|
return mcpResponse({ ok: false, error: "Finding text exceeds 5000 character limit." });
|
|
121
175
|
const normalizedScope = normalizeMemoryScope(scope ?? "shared");
|
|
@@ -207,70 +261,6 @@ export function register(server, ctx) {
|
|
|
207
261
|
}
|
|
208
262
|
});
|
|
209
263
|
});
|
|
210
|
-
server.registerTool("add_findings", {
|
|
211
|
-
title: "◆ phren · save findings (bulk)",
|
|
212
|
-
description: "Tell phren multiple insights for a project's FINDINGS.md in one call.",
|
|
213
|
-
inputSchema: z.object({
|
|
214
|
-
project: z.string().describe("Project name (must match a directory in your phren store)."),
|
|
215
|
-
findings: z.array(z.string()).describe("List of insights to record."),
|
|
216
|
-
sessionId: z.string().optional().describe("Optional session ID from session_start. Pass this if you want session metrics to include this write."),
|
|
217
|
-
}),
|
|
218
|
-
}, async ({ project, findings, sessionId }) => {
|
|
219
|
-
if (!isValidProjectName(project))
|
|
220
|
-
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
221
|
-
const addFindingsDenied = permissionDeniedError(phrenPath, "add_finding", project);
|
|
222
|
-
if (addFindingsDenied)
|
|
223
|
-
return mcpResponse({ ok: false, error: addFindingsDenied });
|
|
224
|
-
if (findings.length > 100)
|
|
225
|
-
return mcpResponse({ ok: false, error: "Bulk add limited to 100 findings per call." });
|
|
226
|
-
if (findings.some((f) => f.length > 5000))
|
|
227
|
-
return mcpResponse({ ok: false, error: "One or more findings exceed 5000 character limit." });
|
|
228
|
-
return withWriteQueue(async () => {
|
|
229
|
-
runCustomHooks(phrenPath, "pre-finding", { PHREN_PROJECT: project });
|
|
230
|
-
// Jaccard "maybe zone" scan per finding — free, no LLM. Agent sees candidates and decides.
|
|
231
|
-
const allPotentialDuplicates = [];
|
|
232
|
-
const extraAnnotationsByFinding = [];
|
|
233
|
-
for (const f of findings) {
|
|
234
|
-
const candidates = findJaccardCandidates(phrenPath, project, f);
|
|
235
|
-
if (candidates.length > 0)
|
|
236
|
-
allPotentialDuplicates.push({ finding: f, candidates });
|
|
237
|
-
try {
|
|
238
|
-
const conflicts = await checkSemanticConflicts(phrenPath, project, f);
|
|
239
|
-
extraAnnotationsByFinding.push(conflicts.checked && conflicts.annotations.length > 0 ? conflicts.annotations : []);
|
|
240
|
-
}
|
|
241
|
-
catch (err) {
|
|
242
|
-
if ((process.env.PHREN_DEBUG))
|
|
243
|
-
process.stderr.write(`[phren] add_findings semanticConflict: ${errorMessage(err)}\n`);
|
|
244
|
-
extraAnnotationsByFinding.push([]);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const result = addFindingsToFile(phrenPath, project, findings, {
|
|
248
|
-
extraAnnotationsByFinding,
|
|
249
|
-
sessionId,
|
|
250
|
-
});
|
|
251
|
-
if (!result.ok)
|
|
252
|
-
return mcpResponse({ ok: false, error: result.error });
|
|
253
|
-
const { added, skipped, rejected } = result.data;
|
|
254
|
-
if (added.length > 0) {
|
|
255
|
-
runCustomHooks(phrenPath, "post-finding", { PHREN_PROJECT: project });
|
|
256
|
-
incrementSessionFindings(phrenPath, added.length, sessionId, project);
|
|
257
|
-
updateFileInIndex(path.join(phrenPath, project, "FINDINGS.md"));
|
|
258
|
-
}
|
|
259
|
-
const rejectedMsg = rejected.length > 0 ? `, ${rejected.length} rejected` : "";
|
|
260
|
-
// ok:true whenever the operation completed without error — use counts to distinguish outcomes.
|
|
261
|
-
return mcpResponse({
|
|
262
|
-
ok: true,
|
|
263
|
-
message: `Added ${added.length}/${findings.length} findings (${skipped.length} duplicates skipped${rejectedMsg})`,
|
|
264
|
-
data: {
|
|
265
|
-
project,
|
|
266
|
-
added,
|
|
267
|
-
skipped,
|
|
268
|
-
rejected,
|
|
269
|
-
...(allPotentialDuplicates.length > 0 ? { potentialDuplicates: allPotentialDuplicates } : {}),
|
|
270
|
-
},
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
264
|
server.registerTool("supersede_finding", {
|
|
275
265
|
title: "◆ phren · supersede finding",
|
|
276
266
|
description: "Mark an existing finding as superseded and link it to the newer finding text.",
|
|
@@ -425,11 +415,15 @@ export function register(server, ctx) {
|
|
|
425
415
|
});
|
|
426
416
|
server.registerTool("remove_finding", {
|
|
427
417
|
title: "◆ phren · remove finding",
|
|
428
|
-
description: "Remove
|
|
429
|
-
"previously captured insight turns out to be wrong, outdated, or no longer relevant."
|
|
418
|
+
description: "Remove one or more findings from a project's FINDINGS.md by matching text. Use this when a " +
|
|
419
|
+
"previously captured insight turns out to be wrong, outdated, or no longer relevant." +
|
|
420
|
+
" Pass a single string or an array of strings.",
|
|
430
421
|
inputSchema: z.object({
|
|
431
422
|
project: z.string().describe("Project name."),
|
|
432
|
-
finding: z.
|
|
423
|
+
finding: z.union([
|
|
424
|
+
z.string().describe("Partial text to match against existing findings."),
|
|
425
|
+
z.array(z.string()).describe("List of partial texts to match and remove."),
|
|
426
|
+
]).describe("Text(s) to match and remove. Pass a string for one, or an array for bulk."),
|
|
433
427
|
}),
|
|
434
428
|
}, async ({ project, finding }) => {
|
|
435
429
|
if (!isValidProjectName(project))
|
|
@@ -437,41 +431,25 @@ export function register(server, ctx) {
|
|
|
437
431
|
const removeDenied = permissionDeniedError(phrenPath, "remove_finding", project);
|
|
438
432
|
if (removeDenied)
|
|
439
433
|
return mcpResponse({ ok: false, error: removeDenied });
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
434
|
+
if (Array.isArray(finding)) {
|
|
435
|
+
return withWriteQueue(async () => {
|
|
436
|
+
const result = removeFindingsCore(phrenPath, project, finding);
|
|
437
|
+
if (!result.ok)
|
|
438
|
+
return mcpResponse({ ok: false, error: result.message });
|
|
443
439
|
const resolvedFindingsDir = safeProjectPath(phrenPath, project);
|
|
444
440
|
if (resolvedFindingsDir)
|
|
445
441
|
updateFileInIndex(path.join(resolvedFindingsDir, "FINDINGS.md"));
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
return mcpResponse({ ok: true, message: result.message, data: result.data });
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
server.registerTool("remove_findings", {
|
|
453
|
-
title: "◆ phren · remove findings (bulk)",
|
|
454
|
-
description: "Remove multiple findings from a project's FINDINGS.md in one call.",
|
|
455
|
-
inputSchema: z.object({
|
|
456
|
-
project: z.string().describe("Project name."),
|
|
457
|
-
findings: z.array(z.string()).describe("List of partial texts to match and remove."),
|
|
458
|
-
}),
|
|
459
|
-
}, async ({ project, findings }) => {
|
|
460
|
-
if (!isValidProjectName(project))
|
|
461
|
-
return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
|
|
462
|
-
const removeFindingsDenied = permissionDeniedError(phrenPath, "remove_finding", project);
|
|
463
|
-
if (removeFindingsDenied)
|
|
464
|
-
return mcpResponse({ ok: false, error: removeFindingsDenied });
|
|
442
|
+
return mcpResponse({ ok: true, message: result.message, data: result.data });
|
|
443
|
+
});
|
|
444
|
+
}
|
|
465
445
|
return withWriteQueue(async () => {
|
|
466
|
-
const result =
|
|
467
|
-
if (result.ok) {
|
|
468
|
-
const resolvedFindingsDir = safeProjectPath(phrenPath, project);
|
|
469
|
-
if (resolvedFindingsDir)
|
|
470
|
-
updateFileInIndex(path.join(resolvedFindingsDir, "FINDINGS.md"));
|
|
471
|
-
}
|
|
446
|
+
const result = removeFindingCore(phrenPath, project, finding);
|
|
472
447
|
if (!result.ok)
|
|
473
448
|
return mcpResponse({ ok: false, error: result.message });
|
|
474
|
-
|
|
449
|
+
const resolvedFindingsDir = safeProjectPath(phrenPath, project);
|
|
450
|
+
if (resolvedFindingsDir)
|
|
451
|
+
updateFileInIndex(path.join(resolvedFindingsDir, "FINDINGS.md"));
|
|
452
|
+
return mcpResponse({ ok: true, message: result.message, data: result.data });
|
|
475
453
|
});
|
|
476
454
|
});
|
|
477
455
|
server.registerTool("push_changes", {
|
|
@@ -511,8 +489,7 @@ export function register(server, ctx) {
|
|
|
511
489
|
hasRemote = remotes.length > 0;
|
|
512
490
|
}
|
|
513
491
|
catch (err) {
|
|
514
|
-
|
|
515
|
-
process.stderr.write(`[phren] push_changes remoteCheck: ${errorMessage(err)}\n`);
|
|
492
|
+
logger.warn("push_changes", `remoteCheck: ${errorMessage(err)}`);
|
|
516
493
|
}
|
|
517
494
|
if (!hasRemote) {
|
|
518
495
|
const changedFiles = status.split("\n").filter(Boolean).length;
|
|
@@ -535,8 +512,7 @@ export function register(server, ctx) {
|
|
|
535
512
|
runGit(["pull", "--rebase", "--quiet"], { timeout: 15000 });
|
|
536
513
|
}
|
|
537
514
|
catch (pullErr) {
|
|
538
|
-
|
|
539
|
-
process.stderr.write(`[phren] push_changes pullRebase: ${pullErr instanceof Error ? pullErr.message : String(pullErr)}\n`);
|
|
515
|
+
logger.warn("push_changes", `pullRebase: ${pullErr instanceof Error ? pullErr.message : String(pullErr)}`);
|
|
540
516
|
const resolved = autoMergeConflicts(phrenPath);
|
|
541
517
|
if (resolved) {
|
|
542
518
|
try {
|
|
@@ -546,14 +522,12 @@ export function register(server, ctx) {
|
|
|
546
522
|
});
|
|
547
523
|
}
|
|
548
524
|
catch (continueErr) {
|
|
549
|
-
|
|
550
|
-
process.stderr.write(`[phren] push_changes rebaseContinue: ${continueErr instanceof Error ? continueErr.message : String(continueErr)}\n`);
|
|
525
|
+
logger.warn("push_changes", `rebaseContinue: ${continueErr instanceof Error ? continueErr.message : String(continueErr)}`);
|
|
551
526
|
try {
|
|
552
527
|
runGit(["rebase", "--abort"]);
|
|
553
528
|
}
|
|
554
529
|
catch (abortErr) {
|
|
555
|
-
|
|
556
|
-
process.stderr.write(`[phren] push_changes rebaseAbort: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}\n`);
|
|
530
|
+
logger.warn("push_changes", `rebaseAbort: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
557
531
|
}
|
|
558
532
|
break;
|
|
559
533
|
}
|
|
@@ -563,8 +537,7 @@ export function register(server, ctx) {
|
|
|
563
537
|
runGit(["rebase", "--abort"]);
|
|
564
538
|
}
|
|
565
539
|
catch (abortErr) {
|
|
566
|
-
|
|
567
|
-
process.stderr.write(`[phren] push_changes rebaseAbort2: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}\n`);
|
|
540
|
+
logger.warn("push_changes", `rebaseAbort2: ${abortErr instanceof Error ? abortErr.message : String(abortErr)}`);
|
|
568
541
|
}
|
|
569
542
|
break;
|
|
570
543
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { mcpResponse } from "./
|
|
1
|
+
import { mcpResponse } from "./types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as crypto from "crypto";
|
|
5
|
-
import { isValidProjectName, errorMessage } from "
|
|
6
|
-
import { queryDocBySourceKey, queryRows, queryFragmentLinks, queryCrossProjectFragments, ensureGlobalEntitiesTable, logFragmentMiss } from "
|
|
7
|
-
import { runtimeFile } from "
|
|
8
|
-
import { withFileLock } from "
|
|
5
|
+
import { isValidProjectName, errorMessage } from "../utils.js";
|
|
6
|
+
import { queryDocBySourceKey, queryRows, queryFragmentLinks, queryCrossProjectFragments, ensureGlobalEntitiesTable, logFragmentMiss } from "../shared/index.js";
|
|
7
|
+
import { runtimeFile } from "../shared.js";
|
|
8
|
+
import { withFileLock } from "../shared/governance.js";
|
|
9
|
+
import { logger } from "../logger.js";
|
|
9
10
|
export function register(server, ctx) {
|
|
10
11
|
// ── search_fragments ──────────────────────────────────────────────────
|
|
11
12
|
server.registerTool("search_fragments", {
|
|
@@ -209,8 +210,7 @@ export function register(server, ctx) {
|
|
|
209
210
|
db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [fragmentName, resolvedFragmentType, new Date().toISOString().slice(0, 10)]);
|
|
210
211
|
}
|
|
211
212
|
catch (err) {
|
|
212
|
-
|
|
213
|
-
process.stderr.write(`[phren] link_findings fragmentInsert: ${errorMessage(err)}\n`);
|
|
213
|
+
logger.debug("graph", `link_findings fragmentInsert: ${errorMessage(err)}`);
|
|
214
214
|
}
|
|
215
215
|
const fragmentResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [fragmentName, resolvedFragmentType]);
|
|
216
216
|
if (!fragmentResult?.length || !fragmentResult[0]?.values?.length) {
|
|
@@ -232,8 +232,7 @@ export function register(server, ctx) {
|
|
|
232
232
|
db.run("INSERT OR IGNORE INTO entities (name, type, first_seen_at) VALUES (?, ?, ?)", [sourceDoc, "document", new Date().toISOString().slice(0, 10)]);
|
|
233
233
|
}
|
|
234
234
|
catch (err) {
|
|
235
|
-
|
|
236
|
-
process.stderr.write(`[phren] link_findings docFragmentInsert: ${errorMessage(err)}\n`);
|
|
235
|
+
logger.debug("graph", `link_findings docFragmentInsert: ${errorMessage(err)}`);
|
|
237
236
|
}
|
|
238
237
|
const docFragmentResult = db.exec("SELECT id FROM entities WHERE name = ? AND type = ?", [sourceDoc, "document"]);
|
|
239
238
|
if (!docFragmentResult?.length || !docFragmentResult[0]?.values?.length) {
|
|
@@ -245,8 +244,7 @@ export function register(server, ctx) {
|
|
|
245
244
|
db.run("INSERT OR IGNORE INTO entity_links (source_id, target_id, rel_type, source_doc) VALUES (?, ?, ?, ?)", [sourceId, targetId, relType, sourceDoc]);
|
|
246
245
|
}
|
|
247
246
|
catch (err) {
|
|
248
|
-
|
|
249
|
-
process.stderr.write(`[phren] link_findings linkInsert: ${errorMessage(err)}\n`);
|
|
247
|
+
logger.debug("graph", `link_findings linkInsert: ${errorMessage(err)}`);
|
|
250
248
|
return mcpResponse({ ok: false, error: "Failed to insert fragment link." });
|
|
251
249
|
}
|
|
252
250
|
// 4a. Also populate global_entities so manual links appear in cross_project_fragments
|
|
@@ -255,8 +253,7 @@ export function register(server, ctx) {
|
|
|
255
253
|
db.run("INSERT OR IGNORE INTO global_entities (entity, project, doc_key) VALUES (?, ?, ?)", [fragmentName, project, sourceDoc]);
|
|
256
254
|
}
|
|
257
255
|
catch (err) {
|
|
258
|
-
|
|
259
|
-
process.stderr.write(`[phren] link_findings globalFragments: ${errorMessage(err)}\n`);
|
|
256
|
+
logger.debug("graph", `link_findings globalFragments: ${errorMessage(err)}`);
|
|
260
257
|
}
|
|
261
258
|
// 4b. Persist manual link so it survives index rebuilds (mandatory — failure aborts the operation)
|
|
262
259
|
const manualLinksPath = runtimeFile(ctx.phrenPath, "manual-links.json");
|
|
@@ -268,7 +265,7 @@ export function register(server, ctx) {
|
|
|
268
265
|
existing = JSON.parse(fs.readFileSync(manualLinksPath, "utf8"));
|
|
269
266
|
}
|
|
270
267
|
catch (err) {
|
|
271
|
-
|
|
268
|
+
logger.error("graph", `link_findings manualLinksRead: manual-links.json is malformed — aborting to avoid data loss: ${errorMessage(err)}`);
|
|
272
269
|
throw err;
|
|
273
270
|
}
|
|
274
271
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { mcpResponse } from "./
|
|
1
|
+
import { mcpResponse } from "./types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
-
import { readInstallPreferences, updateInstallPreferences } from "
|
|
6
|
-
import { readCustomHooks, getHookTarget, HOOK_EVENT_VALUES, validateCustomHookCommand, validateCustomWebhookUrl } from "
|
|
7
|
-
import { hookConfigPath } from "
|
|
8
|
-
import { PROJECT_HOOK_EVENTS, isProjectHookEnabled, readProjectConfig, writeProjectHookConfig } from "
|
|
9
|
-
import { isValidProjectName } from "
|
|
5
|
+
import { readInstallPreferences, updateInstallPreferences } from "../init/preferences.js";
|
|
6
|
+
import { readCustomHooks, getHookTarget, HOOK_EVENT_VALUES, validateCustomHookCommand, validateCustomWebhookUrl } from "../hooks.js";
|
|
7
|
+
import { hookConfigPath } from "../shared.js";
|
|
8
|
+
import { PROJECT_HOOK_EVENTS, isProjectHookEnabled, readProjectConfig, writeProjectHookConfig } from "../project-config.js";
|
|
9
|
+
import { isValidProjectName } from "../utils.js";
|
|
10
10
|
const HOOK_TOOLS = ["claude", "copilot", "cursor", "codex"];
|
|
11
11
|
const VALID_CUSTOM_EVENTS = HOOK_EVENT_VALUES;
|
|
12
12
|
function normalizeHookTool(input) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { mcpResponse } from "./
|
|
1
|
+
import { mcpResponse } from "./types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
|
-
import { runtimeDir } from "
|
|
6
|
-
import { recordFeedback, flushEntryScores, } from "
|
|
7
|
-
import { upsertCanonical } from "
|
|
8
|
-
import { isValidProjectName } from "
|
|
5
|
+
import { runtimeDir } from "../shared.js";
|
|
6
|
+
import { recordFeedback, flushEntryScores, } from "../shared/governance.js";
|
|
7
|
+
import { upsertCanonical } from "../shared/content.js";
|
|
8
|
+
import { isValidProjectName } from "../utils.js";
|
|
9
9
|
export function register(server, ctx) {
|
|
10
10
|
const { phrenPath, withWriteQueue, updateFileInIndex } = ctx;
|
|
11
11
|
server.registerTool("pin_memory", {
|