@phren/cli 0.0.1

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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +590 -0
  3. package/mcp/dist/capabilities/cli.js +61 -0
  4. package/mcp/dist/capabilities/index.js +15 -0
  5. package/mcp/dist/capabilities/mcp.js +61 -0
  6. package/mcp/dist/capabilities/types.js +57 -0
  7. package/mcp/dist/capabilities/vscode.js +61 -0
  8. package/mcp/dist/capabilities/web-ui.js +61 -0
  9. package/mcp/dist/cli-actions.js +302 -0
  10. package/mcp/dist/cli-config.js +580 -0
  11. package/mcp/dist/cli-extract.js +305 -0
  12. package/mcp/dist/cli-govern.js +371 -0
  13. package/mcp/dist/cli-graph.js +169 -0
  14. package/mcp/dist/cli-hooks-citations.js +44 -0
  15. package/mcp/dist/cli-hooks-context.js +56 -0
  16. package/mcp/dist/cli-hooks-globs.js +83 -0
  17. package/mcp/dist/cli-hooks-output.js +130 -0
  18. package/mcp/dist/cli-hooks-retrieval.js +2 -0
  19. package/mcp/dist/cli-hooks-session.js +1402 -0
  20. package/mcp/dist/cli-hooks.js +350 -0
  21. package/mcp/dist/cli-namespaces.js +989 -0
  22. package/mcp/dist/cli-ops.js +253 -0
  23. package/mcp/dist/cli-search.js +407 -0
  24. package/mcp/dist/cli.js +108 -0
  25. package/mcp/dist/content-archive.js +278 -0
  26. package/mcp/dist/content-citation.js +391 -0
  27. package/mcp/dist/content-dedup.js +622 -0
  28. package/mcp/dist/content-learning.js +472 -0
  29. package/mcp/dist/content-metadata.js +186 -0
  30. package/mcp/dist/content-validate.js +462 -0
  31. package/mcp/dist/core-finding.js +54 -0
  32. package/mcp/dist/core-project.js +36 -0
  33. package/mcp/dist/core-search.js +50 -0
  34. package/mcp/dist/data-access.js +400 -0
  35. package/mcp/dist/data-tasks.js +821 -0
  36. package/mcp/dist/embedding.js +344 -0
  37. package/mcp/dist/entrypoint.js +387 -0
  38. package/mcp/dist/finding-context.js +172 -0
  39. package/mcp/dist/finding-impact.js +181 -0
  40. package/mcp/dist/finding-journal.js +122 -0
  41. package/mcp/dist/finding-lifecycle.js +259 -0
  42. package/mcp/dist/governance-audit.js +22 -0
  43. package/mcp/dist/governance-locks.js +96 -0
  44. package/mcp/dist/governance-policy.js +648 -0
  45. package/mcp/dist/governance-scores.js +355 -0
  46. package/mcp/dist/hooks.js +449 -0
  47. package/mcp/dist/impact-scoring.js +22 -0
  48. package/mcp/dist/index-query.js +168 -0
  49. package/mcp/dist/index.js +205 -0
  50. package/mcp/dist/init-config.js +336 -0
  51. package/mcp/dist/init-preferences.js +62 -0
  52. package/mcp/dist/init-setup.js +1305 -0
  53. package/mcp/dist/init-shared.js +29 -0
  54. package/mcp/dist/init.js +1730 -0
  55. package/mcp/dist/link-checksums.js +62 -0
  56. package/mcp/dist/link-context.js +257 -0
  57. package/mcp/dist/link-doctor.js +591 -0
  58. package/mcp/dist/link-skills.js +212 -0
  59. package/mcp/dist/link.js +596 -0
  60. package/mcp/dist/logger.js +15 -0
  61. package/mcp/dist/machine-identity.js +38 -0
  62. package/mcp/dist/mcp-config.js +254 -0
  63. package/mcp/dist/mcp-data.js +315 -0
  64. package/mcp/dist/mcp-extract-facts.js +78 -0
  65. package/mcp/dist/mcp-extract.js +133 -0
  66. package/mcp/dist/mcp-finding.js +557 -0
  67. package/mcp/dist/mcp-graph.js +339 -0
  68. package/mcp/dist/mcp-hooks.js +256 -0
  69. package/mcp/dist/mcp-memory.js +58 -0
  70. package/mcp/dist/mcp-ops.js +328 -0
  71. package/mcp/dist/mcp-search.js +628 -0
  72. package/mcp/dist/mcp-session.js +651 -0
  73. package/mcp/dist/mcp-skills.js +189 -0
  74. package/mcp/dist/mcp-tasks.js +551 -0
  75. package/mcp/dist/mcp-types.js +7 -0
  76. package/mcp/dist/memory-ui-assets.js +6 -0
  77. package/mcp/dist/memory-ui-data.js +513 -0
  78. package/mcp/dist/memory-ui-graph.js +1910 -0
  79. package/mcp/dist/memory-ui-page.js +353 -0
  80. package/mcp/dist/memory-ui-scripts.js +1387 -0
  81. package/mcp/dist/memory-ui-server.js +1218 -0
  82. package/mcp/dist/memory-ui-styles.js +555 -0
  83. package/mcp/dist/memory-ui.js +9 -0
  84. package/mcp/dist/package-metadata.js +13 -0
  85. package/mcp/dist/phren-art.js +52 -0
  86. package/mcp/dist/phren-core.js +108 -0
  87. package/mcp/dist/phren-dotenv.js +67 -0
  88. package/mcp/dist/phren-paths.js +476 -0
  89. package/mcp/dist/proactivity.js +172 -0
  90. package/mcp/dist/profile-store.js +228 -0
  91. package/mcp/dist/project-config.js +85 -0
  92. package/mcp/dist/project-locator.js +25 -0
  93. package/mcp/dist/project-topics.js +1134 -0
  94. package/mcp/dist/provider-adapters.js +176 -0
  95. package/mcp/dist/runtime-profile.js +18 -0
  96. package/mcp/dist/session-checkpoints.js +131 -0
  97. package/mcp/dist/session-utils.js +68 -0
  98. package/mcp/dist/shared-content.js +8 -0
  99. package/mcp/dist/shared-embedding-cache.js +143 -0
  100. package/mcp/dist/shared-fragment-graph.js +456 -0
  101. package/mcp/dist/shared-governance.js +4 -0
  102. package/mcp/dist/shared-index.js +1334 -0
  103. package/mcp/dist/shared-ollama.js +192 -0
  104. package/mcp/dist/shared-paths.js +1 -0
  105. package/mcp/dist/shared-retrieval.js +796 -0
  106. package/mcp/dist/shared-search-fallback.js +375 -0
  107. package/mcp/dist/shared-sqljs.js +42 -0
  108. package/mcp/dist/shared-stemmer.js +171 -0
  109. package/mcp/dist/shared-vector-index.js +199 -0
  110. package/mcp/dist/shared.js +114 -0
  111. package/mcp/dist/shell-entry.js +209 -0
  112. package/mcp/dist/shell-input.js +943 -0
  113. package/mcp/dist/shell-palette.js +119 -0
  114. package/mcp/dist/shell-render.js +252 -0
  115. package/mcp/dist/shell-state-store.js +81 -0
  116. package/mcp/dist/shell-types.js +13 -0
  117. package/mcp/dist/shell-view-list.js +14 -0
  118. package/mcp/dist/shell-view.js +707 -0
  119. package/mcp/dist/shell.js +352 -0
  120. package/mcp/dist/skill-files.js +117 -0
  121. package/mcp/dist/skill-registry.js +279 -0
  122. package/mcp/dist/skill-state.js +28 -0
  123. package/mcp/dist/startup-embedding.js +57 -0
  124. package/mcp/dist/status.js +323 -0
  125. package/mcp/dist/synonyms.json +670 -0
  126. package/mcp/dist/task-hygiene.js +251 -0
  127. package/mcp/dist/task-lifecycle.js +347 -0
  128. package/mcp/dist/tasks-github.js +76 -0
  129. package/mcp/dist/telemetry.js +165 -0
  130. package/mcp/dist/test-global-setup.js +37 -0
  131. package/mcp/dist/tool-registry.js +104 -0
  132. package/mcp/dist/update.js +97 -0
  133. package/mcp/dist/utils.js +543 -0
  134. package/package.json +67 -0
  135. package/skills/README.md +7 -0
  136. package/skills/consolidate/SKILL.md +152 -0
  137. package/skills/discover/SKILL.md +175 -0
  138. package/skills/init/SKILL.md +216 -0
  139. package/skills/profiles/SKILL.md +121 -0
  140. package/skills/sync/SKILL.md +261 -0
  141. package/starter/README.md +74 -0
  142. package/starter/global/CLAUDE.md +89 -0
  143. package/starter/global/skills/humanize.md +30 -0
  144. package/starter/global/skills/pipeline.md +35 -0
  145. package/starter/global/skills/release.md +35 -0
  146. package/starter/machines.yaml +8 -0
  147. package/starter/my-api/.claude/skills/README.md +7 -0
  148. package/starter/my-api/CLAUDE.md +33 -0
  149. package/starter/my-api/FINDINGS.md +9 -0
  150. package/starter/my-api/summary.md +7 -0
  151. package/starter/my-api/tasks.md +7 -0
  152. package/starter/my-first-project/.claude/skills/README.md +7 -0
  153. package/starter/my-first-project/CLAUDE.md +49 -0
  154. package/starter/my-first-project/FINDINGS.md +24 -0
  155. package/starter/my-first-project/summary.md +11 -0
  156. package/starter/my-first-project/tasks.md +25 -0
  157. package/starter/my-frontend/.claude/skills/README.md +7 -0
  158. package/starter/my-frontend/CLAUDE.md +33 -0
  159. package/starter/my-frontend/FINDINGS.md +9 -0
  160. package/starter/my-frontend/summary.md +7 -0
  161. package/starter/my-frontend/tasks.md +7 -0
  162. package/starter/profiles/default.yaml +4 -0
  163. package/starter/profiles/personal.yaml +4 -0
  164. package/starter/profiles/work.yaml +4 -0
  165. package/starter/templates/README.md +7 -0
  166. package/starter/templates/frontend/CLAUDE.md +23 -0
  167. package/starter/templates/frontend/FINDINGS.md +7 -0
  168. package/starter/templates/frontend/reference/README.md +4 -0
  169. package/starter/templates/frontend/summary.md +7 -0
  170. package/starter/templates/frontend/tasks.md +11 -0
  171. package/starter/templates/library/CLAUDE.md +22 -0
  172. package/starter/templates/library/FINDINGS.md +7 -0
  173. package/starter/templates/library/reference/README.md +4 -0
  174. package/starter/templates/library/summary.md +7 -0
  175. package/starter/templates/library/tasks.md +11 -0
  176. package/starter/templates/monorepo/CLAUDE.md +21 -0
  177. package/starter/templates/monorepo/FINDINGS.md +7 -0
  178. package/starter/templates/monorepo/reference/README.md +4 -0
  179. package/starter/templates/monorepo/summary.md +7 -0
  180. package/starter/templates/monorepo/tasks.md +11 -0
  181. package/starter/templates/python-project/CLAUDE.md +21 -0
  182. package/starter/templates/python-project/FINDINGS.md +7 -0
  183. package/starter/templates/python-project/reference/README.md +4 -0
  184. package/starter/templates/python-project/summary.md +7 -0
  185. package/starter/templates/python-project/tasks.md +10 -0
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import { findPhrenPathWithArg, debugLog, runtimeDir, } from "./shared.js";
7
+ import { log as structuredLog } from "./logger.js";
8
+ import { buildIndex, updateFileInIndex as updateFileInIndexFn, } from "./shared-index.js";
9
+ import { runCustomHooks } from "./hooks.js";
10
+ import { register as registerSearch } from "./mcp-search.js";
11
+ import { register as registerTask } from "./mcp-tasks.js";
12
+ import { register as registerFinding } from "./mcp-finding.js";
13
+ import { register as registerMemory } from "./mcp-memory.js";
14
+ import { register as registerData } from "./mcp-data.js";
15
+ import { register as registerGraph } from "./mcp-graph.js";
16
+ import { register as registerSession } from "./mcp-session.js";
17
+ import { register as registerOps } from "./mcp-ops.js";
18
+ import { register as registerSkills } from "./mcp-skills.js";
19
+ import { register as registerHooks } from "./mcp-hooks.js";
20
+ import { register as registerExtract } from "./mcp-extract.js";
21
+ import { register as registerConfig } from "./mcp-config.js";
22
+ import { errorMessage } from "./utils.js";
23
+ import { runTopLevelCommand } from "./entrypoint.js";
24
+ import { startEmbeddingWarmup } from "./startup-embedding.js";
25
+ import { resolveRuntimeProfile } from "./runtime-profile.js";
26
+ import { VERSION as PACKAGE_VERSION } from "./package-metadata.js";
27
+ const handledTopLevelCommand = await runTopLevelCommand(process.argv.slice(2));
28
+ // MCP mode: first non-flag arg is the phren path. Resolve it lazily so CLI commands
29
+ // like `maintain` are not misinterpreted as a filesystem path after the command has run.
30
+ const phrenArg = handledTopLevelCommand ? undefined : process.argv.find((a, i) => i >= 2 && !a.startsWith("-"));
31
+ const phrenPath = handledTopLevelCommand ? "" : findPhrenPathWithArg(phrenArg);
32
+ const STALE_LOCK_MS = 120_000; // 2 min — slightly above EXEC_TIMEOUT_MS (30s) to avoid blocking healthy writers
33
+ function cleanStaleLocks(phrenPath) {
34
+ const dir = runtimeDir(phrenPath);
35
+ try {
36
+ if (!fs.existsSync(dir))
37
+ return;
38
+ for (const entry of fs.readdirSync(dir)) {
39
+ if (!entry.endsWith(".lock"))
40
+ continue;
41
+ const lockPath = path.join(dir, entry);
42
+ try {
43
+ const stat = fs.statSync(lockPath);
44
+ if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
45
+ fs.unlinkSync(lockPath);
46
+ debugLog(`Cleaned stale lock: ${entry}`);
47
+ }
48
+ }
49
+ catch (err) {
50
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
51
+ process.stderr.write(`[phren] cleanStaleLocks statFile: ${errorMessage(err)}\n`);
52
+ }
53
+ }
54
+ }
55
+ catch (err) {
56
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
57
+ process.stderr.write(`[phren] cleanStaleLocks readdir: ${errorMessage(err)}\n`);
58
+ }
59
+ }
60
+ async function main() {
61
+ const profile = resolveRuntimeProfile(phrenPath);
62
+ cleanStaleLocks(phrenPath);
63
+ let db = null;
64
+ let indexReady = false;
65
+ try {
66
+ db = await buildIndex(phrenPath, profile);
67
+ indexReady = true;
68
+ // Load embedding cache and kick off background embedding (fire-and-forget)
69
+ const { getEmbeddingCache } = await import("./shared-embedding-cache.js");
70
+ const embCache = getEmbeddingCache(phrenPath);
71
+ void startEmbeddingWarmup(db, embCache);
72
+ }
73
+ catch (error) {
74
+ const msg = error instanceof Error ? error.message : String(error);
75
+ structuredLog("error", "startup", `Failed to build phren index: ${msg}`);
76
+ console.error("Failed to build phren index at startup:", error);
77
+ process.exit(1);
78
+ }
79
+ let writeQueue = Promise.resolve();
80
+ let writeQueueDepth = 0;
81
+ const MAX_QUEUE_DEPTH = 50;
82
+ const WRITE_TIMEOUT_MS = 30_000;
83
+ async function rebuildIndex() {
84
+ runCustomHooks(phrenPath, "pre-index");
85
+ indexReady = false;
86
+ try {
87
+ db?.close();
88
+ }
89
+ catch (err) {
90
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
91
+ process.stderr.write(`[phren] rebuildIndex dbClose: ${errorMessage(err)}\n`);
92
+ }
93
+ db = await buildIndex(phrenPath, profile);
94
+ indexReady = true;
95
+ runCustomHooks(phrenPath, "post-index");
96
+ }
97
+ async function withWriteQueue(fn) {
98
+ if (writeQueueDepth >= MAX_QUEUE_DEPTH) {
99
+ throw new Error(`Write queue full (${MAX_QUEUE_DEPTH} items). Try again shortly.`);
100
+ }
101
+ writeQueueDepth++;
102
+ const run = writeQueue.then(async () => {
103
+ try {
104
+ return await Promise.race([
105
+ fn(),
106
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Write timeout after 30s")), WRITE_TIMEOUT_MS))
107
+ ]);
108
+ }
109
+ catch (err) {
110
+ const message = errorMessage(err);
111
+ if (message.includes("Write timeout") || message.includes("Write queue full")) {
112
+ debugLog(`Write queue timeout: ${message}`);
113
+ return { ok: false, error: `Write queue timeout: ${message}`, errorCode: "TIMEOUT" };
114
+ }
115
+ throw err;
116
+ }
117
+ finally {
118
+ writeQueueDepth = Math.max(0, writeQueueDepth - 1);
119
+ }
120
+ });
121
+ writeQueue = run.then(() => undefined).catch((error) => {
122
+ try {
123
+ const message = error instanceof Error
124
+ ? error.stack || error.message
125
+ : String(error);
126
+ debugLog(`Write queue error: ${message}`);
127
+ }
128
+ catch (logError) {
129
+ const message = logError instanceof Error ? logError.message : String(logError);
130
+ structuredLog("error", "write-queue", `Failed to log write queue error: ${message}`);
131
+ }
132
+ });
133
+ return run;
134
+ }
135
+ const server = new McpServer({
136
+ name: "phren-mcp",
137
+ version: PACKAGE_VERSION,
138
+ });
139
+ // Track MCP tool calls for telemetry (opt-in only, best-effort)
140
+ const { trackToolCall } = await import("./telemetry.js");
141
+ const origRegisterTool = server.registerTool.bind(server);
142
+ server.registerTool = function (name, config, handler) {
143
+ const registeredName = name;
144
+ const wrapped = async (...args) => {
145
+ if (!indexReady || !db) {
146
+ return {
147
+ content: [{
148
+ type: "text",
149
+ text: JSON.stringify({
150
+ ok: false,
151
+ error: "Index unavailable - check phren setup",
152
+ }, null, 2),
153
+ }],
154
+ };
155
+ }
156
+ try {
157
+ trackToolCall(phrenPath, registeredName);
158
+ }
159
+ catch (err) {
160
+ if ((process.env.PHREN_DEBUG || process.env.PHREN_DEBUG))
161
+ process.stderr.write(`[phren] trackToolCall: ${errorMessage(err)}\n`);
162
+ }
163
+ return handler(...args);
164
+ };
165
+ return origRegisterTool(registeredName, config, wrapped);
166
+ };
167
+ // Register all tool handlers from domain modules
168
+ const ctx = {
169
+ phrenPath,
170
+ profile,
171
+ db: () => {
172
+ if (!db)
173
+ throw new Error("Index unavailable - check phren setup");
174
+ return db;
175
+ },
176
+ rebuildIndex,
177
+ withWriteQueue,
178
+ updateFileInIndex: (filePath) => {
179
+ if (!db)
180
+ throw new Error("Index unavailable - check phren setup");
181
+ updateFileInIndexFn(db, filePath, phrenPath);
182
+ },
183
+ };
184
+ registerSearch(server, ctx);
185
+ registerTask(server, ctx);
186
+ registerFinding(server, ctx);
187
+ registerMemory(server, ctx);
188
+ registerData(server, ctx);
189
+ registerGraph(server, ctx);
190
+ registerSession(server, ctx);
191
+ registerOps(server, ctx);
192
+ registerSkills(server, ctx);
193
+ registerHooks(server, ctx);
194
+ registerExtract(server, ctx);
195
+ registerConfig(server, ctx);
196
+ const transport = new StdioServerTransport();
197
+ await server.connect(transport);
198
+ console.error(`phren-mcp running (${phrenPath})`);
199
+ }
200
+ if (!handledTopLevelCommand) {
201
+ main().catch((err) => {
202
+ console.error("Failed to start phren-mcp:", err);
203
+ process.exit(1);
204
+ });
205
+ }
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Provider-specific MCP configuration backends.
3
+ * Handles IDE/tool config files for Claude, VS Code, Cursor, Copilot CLI, and Codex.
4
+ */
5
+ import * as fs from "fs";
6
+ import * as path from "path";
7
+ import { randomUUID } from "crypto";
8
+ import { execFileSync } from "child_process";
9
+ import { buildLifecycleCommands } from "./hooks.js";
10
+ import { EXEC_TIMEOUT_QUICK_MS, isRecord, hookConfigPath, homePath, readRootManifest, } from "./shared.js";
11
+ import { isFeatureEnabled, errorMessage } from "./utils.js";
12
+ import { probeVsCodeConfig, resolveCodexMcpConfig, resolveCopilotMcpConfig, resolveCursorMcpConfig, } from "./provider-adapters.js";
13
+ import { getMcpEnabledPreference, getHooksEnabledPreference } from "./init-preferences.js";
14
+ import { resolveEntryScript, VERSION } from "./init-shared.js";
15
+ function log(msg) {
16
+ process.stdout.write(msg + "\n");
17
+ }
18
+ function getObjectProp(value, key) {
19
+ const candidate = value[key];
20
+ return isRecord(candidate) ? candidate : undefined;
21
+ }
22
+ function atomicWriteText(filePath, content) {
23
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
24
+ const tmpPath = `${filePath}.tmp-${randomUUID()}`;
25
+ fs.writeFileSync(tmpPath, content);
26
+ fs.renameSync(tmpPath, filePath);
27
+ }
28
+ export function patchJsonFile(filePath, patch) {
29
+ let data = {};
30
+ if (fs.existsSync(filePath)) {
31
+ try {
32
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
33
+ if (!isRecord(parsed))
34
+ throw new Error("top-level JSON value must be an object");
35
+ data = parsed;
36
+ }
37
+ catch (err) {
38
+ throw new Error(`Malformed JSON in ${filePath}: ${errorMessage(err)}`);
39
+ }
40
+ }
41
+ else {
42
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
+ }
44
+ patch(data);
45
+ atomicWriteText(filePath, JSON.stringify(data, null, 2) + "\n");
46
+ }
47
+ function commandExists(cmd) {
48
+ try {
49
+ const whichCmd = process.platform === "win32" ? "where.exe" : "which";
50
+ execFileSync(whichCmd, [cmd], { stdio: ["ignore", "ignore", "ignore"], timeout: EXEC_TIMEOUT_QUICK_MS });
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
57
+ function buildMcpServerConfig(phrenPath) {
58
+ const entryScript = resolveEntryScript();
59
+ if (entryScript && fs.existsSync(entryScript)) {
60
+ return {
61
+ command: "node",
62
+ args: [entryScript, phrenPath],
63
+ };
64
+ }
65
+ return {
66
+ command: "npx",
67
+ args: ["-y", `phren@${VERSION}`, phrenPath],
68
+ };
69
+ }
70
+ function upsertMcpServer(data, mcpEnabled, preferredRoot, phrenPath) {
71
+ const knownRoots = ["mcpServers", "servers"];
72
+ const hadMcp = knownRoots.some((key) => Boolean(getObjectProp(data, key)?.phren));
73
+ if (mcpEnabled) {
74
+ let preferredRootValue = getObjectProp(data, preferredRoot);
75
+ if (!preferredRootValue) {
76
+ preferredRootValue = {};
77
+ data[preferredRoot] = preferredRootValue;
78
+ }
79
+ preferredRootValue.phren = buildMcpServerConfig(phrenPath);
80
+ return hadMcp ? "already_configured" : "installed";
81
+ }
82
+ for (const key of knownRoots) {
83
+ const root = getObjectProp(data, key);
84
+ if (root?.phren)
85
+ delete root.phren;
86
+ }
87
+ return hadMcp ? "disabled" : "already_disabled";
88
+ }
89
+ function configureMcpAtPath(filePath, mcpEnabled, preferredRoot, phrenPath) {
90
+ if (!mcpEnabled && !fs.existsSync(filePath))
91
+ return "already_disabled";
92
+ let status = "already_disabled";
93
+ patchJsonFile(filePath, (data) => {
94
+ status = upsertMcpServer(data, mcpEnabled, preferredRoot, phrenPath);
95
+ });
96
+ return status;
97
+ }
98
+ /**
99
+ * Read/write a TOML config file to upsert or remove [mcp_servers.phren].
100
+ * Lightweight: preserves all other content, only touches the phren section.
101
+ */
102
+ function patchTomlMcpServer(filePath, mcpEnabled, phrenPath) {
103
+ let content = "";
104
+ const existed = fs.existsSync(filePath);
105
+ if (existed) {
106
+ content = fs.readFileSync(filePath, "utf8");
107
+ }
108
+ else if (!mcpEnabled) {
109
+ return "already_disabled";
110
+ }
111
+ const cfg = buildMcpServerConfig(phrenPath);
112
+ const escToml = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
113
+ const argsToml = "[" + cfg.args.map((a) => `"${escToml(a)}"`).join(", ") + "]";
114
+ const newSection = `[mcp_servers.phren]\ncommand = "${escToml(cfg.command)}"\nargs = ${argsToml}\nstartup_timeout_sec = 30`;
115
+ const sectionRe = /^\[mcp_servers\.phren\]\s*\n(?:(?!\[)[^\n]*\n?)*/m;
116
+ const hadSection = sectionRe.test(content);
117
+ if (mcpEnabled) {
118
+ if (hadSection) {
119
+ content = content.replace(sectionRe, newSection + "\n");
120
+ atomicWriteText(filePath, content);
121
+ return "already_configured";
122
+ }
123
+ if (!existed) {
124
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
125
+ }
126
+ const sep = content.length > 0 && !content.endsWith("\n\n") ? (content.endsWith("\n") ? "\n" : "\n\n") : "";
127
+ content += sep + newSection + "\n";
128
+ atomicWriteText(filePath, content);
129
+ return "installed";
130
+ }
131
+ if (!hadSection)
132
+ return "already_disabled";
133
+ content = content.replace(sectionRe, "");
134
+ content = content.replace(/\n{3,}/g, "\n\n");
135
+ atomicWriteText(filePath, content);
136
+ return "disabled";
137
+ }
138
+ export function removeTomlMcpServer(filePath) {
139
+ if (!fs.existsSync(filePath))
140
+ return false;
141
+ let content = fs.readFileSync(filePath, "utf8");
142
+ const sectionRe = /^\[mcp_servers\.phren\]\s*\n(?:(?!\[)[^\n]*\n?)*/m;
143
+ if (!sectionRe.test(content))
144
+ return false;
145
+ content = content.replace(sectionRe, "").replace(/\n{3,}/g, "\n\n");
146
+ atomicWriteText(filePath, content);
147
+ return true;
148
+ }
149
+ export function removeMcpServerAtPath(filePath) {
150
+ if (!fs.existsSync(filePath))
151
+ return false;
152
+ let removed = false;
153
+ patchJsonFile(filePath, (data) => {
154
+ for (const key of ["mcpServers", "servers"]) {
155
+ const root = data[key];
156
+ if (isRecord(root) && root.phren) {
157
+ delete root.phren;
158
+ removed = true;
159
+ }
160
+ }
161
+ });
162
+ return removed;
163
+ }
164
+ export function isPhrenCommand(command) {
165
+ // Detect PHREN_PATH= or legacy PHREN_PATH= env var prefix (present in all lifecycle hook commands)
166
+ if (/\b(?:PHREN_PATH|PHREN_PATH)=/.test(command))
167
+ return true;
168
+ // Detect npx phren/phren package references
169
+ if (command.includes("phren") || command.includes("phren"))
170
+ return true;
171
+ // Detect bare "phren" or "phren" executable segment
172
+ const segments = command.split(/[/\\\s]+/);
173
+ if (segments.some(seg => seg === "phren" || seg.startsWith("phren@") || seg === "phren" || seg.startsWith("phren@")))
174
+ return true;
175
+ // Also match commands that include hook subcommands (used when installed via absolute path)
176
+ const HOOK_MARKERS = ["hook-prompt", "hook-stop", "hook-session-start", "hook-tool"];
177
+ if (HOOK_MARKERS.some(m => command.includes(m)))
178
+ return true;
179
+ return false;
180
+ }
181
+ export function configureClaude(phrenPath, opts = {}) {
182
+ const settingsPath = hookConfigPath("claude");
183
+ const claudeJsonPath = homePath(".claude.json");
184
+ const entryScript = resolveEntryScript();
185
+ const mcpEnabled = opts.mcpEnabled ?? getMcpEnabledPreference(phrenPath);
186
+ const hooksEnabled = opts.hooksEnabled ?? getHooksEnabledPreference(phrenPath);
187
+ const lifecycle = buildLifecycleCommands(phrenPath);
188
+ let status = "already_disabled";
189
+ if (fs.existsSync(claudeJsonPath)) {
190
+ patchJsonFile(claudeJsonPath, (data) => {
191
+ status = upsertMcpServer(data, mcpEnabled, "mcpServers", phrenPath);
192
+ });
193
+ }
194
+ patchJsonFile(settingsPath, (data) => {
195
+ const settingsStatus = upsertMcpServer(data, mcpEnabled, "mcpServers", phrenPath);
196
+ if (status === "already_disabled")
197
+ status = settingsStatus;
198
+ const hooksMap = isRecord(data.hooks) ? data.hooks : (data.hooks = {});
199
+ const upsertPhrenHook = (eventName, hookBody) => {
200
+ if (!Array.isArray(hooksMap[eventName]))
201
+ hooksMap[eventName] = [];
202
+ const eventHooks = hooksMap[eventName];
203
+ const marker = eventName === "UserPromptSubmit" ? "hook-prompt"
204
+ : eventName === "Stop" ? "hook-stop"
205
+ : eventName === "PostToolUse" ? "hook-tool"
206
+ : "hook-session-start";
207
+ // Find the HookEntry containing a phren hook command
208
+ const existingEntryIdx = eventHooks.findIndex((h) => h?.hooks?.some((hook) => typeof hook?.command === "string" &&
209
+ (hook.command.includes(marker) ||
210
+ isPhrenCommand(hook.command))));
211
+ if (existingEntryIdx >= 0) {
212
+ // Only rewrite the matching inner hook item; preserve sibling non-phren hooks
213
+ const entry = eventHooks[existingEntryIdx];
214
+ const innerIdx = (entry.hooks ?? []).findIndex((hook) => typeof hook?.command === "string" &&
215
+ (hook.command.includes(marker) ||
216
+ isPhrenCommand(hook.command)));
217
+ if (innerIdx >= 0 && entry.hooks) {
218
+ entry.hooks[innerIdx] = hookBody;
219
+ }
220
+ else {
221
+ // No matching inner hook found; append our hook body
222
+ if (!entry.hooks)
223
+ entry.hooks = [];
224
+ entry.hooks.push(hookBody);
225
+ }
226
+ }
227
+ else {
228
+ eventHooks.push({ matcher: "", hooks: [hookBody] });
229
+ }
230
+ };
231
+ const toolHookEnabled = hooksEnabled && (isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK", false) || isFeatureEnabled("PHREN_FEATURE_TOOL_HOOK", false));
232
+ if (hooksEnabled) {
233
+ upsertPhrenHook("UserPromptSubmit", {
234
+ type: "command",
235
+ command: lifecycle.userPromptSubmit || `node "${entryScript}" hook-prompt`,
236
+ timeout: 3,
237
+ });
238
+ upsertPhrenHook("Stop", {
239
+ type: "command",
240
+ command: lifecycle.stop,
241
+ });
242
+ upsertPhrenHook("SessionStart", {
243
+ type: "command",
244
+ command: lifecycle.sessionStart,
245
+ });
246
+ if (toolHookEnabled) {
247
+ upsertPhrenHook("PostToolUse", {
248
+ type: "command",
249
+ command: lifecycle.hookTool,
250
+ });
251
+ }
252
+ }
253
+ else {
254
+ for (const hookEvent of ["UserPromptSubmit", "Stop", "SessionStart", "PostToolUse"]) {
255
+ const hooks = hooksMap[hookEvent];
256
+ if (!Array.isArray(hooks))
257
+ continue;
258
+ hooksMap[hookEvent] = hooks.filter((h) => !h.hooks?.some((hook) => typeof hook.command === "string" && isPhrenCommand(hook.command)));
259
+ }
260
+ }
261
+ });
262
+ return status;
263
+ }
264
+ let _vscodeProbeCache = null;
265
+ /** Reset the VS Code path probe cache (for testing). */
266
+ export function resetVSCodeProbeCache() { _vscodeProbeCache = null; }
267
+ function probeVSCodePath() {
268
+ if (_vscodeProbeCache)
269
+ return _vscodeProbeCache;
270
+ _vscodeProbeCache = probeVsCodeConfig(commandExists);
271
+ return _vscodeProbeCache;
272
+ }
273
+ export function configureVSCode(phrenPath, opts = {}) {
274
+ const mcpEnabled = opts.mcpEnabled ?? getMcpEnabledPreference(phrenPath);
275
+ if (opts.scope === "workspace") {
276
+ const manifest = readRootManifest(phrenPath);
277
+ if (manifest?.installMode !== "project-local" || !manifest.workspaceRoot)
278
+ return "no_vscode";
279
+ const mcpFile = path.join(manifest.workspaceRoot, ".vscode", "mcp.json");
280
+ return configureMcpAtPath(mcpFile, mcpEnabled, "servers", "${workspaceFolder}/.phren");
281
+ }
282
+ const probe = probeVSCodePath();
283
+ if (!probe.installed || !probe.targetDir)
284
+ return "no_vscode";
285
+ const mcpFile = path.join(probe.targetDir, "mcp.json");
286
+ return configureMcpAtPath(mcpFile, mcpEnabled, "servers", phrenPath);
287
+ }
288
+ export function configureCursorMcp(phrenPath, opts = {}) {
289
+ const mcpEnabled = opts.mcpEnabled ?? getMcpEnabledPreference(phrenPath);
290
+ const resolved = resolveCursorMcpConfig(commandExists);
291
+ if (!resolved.installed)
292
+ return "no_cursor";
293
+ return configureMcpAtPath(resolved.target, mcpEnabled, "mcpServers", phrenPath);
294
+ }
295
+ export function configureCopilotMcp(phrenPath, opts = {}) {
296
+ const mcpEnabled = opts.mcpEnabled ?? getMcpEnabledPreference(phrenPath);
297
+ const resolved = resolveCopilotMcpConfig(commandExists);
298
+ if (!resolved.installed)
299
+ return "no_copilot";
300
+ let status = "already_disabled";
301
+ if (resolved.hasCliDir) {
302
+ status = configureMcpAtPath(resolved.cliConfig, mcpEnabled, "mcpServers", phrenPath);
303
+ }
304
+ if (resolved.existing && resolved.existing !== resolved.cliConfig) {
305
+ status = configureMcpAtPath(resolved.existing, mcpEnabled, "mcpServers", phrenPath);
306
+ }
307
+ if (!resolved.hasCliDir && !resolved.existing) {
308
+ status = configureMcpAtPath(resolved.cliConfig, mcpEnabled, "mcpServers", phrenPath);
309
+ }
310
+ return status;
311
+ }
312
+ export function configureCodexMcp(phrenPath, opts = {}) {
313
+ const mcpEnabled = opts.mcpEnabled ?? getMcpEnabledPreference(phrenPath);
314
+ const resolved = resolveCodexMcpConfig(phrenPath, commandExists);
315
+ if (!resolved.installed)
316
+ return "no_codex";
317
+ if (resolved.preferToml) {
318
+ return patchTomlMcpServer(resolved.tomlPath, mcpEnabled, phrenPath);
319
+ }
320
+ return configureMcpAtPath(resolved.existingJson, mcpEnabled, "mcpServers", phrenPath);
321
+ }
322
+ export function logMcpTargetStatus(tool, status, phase = "Configured") {
323
+ const text = {
324
+ installed: `${phase} ${tool} MCP`,
325
+ already_configured: `${tool} MCP already configured`,
326
+ disabled: `${tool} MCP disabled`,
327
+ already_disabled: `${tool} MCP already disabled`,
328
+ no_settings: `${tool} settings not found`,
329
+ no_vscode: `${tool} not detected`,
330
+ no_cursor: `${tool} not detected`,
331
+ no_copilot: `${tool} not detected`,
332
+ no_codex: `${tool} not detected`,
333
+ };
334
+ if (text[status])
335
+ log(` ${text[status]}`);
336
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Install preferences: MCP/hooks mode, version tracking.
3
+ */
4
+ import * as fs from "fs";
5
+ import * as path from "path";
6
+ import * as crypto from "crypto";
7
+ import { debugLog, installPreferencesFile } from "./phren-paths.js";
8
+ import { errorMessage } from "./utils.js";
9
+ function preferencesFile(phrenPath) {
10
+ return installPreferencesFile(phrenPath);
11
+ }
12
+ export function governanceInstallPreferencesFile(phrenPath) {
13
+ return path.join(phrenPath, ".governance", "install-preferences.json");
14
+ }
15
+ function readPreferencesFile(file) {
16
+ if (!fs.existsSync(file))
17
+ return {};
18
+ try {
19
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
20
+ return parsed && typeof parsed === "object" ? parsed : {};
21
+ }
22
+ catch (err) {
23
+ debugLog(`readInstallPreferences: failed to parse ${file}: ${errorMessage(err)}`);
24
+ return {};
25
+ }
26
+ }
27
+ function writePreferencesFile(file, current, patch) {
28
+ fs.mkdirSync(path.dirname(file), { recursive: true });
29
+ const tmpPath = `${file}.tmp-${crypto.randomUUID()}`;
30
+ fs.writeFileSync(tmpPath, JSON.stringify({
31
+ ...current,
32
+ ...patch,
33
+ updatedAt: new Date().toISOString(),
34
+ }, null, 2) + "\n");
35
+ fs.renameSync(tmpPath, file);
36
+ }
37
+ export function readInstallPreferences(phrenPath) {
38
+ return readPreferencesFile(preferencesFile(phrenPath));
39
+ }
40
+ export function readGovernanceInstallPreferences(phrenPath) {
41
+ return readPreferencesFile(governanceInstallPreferencesFile(phrenPath));
42
+ }
43
+ export function writeInstallPreferences(phrenPath, patch) {
44
+ writePreferencesFile(preferencesFile(phrenPath), readInstallPreferences(phrenPath), patch);
45
+ }
46
+ export function writeGovernanceInstallPreferences(phrenPath, patch) {
47
+ writePreferencesFile(governanceInstallPreferencesFile(phrenPath), readGovernanceInstallPreferences(phrenPath), patch);
48
+ }
49
+ export function getMcpEnabledPreference(phrenPath) {
50
+ const prefs = readInstallPreferences(phrenPath);
51
+ return prefs.mcpEnabled !== false;
52
+ }
53
+ export function setMcpEnabledPreference(phrenPath, enabled) {
54
+ writeInstallPreferences(phrenPath, { mcpEnabled: enabled });
55
+ }
56
+ export function getHooksEnabledPreference(phrenPath) {
57
+ const prefs = readInstallPreferences(phrenPath);
58
+ return prefs.hooksEnabled !== false;
59
+ }
60
+ export function setHooksEnabledPreference(phrenPath, enabled) {
61
+ writeInstallPreferences(phrenPath, { hooksEnabled: enabled });
62
+ }