@phren/cli 0.0.28 → 0.0.33

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 (153) 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} +25 -21
  7. package/mcp/dist/{cli.js → cli/cli.js} +13 -13
  8. package/mcp/dist/{cli-config.js → cli/config.js} +12 -12
  9. package/mcp/dist/{cli-extract.js → cli/extract.js} +8 -8
  10. package/mcp/dist/{cli-govern.js → cli/govern.js} +28 -17
  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} +58 -117
  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} +12 -11
  21. package/mcp/dist/cli-hooks-git.js +243 -0
  22. package/mcp/dist/cli-hooks-prompt.js +323 -0
  23. package/mcp/dist/cli-hooks-session-handlers.js +337 -0
  24. package/mcp/dist/cli-hooks-stop.js +519 -0
  25. package/mcp/dist/{content-archive.js → content/archive.js} +16 -29
  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} +41 -20
  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} +142 -15
  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 +11 -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} +13 -7
  41. package/mcp/dist/governance/audit.js +30 -0
  42. package/mcp/dist/{governance-locks.js → governance/locks.js} +14 -9
  43. package/mcp/dist/{governance-policy.js → governance/policy.js} +23 -12
  44. package/mcp/dist/{governance-rbac.js → governance/rbac.js} +4 -4
  45. package/mcp/dist/{governance-scores.js → governance/scores.js} +10 -11
  46. package/mcp/dist/hooks.js +53 -37
  47. package/mcp/dist/index-query.js +4 -1
  48. package/mcp/dist/index.js +54 -30
  49. package/mcp/dist/{init-config.js → init/config.js} +6 -6
  50. package/mcp/dist/{init.js → init/init.js} +80 -69
  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} +4 -4
  54. package/mcp/dist/init-bootstrap.js +21 -0
  55. package/mcp/dist/init-detect.js +38 -0
  56. package/mcp/dist/init-env.js +114 -0
  57. package/mcp/dist/init-fresh.js +234 -0
  58. package/mcp/dist/init-hooks.js +26 -0
  59. package/mcp/dist/init-mcp.js +65 -0
  60. package/mcp/dist/init-modes.js +135 -0
  61. package/mcp/dist/init-npm.js +37 -0
  62. package/mcp/dist/init-project-local.js +99 -0
  63. package/mcp/dist/init-semantic.js +48 -0
  64. package/mcp/dist/init-types.js +1 -0
  65. package/mcp/dist/init-uninstall.js +504 -0
  66. package/mcp/dist/init-update.js +96 -0
  67. package/mcp/dist/init-walkthrough.js +524 -0
  68. package/mcp/dist/{link-checksums.js → link/checksums.js} +5 -5
  69. package/mcp/dist/{link-context.js → link/context.js} +4 -4
  70. package/mcp/dist/{link-doctor.js → link/doctor.js} +20 -22
  71. package/mcp/dist/{link.js → link/link.js} +26 -31
  72. package/mcp/dist/{link-skills.js → link/skills.js} +10 -10
  73. package/mcp/dist/logger.js +11 -3
  74. package/mcp/dist/package-metadata.js +1 -1
  75. package/mcp/dist/phren-art.js +4 -126
  76. package/mcp/dist/phren-paths.js +30 -12
  77. package/mcp/dist/proactivity.js +3 -3
  78. package/mcp/dist/profile-store.js +5 -6
  79. package/mcp/dist/project-config.js +2 -2
  80. package/mcp/dist/project-topics.js +17 -47
  81. package/mcp/dist/provider-adapters.js +1 -1
  82. package/mcp/dist/query-correlation.js +1 -1
  83. package/mcp/dist/runtime-profile.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} +28 -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} +19 -42
  90. package/mcp/dist/shared/governance.js +4 -0
  91. package/mcp/dist/{shared-index.js → shared/index.js} +105 -132
  92. package/mcp/dist/{shared-ollama.js → shared/ollama.js} +25 -7
  93. package/mcp/dist/shared/process.js +24 -0
  94. package/mcp/dist/{shared-retrieval.js → shared/retrieval.js} +22 -24
  95. package/mcp/dist/{shared-search-fallback.js → shared/search-fallback.js} +18 -20
  96. package/mcp/dist/{shared-sqljs.js → shared/sqljs.js} +3 -3
  97. package/mcp/dist/{shared-vector-index.js → shared/vector-index.js} +3 -3
  98. package/mcp/dist/shared.js +6 -60
  99. package/mcp/dist/{shell-entry.js → shell/entry.js} +6 -6
  100. package/mcp/dist/{shell-input.js → shell/input.js} +13 -13
  101. package/mcp/dist/{shell-palette.js → shell/palette.js} +3 -3
  102. package/mcp/dist/{shell-render.js → shell/render.js} +2 -2
  103. package/mcp/dist/{shell.js → shell/shell.js} +11 -11
  104. package/mcp/dist/{shell-state-store.js → shell/state-store.js} +5 -5
  105. package/mcp/dist/{shell-view-list.js → shell/view-list.js} +1 -1
  106. package/mcp/dist/{shell-view.js → shell/view.js} +13 -13
  107. package/mcp/dist/{skill-files.js → skill/files.js} +9 -9
  108. package/mcp/dist/{skill-registry.js → skill/registry.js} +5 -5
  109. package/mcp/dist/{skill-state.js → skill/state.js} +1 -4
  110. package/mcp/dist/startup-embedding.js +2 -2
  111. package/mcp/dist/status.js +15 -14
  112. package/mcp/dist/{tasks-github.js → task/github.js} +3 -2
  113. package/mcp/dist/{task-hygiene.js → task/hygiene.js} +4 -4
  114. package/mcp/dist/{task-lifecycle.js → task/lifecycle.js} +8 -13
  115. package/mcp/dist/telemetry.js +3 -4
  116. package/mcp/dist/tool-registry.js +29 -17
  117. package/mcp/dist/tools/config.js +530 -0
  118. package/mcp/dist/{mcp-data.js → tools/data.js} +8 -10
  119. package/mcp/dist/{mcp-extract-facts.js → tools/extract-facts.js} +6 -6
  120. package/mcp/dist/{mcp-extract.js → tools/extract.js} +6 -6
  121. package/mcp/dist/tools/finding.js +584 -0
  122. package/mcp/dist/{mcp-graph.js → tools/graph.js} +11 -14
  123. package/mcp/dist/{mcp-hooks.js → tools/hooks.js} +6 -6
  124. package/mcp/dist/{mcp-memory.js → tools/memory.js} +5 -5
  125. package/mcp/dist/tools/ops.js +468 -0
  126. package/mcp/dist/tools/search.js +672 -0
  127. package/mcp/dist/{mcp-session.js → tools/session.js} +51 -25
  128. package/mcp/dist/{mcp-skills.js → tools/skills.js} +42 -35
  129. package/mcp/dist/{mcp-tasks.js → tools/tasks.js} +155 -282
  130. package/mcp/dist/{memory-ui-data.js → ui/data.js} +31 -17
  131. package/mcp/dist/{memory-ui.js → ui/memory-ui.js} +3 -3
  132. package/mcp/dist/{memory-ui-page.js → ui/page.js} +5 -7
  133. package/mcp/dist/ui/server.js +1024 -0
  134. package/mcp/dist/update.js +2 -2
  135. package/mcp/dist/utils.js +63 -19
  136. package/package.json +2 -2
  137. package/scripts/preuninstall.mjs +31 -0
  138. package/starter/global/CLAUDE.md +3 -2
  139. package/mcp/dist/governance-audit.js +0 -22
  140. package/mcp/dist/mcp-config.js +0 -551
  141. package/mcp/dist/mcp-finding.js +0 -594
  142. package/mcp/dist/mcp-ops.js +0 -363
  143. package/mcp/dist/mcp-search.js +0 -668
  144. package/mcp/dist/memory-ui-server.js +0 -1411
  145. package/mcp/dist/shared-governance.js +0 -4
  146. /package/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
  147. /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
  148. /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
  149. /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
  150. /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
  151. /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
  152. /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
  153. /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
