@phren/cli 0.0.28 → 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 (147) 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 +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} +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 +2 -2
  136. package/scripts/preuninstall.mjs +31 -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/mcp/dist/{content-metadata.js → content/metadata.js} +0 -0
  141. /package/mcp/dist/{shared-stemmer.js → shared/stemmer.js} +0 -0
  142. /package/mcp/dist/{shell-types.js → shell/types.js} +0 -0
  143. /package/mcp/dist/{mcp-types.js → tools/types.js} +0 -0
  144. /package/mcp/dist/{memory-ui-assets.js → ui/assets.js} +0 -0
  145. /package/mcp/dist/{memory-ui-graph.js → ui/graph.js} +0 -0
  146. /package/mcp/dist/{memory-ui-scripts.js → ui/scripts.js} +0 -0
  147. /package/mcp/dist/{memory-ui-styles.js → ui/styles.js} +0 -0
@@ -1,16 +1,19 @@
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 { runtimeFile, getProjectDirs } from "./shared.js";
6
- import { findFtsCacheForPath } from "./shared-index.js";
7
- import { isValidProjectName, errorMessage } from "./utils.js";
8
- import { readReviewQueue, readReviewQueueAcrossProjects } 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";
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";
14
17
  export function register(server, ctx) {
15
18
  const { phrenPath, profile, withWriteQueue } = ctx;
16
19
  // ── add_project ────────────────────────────────────────────────────────────
@@ -49,49 +52,15 @@ export function register(server, ctx) {
49
52
  }
50
53
  });
51
54
  });
52
- server.registerTool("get_consolidation_status", {
53
- title: "◆ phren · consolidation status",
54
- description: "Check whether a project's FINDINGS.md needs consolidation. " +
55
- "Returns entry count since last consolidation, threshold, and recommendation.",
56
- inputSchema: z.object({
57
- project: z.string().optional().describe("Project name. If omitted, checks all projects."),
58
- }),
59
- }, async ({ project }) => {
60
- const projectDirs = project
61
- ? (() => {
62
- if (!isValidProjectName(project))
63
- return [];
64
- const dir = path.join(phrenPath, project);
65
- return fs.existsSync(dir) ? [dir] : [];
66
- })()
67
- : getProjectDirs(phrenPath, profile);
68
- if (project && projectDirs.length === 0) {
69
- return mcpResponse({ ok: false, error: `Project "${project}" not found.` });
70
- }
71
- const results = [];
72
- for (const dir of projectDirs) {
73
- const status = getProjectConsolidationStatus(dir);
74
- if (!status)
75
- continue;
76
- results.push({ ...status, threshold: CONSOLIDATION_ENTRY_THRESHOLD });
77
- }
78
- if (results.length === 0) {
79
- return mcpResponse({ ok: true, message: "No FINDINGS.md files found.", data: { results: [] } });
80
- }
81
- const lines = results.map(r => `${r.project}: ${r.entriesSince} entries since${r.lastConsolidated ? ` ${r.lastConsolidated}` : " (never consolidated)"}` +
82
- `${r.recommended ? " — consolidation recommended" : ""}`);
83
- return mcpResponse({
84
- ok: true,
85
- message: lines.join("\n"),
86
- data: { results },
87
- });
88
- });
89
55
  // ── health_check ───────────────────────────────────────────────────────────
