@phren/cli 0.0.10 → 0.0.11

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 (63) hide show
  1. package/README.md +2 -8
  2. package/mcp/dist/cli-actions.js +5 -5
  3. package/mcp/dist/cli-config.js +334 -127
  4. package/mcp/dist/cli-govern.js +35 -63
  5. package/mcp/dist/cli-graph.js +3 -2
  6. package/mcp/dist/cli-hooks-globs.js +2 -1
  7. package/mcp/dist/cli-hooks-output.js +3 -3
  8. package/mcp/dist/cli-hooks.js +39 -32
  9. package/mcp/dist/cli-namespaces.js +15 -5
  10. package/mcp/dist/cli-search.js +2 -2
  11. package/mcp/dist/content-archive.js +2 -2
  12. package/mcp/dist/content-dedup.js +9 -9
  13. package/mcp/dist/embedding.js +7 -7
  14. package/mcp/dist/entrypoint.js +129 -102
  15. package/mcp/dist/governance-locks.js +6 -5
  16. package/mcp/dist/governance-policy.js +155 -2
  17. package/mcp/dist/governance-scores.js +3 -3
  18. package/mcp/dist/hooks.js +39 -18
  19. package/mcp/dist/index.js +4 -4
  20. package/mcp/dist/init-config.js +3 -24
  21. package/mcp/dist/init-setup.js +5 -5
  22. package/mcp/dist/init.js +170 -23
  23. package/mcp/dist/link-checksums.js +3 -2
  24. package/mcp/dist/link-context.js +1 -1
  25. package/mcp/dist/link-doctor.js +3 -3
  26. package/mcp/dist/link-skills.js +98 -12
  27. package/mcp/dist/link.js +17 -27
  28. package/mcp/dist/machine-identity.js +1 -9
  29. package/mcp/dist/mcp-config.js +247 -42
  30. package/mcp/dist/mcp-data.js +9 -9
  31. package/mcp/dist/mcp-extract-facts.js +1 -1
  32. package/mcp/dist/mcp-extract.js +2 -2
  33. package/mcp/dist/mcp-finding.js +6 -6
  34. package/mcp/dist/mcp-graph.js +11 -11
  35. package/mcp/dist/mcp-ops.js +18 -18
  36. package/mcp/dist/mcp-search.js +8 -8
  37. package/mcp/dist/memory-ui-page.js +23 -0
  38. package/mcp/dist/memory-ui-scripts.js +210 -27
  39. package/mcp/dist/memory-ui-server.js +115 -3
  40. package/mcp/dist/phren-paths.js +7 -7
  41. package/mcp/dist/profile-store.js +2 -2
  42. package/mcp/dist/project-config.js +63 -16
  43. package/mcp/dist/session-utils.js +3 -2
  44. package/mcp/dist/shared-fragment-graph.js +22 -21
  45. package/mcp/dist/shared-index.js +144 -105
  46. package/mcp/dist/shared-retrieval.js +19 -13
  47. package/mcp/dist/shared-search-fallback.js +13 -13
  48. package/mcp/dist/shared-sqljs.js +3 -2
  49. package/mcp/dist/shared.js +3 -3
  50. package/mcp/dist/shell-input.js +1 -1
  51. package/mcp/dist/shell-state-store.js +1 -1
  52. package/mcp/dist/shell-view.js +3 -2
  53. package/mcp/dist/shell.js +1 -1
  54. package/mcp/dist/skill-files.js +4 -10
  55. package/mcp/dist/skill-registry.js +3 -0
  56. package/mcp/dist/status.js +41 -13
  57. package/mcp/dist/task-hygiene.js +1 -1
  58. package/mcp/dist/telemetry.js +5 -4
  59. package/mcp/dist/update.js +1 -1
  60. package/mcp/dist/utils.js +3 -3
  61. package/package.json +2 -2
  62. package/starter/global/skills/audit.md +106 -0
  63. package/mcp/dist/shared-paths.js +0 -1
package/mcp/dist/link.js CHANGED
@@ -1,17 +1,17 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import * as crypto from "crypto";
4
3
  import * as readline from "readline";
5
4
  import * as yaml from "js-yaml";
6
5
  import { execFileSync } from "child_process";
7
- import { fileURLToPath } from "url";
6
+ import { ROOT } from "./package-metadata.js";
8
7
  import { configureClaude, configureCodexMcp, configureCopilotMcp, configureCursorMcp, configureVSCode, ensureGovernanceFiles, getHooksEnabledPreference, getMcpEnabledPreference, isVersionNewer, logMcpTargetStatus, patchJsonFile, setMcpEnabledPreference, } from "./init.js";
9
8
  import { configureAllHooks, detectInstalledTools } from "./hooks.js";
10
9
  import { getMachineName, persistMachineName } from "./machine-identity.js";
