@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.
Files changed (149) hide show
  1. package/mcp/dist/capabilities/cli.js +2 -5
  2. package/mcp/dist/capabilities/mcp.js +5 -8
  3. package/mcp/dist/capabilities/types.js +2 -5
  4. package/mcp/dist/capabilities/vscode.js +2 -5
  5. package/mcp/dist/capabilities/web-ui.js +2 -5
  6. package/mcp/dist/{cli-actions.js → cli/actions.js} +22 -21
  7. package/mcp/dist/{cli.js → cli/cli.js} +13 -13
  8. package/mcp/dist/{cli-config.js → cli/config.js} +9 -9
  9. package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
  10. package/mcp/dist/{cli-govern.js → cli/govern.js} +10 -9
  11. package/mcp/dist/{cli-graph.js → cli/graph.js} +10 -9
  12. package/mcp/dist/{cli-hooks-citations.js → cli/hooks-citations.js} +2 -2
  13. package/mcp/dist/{cli-hooks-context.js → cli/hooks-context.js} +23 -23
  14. package/mcp/dist/{cli-hooks-globs.js → cli/hooks-globs.js} +4 -4
  15. package/mcp/dist/{cli-hooks-output.js → cli/hooks-output.js} +9 -10
  16. package/mcp/dist/{cli-hooks-session.js → cli/hooks-session.js} +42 -57
  17. package/mcp/dist/{cli-hooks.js → cli/hooks.js} +27 -26
  18. package/mcp/dist/{cli-namespaces.js → cli/namespaces.js} +25 -24
  19. package/mcp/dist/{cli-ops.js → cli/ops.js} +9 -9
  20. package/mcp/dist/{cli-search.js → cli/search.js} +8 -7
  21. package/mcp/dist/cli-hooks-git.js +243 -0
  22. package/mcp/dist/cli-hooks-prompt.js +319 -0
  23. package/mcp/dist/cli-hooks-session-handlers.js +349 -0
  24. package/mcp/dist/cli-hooks-stop.js +557 -0
  25. package/mcp/dist/{content-archive.js → content/archive.js} +8 -9
  26. package/mcp/dist/{content-citation.js → content/citation.js} +5 -5
  27. package/mcp/dist/{content-dedup.js → content/dedup.js} +9 -12
  28. package/mcp/dist/{content-learning.js → content/learning.js} +12 -12
  29. package/mcp/dist/{content-validate.js → content/validate.js} +5 -5
  30. package/mcp/dist/{core-finding.js → core/finding.js} +4 -4
  31. package/mcp/dist/{core-project.js → core/project.js} +4 -4
  32. package/mcp/dist/{core-search.js → core/search.js} +2 -2
  33. package/mcp/dist/{data-access.js → data/access.js} +131 -13
  34. package/mcp/dist/{data-tasks.js → data/tasks.js} +7 -5
  35. package/mcp/dist/embedding.js +9 -14
  36. package/mcp/dist/entrypoint.js +13 -11
  37. package/mcp/dist/{finding-context.js → finding/context.js} +2 -2
  38. package/mcp/dist/{finding-impact.js → finding/impact.js} +3 -3
  39. package/mcp/dist/{finding-journal.js → finding/journal.js} +4 -4
  40. package/mcp/dist/{finding-lifecycle.js → finding/lifecycle.js} +4 -4
  41. package/mcp/dist/{governance-audit.js → governance/audit.js} +2 -2
  42. package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
  43. package/mcp/dist/{governance-policy.js → governance/policy.js} +10 -12
  44. package/mcp/dist/{governance-rbac.js → governance/rbac.js} +3 -3
  45. package/mcp/dist/{governance-scores.js → governance/scores.js} +8 -10
  46. package/mcp/dist/hooks.js +39 -31
  47. package/mcp/dist/index-query.js +4 -1
  48. package/mcp/dist/index.js +53 -29
  49. package/mcp/dist/{init-config.js → init/config.js} +6 -6
  50. package/mcp/dist/{init.js → init/init.js} +28 -29
  51. package/mcp/dist/{init-preferences.js → init/preferences.js} +3 -3
  52. package/mcp/dist/{init-setup.js → init/setup.js} +17 -19
  53. package/mcp/dist/{init-shared.js → init/shared.js} +3 -3
  54. package/mcp/dist/init-bootstrap.js +68 -0
  55. package/mcp/dist/init-detect.js +38 -0
  56. package/mcp/dist/init-dryrun.js +55 -0
  57. package/mcp/dist/init-env.js +114 -0
  58. package/mcp/dist/init-fresh.js +239 -0
  59. package/mcp/dist/init-hooks.js +26 -0
  60. package/mcp/dist/init-mcp.js +65 -0
  61. package/mcp/dist/init-migrate.js +51 -0
  62. package/mcp/dist/init-modes.js +135 -0
  63. package/mcp/dist/init-npm.js +37 -0
  64. package/mcp/dist/init-project-local.js +99 -0
  65. package/mcp/dist/init-semantic.js +48 -0
  66. package/mcp/dist/init-types.js +1 -0
  67. package/mcp/dist/init-uninstall.js +482 -0
  68. package/mcp/dist/init-update.js +96 -0
  69. package/mcp/dist/init-walkthrough-merge.js +90 -0
  70. package/mcp/dist/init-walkthrough.js +529 -0
  71. package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
  72. package/mcp/dist/{link-context.js → link/context.js} +4 -4
  73. package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
  74. package/mcp/dist/{link.js → link/link.js} +26 -31
  75. package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
  76. package/mcp/dist/logger.js +11 -3
  77. package/mcp/dist/phren-art.js +0 -6
  78. package/mcp/dist/phren-paths.js +30 -12
  79. package/mcp/dist/proactivity.js +2 -2
  80. package/mcp/dist/profile-store.js +5 -6
  81. package/mcp/dist/project-config.js +2 -2
  82. package/mcp/dist/project-topics.js +1 -1
  83. package/mcp/dist/query-correlation.js +1 -1
  84. package/mcp/dist/{session-checkpoints.js → session/checkpoints.js} +3 -3
  85. package/mcp/dist/{session-utils.js → session/utils.js} +1 -1
  86. package/mcp/dist/{shared-content.js → shared/content.js} +7 -7
  87. package/mcp/dist/{shared-data-utils.js → shared/data-utils.js} +3 -3
  88. package/mcp/dist/{shared-embedding-cache.js → shared/embedding-cache.js} +3 -3
  89. package/mcp/dist/{shared-fragment-graph.js → shared/fragment-graph.js} +15 -24
  90. package/mcp/dist/shared/governance.js +4 -0
  91. package/mcp/dist/{shared-index.js → shared/index.js} +92 -123
  92. package/mcp/dist/{shared-ollama.js → shared/ollama.js} +2 -2
  93. package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +16 -21
  94. package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +17 -20
  95. package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
  96. package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
  97. package/mcp/dist/shared.js +4 -59
  98. package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
  99. package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
  100. package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
  101. package/mcp/dist/{shell-render.js → shell/render.js} +1 -1
  102. package/mcp/dist/{shell.js → shell/shell.js} +11 -11
  103. package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
  104. package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
  105. package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
  106. package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
  107. package/mcp/dist/{skill-registry.js → skill/registry.js} +4 -4
  108. package/mcp/dist/{skill-state.js → skill/state.js} +1 -1
  109. package/mcp/dist/startup-embedding.js +2 -2
  110. package/mcp/dist/status.js +15 -14
  111. package/mcp/dist/{tasks-github.js → task/github.js} +2 -2
  112. package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
  113. package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +7 -7
  114. package/mcp/dist/telemetry.js +3 -4
  115. package/mcp/dist/tool-registry.js +29 -17
  116. package/mcp/dist/tools/config.js +515 -0
  117. package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
  118. package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
  119. package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
  120. package/mcp/dist/{mcp-finding.js → tools/finding.js} +97 -124
  121. package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
  122. package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
  123. package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
  124. package/mcp/dist/{mcp-ops.js → tools/ops.js} +169 -71
  125. package/mcp/dist/{mcp-search.js → tools/search.js} +19 -23
  126. package/mcp/dist/{mcp-session.js → tools/session.js} +48 -23
  127. package/mcp/dist/{mcp-skills.js → tools/skills.js} +33 -35
  128. package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
  129. package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
  130. package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
  131. package/mcp/dist/{memory-ui-page.js → ui/page.js} +4 -6
  132. package/mcp/dist/{memory-ui-server.js → ui/server.js} +30 -22
  133. package/mcp/dist/update.js +2 -2
  134. package/mcp/dist/utils.js +51 -11
  135. package/package.json +17 -11
  136. package/scripts/preuninstall.mjs +139 -0
  137. package/starter/global/CLAUDE.md +3 -2
  138. package/mcp/dist/mcp-config.js +0 -551
  139. package/mcp/dist/shared-governance.js +0 -4
  140. package/starter/global/skills/pipeline.md +0 -35
  141. package/starter/global/skills/release.md +0 -35
  142. /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
  143. /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
  144. /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
  145. /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
  146. /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
  147. /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
  148. /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
  149. /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 "./shared.js";