90
56
  server.registerTool("health_check", {
91
57
  title: "◆ phren · health",
92
- description: "Return phren health status: version, FTS index status, hook registration, and profile/machine info.",
93
- inputSchema: z.object({}),
94
- }, async () => {
58
+ description: "Return phren health status: version, FTS index status, hook registration, profile/machine info, and consolidation status for all projects.",
59
+ inputSchema: z.object({
60
+ include_consolidation: z.boolean().optional()
61
+ .describe("Include consolidation status for all projects (default true)."),
62
+ }),
63
+ }, async ({ include_consolidation }) => {
95
64
  const activeProfile = (() => {
96
65
  try {
97
66
  return resolveRuntimeProfile(phrenPath);
@@ -108,8 +77,7 @@ export function register(server, ctx) {
108
77
  version = pkg.version || "unknown";
109
78
  }
110
79
  catch (err) {
111
- if ((process.env.PHREN_DEBUG))
112
- process.stderr.write(`[phren] healthCheck version: ${errorMessage(err)}\n`);
80
+ logger.debug("healthCheck version", errorMessage(err));
113
81
  }
114
82
  // FTS index (lives in /tmpphren-fts-*/, not .runtime/)
115
83
  let indexStatus = { exists: false };
@@ -117,27 +85,24 @@ export function register(server, ctx) {
117
85
  indexStatus = findFtsCacheForPath(phrenPath, activeProfile);
118
86
  }
119
87
  catch (err) {
120
- if ((process.env.PHREN_DEBUG))
121
- process.stderr.write(`[phren] healthCheck ftsCacheCheck: ${errorMessage(err)}\n`);
88
+ logger.debug("healthCheck ftsCacheCheck", errorMessage(err));
122
89
  }
123
90
  // Hook registration
124
91
  let hooksEnabled = false;
125
92
  try {
126
- const { getHooksEnabledPreference } = await import("./init-preferences.js");
93
+ const { getHooksEnabledPreference } = await import("../init/preferences.js");
127
94
  hooksEnabled = getHooksEnabledPreference(phrenPath);
128
95
  }
129
96
  catch (err) {
130
- if ((process.env.PHREN_DEBUG))
131
- process.stderr.write(`[phren] healthCheck hooksEnabled: ${errorMessage(err)}\n`);
97
+ logger.debug("healthCheck hooksEnabled", errorMessage(err));
132
98
  }
133
99
  let mcpEnabled = false;
134
100
  try {
135
- const { getMcpEnabledPreference } = await import("./init-preferences.js");
101
+ const { getMcpEnabledPreference } = await import("../init/preferences.js");
136
102
  mcpEnabled = getMcpEnabledPreference(phrenPath);
137
103
  }
138
104
  catch (err) {
139
- if ((process.env.PHREN_DEBUG))
140
- process.stderr.write(`[phren] healthCheck mcpEnabled: ${errorMessage(err)}\n`);
105
+ logger.debug("healthCheck mcpEnabled", errorMessage(err));
141
106
  }
142
107
  // Profile/machine info
143
108
  const machineName = (() => {
@@ -145,8 +110,7 @@ export function register(server, ctx) {
145
110
  return getMachineName();
146
111
  }
147
112
  catch (err) {
148
- if ((process.env.PHREN_DEBUG))
149
- process.stderr.write(`[phren] healthCheck machineName: ${errorMessage(err)}\n`);
113
+ logger.debug("healthCheck machineName", errorMessage(err));
150
114
  }
151
115
  return undefined;
152
116
  })();
@@ -155,24 +119,22 @@ export function register(server, ctx) {
155
119
  let proactivity = "high";
156
120
  let taskMode = "auto";
157
121
  try {
158
- const { getWorkflowPolicy } = await import("./governance-policy.js");
122
+ const { getWorkflowPolicy } = await import("../governance/policy.js");
159
123
  const workflowPolicy = getWorkflowPolicy(phrenPath);
160
124
  taskMode = workflowPolicy.taskMode;
161
125
  }
162
126
  catch (err) {
163
- if ((process.env.PHREN_DEBUG))
164
- process.stderr.write(`[phren] healthCheck taskMode: ${errorMessage(err)}\n`);
127
+ logger.debug("healthCheck taskMode", errorMessage(err));
165
128
  }
166
129
  let syncIntent;
167
130
  try {
168
- const { readInstallPreferences } = await import("./init-preferences.js");
131
+ const { readInstallPreferences } = await import("../init/preferences.js");
169
132
  const prefs = readInstallPreferences(phrenPath);
170
133
  proactivity = prefs.proactivity || "high";
171
134
  syncIntent = prefs.syncIntent;
172
135
  }
173
136
  catch (err) {
174
- if ((process.env.PHREN_DEBUG))
175
- process.stderr.write(`[phren] healthCheck proactivity: ${errorMessage(err)}\n`);
137
+ logger.debug("healthCheck proactivity", errorMessage(err));
176
138
  }
177
139
  // Determine sync status from intent + git remote state
178
140
  let syncStatus = "local-only";
@@ -209,6 +171,103 @@ export function register(server, ctx) {
209
171
  syncDetail = "sync was configured but no remote found";
210
172
  }
211
173
  }
174
+ let consolidation = null;
175
+ if (include_consolidation !== false) {
176
+ try {
177
+ const projectDirsForConsol = getProjectDirs(phrenPath, activeProfile);
178
+ const consolResults = [];
179
+ for (const dir of projectDirsForConsol) {
180
+ const status = getProjectConsolidationStatus(dir);
181
+ if (!status)
182
+ continue;
183
+ consolResults.push({ ...status, threshold: CONSOLIDATION_ENTRY_THRESHOLD });
184
+ }
185
+ consolidation = consolResults;
186
+ }
187
+ catch (err) {
188
+ logger.debug("healthCheck consolidation", errorMessage(err));
189
+ consolidation = null;
190
+ }
191
+ }
192
+ const consolSummary = consolidation && consolidation.length > 0
193
+ ? consolidation.filter(r => r.recommended).length > 0
194
+ ? `Consolidation: ${consolidation.filter(r => r.recommended).length} project(s) need consolidation`
195
+ : `Consolidation: all projects OK`
196
+ : null;
197
+ // ── Surface RuntimeHealth warnings ────────────────────────────────────
198
+ const warnings = [];
199
+ try {
200
+ const health = getRuntimeHealth(phrenPath);
201
+ // Unsynced commits
202
+ const unsynced = health.lastSync?.unsyncedCommits;
203
+ if (typeof unsynced === "number" && unsynced > 0) {
204
+ warnings.push(`Unsynced commits: ${unsynced} (last push: ${health.lastSync?.lastPushStatus ?? "unknown"})`);
205
+ }
206
+ // Last auto-save error
207
+ if (health.lastAutoSave?.status === "error") {
208
+ warnings.push(`Last auto-save failed: ${health.lastAutoSave.detail ?? "unknown error"}`);
209
+ }
210
+ // Last push error
211
+ if (health.lastSync?.lastPushStatus === "error") {
212
+ warnings.push(`Last push failed: ${health.lastSync.lastPushDetail ?? "unknown error"}`);
213
+ }
214
+ // Check live unsynced commit count (may differ from cached value)
215
+ if (syncStatus === "synced" && (!unsynced || unsynced === 0)) {
216
+ try {
217
+ const liveUnsynced = await countUnsyncedCommits(phrenPath);
218
+ if (liveUnsynced > 0) {
219
+ warnings.push(`Unsynced commits: ${liveUnsynced} (not yet pushed to remote)`);
220
+ }
221
+ }
222
+ catch (err) {
223
+ logger.debug("healthCheck liveUnsyncedCount", errorMessage(err));
224
+ }
225
+ }
226
+ }
227
+ catch (err) {
228
+ logger.debug("healthCheck runtimeHealth", errorMessage(err));
229
+ }
230
+ // Check recent sync warnings from background sync
231
+ try {
232
+ const syncWarningsPath = runtimeFile(phrenPath, "sync-warnings.jsonl");
233
+ if (fs.existsSync(syncWarningsPath)) {
234
+ const lines = fs.readFileSync(syncWarningsPath, "utf8").trim().split("\n").filter(Boolean);
235
+ const recent = lines.slice(-3); // last 3 warnings
236
+ for (const line of recent) {
237
+ try {
238
+ const entry = JSON.parse(line);
239
+ if (entry.error) {
240
+ warnings.push(`Background sync failed (${entry.at?.slice(0, 16) ?? "unknown"}): ${entry.error}`);
241
+ }
242
+ }
243
+ catch { /* skip malformed lines */ }
244
+ }
245
+ }
246
+ }
247
+ catch (err) {
248
+ logger.debug("healthCheck syncWarnings", errorMessage(err));
249
+ }
250
+ // Check embedding/LLM availability
251
+ try {
252
+ const { getOllamaUrl } = await import("../shared/ollama.js");
253
+ const ollamaUrl = getOllamaUrl();
254
+ const hasEmbeddingApi = !!process.env.PHREN_EMBEDDING_API_URL;
255
+ if (!ollamaUrl && !hasEmbeddingApi) {
256
+ warnings.push("Embeddings: unavailable (no Ollama or API endpoint configured)");
257
+ }
258
+ const hasLlmEndpoint = !!process.env.PHREN_LLM_ENDPOINT;
259
+ const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
260
+ const hasOpenAiKey = !!process.env.OPENAI_API_KEY;
261
+ if (!hasLlmEndpoint && !hasAnthropicKey && !hasOpenAiKey) {
262
+ warnings.push("LLM features: unavailable (no API key configured for semantic dedup/conflict detection)");
263
+ }
264
+ }
265
+ catch (err) {
266
+ logger.debug("healthCheck serviceAvailability", errorMessage(err));
267
+ }
268
+ const warningsSummary = warnings.length > 0
269
+ ? `Warnings: ${warnings.length}\n ${warnings.join("\n ")}`
270
+ : null;
212
271
  const lines = [
213
272
  `Phren v${version}`,
214
273
  `Profile: ${activeProfile || "(default)"}`,
@@ -220,6 +279,8 @@ export function register(server, ctx) {
220
279
  `Proactivity: ${proactivity}`,
221
280
  `Task mode: ${taskMode}`,
222
281
  `Sync: ${syncStatus}${syncStatus !== "synced" ? ` (${syncDetail})` : ""}`,
282
+ consolSummary,
283
+ warningsSummary,
223
284
  `Path: ${phrenPath}`,
224
285
  ].filter(Boolean);
225
286
  return mcpResponse({
@@ -237,6 +298,8 @@ export function register(server, ctx) {
237
298
  taskMode,
238
299
  syncStatus,
239
300
  syncDetail,
301
+ consolidation,
302
+ warnings,
240
303
  phrenPath,
241
304
  },
242
305
  });
@@ -251,7 +314,7 @@ export function register(server, ctx) {
251
314
  .describe("Also validate data files (tasks, findings, governance). Default false."),
252
315
  }),
253
316
  }, async ({ check_data }) => {
254
- const { runDoctor } = await import("./link-doctor.js");
317
+ const { runDoctor } = await import("../link/doctor.js");
255
318
  const result = await runDoctor(phrenPath, true, check_data ?? false);
256
319
  const lines = result.checks.map((c) => `${c.ok ? "ok" : "FAIL"} ${c.name}: ${c.detail}`);
257
320
  const failCount = result.checks.filter((c) => !c.ok).length;
@@ -302,8 +365,7 @@ export function register(server, ctx) {
302
365
  return lines.filter(line => ERROR_PATTERNS.some(p => p.test(line)));
303
366
  }
304
367
  catch (err) {
305
- if ((process.env.PHREN_DEBUG))
306
- process.stderr.write(`[phren] readErrorLines: ${errorMessage(err)}\n`);
368
+ logger.debug("readErrorLines", errorMessage(err));
307
369
  return [];
308
370
  }
309
371
  }
@@ -360,4 +422,40 @@ export function register(server, ctx) {
360
422
  data: { items: result.data },
361
423
  });
362
424
  });
425
+ // ── manage_review_item ──────────────────────────────────────────────────
426
+ server.registerTool("manage_review_item", {
427
+ title: "◆ phren · manage review item",
428
+ 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).",
429
+ inputSchema: z.object({
430
+ project: z.string().describe("Project name."),
431
+ line: z.string().max(10000).describe("The raw queue line text (as returned by get_review_queue)."),
432
+ action: z.enum(["approve", "reject", "edit"]).describe("Action to perform on the queue item."),
433
+ new_text: z.string().max(10000).optional().describe("Required when action is 'edit'."),
434
+ }),
435
+ }, async ({ project, line, action, new_text }) => {
436
+ if (!isValidProjectName(project)) {
437
+ return mcpResponse({ ok: false, error: `Invalid project name: "${project}".` });
438
+ }
439
+ if (action === "edit" && !new_text) {
440
+ return mcpResponse({ ok: false, error: "new_text is required when action is 'edit'." });
441
+ }
442
+ return withWriteQueue(async () => {
443
+ let result;
444
+ switch (action) {
445
+ case "approve":
446
+ result = approveQueueItem(phrenPath, project, line);
447
+ break;
448
+ case "reject":
449
+ result = rejectQueueItem(phrenPath, project, line);
450
+ break;
451
+ case "edit":
452
+ result = editQueueItem(phrenPath, project, line, new_text);
453
+ break;
454
+ }
455
+ if (!result.ok) {
456
+ return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
457
+ }
458
+ return mcpResponse({ ok: true, message: result.data });
459
+ });
460
+ });
363
461
  }