@@ -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", {
@@ -0,0 +1,468 @@
1
+ import { mcpResponse } from "./types.js";
2
+ import { z } from "zod";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import { runtimeFile, getProjectDirs } from "../shared.js";
6
+ import { findFtsCacheForPath } from "../shared/index.js";
7
+ import { isValidProjectName, errorMessage } from "../utils.js";
8
+ import { readReviewQueue, readReviewQueueAcrossProjects, approveQueueItem, rejectQueueItem, editQueueItem } from "../data/access.js";
9
+ import { addProjectFromPath } from "../core/project.js";
10
+ import { PROJECT_OWNERSHIP_MODES, parseProjectOwnershipMode } from "../project-config.js";
11
+ import { resolveRuntimeProfile } from "../runtime-profile.js";
12
+ import { getMachineName } from "../machine-identity.js";
13
+ import { getProjectConsolidationStatus, CONSOLIDATION_ENTRY_THRESHOLD } from "../content/validate.js";
14
+ import { logger } from "../logger.js";
15
+ import { getRuntimeHealth } from "../governance/policy.js";
16
+ import { countUnsyncedCommits } from "../cli-hooks-git.js";
17
+ // ── Handlers ─────────────────────────────────────────────────────────────────
18
+ async function handleAddProject(ctx, { path: targetPath, profile: requestedProfile, ownership }) {
19
+ const { phrenPath, profile, withWriteQueue } = ctx;
20
+ return withWriteQueue(async () => {
21
+ try {
22
+ const added = addProjectFromPath(phrenPath, targetPath, requestedProfile || profile || undefined, parseProjectOwnershipMode(ownership) ?? undefined);
23
+ if (!added.ok) {
24
+ return mcpResponse({
25
+ ok: false,
26
+ error: added.error,
27
+ });
28
+ }
29
+ await ctx.rebuildIndex();
30
+ return mcpResponse({
31
+ ok: true,
32
+ message: `Added project "${added.data.project}" (${added.data.ownership}) from ${added.data.path}.`,
33
+ data: added.data,
34
+ });
35
+ }
36
+ catch (err) {
37
+ return mcpResponse({
38
+ ok: false,
39
+ error: errorMessage(err),
40
+ });
41
+ }
42
+ });
43
+ }
44
+ async function handleHealthCheck(ctx, { include_consolidation }) {
45
+ const { phrenPath, profile } = ctx;
46
+ const activeProfile = (() => {
47
+ try {
48
+ return resolveRuntimeProfile(phrenPath);
49
+ }
50
+ catch {
51
+ return profile || "";
52
+ }
53
+ })();
54
+ // Version
55
+ let version = "unknown";
56
+ try {
57
+ const pkgPath = path.join(path.dirname(new URL(import.meta.url).pathname), "..", "..", "package.json");
58
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
59
+ version = pkg.version || "unknown";
60
+ }
61
+ catch (err) {
62
+ logger.debug("healthCheck version", errorMessage(err));
63
+ }
64
+ // FTS index (lives in /tmpphren-fts-*/, not .runtime/)
65
+ let indexStatus = { exists: false };
66
+ try {
67
+ indexStatus = findFtsCacheForPath(phrenPath, activeProfile);
68
+ }
69
+ catch (err) {
70
+ logger.debug("healthCheck ftsCacheCheck", errorMessage(err));
71
+ }
72
+ // Hook registration
73
+ let hooksEnabled = false;
74
+ try {
75
+ const { getHooksEnabledPreference } = await import("../init/preferences.js");
76
+ hooksEnabled = getHooksEnabledPreference(phrenPath);
77
+ }
78
+ catch (err) {
79
+ logger.debug("healthCheck hooksEnabled", errorMessage(err));
80
+ }
81
+ let mcpEnabled = false;
82
+ try {
83
+ const { getMcpEnabledPreference } = await import("../init/preferences.js");
84
+ mcpEnabled = getMcpEnabledPreference(phrenPath);
85
+ }
86
+ catch (err) {
87
+ logger.debug("healthCheck mcpEnabled", errorMessage(err));
88
+ }
89
+ // Profile/machine info
90
+ const machineName = (() => {
91
+ try {
92
+ return getMachineName();
93
+ }
94
+ catch (err) {
95
+ logger.debug("healthCheck machineName", errorMessage(err));
96
+ }
97
+ return undefined;
98
+ })();
99
+ const projectCount = getProjectDirs(phrenPath, activeProfile).length;
100
+ // Proactivity and taskMode
101
+ let proactivity = "high";
102
+ let taskMode = "auto";
103
+ try {
104
+ const { getWorkflowPolicy } = await import("../governance/policy.js");
105
+ const workflowPolicy = getWorkflowPolicy(phrenPath);
106
+ taskMode = workflowPolicy.taskMode;
107
+ }
108
+ catch (err) {
109
+ logger.debug("healthCheck taskMode", errorMessage(err));
110
+ }
111
+ let syncIntent;
112
+ try {
113
+ const { readInstallPreferences } = await import("../init/preferences.js");
114
+ const prefs = readInstallPreferences(phrenPath);
115
+ proactivity = prefs.proactivity || "high";
116
+ syncIntent = prefs.syncIntent;
117
+ }
118
+ catch (err) {
119
+ logger.debug("healthCheck proactivity", errorMessage(err));
120
+ }
121
+ // Determine sync status from intent + git remote state
122
+ let syncStatus = "local-only";
123
+ let syncDetail = "no git remote configured";
124
+ try {
125
+ const { execFileSync } = await import("child_process");
126
+ const remote = execFileSync("git", ["-C", phrenPath, "remote", "get-url", "origin"], {
127
+ encoding: "utf8",
128
+ stdio: ["ignore", "pipe", "ignore"],
129
+ timeout: 5_000,
130
+ }).trim();
131
+ if (remote) {
132
+ try {
133
+ execFileSync("git", ["-C", phrenPath, "ls-remote", "--exit-code", "origin"], {
134
+ stdio: ["ignore", "ignore", "ignore"],
135
+ timeout: 10_000,
136
+ });
137
+ syncStatus = "synced";
138
+ syncDetail = `origin=${remote}`;
139
+ }
140
+ catch {
141
+ syncStatus = syncIntent === "sync" ? "broken" : "local-only";
142
+ syncDetail = `origin=${remote} (unreachable)`;
143
+ }
144
+ }
145
+ else if (syncIntent === "sync") {
146
+ syncStatus = "broken";
147
+ syncDetail = "sync was configured but no remote found";
148
+ }
149
+ }
150
+ catch {
151
+ if (syncIntent === "sync") {
152
+ syncStatus = "broken";
153
+ syncDetail = "sync was configured but no remote found";
154
+ }
155
+ }
156
+ let consolidation = null;
157
+ if (include_consolidation !== false) {
158
+ try {
159
+ const projectDirsForConsol = getProjectDirs(phrenPath, activeProfile);
160
+ const consolResults = [];
161
+ for (const dir of projectDirsForConsol) {
162
+ const status = getProjectConsolidationStatus(dir);
163
+ if (!status)
164
+ continue;
165
+ consolResults.push({ ...status, threshold: CONSOLIDATION_ENTRY_THRESHOLD });
166
+ }
167
+ consolidation = consolResults;
168
+ }
169
+ catch (err) {
170
+ logger.debug("healthCheck consolidation", errorMessage(err));
171
+ consolidation = null;
172
+ }
173
+ }
174
+ const consolSummary = consolidation && consolidation.length > 0
175
+ ? consolidation.filter(r => r.recommended).length > 0
176
+ ? `Consolidation: ${consolidation.filter(r => r.recommended).length} project(s) need consolidation`
177
+ : `Consolidation: all projects OK`
178
+ : null;
179
+ // ── Surface RuntimeHealth warnings ────────────────────────────────────
180
+ const warnings = [];
181
+ try {
182
+ const health = getRuntimeHealth(phrenPath);
183
+ // Unsynced commits
184
+ const unsynced = health.lastSync?.unsyncedCommits;
185
+ if (typeof unsynced === "number" && unsynced > 0) {
186
+ warnings.push(`Unsynced commits: ${unsynced} (last push: ${health.lastSync?.lastPushStatus ?? "unknown"})`);
187
+ }
188
+ // Last auto-save error
189
+ if (health.lastAutoSave?.status === "error") {
190
+ warnings.push(`Last auto-save failed: ${health.lastAutoSave.detail ?? "unknown error"}`);
191
+ }
192
+ // Last push error
193
+ if (health.lastSync?.lastPushStatus === "error") {
194
+ warnings.push(`Last push failed: ${health.lastSync.lastPushDetail ?? "unknown error"}`);
195
+ }
196
+ // Check live unsynced commit count (may differ from cached value)
197
+ if (syncStatus === "synced" && (!unsynced || unsynced === 0)) {
198
+ try {
199
+ const liveUnsynced = await countUnsyncedCommits(phrenPath);
200
+ if (liveUnsynced > 0) {
201
+ warnings.push(`Unsynced commits: ${liveUnsynced} (not yet pushed to remote)`);
202
+ }
203
+ }
204
+ catch (err) {
205
+ logger.debug("healthCheck liveUnsyncedCount", errorMessage(err));
206
+ }
207
+ }
208
+ }
209
+ catch (err) {
210
+ logger.debug("healthCheck runtimeHealth", errorMessage(err));
211
+ }
212
+ // Check recent sync warnings from background sync
213
+ try {
214
+ const syncWarningsPath = runtimeFile(phrenPath, "sync-warnings.jsonl");
215
+ if (fs.existsSync(syncWarningsPath)) {
216
+ const lines = fs.readFileSync(syncWarningsPath, "utf8").trim().split("\n").filter(Boolean);
217
+ const recent = lines.slice(-3); // last 3 warnings
218
+ for (const line of recent) {
219
+ try {
220
+ const entry = JSON.parse(line);
221
+ if (entry.error) {
222
+ warnings.push(`Background sync failed (${entry.at?.slice(0, 16) ?? "unknown"}): ${entry.error}`);
223
+ }
224
+ }
225
+ catch { /* skip malformed lines */ }
226
+ }
227
+ }
228
+ }
229
+ catch (err) {
230
+ logger.debug("healthCheck syncWarnings", errorMessage(err));
231
+ }
232
+ // Check embedding/LLM availability
233
+ try {
234
+ const { getOllamaUrl } = await import("../shared/ollama.js");
235
+ const ollamaUrl = getOllamaUrl();
236
+ const hasEmbeddingApi = !!process.env.PHREN_EMBEDDING_API_URL;
237
+ if (!ollamaUrl && !hasEmbeddingApi) {
238
+ warnings.push("Embeddings: unavailable (no Ollama or API endpoint configured)");
239
+ }
240
+ const hasLlmEndpoint = !!process.env.PHREN_LLM_ENDPOINT;
241
+ const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
242
+ const hasOpenAiKey = !!process.env.OPENAI_API_KEY;
243
+ if (!hasLlmEndpoint && !hasAnthropicKey && !hasOpenAiKey) {
244
+ warnings.push("LLM features: unavailable (no API key configured for semantic dedup/conflict detection)");
245
+ }
246
+ }
247
+ catch (err) {
248
+ logger.debug("healthCheck serviceAvailability", errorMessage(err));
249
+ }
250
+ const warningsSummary = warnings.length > 0
251
+ ? `Warnings: ${warnings.length}\n ${warnings.join("\n ")}`
252
+ : null;
253
+ const lines = [
254
+ `Phren v${version}`,
255
+ `Profile: ${activeProfile || "(default)"}`,
256
+ machineName ? `Machine: ${machineName}` : null,
257
+ `Projects: ${projectCount}`,
258
+ `FTS index: ${indexStatus.exists ? `ok (${Math.round((indexStatus.sizeBytes ?? 0) / 1024)} KB)` : "missing"}`,
259
+ `MCP: ${mcpEnabled ? "enabled" : "disabled"}`,
260
+ `Hooks: ${hooksEnabled ? "enabled" : "disabled"}`,
261
+ `Proactivity: ${proactivity}`,
262
+ `Task mode: ${taskMode}`,
263
+ `Sync: ${syncStatus}${syncStatus !== "synced" ? ` (${syncDetail})` : ""}`,
264
+ consolSummary,
265
+ warningsSummary,
266
+ `Path: ${phrenPath}`,
267
+ ].filter(Boolean);
268
+ return mcpResponse({
269
+ ok: true,
270
+ message: lines.join("\n"),
271
+ data: {
272
+ version,
273
+ profile: activeProfile || "(default)",
274
+ machine: machineName ?? null,
275
+ projectCount,
276
+ index: indexStatus,
277
+ mcpEnabled,
278
+ hooksEnabled,
279
+ proactivity,
280
+ taskMode,
281
+ syncStatus,
282
+ syncDetail,
283
+ consolidation,
284
+ warnings,
285
+ phrenPath,
286
+ },
287
+ });
288
+ }
289
+ async function handleDoctorFix(_ctx, { check_data }) {
290
+ const { phrenPath } = _ctx;
291
+ const { runDoctor } = await import("../link/doctor.js");
292
+ const result = await runDoctor(phrenPath, true, check_data ?? false);
293
+ const lines = result.checks.map((c) => `${c.ok ? "ok" : "FAIL"} ${c.name}: ${c.detail}`);
294
+ const failCount = result.checks.filter((c) => !c.ok).length;
295
+ return mcpResponse({
296
+ ok: result.ok,
297
+ ...(result.ok ? {} : { error: `${failCount} check(s) could not be auto-fixed: ${lines.filter((l) => l.startsWith("FAIL")).join("; ")}` }),
298
+ message: result.ok
299
+ ? `Doctor fix complete: all ${result.checks.length} checks passed`
300
+ : `Doctor fix complete: ${failCount} issue(s) remain`,
301
+ data: {
302
+ machine: result.machine,
303
+ profile: result.profile,
304
+ checks: result.checks,
305
+ summary: lines.join("\n"),
306
+ },
307
+ });
308
+ }
309
+ async function handleListHookErrors(ctx, { limit }) {
310
+ const { phrenPath } = ctx;
311
+ const maxEntries = limit ?? 20;
312
+ const ERROR_PATTERNS = [
313
+ /\berror\b/i,
314
+ /\bfail(ed|ure|s)?\b/i,
315
+ /\bcrash(ed)?\b/i,
316
+ /\btimeout\b/i,
317
+ /\bEXCEPTION\b/i,
318
+ /\bEACCES\b/,
319
+ /\bENOENT\b/,
320
+ /\bEPERM\b/,
321
+ /\bENOSPC\b/,
322
+ ];
323
+ function readErrorLines(filePath, filterPatterns) {
324
+ try {
325
+ if (!fs.existsSync(filePath))
326
+ return [];
327
+ const content = fs.readFileSync(filePath, "utf8");
328
+ const lines = content.split("\n").filter(l => l.trim());
329
+ if (!filterPatterns)
330
+ return lines; // hook-errors.log: every line is an error
331
+ return lines.filter(line => ERROR_PATTERNS.some(p => p.test(line)));
332
+ }
333
+ catch (err) {
334
+ logger.debug("readErrorLines", errorMessage(err));
335
+ return [];
336
+ }
337
+ }
338
+ // hook-errors.log contains only hook failure lines (no filtering needed)
339
+ const hookErrors = readErrorLines(runtimeFile(phrenPath, "hook-errors.log"), false);
340
+ // debug.log may contain non-error lines, so filter
341
+ const debugErrors = readErrorLines(runtimeFile(phrenPath, "debug.log"), true);
342
+ const allErrors = [...hookErrors, ...debugErrors];
343
+ if (allErrors.length === 0) {
344
+ return mcpResponse({
345
+ ok: true,
346
+ message: "No error entries found. Hook errors go to hook-errors.log; general errors require PHREN_DEBUG=1.",
347
+ data: { errors: [], total: 0 },
348
+ });
349
+ }
350
+ const recent = allErrors.slice(-maxEntries);
351
+ return mcpResponse({
352
+ ok: true,
353
+ message: `Found ${allErrors.length} error(s), showing last ${recent.length}:\n\n${recent.join("\n")}`,
354
+ data: { errors: recent, total: allErrors.length, sources: { hookErrors: hookErrors.length, debugErrors: debugErrors.length } },
355
+ });
356
+ }
357
+ async function handleGetReviewQueue(ctx, { project }) {
358
+ const { phrenPath, profile } = ctx;
359
+ if (project && !isValidProjectName(project)) {
360
+ return mcpResponse({ ok: false, error: `Invalid project name: "${project}".` });
361
+ }
362
+ if (project) {
363
+ const result = readReviewQueue(phrenPath, project);
364
+ if (!result.ok) {
365
+ return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
366
+ }
367
+ const items = result.data.map((item) => ({ ...item, project }));
368
+ return mcpResponse({
369
+ ok: true,
370
+ message: `${items.length} queue item(s) for "${project}".`,
371
+ data: { items },
372
+ });
373
+ }
374
+ const result = readReviewQueueAcrossProjects(phrenPath, profile);
375
+ if (!result.ok) {
376
+ return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
377
+ }
378
+ return mcpResponse({
379
+ ok: true,
380
+ message: `${result.data.length} queue item(s) across all projects.`,
381
+ data: { items: result.data },
382
+ });
383
+ }
384
+ async function handleManageReviewItem(ctx, { project, line, action, new_text }) {
385
+ const { phrenPath, withWriteQueue } = ctx;
386
+ if (!isValidProjectName(project)) {
387
+ return mcpResponse({ ok: false, error: `Invalid project name: "${project}".` });
388
+ }
389
+ if (action === "edit" && !new_text) {
390
+ return mcpResponse({ ok: false, error: "new_text is required when action is 'edit'." });
391
+ }
392
+ return withWriteQueue(async () => {
393
+ let result;
394
+ switch (action) {
395
+ case "approve":
396
+ result = approveQueueItem(phrenPath, project, line);
397
+ break;
398
+ case "reject":
399
+ result = rejectQueueItem(phrenPath, project, line);
400
+ break;
401
+ case "edit":
402
+ result = editQueueItem(phrenPath, project, line, new_text);
403
+ break;
404
+ }
405
+ if (!result.ok) {
406
+ return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
407
+ }
408
+ return mcpResponse({ ok: true, message: result.data });
409
+ });
410
+ }
411
+ // ── Registration ─────────────────────────────────────────────────────────────
412
+ export function register(server, ctx) {
413
+ server.registerTool("add_project", {
414
+ title: "◆ phren · add project",
415
+ description: "Bootstrap a project into phren from a repo or working directory. " +
416
+ "Copies or creates CLAUDE.md/summary/tasks/findings under ~/.phren/<project> and adds the project to the active profile.",
417
+ inputSchema: z.object({
418
+ path: z.string().describe("Project path to import. Pass the current repo path explicitly."),
419
+ profile: z.string().optional().describe("Profile to update. Defaults to the active profile."),
420
+ ownership: z.enum(PROJECT_OWNERSHIP_MODES).optional()
421
+ .describe("How Phren should treat repo-facing instruction files: phren-managed, detached, or repo-managed."),
422
+ }),
423
+ }, (params) => handleAddProject(ctx, params));
424
+ server.registerTool("health_check", {
425
+ title: "◆ phren · health",
426
+ description: "Return phren health status: version, FTS index status, hook registration, profile/machine info, and consolidation status for all projects.",
427
+ inputSchema: z.object({
428
+ include_consolidation: z.boolean().optional()
429
+ .describe("Include consolidation status for all projects (default true)."),
430
+ }),
431
+ }, (params) => handleHealthCheck(ctx, params));
432
+ server.registerTool("doctor_fix", {
433
+ title: "◆ phren · doctor fix",
434
+ description: "Run phren doctor with --fix: re-links hooks, symlinks, context, and memory pointers. " +
435
+ "Returns the list of checks and repair actions taken.",
436
+ inputSchema: z.object({
437
+ check_data: z.boolean().optional()
438
+ .describe("Also validate data files (tasks, findings, governance). Default false."),
439
+ }),
440
+ }, (params) => handleDoctorFix(ctx, params));
441
+ server.registerTool("list_hook_errors", {
442
+ title: "◆ phren · hook errors",
443
+ description: "List recent error entries from phren hook-errors.log and debug.log. " +
444
+ "Useful for diagnosing hook or index failures.",
445
+ inputSchema: z.object({
446
+ limit: z.number().int().min(1).max(200).optional()
447
+ .describe("Max error entries to return (default 20)."),
448
+ }),
449
+ }, (params) => handleListHookErrors(ctx, params));
450
+ server.registerTool("get_review_queue", {
451
+ title: "◆ phren · get review queue",
452
+ description: "List all items in a project's review queue (review.md), or across all projects when omitted. " +
453
+ "Returns items with their id, section (Review/Stale/Conflicts), date, text, confidence, and risky flag.",
454
+ inputSchema: z.object({
455
+ project: z.string().optional().describe("Project name. Omit to read the review queue across all projects in the active profile."),
456
+ }),
457
+ }, (params) => handleGetReviewQueue(ctx, params));
458
+ server.registerTool("manage_review_item", {
459
+ title: "◆ phren · manage review item",
460
+ description: "Manage a review queue item: approve (removes from queue, finding stays), reject (removes from queue AND FINDINGS.md), or edit (updates text in both).",
461
+ inputSchema: z.object({
462
+ project: z.string().describe("Project name."),
463
+ line: z.string().max(10000).describe("The raw queue line text (as returned by get_review_queue)."),
464
+ action: z.enum(["approve", "reject", "edit"]).describe("Action to perform on the queue item."),
465
+ new_text: z.string().max(10000).optional().describe("Required when action is 'edit'."),
466
+ }),
467
+ }, (params) => handleManageReviewItem(ctx, params));
468
+ }