10
- import { safeProjectPath, isFeatureEnabled, errorMessage } from "./utils.js";
11
- import { callLlm } from "./content-dedup.js";
12
- import { withFileLock } from "./shared-governance.js";
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
- if ((process.env.PHREN_DEBUG))
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 "./mcp-types.js";
1
+ import { mcpResponse } from "./types.js";
2
2
  import { z } from "zod";
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";
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 "./mcp-types.js";
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 { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
6
- import { removeFinding as removeFindingCore, removeFindings as removeFindingsCore, } from "./core-finding.js";
7
- import { debugLog, EXEC_TIMEOUT_MS, FINDING_TYPES, normalizeMemoryScope, RESERVED_PROJECT_DIR_NAMES, } from "./shared.js";
8
- import { addFindingToFile, addFindingsToFile, checkSemanticConflicts, autoMergeConflicts, } from "./shared-content.js";
9
- import { jaccardTokenize, jaccardSimilarity, stripMetadata } from "./content-dedup.js";
10
- import { runCustomHooks } from "./hooks.js";
11
- import { incrementSessionFindings } from "./mcp-session.js";
12
- import { extractFragmentNames } from "./shared-fragment-graph.js";
13
- import { extractFactFromFinding } from "./mcp-extract-facts.js";
14
- import { appendChildFinding, editFinding as editFindingCore, readFindings } from "./data-access.js";
15
- import { getActiveTaskForSession } from "./task-lifecycle.js";
16
- import { FINDING_PROVENANCE_SOURCES } from "./content-citation.js";
17
- import { isInactiveFindingLine, supersedeFinding, retractFinding as retractFindingLifecycle, resolveFindingContradiction, } from "./finding-lifecycle.js";
18
- import { permissionDeniedError } from "./governance-rbac.js";
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 a single insight for a project's FINDINGS.md. Call this the moment you discover " +
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.string().describe("The insight, written as a single bullet point. Be specific enough that someone could act on it without extra context."),
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 a finding from a project's FINDINGS.md by matching text. Use this when a " +
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.string().describe("Partial text to match against existing findings."),
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
- return withWriteQueue(async () => {
441
- const result = removeFindingCore(phrenPath, project, finding);
442
- if (result.ok) {
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
- if (!result.ok)
448
- return mcpResponse({ ok: false, error: result.message });
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 = removeFindingsCore(phrenPath, project, findings);
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
- return mcpResponse({ ok: result.ok, message: result.message, data: result.data });
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
- if ((process.env.PHREN_DEBUG))
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
- if ((process.env.PHREN_DEBUG))
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
- if ((process.env.PHREN_DEBUG))
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
- if ((process.env.PHREN_DEBUG))
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
- if ((process.env.PHREN_DEBUG))
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 "./mcp-types.js";
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 "./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";
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
- if (process.env.PHREN_DEBUG)
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
- if (process.env.PHREN_DEBUG)
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
- if (process.env.PHREN_DEBUG)
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
- if (process.env.PHREN_DEBUG)
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
- process.stderr.write(`[phren] link_findings manualLinksRead: manual-links.json is malformed — aborting to avoid data loss: ${errorMessage(err)}\n`);
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 "./mcp-types.js";
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 "./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";
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 "./mcp-types.js";
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 "./shared.js";
6
- import { recordFeedback, flushEntryScores, } from "./shared-governance.js";
7
- import { upsertCanonical } from "./shared-content.js";
8
- import { isValidProjectName } from "./utils.js";
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", {