@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
@@ -2,6 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { runtimeDir } from "./shared.js";
4
4
  import { errorMessage } from "./utils.js";
5
+ import { logger } from "./logger.js";
5
6
  // In-memory buffers keyed by phrenPath to batch disk writes
6
7
  // Keeping per-path buffers avoids silently losing events when the active path changes.
7
8
  const buffers = new Map();
@@ -26,8 +27,7 @@ function loadFromDisk(phrenPath) {
26
27
  };
27
28
  }
28
29
  catch (err) {
29
- if ((process.env.PHREN_DEBUG))
30
- process.stderr.write(`[phren] telemetry loadFromDisk: ${errorMessage(err)}\n`);
30
+ logger.debug("telemetry loadFromDisk", errorMessage(err));
31
31
  return defaults;
32
32
  }
33
33
  }
@@ -58,8 +58,7 @@ function flushTelemetryForPath(phrenPath) {
58
58
  fs.writeFileSync(file, JSON.stringify(data, null, 2) + "\n");
59
59
  }
60
60
  catch (err) {
61
- if ((process.env.PHREN_DEBUG))
62
- process.stderr.write(`[phren] telemetry flush: ${errorMessage(err)}\n`);
61
+ logger.debug("telemetry flush", errorMessage(err));
63
62
  }
64
63
  pendingCounts.set(phrenPath, 0);
65
64
  }
@@ -2,18 +2,18 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  const CATEGORY_BY_MODULE = {
5
- "mcp-search": "Search and browse",
6
- "mcp-tasks": "Task management",
7
- "mcp-finding": "Finding capture",
8
- "mcp-memory": "Memory quality",
9
- "mcp-data": "Data management",
10
- "mcp-graph": "Fragments and graph",
11
- "mcp-session": "Session management",
12
- "mcp-ops": "Operations and review",
13
- "mcp-skills": "Skills management",
14
- "mcp-hooks": "Hooks management",
15
- "mcp-config": "Configuration",
16
- "mcp-extract": "Extraction",
5
+ "search": "Search and browse",
6
+ "tasks": "Task management",
7
+ "finding": "Finding capture",
8
+ "memory": "Memory quality",
9
+ "data": "Data management",
10
+ "graph": "Fragments and graph",
11
+ "session": "Session management",
12
+ "ops": "Operations and review",
13
+ "skills": "Skills management",
14
+ "hooks": "Hooks management",
15
+ "config": "Configuration",
16
+ "extract": "Extraction",
17
17
  };
18
18
  const MODULE_ORDER = Object.keys(CATEGORY_BY_MODULE);
19
19
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -45,7 +45,7 @@ function parseModuleTools(moduleName, source) {
45
45
  });
46
46
  }
47
47
  // Handle loop-registered tools: { tool: "name", ... } patterns followed by registerTool(action.tool, ...)
