@phren/cli 0.0.32 → 0.0.34

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 (59) hide show
  1. package/mcp/dist/cli/actions.js +3 -0
  2. package/mcp/dist/cli/config.js +3 -3
  3. package/mcp/dist/cli/govern.js +18 -8
  4. package/mcp/dist/cli/hooks-context.js +1 -1
  5. package/mcp/dist/cli/hooks-session.js +18 -62
  6. package/mcp/dist/cli/namespaces.js +1 -1
  7. package/mcp/dist/cli/search.js +5 -5
  8. package/mcp/dist/cli-hooks-prompt.js +7 -3
  9. package/mcp/dist/cli-hooks-session-handlers.js +3 -15
  10. package/mcp/dist/cli-hooks-stop.js +10 -48
  11. package/mcp/dist/content/archive.js +8 -20
  12. package/mcp/dist/content/learning.js +29 -8
  13. package/mcp/dist/data/access.js +13 -4
  14. package/mcp/dist/finding/lifecycle.js +9 -3
  15. package/mcp/dist/governance/audit.js +13 -5
  16. package/mcp/dist/governance/policy.js +13 -0
  17. package/mcp/dist/governance/rbac.js +1 -1
  18. package/mcp/dist/governance/scores.js +2 -1
  19. package/mcp/dist/hooks.js +52 -6
  20. package/mcp/dist/index.js +1 -1
  21. package/mcp/dist/init/init.js +66 -45
  22. package/mcp/dist/init/shared.js +1 -1
  23. package/mcp/dist/init-bootstrap.js +0 -47
  24. package/mcp/dist/init-fresh.js +13 -18
  25. package/mcp/dist/init-uninstall.js +22 -0
  26. package/mcp/dist/init-walkthrough.js +19 -24
  27. package/mcp/dist/link/doctor.js +9 -0
  28. package/mcp/dist/package-metadata.js +1 -1
  29. package/mcp/dist/phren-art.js +4 -120
  30. package/mcp/dist/proactivity.js +1 -1
  31. package/mcp/dist/project-topics.js +16 -46
  32. package/mcp/dist/provider-adapters.js +1 -1
  33. package/mcp/dist/runtime-profile.js +1 -1
  34. package/mcp/dist/shared/data-utils.js +25 -0
  35. package/mcp/dist/shared/fragment-graph.js +4 -18
  36. package/mcp/dist/shared/index.js +14 -10
  37. package/mcp/dist/shared/ollama.js +23 -5
  38. package/mcp/dist/shared/process.js +24 -0
  39. package/mcp/dist/shared/retrieval.js +7 -4
  40. package/mcp/dist/shared/search-fallback.js +1 -0
  41. package/mcp/dist/shared.js +2 -1
  42. package/mcp/dist/shell/render.js +1 -1
  43. package/mcp/dist/skill/registry.js +1 -1
  44. package/mcp/dist/skill/state.js +0 -3
  45. package/mcp/dist/task/github.js +1 -0
  46. package/mcp/dist/task/lifecycle.js +1 -6
  47. package/mcp/dist/tools/config.js +415 -400
  48. package/mcp/dist/tools/finding.js +390 -373
  49. package/mcp/dist/tools/ops.js +372 -365
  50. package/mcp/dist/tools/search.js +495 -487
  51. package/mcp/dist/tools/session.js +3 -2
  52. package/mcp/dist/tools/skills.js +9 -0
  53. package/mcp/dist/ui/page.js +1 -1
  54. package/mcp/dist/ui/server.js +645 -1040
  55. package/mcp/dist/utils.js +12 -8
  56. package/package.json +1 -1
  57. package/mcp/dist/init-dryrun.js +0 -55
  58. package/mcp/dist/init-migrate.js +0 -51
  59. package/mcp/dist/init-walkthrough-merge.js +0 -90
@@ -63,453 +63,468 @@ function getTopicConfigData(phrenPath, project) {
63
63
  },
64
64
  };
65
65
  }