11
- import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, homePath, hookConfigPath, installPreferencesFile, } from "./shared.js";
10
+ import { debugLog, EXEC_TIMEOUT_MS, EXEC_TIMEOUT_QUICK_MS, isRecord, homePath, hookConfigPath, installPreferencesFile, atomicWriteText, } from "./shared.js";
12
11
  import { errorMessage } from "./utils.js";
12
+ import { log } from "./init-shared.js";
13
13
  import { listMachines as listMachinesShared, listProfiles as listProfilesShared, setMachineProfile, } from "./profile-store.js";
14
- import { writeSkillMd } from "./link-skills.js";
14
+ import { writeSkillMd, isManagedSymlink } from "./link-skills.js";
15
15
  import { syncScopeSkillsToDir } from "./skill-files.js";
16
16
  import { renderSkillInstructionsSection } from "./skill-registry.js";
17
17
  import { findProjectDir } from "./project-locator.js";
@@ -23,14 +23,6 @@ export { updateFileChecksums, verifyFileChecksums } from "./link-checksums.js";
23
23
  export { findProjectDir } from "./project-locator.js";
24
24
  export { parseSkillFrontmatter, validateSkillFrontmatter, validateSkillsDir, readSkillManifestHooks, } from "./link-skills.js";
25
25
  // ── Helpers (exported for link-doctor) ──────────────────────────────────────
26
- const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
27
- function log(msg) { process.stdout.write(msg + "\n"); }
28
- function atomicWriteText(filePath, content) {
29
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
30
- const tmpPath = `${filePath}.tmp-${crypto.randomUUID()}`;
31
- fs.writeFileSync(tmpPath, content);
32
- fs.renameSync(tmpPath, filePath);
33
- }
34
26
  export { getMachineName } from "./machine-identity.js";