48
- const loopPattern = /for\s*\(const\s+(\w+)\s+of\s+\[([\s\S]*?)\]\s*(?:as\s+const\s*)?\)\s*\{\s*server\.registerTool\(\s*\1\.tool\s*,\s*\{([\s\S]*?)\}\s*,/g;
48
+ const loopPattern = /for\s*\(const\s+(\w+)\s+of\s+\[([\s\S]*?)\]\s*(?:as\s+const\s*)?\)\s*\{[\s\S]*?server\.registerTool\(\s*\1\.tool\s*,\s*\{([\s\S]*?)\}\s*,/g;
49
49
  for (const loopMatch of source.matchAll(loopPattern)) {
50
50
  const [, , itemsBlock, configBlock] = loopMatch;
51
51
  const itemPattern = /\{\s*tool:\s*"([^"]+)"[^}]*verb:\s*"([^"]+)"/g;
@@ -66,11 +66,23 @@ function parseModuleTools(moduleName, source) {
66
66
  export function getRegisteredTools() {
67
67
  const dir = sourceDir();
68
68
  const entries = [];
69
+ const toolsDir = path.join(dir, "tools");
69
70
  for (const moduleName of MODULE_ORDER) {
70
- const tsPath = path.join(dir, `${moduleName}.ts`);
71
- const jsPath = path.join(dir, `${moduleName}.js`);
72
- const sourcePath = fs.existsSync(tsPath) ? tsPath : jsPath;
73
- if (!fs.existsSync(sourcePath))
71
+ // Tool files may live in the tools/ subdirectory or alongside tool-registry.ts
72
+ let sourcePath;
73
+ for (const base of [toolsDir, dir]) {
74
+ const tsPath = path.join(base, `${moduleName}.ts`);
75
+ const jsPath = path.join(base, `${moduleName}.js`);
76
+ if (fs.existsSync(tsPath)) {
77
+ sourcePath = tsPath;
78
+ break;
79
+ }
80
+ if (fs.existsSync(jsPath)) {
81
+ sourcePath = jsPath;
82
+ break;
83
+ }
84
+ }
85
+ if (!sourcePath)
74
86
  continue;
75
87
  entries.push(...parseModuleTools(moduleName, fs.readFileSync(sourcePath, "utf8")));
76
88
  }
@@ -0,0 +1,515 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { mcpResponse } from "./types.js";
4
+ import { z } from "zod";
5
+ import { getRetentionPolicy, updateRetentionPolicy, getWorkflowPolicy, updateWorkflowPolicy, getIndexPolicy, updateIndexPolicy, mergeConfig, VALID_TASK_MODES, VALID_FINDING_SENSITIVITY, } from "../shared/governance.js";
6
+ import { PROACTIVITY_LEVELS, } from "../proactivity.js";
7
+ import { writeGovernanceInstallPreferences, } from "../init/preferences.js";
8
+ import { FINDING_SENSITIVITY_CONFIG, buildProactivitySnapshot, checkProjectInProfile } from "../cli/config.js";
9
+ import { readProjectConfig, updateProjectConfigOverrides, } from "../project-config.js";
10
+ import { isValidProjectName, safeProjectPath } from "../utils.js";
11
+ import { readProjectTopics, writeProjectTopics, } from "../project-topics.js";
12
+ // ── Helpers ─────────────────────────────────────────────────────────────────
13
+ function proactivitySnapshot(phrenPath) {
14
+ const snap = buildProactivitySnapshot(phrenPath);
15
+ return { configured: snap.configured, effective: snap.effective };
16
+ }
17
+ function validateProject(project) {
18
+ if (!isValidProjectName(project))
19
+ return `Invalid project name: "${project}".`;
20
+ return null;
21
+ }
22
+ function checkProjectRegistered(phrenPath, project) {
23
+ const warning = checkProjectInProfile(phrenPath, project);
24
+ if (warning) {
25
+ 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.`;
26
+ }
27
+ return null;
28
+ }
29
+ function normalizeProjectOverrides(raw) {
30
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
31
+ }
32
+ function getProjectOverrides(phrenPath, project) {
33
+ return normalizeProjectOverrides(readProjectConfig(phrenPath, project).config);
34
+ }
35
+ function hasOwnOverride(overrides, key) {
36
+ return Object.prototype.hasOwnProperty.call(overrides, key);
37
+ }
38
+ const projectParam = z.string().optional().describe("Project name. When provided, writes to that project's phren.project.yaml instead of global .config/.");
39
+ // ── Topic helpers (shared by get_config topic domain and set_config topic domain) ──
40
+ function getTopicConfigData(phrenPath, project) {
41
+ const projectDir = safeProjectPath(phrenPath, project);
42
+ if (!projectDir || !fs.existsSync(projectDir)) {
43
+ return { ok: false, error: `Project "${project}" not found in phren.` };
44
+ }
45
+ const result = readProjectTopics(phrenPath, project);
46
+ const configPath = path.join(projectDir, "topic-config.json");
47
+ const raw = fs.existsSync(configPath)
48
+ ? (() => { try {
49
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
50
+ }
51
+ catch {
52
+ return null;
53
+ } })()
54
+ : null;
55
+ return {
56
+ ok: true,
57
+ data: {
58
+ project,
59
+ source: result.source,
60
+ domain: result.domain ?? raw?.domain ?? null,
61
+ topics: result.topics,
62
+ pinnedTopics: raw?.pinnedTopics ?? [],
63
+ },
64
+ };
65
+ }
66
+ // ── Registration ────────────────────────────────────────────────────────────
67
+ export function register(server, ctx) {
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
+ });
101
+ }
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
+ });
170
+ }
171
+ const result = {};
172
+ if (d === "all" || d === "proactivity") {
173
+ result.proactivity = proactivitySnapshot(phrenPath);
174
+ }
175
+ 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 };
184
+ }
185
+ if (d === "all" || d === "retention") {
186
+ result.retention = getRetentionPolicy(phrenPath);
187
+ }
188
+ if (d === "all" || d === "workflow") {
189
+ result.workflow = getWorkflowPolicy(phrenPath);
190
+ }
191
+ if (d === "all" || d === "index") {
192
+ result.index = getIndexPolicy(phrenPath);
193
+ }
194
+ return mcpResponse({
195
+ ok: true,
196
+ message: `Config for ${d === "all" ? "all domains" : d}.`,
197
+ data: result,
198
+ });
199
+ });
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);
256
+ return mcpResponse({
257
+ ok: true,
258
+ message: `Proactivity ${s} set to ${level}.`,
259
+ data: proactivitySnapshot(phrenPath),
260
+ });
261
+ }
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
+ }
290
+ return mcpResponse({
291
+ ok: true,
292
+ message: `Task mode set to ${validMode}.`,
293
+ data: { taskMode: validMode },
294
+ });
295
+ }
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
+ }
325
+ const config = FINDING_SENSITIVITY_CONFIG[validLevel];
326
+ return mcpResponse({
327
+ ok: true,
328
+ message: `Finding sensitivity set to ${validLevel}.`,
329
+ data: { level: validLevel, ...config },
330
+ });
331
+ }
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
+ });
383
+ }
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,
436
+ });
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
+ return mcpResponse({
453
+ ok: true,
454
+ message: "Index policy updated.",
455
+ data: result.data,
456
+ });
457
+ }
458
+ // ── topic ─────────────────────────────────────────────────────
459
+ case "topic": {
460
+ if (!project) {
461
+ return mcpResponse({ ok: false, error: "The 'topic' domain requires a project parameter." });
462
+ }
463
+ const err = validateProject(project);
464
+ if (err)
465
+ 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.` });
469
+ }
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." });
473
+ }
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
494
+ }
495
+ }
496
+ else {
497
+ fs.mkdirSync(projectDir, { recursive: true });
498
+ fs.writeFileSync(configPath, JSON.stringify({ version: 1, domain: topicDomain, topics: [] }, null, 2) + "\n");
499
+ }
500
+ }
501
+ const result = writeProjectTopics(phrenPath, project, normalized);
502
+ if (!result.ok) {
503
+ return mcpResponse({ ok: false, error: result.error });
504
+ }
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
+ }
511
+ default:
512
+ return mcpResponse({ ok: false, error: `Unknown config domain: ${domain}` });
513
+ }
514
+ });
515
+ }
@@ -1,10 +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, errorMessage, safeProjectPath } from "./utils.js";
6
- import { readFindings, readTasks, resolveTaskFilePath, TASKS_FILENAME } from "./data-access.js";
7
- import { debugLog, findArchivedProjectNameCaseInsensitive, findProjectNameCaseInsensitive, normalizeProjectNameForCreate } from "./shared.js";
5
+ import { isValidProjectName, errorMessage, safeProjectPath } from "../utils.js";
6
+ import { readFindings, readTasks, resolveTaskFilePath, TASKS_FILENAME } from "../data/access.js";
7
+ import { debugLog, findArchivedProjectNameCaseInsensitive, findProjectNameCaseInsensitive, normalizeProjectNameForCreate } from "../shared.js";
8
+ import { logger } from "../logger.js";
8
9
  const importPayloadSchema = z.object({
9
10
  project: z.string(),
10
11
  overwrite: z.boolean().optional(),
@@ -77,8 +78,7 @@ export function register(server, ctx) {
77
78
  decoded = JSON.parse(rawData);
78
79
  }
79
80
  catch (err) {
80
- if ((process.env.PHREN_DEBUG))
81
- process.stderr.write(`[phren] import_project jsonParse: ${errorMessage(err)}\n`);
81
+ logger.debug("data", `import_project jsonParse: ${errorMessage(err)}`);
82
82
  return mcpResponse({ ok: false, error: "Invalid JSON input." });
83
83
  }
84
84
  const parsedResult = importPayloadSchema.safeParse(decoded);
@@ -237,8 +237,7 @@ export function register(server, ctx) {
237
237
  }
238
238
  }
239
239
  catch (err) {
240
- if ((process.env.PHREN_DEBUG))
241
- process.stderr.write(`[phren] import_project backupRestore: ${errorMessage(err)}\n`);
240
+ logger.debug("data", `import_project backupRestore: ${errorMessage(err)}`);
242
241
  }
243
242
  }
244
243
  return mcpResponse({
@@ -253,8 +252,7 @@ export function register(server, ctx) {
253
252
  fs.rmSync(backupDir, { recursive: true, force: true });
254
253
  }
255
254
  catch (err) {
256
- if ((process.env.PHREN_DEBUG))
257
- process.stderr.write(`[phren] import_project backupCleanup: ${errorMessage(err)}\n`);
255
+ logger.debug("data", `import_project backupCleanup: ${errorMessage(err)}`);
258
256
  }
259
257
  }
260
258
  return mcpResponse({