66
- // ── Registration ────────────────────────────────────────────────────────────
67
- export function register(server, ctx) {
66
+ // ── Handlers ─────────────────────────────────────────────────────────────────
67
+ async function handleGetConfig(ctx, { domain, project }) {
68
68
  const { phrenPath } = ctx;
69
- // ── get_config ────────────────────────────────────────────────────────────
70
- server.registerTool("get_config", {
71
- title: "◆ phren · get config",
72
- description: "Read current configuration for one or all config domains: proactivity, taskMode, " +
73
- "findingSensitivity, retention (policy), workflow, access, index, topic. " +
74
- "Returns both configured and effective values. When project is provided, returns " +
75
- "the merged view with project overrides applied and _source annotations.",
76
- inputSchema: z.object({
77
- domain: z
78
- .enum(["proactivity", "taskMode", "findingSensitivity", "retention", "workflow", "access", "index", "topic", "all"])
79
- .optional()
80
- .describe("Config domain to read. Defaults to 'all'."),
81
- project: projectParam,
82
- }),
83
- }, async ({ domain, project }) => {
84
- const d = domain ?? "all";
85
- // topic domain requires a project
86
- if (d === "topic") {
87
- if (!project) {
88
- return mcpResponse({ ok: false, error: "The 'topic' domain requires a project parameter." });
89
- }
90
- const err = validateProject(project);
91
- if (err)
92
- return mcpResponse({ ok: false, error: err });
93
- const topicResult = getTopicConfigData(phrenPath, project);
94
- if (!topicResult.ok)
95
- return mcpResponse({ ok: false, error: topicResult.error });
96
- return mcpResponse({
97
- ok: true,
98
- message: `Topic config for "${project}" (source: ${topicResult.data.source}).`,
99
- data: topicResult.data,
100
- });
69
+ const d = domain ?? "all";
70
+ // topic domain requires a project
71
+ if (d === "topic") {
72
+ if (!project) {
73
+ return mcpResponse({ ok: false, error: "The 'topic' domain requires a project parameter." });
101
74
  }
102
- if (project) {
103
- const err = validateProject(project);
104
- if (err)
105
- return mcpResponse({ ok: false, error: err });
106
- const resolved = mergeConfig(phrenPath, project);
107
- const projectOverrides = getProjectOverrides(phrenPath, project);
108
- function src(key) {
109
- return hasOwnOverride(projectOverrides, key) ? "project" : "global";
110
- }
111
- const result = {
112
- _project: project,
113
- _note: "Values marked _source=project override the global default.",
114
- };
115
- if (d === "all" || d === "findingSensitivity") {
116
- const level = resolved.findingSensitivity;
117
- result.findingSensitivity = {
118
- level,
119
- ...FINDING_SENSITIVITY_CONFIG[level],
120
- _source: src("findingSensitivity"),
121
- };
122
- }
123
- if (d === "all" || d === "taskMode") {
124
- result.taskMode = { taskMode: resolved.taskMode, _source: src("taskMode") };
125
- }
126
- if (d === "all" || d === "retention") {
127
- result.retention = {
128
- ...resolved.retentionPolicy,
129
- _source: hasOwnOverride(projectOverrides, "retentionPolicy") ? "project" : "global",
130
- };
131
- }
132
- if (d === "all" || d === "workflow") {
133
- result.workflow = {
134
- ...resolved.workflowPolicy,
135
- _source: hasOwnOverride(projectOverrides, "workflowPolicy") ? "project" : "global",
136
- };
137
- }
138
- if (d === "all" || d === "proactivity") {
139
- const globalSnapshot = proactivitySnapshot(phrenPath).effective;
140
- const base = resolved.proactivity.base ?? globalSnapshot.proactivity;
141
- const findings = resolved.proactivity.findings ?? resolved.proactivity.base ?? globalSnapshot.proactivityFindings;
142
- const tasks = resolved.proactivity.tasks ?? resolved.proactivity.base ?? globalSnapshot.proactivityTask;
143
- result.proactivity = {
144
- base,
145
- findings,
146
- tasks,
147
- _source: {
148
- base: hasOwnOverride(projectOverrides, "proactivity") ? "project" : "global",
149
- findings: hasOwnOverride(projectOverrides, "proactivityFindings")
150
- ? "project"
151
- : hasOwnOverride(projectOverrides, "proactivity")
152
- ? "project"
153
- : "global",
154
- tasks: hasOwnOverride(projectOverrides, "proactivityTask")
155
- ? "project"
156
- : hasOwnOverride(projectOverrides, "proactivity")
157
- ? "project"
158
- : "global",
159
- },
160
- };
161
- }
162
- if (d === "all" || d === "index") {
163
- result.index = getIndexPolicy(phrenPath);
164
- }
165
- return mcpResponse({
166
- ok: true,
167
- message: `Config for ${d === "all" ? "all domains" : d} (project: ${project}).`,
168
- data: result,
169
- });
75
+ const err = validateProject(project);
76
+ if (err)
77
+ return mcpResponse({ ok: false, error: err });
78
+ const topicResult = getTopicConfigData(phrenPath, project);
79
+ if (!topicResult.ok)
80
+ return mcpResponse({ ok: false, error: topicResult.error });
81
+ return mcpResponse({
82
+ ok: true,
83
+ message: `Topic config for "${project}" (source: ${topicResult.data.source}).`,
84
+ data: topicResult.data,
85
+ });
86
+ }
87
+ if (project) {
88
+ const err = validateProject(project);
89
+ if (err)
90
+ return mcpResponse({ ok: false, error: err });
91
+ const resolved = mergeConfig(phrenPath, project);
92
+ const projectOverrides = getProjectOverrides(phrenPath, project);
93
+ function src(key) {
94
+ return hasOwnOverride(projectOverrides, key) ? "project" : "global";
170
95
  }
171
- const result = {};
172
- if (d === "all" || d === "proactivity") {
173
- result.proactivity = proactivitySnapshot(phrenPath);
96
+ const result = {
97
+ _project: project,
98
+ _note: "Values marked _source=project override the global default.",
99
+ };
100
+ if (d === "all" || d === "findingSensitivity") {
101
+ const level = resolved.findingSensitivity;
102
+ result.findingSensitivity = {
103
+ level,
104
+ ...FINDING_SENSITIVITY_CONFIG[level],
105
+ _source: src("findingSensitivity"),
106
+ };
174
107
  }
175
108
  if (d === "all" || d === "taskMode") {
176
- const wf = getWorkflowPolicy(phrenPath);
177
- result.taskMode = { taskMode: wf.taskMode };
178
- }
179
- if (d === "all" || d === "findingSensitivity") {
180
- const wf = getWorkflowPolicy(phrenPath);
181
- const level = wf.findingSensitivity;
182
- const config = FINDING_SENSITIVITY_CONFIG[level];
183
- result.findingSensitivity = { level, ...config };
109
+ result.taskMode = { taskMode: resolved.taskMode, _source: src("taskMode") };
184
110
  }
185
111
  if (d === "all" || d === "retention") {
186
- result.retention = getRetentionPolicy(phrenPath);
112
+ result.retention = {
113
+ ...resolved.retentionPolicy,
114
+ _source: hasOwnOverride(projectOverrides, "retentionPolicy") ? "project" : "global",
115
+ };
187
116
  }
188
117
  if (d === "all" || d === "workflow") {
189
- result.workflow = getWorkflowPolicy(phrenPath);
118
+ result.workflow = {
119
+ ...resolved.workflowPolicy,
120
+ _source: hasOwnOverride(projectOverrides, "workflowPolicy") ? "project" : "global",
121
+ };
122
+ }
123
+ if (d === "all" || d === "proactivity") {
124
+ const globalSnapshot = proactivitySnapshot(phrenPath).effective;
125
+ const base = resolved.proactivity.base ?? globalSnapshot.proactivity;
126
+ const findings = resolved.proactivity.findings ?? resolved.proactivity.base ?? globalSnapshot.proactivityFindings;
127
+ const tasks = resolved.proactivity.tasks ?? resolved.proactivity.base ?? globalSnapshot.proactivityTask;
128
+ result.proactivity = {
129
+ base,
130
+ findings,
131
+ tasks,
132
+ _source: {
133
+ base: hasOwnOverride(projectOverrides, "proactivity") ? "project" : "global",
134
+ findings: hasOwnOverride(projectOverrides, "proactivityFindings")
135
+ ? "project"
136
+ : hasOwnOverride(projectOverrides, "proactivity")
137
+ ? "project"
138
+ : "global",
139
+ tasks: hasOwnOverride(projectOverrides, "proactivityTask")
140
+ ? "project"
141
+ : hasOwnOverride(projectOverrides, "proactivity")
142
+ ? "project"
143
+ : "global",
144
+ },
145
+ };
190
146
  }
191
147
  if (d === "all" || d === "index") {
192
148
  result.index = getIndexPolicy(phrenPath);
193
149
  }
194
150
  return mcpResponse({
195
151
  ok: true,
196
- message: `Config for ${d === "all" ? "all domains" : d}.`,
152
+ message: `Config for ${d === "all" ? "all domains" : d} (project: ${project}).`,
197
153
  data: result,
198
154
  });
155
+ }
156
+ const result = {};
157
+ if (d === "all" || d === "proactivity") {
158
+ result.proactivity = proactivitySnapshot(phrenPath);
159
+ }
160
+ if (d === "all" || d === "taskMode") {
161
+ const wf = getWorkflowPolicy(phrenPath);
162
+ result.taskMode = { taskMode: wf.taskMode };
163
+ }
164
+ if (d === "all" || d === "findingSensitivity") {
165
+ const wf = getWorkflowPolicy(phrenPath);
166
+ const level = wf.findingSensitivity;
167
+ const config = FINDING_SENSITIVITY_CONFIG[level];
168
+ result.findingSensitivity = { level, ...config };
169
+ }
170
+ if (d === "all" || d === "retention") {
171
+ result.retention = getRetentionPolicy(phrenPath);
172
+ }
173
+ if (d === "all" || d === "workflow") {
174
+ result.workflow = getWorkflowPolicy(phrenPath);
175
+ }
176
+ if (d === "all" || d === "index") {
177
+ result.index = getIndexPolicy(phrenPath);
178
+ }
179
+ return mcpResponse({
180
+ ok: true,
181
+ message: `Config for ${d === "all" ? "all domains" : d}.`,
182
+ data: result,
199
183
  });
200
- // ── set_config ──────────────────────────────────────────────────────────
201
- server.registerTool("set_config", {
202
- title: "◆ phren · set config",
203
- description: "Update configuration for a specific domain. Replaces set_proactivity, set_task_mode, " +
204
- "set_finding_sensitivity, set_retention_policy, set_workflow_policy, set_index_policy, " +
205
- "and set_topic_config. When project is provided, writes to that project's phren.project.yaml " +
206
- "instead of global .config/.",
207
- inputSchema: z.object({
208
- domain: z.enum(["proactivity", "taskMode", "findingSensitivity", "retention", "workflow", "index", "topic"]),
209
- settings: z.record(z.string(), z.unknown()).describe("Domain-specific settings. proactivity: { level, scope? } | taskMode: { mode } | " +
210
- "findingSensitivity: { level } | retention: { ttlDays?, retentionDays?, autoAcceptThreshold?, " +
211
- "minInjectConfidence?, decay? } | workflow: { lowConfidenceThreshold?, riskySections?, taskMode?, " +
212
- "findingSensitivity? } | index: { includeGlobs?, excludeGlobs?, includeHidden? } | " +
213
- "topic: { topics, domain? }"),
214
- project: z.string().optional().describe("Project name. When provided, writes to that project's phren.project.yaml instead of global .config/. " +
215
- "Required for the 'topic' domain."),
216
- }),
217
- }, async ({ domain, settings, project }) => {
218
- switch (domain) {
219
- // ── proactivity ───────────────────────────────────────────────
220
- case "proactivity": {
221
- const level = settings.level;
222
- if (!level || !PROACTIVITY_LEVELS.includes(level)) {
223
- return mcpResponse({ ok: false, error: `Invalid proactivity level. Must be one of: ${PROACTIVITY_LEVELS.join(", ")}.` });
224
- }
225
- const scope = settings.scope ?? "base";
226
- if (!["base", "findings", "tasks"].includes(scope)) {
227
- return mcpResponse({ ok: false, error: `Invalid scope. Must be one of: base, findings, tasks.` });
228
- }
229
- const s = scope;
230
- if (project) {
231
- const err = validateProject(project);
232
- if (err)
233
- return mcpResponse({ ok: false, error: err });
234
- const warning = checkProjectRegistered(phrenPath, project);
235
- const key = s === "base" ? "proactivity" : s === "findings" ? "proactivityFindings" : "proactivityTask";
236
- updateProjectConfigOverrides(phrenPath, project, (current) => ({
237
- ...current,
238
- [key]: level,
239
- }));
240
- return mcpResponse({
241
- ok: true,
242
- message: warning
243
- ? `Proactivity ${s} set to ${level} for project "${project}". WARNING: ${warning}`
244
- : `Proactivity ${s} set to ${level} for project "${project}".`,
245
- data: { project, scope: s, level, ...(warning ? { warning } : {}) },
246
- });
247
- }
248
- const patch = {};
249
- if (s === "base")
250
- patch.proactivity = level;
251
- else if (s === "findings")
252
- patch.proactivityFindings = level;
253
- else if (s === "tasks")
254
- patch.proactivityTask = level;
255
- writeGovernanceInstallPreferences(phrenPath, patch);
184
+ }
185
+ async function handleSetConfig(ctx, { domain, settings, project }) {
186
+ const { phrenPath } = ctx;
187
+ switch (domain) {
188
+ // ── proactivity ───────────────────────────────────────────────
189
+ case "proactivity": {
190
+ const level = settings.level;
191
+ if (!level || !PROACTIVITY_LEVELS.includes(level)) {
192
+ return mcpResponse({ ok: false, error: `Invalid proactivity level. Must be one of: ${PROACTIVITY_LEVELS.join(", ")}.` });
193
+ }
194
+ const scope = settings.scope ?? "base";
195
+ if (!["base", "findings", "tasks"].includes(scope)) {
196
+ return mcpResponse({ ok: false, error: `Invalid scope. Must be one of: base, findings, tasks.` });
197
+ }
198
+ const s = scope;
199
+ if (project) {
200
+ const err = validateProject(project);
201
+ if (err)
202
+ return mcpResponse({ ok: false, error: err });
203
+ const warning = checkProjectRegistered(phrenPath, project);
204
+ const key = s === "base" ? "proactivity" : s === "findings" ? "proactivityFindings" : "proactivityTask";
205
+ updateProjectConfigOverrides(phrenPath, project, (current) => ({
206
+ ...current,
207
+ [key]: level,
208
+ }));
256
209
  return mcpResponse({
257
210
  ok: true,
258
- message: `Proactivity ${s} set to ${level}.`,
259
- data: proactivitySnapshot(phrenPath),
211
+ message: warning
212
+ ? `Proactivity ${s} set to ${level} for project "${project}". WARNING: ${warning}`
213
+ : `Proactivity ${s} set to ${level} for project "${project}".`,
214
+ data: { project, scope: s, level, ...(warning ? { warning } : {}) },
260
215
  });
261
216
  }
262
- // ── taskMode ──────────────────────────────────────────────────
263
- case "taskMode": {
264
- const mode = settings.mode;
265
- if (!mode || !VALID_TASK_MODES.includes(mode)) {
266
- return mcpResponse({ ok: false, error: `Invalid task mode. Must be one of: ${VALID_TASK_MODES.join(", ")}.` });
267
- }
268
- const validMode = mode;
269
- if (project) {
270
- const err = validateProject(project);
271
- if (err)
272
- return mcpResponse({ ok: false, error: err });
273
- const warning = checkProjectRegistered(phrenPath, project);
274
- updateProjectConfigOverrides(phrenPath, project, (current) => ({
275
- ...current,
276
- taskMode: validMode,
277
- }));
278
- return mcpResponse({
279
- ok: true,
280
- message: warning
281
- ? `Task mode set to ${validMode} for project "${project}". WARNING: ${warning}`
282
- : `Task mode set to ${validMode} for project "${project}".`,
283
- data: { project, taskMode: validMode, ...(warning ? { warning } : {}) },
284
- });
285
- }
286
- const result = updateWorkflowPolicy(phrenPath, { taskMode: validMode });
287
- if (!result.ok) {
288
- return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
289
- }
217
+ const patch = {};
218
+ if (s === "base")
219
+ patch.proactivity = level;
220
+ else if (s === "findings")
221
+ patch.proactivityFindings = level;
222
+ else if (s === "tasks")
223
+ patch.proactivityTask = level;
224
+ writeGovernanceInstallPreferences(phrenPath, patch);
225
+ return mcpResponse({
226
+ ok: true,
227
+ message: `Proactivity ${s} set to ${level}.`,
228
+ data: proactivitySnapshot(phrenPath),
229
+ });
230
+ }
231
+ // ── taskMode ──────────────────────────────────────────────────
232
+ case "taskMode": {
233
+ const mode = settings.mode;
234
+ if (!mode || !VALID_TASK_MODES.includes(mode)) {
235
+ return mcpResponse({ ok: false, error: `Invalid task mode. Must be one of: ${VALID_TASK_MODES.join(", ")}.` });
236
+ }
237
+ const validMode = mode;
238
+ if (project) {
239
+ const err = validateProject(project);
240
+ if (err)
241
+ return mcpResponse({ ok: false, error: err });
242
+ const warning = checkProjectRegistered(phrenPath, project);
243
+ updateProjectConfigOverrides(phrenPath, project, (current) => ({
244
+ ...current,
245
+ taskMode: validMode,
246
+ }));
290
247
  return mcpResponse({
291
248
  ok: true,
292
- message: `Task mode set to ${validMode}.`,
293
- data: { taskMode: validMode },
249
+ message: warning
250
+ ? `Task mode set to ${validMode} for project "${project}". WARNING: ${warning}`
251
+ : `Task mode set to ${validMode} for project "${project}".`,
252
+ data: { project, taskMode: validMode, ...(warning ? { warning } : {}) },
294
253
  });
295
254
  }
296
- // ── findingSensitivity ────────────────────────────────────────
297
- case "findingSensitivity": {
298
- const level = settings.level;
299
- if (!level || !VALID_FINDING_SENSITIVITY.includes(level)) {
300
- return mcpResponse({ ok: false, error: `Invalid finding sensitivity. Must be one of: ${VALID_FINDING_SENSITIVITY.join(", ")}.` });
301
- }
302
- const validLevel = level;
303
- if (project) {
304
- const err = validateProject(project);
305
- if (err)
306
- return mcpResponse({ ok: false, error: err });
307
- const warning = checkProjectRegistered(phrenPath, project);
308
- updateProjectConfigOverrides(phrenPath, project, (current) => ({
309
- ...current,
310
- findingSensitivity: validLevel,
311
- }));
312
- const config = FINDING_SENSITIVITY_CONFIG[validLevel];
313
- return mcpResponse({
314
- ok: true,
315
- message: warning
316
- ? `Finding sensitivity set to ${validLevel} for project "${project}". WARNING: ${warning}`
317
- : `Finding sensitivity set to ${validLevel} for project "${project}".`,
318
- data: { project, level: validLevel, ...config, ...(warning ? { warning } : {}) },
319
- });
320
- }
321
- const result = updateWorkflowPolicy(phrenPath, { findingSensitivity: validLevel });
322
- if (!result.ok) {
323
- return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
324
- }
255
+ const result = updateWorkflowPolicy(phrenPath, { taskMode: validMode });
256
+ if (!result.ok) {
257
+ return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
258
+ }
259
+ return mcpResponse({
260
+ ok: true,
261
+ message: `Task mode set to ${validMode}.`,
262
+ data: { taskMode: validMode },
263
+ });
264
+ }
265
+ // ── findingSensitivity ────────────────────────────────────────
266
+ case "findingSensitivity": {
267
+ const level = settings.level;
268
+ if (!level || !VALID_FINDING_SENSITIVITY.includes(level)) {
269
+ return mcpResponse({ ok: false, error: `Invalid finding sensitivity. Must be one of: ${VALID_FINDING_SENSITIVITY.join(", ")}.` });
270
+ }
271
+ const validLevel = level;
272
+ if (project) {
273
+ const err = validateProject(project);
274
+ if (err)
275
+ return mcpResponse({ ok: false, error: err });
276
+ const warning = checkProjectRegistered(phrenPath, project);
277
+ updateProjectConfigOverrides(phrenPath, project, (current) => ({
278
+ ...current,
279
+ findingSensitivity: validLevel,
280
+ }));
325
281
  const config = FINDING_SENSITIVITY_CONFIG[validLevel];
326
282
  return mcpResponse({
327
283
  ok: true,
328
- message: `Finding sensitivity set to ${validLevel}.`,
329
- data: { level: validLevel, ...config },
284
+ message: warning
285
+ ? `Finding sensitivity set to ${validLevel} for project "${project}". WARNING: ${warning}`
286
+ : `Finding sensitivity set to ${validLevel} for project "${project}".`,
287
+ data: { project, level: validLevel, ...config, ...(warning ? { warning } : {}) },
330
288
  });
331
289
  }
332
- // ── retention ─────────────────────────────────────────────────
333
- case "retention": {
334
- const { ttlDays, retentionDays, autoAcceptThreshold, minInjectConfidence, decay } = settings;
335
- if (project) {
336
- const err = validateProject(project);
337
- if (err)
338
- return mcpResponse({ ok: false, error: err });
339
- const warning = checkProjectRegistered(phrenPath, project);
340
- const next = updateProjectConfigOverrides(phrenPath, project, (current) => {
341
- const existingRetention = current.retentionPolicy ?? {};
342
- const retentionPatch = { ...existingRetention };
343
- if (ttlDays !== undefined)
344
- retentionPatch.ttlDays = ttlDays;
345
- if (retentionDays !== undefined)
346
- retentionPatch.retentionDays = retentionDays;
347
- if (autoAcceptThreshold !== undefined)
348
- retentionPatch.autoAcceptThreshold = autoAcceptThreshold;
349
- if (minInjectConfidence !== undefined)
350
- retentionPatch.minInjectConfidence = minInjectConfidence;
351
- if (decay !== undefined)
352
- retentionPatch.decay = { ...(existingRetention.decay ?? {}), ...decay };
353
- return { ...current, retentionPolicy: retentionPatch };
354
- });
355
- return mcpResponse({
356
- ok: true,
357
- message: warning
358
- ? `Retention policy updated for project "${project}". WARNING: ${warning}`
359
- : `Retention policy updated for project "${project}".`,
360
- data: { project, retentionPolicy: next.config?.retentionPolicy ?? {}, ...(warning ? { warning } : {}) },
361
- });
362
- }
363
- const globalPatch = {};
364
- if (ttlDays !== undefined)
365
- globalPatch.ttlDays = ttlDays;
366
- if (retentionDays !== undefined)
367
- globalPatch.retentionDays = retentionDays;
368
- if (autoAcceptThreshold !== undefined)
369
- globalPatch.autoAcceptThreshold = autoAcceptThreshold;
370
- if (minInjectConfidence !== undefined)
371
- globalPatch.minInjectConfidence = minInjectConfidence;
372
- if (decay !== undefined)
373
- globalPatch.decay = decay;
374
- const result = updateRetentionPolicy(phrenPath, globalPatch);
375
- if (!result.ok) {
376
- return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
377
- }
378
- return mcpResponse({
379
- ok: true,
380
- message: "Retention policy updated.",
381
- data: result.data,
382
- });
290
+ const result = updateWorkflowPolicy(phrenPath, { findingSensitivity: validLevel });
291
+ if (!result.ok) {
292
+ return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
383
293
  }
384
- // ── workflow ──────────────────────────────────────────────────
385
- case "workflow": {
386
- const { lowConfidenceThreshold, riskySections, taskMode, findingSensitivity } = settings;
387
- if (project) {
388
- const err = validateProject(project);
389
- if (err)
390
- return mcpResponse({ ok: false, error: err });
391
- const warning = checkProjectRegistered(phrenPath, project);
392
- const next = updateProjectConfigOverrides(phrenPath, project, (current) => {
393
- const nextConfig = { ...current };
394
- const shouldUpdateWorkflowPolicy = (lowConfidenceThreshold !== undefined
395
- || riskySections !== undefined
396
- || current.workflowPolicy !== undefined);
397
- if (shouldUpdateWorkflowPolicy) {
398
- const existingWorkflow = current.workflowPolicy ?? {};
399
- nextConfig.workflowPolicy = {
400
- ...existingWorkflow,
401
- ...(lowConfidenceThreshold !== undefined ? { lowConfidenceThreshold } : {}),
402
- ...(riskySections !== undefined ? { riskySections: riskySections } : {}),
403
- };
404
- }
405
- if (taskMode !== undefined)
406
- nextConfig.taskMode = taskMode;
407
- if (findingSensitivity !== undefined)
408
- nextConfig.findingSensitivity = findingSensitivity;
409
- return nextConfig;
410
- });
411
- return mcpResponse({
412
- ok: true,
413
- message: warning
414
- ? `Workflow policy updated for project "${project}". WARNING: ${warning}`
415
- : `Workflow policy updated for project "${project}".`,
416
- data: { project, config: next.config ?? {}, ...(warning ? { warning } : {}) },
417
- });
418
- }
419
- const patch = {};
420
- if (lowConfidenceThreshold !== undefined)
421
- patch.lowConfidenceThreshold = lowConfidenceThreshold;
422
- if (riskySections !== undefined)
423
- patch.riskySections = riskySections;
424
- if (taskMode !== undefined)
425
- patch.taskMode = taskMode;
426
- if (findingSensitivity !== undefined)
427
- patch.findingSensitivity = findingSensitivity;
428
- const result = updateWorkflowPolicy(phrenPath, patch);
429
- if (!result.ok) {
430
- return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
431
- }
432
- return mcpResponse({
433
- ok: true,
434
- message: "Workflow policy updated.",
435
- data: result.data,
294
+ const config = FINDING_SENSITIVITY_CONFIG[validLevel];
295
+ return mcpResponse({
296
+ ok: true,
297
+ message: `Finding sensitivity set to ${validLevel}.`,
298
+ data: { level: validLevel, ...config },
299
+ });
300
+ }
301
+ // ── retention ─────────────────────────────────────────────────
302
+ case "retention": {
303
+ const { ttlDays, retentionDays, autoAcceptThreshold, minInjectConfidence, decay } = settings;
304
+ if (project) {
305
+ const err = validateProject(project);
306
+ if (err)
307
+ return mcpResponse({ ok: false, error: err });
308
+ const warning = checkProjectRegistered(phrenPath, project);
309
+ const next = updateProjectConfigOverrides(phrenPath, project, (current) => {
310
+ const existingRetention = current.retentionPolicy ?? {};
311
+ const retentionPatch = { ...existingRetention };
312
+ if (ttlDays !== undefined)
313
+ retentionPatch.ttlDays = ttlDays;
314
+ if (retentionDays !== undefined)
315
+ retentionPatch.retentionDays = retentionDays;
316
+ if (autoAcceptThreshold !== undefined)
317
+ retentionPatch.autoAcceptThreshold = autoAcceptThreshold;
318
+ if (minInjectConfidence !== undefined)
319
+ retentionPatch.minInjectConfidence = minInjectConfidence;
320
+ if (decay !== undefined)
321
+ retentionPatch.decay = { ...(existingRetention.decay ?? {}), ...decay };
322
+ return { ...current, retentionPolicy: retentionPatch };
436
323
  });
437
- }
438
- // ── index ─────────────────────────────────────────────────────
439
- case "index": {
440
- const { includeGlobs, excludeGlobs, includeHidden } = settings;
441
- const patch = {};
442
- if (includeGlobs !== undefined)
443
- patch.includeGlobs = includeGlobs;
444
- if (excludeGlobs !== undefined)
445
- patch.excludeGlobs = excludeGlobs;
446
- if (includeHidden !== undefined)
447
- patch.includeHidden = includeHidden;
448
- const result = updateIndexPolicy(phrenPath, patch);
449
- if (!result.ok) {
450
- return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
451
- }
452
324
  return mcpResponse({
453
325
  ok: true,
454
- message: "Index policy updated.",
455
- data: result.data,
326
+ message: warning
327
+ ? `Retention policy updated for project "${project}". WARNING: ${warning}`
328
+ : `Retention policy updated for project "${project}".`,
329
+ data: { project, retentionPolicy: next.config?.retentionPolicy ?? {}, ...(warning ? { warning } : {}) },
456
330
  });
457
331
  }
458
- // ── topic ─────────────────────────────────────────────────────
459
- case "topic": {
460
- if (!project) {
461
- return mcpResponse({ ok: false, error: "The 'topic' domain requires a project parameter." });
462
- }
332
+ const globalPatch = {};
333
+ if (ttlDays !== undefined)
334
+ globalPatch.ttlDays = ttlDays;
335
+ if (retentionDays !== undefined)
336
+ globalPatch.retentionDays = retentionDays;
337
+ if (autoAcceptThreshold !== undefined)
338
+ globalPatch.autoAcceptThreshold = autoAcceptThreshold;
339
+ if (minInjectConfidence !== undefined)
340
+ globalPatch.minInjectConfidence = minInjectConfidence;
341
+ if (decay !== undefined)
342
+ globalPatch.decay = decay;
343
+ const result = updateRetentionPolicy(phrenPath, globalPatch);
344
+ if (!result.ok) {
345
+ return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
346
+ }
347
+ return mcpResponse({
348
+ ok: true,
349
+ message: "Retention policy updated.",
350
+ data: result.data,
351
+ });
352
+ }
353
+ // ── workflow ──────────────────────────────────────────────────
354
+ case "workflow": {
355
+ const { lowConfidenceThreshold, riskySections, taskMode, findingSensitivity } = settings;
356
+ if (project) {
463
357
  const err = validateProject(project);
464
358
  if (err)
465
359
  return mcpResponse({ ok: false, error: err });
466
- const projectDir = safeProjectPath(phrenPath, project);
467
- if (!projectDir || !fs.existsSync(projectDir)) {
468
- return mcpResponse({ ok: false, error: `Project "${project}" not found in phren.` });
360
+ // Validate enums before entering the callback
361
+ if (taskMode !== undefined && !VALID_TASK_MODES.includes(taskMode)) {
362
+ return mcpResponse({ ok: false, error: `Invalid task mode. Must be one of: ${VALID_TASK_MODES.join(", ")}.` });
469
363
  }
470
- const topics = settings.topics;
471
- if (!topics || !Array.isArray(topics)) {
472
- return mcpResponse({ ok: false, error: "The 'topic' domain requires a 'topics' array in settings." });
364
+ if (findingSensitivity !== undefined && !VALID_FINDING_SENSITIVITY.includes(findingSensitivity)) {
365
+ return mcpResponse({ ok: false, error: `Invalid finding sensitivity. Must be one of: ${VALID_FINDING_SENSITIVITY.join(", ")}.` });
473
366
  }
474
- const topicDomain = settings.domain;
475
- const normalized = topics.map((t) => ({
476
- slug: t.slug,
477
- label: t.label,
478
- description: t.description ?? "",
479
- keywords: t.keywords ?? [],
480
- }));
481
- // If a domain is provided, patch it onto the existing file before writing topics
482
- if (topicDomain) {
483
- const configPath = path.join(projectDir, "topic-config.json");
484
- if (fs.existsSync(configPath)) {
485
- try {
486
- const existing = JSON.parse(fs.readFileSync(configPath, "utf8"));
487
- if (existing && typeof existing === "object") {
488
- existing.domain = topicDomain;
489
- fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
490
- }
491
- }
492
- catch {
493
- // ignore read errors; writeProjectTopics will still succeed
367
+ const warning = checkProjectRegistered(phrenPath, project);
368
+ const next = updateProjectConfigOverrides(phrenPath, project, (current) => {
369
+ const nextConfig = { ...current };
370
+ const shouldUpdateWorkflowPolicy = (lowConfidenceThreshold !== undefined
371
+ || riskySections !== undefined
372
+ || current.workflowPolicy !== undefined);
373
+ if (shouldUpdateWorkflowPolicy) {
374
+ const existingWorkflow = current.workflowPolicy ?? {};
375
+ nextConfig.workflowPolicy = {
376
+ ...existingWorkflow,
377
+ ...(lowConfidenceThreshold !== undefined ? { lowConfidenceThreshold } : {}),
378
+ ...(riskySections !== undefined ? { riskySections: riskySections } : {}),
379
+ };
380
+ }
381
+ if (taskMode !== undefined)
382
+ nextConfig.taskMode = taskMode;
383
+ if (findingSensitivity !== undefined)
384
+ nextConfig.findingSensitivity = findingSensitivity;
385
+ return nextConfig;
386
+ });
387
+ return mcpResponse({
388
+ ok: true,
389
+ message: warning
390
+ ? `Workflow policy updated for project "${project}". WARNING: ${warning}`
391
+ : `Workflow policy updated for project "${project}".`,
392
+ data: { project, config: next.config ?? {}, ...(warning ? { warning } : {}) },
393
+ });
394
+ }
395
+ const patch = {};
396
+ if (lowConfidenceThreshold !== undefined)
397
+ patch.lowConfidenceThreshold = lowConfidenceThreshold;
398
+ if (riskySections !== undefined)
399
+ patch.riskySections = riskySections;
400
+ if (taskMode !== undefined) {
401
+ if (!VALID_TASK_MODES.includes(taskMode))
402
+ return mcpResponse({ ok: false, error: `Invalid task mode. Must be one of: ${VALID_TASK_MODES.join(", ")}.` });
403
+ patch.taskMode = taskMode;
404
+ }
405
+ if (findingSensitivity !== undefined) {
406
+ if (!VALID_FINDING_SENSITIVITY.includes(findingSensitivity))
407
+ return mcpResponse({ ok: false, error: `Invalid finding sensitivity. Must be one of: ${VALID_FINDING_SENSITIVITY.join(", ")}.` });
408
+ patch.findingSensitivity = findingSensitivity;
409
+ }
410
+ const result = updateWorkflowPolicy(phrenPath, patch);
411
+ if (!result.ok) {
412
+ return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
413
+ }
414
+ return mcpResponse({
415
+ ok: true,
416
+ message: "Workflow policy updated.",
417
+ data: result.data,
418
+ });
419
+ }
420
+ // ── index ─────────────────────────────────────────────────────
421
+ case "index": {
422
+ const { includeGlobs, excludeGlobs, includeHidden } = settings;
423
+ const patch = {};
424
+ if (includeGlobs !== undefined)
425
+ patch.includeGlobs = includeGlobs;
426
+ if (excludeGlobs !== undefined)
427
+ patch.excludeGlobs = excludeGlobs;
428
+ if (includeHidden !== undefined)
429
+ patch.includeHidden = includeHidden;
430
+ const result = updateIndexPolicy(phrenPath, patch);
431
+ if (!result.ok) {
432
+ return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
433
+ }
434
+ return mcpResponse({
435
+ ok: true,
436
+ message: "Index policy updated.",
437
+ data: result.data,
438
+ });
439
+ }
440
+ // ── topic ─────────────────────────────────────────────────────
441
+ case "topic": {
442
+ if (!project) {
443
+ return mcpResponse({ ok: false, error: "The 'topic' domain requires a project parameter." });
444
+ }
445
+ const err = validateProject(project);
446
+ if (err)
447
+ return mcpResponse({ ok: false, error: err });
448
+ const projectDir = safeProjectPath(phrenPath, project);
449
+ if (!projectDir || !fs.existsSync(projectDir)) {
450
+ return mcpResponse({ ok: false, error: `Project "${project}" not found in phren.` });
451
+ }
452
+ const topics = settings.topics;
453
+ if (!topics || !Array.isArray(topics)) {
454
+ return mcpResponse({ ok: false, error: "The 'topic' domain requires a 'topics' array in settings." });
455
+ }
456
+ const topicDomain = settings.domain;
457
+ const normalized = topics.map((t) => ({
458
+ slug: t.slug,
459
+ label: t.label,
460
+ description: t.description ?? "",
461
+ keywords: t.keywords ?? [],
462
+ }));
463
+ // If a domain is provided, patch it onto the existing file before writing topics
464
+ if (topicDomain) {
465
+ const configPath = path.join(projectDir, "topic-config.json");
466
+ if (fs.existsSync(configPath)) {
467
+ try {
468
+ const existing = JSON.parse(fs.readFileSync(configPath, "utf8"));
469
+ if (existing && typeof existing === "object") {
470
+ existing.domain = topicDomain;
471
+ fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
494
472
  }
495
473
  }
496
- else {
497
- fs.mkdirSync(projectDir, { recursive: true });
498
- fs.writeFileSync(configPath, JSON.stringify({ version: 1, domain: topicDomain, topics: [] }, null, 2) + "\n");
474
+ catch {
475
+ // ignore read errors; writeProjectTopics will still succeed
499
476
  }
500
477
  }
501
- const result = writeProjectTopics(phrenPath, project, normalized);
502
- if (!result.ok) {
503
- return mcpResponse({ ok: false, error: result.error });
478
+ else {
479
+ fs.mkdirSync(projectDir, { recursive: true });
480
+ fs.writeFileSync(configPath, JSON.stringify({ version: 1, domain: topicDomain, topics: [] }, null, 2) + "\n");
504
481
  }
505
- return mcpResponse({
506
- ok: true,
507
- message: `Topic config written for "${project}" (${result.topics.length} topics).`,
508
- data: { project, topics: result.topics, domain: topicDomain ?? null },
509
- });
510
482
  }
511
- default:
512
- return mcpResponse({ ok: false, error: `Unknown config domain: ${domain}` });
483
+ const result = writeProjectTopics(phrenPath, project, normalized);
484
+ if (!result.ok) {
485
+ return mcpResponse({ ok: false, error: result.error });
486
+ }
487
+ return mcpResponse({
488
+ ok: true,
489
+ message: `Topic config written for "${project}" (${result.topics.length} topics).`,
490
+ data: { project, topics: result.topics, domain: topicDomain ?? null },
491
+ });
513
492
  }
514
- });
493
+ default:
494
+ return mcpResponse({ ok: false, error: `Unknown config domain: ${domain}` });
495
+ }
496
+ }
497
+ // ── Registration ────────────────────────────────────────────────────────────
498
+ export function register(server, ctx) {
499
+ server.registerTool("get_config", {
500
+ title: "◆ phren · get config",
501
+ description: "Read current configuration for one or all config domains: proactivity, taskMode, " +
502
+ "findingSensitivity, retention (policy), workflow, access, index, topic. " +
503
+ "Returns both configured and effective values. When project is provided, returns " +
504
+ "the merged view with project overrides applied and _source annotations.",
505
+ inputSchema: z.object({
506
+ domain: z
507
+ .enum(["proactivity", "taskMode", "findingSensitivity", "retention", "workflow", "access", "index", "topic", "all"])
508
+ .optional()
509
+ .describe("Config domain to read. Defaults to 'all'."),
510
+ project: projectParam,
511
+ }),
512
+ }, (params) => handleGetConfig(ctx, params));
513
+ server.registerTool("set_config", {
514
+ title: "◆ phren · set config",
515
+ description: "Update configuration for a specific domain. Replaces set_proactivity, set_task_mode, " +
516
+ "set_finding_sensitivity, set_retention_policy, set_workflow_policy, set_index_policy, " +
517
+ "and set_topic_config. When project is provided, writes to that project's phren.project.yaml " +
518
+ "instead of global .config/.",
519
+ inputSchema: z.object({
520
+ domain: z.enum(["proactivity", "taskMode", "findingSensitivity", "retention", "workflow", "index", "topic"]),
521
+ settings: z.record(z.string(), z.unknown()).describe("Domain-specific settings. proactivity: { level, scope? } | taskMode: { mode } | " +
522
+ "findingSensitivity: { level } | retention: { ttlDays?, retentionDays?, autoAcceptThreshold?, " +
523
+ "minInjectConfidence?, decay? } | workflow: { lowConfidenceThreshold?, riskySections?, taskMode?, " +
524
+ "findingSensitivity? } | index: { includeGlobs?, excludeGlobs?, includeHidden? } | " +
525
+ "topic: { topics, domain? }"),
526
+ project: z.string().optional().describe("Project name. When provided, writes to that project's phren.project.yaml instead of global .config/. " +
527
+ "Required for the 'topic' domain."),
528
+ }),
529
+ }, (params) => handleSetConfig(ctx, params));
515
530
  }