35
27
  export function lookupProfile(phrenPath, machine) {
36
28
  const listed = listMachinesShared(phrenPath);
@@ -123,7 +115,7 @@ function setupSparseCheckout(phrenPath, projects) {
123
115
  execFileSync("git", ["rev-parse", "--git-dir"], { cwd: phrenPath, stdio: "ignore", timeout: EXEC_TIMEOUT_QUICK_MS });
124
116
  }
125
117
  catch (err) {
126
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
118
+ if ((process.env.PHREN_DEBUG))
127
119
  process.stderr.write(`[phren] setupSparseCheckout notAGitRepo: ${errorMessage(err)}\n`);
128
120
  return;
129
121
  }
@@ -184,10 +176,9 @@ function symlinkFile(src, dest, managedRoot) {
184
176
  if (stat.isSymbolicLink()) {
185
177
  const currentTarget = fs.readlinkSync(dest);
186
178
  const resolvedTarget = path.resolve(path.dirname(dest), currentTarget);
187
- const managedPrefix = path.resolve(managedRoot) + path.sep;
188
179
  if (resolvedTarget === path.resolve(src))
189
180
  return true;
190
- if (!resolvedTarget.startsWith(managedPrefix)) {
181
+ if (!isManagedSymlink(dest, managedRoot)) {
191
182
  log(` preserve existing symlink: ${dest}`);
192
183
  return false;
193
184
  }
@@ -238,8 +229,7 @@ function writeManagedAgentsFile(src, dest, content, managedRoot) {
238
229
  if (stat.isSymbolicLink()) {
239
230
  const currentTarget = fs.readlinkSync(dest);
240
231
  const resolvedTarget = path.resolve(path.dirname(dest), currentTarget);
241
- const managedPrefix = path.resolve(managedRoot) + path.sep;
242
- if (resolvedTarget === path.resolve(src) || resolvedTarget.startsWith(managedPrefix)) {
232
+ if (resolvedTarget === path.resolve(src) || isManagedSymlink(dest, managedRoot)) {
243
233
  fs.unlinkSync(dest);
244
234
  }
245
235
  else {
@@ -278,7 +268,7 @@ function linkGlobal(phrenPath, tools) {
278
268
  symlinkFile(globalClaude, path.join(copilotInstrDir, "copilot-instructions.md"), phrenPath);
279
269
  }
280
270
  catch (err) {
281
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
271
+ if ((process.env.PHREN_DEBUG))
282
272
  process.stderr.write(`[phren] linkGlobal copilotInstructions: ${errorMessage(err)}\n`);
283
273
  }
284
274
  }
@@ -324,7 +314,7 @@ function linkProject(phrenPath, project, tools) {
324
314
  symlinkFile(src, path.join(copilotDir, "copilot-instructions.md"), phrenPath);
325
315
  }
326
316
  catch (err) {
327
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
317
+ if ((process.env.PHREN_DEBUG))
328
318
  process.stderr.write(`[phren] linkProject copilotInstructions: ${errorMessage(err)}\n`);
329
319
  }
330
320
  }
@@ -348,7 +338,7 @@ function linkProject(phrenPath, project, tools) {
348
338
  addTokenAnnotation(claudeFile);
349
339
  }
350
340
  catch (err) {
351
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
341
+ if ((process.env.PHREN_DEBUG))
352
342
  process.stderr.write(`[phren] linkProject tokenAnnotation: ${errorMessage(err)}\n`);
353
343
  }
354
344
  }
@@ -365,7 +355,7 @@ function linkProject(phrenPath, project, tools) {
365
355
  excludeEntries.push("AGENTS.md");
366
356
  }
367
357
  catch (err) {
368
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
358
+ if ((process.env.PHREN_DEBUG))
369
359
  process.stderr.write(`[phren] linkProject agentsMd: ${errorMessage(err)}\n`);
370
360
  }
371
361
  }
@@ -506,7 +496,7 @@ export async function runLink(phrenPath, opts = {}) {
506
496
  mcpStatus = configureClaude(phrenPath, { mcpEnabled, hooksEnabled }) ?? "installed";
507
497
  }
508
498
  catch (err) {
509
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
499
+ if ((process.env.PHREN_DEBUG))
510
500
  process.stderr.write(`[phren] link configureClaude: ${errorMessage(err)}\n`);
511
501
  }
512
502
  logMcpTargetStatus("Claude", mcpStatus);
@@ -515,7 +505,7 @@ export async function runLink(phrenPath, opts = {}) {
515
505
  vsStatus = configureVSCode(phrenPath, { mcpEnabled }) ?? "no_vscode";
516
506
  }
517
507
  catch (err) {
518
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
508
+ if ((process.env.PHREN_DEBUG))
519
509
  process.stderr.write(`[phren] link configureVSCode: ${errorMessage(err)}\n`);
520
510
  }
521
511
  logMcpTargetStatus("VS Code", vsStatus);
@@ -524,7 +514,7 @@ export async function runLink(phrenPath, opts = {}) {
524
514
  cursorStatus = configureCursorMcp(phrenPath, { mcpEnabled }) ?? "no_cursor";
525
515
  }
526
516
  catch (err) {
527
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
517
+ if ((process.env.PHREN_DEBUG))
528
518
  process.stderr.write(`[phren] link configureCursorMcp: ${errorMessage(err)}\n`);
529
519
  }
530
520
  logMcpTargetStatus("Cursor", cursorStatus);
@@ -533,7 +523,7 @@ export async function runLink(phrenPath, opts = {}) {
533
523
  copilotStatus = configureCopilotMcp(phrenPath, { mcpEnabled }) ?? "no_copilot";
534
524
  }
535
525
  catch (err) {
536
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
526
+ if ((process.env.PHREN_DEBUG))
537
527
  process.stderr.write(`[phren] link configureCopilotMcp: ${errorMessage(err)}\n`);
538
528
  }
539
529
  logMcpTargetStatus("Copilot CLI", copilotStatus);
@@ -542,7 +532,7 @@ export async function runLink(phrenPath, opts = {}) {
542
532
  codexStatus = configureCodexMcp(phrenPath, { mcpEnabled }) ?? "no_codex";
543
533
  }
544
534
  catch (err) {
545
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
535
+ if ((process.env.PHREN_DEBUG))
546
536
  process.stderr.write(`[phren] link configureCodexMcp: ${errorMessage(err)}\n`);
547
537
  }
548
538
  logMcpTargetStatus("Codex", codexStatus);
@@ -566,7 +556,7 @@ export async function runLink(phrenPath, opts = {}) {
566
556
  log(` phren.SKILL.md written (agentskills-compatible tools)`);
567
557
  }
568
558
  catch (err) {
569
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
559
+ if ((process.env.PHREN_DEBUG))
570
560
  process.stderr.write(`[phren] link writeSkillMd: ${errorMessage(err)}\n`);
571
561
  }
572
562
  log("");
@@ -1,17 +1,9 @@
1
1
  import * as fs from "fs";
2
2
  import * as os from "os";
3
- import * as path from "path";
4
- import * as crypto from "crypto";
5
- import { homePath } from "./shared.js";
3
+ import { homePath, atomicWriteText } from "./shared.js";
6
4
  function phrenMachineFilePath() {
7
5
  return homePath(".phren", ".machine-id");
8
6
  }
9
- function atomicWriteText(filePath, content) {
10
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
11
- const tmpPath = `${filePath}.tmp-${crypto.randomUUID()}`;
12
- fs.writeFileSync(tmpPath, content);
13
- fs.renameSync(tmpPath, filePath);
14
- }
15
7
  export function machineFilePath() {
16
8
  return phrenMachineFilePath();
17
9
  }
@@ -1,27 +1,38 @@
1
1
  import { mcpResponse } from "./mcp-types.js";
2
2
  import { z } from "zod";
3
- import { getRetentionPolicy, updateRetentionPolicy, getWorkflowPolicy, updateWorkflowPolicy, getIndexPolicy, updateIndexPolicy, } from "./shared-governance.js";
4
- import { PROACTIVITY_LEVELS, getProactivityLevel, getProactivityLevelForFindings, getProactivityLevelForTask, } from "./proactivity.js";
5
- import { readGovernanceInstallPreferences, writeGovernanceInstallPreferences, } from "./init-preferences.js";
6
- import { FINDING_SENSITIVITY_CONFIG } from "./cli-config.js";
3
+ import { getRetentionPolicy, updateRetentionPolicy, getWorkflowPolicy, updateWorkflowPolicy, getIndexPolicy, updateIndexPolicy, mergeConfig, VALID_TASK_MODES, VALID_FINDING_SENSITIVITY, } from "./shared-governance.js";
4
+ import { PROACTIVITY_LEVELS, } from "./proactivity.js";
5
+ import { writeGovernanceInstallPreferences, } from "./init-preferences.js";
6
+ import { FINDING_SENSITIVITY_CONFIG, buildProactivitySnapshot, checkProjectInProfile } from "./cli-config.js";
7
+ import { readProjectConfig, updateProjectConfigOverrides, } from "./project-config.js";
8
+ import { isValidProjectName } from "./utils.js";
7
9
  // ── Helpers ─────────────────────────────────────────────────────────────────
8
10
  function proactivitySnapshot(phrenPath) {
9
- const prefs = readGovernanceInstallPreferences(phrenPath);
10
- return {
11
- configured: {
12
- proactivity: prefs.proactivity ?? null,
13
- proactivityFindings: prefs.proactivityFindings ?? null,
14
- proactivityTask: prefs.proactivityTask ?? null,
15
- },
16
- effective: {
17
- proactivity: getProactivityLevel(phrenPath),
18
- proactivityFindings: getProactivityLevelForFindings(phrenPath),
19
- proactivityTask: getProactivityLevelForTask(phrenPath),
20
- },
21
- };
11
+ const snap = buildProactivitySnapshot(phrenPath);
12
+ return { configured: snap.configured, effective: snap.effective };
22
13
  }
23
- const TASK_MODES = ["off", "manual", "suggest", "auto"];
24
- const FINDING_SENSITIVITY_LEVELS = ["minimal", "conservative", "balanced", "aggressive"];
14
+ function validateProject(project) {
15
+ if (!isValidProjectName(project))
16
+ return `Invalid project name: "${project}".`;
17
+ return null;
18
+ }
19
+ function checkProjectRegistered(phrenPath, project) {
20
+ const warning = checkProjectInProfile(phrenPath, project);
21
+ if (warning) {
22
+ return `Project '${project}' is not registered in your active profile. Config was written but won't take effect until you run 'phren add' to register the project.`;
23
+ }
24
+ return null;
25
+ }
26
+ function normalizeProjectOverrides(raw) {
27
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
28
+ }
29
+ function getProjectOverrides(phrenPath, project) {
30
+ return normalizeProjectOverrides(readProjectConfig(phrenPath, project).config);
31
+ }
32
+ function hasOwnOverride(overrides, key) {
33
+ return Object.prototype.hasOwnProperty.call(overrides, key);
34
+ }
35
+ const projectParam = z.string().optional().describe("Project name. When provided, writes to that project's phren.project.yaml instead of global .governance/.");
25
36
  // ── Registration ────────────────────────────────────────────────────────────
26
37
  export function register(server, ctx) {
27
38
  const { phrenPath } = ctx;
@@ -30,15 +41,86 @@ export function register(server, ctx) {
30
41
  title: "◆ phren · get config",
31
42
  description: "Read current configuration for one or all config domains: proactivity, taskMode, " +
32
43
  "findingSensitivity, retention (policy), workflow, access, index. " +
33
- "Returns both configured and effective values.",
44
+ "Returns both configured and effective values. When project is provided, returns " +
45
+ "the merged view with project overrides applied and _source annotations.",
34
46
  inputSchema: z.object({
35
47
  domain: z
36
48
  .enum(["proactivity", "taskMode", "findingSensitivity", "retention", "workflow", "access", "index", "all"])
37
49
  .optional()
38
50
  .describe("Config domain to read. Defaults to 'all'."),
51
+ project: projectParam,
39
52
  }),
40
- }, async ({ domain }) => {
53
+ }, async ({ domain, project }) => {
41
54
  const d = domain ?? "all";
55
+ if (project) {
56
+ const err = validateProject(project);
57
+ if (err)
58
+ return mcpResponse({ ok: false, error: err });
59
+ const resolved = mergeConfig(phrenPath, project);
60
+ const projectOverrides = getProjectOverrides(phrenPath, project);
61
+ function src(key) {
62
+ return hasOwnOverride(projectOverrides, key) ? "project" : "global";
63
+ }
64
+ const result = {
65
+ _project: project,
66
+ _note: "Values marked _source=project override the global default.",
67
+ };
68
+ if (d === "all" || d === "findingSensitivity") {
69
+ const level = resolved.findingSensitivity;
70
+ result.findingSensitivity = {
71
+ level,
72
+ ...FINDING_SENSITIVITY_CONFIG[level],
73
+ _source: src("findingSensitivity"),
74
+ };
75
+ }
76
+ if (d === "all" || d === "taskMode") {
77
+ result.taskMode = { taskMode: resolved.taskMode, _source: src("taskMode") };
78
+ }
79
+ if (d === "all" || d === "retention") {
80
+ result.retention = {
81
+ ...resolved.retentionPolicy,
82
+ _source: hasOwnOverride(projectOverrides, "retentionPolicy") ? "project" : "global",
83
+ };
84
+ }
85
+ if (d === "all" || d === "workflow") {
86
+ result.workflow = {
87
+ ...resolved.workflowPolicy,
88
+ _source: hasOwnOverride(projectOverrides, "workflowPolicy") ? "project" : "global",
89
+ };
90
+ }
91
+ if (d === "all" || d === "proactivity") {
92
+ const globalSnapshot = proactivitySnapshot(phrenPath).effective;
93
+ const base = resolved.proactivity.base ?? globalSnapshot.proactivity;
94
+ const findings = resolved.proactivity.findings ?? resolved.proactivity.base ?? globalSnapshot.proactivityFindings;
95
+ const tasks = resolved.proactivity.tasks ?? resolved.proactivity.base ?? globalSnapshot.proactivityTask;
96
+ result.proactivity = {
97
+ base,
98
+ findings,
99
+ tasks,
100
+ _source: {
101
+ base: hasOwnOverride(projectOverrides, "proactivity") ? "project" : "global",
102
+ findings: hasOwnOverride(projectOverrides, "proactivityFindings")
103
+ ? "project"
104
+ : hasOwnOverride(projectOverrides, "proactivity")
105
+ ? "project"
106
+ : "global",
107
+ tasks: hasOwnOverride(projectOverrides, "proactivityTask")
108
+ ? "project"
109
+ : hasOwnOverride(projectOverrides, "proactivity")
110
+ ? "project"
111
+ : "global",
112
+ },
113
+ };
114
+ }
115
+ if (d === "all" || d === "index") {
116
+ result.index = getIndexPolicy(phrenPath);
117
+ }
118
+ return mcpResponse({
119
+ ok: true,
120
+ message: `Config for ${d === "all" ? "all domains" : d} (project: ${project}).`,
121
+ data: result,
122
+ });
123
+ }
42
124
  const result = {};
43
125
  if (d === "all" || d === "proactivity") {
44
126
  result.proactivity = proactivitySnapshot(phrenPath);
@@ -72,16 +154,36 @@ export function register(server, ctx) {
72
154
  server.registerTool("set_proactivity", {
73
155
  title: "◆ phren · set proactivity",
74
156
  description: "Set the proactivity level for auto-capture. Controls how aggressively phren " +
75
- "captures findings and tasks. Supports base level, findings-specific, and task-specific overrides.",
157
+ "captures findings and tasks. Supports base level, findings-specific, and task-specific overrides. " +
158
+ "When project is provided, writes to that project's phren.project.yaml.",
76
159
  inputSchema: z.object({
77
160
  level: z.enum(PROACTIVITY_LEVELS).describe("Proactivity level: high, medium, or low."),
78
161
  scope: z
79
162
  .enum(["base", "findings", "tasks"])
80
163
  .optional()
81
164
  .describe("Which proactivity to set. Defaults to 'base'."),
165
+ project: projectParam,
82
166
  }),
83
- }, async ({ level, scope }) => {
167
+ }, async ({ level, scope, project }) => {
84
168
  const s = scope ?? "base";
169
+ if (project) {
170
+ const err = validateProject(project);
171
+ if (err)
172
+ return mcpResponse({ ok: false, error: err });
173
+ const warning = checkProjectRegistered(phrenPath, project);
174
+ const key = s === "base" ? "proactivity" : s === "findings" ? "proactivityFindings" : "proactivityTask";
175
+ updateProjectConfigOverrides(phrenPath, project, (current) => ({
176
+ ...current,
177
+ [key]: level,
178
+ }));
179
+ return mcpResponse({
180
+ ok: true,
181
+ message: warning
182
+ ? `Proactivity ${s} set to ${level} for project "${project}". WARNING: ${warning}`
183
+ : `Proactivity ${s} set to ${level} for project "${project}".`,
184
+ data: { project, scope: s, level, ...(warning ? { warning } : {}) },
185
+ });
186
+ }
85
187
  const patch = {};
86
188
  if (s === "base")
87
189
  patch.proactivity = level;
@@ -100,11 +202,30 @@ export function register(server, ctx) {
100
202
  server.registerTool("set_task_mode", {
101
203
  title: "◆ phren · set task mode",
102
204
  description: "Set the task automation mode: off (no auto-tasks), manual (user creates), " +
103
- "suggest (phren suggests, user approves), auto (phren creates automatically).",
205
+ "suggest (phren suggests, user approves), auto (phren creates automatically). " +
206
+ "When project is provided, writes to that project's phren.project.yaml.",
104
207
  inputSchema: z.object({
105
- mode: z.enum(TASK_MODES).describe("Task mode: off, manual, suggest, or auto."),
208
+ mode: z.enum(VALID_TASK_MODES).describe("Task mode: off, manual, suggest, or auto."),
209
+ project: projectParam,
106
210
  }),
107
- }, async ({ mode }) => {
211
+ }, async ({ mode, project }) => {
212
+ if (project) {
213
+ const err = validateProject(project);
214
+ if (err)
215
+ return mcpResponse({ ok: false, error: err });
216
+ const warning = checkProjectRegistered(phrenPath, project);
217
+ updateProjectConfigOverrides(phrenPath, project, (current) => ({
218
+ ...current,
219
+ taskMode: mode,
220
+ }));
221
+ return mcpResponse({
222
+ ok: true,
223
+ message: warning
224
+ ? `Task mode set to ${mode} for project "${project}". WARNING: ${warning}`
225
+ : `Task mode set to ${mode} for project "${project}".`,
226
+ data: { project, taskMode: mode, ...(warning ? { warning } : {}) },
227
+ });
228
+ }
108
229
  const result = updateWorkflowPolicy(phrenPath, { taskMode: mode });
109
230
  if (!result.ok) {
110
231
  return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
@@ -120,11 +241,31 @@ export function register(server, ctx) {
120
241
  title: "◆ phren · set finding sensitivity",
121
242
  description: "Set the finding capture sensitivity level. Controls how many findings phren captures per session. " +
122
243
  "minimal: only explicit asks. conservative: decisions/pitfalls only. " +
123
- "balanced: non-obvious patterns. aggressive: capture everything.",
244
+ "balanced: non-obvious patterns. aggressive: capture everything. " +
245
+ "When project is provided, writes to that project's phren.project.yaml.",
124
246
  inputSchema: z.object({
125
- level: z.enum(FINDING_SENSITIVITY_LEVELS).describe("Sensitivity level."),
247
+ level: z.enum(VALID_FINDING_SENSITIVITY).describe("Sensitivity level."),
248
+ project: projectParam,
126
249
  }),
127
- }, async ({ level }) => {
250
+ }, async ({ level, project }) => {
251
+ if (project) {
252
+ const err = validateProject(project);
253
+ if (err)
254
+ return mcpResponse({ ok: false, error: err });
255
+ const warning = checkProjectRegistered(phrenPath, project);
256
+ updateProjectConfigOverrides(phrenPath, project, (current) => ({
257
+ ...current,
258
+ findingSensitivity: level,
259
+ }));
260
+ const config = FINDING_SENSITIVITY_CONFIG[level];
261
+ return mcpResponse({
262
+ ok: true,
263
+ message: warning
264
+ ? `Finding sensitivity set to ${level} for project "${project}". WARNING: ${warning}`
265
+ : `Finding sensitivity set to ${level} for project "${project}".`,
266
+ data: { project, level, ...config, ...(warning ? { warning } : {}) },
267
+ });
268
+ }
128
269
  const result = updateWorkflowPolicy(phrenPath, { findingSensitivity: level });
129
270
  if (!result.ok) {
130
271
  return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
@@ -140,7 +281,8 @@ export function register(server, ctx) {
140
281
  server.registerTool("set_retention_policy", {
141
282
  title: "◆ phren · set retention policy",
142
283
  description: "Update memory retention policy: TTL, retention days, auto-accept threshold, " +
143
- "minimum injection confidence, and decay curve.",
284
+ "minimum injection confidence, and decay curve. " +
285
+ "When project is provided, writes to that project's phren.project.yaml.",
144
286
  inputSchema: z.object({
145
287
  ttlDays: z.number().int().min(1).optional().describe("Days before a finding is considered for expiry."),
146
288
  retentionDays: z.number().int().min(1).optional().describe("Hard retention limit in days."),
@@ -155,20 +297,49 @@ export function register(server, ctx) {
155
297
  })
156
298
  .optional()
157
299
  .describe("Decay multipliers at 30/60/90/120 day marks."),
300
+ project: projectParam,
158
301
  }),
159
- }, async ({ ttlDays, retentionDays, autoAcceptThreshold, minInjectConfidence, decay }) => {
160
- const patch = {};
302
+ }, async ({ ttlDays, retentionDays, autoAcceptThreshold, minInjectConfidence, decay, project }) => {
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
+ const next = updateProjectConfigOverrides(phrenPath, project, (current) => {
309
+ const existingRetention = current.retentionPolicy ?? {};
310
+ const retentionPatch = { ...existingRetention };
311
+ if (ttlDays !== undefined)
312
+ retentionPatch.ttlDays = ttlDays;
313
+ if (retentionDays !== undefined)
314
+ retentionPatch.retentionDays = retentionDays;
315
+ if (autoAcceptThreshold !== undefined)
316
+ retentionPatch.autoAcceptThreshold = autoAcceptThreshold;
317
+ if (minInjectConfidence !== undefined)
318
+ retentionPatch.minInjectConfidence = minInjectConfidence;
319
+ if (decay !== undefined)
320
+ retentionPatch.decay = { ...(existingRetention.decay ?? {}), ...decay };
321
+ return { ...current, retentionPolicy: retentionPatch };
322
+ });
323
+ return mcpResponse({
324
+ ok: true,
325
+ message: warning
326
+ ? `Retention policy updated for project "${project}". WARNING: ${warning}`
327
+ : `Retention policy updated for project "${project}".`,
328
+ data: { project, retentionPolicy: next.config?.retentionPolicy ?? {}, ...(warning ? { warning } : {}) },
329
+ });
330
+ }
331
+ const globalPatch = {};
161
332
  if (ttlDays !== undefined)
162
- patch.ttlDays = ttlDays;
333
+ globalPatch.ttlDays = ttlDays;
163
334
  if (retentionDays !== undefined)
164
- patch.retentionDays = retentionDays;
335
+ globalPatch.retentionDays = retentionDays;
165
336
  if (autoAcceptThreshold !== undefined)
166
- patch.autoAcceptThreshold = autoAcceptThreshold;
337
+ globalPatch.autoAcceptThreshold = autoAcceptThreshold;
167
338
  if (minInjectConfidence !== undefined)
168
- patch.minInjectConfidence = minInjectConfidence;
339
+ globalPatch.minInjectConfidence = minInjectConfidence;
169
340
  if (decay !== undefined)
170
- patch.decay = decay;
171
- const result = updateRetentionPolicy(phrenPath, patch);
341
+ globalPatch.decay = decay;
342
+ const result = updateRetentionPolicy(phrenPath, globalPatch);
172
343
  if (!result.ok) {
173
344
  return mcpResponse({ ok: false, error: result.error, errorCode: result.code });
174
345
  }
@@ -182,18 +353,52 @@ export function register(server, ctx) {
182
353
  server.registerTool("set_workflow_policy", {
183
354
  title: "◆ phren · set workflow policy",
184
355
  description: "Update workflow policy: low-confidence threshold, " +
185
- "risky sections list, task mode, and finding sensitivity.",
356
+ "risky sections list, task mode, and finding sensitivity. " +
357
+ "When project is provided, writes to that project's phren.project.yaml.",
186
358
  inputSchema: z.object({
187
359
  lowConfidenceThreshold: z.number().min(0).max(1).optional()
188
360
  .describe("Confidence below which items are flagged as low-confidence."),
189
361
  riskySections: z.array(z.enum(["Review", "Stale", "Conflicts"])).optional()
190
362
  .describe("Which queue sections are considered risky."),
191
- taskMode: z.enum(TASK_MODES).optional()
363
+ taskMode: z.enum(VALID_TASK_MODES).optional()
192
364
  .describe("Task automation mode."),
193
- findingSensitivity: z.enum(FINDING_SENSITIVITY_LEVELS).optional()
365
+ findingSensitivity: z.enum(VALID_FINDING_SENSITIVITY).optional()
194
366
  .describe("Finding capture sensitivity."),
367
+ project: projectParam,
195
368
  }),
196
- }, async ({ lowConfidenceThreshold, riskySections, taskMode, findingSensitivity }) => {
369
+ }, async ({ lowConfidenceThreshold, riskySections, taskMode, findingSensitivity, project }) => {
370
+ if (project) {
371
+ const err = validateProject(project);
372
+ if (err)
373
+ return mcpResponse({ ok: false, error: err });
374
+ const warning = checkProjectRegistered(phrenPath, project);
375
+ const next = updateProjectConfigOverrides(phrenPath, project, (current) => {
376
+ const nextConfig = { ...current };
377
+ const shouldUpdateWorkflowPolicy = (lowConfidenceThreshold !== undefined
378
+ || riskySections !== undefined
379
+ || current.workflowPolicy !== undefined);
380
+ if (shouldUpdateWorkflowPolicy) {
381
+ const existingWorkflow = current.workflowPolicy ?? {};
382
+ nextConfig.workflowPolicy = {
383
+ ...existingWorkflow,
384
+ ...(lowConfidenceThreshold !== undefined ? { lowConfidenceThreshold } : {}),
385
+ ...(riskySections !== undefined ? { riskySections } : {}),
386
+ };
387
+ }
388
+ if (taskMode !== undefined)
389
+ nextConfig.taskMode = taskMode;
390
+ if (findingSensitivity !== undefined)
391
+ nextConfig.findingSensitivity = findingSensitivity;
392
+ return nextConfig;
393
+ });
394
+ return mcpResponse({
395
+ ok: true,
396
+ message: warning
397
+ ? `Workflow policy updated for project "${project}". WARNING: ${warning}`
398
+ : `Workflow policy updated for project "${project}".`,
399
+ data: { project, config: next.config ?? {}, ...(warning ? { warning } : {}) },
400
+ });
401
+ }
197
402
  const patch = {};
198
403
  if (lowConfidenceThreshold !== undefined)
199
404
  patch.lowConfidenceThreshold = lowConfidenceThreshold;
@@ -2,7 +2,7 @@ import { mcpResponse } from "./mcp-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 } from "./utils.js";
5
+ import { isValidProjectName, errorMessage } from "./utils.js";
6
6
  import { readFindings, readTasks, resolveTaskFilePath, TASKS_FILENAME } from "./data-access.js";
7
7
  import { debugLog, findProjectNameCaseInsensitive, normalizeProjectNameForCreate } from "./shared.js";
8
8
  const importPayloadSchema = z.object({
@@ -75,8 +75,8 @@ export function register(server, ctx) {
75
75
  decoded = JSON.parse(rawData);
76
76
  }
77
77
  catch (err) {
78
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
79
- process.stderr.write(`[phren] import_project jsonParse: ${err instanceof Error ? err.message : String(err)}\n`);
78
+ if ((process.env.PHREN_DEBUG))
79
+ process.stderr.write(`[phren] import_project jsonParse: ${errorMessage(err)}\n`);
80
80
  return mcpResponse({ ok: false, error: "Invalid JSON input." });
81
81
  }
82
82
  const parsedResult = importPayloadSchema.safeParse(decoded);
@@ -241,8 +241,8 @@ export function register(server, ctx) {
241
241
  }
242
242
  }
243
243
  catch (err) {
244
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
245
- process.stderr.write(`[phren] import_project backupRestore: ${err instanceof Error ? err.message : String(err)}\n`);
244
+ if ((process.env.PHREN_DEBUG))
245
+ process.stderr.write(`[phren] import_project backupRestore: ${errorMessage(err)}\n`);
246
246
  }
247
247
  }
248
248
  return mcpResponse({
@@ -261,8 +261,8 @@ export function register(server, ctx) {
261
261
  }
262
262
  }
263
263
  catch (err) {
264
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
265
- process.stderr.write(`[phren] import_project backupCleanup: ${err instanceof Error ? err.message : String(err)}\n`);
264
+ if ((process.env.PHREN_DEBUG))
265
+ process.stderr.write(`[phren] import_project backupCleanup: ${errorMessage(err)}\n`);
266
266
  }
267
267
  }
268
268
  return mcpResponse({
@@ -299,7 +299,7 @@ export function register(server, ctx) {
299
299
  }
300
300
  catch (err) {
301
301
  fs.renameSync(archiveDir, projectDir);
302
- return mcpResponse({ ok: false, error: `Index rebuild failed after archive rename, rolled back: ${err instanceof Error ? err.message : String(err)}` });
302
+ return mcpResponse({ ok: false, error: `Index rebuild failed after archive rename, rolled back: ${errorMessage(err)}` });
303
303
  }
304
304
  return mcpResponse({
305
305
  ok: true,
@@ -322,7 +322,7 @@ export function register(server, ctx) {
322
322
  }
323
323
  catch (err) {
324
324
  fs.renameSync(projectDir, archiveDir);
325
- return mcpResponse({ ok: false, error: `Index rebuild failed after unarchive rename, rolled back: ${err instanceof Error ? err.message : String(err)}` });
325
+ return mcpResponse({ ok: false, error: `Index rebuild failed after unarchive rename, rolled back: ${errorMessage(err)}` });
326
326
  }
327
327
  return mcpResponse({
328
328
  ok: true,
@@ -24,7 +24,7 @@ export function readExtractedFacts(phrenPath, project) {
24
24
  return Array.isArray(data) ? data : [];
25
25
  }
26
26
  catch (err) {
27
- if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
27
+ if ((process.env.PHREN_DEBUG))
28
28
  process.stderr.write(`[phren] readExtractedFacts: ${errorMessage(err)}\n`);
29
29
  return [];
30
30
  }
@@ -1,6 +1,6 @@
1
1
  import { mcpResponse } from "./mcp-types.js";
2
2
  import { z } from "zod";
3
- import { isValidProjectName, safeProjectPath } from "./utils.js";
3
+ import { isValidProjectName, safeProjectPath, errorMessage } from "./utils.js";
4
4
  import { addFindingsToFile } from "./shared-content.js";
5
5
  import { checkOllamaAvailable, checkModelAvailable, generateText, getOllamaUrl, getExtractModel } from "./shared-ollama.js";
6
6
  import { debugLog } from "./shared.js";
@@ -34,7 +34,7 @@ function parseFindings(raw) {
34
34
  }
35
35
  }
36
36
  catch (err) {
37
- debugLog(`auto_extract: failed to parse LLM output as JSON: ${cleaned.slice(0, 200)} (${err instanceof Error ? err.message : String(err)})`);
37
+ debugLog(`auto_extract: failed to parse LLM output as JSON: ${cleaned.slice(0, 200)} (${errorMessage(err)})`);
38
38
  }
39
39
  return [];
40
40
  }