@@ -1,18 +1,19 @@
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 { createHash } from "crypto";
5
- import { isValidProjectName, errorMessage } from "./utils.js";
6
- import { readFindings } from "./data-access.js";
7
- import { debugLog, runtimeFile, DOC_TYPES, FINDING_TAGS, isMemoryScopeVisible, normalizeMemoryScope, } from "./shared.js";
8
- import { FINDING_LIFECYCLE_STATUSES, parseFindingLifecycle, } from "./shared-content.js";
9
- import { decodeStringRow, queryRows, queryDocRows, queryEntityLinks, logEntityMiss, extractSnippet, queryDocBySourceKey, normalizeMemoryId, } from "./shared-index.js";
10
- import { runCustomHooks } from "./hooks.js";
11
- import { entryScoreKey, getQualityMultiplier, getRetentionPolicy } from "./shared-governance.js";
12
- import { callLlm } from "./content-dedup.js";
13
- import { rankResults, searchKnowledgeRows, applyTrustFilter, searchFederatedStores } from "./shared-retrieval.js";
14
- import { parseSourceComment } from "./content-citation.js";
15
- import { resolveActiveSessionScope } from "./mcp-session.js";
5
+ import { isValidProjectName, errorMessage } from "../utils.js";
6
+ import { readFindings } from "../data/access.js";
7
+ import { debugLog, runtimeFile, DOC_TYPES, FINDING_TAGS, isMemoryScopeVisible, normalizeMemoryScope, } from "../shared.js";
8
+ import { FINDING_LIFECYCLE_STATUSES, parseFindingLifecycle, } from "../shared/content.js";
9
+ import { decodeStringRow, queryRows, queryDocRows, queryEntityLinks, logEntityMiss, extractSnippet, queryDocBySourceKey, normalizeMemoryId, } from "../shared/index.js";
10
+ import { runCustomHooks } from "../hooks.js";
11
+ import { entryScoreKey, getQualityMultiplier, getRetentionPolicy } from "../shared/governance.js";
12
+ import { callLlm } from "../content/dedup.js";
13
+ import { rankResults, searchKnowledgeRows, applyTrustFilter, searchFederatedStores } from "../shared/retrieval.js";
14
+ import { parseSourceComment } from "../content/citation.js";
15
+ import { resolveActiveSessionScope } from "./session.js";
16
+ import { logger } from "../logger.js";
16
17
  /**
17
18
  * Q30: Log zero-result queries to .runtime/search-misses.jsonl.
18
19
  * Strips PII-like tokens (emails, UUIDs, numbers) and keeps only query terms.
@@ -35,8 +36,7 @@ export function logSearchMiss(phrenPath, query, project) {
35
36
  fs.appendFileSync(missFile, entry + "\n");
36
37
  }
37
38
  catch (err) {
38
- if ((process.env.PHREN_DEBUG))
39
- process.stderr.write(`[phren] logSearchMiss: ${errorMessage(err)}\n`);
39
+ logger.debug("search", `logSearchMiss: ${errorMessage(err)}`);
40
40
  }
41
41
  }
42
42
  const HISTORY_FINDING_STATUSES = new Set(["superseded", "retracted"]);
@@ -160,8 +160,7 @@ export function register(server, ctx) {
160
160
  createdAt = stat.birthtime.toISOString();
161
161
  }
162
162
  catch (err) {
163
- if ((process.env.PHREN_DEBUG))
164
- process.stderr.write(`[phren] search_knowledge statFile: ${errorMessage(err)}\n`);
163
+ logger.debug("search", `search_knowledge statFile: ${errorMessage(err)}`);
165
164
  }
166
165
  // Extract tags from content (e.g. [decision], [pitfall], [pattern])
167
166
  const tagMatches = doc.content.match(/\[(decision|pitfall|pattern|tradeoff|architecture|bug)\]/gi);
@@ -259,7 +258,7 @@ export function register(server, ctx) {
259
258
  }
260
259
  catch (err) {
261
260
  if (process.env.PHREN_DEBUG) {
262
- process.stderr.write(`[phren] search_knowledge federation: ${errorMessage(err)}\n`);
261
+ logger.debug("search", `search_knowledge federation: ${errorMessage(err)}`);
263
262
  }
264
263
  }
265
264
  }
@@ -421,8 +420,7 @@ export function register(server, ctx) {
421
420
  relatedFragments = [...new Set(relatedFragments)].slice(0, 10);
422
421
  }
423
422
  catch (err) {
424
- if ((process.env.PHREN_DEBUG))
425
- process.stderr.write(`[phren] fragment query: ${errorMessage(err)}\n`);
423
+ logger.debug("search", `fragment query: ${errorMessage(err)}`);
426
424
  }
427
425
  const formatted = results.map((r) => {
428
426
  const fedNote = r.federation_source ? ` [from: ${r.federation_source}]` : "";
@@ -439,8 +437,7 @@ export function register(server, ctx) {
439
437
  synthCache = JSON.parse(fs.readFileSync(synthCachePath, "utf8"));
440
438
  }
441
439
  catch (err) {
442
- if ((process.env.PHREN_DEBUG))
443
- process.stderr.write(`[phren] search_knowledge synthCacheRead: ${errorMessage(err)}\n`);
440
+ logger.debug("search", `search_knowledge synthCacheRead: ${errorMessage(err)}`);
444
441
  }
445
442
  const cached = synthCache[synthKey];
446
443
  const SYNTH_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
@@ -464,8 +461,7 @@ export function register(server, ctx) {
464
461
  fs.writeFileSync(synthCachePath, JSON.stringify(synthCache));
465
462
  }
466
463
  catch (err) {
467
- if ((process.env.PHREN_DEBUG))
468
- process.stderr.write(`[phren] synthCache write: ${errorMessage(err)}\n`);
464
+ logger.debug("search", `synthCache write: ${errorMessage(err)}`);
469
465
  }
470
466
  }
471
467
  }
@@ -1,22 +1,23 @@
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
5
  import * as crypto from "crypto";
6
6
  import { execFileSync } from "child_process";
7
- import { debugLog, isMemoryScopeVisible, normalizeMemoryScope } from "./shared.js";
8
- import { withFileLock } from "./shared-governance.js";
9
- import { isValidProjectName, errorMessage } from "./utils.js";
10
- import { runCustomHooks } from "./hooks.js";
11
- import { readExtractedFacts } from "./mcp-extract-facts.js";
12
- import { resolveFindingSessionId } from "./finding-context.js";
13
- import { readTasks } from "./data-tasks.js";
14
- import { readFindings } from "./data-access.js";
15
- import { getProjectDirs } from "./shared.js";
16
- import { getActiveTaskForSession } from "./task-lifecycle.js";
17
- import { listTaskCheckpoints, writeTaskCheckpoint } from "./session-checkpoints.js";
18
- import { markImpactEntriesCompletedForSession } from "./finding-impact.js";
19
- import { atomicWriteJson, debugError, scanSessionFiles } from "./session-utils.js";
7
+ import { debugLog, isMemoryScopeVisible, normalizeMemoryScope } from "../shared.js";
8
+ import { withFileLock } from "../shared/governance.js";
9
+ import { isValidProjectName, errorMessage } from "../utils.js";
10
+ import { runCustomHooks } from "../hooks.js";
11
+ import { readExtractedFacts } from "./extract-facts.js";
12
+ import { resolveFindingSessionId } from "../finding/context.js";
13
+ import { readTasks } from "../data/tasks.js";
14
+ import { readFindings } from "../data/access.js";
15
+ import { getProjectDirs } from "../shared.js";
16
+ import { getActiveTaskForSession } from "../task/lifecycle.js";
17
+ import { listTaskCheckpoints, writeTaskCheckpoint } from "../session/checkpoints.js";
18
+ import { markImpactEntriesCompletedForSession } from "../finding/impact.js";
19
+ import { atomicWriteJson, debugError, scanSessionFiles } from "../session/utils.js";
20
+ import { getRuntimeHealth } from "../governance/policy.js";
20
21
  const STALE_SESSION_MS = 24 * 60 * 60 * 1000; // 24 hours
21
22
  function collectGitStatusSnapshot(cwd) {
22
23
  try {
@@ -215,6 +216,10 @@ function cleanupStaleSessions(phrenPath) {
215
216
  let cleaned = 0;
216
217
  for (const { fullPath, data: state } of results) {
217
218
  try {
219
+ // Only clean up sessions that have ended (have endedAt). Active sessions
220
+ // (no endedAt) should never be removed regardless of age.
221
+ if (state && !state.endedAt)
222
+ continue;
218
223
  // prefer startedAt from the JSON content over mtime (reliable on noatime mounts)
219
224
  const ageMs = state?.startedAt
220
225
  ? Date.now() - new Date(state.startedAt).getTime()
@@ -403,16 +408,15 @@ export function register(server, ctx) {
403
408
  if (agentScope !== undefined && !normalizedAgentScope) {
404
409
  return mcpResponse({ ok: false, error: `Invalid agentScope: "${agentScope}". Use lowercase letters/numbers with '-' or '_' (max 64 chars).` });
405
410
  }
406
- // Find most recent prior session for context
411
+ // Find most recent prior session for context.
412
+ // When no explicit project is provided, prefer the last ENDED session's
413
+ // project (completed context) over an active session from a different client.
414
+ const priorEnded = findMostRecentSummaryWithProject(phrenPath);
407
415
  const priorResult = findMostRecentSession(phrenPath);
408
416
  const prior = priorResult?.state ?? null;
409
- // Also check ended sessions for summaries and project context.
410
- // findMostRecentSession skips ended sessions, so we need a separate lookup
411
- // to restore project context after a normal session_end.
412
- const priorEnded = prior ? null : findMostRecentSummaryWithProject(phrenPath);
413
- const priorSummary = prior?.summary ?? priorEnded?.summary ?? null;
414
- const priorProject = prior?.project ?? priorEnded?.project;
415
- const priorEndedAt = prior?.endedAt ?? priorEnded?.endedAt;
417
+ const priorSummary = priorEnded?.summary ?? prior?.summary ?? null;
418
+ const priorProject = priorEnded?.project ?? prior?.project;
419
+ const priorEndedAt = priorEnded?.endedAt ?? prior?.endedAt;
416
420
  // Create new session with unique ID in its own file
417
421
  const sessionId = crypto.randomUUID();
418
422
  const next = {
@@ -521,10 +525,31 @@ export function register(server, ctx) {
521
525
  debugError("session_start contextDiff", err);
522
526
  }
523
527
  }
528
+ // ── Surface sync/health warnings ────────────────────────────────────
529
+ const sessionWarnings = [];
530
+ try {
531
+ const health = getRuntimeHealth(phrenPath);
532
+ if (health.lastAutoSave?.status === "error") {
533
+ sessionWarnings.push(`Last auto-save failed: ${health.lastAutoSave.detail ?? "unknown"}`);
534
+ }
535
+ if (health.lastSync?.lastPushStatus === "error") {
536
+ sessionWarnings.push(`Last push failed: ${health.lastSync.lastPushDetail ?? "unknown"}`);
537
+ }
538
+ const unsynced = health.lastSync?.unsyncedCommits;
539
+ if (typeof unsynced === "number" && unsynced > 0) {
540
+ sessionWarnings.push(`${unsynced} unsynced commit${unsynced === 1 ? "" : "s"} — run 'phren doctor' or check git remote`);
541
+ }
542
+ }
543
+ catch (err) {
544
+ debugError("session_start runtimeHealth", err);
545
+ }
546
+ if (sessionWarnings.length > 0) {
547
+ parts.push(`## Sync warnings\n${sessionWarnings.map(w => `- ${w}`).join("\n")}`);
548
+ }
524
549
  const message = parts.length > 0
525
550
  ? `Session started (${sessionId.slice(0, 8)}).\n\n${parts.join("\n\n")}`
526
551
  : `Session started (${sessionId.slice(0, 8)}). No prior context found.`;
527
- return mcpResponse({ ok: true, message, data: { sessionId, project: activeProject, agentScope: activeScope } });
552
+ return mcpResponse({ ok: true, message, data: { sessionId, project: activeProject, agentScope: activeScope, warnings: sessionWarnings.length > 0 ? sessionWarnings : undefined } });
528
553
  });
529
554
  server.registerTool("session_end", {
530
555
  title: "◆ phren · session end",
@@ -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 { isValidProjectName, safeProjectPath } from "./utils.js";
6
- import { parseSkillFrontmatter, validateSkillFrontmatter } from "./link-skills.js";
7
- import { removeSkillPath, setSkillEnabledAndSync } from "./skill-files.js";
8
- import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "./skill-registry.js";
5
+ import { isValidProjectName, safeProjectPath } from "../utils.js";
6
+ import { parseSkillFrontmatter, validateSkillFrontmatter } from "../link/skills.js";
7
+ import { removeSkillPath, setSkillEnabledAndSync } from "../skill/files.js";
8
+ import { buildSkillManifest, findLocalSkill, findSkill, getAllSkills } from "../skill/registry.js";
9
9
  export function register(server, ctx) {
10
10
  const { phrenPath, profile, withWriteQueue, updateFileInIndex } = ctx;
11
11
  // ── list_skills ──────────────────────────────────────────────────────────
@@ -165,36 +165,34 @@ export function register(server, ctx) {
165
165
  return mcpResponse({ ok: true, message: `Removed skill "${name}" (${removedPath}).`, data: { path: removedPath } });
166
166
  });
167
167
  });
168
- for (const action of [
169
- { tool: "enable_skill", enabled: true, verb: "Enable" },
170
- { tool: "disable_skill", enabled: false, verb: "Disable" },
171
- ]) {
172
- server.registerTool(action.tool, {
173
- title: `◆ phren · ${action.enabled ? "enable" : "disable"} skill`,
174
- description: `${action.verb} a skill without deleting its file.`,
175
- inputSchema: z.object({
176
- name: z.string().describe("Skill name (without .md)."),
177
- project: z.string().describe("Project scope or 'global'."),
178
- }),
179
- }, async ({ name, project }) => {
180
- if (project.toLowerCase() !== "global" && !isValidProjectName(project)) {
181
- return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
182
- }
183
- const result = findSkill(phrenPath, profile, project, name);
184
- if (!result) {
185
- return mcpResponse({ ok: false, error: `Skill "${name}" not found in "${project}".` });
186
- }
187
- if ("error" in result) {
188
- return mcpResponse({ ok: false, error: result.error });
189
- }
190
- return withWriteQueue(async () => {
191
- setSkillEnabledAndSync(phrenPath, project, result.name, action.enabled);
192
- return mcpResponse({
193
- ok: true,
194
- message: `${action.verb}d skill "${result.name}" in ${project}.`,
195
- data: { name: result.name, project, enabled: action.enabled },
196
- });
168
+ // ── toggle_skill ─────────────────────────────────────────────────────
169
+ server.registerTool("toggle_skill", {
170
+ title: " phren · toggle skill",
171
+ description: "Enable or disable a skill without deleting its file.",
172
+ inputSchema: z.object({
173
+ name: z.string().describe("Skill name (without .md)."),
174
+ enabled: z.boolean().describe("true to enable, false to disable."),
175
+ project: z.string().describe("Project scope or 'global'."),
176
+ }),
177
+ }, async ({ name, enabled, project }) => {
178
+ if (project.toLowerCase() !== "global" && !isValidProjectName(project)) {
179
+ return mcpResponse({ ok: false, error: `Invalid project name: "${project}"` });
180
+ }
181
+ const result = findSkill(phrenPath, profile, project, name);
182
+ if (!result) {
183
+ return mcpResponse({ ok: false, error: `Skill "${name}" not found in "${project}".` });
184
+ }
185
+ if ("error" in result) {
186
+ return mcpResponse({ ok: false, error: result.error });
187
+ }
188
+ const verb = enabled ? "Enable" : "Disable";
189
+ return withWriteQueue(async () => {
190
+ setSkillEnabledAndSync(phrenPath, project, result.name, enabled);
191
+ return mcpResponse({
192
+ ok: true,
193
+ message: `${verb}d skill "${result.name}" in ${project}.`,
194
+ data: { name: result.name, project, enabled },
197
195
  });
198
196
  });
199
- }
197
+ });
200
198
  }